/octox

Unix-like OS in Rust inspired by xv6-riscv

Primary LanguageRustApache License 2.0Apache-2.0

octox

octox is a Unix-like operating system inspired by xv6-riscv. octox loosely follows the structure and style of xv6, but is implemented in pure Rust.

https://vhs.charm.sh/vhs-6MQBIyAo3DpBrARBxHxL35.gif

  • Everything from kernel, userland, mkfs, to build system is written in safe Rust as much as possible.
  • There are no dependencies on external crates.
  • The userland has a library similar to Rust’s std with K&R malloc.
  • Multi-core support, buddy allocator as kernel-side memory allocator, file system with logging support, etc.

Getting Started

Requirements

  • Install the rust toolchain to have cargo installed by following this guide.
  • Install qemu-system-riscv
  • (option) Install gdb-multiarch

Build and Run

  • Clone this project & enter: git clone ... && cd octox
  • Build: cargo build --target riscv64gc-unknown-none-elf.
  • Run: cargo run --target riscv64gc-unknown-none-elf, then qemu will boot octox. To exit, press Ctrl+a and x.

Play with the Shell

A very simple shell is implemented. In addition to executing commands, you can only do the following things.

  • Pipe: cat file | head | grep test
  • Dump processes: Ctrl + P
  • End of line: Ctrl + D
  • Redirect output: >, >>

Development

Userland Application

The userland comes with a user library called ulib (located at src/user/lib) that is similar to Rust’s std, so you can use it to develop your favorite commands. If you create a bin crate named _command in src/user/bin, the build.rs and mkfs.rs will place a file named command in the file system and make it available for use.

  • In src/user/Cargo.toml, define a bin crate with the name of the command you want to create with a _ prefix
    [[bin]]
    name = "_rm"
    path = "bin/rm.rs"
        
  • userland is also no_std, so don’t forget to add #[no_std]. Use ulib to develop any command you like. Here is an example of the rm command.
    #![no_std]
    use ulib::{env, fs};
    
    fn main() {
        let mut args = env::args().skip(1).peekable();
    
        if args.peek().is_none() {
            panic!("Usage: rm files...")
        }
        for arg in args {
            fs::remove_file(arg).unwrap()
        }
    }
        
  • Then, cargo run --target riscv64gc-unknown-none-elf in the root of octox.
  • To use Vec and String, etc, do the following:
    extern crate alloc;
    use alloc::{string::String, vec::Vec};
        

Kernel

Developing in src/kernel. Here is an example of adding a system call. If you want to add a new system call, you only need to add a definition to the system call table in libkernel, and the userland library will be automatically generated by build.rs.

  • Add a variant and Syscall Number to enum SysCalls in src/kernel/syscall.rs. Here is Dup2 as an example:
    pub enum SysCalls {
        Fork = 1,
        ...,
        Dup2 = 23,
        Invalid = 0,
    }
        
  • Define the function signature of the system call in the TABLE of SysCalls. Use the enum type Fn to describe the return type(U (Unit), I (Integer), N (never)) and use &str to represent arguments. then, define kernel-side implementation as a method on SysCalls. cfg flag is used to control the compilation target for kernel and the rest. Here is an example of dup2:
    impl SysCalls {
        pub const TABLE: [(fn, &'static str); variant_count::<Self>()] = [
            (Fn::N(Self::Invalid), ""),
            (Fn::I(Self::fork), "()"),
            (Fn::N(Self::exit), "(xstatus: i32)"),
            ...,
            (Fn::I(Self::dup2), "(src: usize, dst: usize)"),
        ];
        pub fn dup2() -> Result<usize> {
            #[cfg(not(all(target_os = "none", feature = "kernel")))]
            return Ok(0);
            #[cfg(all(target_os = "none", feature = "kernel"))]
            {
                let p = Cpus::myproc().unwrap().data_mut();
                let src_fd = argraw(0); let dst_fd = argraw(1);
                if src_fd != dst_fd {
                    let mut src = p.ofile.get_mut(src_fd).unwrap()
                        .take().unwrap();
                    src.clear_cloexec();
                    p.ofile.get_mut(dst_fd)
                        .ok_or(FileDescriptorTooLarge)?.replace(src);
                }
                Ok(dst_fd)
            }
        }
        
  • With just these steps, the dup2 system call is implemented in both kernel and userland.

License

Licensed under either of

at your option.

Acknowledgments

octox is inspired by xv6-riscv.

I’m also grateful for the bug reports and discussion about the implementation contributed by Takahiro Itazuri and Kuniyuki Iwashima.

Contribution

This is a learning project for me, and I will not be accepting pull requests until I consider the implementation complete. However, discussions and advice are welcome.