内核开发不是洪水猛兽。一旦你了解到其中的规则,你就会发现,跟开发应用程序一样;两者区别在于要遵守的规则集合不一样。
Linux是UNIX家族的一员,而且其内核源代码唾手可得,因此这里用其来作说明。
规则上,与应用程序(运行于用户空间)的开发不同,主要表现在:
没有C库
用GNU C编程(对于Linux内核而言)
没有内存保护
在内核中很难使用浮点数
内核栈大小固定且很小
由于异步中断、抢占以及支持SMP,需要额外小心同步和并发
移植性问题
下面就来逐个解释。
内核没有链接任何C库。这里面涉及到很多问题,比如鸡生蛋还是蛋生鸡的问题:因为C库都会包裹一些系统调用,可是没有内核时就没有系统调用,那么……呵呵,明白了吧?另一个问题就是大小问题。任何一个C库,甚至其一个子集,对于内核来说都太大了。不过,不要着急,很多常用的库函数在内核中都有实现。
这里面涉及到一个著名的函数:printf(),内核提供了一个替代品:printk()。如果你要做内核开发,就会频繁使用该函数。记住:Linus本人不允许在内核中嵌入调试器(这是另外的话题,有兴趣的可以自己去google一下),因此很多情况下要依靠printk()。
毫无疑问,Linux的内核是用C语言写的。但所用的C并不是ANSI C,而是经过GNU扩展之后的C,这就是为什么Linux内核对于gcc编译器的依赖程度如此之高。GNU对C的扩展中就包括:内联函数(inline functions),分支预测和内联汇编(inline assembly)。分支预测用于判断哪些情况是几乎永远不可能发生的,或者哪些情况几乎永远都会发生----unlikely()和likely()。
当用户空间的代码访问非法地址时,内核能够捕获该错误,然后向进程发送SIGSEGV并终止进程。在UNIX世界,人们总是说kill/杀掉进程,其实,kill仅仅是用来向进程发送信号的,并不是杀掉它----太残忍了。这是题外话了,呵呵。那么,当内核代码访问非法地址时,谁来照顾内核呢?只能自己照顾自己了。非法的地址访问将导致oops,这是重大的问题,没人会告诉你访问了非法地址,但是你可以通过日志来查询/调试。
另外,内核内存是不分页,因此你没申请一个字节,物理内存就少掉一个字节。小心了!
在内核中使用浮点数非常困难,如果你想给自己找麻烦的话,可以试试。用户空间代码要使用浮点数指令时,一般来说会产生一个中断,内核捕获该中断并作相应的处理。然而,内核没法捕获自己。而且,要使用浮点指令,不但要保存浮点寄存器,还要做很多繁琐的事情----光看看内核如何为用户空间代码使用浮点数就知道了,可以参考进程调度里的上下文切换。
用户空间的程序可以在栈上申请大量的空间----定义足够多的局部变量,因为用户空间的栈非常大,而且可以动态增长。不过有些不够智能的系统做不到动态增长。然而,内核的栈非常小,而且无法动态增长。
作为一个抢占式多任务、支持对称多处理(SMP)的系统,同步和并发是任何一个内核hacker都需要时时刻刻小心的问题。调度器“兴之所至”,调度进程,这就需要同步;加上来自CPU外的各种中断导致内核需要对某些代码或数据加以保护。而抢占的意思就是,无论谁占用了CPU,都有可能被其他进程抢掉,内核也不例外。Linux对于竞态条件提供了spinlock(自旋锁)和semaphore(信号量)。
最后就是移植性问题。这个问题从来都不见简单,而且linux的目标是多种平台都能运行,因此移植性显得更为重要。字节序问题就是一个典型的移植性问题。然而,应用程序可能仅仅为一个平台开发。
看了这些,是不是觉得,其实内核开发也不难呢?
有雄心的hacker们,不要被OS kernel这个名词吓倒。看看Linux Kernel Development,准备好Linux内核源码,沏上一杯茶,然后你就可以开发内核了!
参考:
Linux Kernel Development, 2nd edition, by Robert Love
Copyleft (C) 2007 raof01. 本文可以用于除商业用途外的所有用途。若要用于商业用途,请与作者联系。