Review of my OS's microkernel
Posted: Mon Feb 02, 2009 6:06 am
During my great rewrite of my kernel (from C++ -> D), I'm revising my microkernel's system call layout. Before you look through my syscall list I'll describe a little about the functions of my kernel, so they you have a better idea of if these goals can be achieved with the syscalls I have layed out and if I'm missing anything.
It is a microkernel, so there aren't any drivers built in (except for a basic text console driver in the case of kernel panics), they all run in userspace and that is not part of this specification.
The first function of my microkernel is to handle processes, devices, and threads. Processes and devices both exist within their own name systems, they are not part of any VFS. The VFS server (which again, is not part of the microkernel) may decide to integrate these in with files (e.g. put processes under "/processes/" and devices under "/devices/").
Processes can belong to one of 3 classes; drivers, servers, applications. These are all ring 3 processes, but the former classes are able to perform more system calls than the latter classes (their special privileges will be described below).
Drivers (the process name will be prefixed with "d\") can access IO ports, terminate a process/thread belonging to another process, send processes to sleep/wake them up, map physical memory locations into their local space, create/destroy device objects (which are merely references to the driver processes, with a unique per-device ID), listen if an IRQ fires, and transform in to (and back out of) a VM86 task.
The purpose of a driver is to abstract away system devices into "device objects". "Device objects" are merely a name (e.g. "disks\floppy\0") with a the ID of the driver and the ID device, then you communicate with the driver (through a common interface yet to be determined, but will consist of functions like get the type of device, initialise the device, and more specific functions depending on the type of device) using the kernel's IPC system and passing the ID of the device as a parameter.
Drivers can enter/leave VM86 mode freely, though in VM86 mode they can only access the first megabyte of their virtual memory (but they can execute any VM86 instructions they want) and fire an interrupt to leave VM86 mode. Some drivers (e.g. VESA drivers) will take advantage of this so they can access the BIOS (though not too often as it can slow down the entire system).
Applications shouldn't access drivers directly, but they can (though the driver can tell the class of the process, so to make sure that some actions (like writing to a disk) can only be performed by a server). A full-screen game is an example of when an application can access a driver directly (to perform 3D operations, or have exclusive control over the screen and audio output).
Examples of drivers include;
- DMA
- Floppy disk
- Hard drive
- Sound card
- Keyboard
- Video card
Servers (the process name will be prefixed with a "s\") can terminate a process/thread belonging to another process, and send processes to sleep/wake them up. Servers provide the core operating system functions.
Example of servers:
- Window manager
- VFS
- Task manager (which acts more like an application that can be launched when needed, but is classed as a server because it has the ability to kill/pause other processes)
Servers are NOT the same as web servers, file servers, etc - in this OS they would run as applications.
Applications (the process name will be prefixed with an "a\") can only directly modify themselves. They can communicate with other processes (particularly with servers) through the kernel's IPC facilities. These make up the majority (hopefully, ALL) of the user's programs.
Examples of applications:
- Web browsers
- Word processors
- File managers
- Games
- Media players
Rather than dealing with IPC directly to access servers and drivers, they will provide an interface/library (e.g. instead of sending a message, creating a pipe, pass through a buffer of commands, you will have a C interface with functions like WindowManager_Initialise(), WindowManager_CreateWindow(int x, int y, char *name)), drivers though will have a standard interface/library that you can use to access ALL devices of particular type/subtype (e.g. a device library which lets you access GetType() which tells you it's a storage device, then you'd use the storage device library to call GetSize(), Read(), GetMedia(). If the media is a CD, then you can use another interface which defines Eject()).
Memory management is the second function of the kernel. Processes can request more memory (by asking the kernel for another 4KB page or a series of pages) and release unused pages. They can also share pages of memory with other processes (see below).
Scheduling is the third function of the kernel, to ensure we have pseudo-concurrent execution of tasks. The kernel controls which processes are allowed execution type on the processor(s). Processes have a priority based on their class; servers and applications will not get processing time if their is an awake driver, and applications will not get processing time if their is an awake server. Processes (drivers, servers, most applications, though excluding some applications like games) will spend the majority of their time sleeping unless they have a task to perform. All awake threads of the same class will get equal priority.
Inter process communication is the fourth function of the kernel and is important in a microkernel. There are three levels of IPC; messages, pipes, and shared memory.
Dynamic sized messages can be sent freely between processes. Example usage of messages:
- Tell the window manager to create a window.
- Request to initialise a pipe with another process.
Pipes are 4KB of memory that any process can join and write to or read from (the process will sleep if they try to read more than there is in the pipe or write more than there is available in the buffer). Example usage of pipes:
- Stream audio to the sound driver.
- Read a file stream from the VFS.
Shared memory is reference counted memory (divided into 4KB pages) that can exist in more than one process's virtual address space. Shared memory and pipes are random keys as well as their ID to somewhat delay a malicious process from eavesdropping where they shouldn't. Example usage of shared memory:
- Share the application's window's contents with the window manager and/or the graphics driver.
- Implement an application specific method of IPC over shared memory.
There are also "events", which are messages directly from the kernel. The process can get the next event (which can return NULL if nothing is waiting) or atomically sleep until an event is available and then get it (which servers, drivers, and event-driven applications will likely do). Examples of events:
- A message has been received (including the message's ID and size).
- A process has released a pipe or shared memory.
- An IRQ has fired (drivers only).
Though the kernel is designed to be independent of the rest of the operating system (in that it does not care how you implement any drivers or servers), there is exception to this:
- Kernel output (e.g. when a textual message that a process has been terminated because it caused a page fault) is sent as messages to "a\log". "a\log" can be implemented however it wants (printf the message in a text environment, pop up a window in a GUI environment). If "a\log" does not exist, the kernel will panic (it could display the message in text but this brings up the issue of the kernel being dependent on video drivers to switch back to a text-based mode, so the best option would be to send an event telling every process to save its state (if possible) then reboot after a time out).
If you are wondering how the initial processes are loaded (before there is a VFS server or disk drivers loaded), GRUB loads the most minimal requires processes as modules which are used to bootstrap the operating system. An example configuration would be a floppy or hard disk driver, the VFS server, and an initialise program (which handles detecting and loading other drivers and setting up a user environment). These modules are loaded with "driver" privileges, and should downgrade their privileges to an appropriate level.
I have attached my list of system calls (along with what registers they use), though you would likely call these from a high level C interface. For an applications programmer, all of this (system calls, communicating with servers, etc) will be wrapped behind a framework which exposes them as functions like fopen, printf, CreateWindow, OnClick, but that is beyond the scope of this post.
One thing I would like a suggestion on is loading processes. In my current system a process spawns another process by passing a pointer to an executable file in that process's memory. The alternative is that you pass the kernel a path, but I chose this method for these advantages;
- The new process's executable does not have to exist on the file system (it could be extracted from an archive, or streamed over a network).
- It's possible to wrap the ELF executable within a custom file container to provide resources (overcomes the disadvantage below).
The disadvantage of this method is:
- It requires more memory since the the complete ELF executable file must be already in memory of another processes to load it (it can not stream it from disk as it loads). This could be an issue for things like self-extracting archives which may be a 1GB+ executable, but as mentioned above, it is possible for the operating system built on top of this kernel to implement it's own executable file format with the ELF executable being a small subsection of this larger file (then once it is loaded the process can access the rest of it's resources).
- The kernel will be dependent on the VFS.
Processes can also only spawn other processes of an equal or lower class (Drivers->Servers->Applications). How the kernel is bootstrapped is mentioned above.
That is an overview of what my kernel does and is in charge of. Remember that this is a microkernel, and in a microkernel the "kernel" (what is described above) is only a very small part of an operating system. The operating system services like the file system, user management, device management (initialising devices, sorting them), input management, and window management is provided within servers, and their implementation is completely independent of the kernel. I have a large number of servers designed that will run on top of my kernel, as well as an interface for how devices and drivers will communicate, as well as how my application framework will fit in to this design. However, the topic of this thread is strictly the kernel, and I feel that this post has become long enough, and as to not bore potential readers (if I haven't bored you already) I won't begin to explain about the greater OS.
I would also like other people who are working on microkernel to provide an in depth discussion of their kernel, offer suggestions for mine, and share ideas.
It is a microkernel, so there aren't any drivers built in (except for a basic text console driver in the case of kernel panics), they all run in userspace and that is not part of this specification.
The first function of my microkernel is to handle processes, devices, and threads. Processes and devices both exist within their own name systems, they are not part of any VFS. The VFS server (which again, is not part of the microkernel) may decide to integrate these in with files (e.g. put processes under "/processes/" and devices under "/devices/").
Processes can belong to one of 3 classes; drivers, servers, applications. These are all ring 3 processes, but the former classes are able to perform more system calls than the latter classes (their special privileges will be described below).
Drivers (the process name will be prefixed with "d\") can access IO ports, terminate a process/thread belonging to another process, send processes to sleep/wake them up, map physical memory locations into their local space, create/destroy device objects (which are merely references to the driver processes, with a unique per-device ID), listen if an IRQ fires, and transform in to (and back out of) a VM86 task.
The purpose of a driver is to abstract away system devices into "device objects". "Device objects" are merely a name (e.g. "disks\floppy\0") with a the ID of the driver and the ID device, then you communicate with the driver (through a common interface yet to be determined, but will consist of functions like get the type of device, initialise the device, and more specific functions depending on the type of device) using the kernel's IPC system and passing the ID of the device as a parameter.
Drivers can enter/leave VM86 mode freely, though in VM86 mode they can only access the first megabyte of their virtual memory (but they can execute any VM86 instructions they want) and fire an interrupt to leave VM86 mode. Some drivers (e.g. VESA drivers) will take advantage of this so they can access the BIOS (though not too often as it can slow down the entire system).
Applications shouldn't access drivers directly, but they can (though the driver can tell the class of the process, so to make sure that some actions (like writing to a disk) can only be performed by a server). A full-screen game is an example of when an application can access a driver directly (to perform 3D operations, or have exclusive control over the screen and audio output).
Examples of drivers include;
- DMA
- Floppy disk
- Hard drive
- Sound card
- Keyboard
- Video card
Servers (the process name will be prefixed with a "s\") can terminate a process/thread belonging to another process, and send processes to sleep/wake them up. Servers provide the core operating system functions.
Example of servers:
- Window manager
- VFS
- Task manager (which acts more like an application that can be launched when needed, but is classed as a server because it has the ability to kill/pause other processes)
Servers are NOT the same as web servers, file servers, etc - in this OS they would run as applications.
Applications (the process name will be prefixed with an "a\") can only directly modify themselves. They can communicate with other processes (particularly with servers) through the kernel's IPC facilities. These make up the majority (hopefully, ALL) of the user's programs.
Examples of applications:
- Web browsers
- Word processors
- File managers
- Games
- Media players
Rather than dealing with IPC directly to access servers and drivers, they will provide an interface/library (e.g. instead of sending a message, creating a pipe, pass through a buffer of commands, you will have a C interface with functions like WindowManager_Initialise(), WindowManager_CreateWindow(int x, int y, char *name)), drivers though will have a standard interface/library that you can use to access ALL devices of particular type/subtype (e.g. a device library which lets you access GetType() which tells you it's a storage device, then you'd use the storage device library to call GetSize(), Read(), GetMedia(). If the media is a CD, then you can use another interface which defines Eject()).
Memory management is the second function of the kernel. Processes can request more memory (by asking the kernel for another 4KB page or a series of pages) and release unused pages. They can also share pages of memory with other processes (see below).
Scheduling is the third function of the kernel, to ensure we have pseudo-concurrent execution of tasks. The kernel controls which processes are allowed execution type on the processor(s). Processes have a priority based on their class; servers and applications will not get processing time if their is an awake driver, and applications will not get processing time if their is an awake server. Processes (drivers, servers, most applications, though excluding some applications like games) will spend the majority of their time sleeping unless they have a task to perform. All awake threads of the same class will get equal priority.
Inter process communication is the fourth function of the kernel and is important in a microkernel. There are three levels of IPC; messages, pipes, and shared memory.
Dynamic sized messages can be sent freely between processes. Example usage of messages:
- Tell the window manager to create a window.
- Request to initialise a pipe with another process.
Pipes are 4KB of memory that any process can join and write to or read from (the process will sleep if they try to read more than there is in the pipe or write more than there is available in the buffer). Example usage of pipes:
- Stream audio to the sound driver.
- Read a file stream from the VFS.
Shared memory is reference counted memory (divided into 4KB pages) that can exist in more than one process's virtual address space. Shared memory and pipes are random keys as well as their ID to somewhat delay a malicious process from eavesdropping where they shouldn't. Example usage of shared memory:
- Share the application's window's contents with the window manager and/or the graphics driver.
- Implement an application specific method of IPC over shared memory.
There are also "events", which are messages directly from the kernel. The process can get the next event (which can return NULL if nothing is waiting) or atomically sleep until an event is available and then get it (which servers, drivers, and event-driven applications will likely do). Examples of events:
- A message has been received (including the message's ID and size).
- A process has released a pipe or shared memory.
- An IRQ has fired (drivers only).
Though the kernel is designed to be independent of the rest of the operating system (in that it does not care how you implement any drivers or servers), there is exception to this:
- Kernel output (e.g. when a textual message that a process has been terminated because it caused a page fault) is sent as messages to "a\log". "a\log" can be implemented however it wants (printf the message in a text environment, pop up a window in a GUI environment). If "a\log" does not exist, the kernel will panic (it could display the message in text but this brings up the issue of the kernel being dependent on video drivers to switch back to a text-based mode, so the best option would be to send an event telling every process to save its state (if possible) then reboot after a time out).
If you are wondering how the initial processes are loaded (before there is a VFS server or disk drivers loaded), GRUB loads the most minimal requires processes as modules which are used to bootstrap the operating system. An example configuration would be a floppy or hard disk driver, the VFS server, and an initialise program (which handles detecting and loading other drivers and setting up a user environment). These modules are loaded with "driver" privileges, and should downgrade their privileges to an appropriate level.
I have attached my list of system calls (along with what registers they use), though you would likely call these from a high level C interface. For an applications programmer, all of this (system calls, communicating with servers, etc) will be wrapped behind a framework which exposes them as functions like fopen, printf, CreateWindow, OnClick, but that is beyond the scope of this post.
One thing I would like a suggestion on is loading processes. In my current system a process spawns another process by passing a pointer to an executable file in that process's memory. The alternative is that you pass the kernel a path, but I chose this method for these advantages;
- The new process's executable does not have to exist on the file system (it could be extracted from an archive, or streamed over a network).
- It's possible to wrap the ELF executable within a custom file container to provide resources (overcomes the disadvantage below).
The disadvantage of this method is:
- It requires more memory since the the complete ELF executable file must be already in memory of another processes to load it (it can not stream it from disk as it loads). This could be an issue for things like self-extracting archives which may be a 1GB+ executable, but as mentioned above, it is possible for the operating system built on top of this kernel to implement it's own executable file format with the ELF executable being a small subsection of this larger file (then once it is loaded the process can access the rest of it's resources).
- The kernel will be dependent on the VFS.
Processes can also only spawn other processes of an equal or lower class (Drivers->Servers->Applications). How the kernel is bootstrapped is mentioned above.
That is an overview of what my kernel does and is in charge of. Remember that this is a microkernel, and in a microkernel the "kernel" (what is described above) is only a very small part of an operating system. The operating system services like the file system, user management, device management (initialising devices, sorting them), input management, and window management is provided within servers, and their implementation is completely independent of the kernel. I have a large number of servers designed that will run on top of my kernel, as well as an interface for how devices and drivers will communicate, as well as how my application framework will fit in to this design. However, the topic of this thread is strictly the kernel, and I feel that this post has become long enough, and as to not bore potential readers (if I haven't bored you already) I won't begin to explain about the greater OS.
I would also like other people who are working on microkernel to provide an in depth discussion of their kernel, offer suggestions for mine, and share ideas.