From 79ade3039e791e5d26d9f9580078306008f21926 Mon Sep 17 00:00:00 2001 From: Sean Cross Date: Sun, 22 Dec 2019 00:19:46 +0800 Subject: [PATCH] initial commit Signed-off-by: Sean Cross --- .gitattributes | 18 ++++ .gitignore | 2 + README.md | 0 memory.md | 9 ++ processes.md | 223 +++++++++++++++++++++++++++++++++++++++ syscalls.md | 275 +++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 527 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 README.md create mode 100644 memory.md create mode 100644 processes.md create mode 100644 syscalls.md diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d1725ed --- /dev/null +++ b/.gitattributes @@ -0,0 +1,18 @@ +*.py text eol=lf +*.dfu binary +*.png binary +*.jpg binary +*.bin binary +*.elf binary +*.h text eol=lf +*.c text eol=lf +*.s text eol=lf +*.S text eol=lf +README.* text eol=lf +LICENSE text eol=lf +Makefile text eol=lf +*.mk text eol=lf +*.sh text eol=lf +*.ps1 text eol=crlf +.gitignore text eol=lf +.gitattributes text eol=lf \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53eaa21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +**/*.rs.bk diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/memory.md b/memory.md new file mode 100644 index 0000000..0d627cd --- /dev/null +++ b/memory.md @@ -0,0 +1,9 @@ +# Memory Management + +In general, memory cannot be mapped to more than one process. This includes RAM, storage, and io peripherals. + +A process can request specific memory ranges to be allocated. For example, a `uart_server` might request the UART memory region be allocated so that it can handle that device and provide a service. This region cannot be re-mapped to another process until it is freed. + +A process can request more memory for its heap. This will pull memory from the global pool and add it to that process' `heap_size`. Processes start out with a `heap_size` of 0, which does not include the contents of the `.text` or `.data` sections. + +If a process intends to spawn multiple threads, then it must malloc that memory prior to creating the thread. diff --git a/processes.md b/processes.md new file mode 100644 index 0000000..e1da802 --- /dev/null +++ b/processes.md @@ -0,0 +1,223 @@ +# Processes in Xous + +Xous supports multiple processes, each with their own address space. Xous has no concept of threads, priority, or indeed even a runqueue. It only knows how to pass messages, and that it should call the `idle process` when there are no other tasks to run. + +Multi-threading is optionally handled in userspace. + +This document covers the Xous kernel-native multi-processing implementation, along with several possible implementations of the idle process. + +## Kernel Processes + +Each process is a self-contained entity. It has its own memory space, and can only interact with the kernel by making syscalls. Processes are heavyweight. Each process takes up at least 8 kB of RAM -- 4 kB for the pagetable, and 4 kB for the initial memory area. + +### Process Data Structures + +```rust +/// PIDs are 8 bits to reduce the size of the MMU table. +/// This may be revisited at a later time. A `XousPid` of +/// 0 is invalid, and a `XousPid` of !0 (i.e. 0xff) is owned by the kernel +type XousPid = u8; + +/// Represents one process inside the Xous operating system. +/// The memory manager will have memory tables that use the pid to +/// keep track of where memory is allocated, however that is not +/// part of the process table. +struct XousProcess { + + /// Process ID + pid: XousPid, + + /// Parent Process ID + ppid: XousPid, + + /// Various process-specific flags + flags: XousFlags, + + /// MMU offset -- i.e. contents of the `satp` register + // for this process. + mmu_offset: usize, + + /// Last address of the stack pointer. + sp: usize, + + /// The number of bytes that have been allocated to this + /// process heap. This can be changed with `sbrk`. + heap_size: usize, +} +``` + +The kernel keeps track of what memory is owned by a particular process, and handles the actual context switch (i.e. remapping the MMU and saving/restoring registers when moving between processes). It also handles message routing. + +### Message Data Structures + +When a Xous process starts a server, the following structure is allocated in the kernel: + +```rust +struct XousServer { + /// The textual name of this server, used when calling `client_connect()` + name: String, + + /// The process ID of the controlling server + pid: XousPid, + + /// Stack pointer of the thread blocked by `server_receive()`. + /// If the process is not blocked, then this is `None`. + sp: Option, +} + +struct XousMessage { + /// The process ID of the process that sent this message + pid: XousPid, + + /// The stack pointer of the process that sent this message + sp: usize, +} +``` + +### Message Routing + +When a process wants to start a message server, it calls `server_register()` to register itself with a particular name. Then it can call `server_receive()` in order to actually start to process messages. To reply to a message, the server must call `server_reply()`. + +If a process wants to send a message to a server, it calls `message_send()`. If another process is waiting on a message (by blocking on `server_receive()`), then that process is activated immediately and it takes over the remaining quantum of the sending process. If there is no pending server, then the process yields its timeslice and the idle process is resumed. + +### Context Switches + +During a context switch away from a process, all of its registers are pushed to its stack, then the stack pointer is saved into the process' `sp` entry. Similarly, when switching to a process the memory tables are restored and `sp` is recovered, and then all registers are restored from the stack. + +**If the kernel has nothing to do, it activates the parent process**. If a process sends a message, or calls yield, or indicates that it has no more work to do, the kernel will resume the parent process. If there is no parent process, the kernel will wait for an interrupt, and await a message to be delivered from userspace. + +### Implementing Multi Threading + +A process' parent is allowed several specialized syscalls. For example, it has the ability to change the `sp` of a child process. In order to implement threads in userspace, the parent would keep track of what threads are owned by a particular process and then call `resume_at()` in order to resume a process at a different program counter. + +## Resource-constrained "Embedded-style" + +It is possible to use Xous to create an "embedded-style" PID 1 that runs all computing inside a single process. In this system, there would only be a single process. + +An interrupt handler would fire that calls `resume_at()` on the single process in order to switch between various threads. However, from the perspective of the kernel, this is all taking place within a single process. + +## Multi-Threaded "Desktop-style" + +This describes one possible task launcher that could be used to provided multiple threads across multiple processes. + +Note that in this system, `spawn()` would be implemented as a message that gets sent to the task manager in order to call `sys_process_spawn()`. This would take care of setting up threads and running the process. + +### Process Structures + +The following process structures are kept within the task manager, hereafter called `task_manager`: + +```rust + +/// Runnable priority level -- higher values interrupt lower ones +type UserPriority = u8; + +// type UserThreadList = [&UserThread]; +type UserThreadList = Vec; + +enum UserProcessState { + /// This process does not exist, and is free to be allocated. + /// Seen when using statically-allocated process tables. + Empty, + + /// The process was created, but hasn't been set up yet. + /// It is still missing things such as a text section. + Created, + + /// Process is ready for execution + Ready, + + /// This process is currently running + Running, + + /// All threads are blocking waiting on a message + WaitMessage, +} + +/// Desktop-style process wrapper in userspace around the kernel structure +struct UserProcess { + + /// Kernel PID of this process + pid: XousPid, + + /// Priority of this process -- higher values interrupt lower ones + priority: UserPriority, + + /// Current runnable state of this process + state: UserProcessState, + + /// All threads of this process that share a memory space. + /// If this goes to 0, then the process must terminate. + threads: UserThreadList, +} +``` + +### Threads + +Each process has one or more threads. If all of the threads exit, then a process will crash. + +```rust +/// TIDs are 16-bits so we can have lots of threads per-process. +type UserTid = u16; + +/// Current runnable state of an individual thread. +enum UserThreadState { + /// This thread doesn't exist, and is free for use. This + /// is used when thread tables are statically allocated. + Empty, + + /// Thread is ready for execution + Runnable, + + /// Thread is currently running + Active, + + /// Thread has terminated, and this slot is free for reuse + Terminated, +} + +/// One thread of execution. Every process must have at least one +/// thread. Note that the current definition here is two words (8 bytes) +/// of overhead per thread, plus the stack space required to do a +/// context switch. +struct UserThread { + /// Thread ID + tid: UserTid, + + /// Current runnable state of this thread + state: UserThreadState, + + /// Priority of this thread -- higher values interrupt lower ones + priority: UserThreadPriority, + + /// Address of the stack pointer. This is only valid when + /// the `state` is `Ready`. + /// Upon context switch, the entire register set is pushed + /// to the stack and the resulting `$sp` value is stored here. + /// When resuming, `$sp` is restored first, and then the register + /// set is restored from the stack. + sp: usize, + + /// Proces that this thread is waiting on. When this thread + /// is to be resumed, this process will be woken up instead. + wait_process: Option, +} +``` + +## Process Creation + +A process is created with the `process_create()` syscall: + +```rust +pub fn sys_process_create(pages: [XousPage], base_address: usize) -> XousResult +``` + +This will move the memory pages passed in to the new process to the base address specified in `base_address`, and return the new PID. These pages are removed from the current memory space regardless of whether the call succeeds. + +1. The kernel allocates a new `XousProcess` + * A new MMU page is allocated to the kernel, which will become `mmu_offset`. This page is cleared to 0. + * The `heap_size` is set to 0 + * `$sp` is set to `$STACK_BASE-$REG_COUNT*usize` + +At this point, the process is ready to be run. The value of `$pc` should be correctly set in the initial pages that get passed to `process_create()`. + +The initial program loader should take care of extending its own address space if necessary. For processes that don't require additional memory, the entire program can be passed to `process_create()`. diff --git a/syscalls.md b/syscalls.md new file mode 100644 index 0000000..9f5fb07 --- /dev/null +++ b/syscalls.md @@ -0,0 +1,275 @@ +# Syscalls in Xous + +Syscalls enable communication between processes, as well as communication to the kernel. These are guaranteed to never change, but new syscalls may be added. + +Syscalls may take up to seven `usize`-bit arguments, and may return up to seven `usize`-bit output operands, plus a tag indicating success or failure. + +## Syscalls on RISC-V + +RISC-V specifies eight registers as `argument` registers: `$a0`-`$a7`. When performing a syscall, the following convention is used: + +| Register | Usage (Calling) | +| -------- | -------------- | +| a0 | Arg 1 | +| a1 | Arg 2 | +| a2 | Arg 3 | +| a3 | Arg 4 | +| a4 | Arg 5 | +| a5 | Arg 6 | +| a6 | Arg 7 | +| a7 | Syscall Number | + +When returning from the syscall, these registers have the following meaning: + +| Register | Usage (Return) | +| -------- | -------------- | +| a0 | Return type tag | +| a1 | Arg 1 | +| a2 | Arg 2 | +| a3 | Arg 3 | +| a4 | Arg 4 | +| a5 | Arg 5 | +| a6 | Arg 6 | +| a7 | Arg 7 | + +Note that this means that there is a hard limit on the number of arguments that can be passed. + +## List of syscalls + +Actual system calls function names are all prefixed with `sys_`. However, when referred to generically, omit this prefix. For example, prefer `client_send` over `sys_client_send`. + +Calls that may be made from an Interrupt context begin with `sysi_` (or, more generally, +with interrupts disabled). + +```rust + +use core::num::NonZeroUsize; +type MemoryAddress = NonZeroUsize; +type MemorySize = NonZeroUsize; +type StackPointer = usize; +type MessageId = usize; + +enum XousError { + BadAlignment, + BadAddress, + OutOfMemory, + InvalidString, + ServerExists, + ServerNotFound, + ProcessNotFound, + ProcessNotChild, + ProcessTerminated, + Timeout, +} + +struct XousContext { + stack: StackPointer, + pid: XousPid, +} + +struct XousMemoryMessage { + id: MessageId, + in_buf: Option, + in_buf_size: Option, + out_buf: Option, + out_buf_size: Option, +} + +struct XousScalarMessage { + id: MessageId, + arg1: usize, + arg2: usize, + arg3: usize, + arg4: usize, +} + +enum XousMessage { + Memory(XousMemoryMessage), + Scalar(XousScalarMessage), +} + +struct XousMessageReceived { + sender: XousMessageSender, + message: XousMessage, +} + +/// Allocates kernel structures for a new process, and returns the new PID. +/// This removes `page_count` page tables from the calling process at `origin_address` +/// and places them at `target_address`. +/// +/// If the process was created successfully, then the new PID is returned to +/// the calling process. The child is not automatically scheduled for running. +/// +/// # Errors +/// +/// * **BadAlignment**: `origin_address` or `target_address` were not page-aligned, +/// or `address_size` was not a multiple of the page address size. +/// * **OutOfMemory**: The kernel couldn't allocate memory for the new process. +fn sys_process_spawn(origin_address: MemoryAddress, + target_address: MemoryAddress, + address_size: MemorySize) -> Result; + +/// Pauses execution of the current thread and returns execution to the parent +/// process. This function may return at any time in the future, including immediately. +fn sys_process_yield(); + +/// Interrupts the current process and returns control to the parent process. +/// +/// # Errors +/// +/// * **ProcessNotFound**: The provided PID doesn't exist, or is not running on the given CPU. +fn sysi_process_suspend(pid: XousPid, cpu_id: XousCpuId) -> Result<(), XousError>; + +/// Claims an interrupt, and calls the given function +/// in an interrupt context. +fn sys_interrupt_claim(irq: usize, f: F) -> Result<(), XousError> + where F: Fn(usize); + +/// Resumes a process using the given stack pointer. A parent could use +/// this function to implement multi-threading inside a child process, or +/// to create a task switcher. +/// +/// To resume a process exactly where it left off, set `stack_pointer` to `None`. +/// This would be done in a very simple system that has no threads. +/// +/// By default, at most three context switches can be made before the quantum +/// expires. To enable more, pass `additional_contexts`. +/// +/// If no more contexts are available when one is required, then the child +/// automatically relinquishes its quantum. +/// +/// # Returns +/// +/// When this function returns, it provides a list of the processes and +/// stack pointers that are ready to be run. Three can fit as return values, +/// and additional context switches will be supplied in the slice of context +/// switches, if one is provided. +/// +/// # Examples +/// +/// If a process called `yield()`, or if its quantum expired normally, then +/// a single context is returned: The target thread, and its stack pointer. +/// +/// If the child process called `client_send()` and ended up blocking due to +/// the server not being ready, then this would return no context switches. +/// This thread or process should not be scheduled to run. +/// +/// If the child called `client_send()` and the server was ready, then the +/// server process would be run immediately. If the child process' quantum +/// expired while the server was running, then this function would return +/// a single context containing the PID of the server, and the stack pointer. +/// +/// If the child called `client_send()` and the server was ready, then the +/// server process would be run immediately. If the server then finishes, +/// execution flow is returned to the child process. If the quantum then +/// expires, this would return two contexts: the server's PID and its stack +/// pointer when it called `client_reply()`, and the child's PID with its +/// current stack pointer. +/// +/// If the server in turn called another server, and both servers ended up +/// returning to the child before the quantum expired, then there would be +/// three contexts on the stack. +/// +/// # Errors +/// +/// * **ProcessNotFound**: The requested process does not exist +/// * **ProcessNotChild**: The given process was not a child process, and +/// therefore couldn't be resumed. +/// * **ProcessTerminated**: The process has crashed. +fn sys_process_resume(process_id: XousPid, stack_pointer: Option, + additional_contexts: &Option<&[XousContext]>) -> + Result<(Option, + Option, + Option), + XousError>; + +/// Causes a process to terminate immediately. +/// +/// It is recommended that this function only be called on processes that +/// have cleaned up after themselves, e.g. shut down any servers and +/// flushed any file descriptors. +/// +/// # Errors +/// +/// * **ProcessNotFound**: The requested process does not exist +/// * **ProcessNotChild**: The requested process is not our child process +fn sys_process_terminate(process_id: XousPid) -> Result<(), XousError>; + +/// Allocates pages of memory, equal to a total of `size +/// bytes. If a physical address is specified, then this +/// can be used to allocate regions such as memory-mapped I/O. +/// If a virtual address is specified, then the returned +/// pages are located at that address. Otherwise, they +/// are located at an unspecified offset. +/// +/// # Errors +/// +/// * **BadAlignment**: Either the physical or virtual addresses aren't page-aligned, or the size isn't a multiple of the page width. +/// * **OutOfMemory**: A contiguous chunk of memory couldn't be found, or the system's memory size has been exceeded. +fn sys_memory_allocate(physical: Option, + virtual: Option, + size: MemorySize) -> + Result; + +/// Equivalent to the Unix `sbrk` call. Adjusts the +/// heap size to be equal to the specified value. Heap +/// sizes start out at 0 bytes in new processes. +/// +/// # Errors +/// +/// * **OutOfMemory**: The region couldn't be extended. +fn sys_heap_resize(size: MemorySize) -> Result<(), XousError>; + +///! Message Passing Functions + +/// Create a new server with the given name. This enables other processes to +/// connect to this server to send messages. Only one server name may exist +/// on a system at a time. +/// +/// # Errors +/// +/// * **ServerExists**: A server has already registered with that name +/// * **InvalidString**: The name was not a valid UTF-8 string +fn sys_server_create(server_name: usize) -> Result; + +/// Suspend the current process until a message is received. This thread will +/// block until a message is received. +/// +/// # Errors +/// +fn sys_server_receive(server_id: XousSid) -> + Result; + +/// Reply to a message received. The thread will be unblocked, and will be +/// scheduled to run sometime in the future. +/// +/// If the message that we're responding to is a Memory message, then it should be +/// passed back directly to the destination without modification -- the actual contents +/// will be passed in the `out` address pointed to by the structure. +/// +/// # Errors +/// +/// * **ProcessTerminated**: The process we're replying to doesn't exist any more. +/// * **BadAddress**: The message didn't pass back all the memory it should have. +fn sys_server_reply(destination: XousMessageSender, message: XousMessage) -> + Result<(), XousError>; + +/// Look up a server name and connect to it. +/// +/// # Errors +/// +/// * **ServerNotFound**: No server is registered with that name. +fn sys_client_connect(server_name: usize) -> Result; + +/// Send a message to a server. This thread will block until the message is responded to. +/// If the message type is `Memory`, then the memory addresses pointed to will be +/// unavailable to this process until this function returns. +/// +/// # Errors +/// +/// * **ServerNotFound**: The server does not exist so the connection is now invalid +/// * **BadAddress**: The client tried to pass a Memory message using an address it doesn't own +/// * **Timeout**: The timeout limit has been reached +fn sys_client_send(server: XousConnection, message: XousMessage) -> + Result; +```