My scheduler operations implementation A benefit of working on your own operating system is that you’re free from the usual "restraints" of collaboration and real applications. That has always been a major factor in my interest in osdev. You don’t have to worry about releasing your program, or about critical security vulnerabilities, or about hundreds of people having to maintain your code. A benefit of working on your own operating system is that you’re free from the usual "restraints" of collaboration and real applications. That has always been a major factor in my interest in osdev. You don’t have to worry about releasing your program, or about critical security vulnerabilities, or about hundreds of people having to maintain your code. In the OSDev world you’re usually alone, and that solitude gives you the freedom to experiment with unusual programming patterns. While developing a kernel module for my master’s thesis I came across an article on LWN: “ It was fascinating to see how something as low-level as the kernel can still borrow the benefits of object orientation "encapsulation", modularity, and extensibility. This lead me to experimenting with implementing all my kernels services with this approach. The basic idea is to have a "vtable" as a struct with function pointers. Describing the interface for the object. While developing a kernel module for my master’s thesis I came across an article on LWN: “ Object-oriented design patterns in the kernel. ” The article describes how the Linux kernel, despite being written in C, embraces object-oriented principles by using function pointers in structures to achieve polymorphism.It was fascinating to see how something as low-level as the kernel can still borrow the benefits of object orientation "encapsulation", modularity, and extensibility. This lead me to experimenting with implementing all my kernels services with this approach.The basic idea is to have a "vtable" as a struct with function pointers. Describing the interface for the object. /* "Interface" with function pointers */ struct device_ops { void (*start)(void); void (*stop)(void); }; The device in this case will hold a reference to this vtable. /* Device struct holding a pointer to its ops */ struct device { const char *name; const struct device_ops *ops; }; Different type of devices can now utilize the same 'api', while calling very different functions. struct device netdev = { "eth0", &net_ops }; struct device disk = { "sda", &disk_ops }; netdev.ops->start(); // net: up disk.ops->start(); // disk: spinning netdev.ops->stop(); // net: down disk.ops->stop(); // disk: stopped What makes vtables especially powerful is that they can be swapped out at runtime. The caller doesn’t need to change anything, once the vtable is updated, every call is automatically redirected to the new function. With proper synchronization, this provides a very clean way of evolving behavior on the fly. Examples from my OS I’ve used this pattern to implement the idea of “services” in my operating system. Services are the key kernel threads that keep the system going: the networking manager, worker pools, the window server, and so on. I wanted a consistent way to start, stop, and restart these threads interactively from the terminal, without having to hard-code special logic for each one. A service in my operating system consists of a set of operations, start, restart and stop and a PID referencing the running thread. The operations will vary a lot between the different types of services, but as the interface stays the same, the code interacting with the services will have it very easy. /* "Interface" with function pointers */ struct service_ops { void (*start)(void); void (*stop)(void); void (*restart)(void); }; struct service { pid_t pid; const struct service_ops *ops; ... }; Another place where I’ve leaned on vtables in my OS is the scheduler. There are many different strategies for scheduling processes, round robin, shortest job next, FIFO, priority scheduling, and so on. But the interface really only needs a handful of operations: yield, block, add, and next. By defining that interface through a vtable, I can swap out the entire scheduling policy at runtime without touching the rest of the kernel. Example from Linux: Another place where I’ve leaned on vtables in my OS is the scheduler. There are many different strategies for scheduling processes, round robin, shortest job next, FIFO, priority scheduling, and so on. But the interface really only needs a handful of operations: yield, block, add, and next. By defining that interface through a vtable, I can swap out the entire scheduling policy at runtime without touching the rest of the kernel.Example from Linux: include/linux/fs.h struct file_operations { struct module *owner; fop_flags_t fop_flags; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); ... } __randomize_layout; A classic example of this pattern is the file abstraction. Systems like Unix and Plan 9 embrace the philosophy that “everything is a file.” Whether you’re dealing with sockets, devices, or plain text files, they all expose the same simple interface: read and write. Behind the scenes the implementation varies enormously, but the uniform contract makes the complexity disappear. You can read from a pipe just as easily as you can read from a disk file, and user-space code doesn’t have to care which one it’s talking to. That consistency is what makes the abstraction so powerful. Combination with Kernel Modules This approach also pairs nicely with kernel modules. Much like how Linux modules work, custom drivers or hooks can be loaded dynamically in my system by replacing the vtables of certain structures. It’s a neat way to extend the kernel while it’s running, without recompiling or rebooting. Drawbacks Of course, there are drawbacks. The biggest one for me is the syntax. Invoking operations through these structs often ends up looking like: object->ops->start(object) Having to pass the object explicitly every time feels clunky, especially compared to C++ where this is implicit. The function signatures also become more verbose: static void object_start(struct object* this){ this->id = ... ... }