Skip to content

Latest commit

 

History

History
557 lines (498 loc) · 18.9 KB

13-return-to-sender.org

File metadata and controls

557 lines (498 loc) · 18.9 KB

Return to sender

The traditional next step might be to implement a mass storage driver to allow the OS to read and write to disk. Instead we’re going to work on bringing the network device up in the next section.

Starting a driver

In the next section we’re going to work on a driver for a network card, the RTL8139. We’ll start this here by writing the code to request information on PCI devices from the pci process.

Create a new crate:

cargo new rtl8139

then modify Cargo.toml (in the root and rtl8139 directories), the root makefile, add build.rs and a basic rtl8139/src/main.rs file as done for pci in the last section. Compiling everything (make run) should produce a user/rtl8139 executable.

Finding PCI in a Virtual File System

We want this rtl8139 driver to be able to send messages to the pci program. One way to do this would be for the kernel to add a rendezvous handle, but this would be a custom solution and is a good excuse to start developing something more general: A Virtual File System (VFS).

A virtual file system organises resources into a hierarchical structure, with nested directories containing ‘files’. These may correspond to real directories and files on disk, or they may represent devices or other information, for example the Proc filesystem provides access to information on running processes, with a ‘directory’ for each process.

Operating systems vary in how virtual file systems are presented to user programs. In Unix-like operating systems such as Linux almost all programs see the same file system, but not always: the chroot command and FreeBSD jails provide ways to run programs which see a different view of the file system. Plan 9 took this further and allows every program to have a different view of the filesystem.

In EuraliOS the kernel “Merrywig” is going to be quite lazy, and let user programs do most of the work of creating the VFS. The kernel is just going to keep a list of “mount points”, and use that to direct program requests to list directories or open files. We’ll keep the option of either sharing VFS between processes or creating a separate one for each process, by giving each Process a Arc<RwLock<>> to contain a potentially shared Vec of pairs of String (mount path) and Arc<RwLock<Rendezvous>> objects. In kernel/src/process.rs:

struct Process {
    page_table_physaddr: u64,
    handles: Vec<Option<Arc<RwLock<Rendezvous>>>>,
    mounts: Arc<RwLock<Vec<(String, Arc<RwLock<Rendezvous>>)>>> // New
}

Kernel threads (probably?) don’t need mounts, so in new_kernel_thread() we just need to add:

mounts: Arc::new(RwLock::new(Vec::new()))

User programs need a new parameter:

pub struct Params {
    pub handles: Vec<Arc<RwLock<Rendezvous>>>,
    pub io_privileges: bool,
    pub mounts: Arc<RwLock<Vec<(String, Arc<RwLock<Rendezvous>>)>>> // New
}

which is moved into the Process:

process: Arc::new(RwLock::new(Process {
    ...
    mounts: params.mounts, // New
})),

In kernel/src/main.rs the kernel_thread_main() function can now be modified to launch both pci and rtl8139 processes. We need to set up the inputs so that (for now) both the keyboard and the mount point are connected to the same Rendezvous (pci_input):

let pci_input = interrupts::keyboard_rendezvous();
let vga_rz = vga_buffer::start_listener();

process::new_user_thread(
    include_bytes!("../../user/pci"),
    process::Params{
        handles: Vec::from([
            pci_input.clone(),
            vga_rz.clone()
        ]),
        io_privileges: true,
        mounts: Arc::new(RwLock::new(Vec::new()))
    });

process::new_user_thread(
    include_bytes!("../../user/rtl8139"),
    process::Params{
        handles: Vec::from([
            // New input (not shared with anything else)
            Arc::new(RwLock::new(Rendezvous::Empty)),
            // VGA output
            vga_rz
        ]),
        io_privileges: true,
        mounts: Arc::new(RwLock::new(Vec::from([
            // A VFS path to the PCI input
            (String::from("/pci"), pci_input)
        ])))
    });

Open system call

The sys_open syscall will (for now) just send a string, as write_str() in euralios_std/src/debug.rs does. In euralios_std/src/syscalls.rs we can add an open function which is very similar to write_str() but puts syscall number 6 in rax, and gets an error code and handle as return arguments:

