Linux内核设计与实现——从内核出发
获取内核源代码
-
登陆Linux内核官方网站,可以随时获取当前版本的源代码,可以是完整的压缩形式,也可以是增量补丁形式
-
使用git下载
git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
更新自己的分支到Linux的最新分支
git pull
-
安装内核源代码
-
内核压缩以Gnu zip(g zip)和bzip2两种形式发布。bzip2是默认首选形式,叫做linux-x.y.z.tar.bz2
-
bzip2解压
tar xvjf linux-x.y.z.tar.bz2
-
GNU zip解压
tar xvzf linux-x.y.z.tar.gz
-
何处安装并触及源码
内核源码一般安装在/usr/src/linux目录下。注意,不要把这个源码树用于开发,因为编译你的C库所用的内核版本就链接到这棵树。此外,不要以root身份对内核进行修改,而应当建立自己的主目录,仅以root身份安装新内核。即使在安装新内核时,/usr/src/linux目录都应该原封不动
-
-
使用补丁
-
在内核社区中,补丁是通用语。可以以补丁的形式发布对代码的修改,也可以以补丁的形式接收其他人所做的修改
-
增量补丁可以作为版本转移的桥梁:不需要下载全部的内核源码,只需给旧版本打上一个增量补丁
-
应用增量补丁
patch -pl < ../patch-x.y.z
-
一个给定版本的内核补丁总是打在前一个版本上
-
内核源码树
源码树根目录中的很多文件值得提及。
- COPYING文件是内核许可证(GNU GPL v2)
- CREDITS是开发了很多内核代码的开发者列表
- MAINTAINERS是维护者列表,他们负责维护内核子系统和驱动程序
- Makefile是基本内核的Makefile
编译内核
配置内核
-
由于内核提供了数不胜数的功能,支持了难以计数的硬件,因而有许多东西需要配置
-
可配置的各种选项,以CONFIG_FEATURE形式表示,前缀为CONFIG:配置选项既可以用来决定哪些文件编译进内核,也可以通过预处理命令处理代码
-
配置项为二选一或者三选一:二选一就是yes或no,三选一可以是yes,no,module。module以为只该配置项被选定了,但编译的时候这部分功能的实现代码是以模块形式生成,yes为把代码编译进主内核映像中。
-
驱动程序一般都用三选一的配置项
-
配置选项也可以是字符串或整数:这些选项并不控制编译过程,只是指定内核源码可以访问的值,一般以预处理宏的形式表示。比如,配置选项可以指定静态分配数组的大小
-
内核配置工具
-
make config
该工具会逐一遍历所有的配置项,要求用户选择yes,no,module,耗费大量时间
-
make menuconfig
基于ncurse库编制的图形界面工具
-
make gconfig
基于gtk+的图形工具
-
前三种工具将所有配置项分门别类放置,可以按类移动浏览和修改
-
make defconfig
基于默认的配置为你的体系结构创建一个配置(新手练习友好)
-
-
配置项会被存放在内核代码树根目录下的.config文件中,可以查找和修改内核选项,修改后或者在用已有的配置文件配置新的代码树的时候,应该验证和更新配置:
make oldconfig
编译内核之前都应该这么做
-
配置选项CONFIG_IKCONFIG_PROC把完整的压缩过的内核配置文件存放在/proc/config.gz下,可方便克隆配置。若内核启动了CONFIG_IKCONFIG_PROC,就可以从/proc下复制出配置文件并且使用它来编译一个新内核
zcat /proc/config.gz > .config
make oldconfig
-
内核配置好之后,使用
make
编译
减少编译的垃圾信息
-
make > .. /detritus
尽量少地看到垃圾信息,却又不希望错过错误报告和警告信息
-
make > /dev/null
把无用的输出信息重定向到永无返回值的黑洞/dev/null
衍生多个编译作业
-
make程序能把编译过程拆分成多个并行的作业:每个作业并发运行,有助于加快多处理器系统上的编译过程
-
默认情况下,make只衍生一个作业,因为Makefiles常会出现不正确的依赖信息,多个作业可能会互相踩踏,导致编译过程出错。不过,内核的Makefiles没有这样的错误,因此衍生的多个作业编译不会失败
-
make -jn
n是要衍生出的作业数,实际中咩咯处理器一般衍生出一个或者两个作业
-
利用出色的distcc或者ccache工具,也可以动态地改善内核的编译时间
安装新内核
-
编译后的安装就和体系结构以及启动引导工具(bootloader)息息相关:查阅启动引导工具的说明,按照他的指导将内核映像拷贝到合适的位置,并且按照启动要求安装它。一定要保证随时有一个或者两个可以启动的内核,以防新编译的内核出现问题
-
例如,在使用grub的x86系统上,可能需要把arch/i386/boot/bzImage拷贝到/boot目录下,相vmlinuz-version这样命名它,并且编辑/etc/grub/grub.conf文件,为新内核建立一个新的启动项。使用LILO启动的系统应当编辑/etc/lilo.config,然后运行lilo
-
make modules_install
以root身份运行,就可以把所有已编译的模块安装到正确的主目录/lib/modules下
-
编译时也会在内核代码树的根目录下创建一个System.map文件。这是一份符号对照表,用以将内核符号和它们的起始地址对应起来。调试的时候,如果需要把内存地址翻译成容易理解的函数名和变量名,这就会很有用
内核开发的特点
- 相对于用户空间内应用程序的开发,内核开发有一些独特之处
- 内核编程时既不能访问C库也不能访问标准的C头文件
- 内核编程时必须使用GNU C
- 内核编程时缺乏像用户空间那样的内存保护机制
- 内核编程时难以执行浮点运算
- 内核给每个进程只有一个很小的定长堆栈
- 由于内核支持异步中断、抢占和SMP,因此必须时刻注意同步和并发
- 要考虑可以执行的重要性
无libc库抑或无标准头文件
-
主要原因:速度和大小。对内核来说,完整的C库,哪怕时它的一个子集,都太大且太低效了
-
大部分常用的C库函数在内核中都已经实现。比如操作字符串的函数就位于lib/string.c文件中
-
头文件
-
指的是组成内核源代码树的内核头文件
-
基本的头文件位于/include/目录中
-
体系结构相关的头文件位于/arch//include/asm目录下
-
内核的printk()函数
printk(“hello world!A string:’%s’ and an integer:’%d’ ”,str,i);
显著区别在于,printk()允许通过指定一个标志来设置优先级。syslogd会根据这个优先级标志来决定在什么地方显示这条系统消息。
printk(KERN_ERR"this is an error ");
注意,在KERN_ERR和要打印的消息之间没有逗号。优先级标志时预处理程序定义的一个描述性字符串,在编译时优先级标志就要与打印的消息绑在一起处理
-
GNU C
- 内联函数
- C99和GNU C均支持内联函数
- inline工作方式,函数会在它所调用的位置上展开:可以消除函数调用和返回所带来的花销(寄存器存储和恢复)。并且编译器会把调用函数的代码和函数本身放在一起进行优化
- 但会使代码变长,占用更多的内存空间或者占用更多的指令缓存
- 开发者通常把那些对时间要求比较高,而本身长度又比较短的函数定义成内联函数。较大的,会被反复调用的函数不建议做成内联函数
static inline void wolf(unsigned long tail_size)
- 实践中一般在头文件中定义内联函数。由于使用了static关键字,编译时不会会内联函数单独建立一个函数体。如果一个内联函数仅仅在某个源文件中使用,那么也可以把它定义在该文件开始的地方
- 在内核中,为了类型安全和易读性,优先使用内联函数而不是复杂的宏
- 内联汇编
- 使用asm()指令潜入汇编代码
- 偏近体系结构的底层或对执行时间要求严格的地方,一般使用的是汇编语言,其他部分大部分代码是用C语言编写的
- 分支声明
- 对于条件选择语句,gcc内建了一条指令用于优化,在一个条件经常出现,或者该条件很少出现的时候,编译器可以根据这条指令对条件分支选择进行优化
- 内核把这条指令封装成了宏,比如likely()和unlikely()
- 例如,下面是一个条件选择语句:
if (error) {
/*...*/
}
如果想要把这个选择标记成绝少发生的分支:
/*我们认为error绝大部分时间都会为0...*/
if (unlikely(error)) {
/*...*/
}
相反,如果我们想把一个分支标记成通常为真的选择:
/*我们认为success通常都不会为0...*/
if (likely(success)) {
/*...*/
}
- 在进行条件选择语句的优化时,一定要分析该条件的触发频率,合理使用,则可以优化性能。unlikely()在内核中会得到更广泛的使用,因为if语句往往判断一种特殊情况
没有内存保护机制
- 内核中发生的内存错误会导致oops“哎呀”
- 不应该去访问非法的内存地址,引用空指针之类的事情,内核会忽然死掉的
- 内核中的内存都不分页:每用掉一个字节,物理内存就减少一个字节
不要轻易在内核中使用浮点数
- 在内核中使用浮点数时,除了要人工保存和回复浮点寄存器,还有其他一些琐碎的事情要做
- 处理极少情况,不要在内核中使用浮点操作
容量小而固定的栈
- 用户栈空间很大,并且可以动态增长
- 内核栈的准确大小岁体系结构而变。在x86上,栈的大小在编译时配置,可以时4KB也可以是8KB.从历史上说,内核栈的大小是两页:32位机的内核栈是8KB,64位机是16KB
同步和并发
内核很容易产生竞争条件。和单线程的用户空间程序不同,内核的许多特性都要求能够并发地访问共享数据,这就要求有同步机制以保证不出现竞争条件,特别是:
- Linux是抢占式多任务操作系统。内核的进程调度程序即兴对进程进行调度和重新调度。内核必须和这些任务同步。
- Linux内核支持多处理器系统(SMP)。所以,如果没有适当的保护,同时在两个或两个以上的处理器上执行的内核代码很可能会同时访问共享的同一个资源
- 中断是异步到来的,完全不顾及当前正在执行的代码。也就是说,如果不加以适当的保护,中断完全有可能在代码访问资源的时候到来,这样,中断处理程序就有可能访问同一资源
- Linux内核可以抢占。缩以,如果不加以适当的保护,内核中一段正在执行的代码可能会被另外一段代码抢占,从而有可能导致几段代码同时访问相同的资源
- 常用的解决竞争的办法是自旋锁和信号量
可移植性的重要性
- 必须把与体系结构相关的代码从内核代码树的特定目录中适当地分离出来
- 诸如保持字节序、64位对齐、不假定字长和页面长度等一系列准则都有助于一致性
UNIT2 end
开始学习到好多新接触的知识啦,等实习结束回到学校尽量就拿板子上手多练习吧,加油加油!!