Compare commits

..

6 Commits

Author SHA1 Message Date
f575d22cb5 Add VFS diagram image 2025-11-19 22:23:39 +01:00
1f13f2025a FAT integration 2025-11-19 21:56:28 +01:00
475ef950d9 Dark theme 2025-11-12 23:35:42 +01:00
038d924916 MBus messaging IPC 2025-11-12 20:39:02 +01:00
34be439843 TB shell scripting 2025-11-08 17:08:51 +01:00
0f7959a642 Remove beach.jpg 2025-11-08 16:30:03 +01:00
12 changed files with 606 additions and 10 deletions

View File

@ -164,7 +164,7 @@ table thead tr th, table tfoot tr th, table tbody tr td, table tr td, table tfoo
.clearfix:before, .clearfix:after, .float-group:before, .float-group:after { content: " "; display: table; }
.clearfix:after, .float-group:after { clear: both; }
*:not(pre) > code { font-size: 0.9375em; padding: 1px 3px 0; white-space: nowrap; background-color: #f2f2f2; border: 1px solid #cccccc; -webkit-border-radius: 4px; border-radius: 4px; text-shadow: none; }
*:not(pre) > code { font-size: 0.9375em; padding: 1px 3px 0; white-space: nowrap; background-color: #060606; text-shadow: none; }
/*pre, pre > code { line-height: 1.4; color: inherit; font-family: Consolas, "Liberation Mono", Courier, monospace; font-weight: normal; }*/

View File

@ -22,7 +22,7 @@ body {
height: 60px;
}
#footer {
background-color: #f5f5f5;
background-color: #060606;
padding: 0;
}
@ -49,4 +49,4 @@ body {
/*code {
font-size: 80%;
}*/
}*/

File diff suppressed because one or more lines are too long

View File

@ -1 +1,118 @@
.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}
/*
* Derived from einaros's Sons of Obsidian theme at
* http://studiostyl.es/schemes/son-of-obsidian by
* Alex Ford of CodeTunnel:
* http://CodeTunnel.com/blog/post/71/google-code-prettify-obsidian-theme
*/
.str
{
color: #EC7600;
}
.kwd
{
color: #93C763;
}
.com
{
color: #66747B;
}
.typ
{
color: #678CB1;
}
.lit
{
color: #FACD22;
}
.pun
{
color: #F1F2F3;
}
.pln
{
color: #F1F2F3;
}
.tag
{
color: #8AC763;
}
.atn
{
color: #E0E2E4;
}
.atv
{
color: #EC7600;
}
.dec
{
color: purple;
}
pre.prettyprint
{
border: 0px solid #888;
}
ol.linenums
{
margin-top: 0;
margin-bottom: 0;
}
.prettyprint {
background: #000;
}
li.L0, li.L1, li.L2, li.L3, li.L4, li.L5, li.L6, li.L7, li.L8, li.L9
{
color: #555;
list-style-type: decimal;
}
li.L1, li.L3, li.L5, li.L7, li.L9 {
background: #111;
}
@media print
{
.str
{
color: #060;
}
.kwd
{
color: #006;
font-weight: bold;
}
.com
{
color: #600;
font-style: italic;
}
.typ
{
color: #404;
font-weight: bold;
}
.lit
{
color: #044;
}
.pun
{
color: #440;
}
.pln
{
color: #000;
}
.tag
{
color: #006;
font-weight: bold;
}
.atn
{
color: #404;
}
.atv
{
color: #060;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -0,0 +1,191 @@
= FAT filesystem integration into MOP2
Kamil Kowalczyk
2025-11-19
:jbake-type: post
:jbake-tags: MOP2 osdev
:jbake-status: published
Hello, World!
I would like to present to you FAT filesystem integration into the MOP2 operating system! This was
possible thanks to `fat_io_lib` library by UltraEmbedded, because there's no way I'm writing an
entire FAT driver from scratch ;).
Source for `fat_io_lib`: https://github.com/ultraembedded/fat_io_lib
== Needed changes and a "contextual" system
Integrating fat_io_lib wasn't so straightforward as I thought it would be. To understand the problem
we need to first understand the current design of MOP2's VFS.
=== MOP2's VFS explained
image::/img/fs-design-diagram1.png["FS diagram"]
The VFS (ie. Virtual File System) is a Windows/DOS-style labeled VFS. Mountpoints are identified by
a label, like for eg. `sys:/boot/mop2` or `base:/scripts/mount.tb`, which kinda looks like
`C:\Path\To\File` in Windows. I've chosen this style of VFS over the UNIX kind, because it makes more
sense to me personally. A mountpoint label can point to a physical device, a virtual device (ramsd)
or a subdevice/partition device. In the UNIX world on the other hand, we have a hierarchical VFS, so
paths look like `/`, `/proc`, `/dev`, etc. This is a little confusing to reason about, because you'd
think that if `/` is mounted let's say on a drive `sda0`, then `/home` would be a home directory
within the root of that device. This is not always the case. `/` can be placed on `sda0`, but `/home`
can be placed on `sdb0`, so now something that looks like a subdirectory, is now a pointer to an
entirely different media. Kinda confusing, eh?
=== The problem
So now that we know how MOP2's VFS works, let's get into low-level implementation details. To handle
mountpoints we use a basic hashtable. We just map the label to the proper underlying file system
driver like so:
[source]
----
base -> Little FS
uhome -> Little FS
sys -> FAT16
----
Through this table, we can see that we have two mountpoints (base and uhome), which both use Little
FS, but are placed on different media entirely. Base is located on a ramsd, which is entirely virtual
and uhome is a partition on a physical drive.
To manage such setup, we need to have separate filesystem library contexts for each mountpoint.
A context here means an object, which stores info like block size, media read/write/sync hooks,
media capacity and so on. Luckly, with Little FS we could do this out of the box, because it blesses
us with `lfs_t` - an instance object. We can then create this object for each Little FS mountpoint.
.Internals of the VFS mountpoint structure
[source,c]
----
typedef struct VfsMountPoint {
int _hshtbstate;
char label[VFS_MOUNTPOINT_LABEL_MAX];
int32_t fstype;
StoreDev *backingsd;
VfsObj *(*open)(struct VfsMountPoint *vmp, const char *path, uint32_t flags);
int32_t (*cleanup)(struct VfsMountPoint *vmp);
int32_t (*stat)(struct VfsMountPoint *vmp, const char *path, FsStat *statbuf);
int32_t (*fetchdirent)(struct VfsMountPoint *vmp, const char *path, FsDirent *direntbuf, size_t idx);
int32_t (*mkdir)(struct VfsMountPoint *vmp, const char *path);
int32_t (*delete)(struct VfsMountPoint *vmp, const char *path);
// HERE: instance objects for the underlying filesystem driver library.
union {
LittleFs littlefs;
FatFs fatfs;
} fs;
SpinLock spinlock;
} VfsMountPoint;
typedef struct {
SpinLock spinlock;
VfsMountPoint mountpoints[VFS_MOUNTPOINTS_MAX];
} VfsTable;
// ...
----
.Little FS init code
[source,c]
----
int32_t vfs_init_littlefs(VfsMountPoint *mp, bool format) {
// Configure Little FS
struct lfs_config *cfg = dlmalloc(sizeof(*cfg));
memset(cfg, 0, sizeof(*cfg));
cfg->context = mp;
cfg->read = &portlfs_read; // Our read/write hooks
cfg->prog = &portlfs_prog;
cfg->erase = &portlfs_erase;
cfg->sync = &portlfs_sync;
cfg->block_size = LITTLEFS_BLOCK_SIZE;
cfg->block_count = mp->backingsd->capacity(mp->backingsd) / LITTLEFS_BLOCK_SIZE;
// left out...
int err = lfs_mount(&mp->fs.littlefs.instance, cfg);
if (err < 0) {
ERR("vfs", "Little FS mount failed %d\n", err);
return E_MOUNTERR;
}
// VFS hooks
mp->cleanup = &littlefs_cleanup;
mp->open = &littlefs_open;
mp->stat = &littlefs_stat;
mp->fetchdirent = &littlefs_fetchdirent;
mp->mkdir = &littlefs_mkdir;
mp->delete = &littlefs_delete;
return E_OK;
}
----
So where is the problem? It's in the fat_io_lib library. You see, the creators of Little FS thought
this out pretty well and designed their library in such manner that we can do cool stuff like this.
The creators of fat_io_lib on the other hand... yeah. Here are the bits of internals of fat_io_lib:
[source,c]
----
//-----------------------------------------------------------------------------
// Locals
//-----------------------------------------------------------------------------
static FL_FILE _files[FATFS_MAX_OPEN_FILES];
static int _filelib_init = 0;
static int _filelib_valid = 0;
static struct fatfs _fs;
static struct fat_list _open_file_list;
static struct fat_list _free_file_list;
----
There's no concept of a "context" - everything is thrown into a bunch of global variables.
To clarify: THIS IS NOT BAD! This is good if you need a FAT library for let's say a microcontroller,
or some other embedded device. Less code/stuff == less memory usage and so on.
When I was searching online for a FAT library, I wanted something like Little FS, but to my suprise
there are no libraries for FAT designed like this? Unless it's a homebrew OsDev project, of course.
I've even went on reddit to ask and got nothing, but cricket noises ;(.
I've decided to modify fat_io_lib to suit my needs. I mean, most of the code is already written for
me anyway, so I'm good.
The refactoring was quite easy to my suprise! The state of the library is global, but it's all nicely
placed in one spot, so we can then move it out into a struct:
[source,c]
----
struct fat_ctx {
FL_FILE _files[FATFS_MAX_OPEN_FILES];
int _filelib_init;
int _filelib_valid;
struct fatfs _fs;
struct fat_list _open_file_list;
struct fat_list _free_file_list;
void *extra; // we need this to store a ref to the mountpoint to access storage device hooks
};
----
[source,c]
----
// This is what our VfsMountPoint struct from earlier was referencing, BTW.
typedef struct {
struct fat_ctx instance;
} FatFs;
----
I've then gone on a compiler error hunt for about 2 hours! All I had to do is change references for
eg. from `_fs.some_field` into `ctx->_fs.some_field`. It was all pretty much brainless work - just
compile the code, read the error line number, edit the variable reference, repeat.
== Why do I even need FAT in MOP2?
I need it, because Limine (the bootloader) uses FAT16/32 (depending on what the user picks) to store
the kernel image, resource files and the bootloader binary itself. It'd be nice to be able to view
all of these files and manage them, to maybe in the future for eg. update the kernel image from the
system itself (self-hosting, hello?).
== Fruits of labour
Here are some screenshots ;).
image::/img/fatfs-showcase2.png["showcase 2"]
image::/img/fatfs-showcase1.png["showcase 1"]

