很多人觉得操作系统内核和“面向对象”八竿子打不着。毕竟 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上下文模板一样。