Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion doc/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,16 @@
- [The `print()` and `format()` functions](./kernel/print.md)
- [Init Functions](./kernel/init.md)
- [Devices](./kernel/devices.md)
- [Threads and Harts](./kernel/threads-and-harts.md)
- [Containers]()
- [Lists](./kernel/containers/lists.md)
- [Guards](./kernel/containers/guards.md)
- [Reference counting](./kernel/containers/refcounting.md)
- [Memory management]()
- [Overview](./kernel/mm/overview.md)
- [Memory map](./kernel/mm/memory-map.md)
- [Booting](./kernel/mm/booting.md)
- [Tasks]()
- [Overview](./kernel/task/overview.md)
- [Targets](./targets.md)
- [Tutorials](./tutorials/tutorials.md)
- [First Day](./tutorials/first-day.md)
Expand Down
58 changes: 58 additions & 0 deletions doc/kernel/containers/guards.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Guards

RAII, or "resource acquisition is initialization," is a common pattern in C++, Rust, and similar languages.
With RAII, some object or resource being acquired (i.e., having a value of that type in a valid state) is tied to that object being initialized and deinitialized.

In C++, this is accomplished with constructors and destructors; in Rust, with privacy and the Drop trait.
In C, we can implement this with [the `cleanup` attribute](https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.html#index-cleanup-variable-attribute).
This attribute applies to a variable, and registers a function to run when the variable goes out of scope.

This is roughly equivalent to the Drop trait, with one limitation -- we can't easily move the variable to extend its lifetime past the scope it's declared in.
However, this is still useful enough for us to have RAII objects to manage access to resources.

The objects that manage access to the resource are called "guards."
These are usually used to handle locks and refcounts.

## Using a guard

Most types of guards will have a macro for acquiring the guard, typically named something like `FOO_GUARD`.
This macro takes a variable name (for the guarded resource) as the first argument, and some number of arguments afterwards.

Typically, there's another macro for declaring guarded variables, to make it harder to accidentally use them without acquiring the guard.
For example:

```c
#include <mutex.h>
#include <print.h>

struct foo {
// Declare a mutex named bar and some variables protected by it.
MUTEX_GUARDED(bar, {
int baz;
char *asdf;
});

// This variable is not protected by the mutex.
int qwerty;
};

int foo_func(struct foo* foo) {
int x = 3 * foo->qwerty;

// Because of the MUTEX_GUARDED macro, we can't just write e.g.
// x += foo->baz;

{
MUTEX_GUARD(g, foo, bar);

// We can access the variables through g instead, though!
x += g->baz;

print("{cstr}", g->asdf);
}

// Mutex is no longer held here, but g is out of scope, so we can't use
// foo->baz or g->baz.
return x;
}
```
85 changes: 85 additions & 0 deletions doc/kernel/containers/lists.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Lists

Many resources in the ukoOS kernel need to be in some registry.
For example, devices of a certain device class (e.g. UARTs) generally need to be in some list, so that the kernel can iterate through all UARTs.
Lists (`struct list_head`, after the same structure in Linux) allow for this in ukoOS.
See `src/kernel/include/list.h` for the actual list API.

ukoOS lists are doubly-linked circular *intrusive* linked lists; that is, rather than the list containing the element in some way (whether by having a pointer that points to it or by including it in each link's memory allocation), elements contain the link.

For example, let's say we had a list of RGB colors and names.
We might write that data structure as:

```c
struct named_color {
struct list_head list;
const char *name;
u8 r, g, b;
};
```

This might seem strange, but it has one big advantage over a container that owns its data -- elements can belong to multiple lists.
For example, pretend we had both an `all_colors` list and a `favorite_colors` list:

```c
struct named_color {
struct list_head all_colors;
struct list_head favorite_colors;
const char *name;
u8 r, g, b;
};
```

This lets the color be in both lists at once.

Lists are intended to have a single owning node / sentinel node.
This is **not** embedded in the structure like other nodes are, but stands alone, often as a global:

```c
struct named_color {
struct list_head all_colors;
struct list_head favorite_colors;
const char *name;
u8 r, g, b;
};

struct list_head all_colors = LIST_INIT(all_colors);
struct list_head favorite_colors = LIST_INIT(favorite_colors);
```

Since the lists are circular, we can use this sentinel node to know when we've reached the end of the list.

We can also use this to create a tree:

```c
struct tree_node {
/**
* The parent of this node.
*/
struct tree_node *parent;

/**
* This node's link in its parent's children.
*/
struct list_head list;

/**
* This node's children.
*/
struct list_head children;
};
```

The `container_of` macro can be used to go from a `struct list_head *` to a `struct tree_node *`:

```c
struct tree_node node = { /* ... */ };

struct list_head *ptr1 = &node.list;
struct tree_node *node1 = container_of(ptr1, struct tree_node, list);
assert(node1 == &node);

struct list_head *ptr2 = &node.children;
struct tree_node *node2 = container_of(ptr2, struct tree_node, children);
assert(node2 == &node);
```
44 changes: 44 additions & 0 deletions doc/kernel/containers/refcounting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Reference counting

Many resources in the ukoOS kernel have dynamic enough ownership that reference counting is useful to determine if they're still in use and when they should be freed.

We have a helper type, `refcount_t`, to help manage these.
This is similar to the type with the same name in the Linux kernel, but we provide different helpers.

```c
#include <refcount.h>

/**
* This type is some random example resource. It's always
*/
struct foo {
/**
* This field stores the actual reference count. This is an integral type
* that is no wider than usize.
*/
refcount_t refcount;

/**
* A list the value is in.
*/
struct list_head all_foos;

/**
* Another list the value might be in (or the list might be self-linked).
*/
struct list_head bars;
};

struct list_head all_foos;

void add_foos_not_in_a_bars_to_list(struct list_head *bars) {
for (struct list_head *iter = all_foos.next; iter != all_foos;
iter = iter->next) {
struct foo *foo = container_of(iter, struct foo, all_foos);
if (list_is_empty(foo->bars))
continue;
list_push(bars, &foo->bars);
}
}

```
64 changes: 64 additions & 0 deletions doc/kernel/task/overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Overview

In ukoOS, there are three related concepts that are important to keep separate.

- **harts**, or hardware threads.
Colloquially, we might call these "cores" or "CPUs."
This terminology comes from RISC-V, but the concept applies to any architecture.
- **tasks**, or kernel threads.
The kernel manages these automatically, and will create and destroy them at various times.
- **threads**, or user threads.
These are the threads that userspace programmers talk about.
They are created only in response to userspace syscalls.

Each of these notions of threads also has its own notion of "thread-locals."
These are stored in various places.

- Hart-locals are pointed to by the `sscratch` CSR, so we can get to them in trap handlers, regardless of whether the trap handler interrupted kernel-space or user-space execution.
- The hart-locals are defined as `struct hart_locals` in `src/kernel/include/hart_locals.h`.
- Task-locals and thread-locals are pointed to by the `tp` register (`x4`).
- These are not yet implemented, but probably require linker magic.

## Lifecycle

When the kernel boots, there is one "root task."
This task needs to bootstrap itself a bit to end up with all the right state for a task and start the scheduler.
Once this is done, that task isn't treated specially by ukoOS.

Other tasks are created with the `task_create` function.
This creates a task, including allocating and mapping a stack, but does not initialize the registers or schedule the task.

The `task_set_pc` function sets the program counter and arguments for the task, sets up the stack, etc.

The `task_spawn` function marks the task as schedulable and adds it to the scheduler's queues.

If a task calls the `task_exit` function, or another task calls the `task_kill` function and passes it as an argument, the task exits.

When a task exits:

- Any spinlocks or sleeplocks held by the task are poisoned.
- Any `refcount_guard`s held by the task are `decref`ed.
- Any links or monitors attached to the task will be triggered and removed.
- The task becomes unschedulable and is removed from the scheduler's queues.
(All the previous tasks are scheduled as normal, though the task's priority is boosted to 14 while cleanup is occurring.)

Once the task's reference count reaches zero, it will be freed.

## Links and Monitors

In ukoOS, tasks that communicate with tasks often need to be aware of when those tasks exit.
This can be achieved with **links** and **monitors**.
These are closely inspired by the Erlang concepts with the same names.

Links are bidirectional; if task `A` is linked to task `B`, then task `B` is linked to task `A`.
Links ensure that if a task exits, other tasks do as well.

A link can be created between the current task and another task with the `task_link` function.
A link can be removed between the current task and another task with the `task_unlink` function.
Only one link can exist at a time between two tasks: if a link already exists, `task_link` is a no-op; if a link does not exist, `task_unlink` is a no-op.

Monitors, on the other hand, are unidirectional.
Unlike in Erlang, a monitor in ukoOS connects a channel to a task.
When the task exits, the channel gets sent a `struct monitor_status`.

## Inter-task communication
18 changes: 0 additions & 18 deletions doc/kernel/threads-and-harts.md

This file was deleted.

1 change: 1 addition & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
pkgs.pkgsCross.riscv64-embedded.stdenv.cc.bintools.bintools
pkgs.pkgsCross.riscv64-embedded.stdenv.cc.cc
pkgs.python3
pkgs.watchexec
];

dontUnpack = true;
Expand Down
1 change: 1 addition & 0 deletions src/kernel/include.mak
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ kernel-objs-c += random
kernel-objs-c += selftest
kernel-objs-c += swar_test
kernel-objs-c += symbolicate
kernel-objs-c += task
include $(srcdir)/src/kernel/drivers/include.mak

# The architecture-specific file needs to be last, since it calls
Expand Down
Loading