https://github.com/demindiro/norost-a
A first attempt at writing an OS
https://github.com/demindiro/norost-a
Last synced: over 1 year ago
JSON representation
A first attempt at writing an OS
- Host: GitHub
- URL: https://github.com/demindiro/norost-a
- Owner: Demindiro
- License: gpl-3.0
- Created: 2021-08-30T10:47:51.000Z (almost 5 years ago)
- Default Branch: master
- Last Pushed: 2021-08-30T10:49:15.000Z (almost 5 years ago)
- Last Synced: 2025-02-07T06:26:52.220Z (over 1 year ago)
- Language: Rust
- Size: 1.42 MB
- Stars: 0
- Watchers: 3
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.rst
- License: LICENSE
Awesome Lists containing this project
README
========
Norost A
========
This is my first attempt at making an operating system. The kernel and driver
are all written in Rust. Implemented features include:
* Microkernel design.
* Pre-emptive multitasking.
* Asynchronous IPC with optional synchronization using ``io_wait``.
* Interrupt handling in userspace.
* PCI driver.
* virtio-block, virtio-input & virtio-gpu
* Very, very basic driver for FAT.
* Very basic console using either UART or virtio-gpu.
* Partial implementation of libc.
The OS is written for RISC-V as it is a much cleaner architecture than x86. The
specification is easy to comprehend which got me going quickly. Additionaly,
it's an open standard which means any company can design & produce RISC-V
hardware.
While I did get quite far IMO, I decided to stop developing this project
because the design is lacking. The initial design was not much more than some
notes with rough implementation details as I was very inexperienced with OS
development. I did amend the design over time but it ended up being messy and
more complicated than I like.
**Note**: I use tasks and processes interchangeably as there is no clear
distinction between either in this design.
Motivation
~~~~~~~~~~
Many computers spend a lot of time doing nothing or being turned off. I believe
this is wasteful as those resources could be used for other tasks (compiling,
rendering ...). While there are tools that solve this (e.g. ``distcc``) they're
usually constrained to a specific application. Instead, it would be better if
the OS provided some basic utilities so all applications can take advantage of
it. Hence, the long, long, looooong term goal was to create a **distributed
OS**.
Basic design
~~~~~~~~~~~~
A microkernel design was chosen so that all applications & drivers use a common
interface. This would make it easier to emulate drivers, e.g. a local disk
would be indistinguishable from network-attached storage. This is helpful when
creating a distributed OS where many resources may be remote.
Since remote resources are usually very slow to access compared to local
resources, IPC is designed to be fully asynchronous. This should scale better
as networks grow larger. However, some programs cannot continue before
receiving a resource. These can explicitly wait for incomping I/O using a
system call.
Microkernels are usually considered slow because of the increased amount of IPC
and context switches compared to monolithic kernels. I believe this can be
remedied for performance-critical applications by simply communicating directly
with devices. To aid with this, drivers are split in a standard executable and
a library containing the actual functionality.
IPC
'''
The main communication mechanism is through two ring buffers, one for
transmitting and one for receiving data. The queue contain 16-bit indices
pointing to packet slots. Each packet has the following info:
* An address to which process to send the packet to or from whom the packet was
received.
* Data with a length and offset.
* A name with a short length.
* A UUID to optionally avoid the need for long names.
* An opcode field describing the operation to apply.
* An ID to distinguish multiple operations on the same object.
To avoid redundant copying, data would be exchanged through moving or sharing
mappings. Moving pages would avoid incrementing a reference counter.
To figure out which address range to map the data to, the receiving process
specifies a number of ranges in a stack shared with the kernel.
To improve efficiency, one-off requests can be made without explicitly opening
or closing files. Open/close is still supported if some form of persistent
connection is required. Servers can automatically close connections by watching
the state of the requesting process, i.e. if the client exits, the server
receives a "terminate" packet generated by the kernel.
Notifications
'''''''''''''
Notifications are the software equivalent of hardware interrupts: they would
trigger an immediate context switch to the receiving process and runs a handler
that has been specified beforehand. This is useful for soft real-time applications.
The same mechanism is used for passing hardware interrupts to a userspace
process: userspace reserves one or more interrupts. When the interrupt occurs,
the kernel looks up which process reserved it and immediately switches to it.
Since IRQs may be shared but only one process can reserve it, notifications can
be deferred to other processes when returning from the handler.
Memory management
'''''''''''''''''
Due to being a microkernel, memory management can be somewhat simplified:
Instead of having a full-blown memory allocator, address ranges are reserved at
compile-time. Each subsystem then allocates & maps pages as needed.
The physical memory manager uses a bitmap to keep track of free & allocated
pages. To speed up frequent allocations & deallocations, each hart has a stack
of recently used PPNs.
The virtual memory manager treats the upper half of the address space as global
& kernel-owned while the lower half is used by userland. To scale with systems
that may have more physical memory than can be mapped idempotently two hugepage
slots are reserved. These slots are used for updating the page tables. Since
hugepages are used TLB flushing of these temporary mappings should have only a
minor performance impact as the table will be cached and only one load is
necessary.
Shared pages are supported through reference counting each page. To avoid
redundantly initializing counters, a distinction is made between private and
shared pages.
The kernel does not directly keep track of allocated ranges. Instead, it is up
to userspace to allocate & map pages to specific addresses & keep track of
addresses that are already in use. This can be arbitrarily complex depending on
the needs of the application.
Task groups
'''''''''''
To isolate tasks they are put in groups. Each task can specify whether they
allow IPC from outside their group or not. This also affects the scheduler, as
the scheduler has per-group "fairness" granulity.
To further improve isolation, tasks can be sandboxed. Any packets sent by
sandboxed tasks are sent to a "host" task, which can filter the packets.
Brief history
~~~~~~~~~~~~~
I started this project in May after having my interest piqued seeing someone
else develop an OS. I looked up some resources regarding OS development and
figured I should be able to create a basic OS.
I did not want to create "yet another UNIX" however. Instead I wanted to
address an issue I perceive with modern OSes: idling (see _`Motivation`).
While I did have prior programming experience, I had no experience with OSDev
specifically. Since I didn't know what exactly I needed to do, I instead
drafted a rough design and figured I could go in detail later.
I had a very barebones kernel working relatively quickly. However, it ran in
M-mode which is intended for firmware. One painful rework later I got the
kernel running in S-mode & virtual memory worked "properly".
After that a prototype IPC implementation was added where the kernel emulated a
server process. Eventually it got replaced with a proper driver server once I
figured out how to use PCI and virtio. I implemented a subset of the C library
so I had something to test against. MiniSH evolved out of this as a way to test
the library itself.
When IPC and the OS library "dux" was somewhat fleshed out I started working on
interrupts & pre-emption so tasks wouldn't use 100% CPU all the time. It was
only at this time I designed the notification mechanism. Around this time I
also discovered QEMU wasn't handling the timer correctly. Good emulation of
buggy hardware I suppose :P
Finally, I started implementing a virtio-gpu driver as every OS seems to have
some form of GUI nowadays. It was around this period I started to realize my
design was lacking: I had no proper way of detecting when a connection should
be closed. I did amend the design with a simple solution (the "watching
processes" thing) so I didn't think much of it further.
When redesigning & reimplementing the IPC packet format to be smaller & more
efficient for small data I realized it became a lot more complex than I
anticipated. Since higher complexity usually means reduced latency/performance
too I figured I should take a step back and carefully reconsider the design.
Some thinking later I got some other ideas that could improve efficiency and
simplicity but would require a large redesign. At that point I decided to stop
development, clean up whatever I have and put it online somewhere.
Future
~~~~~~
I intend to continue with OSDev but with a design from scratch. I intend to
reuse the code in this repository as a good chunk of it will certainly still
be useful.
I will be spending much more time on the design now that I have some experience
and know what to do (and not do). This should avoid frequent rewrites in
certain areas.