《内核设计与实现》读书笔记(二)- 从内核出发
在尝试内核开发之前,需要对内核有个整体的了解。
主要内容:
- 获取内核源码
- 内核源码树
- 编译内核
- 内核开发的特点
1. 获取内核源码
内核是开源的,所有获取源码特别方便,参照以下的网址,可以通过git或者直接下载压缩好的源码包。
http://www.kernel.org
2. 内核源码的结构
目录 | 说明 |
arch | 特定体系结构的代码 |
block | 块设备I/O层 |
crypo | 加密API |
Documentation | 内核源码文档 |
drivers | 设备驱动程序 |
firmware | 使用某些驱动程序而需要的设备固件 |
fs | VFS和各种文件系统 |
include | 内核头文件 |
init | 内核引导和初始化 |
ipc | 进程间通信代码 |
kernel | 像调度程序这样的核心子系统 |
lib | 通用内核函数 |
mm | 内存管理子系统和VM |
net | 网络子系统 |
samples | 示例,示范代码 |
scripts | 编译内核所用的脚本 |
security | Linux 安全模块 |
sound | 语音子系统 |
usr | 早期用户空间代码(所谓的initramfs) |
tools | 在Linux开发中有用的工具 |
virt | 虚拟化基础结构 |
3. 编译内核
3.1 配置内核
得到Linux源码后,在编译它之前可以配置和定制。可以把自己需要的特殊功能和驱动程序编译进内核。
配置选项可以用来决定哪些文件编译进内核,也可以通过预处理命令处理代码。
这些配置选项要么是二选一(yes或no),要么是三选一(yes、no或module)。module意味着该配置项选定了,但编译时这部分功能的实现代码是以模块(一种可以动态安装的独立代码段)的形式生成。yes选项表示把代码编译进主内核映像中,而不是作为一个模块。驱动程序一般都用三选一的配置项。
3.1.1内核提供的内核配置工具
$ make config ---> 逐一遍历所有配置项,要求用户选择yes、no或module,耗时长,不推荐
$ make menuconfig ---> 基于ncurse库编制的图形界面工具,推荐
$ make gconfig ---> 基于gtk+的图形工具
$ make defconfig ---> 会基于默认的配置为你的体系结构创建一个配置
配置后这些配置项会保存在内核代码树根目录下的.config文件中,可以在.config文件中查找和修改内核选项。
3.2 编译内核
一旦配置好了内核(即.config文件按自己要求生成完),就可以用一个简单明了编译它:
$ make
3.2.1 减少编译的垃圾信息
尽少看到垃圾信息,又不希望错过错误和警告信息,可以使用下面这个命令来对输出进行重定向:
$ make > .. /detritus
这样一旦需要查看编译的输出信息,你可以查看detritus这个文件,不过,因为错误和警告都在屏幕上显示,所以查看的可能性不大。我们可以输入以下命令,让输出信息重定向到永无返回值的黑洞/dev/null
$ make > /dev/null
4. 内核开发的特点
- 内核编程时既不能访问C库也不能访问标准的C头文件
- 内核编程时必须使用GNU C
- 内核编程时缺乏像用户空间那样的内存保护机制
- 内核编程时难以执行浮点运算
- 内核给每个进程只有一个很小的定长堆栈
- 由于内核支持异步中断、抢占和SMP,因此必须时刻注意同步和并发
- 要考虑可移植性的重要性
4.1 内核编程时既不能访问C库也不能访问标准的C头文件
为了保证内核的小和高效,内核开发中不能使用C标准库,所以连最常用的printf函数也没有,但是还好有个printk函数来代替。
4.2 内核编程时必须使用GNU C,推荐使用gcc 4.4或以后的版本来编译内核
因为使用GNU C,所以内核中常使用GNU C中的一些扩展:
4.2.1 内联(inline)函数
内联函数在编译时会在它被调用的地方展开,减少函数调用和返回的开销,性能较好。但是,频繁使用内联函数也会是代码变长,从而在运行时占用更多的内存。
所以内联函数使用时最好满足以下几点:函数较小,不被反复调用,对程序时间要求比较严格。
定义一个内联函数时,需要使用static作为关键字,并用inline限定它。
内联函数示例:static inline void sample();
4.2.2 内联汇编
内联汇编用于偏近底层或对执行时间严格要求的地方。示例如下:
unsigned int low, high; asm volatitle("rdtsc" : "=a" (low), "=d" (high)); /* low和high分别包含64位时间戳的低32位和高32位*/
4.2.3 分支声明
在条件选择语句中,如果一个条件经常出现或条件很少出现时,那么可以用likely和unlikely来优化这段判断的代码。
/* 下面是一个条件选择语句 */ if (error) { /* ... */ } /* 如果error绝大多数时间都会为0 */ if (unlikely(error)) { /* ... */ } /* 如果success在绝大多数情况下不为0(真) */ if (likely(success)) { /* ... */ }
4.3 内核编程时缺乏像用户空间那样的内存保护机制
因为内核是最底层的程序,所以如果内核访问的非法内存,那么整个系统都会挂掉!!所以内核开发的风险比用户程序开发的风险要大。
而且,内核中的内存是不分页的,每用一个字节的内存,物理内存就少一个字节。所以内核中使用内存一定要谨慎。
4.4 内核编程时难以执行浮点运算
内核不能完美的支持浮点操作,使用浮点数时,需要人工保存和恢复浮点寄存器及其他一些繁琐的操作。
4.5 内核给每个进程只有一个很小的定长堆栈
内核栈的大小有编译内核时决定的,对于不用的体系结构,内核栈的大小虽然不一样,但都是固定的。
查看内核栈大小的方法:
ulimit -a | grep "stack size"
4.6 由于内核支持异步中断、抢占和SMP,因此必须时刻注意同步和并发
Linux是多用户的操作系统,所以必须处理好同步和并发操作,防止因竞争而出现死锁。
4.7 要考虑可移植性的重要性
Linux内核可用于不用的体现结构,支持多种硬件。所以开发时要时刻注意可移植性,尽量使用体系结构无关的代码。