• 易语言字节集 > 从内核出发
  • 从内核出发

    免费下载 下载该文档 文档格式:PDF   更新时间:2014-09-19   下载次数:0   点击次数:1
    第2章从内核出发 在这一章,我们将介绍 Linux 内核的一些基本常识 : 从何处获取源码,如何编译它,又如何 安装新内核.那么,让我们考察一下内核程序与用户空间程序的差异,以及内核中所使用的通 用编程结构.虽然内核在很多方面有其独特性,但从现在来看,它和其他大型软件项目并无多 大差别. 2.1 获取内核源码 登录 Linux 内核官方网站 http://www.kernel.org,可以随时获取当前版本的 Linux 源代码,可 以是完整的压缩形式(使用 tar 命令创建的一个压缩文件) ,也可以是增量补丁形式. 除特殊情况下需要 Linux 源码的旧版本外,一般都希望拥有最新的代码.kernel.org 是源码 的库存之处,那些领导潮流的内核开发者所发布的增量补丁也放在这里. 2.1.1 使用 Git 在过去的几年中,Linus 和他领导的内核开发者们开始使用一个新版本的控制系统来管理 Linux 内核源代码.Linus 创造的这个系统称为 Git.与CSV 这样的传统的版本控制系统不同, Git 是分布式的,它的用法和工作流程对许多开发者来说都很陌生.我强烈建议使用 Git 来下载 和管理 Linux 内核源代码. 你可以使用 Git 来获取最新提交到 Linus 版本树的一个副本 : $ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git 当下载代码后,你可以更新你的分支到 Linus 的最新分支 : $ git pull 有了这两个命令,就可以获取并随时保持与内核官方的代码树一致.要提交和管理自己的修 改,请看第 20 章.关于 Git 的全面讨论已经超出了本书的范围,许多在线资源都提供了有效的 指导. 2.1.2 安装内核源代码 内核压缩以 GNU zip(gzip)和bzip2 两种形式发布.bzip2 是默认和首选形式,因为它在压 缩上比 gzip 更有优势.以bzip2 形式发布的 Linux 内核叫做 linux-x.y.z.tar.bz2,这里 x.y.z 是内核 源码的具体版本.下载了源代码之后,就可以轻而易举地对其解压.如果压缩形式是 bzip2,则11 从内核出发 运行 : $ tar xvjf linux-x.y.z.tar.bz2 如果压缩形式是 GNU 的zip,则运行 : $ tar xvzf linux-x.y.z.tar.gz 解压后的源代码位于 linux-x.y.z. 目录下.如果你是使用 git 获取和管理内核源代码,那么就 不需要下载压缩文件,只要像前面描述的那样运行 git clone 命令,git 就会下载并且解压最新的 源代码. 何处安装并触及源码 内核源码一般安装在 /usr/src/linux 目录下.但请注意,不要把这个源码树用于开发,因 为编译你的 C 库所用的内核版本就链接到这棵树.此外,不要以 root 身份对内核进行修改, 而应当是建立自己的主目录,仅以 root 身份安装新内核.即使在安装新内核时,/usr/src/linux 目录都应当原封不动. 2.1.3 使用补丁 在Linux 内核社区中,补丁是通用语.你可以以补丁的形式发布对代码的修改,也可以以补 丁的形式接收其他人所做的修改.增量补丁可以作为版本转移的桥梁.你不再需要下载庞大的内 核源码的全部压缩,而只需给旧版本打上一个增量补丁,让其旧貌换新颜.这不仅节约了带宽, 还省了时间.要应用增量补丁,从你的内部源码树开始 , 只需运行 : $ patch -p1 < ../patch-x.y.z 一般来说,一个给定版本的内核补丁总是打在前一个版本上. 有关创建和应用补丁更深入的讨论会在后续章节进行. 2.2 内核源码树 内核源码树由很多目录组成,而大多数目录又包含更多的子目录.源码树的根目录及其子目 录如表 2-1 所示. 表2-1 内核源码树的根目录描述 目录描述arch 特定体系结构的源码 block 块设备 I/O 层crypto 加密 API Documentation 内核源码文档 drivers 设备驱动程序 ?rmware 使用某些驱动程序而需要的设备固件 fs VFS 和各种文件系统 12 第2章目录描述include 内核头文件 init 内核引导和初始化 ipc 进程间通信代码 kernel 像调度程序这样的核心子系统 lib 通用内核函数 mm 内存管理子系统和 VM net 网络子系统 samples 示例,示范代码 scripts 编译内核所用的脚本 security Linux 安全模块 sound 语音子系统 usr 早期用户空间代码 (所谓的 initramfs) tools 在Linux 开发中有用的工具 virt 虚拟化基础结构 在源码树根目录中的很多文件值得提及.COPYING 文件是内核许可证(GNU GPL v2) . CREDITS 是开发了很多内核代码的开发者列表.MAINTAINERS 是维护者列表,它们负责维护 内核子系统和驱动程序. Make?le 是基本内核的 Make?le. 2.3 编译内核 编译内核易如反掌.让人叹为观止的是,这实际上比编译和安装像 glibc 这样的系统级组 伴还要简单.2.6 内核提供了一套新工具,使编译内核更加容易,比早期发布的内核有了长足 的进步. 2.3.1 配置内核 因为 Linux 源码随手可得,那就意味着在编译它之前可以配置和定制.的确,你可以把自己 需要的特定功能和驱动程序编译进内核.在编译内核之前,首先你必须配置它.由于内核提供了 数不胜数的功能,支持了难以计数的硬件,因而有许多东西需要配置.可以配置的各种选项,以CONFIG_FEATURE 形式表示,其前缀为 CONFIG.例如,对称多处理器(SMP)的配置选项为 CONFIG_SMP.如果设置了该选项,则SMP 启用,否则,SMP 不起作用.配置选项既可以用来 决定哪些文件编译进内核,也可以通过预处理命令处理代码. 这些配置项要么是二选一, 要么是三选一. 二选一就是yes 或no. 比如CONFIG_ PREEMPT 就是二选一,表示内核抢占功能是否开启.三选一可以是 yes、no 或module.module 意味着该配置项被选定了,但编译的时候这部分功能的实现代码是以模块(一种可以动态安装的 独立代码段)的形式生成.在三选一的情况下,显然 yes 选项表示把代码编译进主内核映像中, 而不是作为一个模块.驱动程序一般都用三选一的配置项. (续) 13 从内核出发 配置选项也可以是字符串或整数.这些选项并不控制编译过程,而只是指定内核源码可以访 问的值,一般以预处理宏的形式表示.比如,配置选项可以指定静态分配数组的大小. 销售商提供的内核,像Canonical 的Ubuntu 或者 Red Hat 的Fedora,他们的发布版中包含了 预编译的内核,这样的内核使得所需的功能得以充分地启用,并几乎把所有的驱动程序都编译成 模块.这就为大多数硬件作为独立的模块提供了坚实的内核支持.但是,话又说回来,如果你是 一个内核黑客,你应当编译自己的内核,并按自己的意愿决定包括或不包含哪一模块. 内核提供了各种不同的工具来简化内核配置.最简单的一种是一个字符界面下的命令行工具 : $ make con?g 该工具会逐一遍历所有配置项,要求用户选择 yes、no 或是 module(如果是三选一的话) . 由于这个过程往往要耗费掉很长时间,所以,除非你的工作是按小时计费的,否则应该多利用基 于ncurse 库编制的图形界面工具 : $ make menucon?g 或者,是用基于 gtk+ 的图形工具 : $ make gcon?g 这三种工具将所有配置项分门别类放置,比如按"处理器类型和特点" .你可以按类移动、 浏览内核选项,当然也可以修改其值. 这条命令会基于默认的配置为你的体系结构创建一个配置 : $ make defcon?g 尽管这些缺省值有点随意性(在i386 上,据说那就是 Linus 的配置) ,但是,如果你从未配 置过内核,那它们会提供一个良好的开端.赶快行动吧,运行这条命令,然后回头看看,确保为 你的硬件所配置的选项是启用的. 这些配置项会被存放在内核代码树根目录下的 .con?g 文件中.你很容易就能找到它(内核 开发者差不多都能找到) ,并且可以直接修改它.在这里面查找和修改内核选项也很容易.在你修 改过配置文件之后,或者在用已有的配置文件配置新的代码树的时候,你应该验证和更新配置 : $ make oldcon?g 事实上,在编译内核之前你都应该这么做. 配置选项 CONFIG_IKCONFIG_PROC 把完整的压缩过的内核配置文件存放在 /proc/con?g. gz 下,这样当你编译一个新内核的时候就可以方便地克隆当前的配置.如果你目前的内核已经 启用了此选项,就可以从 /proc 下复制出配置文件并且使用它来编译一个新内核 : $ zcat /proc/con?g.gz > .con?g $ make oldcon?g 一旦内核配置好了(不论你是如何配置的) ,就可以使用一个简单的命令来编译它了 : $ make 这跟 2.6 以前的版本不同,你不用在每次编译内核之间都运行 make dep 了—代码之间的 14 第2章依赖关系会自动维护.你也无须再指定像老版本中 bzImage 这样的编译方式或独立地编译模块, 默认的 Make?le 规则会打点这一切. 2.3.2 减少编译的垃圾信息 如果你想尽量少地看到垃圾信息,却又不希望错过错误报告与警告信息的话,你可以用以下 命令来对输出进行重定向 : $ make > .. /detritus 一旦你需要查看编译的输出信息,你可以查看这个文件.不过,因为错误和警告都会在屏幕 上显示,所以你需要看这个文件的可能性不大.事实上,我只不过输入如下命令 : $ make > /dev/null 就可把无用的输出信息重定向到永无返回值的黑洞 /dev/null. 2.3.3 衍生多个编译作业 make 程序能把编译过程拆分成多个并行的作业.其中的每个作业独立并发地运行,这有助 于极大地加快多处理器系统上的编译过程,也有利于改善处理器的利用率,因为编译大型源代码 树也包括 I/O 等待所花费的时间(也就是处理器空下来等待 I/O 请求完成所花费的时间) . 默认情况下,make 只衍生一个作业,因为 Make?les 常会出现不正确的依赖信息.对于不正确 的依赖,多个作业可能会互相踩踏,导致编译过程出错.当然,内核的 Make?les 没有这样的编码 错误,因此衍生出的多个作业编译不会出现失败.为了以多个作业编译内核,使用以下命令 : $ make -jn 这里,n 是要衍生出的作业数.在实际中,每个处理器上一般衍生出一个或者两个作业.例如,在一个 16 核处理器上,你可以输入如下命令 : $ make -j32 > /dev/null 利用出色的 distcc 或者 ccache 工具,也可以动态地改善内核的编译时间. 2.3.4 安装新内核 在内核编译好之后,你还需要安装它.怎么安装就和体系结构以及启动引导工具(boot loader)息息相关了—查阅启动引导工具的说明,按照它的指导将内核映像拷贝到合适的位置, 并且按照启动要求安装它.一定要保证随时有一个或两个可以启动的内核,以防新编译的内核出 现问题. 例如,在使用 grub 的x86 系统上,可能需要把 arch/i386/boot/bzImage 拷贝到 /boot 目录下, 像vmlinuz-version 这样命名它,并且编辑 /etc/grub/grub.conf 文件,为新内核建立一个新的启动 项 .使用 LILO 启动的系统应当编辑 /etc/lilo.conf,然后运行 lilo. 所幸,模块的安装是自动的,也是独立于体系结构的.以root 身份,只要运行 : 15 从内核出发 % make modules_install 就可以把所有已编译的模块安装到正确的主目录 /lib/modules 下. 编译时也会在内核代码树的根目录下创建一个 System.map 文件.这是一份符号对照表,用 以将内核符号和它们的起始地址对应起来.调试的时候,如果需要把内存地址翻译成容易理解的 函数名以及变量名,这就会很有用. 2.4 内核开发的特点 相对于用户空间内应用程序的开发,内核开发有一些独特之处.尽管这些差异并不会使开发 内核代码的难度超过开发用户代码,但它们依然有很大不同. 这些特点使内核成了一只性格迥异的猛兽.一些常用的准则被颠覆了,而又必须建立许多全 新的准则.尽管有许多差异一目了然(人人都知道内核可以做它想做的任何事) ,但还是有一些 差异晦暗不明.最重要的差异包括以下几种 : ? 内核编程时既不能访问 C 库也不能访问标准的 C 头文件. ? 内核编程时必须使用 GNU C. ? 内核编程时缺乏像用户空间那样的内存保护机制. ? 内核编程时难以执行浮点运算. ? 内核给每个进程只有一个很小的定长堆栈. ? 由于内核支持异步中断、抢占和 SMP,因此必须时刻注意同步和并发. ? 要考虑可移植性的重要性. 让我们仔细考察一下这些要点,所有内核开发者必须牢记以上要点. 2.4.1 无libc 库抑或无标准头文件 与用户空间的应用程序不同,内核不能链接使用标准 C 函数库—或者其他的那些库也不 行.造成这种情况的原因有许多,其中就包括先有鸡还是先有蛋这个悖论.不过最主要的原因还 是速度和大小.对内核来说,完整的 C 库—哪怕是它的一个子集,都太大且太低效了. 别着急,大部分常用的 C 库函数在内核中都已经得到了实现.比如操作字符串的函数组就 位于 lib/string.c 文件中.只要包含 头文件,就可以使用它们. 头文件 当我在本书中谈及头文件时,都指的是组成内核源代码树的内核头文件.内核源代码文 件不能包含外部头文件,就像它们不能用外部库一样. 基本的头文件位于内核源代码树顶级目录下的 include 目录中.例如,头文件 对应内核源代码树的 include/linux/inotify.h. 体系结构相关的头文件集位于内核源代码树的 arch//include/asm 目录下.例如,如果编译的是 x86 体系结构,则体系结构相关的头文件就是 arch/x86/include/asm.内核 代码通过以 asm/ 为前缀的方式包含这些头文件,例如 . 16 第2章在所有没有实现的函数中,最著名的就数 printf() 函数了.内核代码虽然无法调用 printf(), 但它提供的 printk() 函数几乎与 printf() 相同.printk() 函数负责把格式化好的字符串拷贝到内核 日志缓冲区上,这样,syslog 程序就可以通过读取该缓冲区来获取内核信息.printk() 的用法很 像printf() : printk("Hello world! A string:'%s' and an integer:'%d'\n", str, i); printk() 和printf() 之间的一个显著区别在于,printk() 允许你通过指定一个标志来设置优先 级.syslogd 会根据这个优先级标志来决定在什么地方显示这条系统消息.下面是一个使用这种 优先级标志的例子 : printk(KERN_ERR "this is an error!\n"); 注意 在KERN_ERR 和要打印的消息之间没有逗号,这样写是别有用意的.优先级标志是 预处理程序定义的一个描述性字符串,在编译时优先级标志就与要打印的消息绑在一 起处理.贯穿整本书,我们会使用 printk(). 2.4.2 GNU C 像所有自视清高的 Unix 内核一样,Linux 内核是用 C 语言编写的.让人略感惊讶的是,内 核并不完全符合 ANSI C 标准.实际上,只要有可能,内核开发者总是要用到 gcc 提供的许多语 言的扩展部分. (gcc 是多种 GNU 编译器的集合,它包含的 C 编译器既可以编译内核,也可以编 译Linux 系统上用 C 语言写的其他代码. ) 内核开发者使用的 C 语言涵盖了 ISO C99 标准和 GNU C 扩展特性.这其中的种种变化把 Linux 内核推向了 gcc 的怀抱,尽管目前出现了一些新的编译器如 Intel C,已经支持了足够多的 gcc 扩展特性,完全可以用来编译 Linux 内核了.最早支持 gcc 的版本是 3.2,但是推荐使用 gcc 4.4 或之后的版本.Linux 内核用到的 ISO C99 标准的扩展没有什么特别之处,而且 C99 作为 C 语言官方标准的修订本,不可能有大的或是激进的变化.让人感兴趣的,与标准 C 语言有区别的, 通常也是人们不熟悉的那些变化,多数集中在 GNU C 上.就让我们研究一下内核代码中所使用到 的C语言扩展中让人感兴趣的那部分吧,这些变化使内核代码有别于你所熟悉的其他项目. 1. 内联(inline)函数 C99 和GNU C 均支持内联函数.inline 这个名称 就可以反映出它的工作方式,函数会在它 所调用的位置上展开.这么做可以消除函数调用和返回所带来的开销(寄存器存储和恢复) .而且,由于编译器会把调用函数的代码和函数本身放在一起进行优化,所以也有进一步优化代码的 可能.不过,这么做是有代价的(天下没有免费的午餐) ,代码会变长,这也就意味着占用更多 ISO C99 是ISO C 的最新修订版.C99 相对于前一个修订版 C90 做了许多加强,ISO C99 引入了指定初始化, 可变长度的数组,C++ 风格的注释,long long 和complex 数据类型,但是 linux 内核只使用了 C99 特性的一个 子集. 译者注 : inline 翻译成内联似乎并不贴切,直译应该是"在字里行间展开"的意思,不过约定俗成,我们也把 它翻译成"内联" . 17 从内核出发 的内存空间或者占用更多的指令缓存.内核开发者通常把那些对时间要求比较高,而本身长度 又比较短的函数定义成内联函数.如果一个函数较大,会被反复调用,且没有特别的时间上的限 制,我们并不赞成把它做成内联函数. 定义一个内联函数的时候,需要使用 static 作为关键字,并且用 inline 限定它.比如 : static inline void wolf(unsigned long tail_size) 内联函数必须在使用之前就定义好,否则编译器就没法把这个函数展开.实践中一般在头文 件中定义内联函数.由于使用了 static 作为关键字进行限制,所以编译时不会为内联函数单独建 立一个函数体.如果一个内联函数仅仅在某个源文件中使用,那么也可以把它定义在该文件开始 的地方. 在内核中,为了类型安全和易读性,优先使用内联函数而不是复杂的宏. 2. 内联汇编 gcc 编译器支持在 C 函数中嵌入汇编指令.当然,在内核编程的时候,只有知道对应的体系 结构,才能使用这个功能. 我们通常使用 asm() 指令嵌入汇编代码.例如,下面这条内联汇编指令用于执行 x86 处理器 的rdtsc 指令,返回时间戳(tsc)寄存器的值 : unsigned int low, high; asm volatile("rdtsc" : "=a" (low), "=d" (high)); /* low 和high 分别包含 64 位时间戳的低 32 位和高 32 位*/ Linux 的内核混合使用了 C 语言和汇编语言.在偏近体系结构的底层或对执行时间要求严格 的地方,一般使用的是汇编语言.而内核其他部分的大部分代码是用 C 语言编写的. 3. 分支声明 对于条件选择语句,gcc 内建了一条指令用于优化,在一个条件经常出现,或者该条件很少 出现的时候,编译器可以根据这条指令对条件分支选择进行优化.内核把这条指令封装成了宏, 比如 likely() 和unlikely(),这样使用起来比较方便. 例如,下面是一个条件选择语句 : if (error) { } 如果想要把这个选择标记成绝少发生的分支 : /* 我们认为 error 绝大多数时间都会为 0...*/ if (unlikely(error)) { } 相反,如果我们想把一个分支标记为通常为真的选择 : /* 我们认为 success 通常都不会为 0 */ if (likely(success)) { } 18 第2章在你想要对某个条件选择语句进行优化之前,一定要搞清楚其中是不是存在这么一个条件, 在绝大多数情况下都会成立.这点十分重要 : 如果你的判断正确,确实是这个条件占压倒性的地 位,那么性能会得到提升 ; 如果你搞错了,性能反而会下降.正如上面这些例子所示,通常在对 一些错误条件进行判断的时候会用到 unlikely() 和likely().你可以猜到,unlikely() 在内核中会得 到更广泛的使用,因为 if 语句往往判断一种特殊情况. 2.4.3 没有内存保护机制 如果一个用户程序试图进行一次非法的内存访问,内核就会发现这个错误,发送 SIGSEGV 信号,并结束整个进程.然而,如果是内核自己非法访问了内存,那后果就很难控制了. (毕竟, 有谁能照顾内核呢?)内核中发生的内存错误会导致 oops,这是内核中出现的最常见的一类错 误.在内核中,不应该去做访问非法的内存地址,引用空指针之类的事情,否则它可能会死掉, 却根本不告诉你一声—在内核里,风险常常会比外面大一些. 此外,内核中的内存都不分页.也就是说,你每用掉一个字节,物理内存就减少一个字节. 所以,在你想往内核里加入什么新功能的时候,要记住这一点. 2.4.4 不要轻易在内核中使用浮点数 在用户空间的进程内进行浮点操作的时候,内核会完成从整数操作到浮点数操作的模式转 换.在执行浮点指令时到底会做些什么,因体系结构不同,内核的选择也不同,但是,内核通常 捕获陷阱并着手于整数到浮点方式的转变. 与用户空间进程不同,内核并不能完美地支持浮点操作,因为它本身不能陷入. 在内核中 使用浮点数时,除了要人工保存和恢复浮点寄存器,还有其他一些琐碎的事情要做.如果要直截 了当地回答,那就是 : 别这么做了,除了一些极少的情况,不要在内核中使用浮点操作. 2.4.5 容积小而固定的栈 用户空间的程序可以从栈上分配大量的空间来存放变量,甚至巨大的结构体或者是包含数以 千计的数据项的数组都没有问题.之所以可以这么做,是因为用户空间的栈本身比较大,而且还 能动态地增长(年长的开发者回想一下 DOS 那个年代,这种低级的操作系统即使在用户空间也 只有固定大小的栈) . 内核栈的准确大小随体系结构而变.在x86 上,栈的大小在编译时配置,可以是 4KB 也可 以是 8KB.从历史上说,内核栈的大小是两页,这就意味着,32 位机的内核栈是 8KB,而64 位 机是 16KB,这是固定不变的.每个处理器都有自己的栈. 关于内核栈的更多内容,会在后面的章节中讨论. 2.4.6 同步和并发 内核很容易产生竞争条件.和单线程的用户空间程序不同,内核的许多特性都要求能够并发 地访问共享数据,这就要求有同步机制以保证不出现竞争条件,特别是 : 19 从内核出发 ? Linux 是抢占多任务操作系统.内核的进程调度程序即兴对进程进行调度和重新调度.内 核必须和这些任务同步. ? Linux 内核支持对称多处理器系统(SMP) .所以,如果没有适当的保护,同时在两个或两 个以上的处理器上执行的内核代码很可能会同时访问共享的同一个资源. ? 中断是异步到来的,完全不顾及当前正在执行的代码.也就是说,如果不加以适当的保 护,中断完全有可能在代码访问资源的时候到来,这样,中段处理程序就有可能访问同一 资源. ? Linux 内核可以抢占.所以,如果不加以适当的保护,内核中一段正在执行的代码可能会 被另外一段代码抢占,从而有可能导致几段代码同时访问相同的资源. 常用的解决竞争的办法是自旋锁和信号量. 我们将在后面的章节中详细讨论同步和并发执行. 2.4.7 可移植性的重要性 尽管用户空间的应用程序不太注意移植问题,然而 Linux 却是一个可移植的操作系统,并且 要一直保持这种特点.也就是说,大部分 C 代码应该与体系结构无关,在许多不同体系结构的 计算机上都能够编译和执行,因此,必须把与体系结构相关的代码从内核代码树的特定目录中适 当地分离出来. 诸如保持字节序、64 位对齐、不假定字长和页面长度等一系列准则都有助于移植性.对移 植性的深度讨论将在后面的章节中进行. 2.5 小结 毫无疑义,内核有独一无二的特质.它实施自己的规则和奖罚措施,拥有整个系统的最高管 理权.当然,Linux 内核的复杂性和高门槛与其他大型软件项目并无差异.在内核开发之路上 最重要的步骤是要意识到内核并没有那么可怕.陌生是肯定的,但真的就不可逾越?事实并非 如此. 本章和以前的章节为贯穿本书剩余章节所讨论的主题奠定了基础.在后续的每一章中,我 们都会涵盖内核的一个具体概念或子系统.在探索的征途中,最重要的是要阅读和修改内核源 代码,只有通过实际的阅读和实践才会理解内核.内核源代码是可以免费获取的,直接用就可 以了!
  • 下载地址 (推荐使用迅雷下载地址,速度快,支持断点续传)
  • 免费下载 PDF格式下载
  • 您可能感兴趣的
  • 易语言字节集到文本  易语言字节集转换工具  易语言字节集转换  易语言字节集替换教程  易语言替换字节集  易语言文本转字节集  易语言写内存字节集  e语言把图片变成字节  汇编语言双字节除法  汇编语言多字节加法