目录, 显示框架, 无框架

第 11 章
内核机制


本章描述了 Linux 内核需要提供的一些通用任务和机制,以便内核的其他部分能够有效地协同工作。

11.1  底半部处理


图 11.1:底半部处理数据结构

在内核中,经常会有您不想立即执行某些工作的时候。一个很好的例子是在中断处理期间。当中断被触发时,处理器停止了它正在做的事情,操作系统将中断传递给相应的设备驱动程序。设备驱动程序不应花费太多时间处理中断,因为在此期间,系统中没有其他任何程序可以运行。通常有一些工作可以稍后完成。Linux 的底半部处理程序被发明出来,以便设备驱动程序和 Linux 内核的其他部分可以将工作排队,以便稍后完成。图  11.1 显示了与底半部处理相关的内核数据结构。

可以有多达 32 个不同的底半部处理程序;bh_base是指向每个内核底半部处理例程的指针向量。bh_activebh_mask的位根据已安装且处于活动状态的处理程序进行设置。如果bh_mask的第 N 位被设置,则bh_base的第 N 个元素包含底半部例程的地址。如果bh_active的第 N 位被设置,则应在调度程序认为合理时尽快调用第 N 个底半部处理程序例程。这些索引是静态定义的;定时器底半部处理程序是最高优先级(索引 0),控制台底半部处理程序是优先级次之(索引 1),依此类推。通常,底半部处理例程具有与其关联的任务列表。例如,immediate 底半部处理程序遍历立即任务队列(tq_immediate),其中包含需要立即执行的任务。

一些内核的底半部处理程序是设备特定的,但其他处理程序更通用

TIMER
每次系统定期定时器中断时,此处理程序都会被标记为活动状态,并用于驱动内核的定时器队列机制,
CONSOLE
此处理程序用于处理控制台消息,
TQUEUE
此处理程序用于处理tty消息,
NET
此处理程序处理一般网络处理,
IMMEDIATE
这是一个通用处理程序,供多个设备驱动程序使用,用于将工作排队以便稍后完成。

每当设备驱动程序或内核的某些其他部分需要调度工作以便稍后完成时,它会将工作添加到适当的系统队列(例如定时器队列),然后向内核发出信号,表明需要进行某些底半部处理。它通过设置bh_active中相应的位来完成此操作。如果驱动程序已将某些内容排队到立即队列中,并希望立即底半部处理程序运行并处理它,则设置位 8。在每个系统调用结束时,就在控制权返回给调用进程之前,会检查bh_active位掩码。如果它有任何位设置,则会调用处于活动状态的底半部处理例程。首先检查位 0,然后是位 1,依此类推,直到位 31。

在每次调用底半部处理例程时,都会清除bh_active中的位。bh_active是瞬态的;它仅在调用调度程序之间有意义,并且是一种在没有工作要执行时避免调用底半部处理例程的方法。

11.2  任务队列


图 11.2:任务队列

任务队列是内核延迟工作到稍后的方法。Linux 具有一种通用的机制,用于在队列上排队工作并在以后处理它们。

任务队列通常与底半部处理程序结合使用;定时器任务队列在定时器队列底半部处理程序运行时处理。任务队列是一个简单的数据结构,见图  11.2,它由一个单链表组成,链表中的每个元素都是tq_struct数据结构,每个数据结构都包含一个例程的地址和一个指向某些数据的指针。

当处理任务队列上的元素时,将调用例程,并将向其传递一个指向数据的指针。

内核中的任何内容,例如设备驱动程序,都可以创建和使用任务队列,但内核创建和管理了三个任务队列

timer
此队列用于排队将在下一个系统时钟滴答后尽快完成的工作。每个时钟滴答,都会检查此队列以查看是否包含任何条目,如果包含,则会激活定时器队列底半部处理程序。当调度程序下次运行时,定时器队列底半部处理程序将与所有其他底半部处理程序一起处理。此队列不应与系统定时器混淆,系统定时器是一种更复杂的机制。
immediate
此队列也在调度程序处理活动底半部处理程序时处理。立即底半部处理程序的优先级不如定时器队列底半部处理程序,因此这些任务将稍后运行。
scheduler
此任务队列由调度程序直接处理。它用于支持系统中的其他任务队列,在这种情况下,要运行的任务将是一个处理任务队列的例程,例如设备驱动程序的任务队列。

