相对于用户空间内应用程序的开发,内核开发有一些独特之处。尽管这些差异并不会使开发内核代码的难度超过开发用户代码,但它们依然有很大不同。最重要的差异包括以下几种 :
内核编程时既不能访问 C 库也不能访问准的C头文件
内核编程时必须使用 GNU C。
内核编程时缺乏像用户空间那样的内存保护机制·内核编程时难以执行浮点运算。
内核给每个进程只有一个很小的定长堆栈
由于内核支持异步中断、抢占和 SMP,因此必须时刻注意同步和并发·要考虑可移植性的重要性。
让我们仔细考察一下这些要点,所有内核开发者必须牢记以上要点。
无libc库抑或无标准头文件
与用户空间的应用程序不同,内核不能链接使用标准 C 函数库一一或者其他的那些库也不行。造成这种情况的原因有许多,其中就包括先有鸡还是先有蛋这个悖论。不过最主要的原因还是速度和大小。对内核来说,完整的 C库一一哪怕是它的一个子集,都太大且太低效了。
大部分常用的 C库函数在内核中都已经得到了实现。比如操作字符串的函数组就位于 lib/string.c 文件中。只要包含 <linux/string.h> 头文件,就可以使用它们。
当谈及头文件时,都指的是组成内核源代码树的内核头文件。内核源代码文件不能包含外部头文件,就像它们不能用外部库一样。基本的头文件位于内核源代码树顶级目录下的include 目录中。例如,头文件 <linuxinotify.h>对应内核源代码树的include/linux/inotify.h。体系结构相关的头文件集位于内核源代码树的arch//include/asm目录下。例如,如果编译的是x86体系结构,则体系结构相关的头文件就是arch/x86/include/asm。内核代码通过以asm/为前缀的方式包含这些头文件,例如<asm/ioctl.h>。
在所有没有实现的函数中,最著名的就数 printf()函数了。内核代码虽然无法调用 printf()但它提供的 printk()函数几乎与 printf()同printk()函负责把格化好的符贝到内核日志缓冲区上,这样,syslog 程序就可以通过读取该缓冲区来获取内核信息。printk()的用法很像 printf0() :
printk("Hello world! A string:'s' and an integer;'d'n",str,i);
printk0和 printf0之间的一个显著区别在于,printk0允许你通过指定一个标志来设置优先级。syslogd 会根据这个优先级标志来决定在什么地方显示这条系统消息。下面是一个使用这种优先级标志的例子:
printk(KERN ERR "this is an error!\n");
GNU C
像所有自视清高的 Unix 内核一样,Linux 内核是用C语言编写的。让人略感惊讶的是,内核并不完全符合 ANSIC标准。实际上,只要有可能,内核开发者总是要用到 gcc 提供的许多语言的扩展部分。(gcc 是多种 GNU 编译器的集合,它包含的 C 编译器既可以编译内核,也可以编译 Linux 系统上用C语言写的其他代码。)
没有内存保护机制
如果一个用户程序试图进行一次非法的内存访问,内核就会发现这个错误,发送 SIGSEGV信号,并结束整个进程。然而,如果是内核自已非法访问了内存,那后果就很难控制了。(毕竟有谁能照顾内核呢?)内核中发生的内存错误会导致 ps,这是内核中出现的最常见的一类错误。在内核中,不应该去做访向非法的内存地址,引用空指针之类的事情,否则它可能会死掉,却根本不告诉你一声,在内核里,风险常常会比外面大一些。此外,内核中的内存都不分页。也就是说,你每用掉一个字节,物理内存就减少一个字节所以,在你想往内核里加入什么新功能的时候,要记住这一点。
不要轻易在内核中使用浮点数
在用户空间的进程内进行浮点操作的时候,内核会完成从整数操作到浮点数操作的模式转换。在执行浮点指令时到底会做些什么,因体系结构不同,内核的选择也不同,但是,内核通常捕获陷阱并着手于整数到浮点方式的转变。
与用户空间进程不同,内核并不能完美地支持浮点操作,因为它本身不能陷入。在内核中使用浮点数时,除了要人工保存和恢复浮点寄存器,还有其他一些琐碎的事情要做。如果要直截了当地回答,那就是:别这么做了,除了一些极少的情况,不要在内核中使用浮点操作。
容积小而定的栈
用户空间的程序可以从栈上分配大量的空间来存放变量,甚至巨大的结构体或者是包含数以千计的数据项的数组都没有问题。之所以可以这么做,是因为用户空间的栈本身比较大,而且还能动态地增长(年长的开发者回想一下 DOS 那个代,这种低的作系统使在用户空间只有固定大小的栈)。内核栈的准确大小随体系结构而变。在 x86 上,的大小在编译时配置,可以是 4KB 也可以是 8KB。从历史上说,内核栈的大小是两页,这就意味着,32 位机的内核是8KB,而64位机是 16KB,这是固定不变的。每个处理器都有自己的栈。
同步和并发
内核很容易产生竞争条件。和单线程的用户空间程序不同,内核的许多特性都要求能够并发地访问共享数据,这就要求有同步机制以保证不出现竞争条件,特别是:
Linux是抢占多任务操作系统。内核的进程调度程序即兴对进程进行调度和重新调度。内核必须和这些任务同步。
Linux 内核支持对称多处理器系统(SMP)。所以,如果没有适当的保护,同时在两个或两个以上的处理器上执行的内核代码很可能会同时访问共享的同一个资源。
中断是异步到来的,完全不顾及当前正在执行的代码。也就是说,如果不加以适当的保护,中断完全有可能在代码访问资源的时候到来,这样,中段处理程序就有可能访向同一资源。
Linux 内核可以抢占。所以,如果不加以适当的保护,内核中一段正在执行的代码可能会被另外一段代码抢占,从而有可能导致几段代码同时访问相同的资源。常用的解决竞争的办法是自旋锁和信号量。我们将在后面的章节中详细讨论同步和并发执行
可移植性的重要性
尽管用户空间的应用程序不太注意移植问题,然而 Linux 却是一个可移植的操作系统,并且要一直保持这种特点。也就是说,大部分 C代码应该与体系结构无关,在许多不同体系结构的计算机上都能够编译和执行,因此,必须把与体系结构相关的代码从内核代码树的特定目录中适当地分离出来。
诸如保持字节序、64 位对齐、不假定字长和页面长度等一系列准则都有助于移植性。
以上内容来自《Linux内核设计与实现》
下载电子书可向公众号发送”电子书“来获取相关pdf书籍。