View File

@ -0,0 +1,153 @@
= MBus - in-kernel messaging system
Kamil Kowalczyk
2025-11-12
:jbake-type: post
:jbake-tags: MOP2 osdev
:jbake-status: published
In this article I would like to present to you `MBus` - a kernel-space messaging IPC mechanism for
the MOP2 operating system.
== One-to-many messaging
MBus is a one-to-many messaging system. This means that there's one sender/publisher and many
readers/subscribers. Think of a YouTube channel - a person posts a video and their subscribers get
a push notification that there's content to consume.
image::/img/mbus-diagram1.png["One-to-many messaging diagram"]
== User-space API
This is the user-space API for MBus. The application can create a message bus for `objmax`
messages of `objsize`. Message buses are indentified via a global string ID, for eg. the PS/2
keyboard driver uses ID "ps2kb".
`ipc_mbusattch` and `ipc_mbusdttch` are used for attaching/detattching a consumer to/from the
message bus.
[source,c]
----
int32_t ipc_mbusmake(char *name, size_t objsize, size_t objmax);
int32_t ipc_mbusdelete(char *name);
int32_t ipc_mbuspublish(char *name, const uint8_t *const buffer);
int32_t ipc_mbusconsume(char *name, uint8_t *const buffer);
int32_t ipc_mbusattch(char *name);
int32_t ipc_mbusdttch(char *name);
----
=== Usage
The usage of MBus can be found for eg. inside of TB - the MOP2's shell:
.Initalizing interactive shell mode
[source,c]
----
if (CONFIG.mode == MODE_INTERACTIVE) {
ipc_mbusattch("ps2kb");
do_mode_interactive();
// ...
----
.Reading key presses
[source,c]
----
// ...
int32_t read = ipc_mbusconsume("ps2kb", &b);
if (read > 0) {
switch (b) {
case C('C'):
case 0xE9:
uprintf("\n");
goto begin;
break;
case C('L'):
uprintf(ANSIQ_CUR_SET(0, 0));
uprintf(ANSIQ_SCR_CLR_ALL);
goto begin;
break;
}
// ...
----
Previously reading the keyboard was done in a quite ugly manner via specialized functions of the
`ps2kbdev` device object (`DEV_PS2KBDEV_ATTCHCONS` and `DEV_PS2KBDEV_READCH`). It was a one big
hack, but the MBus API has turned out quite nicely ;).
With the new MBus API, the PS/2 keyboard driver becomes way cleaner than before (you can dig
through the commit history...).
.Kernel-side code for the PS/2 keyboard driver
[source,c]
----
// ...
IpcMBus *PS2KB_MBUS;
void ps2kbdev_intr(void) {
int32_t c = ps2kb_intr();
if (c >= 0) {
uint8_t b = c;
ipc_mbuspublish("ps2kb", &b);
}
}
void ps2kbdev_init(void) {
intr_attchhandler(&ps2kbdev_intr, INTR_IRQBASE+1);
Dev *ps2kbdev;
HSHTB_ALLOC(DEVTABLE.devs, ident, "ps2kbdev", ps2kbdev);
spinlock_init(&ps2kbdev->spinlock);
PS2KB_MBUS = ipc_mbusmake("ps2kb", 1, 0x100);
}
----
The messaging logic is ~20 lines of code now.
== The tricky part
The trickiest part to figure out while implementing MBus was to implement
cleaning up dangling/dead consumers. In the current model, a message bus
doesn't really know if a consumer has died without explicitly detattching
itself from the bus. This is solved by going through each message bus and
it's corresponding consumers and deleting the ones that aren't in the list
of currently running processes. This operation is ran every cycle of the
scheduler - you could say it's a form of garbage collection. All of this
is implemented inside `ipc_mbustick`:
[source,c]
----
void ipc_mbustick(void) {
spinlock_acquire(&IPC_MBUSES.spinlock);
// Go through all message buses
for (size_t i = 0; i < LEN(IPC_MBUSES.mbuses); i++) {
IpcMBus *mbus = &IPC_MBUSES.mbuses[i];
// Skip unused slots
if (mbus->_hshtbstate != HSHTB_TAKEN) {
continue;
}
IpcMBusCons *cons, *constmp;
spinlock_acquire(&mbus->spinlock);
// Go through every consumer of this message bus
LL_FOREACH_SAFE(mbus->consumers, cons, constmp) {
spinlock_acquire(&PROCS.spinlock);
Proc *proc = NULL;
LL_FINDPROP(PROCS.procs, proc, pid, cons->pid);
spinlock_release(&PROCS.spinlock);
// If not on the list of processes, purge!
if (proc == NULL) {
LL_REMOVE(mbus->consumers, cons);
dlfree(cons->rbuf.buffer);
dlfree(cons);
}
}
spinlock_release(&mbus->spinlock);
}
spinlock_release(&IPC_MBUSES.spinlock);
}
----
As you can see it's a quite heavy operation and thus not ideal - but still
way ahead of what we had before. I guess the next step would be to figure out
a way to optimize this further, although the system doesn't seem to take a
noticable hit in performance (maybe do some benchmarks in the future?).

View File

@ -0,0 +1,131 @@
= TB shell scripting for MOP2
Kamil Kowalczyk
2025-11-08
:jbake-type: post
:jbake-tags: MOP2 osdev
:jbake-status: published
This post is about TB (ToolBox) - the shell interpreter for MOP2 operating system.
= Invoking applications
Applications are invoked by providing an absolute path to the binary executable and the list of
arguments. In MOP2 paths must be formatted as `MOUNTPOINT:/path/to/my/file`. All paths are absolute
and MOP2 doesn't support relative paths (there's no concept of a CWD or current working directory).
.Example of listing currently running processes
[source]
----
base:/bin/pctl ls
----
Typing out the entire path might get tiresome. Imagine typing `MOUNTPOINT:/path/to/app arg more args`
every time you want to call an app. This is what TB aliases/macros are for. They make the user type
less ;).
.Example of calling an application via an alias
[source]
----
$pctl ls
----
Now that's way better!
=== Creating new aliases
To create an alias we can type
[source]
----
mkalias pctl base:/bin/pctl
----
And then we can use our `$pctl`!
But there's another issue - we have to write aliases for every application, which isn't better than
us typing out the entire path. Luckliy, there's a solution for this. TB has two useful functions
that can help us solve this - `eachfile` and `mkaliasbn`.
`eachfile` takes a directory, an ignore list and a command, which is run for every entry in the said
directory. We can also access the current directory entry via special symbol called `&EF-ELEM`.
In `base/scripts/rc.tb` we can see the full example in action.
[source]
----
eachfile !.gitkeep base:/bin \
mkaliasbn &EF-ELEM
----
This script means: for each file in base:/bin (excluding .gitkeep), call mkaliasbn for the current
entry. `mkaliasbn` then takes the base name of a path, which is expanded by `&EF-ELEM` and creates
an alias. `mkaliasbn` just simply does `mkalias <app> MP:/path/<app>`.
== Logging
In the UNIX shell there's one very useful statement - `set -x`. `set -x` tells the shell to print out
executed commands. It's useful for script debugging or in general to know what the script does (or
if it's doing anything / not stuck). This is one thing that I hate about Windows - it shows up a
stupid dotted spinner and doesn't tell you what it's doing and you start wondering. Is it stuck?
Is it waiting for a drive/network/other I/O? Is it bugged? Can I turn of my PC? Will it break if I
do? The user SHOULD NOT have these kinds of questions. That's why I believe that `set -x` is very
important.
I wanted to have something similar in TB, so I've added a `setlogcmds` function. It takes `yes` or
`no` as an argument to enable/disable logging. It can be invoked like so:
[source]
----
setlogcmds yes
----
Now the user will see printed statements, for eg. during the system start up:
[source]
----
this is an init script!
+$tb -m runfile -f base:/scripts/mount.tb
Mounting filesystems...
+$fs mount -mp uhome -fs LittleFS -dev atasd-ch0-M-part2 -fmt no
OK uhome
----
== String stack and subshells
In UNIX shell, it's common to get the output of an application, store it into a variable and then
pass that variable around to other apps. For eg:
[source]
----
# Use of a subshell
MYVAR=$(cat file.txt)
echo $MYVAR | myapp # or something...
----
In TB, I've decided to go with a stack, since I find it easier to implement than a variable hashmap.
A stack can be implemented using any dynamic list BTW.
The stack in TB is manipulated via `stackpush` and `stackpop` functions. We can `stackpush` a string
using `stackpush <my string>` and then `stackpop` it to remove it. We can also access the top of
the stack via `$stack`. It's a special magic alias, which expands to the string that is at the top.
An example of stack usage would be:
[source]
----
stackpush 'hello world string!'
print $stack
stackpop
----
=== The `do` function
The `do` function does what a subshell does in UNIX shell. We can `do` a command an then have it's
output placed at the top of the stack. An example of this would be:
[source]
----
do print 'hello world from subshell'
print $stack
stackpop
----
It's a simpler, more primitive mechanism than the UNIX subshells, but it gets the job done ;).