/// Returns a handle on success, or an error code
pub fn open(path: &str) -> Result<u32, u64> {
    let error: u64;
    let handle: u32;
    unsafe {
        asm!("mov rax, 6", // syscall function
             "syscall",
             in("rdi") path.as_ptr(), // First argument
             in("rsi") path.len(), // Second argument
             out("rax") error,
             lateout("rdi") handle,
             out("rcx") _,
             out("r11") _);
    }
    if error == 0 {
        Ok(handle)
    } else {
        Err(error)
    }
}

This new syscall will be handled in kernel/src/syscalls.rs. The dispatch_syscall() function needs to match syscall 6:

6 => sys_open(context_ptr, arg1 as *const u8, arg2 as usize),

The sys_open() function starts almost the same as sys_write() except we’ve added some error code returns in rax:

const SYSCALL_ERROR_PARAM: usize = 5; // Invalid parameter
const SYSCALL_ERROR_UTF8: usize = 6; // UTF8 conversion error

fn sys_open(
    context_ptr: *mut Context,
    ptr: *const u8,
    len: usize) {

    let context = unsafe {&mut (*context_ptr)};

    // Check input length
    if len == 0 {
        context.rax = SYSCALL_ERROR_PARAM;
        return;
    }
    // Convert raw pointer to a slice
    let u8_slice = unsafe {slice::from_raw_parts(ptr, len)};

    if let Ok(path_string) = str::from_utf8(u8_slice) {
        // Open path
    } else {
        // Bad utf8 conversion
        context.rax = SYSCALL_ERROR_UTF8;
    }
}

To open the path and get the handle we’re going to call a function in the process module, open_path(). That function will either return a handle, or an error code:

match process::open_path(context, &path_string) {
    Ok(handle) => {
        context.rax = 0; // No error
        context.rdi = handle; // Return handle
    }
    Err(error_code) => {
        context.rax = error_code;
    }
}

In kernel/src/process.rs the open_path() function will do the work of resolving paths to resources. We need to get references to the thread and process structs:

pub fn open_path(
    current_context: &mut Context,
    path: &str) -> Result<usize, usize> {

    if let Some(current_thread) = CURRENT_THREAD.read().as_ref() {
        println!("Thread {} opening {}", current_thread.tid, path);

        let mut process = current_thread.process.write();

        // Lookup path, add to handles
    }
    Err(0)
}

Then lookup the Rendezvous corresponding to the path. In future this will be more involved but for now it will just do a simple text match, and we’ll make it more useful in future sections.

let option_rv = if let Some((_mount, rv)) =
    process.mounts.read().iter().find(
        |&(mount, _rv)| mount == path) {
        Some(rv.clone())
    } else {
        None
    };

And if a Rendezvous was found, add it to the Vec of handles:

if let Some(rv) = option_rv {
    let handle = process.add_handle(rv.clone());
    return Ok(handle);
} else {
    return Err(syscalls::SYSCALL_ERROR_NOTFOUND);
}

Where we’ve added a method Process::add_handle:

impl Process {
    fn add_handle(&mut self, rv: Arc<RwLock<Rendezvous>>) -> usize {
        // Find if there is an empty handles slot
        if let Some(index) = self.handles.iter().position(
            |handle| handle.is_none()) {
            self.handles[index] = Some(rv);
            return index;
        }
        // No free slot -> Add one
        self.handles.push(Some(rv));
        self.handles.len() - 1
    }
}

To try this out, in rtl8139/src/main.rs we can try opening the “/pci” path to get a handle, then sending a character message:

#[no_mangle]
fn main() {
    debug_println!("rtl8139");

    let handle = syscalls::open("/pci").expect("Couldn't open");
    debug_println!("{}", handle);

    syscalls::send(handle,
                   syscalls::Message::Short(
                       0, 'X' as u64, 0));
}

This produces the result in figure fig-open, showing character ‘88’ is received (ASCII ‘X’).

./img/13-01-open.png

Rendezvous blocking

If several threads (A, B and C) are sharing a Rendezvous, such as the /pci mount point, we could have the following situation:

  • Thread A receives, waits for a message;
  • Thread B sends, passes to thread A as intended
  • Thread C is scheduled and sends a message intended for thread A
  • Thread B now runs, calls receive and gets the message sent by thread C rather than the reply from thread A.

