包你能学会的技术:Linux内核入门集(4)

  无libc或标准头

  和用户空间应用程序不一样,内核并没有链接到标准的C库,也没有链接到任何其它的库,这样设计的原因有很多,包括如先有鸡还是先有蛋的问题,但主要原因还是速度和内核大小,不要说完整的C库,就是它的一个子集也够大,内核太大只会导致效率低下。

  不要担心,许多常用的libc函数都在内核中实现了,例如,常见的字符串操作函数就位于lib/string.c中,只需要包括它的头文件<linux/string.h >就可以了。

  这里的头文件指的是内核源代码树中的头文件,内核也只能使用树内的头文件,基础文件位于源代码根目录的include/目录下,例如,<linux/inotify.h>头文件就位于include/linux/inotify.h。

  与架构相关的头文件则位于arch/<architecture>/include/asm,例如,如果在x86架构下编译,与你架构相关的文件就是arch/x86/include/asm,只需要在引用这些头的地方加上asm/前缀即可,如<asm/ioctl.h>。

  漏掉的大部分都是类似printf()这样的函数,内核不会使用printf(),但它提供了printk()函数,其表现绝不比printf()差,printk()会拷贝格式化的字符串到内核日志缓冲区,syslog程序就是从这里读取信息的,其用法也和printf()类似:

  printk("Hello world! A string '%s' and an integer '%d'\n", str, i);

  printf()和printk()之间最大的不同是,printk()允许你指定一个优先级标记,syslogd使用这个标记确定在哪里显示内核消息,下面是一个使用优先级标记的示例:

  printk(KERN_ERR "this is an error!\n");

  注意在KERN_ERR和打印的消息之间没有逗号,这是故意这么设计的,优先级使用一个预定义的字符定义,在编译期间它与打印的信息是串联的。

  GNU C

  和许多Unix内核类似,Linux内核也是用C编写的,但也许会让人很意外,内核不是用严谨的ANSI C编写的,内核开发人员用的却是gcc(GNU编译器集,包含了编译内核和Linux C程序的C编译器)中的各种语言扩展。

  内核开发人员同时使用了C语言的ISO C99和GNU C扩展,这些变化让Linux内核与gcc结合得更紧密,但最近又出现了一个编译器 – 英特尔的C编译器 – 也对gcc的功能支持得相当好,因此也可以用它来编译Linux内核。最低支持的gcc版本是3.2,建议采用gcc 4.4或更高的版本编译。使用ISO C99扩展也是可以的,因为C99是C语言的官方版本。

  内联函数

  C99和GNU C都支持内联函数,内联函数是直接插入到每个函数调用的位置的,消除了函数调用和返回的开销,允许进一步优化,因为编译器可以同时优化调用者和被调用函数,但它也有缺点,代码大小会增加,因为函数的内容被直接复制到所调用者内部了,因此也会增加内存消耗和指令缓存空间。内核开发人员一般在小型时间很关键的函数中才会使用内联函数。

  定义函数时,使用static和inline关键字声明内联函数,例如:

  static inline void wolf(unsigned long tail_size)

  函数必须先声明后使用,否则编译器就不能使函数内联,一般做法是将内联函数放在头文件中,因为它们被标记为static,不会创建输出函数,如果内联函数仅在一个文件中使用,可以放在该文件的顶部。

  在内核中,与复杂的宏相比,出于安全和可读性方面考虑,内联函数是首选。

  内联汇编

  Gcc C编译器允许在C函数中嵌入汇编指令,asm()编译器指令用于内联汇编代码,例如,这个内联汇编指令执行x86处理器的rdtsc指令,返回时间戳寄存器(tsc)的值:

unsigned int low, high;
asm volatile("rdtsc" : "=a" (low), "=d" (high));
/* low and high now contain the lower and upper 32-bits of the 64-bit tsc */

  Linux内核是用C和汇编语言混合编写的,与底层硬件相关的代码很多都是用汇编语言写的,剩下的大部分内核代码都是直接用C编写的。

  分支注解

  Gcc C编译器内置了一个指令优化条件分支,内核将这个打包成易于使用的宏 -likely()和unlikely()。

  先看下面这样的if语句:

if (error) { /* ... */ }

  将这个分支标记为非常不可能采用

/* we predict 'error' is nearly always zero ... */ if (unlikely(error)) { /* ... */ }

  相反,将这个分支标记为非常可能采用

/* we predict 'success' is nearly always nonzero ... */ if (likely(success)) { /* ... */ }

  当分支指令已经知道一个优先级,或你想在一种情况下优化另一种情况时应该使用上述指令,最重要的是,当分支正确标记时,这些指令会提升性能,但如果分支标记错误则会降低性能,在内核代码中,unlikely()要使用得更多,因为if语句倾向于表示一种特殊情况。

  无内存保护

  当用户空间的应用程序尝试一个非法的内存访问时,内核可以捕捉到错误,发送SIGSEGV信号,杀掉进程,如果内核尝试一个非法的内存访问时,结果就不受控制了,因为谁也无法去控制内核,这也是内核最主要的失误。

  此外,内核内存也是不可分页的,因此你消耗的每个内存字节都比物理内存的一个字节要少。

  不能(容易)使用浮点数

  当用户空间进程使用浮点指令时,内核要负责处理从整型到浮点模式的转换。

  与用户空间不一样,内核不能无缝支持浮点数,因为它自己不能轻易地捕捉到自己,在内核中使用浮点数需要手动保存和恢复浮点数寄存器,因此除非却有必要,否则尽量不要在内核中做浮点运算。

  小型,固定大小的堆栈

  用户空间可以静态分配许多不同的堆栈,包括巨型结构和千元数组,这个行为是合法的,因为用户空间有很大的堆栈,并可以动态增长。

  内核堆栈不大也不是动态的,相反,它很小且是固定的,内核堆栈的精确大小根据架构有所不同,在x86上,堆栈大小是在编译时确定的,一般是4KB或8KB,历史上,内核堆栈有2页,通常表示它处于32位架构上,大小是8KB,如果是16KB就表示是64位架构,总之大小是固定的,每个进程接收它自己的堆栈。

  同步和并发

  内核最容易受竞争条件影响,和一个单线程的用户空间应用程序不一样,有许多内核特性允许同时访问共享资源,因此需要同步以防止竞争,特别是:

  ◆Linux是一种抢占式多任务操作系统,进程是由内核的进程调度器随意调度和再次调度的,内核必须在这些任务之间同步;

  ◆Linux支持对称多处理(SMP),因此,如果没有适当的保护,在两个或多个处理器上同时执行的内核代码可能会同时访问相同的资源;

  ◆中断是异步发生的,因此,如果没有适当的保护,在访问资源期间也可能发生中断,中断处理程序可能就会访问到相同的资源;

  ◆Linux是有优先权的,因此,如果没有适当的保护,内核代码可能会优先执行,访问其它代码正在使用的资源。

  解决这些问题的一般方法是自旋锁和信号量。

  可移植性的重要性

  虽然用户空间应用程序一般不会太重视可移植性,但Linux的确是一个可移植性操作系统,应该保持一致,这意味着与架构无关的C代码必须在大量的系统上正确地编译和运行,与架构相关的代码必须在内核源代码树中使用特定的目录分隔开。

  总结

  可以肯定,内核有它独特的性质,它有它自己的一些原则,不过,内核的复杂性和障碍与其它大型软件项目相比,并没有什么大的不同,Linux开发道路上最重要的一步是认识到内核并不可怕,不熟悉?当然!不可逾越?当然不是!

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/26593.html