【Linux内核设计思想】二、内核编译及内核开发的特点

2024-08-08 17:06:37 浏览数 (2)

获取内核源码

在Linux内核官方网站即可下载最新Linux源码

http://www.kernel.org

我们一般应该下载最新的稳定版本Linux内核源码进行学习。

源码下载后,通过tar命令进行解压即可

代码语言:javascript复制
tar xvzf linux-X.X.X.tar.gz

解压后源码会存在linux-X.X.X文件夹中。

内核源码一般都安装在 /usr/src/linux 目录下,但我们开发时不要直接对这个源码树进行开发,因为编译C库所用的内核版本就是该源码树。并且一般不要以root身份修改内核,我们应该自己另外建立一个目录,并以root身份在该目录安装新内核,/usr/src/linux 目录应原封不动的继续存在。

一般以补丁的形式发布对代码的修改,并以补丁的形式接收其他人发布的修改。

内核源码树由很多目录组成,其根目录及描述如下

在内核源码树根目录下还有一些文件,COPYING文件是内核许可证,CREDITS是开发者列表并包含了一些内核代码细节,MAINTAINERS是维护内核子系统和驱动程序的维护者列表,Makefile是编译内核的基础。

编译内核

内核源码在编译时可以进行配置和定制,我们可以把自己需要的功能和驱动程序编译进内核。可以配置的选项以CONFIG_FEATURE形式表示,比如,对称多处理器(SMP)的配置选项为CONFIG_SMP,如果设置了该选项,则SMP启用,否则SMP不起作用。配置选项可以用来决定那些文件编译进内核,选项有二选一和三选一,yes/no/module。在三选一的情况下多了一个 module 选项,如果选择 module 选项,表示该配置项已被选定,但编译的时候这部分功能的实现代码是以模块(一种可以动态安装的独立代码段)的形式生成,而 yes 选项表示把代码编译进主内核映像,而不是作为一个模块。

配置选项也可以是字符串或者整数,这些选项不用于控制编译过程,而是用于指定内核源码可以访问的值,一般以预处理宏的形式表示,比如我们可以通过配置选项指定静态分配数组的大小。

内核提供了很多工具来简化内核配置过程,最简单最耗时的是命令行工具,该工具会遍历所有选项并让你选择配置选项 yes/no/module

代码语言:javascript复制
make config

还有其他更方便的图形工具可供使用

代码语言:javascript复制
make menuconfig
make xconfig
make gconfig
make defconfig #创建默认配置

也可以查看并修改配置文件来修改这些配置选项,这些选项存放在内核源码树目录的 config 文件中。

通过如下命令验证并更新配置

代码语言:javascript复制
make oldconfig

配置完成后,就可以编译内核了,使用命令

代码语言:javascript复制
make

在编译时,往往会打印很多信息并刷屏,如果不想看到这些信息,可以执行下面命令来编译

代码语言:javascript复制
make > /dev/null

编译好内核后,就到了安装步骤。安装内核与操作系统体系结构和启动引导工具(boot loader)相关,按照要求将内核映像拷贝到适当位置。这个过程可能需要把某个模块拷贝到指定目录,并且需要编译相应的配置文件,建立启动项等等。

我们可以通过命令来把编译好的模块安装到正确的主目录 /lib 下

代码语言:javascript复制
make modules install

这个命令需要 root 权限。

编译时还会在内核代码树的根目录下创建一个 System.map 文件,这是一个符号对照表,用来将内核符号和它们的起始地址一一对应,调试时可以把内存地址翻译成函数名或变量名以便于理解。

内核开发的特点

内核编程时不能访问C库,即没有 libc 库。

在用户空间编程时,我们可以调用C库函数,但是在内核编程时,内核无法链接标准C函数库,实际上其他一些库也无法使用。但是大部分C库函数都已经在内核中实现了,只要包含相应头文件就可以调用,比如字符串操作函数库 lib/string.c,头文件为<linux/string.h>。

在内核编程时,所用的头文件都是源码树的内核头文件,内核源码文件不能包含外部头文件。

在内核中也有一些C库函数并没有实现,比如 printf() 函数,但是内核中实现了一个叫 printk() 的函数。printk() 函数负责把格式化好的字符串拷贝到内核日志缓冲区,syslog 程序通过读取该缓冲区来获取内核信息。并且 printk() 函数允许通过指定一个标志来设置优先级,syslog 程序根据这个优先级标志来决定在什么地方显示这条系统消息。

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

