1、Linux 2.6 内核标准教程 前言前言写作背景自由、开放的 Linux 操作系统正在蓬勃发展,得到了广泛的应用,Linux 操作系统的用户数量迅速增长,很多 Linux 爱好者希望能够学习、掌握 Linux 内核的原理、机制,能够阅读 Linux 内核代码,并能够加以应用,但在实际的学习过程中常遇到以下问题。“Linux 内核学习门槛较高,初学者总想迅速读懂内核源代码,往往在不清楚内核运行机制和内核代码结构的情况下就开始阅读 Linux 内核代码,会遇到很大障碍。“随着 Linux 内核的发展,内核的代码量日益增加,系统规模不断扩大,复杂度不断提高。如何在纷繁芜杂的代码中找到自己所需的信息
2、,是一个亟待解决的问题。“研究、学习过程中不仅需要掌握 C 语言、操作系统方面的知识,而且还要需要掌握汇编语言、内联汇编、编译器、链接器、链接脚本等各方面的知识。因此,读者亟需这样一本书。“能够在需要的地方深入浅出地讲解研究、学习 Linux 内核所需的知识点。“能够对内核的核心框架进行全面剖析,引导读者走出由错综复杂的函数和数据结构组成的迷宫,使读者可以在较短的时间内掌握 Linux 内核的精髓。本书特点(1)基于 Linux 2.6 版本内核进行讲解。相对于早期版本,本书讨论的 Linux 2.6 版本内核已经非常成熟,具有 O(1)调度算法、改进的 NPTL 线程模型、内核态抢占等新的特
3、性,具有良好的响应能力(软实时)。这些重要特性保证了 Linux 2.6 版本内核具有非常广泛的实际应用价值,更适用于实际产品的开发。(2)专注于 Linux 内核核心模块,使读者在较短的时间内掌握 Linux 的精髓。对于 Linux 2.6 版本内核这一“庞然大物“,本书选取了内核的核心关键模块,在有限的篇幅内对 Linux 内核的工作原理进行深入、透彻的讲解。(3)分析和讲解细致、透彻。本书对每个内核核心模块,都深入剖析其数据结构、访问接口、工作机制和内核实现。本书给出了 Linux 内核中每个内核模块运作机制的基本轮廓,并用示意图加以说明,帮助读者掌握相应的运作模型,并对内核实现的关键
4、细节、关键代码进行详细分析。本书主要内容第 1 章首先对内核的目录结构进行了介绍,然后介绍了 Linux 2.6 内核的新特性,最后介绍了内核探索工具和阅读本书的方法。第 2 章首先详细分析了 Linux 系统在标准 PC 上的引导过程,以及系统控制权交给内核镜像 bzImage 的过程,然后讲解内核的初始化过程,如何为第一个 C 函数设置所需的运行环境,并分析了系统初始化入口函数 start_kernel (),使读者了解 Linux 系统最基本的初始化过程,该过程对理解其他内核模块的初始化有重要意义。第 3 章主要讲解内存管理,这是 Linux 内核中最复杂、最核心的内核模块。本章首先介绍
5、了 IA32 体系结构提供的内存管理机制-分段机制和分页机制,讨论了两者间的关系和Linux 内核所做的取舍;随后详细分析了内核页表的初始化过程,并对 Linux 内核的内存模型进行了讲解,分析 Linux 是如何对 NUMA 架构提供支持的;最后讲解了物理页框的分配、回收过程,并对内核地址空间的划分和用途进行分析。第 4 章主要讲解 Linux 内核中进程、线程的概念,对其所涉及的关键数据结构进行了讲解,分析了 Linux 内核中进程组织形式和它们各自的用途,对进程的创建过程进行了详细的讨论和分析;然后详细介绍 2.6 版本内核中新采用的 O(1)复杂度调度器的基本思想和实现细节;最后对系统
6、中的第一个进程(0 号进程)的创建过程进行详细分析。第 5 章所讲解的中断、异常机制是计算机系统的核心,系统调用和时间度量都是建立在该机制之上的。本章首先介绍了 IA32 体系结构的中断、异常机制,然后讲解了中断描述符表的初始化过程以及中断、异常处理过程中计算机软、硬件的工作状态和处理方法,最后讲解中断延迟问题。第 6 章详细讲解了内核时间度量的架构和需要的硬件支持,并对时钟中断的处理过程进行分析,最后讲解 Linux 内核软定时器的工作原理和实现细节。第 7 章首先讲解系统调用接口的作用和访问手段,然后讲解系统调用的工作机制和参数传递问题,并介绍如何向内核添加系统调用,最后讲解 IA32 体
7、系架构引入的快速系统调用指令和 Linux 内核对其提供的支持。第 8 章主要讲解 Linux 内核各种模块所使用的同步机制,首先讲解同步的基本原理,然后对构建在基本同步原理之上的同步机制进行了详细的讲解。参与本书编写的人员本书内容来源于华清远见嵌入式培训中心(http:/ 、武凡琦、陈群星、罗璋、赵炳和、崔海峰、孙琼、田旭、范文庆、钟金鑫、王欣、张曦文、尚玉珊、张丛辉、王玮、刘超、张圣亮、李凡、马堃、徐路迎、赵国锋、孙颂武、汪荷君、孙明、林雪梅、张墨、黄惠英、刘雯、张墨、郭永红、周瑜、王建伟等,在此表示衷心的感谢。本书之外的内容河秦是王宗涛的笔名,用以表达他和他爱人对家乡-河底、秦王寨的深厚
8、感情,感谢家人们一年多来所给与的支持和鼓励,没有他们的支持和鼓励,很难有本书的出版。联系我们由于笔者水平有限、编写时间仓促,书中难免存在疏漏和不足之处,恳请广大读者提出宝贵意见。本书的相关资料和嵌入式系统相关资料、公开视频,请参见http:/www.farsight. download。我们为本书开通了专门的配套网站,网址是 http:/ 者 2008 年月Linux 2.6 内核标准教程 目录目录第 1 章 Linux 内核学习基础 11.1 为什么研究 Linux 内核 21.1.1 Linux 的历史来源 21.1.2 Linux 的发展现状 31.1.3 Linux 的前景展望 31
9、.2 选择什么版本进行研究 31.3 内核基本结构 41.3.1 内核在操作系统中的地位 41.3.2 Linux 2.6 内核源代码目录树简介 51.3.3 Linux 2.6 内核的新特性 81.4 如何阅读本书 91.4.1 内核探索工具 101.4.2 推荐阅读方法 12第 2 章 引导过程分析 142.1 内核镜像的构建过程 152.1.1 编译内核的步骤及分析 152.1.2 内核镜像构建过程分析 162.2 系统引导过程分析 182.2.1 傀儡引导扇区 182.2.2 探测系统资源 212.2.3 解压内核镜像 352.2.4 进入保护模式 402.2.5 系统最终初始化 47
10、2.3 系统引导过程总结 47第 3 章 内存管理 503.1 基础知识 513.1.1 存储器地址 513.1.2 分段机制 523.1.3 分页机制 593.2 内核页表的初始化过程 653.2.1 启用分页机制 653.2.2 构建内核页表 683.3 物理内存的描述方法 763.3.1 内存节点 773.3.2 内存区域 813.3.3 物理页框 853.4 物理内存的初始化过程 863.4.1 探测系统物理内存 873.4.2 初始化内存分配器 893.5 物理内存的分配与回收 1013.5.1 伙伴分配算法 1013.5.2 对象缓冲技术 1033.6 内核地址空间 1053.6.
11、1 常规映射地址空间 1053.6.2 固定映射地址空间 1073.6.3 长久内核映射空间 1093.6.4 临时内核映射空间 1163.6.5 非连续映射地址空间 119第 4 章 进程管理 1284.1 进程与线程的概念 1294.1.1 程序与进程 1294.1.2 进程与线程 1294.2 进程描述符 1314.2.1 进程标识符 1324.2.2 进程的状态 1324.2.3 进程上下文 1344.2.4 当前进程 1394.3 进程的组织形式 1434.3.1 进程标识符构成的哈希表 1434.3.2 所有进程构成的双向链表 1484.3.3 执行态进程组成的运行队列 1494.
12、3.4 阻塞态进程组成的等待队列 1524.4 进程的创建过程 1554.4.1 进程创建的接口函数 1564.4.2 进程创建的处理过程 1624.5 进程调度算法 1774.5.1 进程的分类 1784.5.2 进程优先级 1784.5.3 时间片分配 1814.5.4 进程调度时机 1824.6 进程切换过程分析 1834.6.1 选取合适进程 1834.6.2 完成上下文切换 1844.7 空闲进程的初始化 1874.7.1 空闲进程的内核态栈 1874.7.2 空闲进程的内存描述符 1884.7.3 空闲进程的硬件上下文 1904.7.4 空闲进程的任务状态段 190第 5 章 中断
13、和异常 1925.1 基础知识 1935.1.1 中断和异常的定义 1935.1.2 中断和异常的分类 1935.1.3 中断和异常的对比 1945.2 处理机制 1955.2.1 IA32 架构下的处理机制 1955.2.2 Linux 内核的实现策略 2005.3 中断描述符表的初始化 2045.3.1 中断描述符表的初步初始化 2045.3.2 中断描述符表的最终初始化 2065.4 具体处理过程 2165.4.1 公用的硬件处理阶段 2175.4.2 中断的软件处理阶段 2185.4.3 异常的软件处理阶段 2295.5 延迟处理机制 2335.5.1 softirq 延迟处理 234
14、5.5.2 tasklet 延迟处理 2395.5.3 work queue 延迟处理 242第 6 章 时间度量 2496.1 硬件支持 2506.1.1 实时钟 RTC 2506.1.2 系统时钟 2506.2 软件架构 2526.2.1 相对时间 2526.2.2 墙上时间 2576.2.3 内核定时器 2576.3 时间度量的初始化过程 2606.3.1 内核定时器初始化 2606.3.2 系统时钟的初始化 2636.3.3 初始化时钟中断源 2656.4 时钟中断处理过程 2666.4.1 找回遗失的时钟中断 2676.4.2 更新 jiffies_64、xtime 2696.4.3
15、 对当前进程记账 2716.4.4 时钟中断处理小结 2726.5 内核定时器工作原理 2736.5.1 初始化内核定时器节点 2736.5.2 激活内核定时器节点 2736.5.3 内核定时器的处理过程 2776.6 微秒级延迟 2806.6.1 微妙级延迟的访问接口 2816.6.2 微妙级延迟的实现方法 281第 7 章 系统调用 2857.1 系统服务接口的种类 2867.1.1 系统调用接口 2867.1.2 应用编程接口 2867.2 系统调用的访问手段 2867.2.1 使用封装函数 2867.2.2 使用通用接口 2877.2.3 使用内嵌汇编 2887.3 系统调用的工作机制
16、 2897.3.1 系统调用的基本要素 2907.3.2 系统调用门的初始化 2927.3.3 系统调用的处理过程 2927.4 系统调用的参数传递 2977.4.1 少量参数的情况 2977.4.2 大量参数的情况 2987.5 如何添加新系统调用 2997.5.1 前期准备工作 2997.5.2 添加处理函数 3007.5.3 测试新系统调用 3017.6 什么是快速系统调用 3027.6.1 工作机制 3027.6.2 实现策略 3057.6.3 处理过程 309第 8 章 内核同步机制 3128.1 同步基本原理 3138.1.1 原子变量 3138.1.2 中断禁用 3158.1.3
17、 内核态抢占 3168.2 系统引导过程分析 3188.2.1 普通自旋锁 3188.2.2 读写自旋锁 3258.2.3 顺序自旋锁 3318.3 信号量机制 3348.3.1 普通信号量 3358.3.2 读写信号量 3398.4 其他同步机制 3408.4.1 每处理器变量 3418.4.2 RCU 同步机制 345附录 A Linux 内核双向链表 350A.1 内核链表表头 351A.2 内核链表遍历 352A.3 内核链表遍历 353附录 B 跟踪调试内核 354B.1 安装辅助工具 355B.2 准备内核镜像 355B.3 准备根文件系统 355B.4 进行源码级调试 356附录
18、 C Linux 内核汇编语法 358C.1 常规汇编语法 359C.1.1 寄存器前缀 359C.1.2 立即数前缀 359C.1.3 操作数顺序 359C.1.4 操作数宽度 359C.1.5 内存寻址格式 359C.2 内嵌汇编语法 360C.2.1 内嵌汇编举例 360C.2.2 内嵌汇编格式-格式框架 361C.2.3 内嵌汇编格式-语句模板 361C.2.4 内嵌汇编格式-输出列表 361C.2.5 内嵌汇编格式-输入列表 362C.2.6 内嵌汇编格式-修饰字符 362C.2.7 内嵌汇编格式-破坏描述 364附录 D 参考文献 366D.1 关于 IA32 体系结构的资源 36
19、7D.2 关于 Linux 内核的相关资源 367D.3 关于计算机基本原理的资源 367D.4 其他相关资源 368Linux 2.6 内核标准教程 序序GNU/Linux 迅猛发展、无处不在,渗透到人类生产、生活的各个角落,为人类创造了巨大的价值;不仅如此,基于 GPL 自由、开放的 Linux 内核还为深入学习操作系统原理、了解高性能计算机系统提供了一个优秀的平台。随着 Linux 内核的不断发展,各种新特性被添加进来,内核的规模越来越大,整个系统也越来越复杂,内核的学习难度不断增加,广大 Linux 爱好者非常需要一本能以简单的方式透彻分析 Linux 内核的图书。本书语言简洁、流畅,
20、并用大量的图表直观、形象地道出了 Linux 内核实现的精妙之处;将带领读者进入 Linux 内核的神奇世界,是一本不可多得的好书,希望能在 Linux 的普及、推广中发挥重大作用。周立功第 6 章 时间度量时间度量在一个操作系统中占据着重要的地位,操作系统中的很多活动都离不开时间度量。例如,判断当前进程的时间片用完、并进行调度,向用户进程提供当前的年月日,在一段时间(如 3 秒钟)后执行一个任务,在将来的某一时刻执行一个任务等。一般来说,时间度量子系统需要使用和维护两种类型的时间:墙上时间 xtims,相对时间 jiffies。这两种不同类型的时间有不同的用途,计算机系统采用不同的机制对这两
21、种时间进行度量和维护。本章随后围绕这两个主体进行阐述,从硬件支持到软件架构;从时间度量模块的初始化到如何使用时间度量的工作机制;从软件定时器的使用到软件定时器的工作原理。内容简介如下。6.1、6.2 节介绍时间度量所需的硬件和软件架构。6.3 节讲解系统时钟度量子系统的初始化过程,介绍了内定时期、系统时钟中断源、时钟中断处理函数的初始化过程。6.4 节详细讨论了时钟中断的处理过程,介绍了时钟中断的处理过程和时钟中断下半部完成的工作。6.5 节对内核定时进行专门的论述,详细介绍了内核定时器的工作原理。6.6 节讨论了 Linux 内核中微秒级延迟的实现方法。6.1 硬件支持本节从硬件角度介绍了
22、PC 架构下的时钟源,为系统所需的两种不同时间度量提供所需的硬件支持。6.1.1 实时钟 RTC从第一台 IBM 个人计算机起,所有的个人计算机都包含一个实时钟(Real Time Clock,简称为 RTC)的时钟芯片。该芯片内部拥有 256 字节的 CMOS RAM,用于记录系统当前时刻距 UTC 时间 1970/01/01/00:00:00 的秒数和 BIOS 配置参数。此芯片靠主板上的电池供电,因此当计算机关掉后,时钟芯片仍能正常工作。早期的 RTC 芯片是 Motorola 的MC146818 或其他兼容的芯片。随着计算机工业的发展,实时钟芯片已经集成到系统的南桥芯片中了,如在 In
23、tel 2005 年 5 月发布的 945G 的芯片组中的南桥芯片 82801GR ICH7R 中,就集成了一个 MC146818 兼容的实时钟功能模块。实时钟芯片主要用于向系统提供墙上时间(wall time)。在系统初始化的过程中,内核会通过函数 get_cmos_time()读取实时钟芯片内所保存的时间来初始化系统的墙上时间。此外,实时钟芯片还可用来产生周期信号,这样该设备就能被当作一个定时器使用,产生的定时信号通过 8 号中断向量提交给系统。实时钟芯片也可用于在指定的时间唤醒计算机。对操作系统而言,RTC 仅作为一个外部计时设备而存在,Linux 内核仅在系统初始化过程中根据 RTC
24、的值来设置系统的绝对时间,系统启动以后的计时与该时钟源不再有联系。读者可以与下一节中的系统时钟进行比较。6.1.2 系统时钟操作系统应该具备在将来某个时刻调度某个任务的能力,所以需要一种能保证任务准时调度运行的机制。该机制的核心就是系统时钟。与实时钟 RTC 不同,系统时钟是定时器硬件和系统软件的结合。1系统时钟中断源系统时钟硬件在通过编程配置后可以产生一定频率的中断。在个人计算机中,与该中断相关的中断向量号是 0。系统软件通过累计从开机到现在产生该中断的次数来维护系统时间,形成系统时钟。本小节主要介绍在个人计算机中常见的、可以用于系统时钟的硬件定时器。(1)8254 可编程定时器。当前使用最
25、普遍的定时器硬件芯片是 Intel 8254 可编程定时器芯片(Programable Interval Timer,简称为 PIT),该芯片由一个 1 193 181Hz 的振荡器驱动,含有 3 个独立的通道;每个通道包含一个 16 位的计数器。对于每一个到达的时钟脉冲,通道中计数器中的值减 1,当计数器减到 0 时,相应的通道就会产生一次输出。其中通道 0 的输出连接到了中断控制器,其对应的中断向量号为 0,用于产生系统时钟所需要的滴答;通道 1 的输出在早期的计算机中用于 DRAM 的刷新,新近的计算机系统中有专门的硬件负责 DRAM 的刷新,通道 1 的功能已经不存在了;通道 2 的输
26、出连接到了位于主板上的蜂鸣器(PC Speaker),控制蜂鸣器发出一定频率的声音。这里介绍一下驱动 8254 工作时钟频率的来历。最初的个人电脑设计时出于成本上的考虑,主板上采用了当时广泛用于电视机且价格最便宜的一个 14.318 18MHz 振荡器,该振荡器的频率远远高于系统其他器件所要求的工作频率。设计师采用了 3 分频后得到 4.77MHz驱动中央处理器 8088;采用 4 分频后得到 3.58MHz 信号用于驱动彩色图形适配器;最后将系统各种频率的基频 1.193 181 6MHz(各种频率的最大公约数,即 12 分频)信号用做系统可编程定时器芯片的输入时钟。为了保持兼容性,可编程定
27、时器 8254 一直采用这个频率的时钟作为输入。(2)高精度事件定时器。高精度事件定时器(High Precision Event Timers)被设计用于取代 8254 可编程定时器的全部功能和实时钟 RTC 芯片的周期性中断功能,和 8254 可编程定时器相比,该定时器能产生更高精度的周期性中断;和实时钟 RTC 芯片的周期性中断相比,该定时器能提供更高精度、更宽范围的中断频率。该硬件定时器遵循 Intel 和 Microsoft 联合制定的高精度事件定时器规范。该规范中规定一个高精度事件定时器最多拥有 32 个定时器。通过配置后,timer 0 用于取代 8254可编程定时器所产生的时钟
28、中断;timer 1 作为硬件定时器取代实时钟 RTC 芯片的周期性中断功能;其余的 timer 作为硬件定时器供内核或用户进程直接使用。(3)处理器本地时钟。在多处理器系统中,处理器本地时钟(CPU Local Timer)用于向本地处理器发送时钟中断请求,更新本地处理器上的相对时间 jiffies。2其他辅助时钟源这类辅助时钟源不具备向系统发出中断请求的功能,但有比能产生系统时钟中断的定时器硬件更高的计时精度。在系统时钟中断处理过程中,处理程序可以利用这些时钟的值来完成高精度时间度量,如“6.6 微秒级延迟“中的 udelay、ndelay 就使用了这类的时钟源(如时间戳计数器)完成高精度
29、的延迟。下面介绍常见的这类辅助时钟源。(1)时间戳计数器。从 Pentium 开始,所有的 Intel 处理器都包含一个 64 位的寄存器,该寄存器被称为时间戳记数器(Time Stamp Counter,简称为 TSC)。TSC 在 CPU 的每个时钟信号到来一次时加 1,实际上该寄存器是一个不断增加的计数器,如果处理器的主频为 1GHz,那么 TSC 寄存器的每 1ns 增加 1。汇编指令 rdtsc 可用于读取 TSC 的值。利用 CPU 的 TSC,操作系统通常可以得到更为精准的时间度量。(2)电源管理时钟。内核中,除了使用上面的时间戳记数器作为系统时钟的辅助时钟源外,电源管理时钟(A
30、CPI Power Management Timer)也可作为系统的辅助时钟源。这里对这些时钟源不做详细介绍。3与系统时钟相关的宏定义(1)宏定义 HZ。宏定义 Hz 记录了不同体系结构下,系统时钟所要求的可编程定时器产生中断的频率。在 IA32 体系结构下该宏定义在文件 src/include/asm-i386/param.h 中的第 6 行定义如下:#define HZ CONFIG_HZ /* Internal kernel timer frequency */其中的 CONFIG_HZ 是内核配置选项,该内核配置选项有 3 个频率候选值依次是100Hz、1 000Hz、250Hz,分别
31、用于要求高系统吞吐量的服务器系统、要求快速响应的个人桌面计算机系统以及兼有两种类型应用的计算机系统中。(2)宏定义 CLOCK_TICK_RATE。宏定义 CLOCK_TICK_RATE 记录了不同体系结构下,驱动可编程定时器工作的输入时钟频率。在 IA32 体系结构下该值在文件 src/include/asm-i386/timex.h 中的第 15 行定义如下:#define CLOCK_TICK_RATE 1193182 /* Underlying HZ */其中,数值 1 193 182 是 8254 可编程定时器的输入时钟频率。详情请参见本小节中对8254 可编程定时器的分析。(3)宏
32、定义 LATCH。宏定义 LATCH 记录了上述两个宏定义的比值,用于在内核初始化过程中设置可编程定时器中计数器寄存器 counter 的初始值。在 IA32 体系结构下,该宏定义在文件src/include/ Linux/jiffies.h 中第 46 行定义如下:#define LATCH (CLOCK_TICK_RATE + HZ/2) / HZ) /* For divider */6.2 软件架构本节介绍 Linux 内核中和时间度量密切相关的两个重要变量 jiffies(jiffies_64)、xtime。这两个变量分别用来记录从系统启动直到当前时刻的系统时钟产生的滴答数和系统的墙上
33、时间。6.2.1 相对时间(1)相对时间由系统时钟中断进行维护,存储于系统核心变量 jiffies 中。该变量记录了系统启动到当前时刻为止系统时钟产生中断的次数,用于提示内核或用户进程一段指定的时间已经过去了,与墙上时间没有直接和必然的联系。1jiffies 与 jiffies_64在 Linux 内核中,变量 jiffies 用于记录系统自启动到当前时刻为止系统时钟产生的滴答数;在系统响应时钟中断、进行中断处理过程中,时钟中断处理程序timer_interrupt()将该变量的值加 1。因为一秒钟内产生系统时钟中断次数等于宏定义 HZ的值,所以变量 jiffies 的值在一秒内增加 HZ。由
34、此可知,系统自启动到当前时刻为止运行了 jiffies/HZ 秒。在 Linux 内核 2.4.21 中,变量 jiffies 在文件 src/kernel/timer.c中的第 68 行定义如下。unsigned long volatile jiffies; 可知该变量 jiffies 在 32 位的系统中为 32 位无符号整型,在 64 位系统中为 64 位无符号整型。为了提高时间度量的准确度,Linux 内核 2.6 中提高了系统时钟中断的频率,从 2.4中的 100Hz 提高到 2.6 中最高 1 000Hz(内核 2.6 中,时钟中断频率在内核配置阶段可以选 100、250、1 00
35、0 三个频率值之一)。使得原来工作 497 天才发生溢出的 32 位的jiffies 计数器现在只需 49.7 天就发生溢出了。溢出发生时会给内核时间度量带来混乱和其他潜在的未知问题,所以内核 2.6 中引入了一个 64 位无符号整型变量 jiffies_64,使得系统能在系统时钟频率为 1 000Hz 的情况下运行 584 942 417 年而不发生溢出,从而有效预防了溢出导致的潜在问题(因为任何人的计算机都不可能连续运行这么长的时间)。为了保证兼容性以及访问的效率,内核中保留了 jiffies 这个变量,因为大量的驱动程序使用该变量来进行一些和时间相关的操作;而系统核心使用 64 位的 j
36、iffies_64 变量来记录自系统启动到当前时刻为止的时钟滴答次数。关于 jiffies 变量,在文件src/include/Linux/jiffies.h 中的第 85 行有以下声明:extern unsigned long volatile _jiffy_data jiffies;可知该变量在 32 位的系统中为 32 位无符号整型,在 64 位系统中为 64 位无符号整型,但该变量在内核源代码中没有定义,即没有为之分配存储空间。通过分析连接脚本src/arch/i386/kernel/vmLinux.lds.S,可知该 32 位的变量 jiffies 指向了 64 位变量jiffies
37、_64 的低 32 位,即变量 jiffies 和变量 jiffies_64 共享了部分存储空间。变量jiffies_64 是一个 64 位的无符号整形数据,它在文件 src/kernel/timer.c 中的第 49 行定义如下。这两个变量的关系如图 6.1 所示。u64 jiffies_64 _cacheline_aligned_in_smp = INITIAL_JIFFIES;图 6.1 jiffies 和 jiffies_64 的关系这里将 jiffies_64 变量的值初始化为 INITIAL_JIFFIES,该数值在文件 src/include/ Linux/jiffies.h 中
38、的第 125 行定义为(unsigned long)(unsigned int)(-300*HZ),该数值使得系统在启动后 5 分钟时 jiffies 产生回绕,这样有利于在早期发现设备驱动程序中因 jiffies 回绕导致的逻辑错误,方便驱动程序的开发过程。在设备驱动程序中,通常应该使用 jiffies 变量。在 32 位的系统中访问 64 位的jiffies_64 没有访问 32 位的 jiffies 快,因为在 32 位系统中访问 64 位的变量需要两次内存访问,而且这两次内存访问,而操作之间可能被中断造成读取数据的不正确。对于需要访问 64 位的 jiffies_64 变量的情况(很少
39、,在设备驱动程序中不用访问变量jiffies_64,通常只在内核的核心代码才可能需要访问变量 jiffies_64),内核提供了函数 get_jiffies_64()来访问该 64 位变量。该函数采用了加锁机制防止读取数据的不正确。6.2.1 相对时间(2)2关于 jiffies 比较由上面的分析可知,为了兼容性及性能内核保留了 jiffies 这一变量,在 IA32 体系结构下是 64 位变量的 jiffies_64 的低 32 位,该变量主要由大量的驱动程序使用。在系统时钟中断频率为 1 000Hz 时,32 位的 jiffies 变量每隔 49.7 天就会发生溢出;在发生溢出时,32 位
40、无符号变量 jiffies 从最大值 232-1 变为最小值 0。下面讨论该 32 位变量jiffies 的溢出问题及解决方法。(1)jiffies 溢出导致程序逻辑错。下面通过一段代码来演示 jiffies 溢出造成的逻辑错误问题。示例程序 6.1:jiffies 溢出导致逻辑错误-1. unsigned long timeout;2. 3. init_device();4. 5. timeout = jiffies + HZ;6. 7. while(!device_is_ok()8. 9. if (jiffies timeout)10. 11. timeout();12. exit; 13
41、. 14. 15. 16. other_works();-这个代码片段含义是:首先通过函数 init_device()完成设备的初始化工作,然后通过函数 device_is_ok()判断设备状态是否正常。如果经过了一秒钟的时间设备还没有转化为正常状态,那么调用 timeout 函数进行超时处理。然后退出;如果这段时间内设备转化为正常状态,那么执行函数 other_works()进行下一步处理。该程序的逻辑乍看起来非常正常,没有问题。下面我们来分析一下 jiffies 溢出导致的程序逻辑出错问题。假设执行第 5 行代码时 jiffies 的值处于 232?1?999232?1 范围之内,同时假设
42、宏定义 HZ 的取值为 1 000,那么 timeout 的取值必然在 0 和 999 之间;由于外设的响应速度比较慢,第一次通过函数 device_is_ok()判断设备是否就绪往往不会成功(即该函数返回 FALSE),此时会运行第 913 行来判断是否超时;此时 jiffies 的值很可能没有回绕到最小值,进而必定大于 timeout 的值,使得第一次判断就发生了超时,执行超时处理函数 timeout,然后退出。这显然是个错误。(2)用于 jiffies 比较的宏定义。为了防止 jiffies 回绕造成程序的逻辑错误,Linux 内核中提供了以下 4 个宏定义用于 jiffies 间的比较
43、。time_after(a,b)ime_before(a,b)time_after_eq(a,b)time_before_eq(a,b)采用这 4 个宏定义进行 jiffies 间的比较,可以有效地解决因 jiffies 溢出造成的程序逻辑出错。针对示例程序 6.1 中的逻辑错误,可以通过宏定义 time_after(a,b)加以改正,改正后的代码请参见示例程序 6.2。示例程序 6.2:通过宏定义修复 jiffies 溢出导致逻辑错误-1. unsigned long timeout;2. 3. init_device();4. 5. timeout = jiffies + HZ;6. 7.
44、 while(!device_isok()8. 9. if ( time_after( jiffies , timeout) )10. 11. timeout();12. exit; 13. 14. 15. 16. other_works();-接下来分析为什么使用该宏定义就能消除因 jiffies 溢出导致的程序逻辑错误。这里以宏定义 time_after(a,b)为例进行分析,其他 3 个宏定义在原理上和 time_after(a,b)是一样的,这里不再赘述。宏定义 time_after(a,b)在文件 src/include/Linux/jiffies.h 中的第 109 行开始定义,代
45、码如下:#define time_after(a,b) ( typecheck(unsigned long, a) 1 273 for (j = 0; j tv5.vec + j);1 275 INIT_LIST_HEAD(base-tv4.vec + j);1 276 INIT_LIST_HEAD(base-tv3.vec + j);1 277 INIT_LIST_HEAD(base-tv2.vec + j);1 278 1 279 for (j = 0; j tv1.vec + j);1 281 1 282 base-timer_jiffies = jiffies;1 283 第 1 26
46、81 271 行代码声明了该函数使用的局部变量 j、base,并初始化 base 指向每处理器变量 tvec_bases 的本地拷贝。第 1 272 行代码通过宏定义 spin_lock_init()初始化了保护待处理内核定时器链表的自旋锁。关于该宏定义的工作原理,请参见 8.2 小节。第 1 2731 278 行代码通过宏定义 INIT_LIST_HEAD()初始化了变量tv2、tv3、tv4、tv5 所包含的链表表头。第 1 2791 280 行代码通过宏定义 INIT_LIST_HEAD()初始化了变量 tv1 包含的共 256个链表表头。第 1 282 行代码初始化成员变量 timer
47、_jiffies 的值为系统当前 jiffies 的值,表明关联到当前处理器上的内核定时器的最早超时定时器为当前 jiffies 所代表的时刻。6.3.2 系统时钟的初始化(1)系统时钟初始化由函数 time_init()进行处理,该函数在系统初始化过程中被位于文件 src/init/main.c 中第 445 行的系统初始化主函数 start_kernel()调用。函数time_init()初始化了系统的墙上时间、系统时钟源、系统时钟中断处理函数,下面对这三部分进行阐述。1墙上时间初始化系统墙上时间初始化由函数 time_init()完成,该函数通过读取实时钟芯片 RTC 所保存的值初始化了
48、系统核心变量 xtime。下面对该函数进行详细的分析。代码清单 6.6-函数 time_init功能简介:该函数负责初始化系统墙上时间 ximte,为系统选择合适的时钟中断源,并为之设置时钟中断处理函数。文件:src/arch/i386/kernel/time.c464 void _init time_init(void)465 466 #ifdef CONFIG_HPET_TIMER467 if (is_hpet_capable() 468 /*469 * HPET initialization needs to do memory-mapped io. So, let470 * us do a late initialization after mem_init().471 */472 late_time_init =