当处理任务队列时,指向队列中第一个元素的指针将从队列中删除,并替换为空指针。实际上,这种删除是原子操作,无法中断。然后,队列中的每个元素依次调用其处理例程。队列中的元素通常是静态分配的数据。但是,没有用于丢弃已分配内存的固有机制。任务队列处理例程只是移动到列表中的下一个元素。任务本身的工作是确保它正确清理任何已分配的内核内存。

11.3  定时器


图 11.3:系统定时器

操作系统需要能够安排将来某个时候的活动。需要一种机制,通过该机制可以将活动安排在相对精确的时间运行。任何希望支持操作系统的微处理器都必须具有一个可编程间隔定时器,该定时器定期中断处理器。这种定期中断被称为系统时钟滴答,它像节拍器一样,协调系统的活动。

Linux 对时间的看法非常简单;它以自系统启动以来的时钟滴答数来衡量时间。所有系统时间都基于此测量,这被称为jiffies,以同名的全局可用变量命名。

Linux 有两种类型的系统定时器,它们都将例程排队以便在某个系统时间调用,但它们的实现略有不同。图  11.3 显示了这两种机制。

第一种,旧的定时器机制,有一个 32 个指针的静态数组,指向timer_struct数据结构和一个活动定时器的掩码,timer_active.

定时器在定时器表中的位置是静态定义的(很像底半部处理程序表bh_base)。条目主要在系统初始化时添加到此表中。第二种,较新的机制使用链接列表,其中包含timer_list数据结构,并按升序到期时间顺序排列。

两种方法都使用jiffies中的时间作为到期时间,因此希望在 5 秒后运行的定时器必须将 5 秒转换为jiffies的单位,并将其添加到当前系统时间,以获得以jiffies为单位的系统时间,定时器应在该时间到期。每个系统时钟滴答,定时器底半部处理程序都会被标记为活动状态,以便当调度程序下次运行时,将处理定时器队列。定时器底半部处理程序处理两种类型的系统定时器。对于旧的系统定时器,会检查timer_active位掩码中已设置的位。

如果活动定时器的到期时间已过期(到期时间小于当前系统jiffies),则会调用其定时器例程,并清除其活动位。对于新的系统定时器,会检查timer_list数据结构的链表中的条目。

每个过期的定时器都会从列表中删除,并调用其例程。新的定时器机制的优势在于能够将参数传递给定时器例程。

11.4  等待队列

在很多时候,进程必须等待系统资源。例如,进程可能需要描述文件系统中目录的 VFS inode,而该 inode 可能不在缓冲区缓存中。在这种情况下,进程必须等待从包含文件系统的物理介质中获取该 inode,然后才能继续。

wait_queue

*task

*next

图 11.4:等待队列

Linux 内核使用一个简单的数据结构,即等待队列(见图  11.4),

它由指向进程task_struct的指针和指向等待队列中下一个元素的指针组成。

当进程被添加到等待队列的末尾时,它们可以是可中断的或不可中断的。可中断进程可能会被诸如定时器到期或信号传递等事件中断,而它们正在等待等待队列。等待进程的状态将反映这一点,并且是INTERRUPTIBLEUNINTERRUPTIBLE。由于此进程现在无法继续运行,因此运行调度程序,当它选择要运行的新进程时,等待进程将被挂起。1

当处理等待队列时,等待队列中每个进程的状态都会设置为RUNNING。如果进程已从运行队列中删除,则将其放回运行队列。调度程序下次运行时,等待队列中的进程现在是运行的候选者,因为它们现在不再等待。当调度等待队列中的进程时,它要做的第一件事就是将自己从等待队列中删除。等待队列可用于同步对系统资源的访问,Linux 在其信号量实现中使用了等待队列(见下文)。