我们知道,Linux 内核是使用C语言编写的,但是,内核代码并不完全符合 ANSI C 标准,它用到了 gcc 提供的许多语言扩展部分。gcc 是多种 GNU 编译器的集合,它包含的C编译器既可以编译内核,也可以编译 Linux 系统上的其它C源代码。总之,内核开发者使用的C语言包含 ISO C99 标准以及 GNU C 扩展特性。下面列举内核源码中使用到的一些C语言扩展:

① 内联函数(inline)

内联函数顾名思义,就是“在字里行间展开”的意思,内联函数会在它被调用的位置展开,这样做消除了函数调用和返回所带来的开销,比如寄存器的存储和恢复等。而且,编译器会把调用函数的代码和函数本身放在一起进行优化,这就有了代码进一步优化的可能。当然内联函数也有缺点,那就是会使代码变长,会占用更多的内存空间和指令缓存。

我们通常把一些对时间要求高,且本身代码长度较短的函数定义为内联函数。那些对时间要求不高且被反复调用的函数不要定义为内联函数。

再定义一个内联函数时,通常需要使用 static 关键字,并且需要使用 inline 进行限定。

代码语言:javascript复制
static inline func(){
    ;
}

内联函数必须在使用之前就定义好,否则的话编译器无法进行函数展开。在编程时,通常在头文件中定义内联函数(如果内联函数仅在某个源文件中使用,也可以在该文件头部定义内联函数)。由于使用了 static 关键字,编译时不会为内联函数单独建一个函数体。在内核编程时,考虑到类型安全因素,应优先使用内联函数而不是宏。

② 内联汇编

gcc 编译器支持在C函数中嵌入汇编指令,Linux 内核就是用了C和汇编混合编程,在偏近体系结构的底层或对执行时间要求严格的地方,一般都是使用汇编语言编写的。

③ 分支声明

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

代码语言:javascript复制
if(flag){
    ;
}

如果 flag 为0的概率远远大于它为真的概率,也就是说 flag 这条分支大概率不会发生,我们可以标记为

代码语言:javascript复制
/* 我们认为flag大概率为0 */
if(unlikely(flag)){
    ;
}

反之

代码语言:javascript复制
/* 我们认为flag大概率为1 */
if(likely(flag)){
    ;
}

这种标记大幅提升性能的前提是你的判断是正确的,如果判断错了可能反而会大幅度降低性能。

内核编程时没有像用户空间那样的内存保护机制。

如果是一个用户程序对内存进行非法访问,那么内核会报错,发送 SIGSEGV 并结束进程。但是,如果是内核自身非法访问内存,那么可能它会直接死掉并且不会报错。内核中的内存错误会导致 oops,这也是内核中出现较多的一类错误,并且内核中的内存不会分页,每用掉一个字节,物理内存就减少一个字节。

内核编程时不要轻易使用浮点数。

如果我们在用户空间进行浮点操作,内核会完成从整数操作到浮点操作的转换。但是在内核中使用浮点数会非常麻烦,这需要你人工保存和恢复浮点寄存器,以及其他一些操作都要人工完成,所以在内核编程时不要使用浮点数。

内核只有一个很小且固定的堆栈。

用户空间栈是非常大的,并且可以动态增长,因此我们可以在用户空间编程时分配大量栈内存。但是内核栈的大小是固定的,它和体系结构有关,在 x86 上,栈的大小在编译时配置,可以是4KB或8KB,一般来说,内核栈的大小是两页,在32位机器内核栈大小为8KB,在64位机器内核栈大小为16KB,这是固定的,每个处理器都有自己的栈。

由于内核支持异步中断、抢占和SMP,所以必须时刻注意同步和并发。

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

  • Linux是抢占多任务操作系统,内核的进程调度程序即兴对进程进行调度和重新调度,内核必须对这些任务同步。
  • Linux内核支持多处理器系统,如果没有保护,在多个处理器上运行的代码很可能会同时访问共享的同一资源。
  • 中断是异步到来的,不会考虑正在执行的代码,如果不加保护,中断有可能会在代码访问共享资源时到来,而中断处理程序也有可能会访问该共享资源。
  • Linux内核可以抢占,如果不加保护,内核中正在执行的代码可能会被另一段代码抢占,并且这几段代码可能同时访问相同资源。

通常使用自旋锁和信号量来解决竞争问题。

需要考虑可移植性。

Linux是一个可移植的操作系统,也就是说大部分C代码应该与体系结构无关,在各种不同体系结构的计算机上都能编译和执行,这就意味着,必须把体系结构相关的代码从内核代码树的特定目录中分离出来。

0 人点赞