Pennant的日常
分享工作上的点点滴滴

Linux内核开发的特点

    相对于用户空间内的应用程序开发,内核开发存在很多的不同,最重要的差异包括以下几种:

    1)内核编程时不能访问C库。

    2)内核编程时必须使用GNU C。

    3)内核编程时缺乏像用户空间那样的内存保护机制。

    4)内核编程时浮点数很难使用。

    5)内核只有一个很小的定长堆栈。

    6)由于内核支持异步中断,抢占和SMP,因此必须时刻注意同步和并发。

    7)要考虑可移植性的重要性。

 

1.没有libc库

    与用户空间的应用程序不同,内核不能链接使用标准C函数库(其他的那些库也不行)。最主要的原因在于速度和大小。虽然不能使用,但大部分常用的C库函数在内核中都已经得到实现。比如说操作字符串的函数组就位于lib/string.c文件中,只要包含<linux/string.h>头文件,就可以使用它们。

    内核并没有实现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 a error!\n");

 

2.GNU C

    Linux内核是用C语言编写的,但它并不完全符合ANSI C标准,而使用到gcc提供的许多语言扩展部分。

    1)内联(inline)函数

        inline使函数在它被调用的位置上展开,这样做可以消除函数调用和返回所带来的开销(寄存器存储和恢复)。而且,由于编译器会把调用函数的代码和函数本身放在一起进行优化,所以也有进一步优化代码的可能。不过,这样做是有代价的,代码会变长,意味着占用更多的内存空间或者占用更多的指令缓存。通常把那些对时间要求比较高,而本身长度又比较短的函数定义成内联函数。

        static inline void dog(unsinged long tail_size);

        在内核中,为了类型安全的原因,优先使用内联函数而不是复杂的宏。

    2)内联汇编

        gcc编译器支持在C函数中嵌入汇编指令。当然,在内核编程的时候,只有知道对应的体系结构,才能使用这个功能。

    3)分支声明

        对于条件选择语句,gcc内建了一条指令用于优化,在一个条件经常出现,或者该条件很少出现的时候,编译器可以根据这条指令对条件分支选择进行优化,内核把这条指令封装成了宏,比如likely()和unlikely()。

        例如,下面是一个条件选择语句:

        if (foo) {

            /* .......... */

        }

        如果想要把这个选择标记成绝少发生的分支:

        /* 我们认为foo绝大多数时间都会为 0   */

        if (unlikely(foo)) {

            /* .......... */

        }

        相反,如果我们想把一个分支标记为通常为真的选择

        /* 我们认为foo通常都不会为 0    */

        if (likely(foo)) {

            /* .......... */

        }

        在想要对某个条件选择语句进行优化之间,一定要搞清楚其中是不是存在这么一个条件,在绝大多数情况下都会成立,这点十分重要:如果你的判断正确,确定是这个条件占压倒性的地位,那么性能会得到提升,如果你搞错了,性能反而会下降。在对一些错误条件进行判断的时候,常常用到unlikely()和likely()。可以猜到,unlikely()在内核中得到广泛使用,因为if语句往往判断一种特殊情况。

 

3.没有内存保护机制

    如果一个用户程序试图进行一次非法的内存访问,内核会发现这个错误,发送SIGSEGV,并结束整个进程。然而,如果是内核自己非法访问了内存,那后果就很难控制了,内核中发生的内存错误会导致oops,这是内核中出现的最常见的一类错误。在内核中,不应该去做访问非法的内存地址,引用空指针之类的事情,否则它可能会死掉,却根本不知会你一声——在内核里,风险常常会比外面大一些。

    此外,内核中的内存都不分页,也就是说,用掉一个字节,物理内存就减少一个字节。所以,在你想往内核里加入什么新功能的时候,要记住这一点。

 

4.不要轻易在内核中使用浮点数

    用户空间的进程内进行浮点操作的时候,内核会完成从整数操作到浮点数操作的模式转换。在执行浮点指令时到底会做些什么,因体系结构不同,内核的选择也不同,但是,内核通常捕获陷阱并做相应处理。

    和用户空间进程不同,内核并不能完美地支持浮点操作,因为它本身不能陷入。在内核中使用浮点数时,除了要人工保存和恢复浮点寄存器,还有其他一些琐碎的事情要做。如果要直截了当的回答,那就是:别这么做,不要在内核中使用浮点数。

 

5.容积小而固定的栈

    用户空间的程序可以从栈上分配大量的空间来存放变量,甚至巨大的结构体或者是包含许多数据项的数组都没有问题。因为用户空间的栈本身比较大,而且还能动态的增长。

    内核栈的准确大小随体系结构而变。在x86上,栈的大小在编译时配置,可以是4KB或8KB。内核栈的大小是两页,所以32位机的内核栈是8KB,而64位机是16KB,这是固定不变的。每个处理器都有自己的栈。

 

6.同步和并发

    内核很容易产生竞争条件,因内核的许多特性都要求能够并发的访问共享数据,这就要求有同步机制保证不出现竞争条件,尤其是:

    1)Linux是抢占多任务操作系统。内核的进程调度程序即兴对进程进行调度和重新调度。内核必须对这些任务同步。

    2)Linux内核支持多处理器系统。如果没有适当的保护,在两个或两个以上的处理器上运行的代码很可能会访问共享的同一个资源。

    3)中断是异步到来的,完全不顾及当前正在执行的代码。如果不加以适当的保护,中断完全有可能在代码访问共享资源的当中到来,这样中断处理程序就有可能访问同一资源。

    4)Linux内核可以抢占,如果不加以适当的保护,内核中的一段正在执行的代码可能会被另外一段代码抢占,从而有可能导致几段代码同时访问相同的资源。

 

7.可移植性的重要性

    Linux是一个可移植的操作系统,大部分C代码应该与体系结构无关,在许多不同体系结构的计算机上都可以编译和执行。因此,必须把体系结构相关的代码从内核代码树的特定目录中适当地分享出来。

    诸如保持字节序,64位对齐,不假定字长和页面长度等一系列准则都有助于移植性。

 

posted on 2012-12-13 09:16  汝熹  阅读(630)  评论(1编辑  收藏  举报