Chapter 2 - 从内核出发
Created by : Mr Dk.
2019 / 10 / 07 22:55
Nanjing, Jiangsu, China
2.1 获取内核源码
略过...
2.2 内核代码树
2.3 编译内核
2.3.1 配置内核
由于内核提供了数不胜数的功能,支持了难以计数的硬件,所以需要配置很多东西。所有可配置的选项都以 CONFIG
开头,要么是二选一,要么是三选一。
- yes - 编译进主内核映像中
- no
- module - 编译时以模块的形式生成 (可以动态安装的独立代码段)
make config
这个工具会逐一遍历所有配置项,要求用于二选一或三选一。这样很麻烦,所以可以这样:
make defconfig
基于默认的配置,为你的体系结构创建一个配置。所有的配置项都被存放在内核代码 root 目录下的 .config
中,该文件可以被直接修改。修改配置文件之后,应当验证和更新配置:
make oldconfig
之后就 make
编译吧。
2.3.2 减少编译的垃圾信息
将 make
重定向到某个文件或 /dev/null
中。
2.3.3 衍生多个编译作业
make -jn
2.3.4 安装新内核
内核编译好后,需要将内核映像拷贝到合适的位置并安装。因体系结构和启动引导工具而异。
2.4 内核开发的特点
内核开发与用户空间内的应用程序开发的差异:
- 不能访问 C 库,也不能访问标准的 C 头文件
- 必须使用 GNU C
- 缺乏像用户空间那样的内存保护机制
- 难以执行浮点运算
- 内核给每个进程一个很小的定长堆栈
- 需要时刻注意同步和并发
- 要考虑可移植性
2.4.1 无 libc 库异或无标准头文件
因为 C 库中的函数对于内核来说太庞大,性能低下,大部分常用的 C 库函数在内核中都已经得到了实现,比如 string.h
等。最著名的 printf()
也由 printk()
实现。
2.4.2 GNU C
内核并不完全符合 ANSI C 标准,而是使用了 ISO C99 标准和 GNU C 的扩展特性。
内联函数 (inline)
函数在被调用的位置展开:
- 消除函数调用和返回的开销 (寄存器存储和恢复)
- 编译器可以把调用函数的代码和函数本身一起进行优化
- 代码变长,占用更多的内存和指令缓存
通常会把对时间要求高,而自身长度较短的函数定义为内联函数。需要使用 static
关键字来声明。
static inline void wolf(unsigned long tail_size);
在内核中,为了 类型安全 和 易读性,优先使用内联函数,而不是 宏。
内联汇编
gcc 编译器支持在 C 函数中嵌入汇编指令。只有知道对应的体系结构,才能使用这个功能。在偏近体系结构底层,或对时间要求严格的地方,使用汇编。
分支声明
对于条件选择语句,gcc 内建了指令用于优化:
- 一个条件经常出现
- 一个条件很少出现
编译器根据这个指令对条件分支选择进行优化,内核把这条指令封装为宏。
if (error) {
// ...
}
//
if (unlikely(error)) {
// ...
}
//
if (likely(error)) {
// ...
}
需要判断是否存在条件,使得大多数情况下都会成立:
- 如果判断正确,那么这样能提高性能
- 否则性能反而会下降
2.4.3 没有内存保护机制
内核访问了非法内存,后果难以控制。内核中的内存都不分页,用掉一个字节,物理内存就少一个字节。
2.4.4 不要轻易在内核中使用浮点数
用户空间中的浮点数操作,内核会在其中完成从整数操作到浮点数操作的模式转换。在内核中使用浮点数,需要人工保存和恢复浮点寄存器,还要做一些琐碎的事——尽量 别这么做 !除了极少数情况,不要在内核中使用浮点操作。
2.4.5 容积小而固定的栈
用户空间的栈比较大,并且可以动态增长。内核栈的大小随体系结构而变,而且 大小固定。
2.4.6 同步和并发
内核很容易产生竞争条件:
- Linux 是抢占多任务 OS - 内核必须和其调度的任务同步
- Linux 内核支持对称多处理器系统 (SMP) - 同时在两个以上的 CPU 上执行的内核代码很可能会同时访问共享资源
- 中断是异步到来的 - 中断完全可以在代码访问资源时到来,这样中断处理程序也能访问同一资源
- Linux 内核可以抢占 - 内核中一段正在执行的代码可能会被另一段代码抢占,导致几段代码同时访问相同的资源
需要用机制解决竞争。
2.4.7 可移植性的重要性
大部分 C 代码应该与体系结构无关,必须把与体系结构相关的代码从内核代码树的特定目录中分离。
Summary
OS 内核之所以难、复杂,就是因为这些吧 🥱