C语言面向对象设计模式与内核开发
Object-oriented design patterns in C and kernel development

原始链接: https://oshub.org/projects/retros-32/posts/object-oriented-design-patterns-in-osdev

## 用于灵活内核设计的虚函数表 开发个人操作系统允许在不受协作限制的情况下进行实验。受到 LWN 上一篇关于 Linux 内核中面向对象模式的文章启发,作者实现了一种“虚函数表”(vtable)方法——使用包含函数指针的结构体——在内核中实现多态性和灵活性。 这涉及通过 vtable 定义一个通用接口(例如 `start`、`stop`),然后让不同的“对象”(设备、服务、调度器)持有指向该接口特定实现的指针。这允许对各种组件进行一致的交互。至关重要的是,vtable 可以在运行时交换,从而实现动态行为更改,而无需修改代码。 作者将其用于内核服务(网络、窗口服务器)和调度器,允许运行时策略更改。这反映了 Unix 中的“一切皆文件”抽象,尽管实现各不相同,但仍提供统一的接口。这种方法也与内核模块很好地集成,从而实现动态扩展。 虽然语法可能冗长(需要显式对象传递),但它可以提高代码清晰度和依赖项管理。最终,vtable 提供了一种强大的方法,用于构建灵活且可扩展的 C 语言内核。

## Hacker News 讨论:C 中的面向对象设计与内核开发 一篇 Hacker News 讨论围绕一篇文章展开,该文章详细介绍了 Linux 内核如何利用结构体内的函数指针来实现多态性,从而在用 C 编写的情况下有效地实现面向对象原则。 核心争论在于这种技术*是否*是面向对象编程 (OOP),或者是一种前身——抽象数据类型 (ADT)。 许多评论者认为它早于并启发了 OOP,与 OOP 需要实现或复杂的继承结构相比,它通过 NULL 检查提供了更灵活的可选函数。 另一些人则指出 Smalltalk 和 Objective-C 等语言,它们在运行时消息分发方面提供了类似动态行为。 对话还涉及 OOP 的演变,对 C++ 和 Java 的类中心方法进行了批评,并对开发者未能探索多样化的实现方法表示遗憾。 提到了诸如“plexes”(早期的 ADT)之类的历史概念,以及 NeXTSTEP 的 PDO 用于微服务等技术,说明了这些模式的悠久历史。 最终,讨论强调了 ADT 和 OOP 之间微妙的关系,以及根据语言和上下文利用不同方法的优势。
相关文章

原文
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.

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: 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.
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: 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.
DrawbacksOf 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 = ...
    ...
}

That said, I’ve come to see a benefit here as well. By requiring this explicitly, the function’s dependencies are clearer! It’s obvious which context is being operated on. It makes the coupling between the object and its operations more transparent, which in kernel code is often a good trade-off.

In the end, vtables give me a simple way to keep my kernel code flexible without adding a lot of extra complexity. They let me swap out behavior at runtime, keep a consistent interface across very different subsystems, and add new features without rewriting everything.

Most importantly, it has made me use C in a new way and given me a playground for experimentation, and that’s what makes OS development so much fun!

Further reading:
The xine project discusses an interesting approach to private variables with the same vtable approach as discussed here.

联系我们 contact @ memedata.com