在内核里玩面向对象:如何用vtable改造操作系统


很多人觉得操作系统内核和“面向对象”八竿子打不着。毕竟 C 语言里没有 class、没有继承、多态要靠硬功夫实现,更别提那些“封装”和“接口抽象”的概念了。
但事实是:只要你敢折腾,内核代码也可以写出“对象的味道”。

我自己做 OS 开发主要是为了兴趣和研究,不像商业软件那样要担心上线、要顾虑安全漏洞、要考虑几百个维护者的意见。一个人捣鼓最大的好处就是:想怎么玩就怎么玩。

在这种自由度下,我尝试用 C 语言模拟出面向对象的设计模式,最后发现——它真的很好用。



灵感来自 Linux 内核

有一次我在做硕士论文时看到 LWN 上的一篇文章,讲的是《Object-oriented design patterns in the kernel》。
它提到 Linux 虽然是纯 C 写的,但广泛用到了“函数指针表”这个技巧来模拟多态。

换句话说,Linux 内核也在用一种类似 C++ 虚函数表(vtable)的机制:把函数指针装进结构体里,当成对象的“接口”,而实现细节则交给不同的模块去填充。

举个简单例子:

c
struct device_ops {
    void (*start)(void);
    void (*stop)(void);
};

struct device {
    const char *name;
    const struct device_ops *ops;
};

网卡和磁盘看起来完全不同,但它们都能用相同的 API:

c
struct device netdev = { "eth0", &net_ops };
struct device disk   = { "sda",  &disk_ops };

netdev.ops->start();
disk.ops->start();

netdev.ops->stop();
disk.ops->stop();

你看,调用方根本不用关心它是网卡还是硬盘,只要“能 start、能 stop”就行。



为什么 vtable 这么香?

vtable 的最大好处是:它能在运行时被替换。
也就是说,你不需要改任何调用代码,只要换一张函数表,所有行为就会立刻切换。

在内核里,这意味着什么?
意味着调度策略可以热插拔,驱动可以动态替换,服务逻辑可以热更新。
就好像系统有一层“插槽”,我想试试新策略,就把旧表拔掉,插上新表。

当然,前提是你要处理好同步问题,不然并发条件下可能炸锅。



我的 OS 实践:服务抽象

我给自己的操作系统搞了一个统一的“服务”抽象。
这里的服务不是 nginx 这种,而是内核里那些关键线程:网络管理器、线程池、窗口服务器等等。

我希望在终端里能统一地 start / stop / restart 这些服务,而不是每次都去写一堆 if/else。

于是我定义了一个接口:

c
struct service_ops {
    void (*start)(void);
    void (*stop)(void);
    void (*restart)(void);
};

struct service {
    pid_t pid;
    const struct service_ops *ops;
};

不管服务内部逻辑有多复杂,上层管理都可以一视同仁。
这让我在做交互式调试时特别舒服:输入一个命令,就能优雅地操作各种服务,而不是满屏特例代码。



调度器:策略即插件

另一个收获巨大的地方就是调度器。
调度算法千奇百怪:轮转、最短作业优先、FIFO、优先级调度……
但它们的“接口”其实很小:

* yield
* block
* add
* next

所以我干脆也用 vtable:

c
struct sched_ops {
    void (*yield)(void);
    void (*block)(void);
    void (*add)(struct task *);
    struct task* (*next)(void);
};

结果就是:我可以在不重启、不重编译的情况下,随时切换整个调度策略。
今天想玩轮转,明天想试优先级调度,换一张函数表就搞定。

这感觉就像在玩“调度插件”,非常解耦。



Linux 的经典案例:文件操作

其实这不是我瞎折腾,Linux 内核里早就有成熟的例子:

c
struct file_operations {
    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 *);
    ...
};

这就是为什么 Unix 世界里能说“万物皆文件”。
无论是磁盘、管道、套接字还是字符设备,它们都暴露相同的 read / write 接口。
至于背后实现千差万别,调用方根本不用管。

这种一致性,才是真正的威力所在。



内核模块的天然搭档

当你把 vtable 和“可加载模块”结合起来,就会发现它们天生合拍。
新驱动、钩子、扩展功能,统统可以通过替换 vtable 注入到运行中的系统里。

这意味着什么?
意味着你可以“边跑边换脑子”,不用停机、不用重启,直接把新逻辑切进去。
对一个操作系统开发者来说,这是最酷的魔法之一。



缺点:语法有点丑

当然,凡事有代价。
C 写 vtable 最大的槽点就是:

c
object->ops->start(object);

既要链式访问 ops,又要手动传 this,看起来既冗长又别扭。
而在 C++ 里,这一切都能优雅地写成 object.start()

更麻烦的是函数签名:

c
static void object_start(struct object* this){
    ...
}

这种写法又长又累赘。



但缺点也是优点

后来我发现,这种“丑陋”其实也有好处:
因为每个函数都显式地传入对象,你很清楚它依赖什么上下文。
耦合关系不再藏在语法糖里,而是摊开给你看。

在内核代码这种极度强调清晰和安全的地方,这反而是一种优势。



总结

玩 vtable 的过程,让我在 OS 开发中获得了前所未有的自由:

* 行为可以在运行时替换
* 接口保持一致,子系统更解耦
* 增加新特性不需要大改旧代码
* 能够像玩积木一样组合与试验

更重要的是:我用 C 玩出了“面向对象”的味道,甚至比 C++ 更透明、更轻量。
这正是操作系统开发最迷人的地方:它是一个自由的实验场。

如果你也对内核设计感兴趣,建议试着用这种方式思考,或许你会发现新的乐趣。



极客辣评
只要与Context上下文具体情况有关的,都可以做成模板,适合一类情况的模板,这就是面向对象! 面向对象是Context的模板,正如高聚合成的对象是DDD上下文模板一样。