11.5  忙等待锁

这些锁更广为人知的名称是自旋锁,它们是保护数据结构或代码片段的原始方法。它们一次只允许一个进程位于代码的关键区域内。它们在 Linux 中用于限制对数据结构中字段的访问,使用单个整数字段作为锁。每个希望进入该区域的进程都尝试将锁的初始值从 0 更改为 1。如果其当前值为 1,则进程会再次尝试,在代码的紧密循环中自旋。对保存锁的内存位置的访问必须是原子的,读取其值、检查其是否为 0,然后将其更改为 1 的操作不能被任何其他进程中断。大多数 CPU 架构都通过特殊指令提供对此的支持,但您也可以使用非缓存主内存来实现忙等待锁。

当拥有进程离开代码的关键区域时,它会递减忙等待锁,将其值返回为 0。任何在锁上自旋的进程现在都会将其读取为 0,第一个执行此操作的进程会将其递增为 1 并进入关键区域。

11.6  信号量

信号量用于保护代码或数据结构的关键区域。请记住,对关键数据片段(例如描述目录的 VFS inode)的每次访问都是由代表进程运行的内核代码进行的。允许一个进程更改另一个进程正在使用的关键数据结构是非常危险的。一种实现此目的的方法是在被访问的关键数据片段周围使用忙等待锁,但这是一种简单的方法,不会提供非常好的系统性能。相反,Linux 使用信号量来允许一次只有一个进程访问代码和数据的关键区域;所有其他希望访问此资源的进程都将被迫等待,直到资源空闲。等待进程被挂起,系统中的其他进程可以继续正常运行。

Linuxsemaphore数据结构包含以下信息

count
此字段跟踪希望使用此资源的进程计数。正值表示资源可用。负值或零值表示进程正在等待它。初始值为 1 表示一次只能有一个进程可以使用此资源。当进程想要此资源时,它们会递减计数,当它们完成使用此资源时,它们会递增计数,
waking
这是等待此资源的进程计数,也是当此资源空闲时等待被唤醒的进程数,
wait queue
当进程正在等待此资源时,它们会被放入此等待队列,
lock
一个在访问waking字段时使用的忙等待锁。

假设信号量的初始计数为 1,则第一个到达的进程会看到计数为正,并将其递减 1,使其变为 0。该进程现在“拥有”受信号量保护的代码或资源的关键片段。当进程离开关键区域时,它会递增信号量的计数。最佳情况是没有其他进程争夺关键区域的所有权。Linux 已经实现了信号量,以便在此最常见的情况下高效工作。

如果另一个进程希望在关键区域被进程拥有时进入,它也会递减计数。由于计数现在为负 (-1),因此进程无法进入关键区域。相反,它必须等到拥有进程退出它。Linux 使等待进程休眠,直到拥有进程在退出关键区域时唤醒它。等待进程将自己添加到信号量的等待队列,并坐在循环中检查waking字段的值并调用调度程序,直到waking为非零。

关键区域的所有者递增信号量的计数,如果计数小于或等于零,则表示有进程正在休眠,等待此资源。在最佳情况下,信号量的计数将已返回其初始值 1,并且不需要进一步的工作。拥有进程递增唤醒计数器,并唤醒在信号量的等待队列上休眠的进程。当等待进程唤醒时,唤醒计数器现在为 1,它知道它现在可以进入关键区域。它递减唤醒计数器,将其值返回为零,并继续。对信号量的唤醒字段的所有访问都受使用信号量锁的忙等待锁保护。


脚注

1 审阅注释 有什么可以阻止处于状态INTERRUPTIBLE的任务在调度程序下次运行时运行?等待队列中的进程在被唤醒之前永远不应运行。


文件由 TTH 1.0 版本从 TEX 翻译而来。
章首, 目录, 显示框架, 无框架
� 1996-1999 David A Rusling 版权声明