Linux 版 (精华区)
发信人: netiscpu (网中自由鸟), 信区: Linux
标 题: Linux实用教程(部分)(8)
发信站: 哈工大紫丁香 (Thu May 20 18:14:40 1999), 转信
第十五章
其 他 内 核 机 制
15.1 底半处理
我们知道,发生中断时,处理器要停止当前正在执行的指令,而操作系统负责将中断发
送到对应的设备驱动程序去处理。在中断的处理过程中,系统不能进行其他任何工作,
因此,在这段时间内,设备驱动程序要以最快的速度完成中断处理,而其他大部分工作
在中断处理过程之外进行。Linux 内核利用底半处理过程帮助实现中断的快速处理。
图 15-1 是与底半处理过程相关的内核数据结构。bh_base 代表的指针数组中可包含
32 个不同的底半处理过程。bh_mask 和 bh_active 的数据位分别代表对应的底半处理
过程是否安装和激活。如果 bh_mask 的第 N 位为 1,则说明 bh_base 数组的第 N 个
元素包含某个底半处理过程的地址;如果 bh_active 的第 N 位为 1,则说明必须由调
度程序在适当的时候调用第 N 个底半处理过程。
图 15-1 底半处理数据结构
bh_base 数组的索引是静态定义的,定时器底半处理过程的地址保存在第 0 个元素中
,控制台底半处理过程的地址保存在第 1 个元素中等等。典型来说,每个底半处理过
程和相应的任务队列关联。当 bh_mask 和 bh_active 表明第 N 个底半处理过程已被
安装且处于活动状态,则调度程序会调用第 N 个底半处理过程,该底半处理过程最终
会处理与之相关的任务队列中的各个任务。因为调度程序从第 0 个元素开始依次检查
每个底半处理过程,因此,第 0 个底半处理过程具有最高的优先级,第 31 个底半处
理过程的优先级最低。
内核中的某些底半处理过程是和特定设备相关的,而其他一些则更一般一些。表 15-1
列出了内核中通用的底半处理过程。
表 15-1 Linux 中通用的底半处理过程
TIMER(定时器)
在每次系统的周期性定时器中断中,该底半处理过程被标记为活动状态,并用来驱动
内核的定时器队列机制。
CONSOLE(控制台)
该处理过程用来处理控制台消息。
TQUEUE(TTY 消息队列)
该处理过程用来处理 tty 消息。
NET(网络)
该处理过程用于一般网络处理。
IMMEDIATE(立即)
这是一个一般性处理过程,许多设备驱动程序利用该过程对自己要在随后处理的任务
进行排队。
当某个设备驱动程序,或内核的其他部分需要将任务排队进行处理时,它将任务添加到
适当的系统队列中(例如,添加到系统的定时器队列中),然后通知内核,表明需要进
行底半处理。为了通知内核,只需将 bh_active 的相应数据位置为 1。例如,如果驱
动程序在 immediate 队列中将某任务排队,并希望运行 IMMEDIATE 底半处理过程来处
理排队任务,则只需将 bh_active 的第 8 位置为 1。在每个系统调用结束并返回调用
进程之前,调度程序要检验 bh_active 中的每个位,如果有任何一位为 1,则相应的
底半处理过程被调用。每个底半处理过程被调用时,bh_active 中的相应为被清除。b
h_active 中的置位只是暂时的,在两次调用调度程序之间 bh_active 的值才有意义,
如果 bh_active 中没有置位,则不需要调用任何底半处理过程。
15.2 任务队列
任务队列是内核将任务延迟到以后处理的一种方法。任务队列和底半处理过程经常结合
起来使用,例如,定时器任务队列在定时器底半处理过程中进行处理。任务队列的数据
结构很简单,实际就是普通的单向链表结构,见图 15-2,每个 tq_struct 数据结构作
为任务队列的节点,包含了一个例程地址和指向一些数据的指针。当任务队列中的节点
被处理时,将调用例程并传递数据指针。
内核中的任何部分(例如驱动程序)都可以建立并使用任何队列,由内核建立并维护的
三个一般性任务队列在表 15-2 中列出。
表 15-2 Linux 内核中三个一般性任务队列
TIMER(定时器)
该队列用来排队需要在系统时钟滴答之后尽可能快地完成的任务。每次时钟滴答时,
如果该队列中包含有元素,则定时器队列底半处理例程标记为活动状态。在随后运行的
调度程序中,定时器队列底半处理例程被调用,从而定时器队列中排队的任务也被处理
。
IMMEDIATE(立即)
该队列在调度程序处理活动的底半处理程序时处理。因为 IMMEDIATE 底半处理过程的
优先级较低,因此比起定时器底半处理过程,对这些任务的处理要稍微拖后一些。
SCHEDULER(调度程序)
该任务队列由调度程序直接处理。该队列用来支持系统中的其他任务队列,这种情况
下,要运行的任务实际是处理某个任务队列的例程。
图 15-2 任务队列数据结构
在处理任务队列时,队列中第一个元素从队列中移出,并用空指针代替。移出操作必须
是一个原子操作,也就是说,是不能被中断的操作。队列中每个处理例程依次调用。队
列中的元素通常是静态分配的数据,因为没有内建用来丢弃已分配内存的机制,因此,
任务队列的处理过程简单移向后续的链表元素。对已分配内核内存的清除工作由任务本
身完成。
15.3 时间和定时器
对一个操作系统来说,它必须具有调度未来任务的能力。如果必须在相对精确的时间内
调度某个任务,操作系统必须具有一定的机制来实现该功能。为了给操作系统提供这样
的机制,PC 机中一般存在一个可编程的间隔定时器,定时器可以以指定的时间周期性
地中断处理器。这种周期性的定时器中断在操作系统中一般称为时钟滴答,它的作用就
象音乐中的节拍一样,协调着系统中所有的活动。
除此之外,操作系统还必须具备一定的接口记录系统的时间,并为程序提供时间服务。
一般来讲,操作系统和计算机硬件一起维护着系统中的时间。在 PC 机中,Linux 利用
BIOS CMOS 中记录的时间(称为“硬件时钟”)作为系统启动时的时间基准,而在系
统运行时,利用时钟滴答测量系统的时间(称为“软件时钟”)。例如,启动时从 CM
OS 中读取的时间为 1998.2.1 0:00:00,假设系统的时钟滴答为每秒 100 次,则经过
1000 次时钟滴答之后,时间为 1998.2.1 0:00:10。Linux 利用 jiffies(瞬时)作
为系统时间的测量基准,所有的时间都从 1970.1.1 0:00:00 开始计算,系统启动时,
将 CMOS 中记录的时间转化为从 1970.1.1 0:00:00 算起的 jiffies 值。在 Linux 内
核中,时间以格林尼治时间记录,将格林尼治时间转换为本地时间的任务则由应用程序
负责,实际上,Linux 内核中没有任何时区的概念。
利用上面的方法记录时间是有问题的,类似现在的 2000 年问题。假定一个 jiffies
等于 1/100 秒,并利用 32 位无符号长整型整数保存 jiffies 值,则可以计算得到能
够记录的最大秒数为42949672.96 秒,约合 1.38 年,因此这种方法无法在实际当中使
用。实际上,Linux 的 jiffies 值由两部分组成,分别用 32 位无符号整数记录自 1
970.1.1 00:00:00 开始的秒数以及秒数千分值。这样,Linux 可正确处理的时间值最
大到 1970 年后的 138 年,即 2108 年,而时间的计量也可精确到千分之一秒。在到
达 2108 年之前,人们早就会想出更好的办法来计时。
图 15-3 Linux 中的两种系统定时器。(a) 老定时器结构;(b) 新定时器结构
Linux 具有两种类型的系统定时器,这两种定时器均具有对应的例程队列,必须在到达
给定的系统时间时调用,但这两种定时器的具体实现方法有一些不同。图 15-3 说明了
这两种定时器机制。第一种定时器机制,也是老的定时器机制,利用一个可保存 32 个
指针的数组定义定时器。每个指针可指向一个 timer_struct 结构,而 timer_active
是活动定时器的掩码(这和底半处理过程的数据结构类似)。数组中的元素通常是静
态定义的,在系统初始化过程中填充这些元素。第二种定时器机制,是比较新的定时器
机制,它用链表结构以定时器到期时间的升序组织定时器。
这两种定时器均利用 jiffies 值作为定时器的到期时间。如果某个定时器要在 5 秒之
后到期,则必须将 5 秒转换为对应的 jiffies 值,加上当前的系统时间后(以 jief
fies 为单位),得到的便是该定时器到期的系统时间。每次系统时钟的滴答中,定时
器底半处理过程被标记为活动状态,当调度程序随后运行时,定时器队列将得到处理。
定时器底半处理过程要处理上述两种类型的定时器。对老的系统定时器,检验 timer_
active 中的相应位,以便确定活动的定时器。如果活动定时器已到期(到期时间大于
或等于当前系统的 jiffies),则调用对应的定时器例程,并清除 timer_active 中的
相应位。对新的系统定时器,检验链表中的 timer_list 数据结构,每个到期的定时器
从链表中移出,而对应的定时器例程被调用。新的定时器机制可以将参数传递到定时器
例程中。
15.4 等待队列
在进程的执行过程中,有时难免要等待某些系统资源。例如,如果某个进程要读取一个
描述目录的 VFS 索引节点,而该节点当前不再缓冲区高速缓存中,这时,该进程就必
须等待系统从包含文件系统的物理介质中获取索引节点,然后才能继续运行。
图 15-4 Linux 中的等待队列
Linux 利用一个简单的数据结构来处理这种情况。如图 15-4 所示,是 Linux 中的等
待队列,该队列中的元素包含一个指向进程 task_struct 结构的指针,以及一个指向
等待队列中下一个元素的指针。
对于添加到某个等待队列的进程来说,它可能是可中断的,也可能是不可中断的。当可
中断的进程在等待队列中等待时,它可以被诸如定时器到期或信号的发送等事件中断。
如果等待进程是可中断的,则进程状态为 INTERRUPTIBLE;如果等待进程是不可中断的
,则进程状态为 UNINTERRUPTIBLE。
15.5 Buzz 锁
Buzz 锁,也即“自旋锁”,是用来保护数据和代码段的最原始方法。利用 Buzz 锁,
可每次只允许一个进程进入关键代码段。Linux 利用 Buzz 锁限制对某些数据结构域的
访问,并利用一个整型域作为锁。每个要进入关键代码段的进程首先试图将该整数的值
从 0 修改为 1。如果当前值为 0,则进程可以立即进入关键代码段,而整数值变为 1
;如果当前值为 1,则说明其他进程已进入该关键代码段,进程循环检查整数值,直到
值变为 0,这时进程可修改值为 1,并进入关键代码段。进程在退出关键代码段时,将
Buzz 锁的值修改为0,以便其他进程可以进入该关键段。
对用来保存 Buzz 锁的内存的访问必须是原子操作,也就是不能被中断的操作。大部分
CPU 提供特殊的指令支持 Buzz 锁的原子操作,当然,也可以利用非缓存的内存实现
Buzz 锁。
15.6 信号量
信号量也用来保护关键代码或数据结构。我们都知道,关键代码段的访问,是由内核代
表进程完成的,如果让某个进程修改当前由其他进程使用的关键数据结构,其后果是不
堪设想的。可以利用 Buzz 锁实现对关键代码段和数据的互斥访问,但是,如前所述,
Buzz 锁是一种非常原始的方法,并且由于对 Buzz 锁值的循环重复测试,无法为系统
提供更好的性能。取而代之,Linux 利用信号量实现对关键代码和数据的互斥访问,同
一时刻只能有一个进程反问某个关键资源,所有其他要访问该资源的进程必须等待直到
该资源空闲为止。等待进程处于暂停状态,而系统中的其他进程则可运行如常。
Linux 信号量数据结构中包含如表 15-3 所示的信息。
表 15-3 Linux 信号量数据结构中包含的信息
count(计数)
该域用来跟踪希望访问该资源的进程个数。正值表示资源是可用的,而负值或零表示
有进程正在等待该资源。该计数的初始值为 1,表明同一时刻有且只能有一个进程可访
问该资源。进程要访问该资源时,对该计数减 1,结束对该资源的访问时,对该计数加
1。
waking(等待唤醒计数)
等待该资源的进程个数,也是当该资源空闲时等待唤醒的进程个数。
等待队列
某个进程等待该资源时被添加到该等待队列中。
lock(锁)
用来实现对 waking 域的互斥访问的 Buzz 锁。
假定该信号量的初始计数为 1,第一个要求访问资源的进程可对计数减 1,并可成功访
问资源。现在,该进程是“拥有”由信号量所包含的资源或关键代码段的进程。当该进
程结束对资源的访问时,对计数加 1。最优的情况是没有其他进程和该进程一起竞争资
源所有权。Linux 针对这种最常见的情况对信号量进行了优化,从而可以让信号量高效
工作。
当某个进程当前拥有资源时,如果其他进程要访问该资源,它首先将信号量计数减 1。
因为现在计数值是负值 (-1),因此该进程不能进入关键段,相反,它必须等待资源
当前的拥有者释放所有权。Linux 将等待进程置入休眠状态,直到所有者退出关键段时
唤醒。等待进程将自己添加到信号量的等待队列中,然后循环检测信号量 waking 域的
值,当 waking 非零时调用调度程序。
关键段的所有者增加信号量的计数,如果计数大于或等于 0,表明其他进程正在处于休
眠状态而等待该资源。在最优情况下,信号量的计数将返回到初值 1,因此没有必要进
行额外的工作。在其他情况下,资源的拥有者要增加 waking 计数,并唤醒处于信号量
等待队列中的休眠进程。当休眠进程被唤醒之后,waking 计数的当前值为 1,因此可
以进入关键段,这时,它减小 waking 计数,将 waking 计数的值还原为 0。对信号量
waking 域的互斥访问利用信号量的 lock 域作为 Buzz 锁而实现。
15.7 模块
从结构上来讲,操作系统有微内核结构和单块结构之分,Windows NT 和 MINIX 是典型
的微内核操作系统,而 Linux 则是单块结构的操作系统。微内核结构可方便地在系统
中添加新的组件,而单块结构却不容易做到这一点。为此,Linux 支持可动态装载和卸
载的模块。利用模块,可方便地在内核中添加新的组件或卸载不再需要的内核组件。大
多数 Linux 内核模块是设备驱动程序以及伪设备驱动程序(网络驱动程序、文件系统
等)。
利用内核模块的动态装载性具有如下优点:
将内核映象的尺寸保持在最小,并具有最大的灵活性;
便于检验新的内核代码,而不需重新编译内核并重新引导。
但是,内核模块的引入也带来了如下问题:
对系统性能和内存利用有负面影响;
装入的内核模块和其他内核部分一样,具有相同的访问权限,因此,差的内核模块会导
致系统崩溃; 为了内核模块访问所有内核资源,内核必须维护符号表,并在装入和卸
载模块时修改这些符号表; 有些模块要求利用其他模块的功能,因此,内核要维护模
块之间的依赖性。 内核必须能够在卸载模块时通知模块,并且要释放分配给模块的内
存和中断等资源; 内核版本和模块版本的不兼容,也可能导致系统崩溃,因此,严格
的版本检查是必需的。 15.7.1 装载模块
有两种方法可用来装载模块:
利用 insmod 命令手工将模块插入内核;
由内核在必要时装载模块,称为“需求装载”。
利用需求装载时,内核通过向守护进程 kerneld 发送请求而装载适当的模块。这一守
护进程实际是一个普通的用户进程,通常在系统引导时启动,并具有超级用户权限。k
erneld 进程在启动时为内核打开一个进程间通讯通道,内核可以利用该通道向 kerne
ld 进程发送任务的执行请求。kerneld 进程的首要任务是装载和卸载模块,另外,该
进程也负责其他一些任务,例如打开和关闭 PPP 链接等。kerneld 实际运行相应的工
具完成任务,对装载模块而言,它利用的是 insmod 命令。因此,该进程实际是代表内
核完成某些任务的代理。
图 15-5 装入 VFAT 和 FAT 之后的内核模块表
执行 insmod 命令时,必须指定要装载模块的位置;对需求装载的内核模块,通常保存
在 /lib/modules/kernel-version。和系统的其他程序一样,内核模块实际是经连接的
目标文件,但模块是可重定位的,也就是说,为了让装入的模块和已有的内核组件之间
可以互相访问,模块不能连接为从特定地址执行的映象文件。模块可以是 a.out 或 e
lf 格式的目标文件。insmod 利用一个特权系统调用,可找到内核的导出符号表,符号
成对出现,一个是符号名称,另外一个是符号的值,例如符号的地址。内核维护一个由
module_list 指针指向的 module 链表,其中第一个 module 数据结构保存有内核的
导出符号表(见图 15-5)。并不是所有的内核符号均在符号表中导出,而只有一些特
殊的符号才被添加到符号表中。例如,“request_irq”是一个导出符号,它是一个内
核例程,可由驱动程序申请控制某个特定的系统中断。利用 ksyms 命令或查看 /proc
/ksyms 文件内容,可非常方便地看到所有的内核导出符号及其符号值。利用 ksyms 命
令,不仅可以看到内核的所有符号,也可以看到只由以装载模块导出的符号。insmod
命令将模块读到它本身的虚拟内存中,然后利用内核导出的符号表,修正尚未解析的对
内核例程囊谩U庵中拚导适嵌阅?樵谀诖嬷械挠诚蠼行拚nsmod 将符号的地址
写入模块中适当的位置而实现修正。
insmod 命令修正模块对内核符号的引用之后,再次利用特权系统调用请求内核分配足
够的物理内存空间保存新的模块。内核将分配新的 module 数据结构以及足够的内核内
存,并将新模块添加在内核模块表的末尾。新的内核模块标记为 UNINITIALIZED(未初
始化)。图 15-5是装入 VFAT 和 FAT 模块之后的内核模块表。图中并没有表示出第一
个模块,它实际是一个伪模块,仅仅用来保存内核的导出符号表。利用 lsmod 命令可
列出所有已装载的内核模块以及它们的内在依赖性。内核为新模块分配的内核内存映射
到 insmod 进程的地址空间中,这样,insmod 就可以将模块复制到新分配的内存中。
insmod 还对模块进行重新定位,经重定位之后,新的模块就可以从新分配的内核地址
开始运行了。显然,模块不能期望自己能够在不同的 Linux 系统,或前后两次装入时
被装载到相同地址,重定位操作可通过对模块映象中适当的地址进行修正而解决这一问
题。
新的模块也要向内核导出符号,由 insmod 建立相应的符号表。每个内核模块必须包含
模块的初始化和清除例程,这些例程作为每个模块均具备的例程而不被导出,但 insm
od 必须知道它们的地址,并将地址传递给内核。insmod 同样利用特权系统调用将模块
的初始化和清除例程地址传递给内核。
新的模块添加到内核之后,它必须更新内核符号集并修改使用新模块的模块。由其他模
块依赖的模块必须在自身符号表的末尾维护一个引用表,并指向其他模块的 module 结
构。例如,图 15-5 表明 VFAT 文件系统模块依赖于 FAT 文件系统模块,因此,FAT
模块包含一个对 VFAT 模块的引用,该引用在装入 VFAT 模块时添加。
内核成功调用模块的初始化例程之后继续模块的安装,最后,模块状态被设置为 RUNN
ING(运行)。模块的清除例程保存在 module 数据结构中,在卸载模块时由内核调用
。
15.7.2 卸载模块
和模块的装载类似,可利用 rmmod 命令手工卸载模块,当对需求装载的模块则由 ker
neld 在不再需要时自动卸载。每次 kerneld 的空闲定时器到期时,它会利用系统调用
将当前不再使用的需求装载模块从内核中移走。启动 kerneld 时指定该定时器的时间
,通常的时间为 180 秒。
如果内核的其他部分依赖于装入的模块时,该模块不能卸载。例如,如果挂装了 FAT
文件系统,则不能卸载已装入的 FAT 文件系统模块。lsmod 命令的输出会显示已安装
模块的使用计数,例如:
Module: #pages: Used by:
msdos 5 1
vfat 4 1 (autoclean)
fat 6 2 (autoclean)
使用计数就是依赖于该模块的内核实体个数。模块的使用计数保存在模块映象的第一个
长整型中。但是,这一长整型中还包含有 AUTOCLEAN 和 VISITED 标志。这两个标志均
由需求装载的模块使用。具有 AUTOCLEAN 标志的模块是系统认为可以自动卸载的模块
。具有 VISITED 标志的模块表明正由其他内核组件使用,当任何其他内核组件使用该
模块是设置该标志。当 kerneld 请求系统移走不使用的需求装载模块时,系统首先寻
找可以移走的模块,但系统只查看标记为 AUTOCLEAN,并且状态处于 RUNNING 的模块
。如果上述模块的 VISITED 标志被清除,则系统将卸载该模块,否则系统会清除 VIS
ITED 标志并查看下一个模块。
假定某个模块是可卸载的,则系统调用其清除例程释放分配该模块的内核资源。相应的
module 数据结构被标志为 DELETED 并从内核模块链表中断开。所有由该模块依赖的
模块,系统会修改它们的引用表以便取消依赖性。最后,系统释放模块的内核内存。
15.8 相关系统工具和系统调用
15.8.1 显示和设置时间
如前所述,Linux 内核中保持着格林尼治时间,要获得本地时间,系统必须维护时区信
息。系统时区一般由符号链接 /etc/localtime 确定。但是,不同的用户可具有自己私
有的时区设置,这一般通过 TZ 环境变量来设置。
date 命令可显示当前日期和时间,如:
$ date
Wed Feb 17 09:56:45 CST 1999
$
上述命令执行结果显示北京时间。利用 -u 选项,可显示格林尼治时间,如:
$ date u
Wed Feb 17 01:56:54 UTC 1999
$
也可以利用 date 命令设置内核的软件时钟:
# date 07142157
Sun Jul 14 21:57:00 CST 1999
# date
Sun Jul 14 21:57:02 CST 1999
#
date 命令可显示和设置软件时钟,而 clock 命令则用来同步硬件和软件时钟。系统引
导时,可利用该命令读取硬件时钟并设置软件时钟。如果需要同时设置硬件和软件时钟
,则可首先利用 date 命令设置软件时钟,然后利用 clock 命令的 -w 选项设置硬件
时钟。
通常来说,PC 机的硬件时钟,即 BIOS 所维护的时钟记录本地时间,如果硬件时钟记
录格林尼治时间,则必须利用 clock 命令的 -u 选项,否则 clock 会认为硬件时间为
本地时间。
由于 Linux 系统软件时钟的实现利用了定时器中断,如果系统中运行的进程过多,则
对定时器中断的响应时间会增加,从而使软件时钟变得不可靠起来。但硬件时钟通常是
比较精确的,因此,如果经常引导 Linux 系统,则软件时钟相对精确。如果要调整硬
件时钟,最简单的方法是修改 BIOS 时钟,也可以利用上述的方法,先用 date 设置软
件时钟,再利用 clock 命令的 w 选项设置硬件时钟。
在网络环境中,有一些计算机可提供非常精确的时间,这些计算机通常称为“时间服务
器”。利用 rdate 和 netdate 命令可同步本地计算机和时间服务器之间的时钟。
15.8.2 管理内核模块
利用 insmod 命令可手工装入内核模块;利用 lsmod 可查看当前装入的内核模块以及
需求装载模块的使用计数及标志信息;利用 rmmod 则可以卸载指定的模块。
15.8.3 系统调用
表 15-4 简要列出了和时间、定时器以及模块相关的系统调用。标志列中各字母的意义
可参见 表 10-1 的说明。
表 15-4 相关系统调用
系统调用
说明
标志
adjtimex
设置或获取内核时间变量
-c
create_module
为可装载内核模块分配空间
-
delete_module
卸载内核模块
-
get_kernel_syms
获取内核符号表或其大小
-
getitimer
获取间隔定时器的值
mc
gettimeofday
获取自 1970.1.1 以来的时区和秒数
mc
init_module
插入可装载内核模块
-
setitimer
设置间隔定时器
mc
settimeofdat
设置自 1970.1.1 以来的时区和秒数
mc
stime
设置自 1970.1.1 以来的秒数
mc
time
获取自 1970.1.1 以来的秒数
m+c
--
☆ 来源:.哈工大紫丁香 bbs.hit.edu.cn.[FROM: bin@mtlab.hit.edu.cn]
Powered by KBS BBS 2.0 (http://dev.kcn.cn)
页面执行时间:209.723毫秒