GitHub - vvaltchev/tilck: A Tiny Linux-Compatible Kernel
source link: https://github.com/vvaltchev/tilck
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Tilck (Tiny Linux-Compatible Kernel)
Contents
Overview
What is Tilck?
Tilck
is an educational monolithic x86 kernel designed to be Linux-compatible at
binary level. Project's small-scale and simple design makes it the perfect playground
for playing in kernel mode while retaining the ability to compare how the very same
usermode bits run on the Linux kernel as well. That's a unique feature in the
realm of educational kernels. Thanks to that, to build a program for Tilck it's enough to
use a i686-musl
toolchain from bootlin.com. Tilck
has no need to have its own set of custom written applications, like most educational
kernels do. It just runs mainstream Linux programs like the BusyBox suite.
While the Linux-compatibility and the monolithic design might seem a limitation from
the OS research point of view, on the other side, such design bring the whole project
much closer to real-world applications in the future, compared to the case where
some serious (or huge) effort is required to port pre-existing software on it. Also,
nothing stops Tilck from implementing custom non-Linux syscalls that aware apps might
take advantage of.
Future plans
In the long term, depending on how successful the project will be, Tilck
might
become suitable for embedded systems on which an extra-simple and fully deterministic
kernel is required or, at least, it is considered the optimal solution. With a fair
amount of luck, Tilck
might be able to fill the gap between Embedded Linux and
typical real-time operating systems like FreeRTOS or Zephyr. In any case, at some
point it will be ported to the ARM
family and it might be adapted to run on MMU-less
CPUs as well. Tilck would be a perfect fit for that because consuming a tiny amount of
RAM has always been a key point in Tilck's design. Indeed, the kernel can comfortably
boot and run on a i686 QEMU machine with just 4 MB of memory today. Of course, that's
pointless on x86, but on an ARM Cortex-R it won't be anymore the case.
What Tilck is NOT ?
An attempt to re-write and/or replace the Linux kernel. Tilck is a completely different kernel that has a partial compatibility with Linux just in order to take advantage of its programs and toolchains. Also, that helps a lot to validate its correctness: if a program works correctly on Linux, it must work the same way on Tilck as well (except for the not-implemented features). But, having a fair amount of Linux programs working on it, is just a starting point: after that, Tilck will evolve in a different way and it will have its own unique set of features as well. Tilck is fundamentally different from Linux in its design and its trade-offs as it does not aim to target multi-user server or desktop machines. Currently, it targets the educational world, while in the future it might target embedded systems or something else.
Features
Tilck is a preemptable monolithic (but with compile-time modules) *NIX kernel,
implementing about ~100 Linux syscalls (both via int 0x80
and sysenter
) on
x86. At its core, the kernel is not x86-centric even if it runs only on x86 at
the moment. Everything arch-specific is isolated. Because of that, most of
kernel's code can be already compiled for any architecture and can be used in
kernel's unit tests.
Hardware support
While the kernel uses a fair amount of legacy hardware like the 8259 PICs for IRQs, the legacy 8254 PIT for the system timer, the legacy 16550 UART for serial communication, the 8042 kb controller, the 8237 ISA DMA, and the Sound Blaster 16 sound card (QEMU only), it has also support for some recent hardware features like SSE, AVX and AVX2 fpu instructions, PAT, i686 sysenter, enumeration of PCI Express devices (via ECAM) and, above all, ACPI support via ACPICA. ACPI is currently used to receive power-button events, to reboot or power-off the machine, and to read the current parameters of machine's batteries (when implemented via ACPI control methods).
Comments about physical hardware
The operating system has been regularly tested on physical hardware from its inception by booting it with an USB stick (see the notes below). Test machines include actual i686 machines, older x86_64 machines with BIOS-only firmware, newer x86_64 machines with UEFI+CSM and finally super-recent pure-UEFI machines. For a long time, Tilck's development strictly complied with the following rule: if you cannot test it on real hardware, do not implement it in Tilck. Only recently, that rule has been relaxed a little in order to play with SB16. It is possible that, in the future, there might be a few other drivers that would be tested only on virtual machines: their development is justified by the educational value it will bring to the operating system and the infrastructure built for them will be reused for other drivers of the same kind. But that will never become a common practice. Tilck is designed to work on real hardware, where any kind of weird things happen. Being reliable there is critical for Tilck's success.
File systems
Tilck has a simple but full-featured (both soft and hard links, file holes, memory mapping, etc.) ramfs implementation, a minimalistic devfs implementation, read-only support for FAT16 and FAT32 (used for initrd) allowing memory-mapping of files, and a sysfs implementation used to provide a full view of ACPI's namespace, the list of all PCI(e) devices and Tilck's compile-time configuration. Clearly, in order to work with multiple file systems at once, Tilck has a simple VFS implementation as well.
Processes and signals
While Tilck uses internally the concept of thread, multi-threading is not currently
exposed to userspace (kernel threads exist, of course). Both fork()
and vfork()
are
properly implemented and copy-on-write is used for fork-ed processes. The waitpid()
syscall is fully implemented (which implies process groups etc.). The support for
POSIX signals is partial: custom signal handlers are supported using the rt_sigaction()
interface, but most of the SA_* flags are not supported and handlers cannot interrupt
each other, yet. rt_sigprocmask()
, sys_rt_sigpending()
, sys_rt_sigsuspend()
work as expected, as well as special signals like SIGSTOP, SIGCONT and SIGCHLD.
For more details, see the syscalls document.
One interesting feature in this area deserves a special mention: despite the lack of
multi-threading in userspace, Tilck has full support for TLS (thread-local storage) via
set_thread_area()
, because libmusl
requires it, even for classic single-threaded
processes.
In addition to the classic read()
and write()
syscalls, Tilck supports vectored I/O
via readv()
and writev()
as well. In addition to that, non blocking I/O, select()
and poll()
are supported too. Fortunately, no program so far needed epoll
:-)
Console
Tilck has a console supporting more than 90% of Linux's console's features. It works in the same way (using layers of abstraction) both in text mode and in framebuffer mode. The effort to implement such a powerful console was driven by the goal to make Vim work smoothly on Tilck, with syntax highlighting etc. While it's true that such a thing has a little to do with "proper" kernel development, being able to run a "beast" like Vim on a simple kernel like Tilck, is a great achievement by itself because it shows that Tilck can run correctly programs having a fair amount of complexity.
Userspace applications
Tilck can run a fair amount of console applications like the BusyBox suite, Vim, TinyCC, Micropython, Lua, and framebuffer applications like a port of DOOM for the Linux console called fbDOOM. Check project's wiki page for more info about that.
Screenshots
For full-size screenshots and much more stuff, check Tilck's wiki page.
Booting Tilck
Tilck's bootloader
Tilck
comes with an interactive bootloader working both on legacy BIOS and on
UEFI systems as well. The bootloader allows the user to choose the desired video
mode, the kernel file itself and to edit kernel's cmdline.
3rd-party bootloaders
Tilck
can be loaded by any bootloader supporting multiboot 1.0
. For example,
qemu's built-in bootloader works perfectly with Tilck
:
qemu-system-i386 -kernel ./build/tilck -initrd ./build/fatpart
Actually that way of booting the kernel is used in the system tests. A shortcut for it is:
./build/run_multiboot_qemu
Grub support
Tilck
can be easily booted with GRUB. Just edit your /etc/grub.d/40_custom
file (or create another one) by adding an entry like:
menuentry "Tilck" {
multiboot <PATH-TO-TILCK-BUILD-DIR>/tilck
module --nounzip <PATH-TO-TILCK-BUILD-DIR>/fatpart
boot
}
After that, just run update-grub
as root and reboot your machine.
Documentation and HOWTOs
Project's main documentation can be found in the docs/
directory. However,
Tilck's wiki can be used to
navigate through those documention files with the addition of much extra content
like screenshots. Here below, instead, there's a quick starter guide, focusing
on the most common scenarios.
Building Tilck
The project supports a fair amount of build configurations and customizations
but building using its default configuration can be described in just a few
steps. The only true requirement for building Tilck is having a Linux
x86_64 host system or Microsoft's WSL
. Steps:
- Enter project's root directory.
- Build the toolchain (just the first time) with:
./scripts/build_toolchain
- Compile the kernel and prepare the bootable image with:
make -j
At this point, there will be an image file named tilck.img
in the build
directory. The easiest way for actually trying Tilck
at that point is to run:
./build/run_qemu
.
Running it on physical hardware
The tilck.img
image is, of course, bootable on physical machines as well,
both on UEFI systems and on legacy ones. Just flush the image file with dd
to a usb stick and reboot your machine.
Other configurations
To learn much more about how to build and configure Tilck, check the building
guide in the docs/
directory.
Testing Tilck
Tilck has unit tests, kernel self-tests, system tests (using the syscall interface), and automated interactive system tests (simulating real user input through QEMU's monitor) all in the same repository, completely integrated with its build system. In addition to that, there's full code coverage support and useful scripts for generating HTML reports (see the coverage guide). Finally, Tilck is fully integrated with the Azure Pipelines CI, which validates each pushed branch with builds and test runs in a variety of configurations. Kernel's coverage data is also uploaded to CodeCov. Below, there are some basic instructions to run most of Tilck's tests. For the whole story, please read the testing document.
Running Tilck's tests
Running Tilck's tests is extremely simple: it just requires to have python 3
installed on the machine. For the self-tests and the classic
system tests, run:
<BUILD_DIR>/st/run_all_tests -c
To run the unit tests instead:
-
Install the googletest library (once) with:
./scripts/build_toolchain -s build_gtest
-
Build the unit tests with:
make -j gtests
-
Run them with:
<BUILD_DIR>/gtests
To learn much more about Tilck's tests in general and to understand how to run its interactive system tests as well, read the testing document.
Debugging Tilck
With QEMU
's integrated GDB
server, it's possible to debug the Tilck kernel
with GDB almost as if it were a regular process. It just gets tricky when
context switches happen, but GDB cannot help with that. To debug it with GDB,
follow the steps:
-
(Optional) Prepare a debug build of Tilck, for a better debugging experience.
-
Run Tilck's VM with:
./build/run_nokvm_qemu
but, remain at the bootloader stage. -
In a different terminal, run:
gdb ./build/tilck_unstripped
. -
In GDB, run:
target remote :1234
to connect to QEMU's gdb server. -
Set one or more breakpoints using commands like:
break kmain
. -
Type
c
to allow execution to continue and boot the OS by pressing ENTER in the bootloader.
In order to make the debugging experience better, Tilck comes with a set of
GDB scripts (see other/gdb_scripts
). With them, it's super-easy to list
all the tasks on the system, the handles currently opened by any given process
and more. In order to learn how to take advantage of those GDB scripts and anything
else related to debugging the Tilck project, check the debugging document.
Tilck's debug panel
Debugging Tilck with GDB while it's running inside a VM is very convenient, but
in other cases (e.g. Tilck on real hardware) we don't have GDB support. In
addition to that, even when the kernel is running inside a VM, there are some
features that are just much more convient to expose directly from the kernel
itself rather than through GDB scripts. One way to expose kernel info to
userspace is to use sysfs
, but that's not necessarily the most convenient way
for everything (still, Tilck does have sysfs implementation), especially when
interaction with the kernel itself is needed for debugging purposes. To help
in those cases, a debug panel has been introduced inside Tilck itself. It
started as something like Linux's Magic SysRq which evolved in a sort of TUI
application with debug info plus tracing capabilities for user processes. In the
future, it will support some proper debugging features as well. To learn more
about it, check the the debugging document.
A comment about user experience
Tilck particularly distinguishes itself from many open source projects in one
way: it really cares about the user experience (where "user" means
"developer"). It's not the typical super-cool low-level project that's insanely
complex to build and configure; it's not a project requiring 200 things to be
installed on the host machine. Building such projects may require hours or even
days of effort (think about special configurations e.g. building with a
cross-compiler). Tilck instead, has been designed to be trivial to build and
test even by inexperienced people with basic knowledge of Linux. It has a
sophisticated script for building its own toolchain that works on all the major
Linux distributions and a powerful CMake-based build system. The build of Tilck
produces an image ready to be tested with QEMU or written on a USB stick. (To
some degree, it's like what the buildroot
project does for Linux, but it's
much simpler.) Finally, the project includes also scripts for running Tilck
on QEMU with various configurations (BIOS boot, UEFI boot, direct (multi-)boot
with QEMU's -kernel
option, etc.).
Motivation
The reason for having the above mentioned features is to offer its users and
potential contributors a really nice experience, avoiding any kind of
frustration. Hopefully, even the most experienced engineers will enjoy a zero
effort experience. But it's not all about reducing the frustration. It's also
about not scaring students and junior developers who might be just curious to
see what this project is all about and maybe eager to write a simple program for
it and/or add a couple of printk()
's here and there in their fork. Hopefully,
some of those people just playing with Tilck might actually want to contribute
to its development.
In conclusion, even if some parts of the project itself are be pretty complex, at least building and running its tests must be something anyone can do.
FAQ (by vvaltchev)
Why Tilck does not have the feature/abstraction XYZ like other kernels do?
Tilck
is not meant to be a full-featured production kernel. Please, refer to
Linux (or other kernels) for that. The idea for the moment was just to implement an
educational kernel able to run a class of Linux console applications on real hardware.
At some point in the future and with a lot of luck, Tilck
might actually have a
chance to be used in production embedded environments (as mentioned above) but it will
still be by design limited in terms of features compared to Embedded Linux. Tilck
will always try to be different from Linux, simply because Linux is already great
per se and it does not make any sense trying to reimplement it. Instead, it's worth
trying to create something new while playing the "linux-compatibility card".
What I expect is Tilck
to start "stealing" ideas from hard real-time kernels,
once it gets ported to ARM and MMU-less CPUs. But today, the project is not there yet.
Why Tilck runs only on x86 (ia-32)?
Actually Tilck runs only on x86 for the moment. The kernel was born as a purely
educational project and the x86 architecture was already very friendly to me at
the time. Moving from x86 usermode assembly to "kernel" mode (real-mode and the
transitions back and forth to protected mode for the bootloader) required quite an
effort, but it still was, in my opinion, easier than "jumping" directly into a
completely unknown (for me) architecture, like ARM
. I've also considered writing
from the beginning a x86_64
kernel running completely in long mode
but I
decided to stick initially with the i686
architecture for the following reasons:
-
The
long mode
is, roughly, another "layer" added on the top of 32-bit protected mode: in order to have a full understanding of its complexity, I thought it was better to start first with its legacy. -
The
long mode
does not have a full support for segmentation, while I wanted to get confident with this technology as well. -
The
long mode
has a 4-level paging system, which is more complex to use that the classic 2-level paging supported byia-32
(it was better to start with something simpler). -
I never considered the idea of writing a kernel for desktop or server-class machines where supporting a huge amount of memory is a must. We already have Linux for that.
-
It seemed to me at the time, that more online "starters" documentation existed (like the articles on https://wiki.osdev.org/) for
i686
compared to any other architecture.
Said that, likely I'll make Tilck
to support also x86_64
and being able to run
in long mode
at some point but, given the long-term plans for it as a tiny kernel
for embedded systems, making it to run on ARM
machines has priority over supporting
x86_64
. Anyway, at this stage, making the kernel (mostly arch-independent code)
powerful enough has absolute priority over the support for any specific architecture.
x86
was just a pragmatic choice for its first archicture.
Why having support for FAT32?
The first reason for supporting FAT32 (and FAT16) was that a FAT partition is required
for booting with UEFI. Therefore, it was convienent at the time to store there also
all the rest of the "initrd" files (init, busybox etc.). After the boot, ramfs
is
mounted at root, while the FAT32 boot partition is mounted at /initrd. Part of the FAT32
code in the kernel is reused by the legacy bootloader in order to read the kernel
file from the boot partition. Today, the "initrd" files are NOT stored anymore in the
boot partition; there are two separate FAT partitions instead: bootpart
, a
small partition containing just the kernel file and the EFI bootloaders, and
fatpart
, a slightly bigger partition (depending on the configration) containing
the initial ramdisk files (e.g. busybox) instead. After the boot, fatpart
remains
mounted at /initrd
, while none of the contents of bootpart
are kept.
Why keeping the initrd mounted?
To minimize the peak in memory usage during boot. Consider the idea of having
a tgz
archive and having to extract all of its files in the root directory:
doing that will require, even for a short period of time, keeping both the archive
and all of its contents in memory. This is against Tilck's effort to reduce its
memory footprint as much as possible; part of project's goals is being able to
run on very limited systems.
Why using 3 spaces as indentation?
Long story. It all started after using that coding style for years at VMware. Initially, it looked pretty weird to me, but at some point I felt in love with the way code looked. I got convinced that 2 spaces are just not enough, while 4 spaces are somehow "too much". Therefore, when I started the project in 2016, I decided to stick with the indentation size I liked most, even if I knew that using 4 spaces would have been better for most people. Today, I'm not sure if that was the right decision, but I still like the way Tilck's code looks.
Why many commit messages are so short?
It is well-known that all popular open source projects care about having good
commit messages and nice git history. It is an investment that at some point
pays off. A few years ago, I even wrote a blog post about that. The problem is
that such investment actually starts paying off only when multiple people
contribute to a project or the project is really mature enough. It took a
long time for me to start considering Tilck as kind of mature. Actually,
that's not even a binary value, it's a slow process instead: with time (and
commits!), the project matured even if it still has a long way to go. Therefore,
by looking at the commits from the initial one to today, it's possible to
observe how they improved, both from the message point of view and from the
content point of view as well. In particular, during the last ~1,000 commits I
started not only re-ordering commits but to split, edit, and squash them all the
time. Git's add -p
became a friend too. That's because today Tilck is pretty
stable and it starts to be a medium-sized project with its 97,000+ physical
lines of code and a git history of 5,000+ commits. It deserves much more effort
on each commit, compared to the past.
At the beginning, Tilck was just a small experimental and unstable project on
which I worked alone in my free time. It had even a different name,
ExperimentOS
. Its source was also subject to drastic changes very often and I
didn't have a clear roadmap for it either. It was an overkill to spend so much
effort on each commit message as if I were preparing it for the Linux kernel.
Tilck is still obviously not Linux so, don't expect to see 30+ lines of
description for EVERY commit message from now on, BUT, the quality is raising
through a gradual process and that's pretty natural. As other people start to
contribute to the project, we all will have to raise further the bar in order to
the collaboration to succeed and being able to understand each other's code
faster. Today, the project still doesn't have regular contributors other than
myself and that's why many commits still have short commit messages.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK