Chinaunix首页 | 论坛 | 博客
  • 博客访问: 468458
  • 博文数量: 150
  • 博客积分: 2706
  • 博客等级: 少校
  • 技术积分: 1200
  • 用 户 组: 普通用户
  • 注册时间: 2009-11-09 11:41
文章分类

全部博文(150)

文章存档

2012年(7)

2011年(6)

2010年(68)

2009年(69)

我的朋友

分类: LINUX

2009-11-28 12:40:24

 
相对于用户空间内应用程序的开发,内核开发有很大的不同。这种差异给开发内核带来了了挑战,但这并不意味着开发内核就比开发应用程序难多少。
这种差异使内核成了一只性格迥异的猛兽。一些常用的准则被颠覆了,而又必须建立许多全新的准则。尽管有许多差异一目了然(人人都知道内核可以做它想做的任何事),但还是有一些差异晦暗不明。最重要的差异包括以下几种:

* 内核编程时不能访问C库。
* 内核编程时必须使用GNU C。
* 内核编程时缺乏像用户空间那样的内存保护机制。
* 内核编程时浮点数很难使用。
* 内核只有一个很小的定长堆栈。
* 由于内核支持异步中断、抢占和SMP,因此必须时刻注意同步和并发。
* 要考虑可移植性的重要性。
让我们仔细考察一下这些要点,所有这些东西在内核开发中必须时刻牢记。
1   没有libc库
与用户空间的应用程序不同,内核不能链接使用标准C函数库(其他的那些库也不行)。造成这种情况的原因有许多,其中就包括先有鸡还是先有蛋这个驳论。不过最主要的原因还是在于速度和大小。对内核来说,完整的C库太大了—即便是从中抽取一个合适的子集—大小和效率都不能被接受。
别着急,大部分常用的C库函数在内核中都已经得到实现了。比如说操作字符串的函数组就位于lib/string.c文件中。只要包含头文件,就可以使用它们。
头文件
当我们在这里谈及头文件时,都指的是组成内核源代码树的内核头文件。内核源代码文件不能包含外部头文件,就像它们不能用外部库一样。


