loading...

Programming for Redox OS

legolord208 profile image jD91mZM2 ・5 min read

If you don't know what Redox OS is, it's an entire operating system written in Rust. This is because Rust has some amazing guarantees about memory and thread safety. To make it even safer, it's a microkernel as opposed to a monolithic kernel. That means that for example drivers and similar run in userspace and cannot crash the entire computer in the same way.

To communicate between processes and the kernel, Redox OS has custom schemes. It follows the tradition of having everything be a URL. Schemes are dead simple and super awesome. You open a file with the path scheme:path. If no scheme is specified it defaults to the one of the current working directory - which is usually going to be file:.

TCP

An example of the scheme is the TCP protocol. Even though networking is supposed to be hard, in redox it's dead simple. You just open("tcp:address", O_RDWR).
Here's an example that curls example.com without any libraries what-so-ever (not an idiomatic use of the http protocol - prefer a library for doing http requests):

let mut file = File::open("tcp:93.184.216.34:80")?; // example.com
file.write(b"GET / HTTP/1.1\nHost: example.com\nConnection: close\n\n")?;

io::copy(&mut file, &mut io::stdout())?;

Program IPC

Another example of a scheme is one I wrote myself - the ipcd daemon. It's a simple scheme for inter-process communication. You call open("chan:name", O_CREAT) to create a server, and dup("listen") to accept a connection. The dup syscall is for duplicating files, but can optionally take a path which can be used from the scheme.
Here is a simple hello world server using ipcd - this time we need the syscall library for redox in order to duplicate the file.

let server = File::create("chan:hello")?;

loop {
    let stream = syscall::dup(server.as_raw_fd(), b"listen")?;
    let mut stream = unsafe { File::from_raw_fd(stream) };

    stream.write(b"Hello World!\n")?;
}

And to read from this ipc channel you just have to cat chan:hello.

Events

This scheme is somewhat special, not because it has any special hardcoded exception to it, but because it's used everywhere, even in other schemes.
The event scheme is kind of like epoll in linux. You write an event to an instance and it registers to listen for events from file (or instance of a scheme), and then you read and it blocks until the first event.
There is an excellent example in the event overhaul RFC.

This scheme works very well with the time: scheme, and can be used for setting a timeout to reading. The time scheme is simple, can read the current time and write a deadline which triggers an event when that time is elapsed.
Here's an example that timeouts a read to 2 seconds.

const TOKEN_TIME: usize = 0;
const TOKEN_FILE: usize = 1;

let mut selector = File::open("event:")?;
let mut some_file = /* anything you want to read from here, like a tcp stream */;

// Deadline
let mut timeout = File::open("time:")?;
let mut time = TimeSpec::default();
timeout.read(&mut time)?;
time.tv_sec += 2; // deadline to 2 seconds in the future
timeout.write(&time)?;

selector.write(Event {
    id: timeout.as_raw_fd(),
    flags: EVENT_READ,
    data: TOKEN_TIME
})?;
selector.write(Event {
    id: some_file,
    flags: EVENT_READ,
    data: TOKEN_FILE
})?;

let mut event = Event::default();
selector.read(&mut event)?;

match event.data {
    TOKEN_TIME => {
        // The timeout of 2 seconds was exceeded
    },
    TOKEN_FILE => {
        // The file can now be read!
        // How much can be read is undefined and depends on the scheme.
        // Most built-in schemes are nowadays edge triggered and you should
        // have the file be non-blocking and read over and over again
        // until you hit EAGAIN.
        // This matches the behavior of linux.
        let mut buf = [0; 16];
        loop {
            match some_file.read(&mut buf)? {
                Ok(0) => break, // EOF, we read everything!
                Ok(n) => {
                    // Handle the read here.
                    io::stdout().write(&buf[..n])?;
                },
                Err(ref e) if e.kind() == ErrorKind::WouldBlock => break, // can't read any more this round.
                Err(e) => return Err(e)
            }
        }
    }
}

How does it work?

Note: Writing a custom scheme requires root

Main Loop

The scheme is registered using the root scheme - which has no name. open(":test", O_CREAT) registers the scheme and returns a file you can read packets from.
How do you handle the packets? That's where the Scheme family of traits come in! They have a function called handle that is already implemented, that reads the packet and calls the correct trait function.
A simple loop may look like this:

let mut handler = Handler; // handler implements SchemeMut
let mut scheme = File::create(":hello")?;
loop {
    let mut packet = Packet::default();
    scheme.read(&mut packet)?;

    handler.handle(&mut packet);
    scheme.write(&packet)?;
}

Sadly most schemes' loops look more difficult because they might need to read from multiple schemes at the same time, or reschedule blocking functions.

Unique IDs

The scheme has functions like these:

fn open(&mut self, path: &[u8], flags: usize, _uid: u32, _gid: u32) -> Result<usize>;

where you return any integer you want!
A cool thing about Redox schemes is that each instance of the scheme (file descriptor) gets mapped to an ID, and multiple file descriptors can have the same ID (so you don't have to implement dups without a path yourself).
What almost all schemes do here is keep an integer that points to the next id, and then on open it adds the id to the an internal map together with some struct with data about the handle, and finally increments the integer.
Later functions are called with the id:

fn read(&mut self, id: usize, buf: &mut [u8]) -> Result<usize>

Blocking

A blocking function like read is usually rescheduled by the scheme when it fails. Usually they used to use the EWOULDBLOCK error internally to add the packet to a list of todos and then later (on either another scheme event or some timer) try to process that packet again. To ease with this, we now have a SchemeBlock trait that can return a None to indicate that it should be rescheduled.
For an example of a really simple blocking loop, check ipcd's main file.

Events

To make your schemes support events, you just need to write a packet to the scheme file with the values of a=SYS_FEVENT,b=id,c=flag,d=count (I don't think count is used anymore, you might just leave it at 1). There is also an fevent scheme function that will let you know when somebody has registered your scheme to an event instance. This can be used to reset variables to keep track of notified state in schemes that send edge triggered events.

Discussion

pic
Editor guide
Collapse
gulshan profile image
Gulshan

It's fascinating. Can redox os be run on docker? I could not find much with a quick search.

Collapse
legolord208 profile image
jD91mZM2 Author

It should, see gitlab.redox-os.org/redox-os/redox.... Last time I tried it I had to modify things slightly to set up git correctly - if this happens to you leave a bug report