PatchworkOS is a modular non-POSIX operating system for the x86_64 architecture that rigorously follows an "everything is a file" philosophy, in the style of Plan9. Built from scratch in C and assembly, its intended to be an educational and experimental operating system.
In the end this is a project made for fun, but the goal is to make a "real" operating system, one that runs on real hardware and has the performance one would expect from a modern operating system without jumping ahead to user space features, a floppy disk driver and a round-robin scheduler is not enough.
Also, this is not a UNIX clone, its intended to be a (hopefully) interesting experiment in operating system design by attempting to use unique algorithms and designs over tried and tested ones. Sometimes this leads to bad results, and sometimes, with a bit of luck, good ones.
Finally, despite its experimental nature and scale, the project aims to remain approachable and educational, something that can work as a middle ground between fully educational operating systems like xv6 and production operating system like Linux.
Will this project ever reach its goals? Probably not, but thats not the point.
- Fully preemptive and tickless EEVDF scheduler based upon the original paper and implemented using a Augmented Red-Black tree to achieve
O(log n)worst case complexity. EEVDF is the same algorithm used in the modern Linux kernel, but ours is obviously a lot less mature. - Multithreading and Symmetric Multi Processing with fine-grained locking.
- Physical and virtual memory management is
O(1)per page andO(n)wherenis the number of pages per allocation/mapping operation, see benchmarks for more info. - File based IPC including pipes, shared memory, sockets and Plan9 inspired "signals" called notes.
- File based device APIs, including framebuffers, keyboards, mice and more.
- Synchronization primitives including mutexes, read-write locks, sequential locks, futexes and others.
- Highly Modular design, even SMP Bootstrapping is done in a module.
- Unix-style VFS with mountpoints, hardlinks, per-process namespaces, etc.
- Custom Framebuffer BitMaP (.fbmp) image format, allows for faster loading by removing the need for parsing.
- Custom Grayscale Raster Font (.grf) font format, allows for antialiasing and kerning without complex vector graphics.
- Custom C standard library and system libraries.
- Highly modular shared memory based desktop environment.
- Theming via config files.
- Note that currently a heavy focus has been placed on the kernel and low-level stuff, so user space is quite small... for now.
And much more...
- Replaced
fork(), exec()withspawn(). - No "user" concept.
- Non-POSIX standard library.
- Even heavier focus on "everything is a file".
- File flags instead of file modes/permissions.
- Currently limited to RAM disks only (Waiting for USB support).
- Only support for x86_64.
- Fully Asynchronous I/O and syscalls (io_uring?).
- USB support (The holy grail).
As one of the main goals of PatchworkOS is to be educational, I have tried to document the codebase as much as possible along with providing citations to any sources used. Currently, this is still a work in progress, but as old code is refactored and new code is added, I try to add documentation.
If you are interested in knowing more, then you can check out the Doxygen generated documentation. For an overview check the topics section in the sidebar.
PatchworkOS uses a "modular" kernel design, meaning that instead of having one big kernel binary, the kernel is split into several smaller "modules" that can be loaded and unloaded at runtime. In effect, the kernel can rewrite itself by adding and removing functionality as needed.
This is highly convenient for development but it also has practical advantages, for example, there is no need to load a driver for a device that is not attached to the system, saving memory.
The process of making a module is intended to be as straightforward as possible. For the sake of demonstration, we will create a simple "Hello, World!" module.
First, we create a new directory in src/kernel/modules/ named hello, and inside that directory we create a hello.c file to which we write the following code:
#include <kernel/module/module.h>
#include <kernel/log/log.h>
#include <stdint.h>
uint64_t _module_procedure(const module_event_t* event)
{
switch (event->type)
{
case MODULE_EVENT_LOAD:
LOG_INFO("Hello, World!\n");
break;
default:
break;
}
return 0;
}
MODULE_INFO("Hello", "<author>", "A simple hello world module", "1.0", "MIT", "BOOT_ALWAYS");An explanation of the code will be provided later.
Now we need to add the module to the build system. To do this, just copy a existing module's .mk file without making any modifications. For example, we can copy src/modules/drivers/ps2/ps2.mk to src/modules/hello/hello.mk. The build system will handle the rest, including copying the module to the final image.
Now, we can build and run PatchworkOS using make all run, or we could use make all and then flash the generated bin/PatchworkOS.img file to a USB drive.
Now to validate that the module is working, you can either watch the boot log and spot the Hello, World! message, or you could use grep on the /dev/klog file in the terminal program like so:
cat /dev/klog | grep "Hello, World!"This should output something like:
[ 0.747-00-I] Hello, World!Thats all, if this did not work, make sure you followed all the steps correctly, if there is still issues, feel free to open an issue.
Whatever you want. You can include any kernel header, or even headers from other modules, create your own modules and include their headers or anything else. There is no need to worry about linking, dependencies or exporting/importing symbols, the kernels module loader will handle all of it for you. Go nuts.
This code in the hello.c file does a few things. First, it includes the relevant kernel headers.
Second, it defines a _module_procedure() function. This function serves as the entry point for the module and will be called by the kernel to notify the module of events, for example the module being loaded or a device attached. On the load event, it will print using the kernels logging system "Hello, World!", resulting in the message being readable from /dev/klog.
Finally, it defines the modules information. This information is, in order, the name of the module, the author of the module (thats you), a short description of the module, the module version, the licence of the module, and finally a list of "device types", in this case just BOOT_ALWAYS, but more could be added by separating them with a semicolon (;).
The list of device types is what causes the kernel to actually load the module. I will avoid going into to much detail (you can check the documentation for that), but I will explain it briefly.
The module loader itself has no idea what these type strings actually are, but subsytems within the kernel can specify that "a device of the type represented by this string is now available", the module loader can then load either one or all modules that have specified in their list of device types that it can handle the specified type. This means that any new subsystem, ACPI, USB, PCI, etc, can implement dynamic module loading using whatever types they want.
So what is BOOT_ALWAYS? It is the type of a special device that the kernel will pretend to "attach" during boot. In this case, it simply causes our hello module to be loaded during boot.
For more information, check the Module Doxygen Documentation.
PatchworkOS features a from-scratch ACPI implementation and AML parser, with the goal of being, atleast by ACPI standards, easy to understand and educational. It is tested on the Tested Configurations below and against ACPICA's runtime test suite, but remains a work in progress (and probably always will be).
See ACPI Doxygen Documentation for a progress checklist.
See ACPI specification Version 6.6 as the main reference.
ACPI or Advanced Configuration and Power Interface is used for alot of things in modern systems but mainly power management and device enumeration/configuration. Its not possible to go over everything here, instead a brief overview of the parts most likely to cause confusion while reading the code will be provided.
It consists of two main parts, the ACPI tables and AML bytecode. If you have completed a basic operating systems tutorial, you have probably seen the ACPI tables before, for example the RSDP, FADT, MADT, etc. These tables are static in memory data structures storing information about the system, they are very easy to parse but are limited in what they can express.
AML or ACPI Machine Language is a turning complete "mini language", and the source of mutch frustration, that is used to express more complex data, primarily device configuration. This is needed as its impossible for any specification to account for every possible hardware configuration that exists currently, much less that may exist in the future. So instead of trying to design that, what if we could just had a small program generate whatever data we wanted dynamically? Well thats more or less what AML is.
To demonstrate how ACPI is used for device configuration, we will use the PS/2 driver as an example.
If you have followed a basic operating systems tutorial, you have probably implemented a PS/2 keyboard driver at some point, and most likely you hardcoded the I/O ports 0x60 and 0x64 for data and commands respectively, and IRQ 1 for keyboard interrupts.
Using this hardcoded approach will work for the vast majority of systems, but, perhaps surprisingly, there is no standard that guarantees that these ports and IRQs will actually be used for PS/2 devices. Its just a silent agreement that pretty much all systems adhere to for legacy reasons.
But this is where the device configuration from AML comes in, it lets us query the system for the actual resources used by the PS/2 keyboard, so we dont have to rely on hardcoded values.
If you where to decompile the AML bytecode into its original ASL (ACPI Source Language), you might find something like this:
Device (KBD)
{
Name (_HID, EisaId ("PNP0303") /* IBM Enhanced Keyboard (101/102-key, PS/2 Mouse) */) // _HID: Hardware ID
Name (_STA, 0x0F) // _STA: Status
Name (_CRS, ResourceTemplate () // _CRS: Current Resource Settings
{
IO (Decode16,
0x0060, // Range Minimum
0x0060, // Range Maximum
0x01, // Alignment
0x01, // Length
)
IO (Decode16,
0x0064, // Range Minimum
0x0064, // Range Maximum
0x01, // Alignment
0x01, // Length
)
IRQNoFlags ()
{1}
})
}Note that just like C compiles to assembly, ASL compiles to AML bytecode, which is what the OS actually parses.
In the example ASL, we se a Device object representing a PS/2 keyboard. It has a hardware ID (_HID), which we can cross reference with a online database to confirm that it is indeed a PS/2 keyboard, a status (_STA), which is just a bitfield indicating if the device is present, enabled, etc, and finally the current resource settings (_CRS), which is the thing we are really after.
The _CRS might look a bit complicated but focus on the IO and IRQNoFlags entries. Notice how they are specifying the I/O ports and IRQ used by the keyboard? Which in this case are indeed 0x60, 0x64 and 1 respectively. So in this case the standard held true.
So how is this information used? Durring boot, the _CRS information of each device is parsed by the ACPI subsystem, it then queries the kernel for the needed resources, assigned them to each device and makes the final configuration available to drivers.
Then when the PS/2 driver is loaded, it gets told "you are handling a device with the name \_SB_.PCI0.SF8_.KBD_ (which is just the full path to the device object in the ACPI namespace) and the type PNP0303", it can then query the ACPI subsystem for the resources assigned to that device, and use them instead of hardcoded values.
Having access to this information for all devices also allows us to avoid resource conflicts, making sure two devices are not trying to use the same IRQ(s) or I/O port(s).
Of course, it gets way, way worse than this, but hopefully this clarifies why the PS/2 driver and other drivers, might look a bit different than what you might be used to.
PatchworkOS strictly follows the "everything is a file" philosophy in a way similar to Plan9, this can often result in unorthodox APIs or could just straight up seem overly complicated, but it has its advantages. We will use sockets to demonstrate the kinds of APIs this produces.
In order to create a local seqpacket socket, you open the /net/local/seqpacket file. The opened file can be read to return the ID of your created socket. We provide several helper functions to make this easier, first, without any helpers, you would do
fd_t fd = open("/net/local/seqpacket");
if (fd == ERR)
{
/// ... handle error ...
}
char id[32] = {0};
if (read(fd, id, 31) == ERR)
{
/// ... handle error ...
}
close(fd);Using the sread() helper that reads the entire contents of a file descriptor to simplify this to
fd_t fd = open("/net/local/seqpacket");
if (fd == ERR)
{
/// ... handle error ...
}
char* id = sread(fd);
if (id == NULL)
{
/// ... handle error ...
}
close(fd);
// ... do stuff ...
free(id);Finally, you can use the sreadfile() helper that reads the entire contents of a file from its path to simplify this even further to
char* id = sreadfile("/net/local/seqpacket");
if (id == NULL)
{
/// ... handle error ...
}
// ... do stuff ...
free(id);Note that the socket will persist until the process that created it and all its children have exited. Additionally, for error handling, all functions will return either NULL or ERR on failure, depending on if they return a pointer or an integer type respectively. The per-thread errno variable is used to indicate the specific error that occurred, both in user space and kernel space.
The ID that we have retrieved is the name of a directory in the /net/local directory, in which are three files that we use to interact with the socket. These files are:
data: used to send and retrieve datactl: Used to send commandsaccept: Used to accept incoming connections
So, for example, the sockets data file is located at /net/local/[id]/data.
Say we want to make our socket into a server, we would then use the bind and listen commands with the ctl file, Once again, we provide several helper functions to make this easier. First, without any helpers, you would do
char ctlPath[MAX_PATH] = {0};
if (snprintf(ctlPath, MAX_PATH, "/net/local/%s/ctl", id) < 0)
{
/// ... handle error ...
}
fd_t ctl = open(ctlPath);
if (ctl == ERR)
{
/// ... handle error ...
}
if (write(ctl, "bind myserver && listen") == ERR) // We use && to chain commands.
{
/// ... handle error ...
}
close(ctl);Using the F() macro which allocates formatted strings on the stack and the swrite() helper that writes a null-terminated string to a file descriptor, we can simplify this to
fd_t ctl = open(F("/net/local/%s/ctl", id));
if (ctl == ERR)
{
/// ... handle error ...
}
if (swrite(ctl, "bind myserver && listen") == ERR)
{
/// ... handle error ...
}
close(ctl);Finally, using the swritefile() helper that writes a null-terminated string to a file from its path, we can simplify this even further to
if (swritefile(F("/net/local/%s/ctl", id), "bind myserver && listen") == ERR)
{
/// ... handle error ...
}Note that we name our server myserver.
If we wanted to accept a connection using our newly created server, we just open its accept file by writing
fd_t fd = open(F("/net/local/%s/accept", id));
if (fd == ERR)
{
/// ... handle error ...
}
/// ... do stuff ...
close(fd);The file descriptor returned when the accept file is opened can be used to send and receive data, just like when calling accept() in for example Linux or other POSIX operating systems. The entire socket API attempts to mimic the POSIX socket API, apart from using these weird files everything (should) work as expected.
For the sake of completeness, if we wanted to connect to this server, we can do
char* id = sreadfile("/net/local/seqpacket"); // Create new socket and get its ID.
if (id == NULL)
{
/// ... handle error ...
}
if (swritefile(F("/net/local/%s/ctl", id), "connect myserver") == ERR) // Connect to the server named "myserver".
{
/// ... handle error ...
}
/// ... do stuff ...
free(id);Namespaces are a set of mountpoints that is unique per process, which allows each process a unique view of the file system and is utilized for access control.
Think of it like this, in the common case, you can mount a drive to /mnt/mydrive and all processes can then open the /mnt/mydrive path and see the contents of that drive. However, for security reasons we might not want every process to be able to see that drive, this is what namespaces enable, allowing mounted file systems or directories to only be visible to a subset of processes.
As an example, the "id" directories mentioned in the socket example are a separate "sysfs" instance mounted in the namespace of the creating process, meaning that only that process and its children can see their contents.
To control which processes can see a newly mounted or bound file system or directory, we use a propegation system, where a the newly created mountpoint can be made visible to either just the creating process, the creating process and its children, or the creating process, its children and its parents. Additionally, its possible to specify the behaviour of mountpoint inheritance when a new process is spawned.
In cases where the propagation system is not sufficient, it's possible for two processes to voluntarily share a mountpoint in their namespaces using bind() in combination with two new system calls share() and claim().
For example, if process A wants to share its /net/local/5 directory from the socket example with process B, they can do
// In process A
fd_t dir = open("/net/local/5:directory");
// Create a "key" for the file descriptor, this is a unique one time use randomly generated token that can be used to retrieve the file descriptor in another process.
key_t key;
share(&key, dir, CLOCKS_PER_SEC * 60); // Key valid for 60 seconds (CLOCKS_NEVER is also allowed)
// In process B
// The key is somehow communicated to B via IPC, for example a pipe, socket, argv, etc.
key_t key = ...;
// Use the key to open a file descriptor to the directory, this will invalidate the key.
fd_t dir = claim(key);
// Will error here if the original file descriptor in process A has been closed, process A exited, or the key expired.
// Make "dir" ("/net/local/5" in A) available in B's namespace at "/any/path/it/wants". In practice it might be best to bind it to the same path as in A to avoid confusion.
bind(dir, "/any/path/it/wants");
// Its also possible to just open paths in the shared directory without polluting the namespace using openat().
fd_t somePath = openat(dir, "data");Note that error checking is ommited for brevity.
This system guarantees consent between processes, and can be used to implement more complex access control systems.
An interesting detail is that when process A opens the net/local/5 directory, the dentry underlying the file descriptor is the root of the mounted file system, if process B were to try to open this directory, it would still succeed as the directory itself is visible, however process B would instead retrieve the dentry of the directory in the parent superblock, and would instead see the content of that directory in the parent superblock. If this means nothing to you, don't worry about it.
You may have noticed that in the above section sections, the open() function does not take in a flags argument. This is because flags are part of the file path directly so if you wanted to create a non-blocking socket, you can write
open("/net/local/seqpacket:nonblock");Multiple flags are allowed, just separate them with the : character, this means flags can be easily appended to a path using the F() macro. Each flag also has a short hand version for which the : character is ommited, for example to open a file as create and exclusive, you can do
open("/some/path:create:exclusive");or
For a full list of available flags, check the Doxygen documentation.
Permissions are also specified using file paths there are three possible permissions, read, write and execute. For example to open a file as read and write, you can do
open("/some/path:read:write");or
Permissions are inherited, you cant use a file with lower permissions to get a file with higher permissions. Consider the namespace section, if a directory was opened using only read permissions and that same directory was bound, then it would be impossible to open any files within that directory with any permissions other than read.
For a full list of available permissions, check the Doxygen documentation.
Im sure you have heard many an argument for and against the "everything is a file" philosophy. So I wont go over everything, but the primary reason for using it in PatchworkOS is "emergent behavior" or "composability" which ever term you prefer.
Take the namespace sharing example, notice how there isent any actually dedicated "namespace sharing" system? There are instead a series of small, simple building blocks that when added together form a more complex whole. That is emergent behavior, by keeping things simple and most importantly composable, we can create very complex behaviour without needing to explicitly design it.
Lets take another example, say you wanted to wait on multiple processes with a waitpid() syscall. Well, thats not possible. So now we suddenly need a new system call. Meanwhile, in a "everything is a file system" we just have a pollable /proc/[pid]/wait file that blocks untill the process dies and returns the exit status, now any behaviour that can be implemented with poll() can be used while waiting on processes, including waiting on multiple processes at once, waiting on a keyboard and a process, waiting with a timeout, or any weird combination you can think of.
Plus its fun.
All benchmarks were run on real hardware using a Lenovo ThinkPad E495. For comparison, I've decided to use the Linux kernel, specifically Fedora since It's what I normally use.
Note that Fedora will obviously have a lot more background processes running and security features that might impact performance, so these benchmarks are not exactly apples to apples, but they should still give a good baseline for how PatchworkOS performs.
All code for benchmarks can be found in the benchmark program, all tests were run using the optimization flag -O3.
The test maps and unmaps memory in varying page amounts for a set amount of iterations using generic mmap and munmap functions. Below is the results from PatchworkOS as of commit 4b00a88 and Fedora 40, kernel version 6.14.5-100.fc40.x86_64.
xychart-beta
title "Blue: PatchworkOS, Green: Linux (Fedora), Lower is Better"
x-axis "Page Amount (in 50s)" [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30]
y-axis "Time (ms)" 0 --> 40000
line [157, 275, 420, 519, 622, 740, 838, 955, 1068, 1175, 1251, 1392, 1478, 1601, 1782, 1938, 2069, 2277, 2552, 2938, 3158, 3473, 3832, 4344, 4944, 5467, 6010, 6554, 7114, 7486]
line [1138, 2226, 3275, 4337, 5453, 6537, 7627, 8757, 9921, 11106, 12358, 13535, 14751, 16081, 17065, 18308, 20254, 21247, 22653, 23754, 25056, 26210, 27459, 28110, 29682, 31096, 33547, 34840, 36455, 37660]
We see that PatchworkOS performs better across the board, and the performance difference increases as we increase the page count.
There are a few potential reasons for this, one is that PatchworkOS does not use a separate structure to manage virtual memory, instead it embeds metadata directly into the page tables, and since accessing a page table is just walking some pointers, its highly efficient, additionally it provides better caching since the page tables are likely already in the CPU cache.
In the end we end up with a
Note that as the number of pages increases we start to see less and less linear performance, this is most likely due to CPU cache saturation.
For fun, we can throw the results into desmos to se that around
Performing quadratic regression on the same data gives us
From this we see that for
Of course, there are limitations to this approach, for example, it is in no way portable (which isn't a concern in our case), each address space can only contain spawn() instead of a fork()).
All in all, this algorithm would not be a viable replacement for existing algorithms, but for PatchworkOS, it serves its purpose very efficiently.
PatchworkOS includes its own shell utilities designed around its file flags system, when file flags are used we also demonstrate the short form. Included is a brief overview with some usage examples. For convenience the shell utilities are named after their POSIX counterparts, however they are not drop-in replacements.
Opens a file path and then immediately closes it.
# Create the file.txt file only if it does not exist.
touch file.txt:create:exclusive
touch file.txt:ce
# Create the mydir directory.
touch mydir:create:directory
touch mydir:cdReads from stdin or provided files and outputs to stdout.
# Read the contents of file1.txt and file2.txt.
cat file1.txt file2.txt
# Read process exit status (blocks until process exits)
cat /proc/1234/wait
# Copy contents of file.txt to dest.txt and create it.
cat < file.txt > dest.txt:create
cat < file.txt > dest.txt:cWrites to stdout.
# Write to file.txt.
echo "..." > file.txt
# Append to file.txt, makes ">>" unneeded.
echo "..." > file.txt:append
echo "..." > file.txt:aReads the contents of a directory to stdout.
# Prints the contents of mydir.
ls mydir
# Recursively print the contents of mydir.
ls mydir:recursive
ls mydir:RRemoves a file or directory.
# Remove file.txt.
rm file.txt
# Recursively remove mydir and its contents.
rm mydir:directory:recursive
rm mydir:dRThere are other utils available that work as expected, for example stat and link.
| Requirement | Details |
|---|---|
| OS | Linux (WSL might work, but I make no guarantees) |
| Tools | GCC, make, NASM, mtools, QEMU (optional) |
# Clone this repository, you can also use the green Code button at the top of the Github.
git clone https://github.com/KaiNorberg/PatchworkOS
cd PatchworkOS
# Build (creates PatchworkOS.img in bin/)
make all
# Run using QEMU
make run# Clean build files
make clean
# Build with debug mode enabled
make all DEBUG=1
# Build with debug mode enabled and testing enabled (you will need to have iasl installed)
make all DEBUG=1 TESTING=1
# Debug using qemu with one cpu and GDB
make all run DEBUG=1 QEMU_CPUS=1 GDB=1
# Debug using qemu and exit on panic
make all run DEBUG=1 QEMU_EXIT_ON_PANIC=1
# Generate doxygen documentation
make doxygen
# Create compile commands file
make compile_commandsSource code can be found in the src/ directory, with public API headers in the include/ directory, private API headers are located alongside their respective source files.
.
├── meta // Meta files including screenshots, doxygen, etc.
├── lib // Third party files, for example doomgeneric.
├── root // Files to copy to the root of the generated image.
└── <src|include> // Source code and public API headers.
├── kernel // The kernel and its core subsystems.
├── modules // Kernel modules, drivers, filesystems, etc.
├── programs // User space programs.
├── libstd // The C standard library.
└── libpatchwork // The PatchworkOS system library, gui, etc.
For frequent testing, it might be inconvenient to frequently flash to a USB. You can instead set up the .img file as a loopback device in GRUB.
Add this entry to the /etc/grub.d/40_custom file:
menuentry "Patchwork OS" {
set root="[The grub identifer for the drive. Can be retrived using: sudo grub2-probe --target=drive /boot]"
loopback loop0 /PatchworkOS.img # Might need to be modified based on your setup.
set root=(loop0)
chainloader /efi/boot/bootx64.efi
}Regenerate grub configuration using sudo grub2-mkconfig -o /boot/grub2/grub.cfg.
Finally copy the generated .img file to your /boot directory, this can also be done with make grub_loopback.
You should now see a new entry in your GRUB boot menu allowing you to boot into the OS, like dual booting, but without the need to create a partition.
- QEMU boot failure: Check if you are using QEMU version 10.0.0, as that version has previously caused issues. These issues appear to be fixed currently however consider using version 9.2.3
- Any other errors?: If an error not listed here occurs or is not resolvable, please open an issue in the GitHub repository.
Testing uses a GitHub action that compiles the project and runs it for some amount of time using QEMU with DEBUG=1, TESTING=1 and QEMU_EXIT_ON_PANIC=1 set. This will run some additional tests in the kernel (for example it will clone ACPICA and run all its runtime tests), and if QEMU has not crashed by the end of the allotted time, it is considered a success.
Note that the QEMU_EXIT_ON_PANIC flag will cause any failed test, assert or panic in the kernel to exit QEMU using their "-device isa-debug-exit" feature with a non-zero exit code, thus causing the GitHub action to fail.
- QEMU emulator version 9.2.3 (qemu-9.2.3-1.fc42)
- Lenovo ThinkPad E495
- Ryzen 5 3600X | 32GB 3200MHZ Corsair Vengeance
Currently untested on Intel hardware (broke student, no access to hardware). Let me know if you have different hardware, and it runs (or doesn't) for you!
Contributions are welcome! Anything from bug reports/fixes, performance improvements, new features, or even just fixing typos or adding documentation.
If you are unsure where to start, check the Todo List.
Check out the contribution guidelines to get started.
The first Reddit post and image of PatchworkOS from back when getting to user space was a massive milestone and the kernel was supposed to be a UNIX-like microkernel.