To prevent this we need to block the Rendezvous, so that only the recipient can send the reply. First we’ll modify Rendezvous in kernel/src/rendezvous.rs and make three changes:

  1. Modify the Receiving state so that it can be restricted to only receiving from one thread.
  2. Add a new state, SendReceiving which represents a thread sending a message and expecting a reply back from the same thread that receives the message.
  3. Add a send_receive method which puts a Rendezvous into the SendReceive state.

The Rendezvous enum type becomes:

pub enum Rendezvous {
    Empty,
    Sending(Option<Box<Thread>>, Message),
    Receiving(Box<Thread>, Option<u64>), // Added optional thread ID
    SendReceiving(Box<Thread>, Message), // New
 }

so now the Receiving state can optionally block all messages except those from a specified thread. The SendReceiving state differs from Sending because it needs to have a thread to return a message to, so Box<Thread> is not optional.

The functions send, receive, and send_receive (which we’re going to add soon) handle transitions between states. In send() the Receiving case needs to now include a check of the thread ID:

Rendezvous::Receiving(_, some_tid) => {
    if let Some(tid) = some_tid {
        // Restricted to a single thread
        if let Some(t) = &thread {
            if t.tid() != *tid {
                // Wrong thread ID
                t.return_error(syscalls::SYSCALL_ERROR_RECV_BLOCKING);
                return (thread, None);
            }
        } else {
            // No sender thread => error
            return (thread, None);
        }
    }
    ...

We also need to handle the case that send() is called on a Rendezvous in the SendReceiving state. This just signals an error because there can’t be two threads sending:

Rendezvous::SendReceiving(_, _) => {
    if let Some(t) = &thread {
        t.return_error(syscalls::SYSCALL_ERROR_SEND_BLOCKING);
    }
    (thread, None)
}

In the receive() method the Empty case is slightly modified, just adding a None argument to indicate that messages from any thread can be received:

Rendezvous::Empty => {
    *self = Rendezvous::Receiving(thread, None); // Added 'None'
    (None, None)
}

The SendReceiving case is more interesting: We are receiving a message from a thread which expects a reply from the receiving thread. We therefore move the sending Thread object from the SendReceiving state into a Receiving state, along with the ID of the receiving thread:

Rendezvous::SendReceiving(_, _) => {
    // Sending, expecting a reply from the same thread
    if let Rendezvous::SendReceiving(snd_thread, message) = mem::replace(self, Rendezvous::Empty) {
        thread.return_message(message);
        // Wait for a reply from the receiving thread
        *self = Rendezvous::Receiving(snd_thread, Some(thread.tid()));
        return (Some(thread), None);
    }
    (None, None)
}

The send_receive() function can now be defined as:

pub fn send_receive(&mut self, thread: Box<Thread>, message: Message)
                    -> (Option<Box<Thread>>, Option<Box<Thread>>) {
    match &*self {
        Rendezvous::Empty => {
            *self = Rendezvous::SendReceiving(thread, message);
            (None, None)
        }
        Rendezvous::Sending(_, _) => {
            // Signal error to thread: Can't have two sending threads
            thread.return_error(syscalls::SYSCALL_ERROR_SEND_BLOCKING);
            (Some(thread), None)
        }
        Rendezvous::Receiving(_, some_tid) => {
            if let Some(tid) = some_tid {
                // Restricted to a single thread
                if thread.tid() != *tid {
                    // Wrong thread ID
                    thread.return_error(syscalls::SYSCALL_ERROR_RECV_BLOCKING);
                    return (Some(thread), None);
                }
            }

            // Complete the message transfer
            if let Rendezvous::Receiving(rec_thread, _) = mem::replace(self, Rendezvous::Empty) {
                rec_thread.return_message(message);

                // Calling thread waits for a reply
                *self = Rendezvous::Receiving(thread, Some(rec_thread.tid()));

                return (Some(rec_thread), None);
            }
            (None, None) // This should never be reached
        }
        Rendezvous::SendReceiving(_, _) => {
            // Signal error to thread: Can't have two sending threads
            thread.return_error(syscalls::SYSCALL_ERROR_SEND_BLOCKING);
            (Some(thread), None)
        }
    }
}

Send-Receive system call

In kernel/src/syscalls.rs the Send-Receive system call can be implemented by much of the same code as sys_send(). We can handle both by passing both in dispatch_syscall():

4 => sys_send(context_ptr, syscall_id, arg1, arg2, arg3), // sys_send
5 => sys_send(context_ptr, syscall_id, arg1, arg2, arg3), // sys_sendreceive

then in sys_send we can again check the value of syscall_id:

let (thread1, thread2) = match syscall_id & 0xFF {
    5 => rdv.write().send(
        Some(thread),
        message),
    6 => rdv.write().send_receive(
        thread,
        message),
    _ => panic!("Internal error")
};

Standard library implementation

In the standard library euralios_std/src/syscalls.rs we can simplify the send, receive and send_receive functions by defining methods to convert Message objects to and from register values. For now these still only handle Short messages. In euralios_std/src/message.rs:

impl Message {
    pub fn to_values(&self)
                 -> Result<(u64, u64, u64, u64), u64> {
        match self {
            Message::Short(data1, data2, data3) => {
                Ok((0, *data1, *data2, *data3))
            },
            _ => Err(0)
        }
    }
    pub fn from_values(_ctrl: u64,
                   data1: u64, data2: u64, data3: u64)
                   -> Message {
        Message::Short(data1, data2, data3)
    }
}

The new send_receive() function can then be implemented as:

pub fn send_receive(
    handle: u32,
    message: Message
) -> Result<Message, u64> {

    // Convert the message to register values
    let (ctrl, data1, data2, data3) = message.to_values()?;

    // Values to be received
    let err: u64;
    let (ret_ctrl, ret_data1, ret_data2, ret_data3): (u64, u64, u64, u64);
    unsafe {
        asm!("syscall",
             in("rax") 5 | ctrl | ((handle as u64) << 32),
             in("rdi") data1,
             in("rsi") data2,
             in("rdx") data3,
             lateout("rax") ret_ctrl,
             lateout("rdi") ret_data1,
             lateout("rsi") ret_data2,
             lateout("rdx") ret_data3,
             out("rcx") _,
             out("r11") _);
    }
    let err = ret_ctrl & 0xFF;
    if err == 0 {
        return Ok(Message::from_values(ret_ctrl,
                                       ret_data1, ret_data2, ret_data3));
    }
    Err(err)
}

Using Send-Receive

In the rtl8139 program we can now try using send_receive() to find a device using the pci program. First we open the “/pci” path, then send a message requesting a device with vendor 0x10EC and device ID 0x8139 which is the RTL8139 network chip.

let handle = syscalls::open("/pci").expect("Couldn't open pci");

let reply = syscalls::send_receive(
    handle,
    syscalls::Message::Short(
        pci::FIND_DEVICE, 0x10EC, 0x8139)).unwrap();

The reply could be an address (ADDRESS, 257) if it was found, a not-found message (NOTFOUND, 258), or something else:

let address = match reply {
    syscalls::Message::Short(pci::ADDRESS,
                             address, _) => {
        debug_println!("rtl8139 found at address: {:08X}", address);
        address
    }
    syscalls::Message::Short(pci::NOTFOUND,
                             _, _) => {
        debug_println!("rtl8139 not found");
        return;
    }
    _ => {
        debug_println!("rtl8139 unexpected reply: {:?}", reply);
        return;
    }
};

Note that we should also handle the case that the Rendezvous is blocked, in which case send_receive() will return an Err and we should probably wait and try again.

Changing network card

The default network card on QEMU is not an RTL8139 so the rtl8139 driver won’t get a PCI address. To configure QEMU’s networking to emulate an RTL8139 network card we need to add args in kernel/Cargo.toml:

[package.metadata.bootimage]
run-args = ["-nic", "user,model=rtl8139"]  # New

Running this should produce something like the output in fig-address below:

./img/13-02-address.png

Now that we have the address of the RTL8139 device, we can start to develop the driver for it in the next section.