在所有没有实现的函数中,最著名的就数printf()函数了。内核代码虽然无法调用printf(),但它可以调用printk()函数。printk()函数负责把格式化好的字符串拷贝到内核日志缓冲区上,这样,syslog程序就可以通过读取该缓冲区来获取内核信息。printk()的用法很像printf():
printk("Hello world! A string: %s and an integer: %d\n", a_string, an_integer);
printk()和printf()之间的一个显著区别在于printk()允许你通过指定一个标志来设置优先级。Syslog会根据这个优先级标志来决定在什么地方显示这条系统消息。下面是一个使用这种优先级标志的例子:
printk(KERN_ERR "this is an error!\n");
2   GNU C
像所有自视清高的Unix内核一样,Linux内核是用C语言编写的。让人略感惊讶的是,内核并不完全符合ANSI C标准。实际上,只要有可能,内核开发者总是要用到gcc提供的许多语言扩展部分。(gcc是多种GNU编译器的集合,它包含的C编译器既可以编译内核,也可以编译Linux系统上用C写的其他代码。)
内核开发者使用的C语言涵盖了ISO C995标准和GNU C扩展特性。这其中的种种变化把Linux内核推向了gcc的怀抱。尽管目前出现了一些新的编译器如Intel C,已经支持了足够多的gcc扩展特性,完全可以用来编译Linux内核了。Linux内核用到的ISO C995标准的扩展没有什么特别之处,而且C99[1]作为C语言官方标准的修订本,不可能有大的或是激进的变化。让人感兴趣的是,多数GUN C与标准C有区别,而通常人们也不熟悉的这些差异。下面我们给出内核代码中所使用到的C语言扩展中,大家会感兴趣的部分吧。
2.1. 内联(inline)函数
GNU的C编译器支持内联函数。Inline这个名称(inline翻译成内联似乎并不贴切,直译应该是“在字里行间展开”的意思,不过约定俗成,这里我们也把它翻译成“内联”)就可以反映出它的工作方式,函数会在它所调用的位置上展开。这么做可以消除函数调用和返回所带来的开销(寄存器存储和恢复),而且,由于编译器会把调用函数的代码和函数本身放在一起进行优化,所以也有进一步优化代码的可能。不过,这么做是有代价的(天下没有免费的午餐),代码会变长,这也就意味着占用更多的内存空间或者占用更多的指令缓存。内核开发者通常把那些对时间要求比较高,而本身长度又比较短的函数定义成内联函数。如果你把一个大块头的程序做成了内联函数,却并不需要争分夺秒,反而反复调用它,想想是否值得?
定义一个内联函数的时候,需要使用static作为关键字,并且用inline限定它。比如:
static inline void dog(unsigned long tail_size)
内联函数必须在使用之前就定义好,否则编译器就没法把这个函数展开。实践中一般在头文件中定义内联函数。由于使用了static作为关键字进行限制,所以编译时不会为内联函数单独建立一个函数体。如果一个内联函数仅仅在某个源文件中使用,那么也可以把它定义在该文件开始的地方。
在内核中,为了类型安全起见,优先使用内联函数而不是复杂的宏。
2.2 内联汇编
gcc编译器支持在C函数中嵌入汇编指令。当然,在内核编程的时候,只有知道对应的体系结构,才能使用这个功能。
Linux的内核混合使用了C和汇编语言。在偏近体系结构的底层或对执行时间要求严格的地方,一般使用的是汇编语言。而内核其他部分的大部分代码是C语言写的。
3. 分支声明
对于条件选择语句,gcc内建了一条指令用于优化,在一个条件经常出现,或者该条件很少出现的时候,编译器可以根据这条指令对条件分支选择进行优化。内核把这条指令封装成了宏,比如likely()和unlikely(),这样使用起来比较方便。
例如,下面是一个条件选择语句:
if (foo) {
    /* .. */
}
如果想要把这个选择标记成绝少发生的分支:
/* 我们认为foo绝大多数时间都会为0.. */
if (unlikely(foo)) {
    /* .. */
}
相反,如果我们想把一个分支标记为通常为真的选择:
/* 我们认为foo通常都不会为0 */
if  (likely(foo)) {
      /* .. */
}
在你想要对某个条件选择语句进行优化之前,你一定要搞清楚其中是不是存在这么一个条件,在绝大多数情况下都会成立。这点十分重要:如果你的判断正确,确实是这个条件占压倒性的地位,那么性能会得到提升,如果你搞错了,性能反而会下降。在对一些错误条件进行判断的时候,常常用到unlikely()和likely()。你可以猜到,因为if语句往往判断一种特殊情况,因此unlikely()在内核中得到广泛使用。
3.   没有内存保护机制
如果一个用户程序试图进行一次非法的内存访问,内核会发现这个错误,发送 SIGSEGV,并结束整个进程。然而,如果是内核自己非法访问了内存,那后果就很难控制了。(毕竟,有谁能照顾内核呢?)内核中发生的内存错误会导致oops,这是内核中出现的最常见的一类错误。在内核中,你不应该去访问非法的内存地址,引用空指针之类的事情,否则它可能会死掉,却根本不知会通知你一声—在内核里,风险常常会比外面大一些。
此外,内核中的内存都不分页。也就是说,你每用掉一个字节,物理内存就减少一个字节。所以,在你想往内核里加入什么新功能的时候,要记住这一点。
4.   不要轻易在内核中使用浮点数
在用户空间的进程内进行浮点操作的时候,内核会完成从整数操作到浮点数操作的模式转换。在执行浮点指令时到底会做些什么,因体系结构不同,内核的选择也不同,但是,内核通常捕获陷阱并做相应处理。
和用户空间进程不同,内核并不能完美地支持浮点操作,因为它本身不能陷入。在内核中使用浮点数时,除了要人工保存和恢复浮点寄存器,还有其他一些琐碎的事情要做。如果要直截了当的回答,那就是:别这么做了,不要在内核中使用浮点数。
5.   容积小而固定的栈
用户空间的程序可以从栈上分配大量的空间来存放变量,甚至巨大的结构体或者是包含许多数据项的数组都没有问题。之所以可以这么做,是因为用户空间的栈本身比较大,而且还能动态的增长.
内核栈的准确大小随体系结构而变。在x86上,栈的大小在编译时配置,可以是4KB也可以是8KB。从历史上说,内核栈的大小是两页,这就意味着,32位机的内核栈是8KB,而64位机是16KB,这是固定不变的。每个处理器都有自己的栈。
6   同步和并发
内核很容易产生竞争条件。和单线程的用户空间程序不同,内核的许多特性都要求能够并发的访问共享数据,这就要求有同步机制保证不出现竞争条件,特别是:
* Linux是抢占多任务操作系统。内核的进程调度程序即兴对进程进行调度和重新调度。内核必须对这些任务同步。
* Linux内核支持多处理器系统。所以,如果没有适当的保护,在两个或两个以上的处理器上运行的代码很可能会同时访问共享的同一个资源。
* 中断是异步到来的,完全不顾及当前正在执行的代码。也就是说,如果不加以适当的保护,中断完全有可能在代码访问共享资源的当间到来,这样,中段处理程序就有可能访问同一资源。
* Linux内核可以抢占。所以,如果不加以适当的保护,内核中一段正在执行代码可能会被另外一段代码抢占,从而有可能导致几段代码同时访问相同的资源。
常用的解决竞争的办法是自旋锁和信号量。

7.   可移植性的重要性
尽管用户空间的应用程序不太注意移植问题,然而Linux却是一个可移植的操作系统,并且要一直保持这种特点。也就是说,大部分C代码应该与体系结构无关,在许多不同体系结构的计算机上都能够编译和执行,因此,必须把体系结构相关的代码从内核代码树的特定目录中适当地分离出来。诸如保持字节序、64位对齐、不假定字长和页面长度等一系列准则都有助于移植性。
8 小结
内核的确是一头独一无二的猛兽:没有内存保护,没有靠得住的libc,小的堆栈,庞大的源码树。Linux内核遵循它自己的游戏规则,以大人物的架势运行,运行足够长的时间后才停止,打破了我们惯以为常的习俗。尽管如此,内核不外乎就是一个程序,它与我们司空见惯的程序没有多大区别。不必望而生畏:直面它、呼唤它、摆布它。  
意识到内核并不像咋看起来那样使人畏惧,这就是良好的开端。不过,要梦想成真,你必须全身心地投入,阅读源码、剖析源码,并毫不气馁。

 

摘自我们翻译的《Linux kernel development》一书。



[1]ISO C99ISO C的最新修订版。C99相对于前一个修订版作了许多加强,ISO C90引入了命名结构体初始化和complex数据类型。后者不能在内核内安全地使用。

阅读(702) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~