Presentation is loading. Please wait.

Presentation is loading. Please wait.

第 3 章 中断和中断处理 硬件中断机制是一个操作系统内核中非常重要的部分。它的设计直接影响到操作系统整体的性能。它与硬件平台和内核的其它部分,如内存管理、进程调度、设备驱动等都有很密切的关系。因此,它也是操作系统中比较复杂的一个模块。 Linux的硬件中断机制的设计有很多独到之处,本章把kernel.

Similar presentations


Presentation on theme: "第 3 章 中断和中断处理 硬件中断机制是一个操作系统内核中非常重要的部分。它的设计直接影响到操作系统整体的性能。它与硬件平台和内核的其它部分,如内存管理、进程调度、设备驱动等都有很密切的关系。因此,它也是操作系统中比较复杂的一个模块。 Linux的硬件中断机制的设计有很多独到之处,本章把kernel."— Presentation transcript:

1 第 3 章 中断和中断处理 硬件中断机制是一个操作系统内核中非常重要的部分。它的设计直接影响到操作系统整体的性能。它与硬件平台和内核的其它部分,如内存管理、进程调度、设备驱动等都有很密切的关系。因此,它也是操作系统中比较复杂的一个模块。 Linux的硬件中断机制的设计有很多独到之处,本章把kernel 2.4和kernel 2.2.x的相关机制进行详细的对比,使读者能够更好的领会最新的kernel 2.4中的硬件中断机制。

2 3.1 硬件提供的中断机制和约定 硬中断即和硬件相关的中断也就是通常意义上的“中断处理程序”,它是直接处理由硬件发过来的中断信号的。当某个设备发出中断请求时,CPU停止正在执行的指令,转而跳到包括中断处理代码或者包括指向中断处理代码的转移指令所在的内存区域。这些代码一般在CPU的中断方式下运行。就回去自己驱动的设备上去看看设备的状态寄存器以了解发生了什么事情,并进行相应的操作。当中断处理完毕以后,CPU将恢复到以前的状态,继续执行中断处理前正在执行的指令。 中断的流程如图3.1所示。

3 3.1 硬件提供的中断机制和约定 图3.1 中断流程 大多数处理器在处理中断过程方式下将不会再有中断发生。但有些CPU的中断有自己的优先权,更高优先权的中断则可以发生。这意味着第一级的中断处理程序必须拥有自己的堆栈,以便在处理更高级别的中断前保存CPU的执行状态。

4 3.1 硬件提供的中断机制和约定 Linux系统是包含内核、系统工具、完整的开发环境和应用的类Unix操作系统。这个系统是由全世界各地的成千上万的程序员设计和实现的。1984年,Richard Stallman创立了GNU工程,其目标是开发一个完全免费的类Unix系统及其应用程序。1991年,芬兰赫尔辛基大学一位名叫Linus Torvalds的学生开始了开放源代码的Linux雏形的设计。其目的是建立不受任何商品化软件的版权制约的、全世界都能自由使用的Unix兼容产品 由于Linux是一套具有Unix全部功能的免费操作系统,它在众多的软件中占有很大的优势,为广大的计算机爱好者提供了学习、探索以及修改计算机操作系统内核的机会

5 3.1.1 中断产生的过程 CPU 在一些外部硬件的帮助下处理中断。中断处理硬件和具体的系统相关,但一般来说,这些硬件系统和 i386 处理器的中断系统在功能上是一致的。 图3.2 i386 PC 可编程中断控制器8259A级链示意图

6 3.1.1 中断产生的过程 对于中断,CPU只提供两条外接引线:NMI和INTR;这里的中断线是实际存在的电路,它们通过硬件接口连接到CPU外的设备控制器上。NMI只能通过端口操作来屏蔽,它通常用于电源掉电和物理存储器奇偶验错;INTR可通过直接设置中断屏蔽位来屏蔽,它可用来接受外部中断信号。INTR只有一条引线,为更好的处理外部设备,x86微机通过外接两片级连了可编程中断控制器8259A,以接受更多的外部中断信号。每个8259A中断控制器可以管理8条中断线,当两个8259级联的时候共可以控制15条中断线。在图3.2表示了两个级联的中断控制器,从属中断控制器的输出连接到了主中断控制器的第 3 个中断信号输入,这样,该系统可处理的外部中断数量最多可达 15 个。图的右边是 i386 PC 中各中断输入管脚的一般分配。可通过对8259A的初始化,使这15个外接引脚对应256个中断向量的任何15个连续的向量。设备通过中断线向中断控制器发送高电平告诉操作系统它产生了一个中断,而操作系统会从中断控制器的状态位知道是哪条中断线上产生了中断。

7 3.1.1 中断产生的过程 8259A主要完成中断优先级排队管理、接受外部中断请求和向CPU提供中断类型号这样一些任务。
由于Intel公司保留0-31号中断向量用来处理异常事件,所以,硬中断必须设在31以后,Linux则在实模式下初始化时把硬中断设在0x20-0x2F。 外部设备产生的中断实际是电平的变化信号,外部设备产生的中断信号在IRQ(中断请求)管脚上,这一信号首先由中断控制器处理。中断控制器可以响应多个中断输入,它的输出连接到 CPU 的 INT 管脚,CPU 在该管脚上的电平变化可通知处理器产生了中断。如果 CPU 这时可以处理中断,CPU 会通过 INTA(中断确认)管脚上的信号通知中断控制器已接受中断,这时,中断控制器可将一个 8 位数据放置在数据总线上,这一 8 位数据也称为中断向量号,CPU 依据中断向量号和中断描述符表(IDT)中的信息自动调用相应的中断服务程序。

8 3.1.1 中断产生的过程 中断控制器中的控制寄存器实际映射到了 CPU 的 I/O 地址空间中,通过对寄存器的设置,可设定中断控制器屏蔽某些中断,也可以指定中断控制器的特殊响应方式,因此,中断控制器也称为可编程中断控制器。在 Linux 中,两个中断控制器初始设置为固定优先级的中断响应方式。有关可编程控制器的详细信息可参阅有关的资料。 中断处理程序得知设备发生了一个中断,但并不知道设备发生了什么事情,只有在访问了设备上的一些状态寄存器以后,才能知道具体发生了什么,要怎么去处理。

9 3.1.2 中断请求 设备只有对某一条确定的中断线拥有了控制权,才可以向这条中断线上发送信号。由于计算机的外部设备越来越多,所以15条中断线已经不够用了。要使用中断线,就得进行中断线的申请,就是IRQ(Interrupt Requirement),也常把申请一条中断线称为申请一个IRQ或者是申请一个中断号。 IRQ是非常宝贵的,所以建议只有当设备需要中断的时候才申请占用一个IRQ,或者是在申请IRQ时采用共享中断的方式,这样可以让更多的设备使用中断。无论对IRQ的使用方式是独占还是共享,申请IRQ的过程都分为3步: (1) 将所有的中断线探测一遍,看看哪些中断还没有被占用。从这些还没有被占用的中断中选一个作为该设备的IRQ。 (2)通过中断申请函数申请选定的IRQ,这是要指定申请的方式是独占还是共享。 (3)根据中断申请函数的返回值决定怎么做:如果成功了则执行中断,如果没成功则或者重新申请或者放弃申请并返回错误。 22

10 3.1.3 置中断标志位 在处理中断的时候,中断控制器会屏蔽掉原先发送中断的那个设备,直到它发送的上一个中断被处理完了为止。因此如果发送中断的那个设备载中断处理期间又发送了一个中断,那么这个中断就被永远的丢失了。 这种情况之所以发生,是因为中断控制器并不能缓冲中断信息。当前面的一个中断没有处理完之前又有新的中断到达,中断控制器就会丢掉新的中断。这个问题可以通过设置主处理器(CPU)上的“置中断标志位”(sti)来解决,因为主处理器具有缓冲中断的功能。如果使用了“置中断标志位”,在处理完中断以后使用sti函数就可以使先前被屏蔽的中断得到服务。

11 3.1.3 中断处理程序的不可重入性 有时候需要屏蔽中断,是出于管理上的考虑。因为中断处理程序是不可重入的,所以不能并行执行同一个中断处理程序,因此在中断处理的过程中要屏蔽由同一个IRQ来的新中断。 由于设备驱动程序要和设备的寄存器打交道,设备寄存器就是全局变量。如果一个中断处理程序可以并行,很有可能会发生驱动程序锁死的情况。当驱动程序锁死的时候,操作系统并不一定会崩溃,但是锁死的驱动程序所支持的那个设备就不能再使用了。因此,最简单的办法就是禁止同一设备的中断处理程序并行,即设备的中断处理程序是不可重入的。由于中断处理程序要求不可重入,编写可重入的中断处理程序则几乎是不可能的。所以通常不必编写可重入的中断处理程序。但可编写可重入的设备驱动程序。 一旦中断的竞争条件出现,有可能会发生死锁的情况,严重时可能会将整个系统锁死。所以一定要避免竞争条件的出现。

12 3.1.4 时钟和定时器中断 操作系统应该能够在将来某个时刻准时调度某个任务。所以需要一种能保证准时调度某个任务运行的机制。希望支持每种操作系统的微处理器必须包含一个可周期性中断它的可编程间隔定时器。该定时器可以在指定的时间周期性地中断处理器。这个周期性中断被称为系统时钟周期,它像音乐中的节拍器一样来协调着系统中所有的活动。 除此之外,操作系统还必须具备一定的接口记录系统的时间,并为程序提供时间服务。一般来说,操作系统和计算机硬件一起维护着系统中的时间。Linux的时钟观念很简单:它表示系统启动后的以时钟周期计数的时间。在 PC 机中,Linux 利用 BIOS CMOS 中记录的时间(称为“硬件时钟”)作为系统启动时的时间基准,而在系统运行时,利用时钟周期测量系统的时间(称为“软件时钟”)。

13 3.1.4 时钟和定时器中断 Linux 利用全局变量jiffies(瞬时)作为系统时间的测量基准,所有的时间都从 :00:00 开始计算,系统启动时,将 CMOS 中记录的时间转化为从 :00:00 算起的 jiffies 值。Linux 内核中没有任何时区的概念,Linux 内核中的时间以格林尼治时间记录,将格林尼治时间转换为本地时间的任务则由应用程序负责。 Linux 的 jiffies 值由两部分组成,分别用 32 位无符号整数记录自 :00:00 开始的秒数以及秒数千分值。这样,Linux 可正确处理的时间值最大到 1970 年后的138 年,即 2108 年,而时间的计量也可精确到千分之一秒。在到达 2108 年之前,人们会想出更好的办法来计时。 Linux包含两种类型的系统定时器,它们都可以在某个系统时间上被队列例程使用,但是它们的实现稍有区别。图 3.3 说明了这两种定时器机制。

14 3.1.4 时钟和定时器中断 图 3.3 Linux 中的两种系统定时器

15 3.1.4 时钟和定时器中断 第一种是老的定时器机制,它包含指向timer_struct结构的32位指针的静态数组以及当前活动定时器的掩码 :time_active。 此定时器表中的位置是静态定义的(类似底层部分的数据结构bh_base)。数组中的元素通常是静态定义的,在系统初始化过程中填充这些元素。其入口在系统初始化时被加入到表中。 第二种是相对较新的定时器机制,它使用以定时器到期时间的升序排列的timer_list链表结构组织。

16 3.1.4 时钟和定时器中断 这两种方法都使用jiffies作为时间周期的终结。如果某个定时器要在 5 秒之后到期,则必须将5秒时间转换成对应的 jiffies 值,并且将它和以jiffies计数的当前系统时间相加从而得到该定时器到期的系统时间。在每个系统时钟周期里,定时器的底层部分处理过程被标记成活动状态,当调度程序下次运行时能进行定时器队列的处理。定时器底层部分处理过程要处理上述两种类型的系统定时器。对老的系统定时器来说,就是检查timer_active位是否置位。如果活动定时器已经到期(到期时间大于或等于当前系统的 jiffies),则调用对应的定时器例程,并清除 timer_active 中的相应活动位。对于新定时器,则检查链表中的 timer_list 数据结构,每个到期的定时器从链表中移出,而对应的定时器例程被调用。新的定时器机制的优点之一是能传递一个参数给定时器例程。 Linux提供了两种定时器服务。一种早期的由timer_struct等结构描述,由run_old_times函数处理。另一种“新”的服务由timer_list等结构描述,由add_timer、del_timer、cascade_time和run_timer_list等函数处理。

17 3.1.4 时钟和定时器中断 早期的定时器服务利用如下数据结构: struct timer_struct {
unsigned long expires;  //本定时器被唤醒的时刻 void (*fn)(void);   // 定时器唤醒后的处理函数 } struct timer_struct timer_table[32];  //最多可同时启用32个定时器 unsigned long timer_active; // 每位对应一定时器,置1表示启用 新的定时器服务依靠链表结构突破了32个的限制,利用如下的数据结构: struct timer_list { struct timer_list *next; struct timer_list *prev; unsigned long expires; unsigned long data;  // 用来存放当前进程的PCB块的指针,可作为参数传 void (*function)(unsigned long);  给function

18 3.1.4 时钟和定时器中断 系统启动核心时,调用start_kernal()开始各方面的初始化,在这之前,各种中断都被禁止,只有在完成必要的初始化后,直到执行完Kmalloc_init()后,才允许中断(init\main.c)。 在CPU调度时、系统调用返回前和中断处理返回前都会作判断调用do_bottom_half函数。Do_bottom_half函数依次扫描32个队列,找出需要服务的队列,执行服务后把对应该队列的bh_active的相应位置0。由于bh_active标志中TIMER_BH对应的bit为1,因而系统根据服务函数入口地址数组bh_base找到函数timer_bh()的入口地址,并马上执行该函数,在函数timer_bh中,调用函数run_timer_list()和函数run_old_timers()函数,定时执行服务。

19 3.2 Linux的中断处理 3.2.1 Linux中断处理程序的特色
考虑到中断处理的效率,Linux的中断处理程序分为两个部分:上半部(top half)和下半部(bottom half)。上半部的功能是“登记中断”。当一个中断发生时,就把设备驱动程序中中断例程的下半部挂到该设备的下半部执行队列中去,然后就等待新的中断的到来。这样,上半部执行的速度就会很快,就可以接受所负责设备产生的更多中断。上半部之所以要快,是因为它是完全屏蔽中断的,其它的中断只能等到这个中断处理程序执行完毕以后才能申请,不能得到及时的处理。快速的中断处理程序就可以对设备产生的中断尽可能多地进行服务。

20 3.2 Linux的中断处理 有些中断事件的处理比较复杂,中断处理程序必须多花一点时间才能够把事情做完。为了化解在短时间内完成复杂处理的矛盾,Linux引入了下半部的概念。下半部和上半部最大的不同是下半部是可中断的,而上半部是不可中断的。上半部只是将下半部放入了它们所负责的设备的中断处理队列中去,然后就什么都不管了。因此,下半部几乎做了中断处理程序所有的工作,包括查看设备上的寄存器以获得产生中断的事件信息,并根据这些信息进行相应的处理。如果下半部不知道怎么去做,它就使用鸵鸟算法来解决问题,即忽略这个事件。 由于下半部是可中断的,所以在它运行期间,如果其它的设备产生了中断,这个下半部可以暂时的中断掉,等到那个设备的上半部运行完了,再回头来运行它。但是要注意,如果一个设备中断处理程序正在运行,无论它是运行上半部还是运行下半部,只要中断处理程序还没有处理完毕,在这期间设备产生的新的中断都将被忽略掉。因为中断处理程序是不可重入的,同一个中断处理程序是不能并行的。

21 3.2 Linux的中断处理 Linux将中断处理程序划分成两个部分的一个原因,是要把中断的总延迟时间最小化。Linux内核定义了两种类型的中断,快速的和慢速的,这两者之间的一个区别是慢速中断自身还可以被中断,而快速中断则不能。因此,当处理快速中断时,如果有其它中断到达;不管是快速中断还是慢速中断,它们都必须等待。为了尽可能快地处理这些其它的中断,内核就需要尽可能地将处理延迟到下半部分执行。 其次,当内核执行上半部分时,正在服务的这个特殊IRQ将会被可编程中断控制器禁止,于是,连接在同一个IRQ上的其它设备就只有等到该该中断处理被处理完毕后果才能发出IRQ请求。而采用Bottom_half机制后,不需要立即处理的部分就可以放在下半部分处理,从而,加快了处理机对外部设备的中断请求的响应速度。

22 3.2 Linux的中断处理 还有一个原因是,处理程序的下半部分还可以包含一些并非每次中断都必须处理的操作;对这些操作,内核可以在一系列设备中断之后集中处理一次就可以了。即在这种情况下,每次都执行并非必要的操作完全是一种浪费,而采用Bottom_half机制后,可以稍稍延迟并在后来只执行一次就行了。 在下半部中也可以进行中断屏蔽。如果某一段代码不能被中断的话。可以使用cti、sti或者是save_flag、restore_flag来实现。

23 3.2.2 中断的相关数据结构 从数据结构入手,应该说是分析操作系统源码最常用的和最主要的方法。因为操作系统的几大功能部件,如进程管理、设备管理、内存管理等,都可以通过对其相应的数据结构的分析来弄懂其实现机制。很好的掌握这种方法,对分析Linux内核大有帮助。 中断向量在保护模式下的实现机制是中断描述符表 (Interrupt Descriptor Table, IDT),中断描述符表的结构如图3.4所示。中断描述符表即中断向量表相当于一个数组,包含256个中断描述符,每个中断描述符8位,对应硬件提供的256个中断服务例程的入口,即256个中断向量。IDT的位置由idtr确定,idtr是个48位的寄存器,高32位是IDT的基址,低16位为IDT的界限(通常为2k=256*8)。

24 3.2.2 中断的相关数据结构 图 3.4 Linux 的中断处理数据结构

25 3.2.2 中断的相关数据结构 在 i386 系统中,Linux 启动时要设置系统的中断描述符表IDT。IDT 中包含各个中断(以及异常,诸如浮点运算溢出)的服务程序地址,中断服务程序地址由 Linux 提供。每个设备驱动程序可以在图 3.4 所示的结构(irq_action)中注册自己的中断及中断处理程序地址。Linux 的中断服务程序根据 irq_action 中的注册信息调用相应的设备驱动程序的中断处理程序。和硬件相关的中断处理代码隐藏在中断服务程序中,这样,设备驱动程序的中断处理程序可在不同平台之间方便移植。一般来说,CPU 在处理中断时,首先要在堆栈中保存与 CPU 指令执行相关的寄存器(例如指令计数寄存器),然后调用中断服务程序,中断服务程序结束时再恢复这些寄存器。

26 3.2.2 中断的相关数据结构 图3.5 与硬中断相关的几个数据结构的关系

27 3.2.2 中断的相关数据结构 irq_action 实际是一个数组,其中包含指向 irqaction 的指针,每个数组元素分别定义一个 IRQ。Linux 内核提供相应的操作函数,设备驱动程序可调用这些操作函数设置相应的中断处理函数。一般在系统启动时,由各个设备驱动程序通过如下途径获取相关的设备 IRQ 并设置对应的 irq_action 数组元素所指向的 irqaction 结构。 由于0-31号中断向量已被Intel保留,就剩下32-255共224个中断向量可用。在Linux中,这224个中断向量除了0x80 (SYSCALL_VECTOR)用作系统调用总入口之外,其它都用在外部硬件中断源(包括可编程中断控制器8259A的15个irq)上。实际上,当没有定义CONFIG_X86_IO_APIC时,其它223(除0x80外)个中断向量,只利用了从32号开始的15个,其它208个空着未用。这些中断服务程序入口的设置将在下面详细说明。 与硬中断相关数据结构主要有三个, 三者关系如图3.5所示。

28 3.2.2 中断的相关数据结构 (1) 定义在/arch/i386/Kernel/irq.h中的struct hw_interrupt_type数据结构,它是一个抽象的中断控制器。这包含一系列的指向函数的指针,这些函数处理控制器特有的操作: typename:控制器的名字。 startup:允许从给定的控制器的IRQ所产生的事件。 shutdown:禁止从给定的控制器的IRQ所产生的事件。 handle:根据提供给该函数的IRQ,处理唯一的中断。 enable和disable:这两个函数基本上和startup和shutdown相同; struct hw_interrupt_type { const char * typename; void (*startup)(unsigned int irq); void (*shutdown)(unsigned int irq); void (*handle)(unsigned int irq, struct pt_regs * regs); void (*enable)(unsigned int irq); void (*disable)(unsigned int irq); };

29 3.2.2 中断的相关数据结构 (2) 定义在/arch/i386/Kernel/irq.h中的另外一个数据结构是irq_desc_t,它具有如下成员: status:一个整数。代表IRQ的状态:IRQ是否被禁止了,有关IRQ的设备当前是否正被自动检测,等等。 handler:指向hw_interrupt_type的指针。 action:指向irqaction结构组成的队列的头。正常情况下每个IRQ只有一个操作,因此链接列表的正常长度是1(或者0)。但是,如果IRQ被两个或者多个设备所共享,那么这个队列中就有多个操作。 depth:irq_desc_t的当前用户的个数。主要是用来保证在中断处理过程中IRQ不会被禁止。

30 3.2.2 中断的相关数据结构 irq_desc是irq_desc_t 类型的数组。对于每一个IRQ都有一个数组入口,即数组把每一个IRQ映射到和它相关的处理程序和irq_desc_t中的其它信息。 typedef struct { unsigned int status; // IRQ status - IRQ_INPROGRESS, IRQ_DISABLED struct hw_interrupt_type *handler; // handle/enable/disable functions struct irqaction *action; // IRQ action list unsigned int depth; // Disable depth for nested irq disables } irq_desc_t;

31 3.2.2 中断的相关数据结构 (3) 定义在include/linux/ interrupt.h中的struct irqaction数据结构包含了内核接收到特定IRQ之后应采取的操作,其成员如下: handler:是一指向某个函数的指针。该函数就是所在结构对相应中断的处理函数。 flags:取值只有SA_INTERRUPT(中断可嵌套),SA_SAMPLE_RANDOM(这个中断是源于物理随机性的),和SA_SHIRQ(这个IRQ和其它struct irqaction共享)。 mask:在x86或者体系结构无关的代码中不会使用(除非将其设置为0);只有在SPARC64的移植版本中要跟踪有关软盘的信息时才会使用它。 name:产生中断的硬件设备的名字。因为不止一个硬件可以共享一个IRQ。 dev_id:标识硬件类型的一个唯一的ID。Linux支持的所有硬件设备的每一种类型,都有一个由制造厂商定义的在此成员中记录的设备ID。 next:如果IRQ是共享的,那么这就是指向队列中下一个struct irqaction结构的指针。通常情况下,IRQ不是共享的,因此这个成员就为空。

32 3.2.2 中断的相关数据结构 struct irqaction {
void (*handler)(int, void *, struct pt_regs *); unsigned long flags; unsigned long mask; const char *name; void *dev_id; struct irqaction *next; };

33 3.2.3 中断向量表IDT的初始化 Linux内核在初始化阶段完成了对页式虚拟管理的初始化后,调用trap_init()和init_IRQ()两个函数进行中断机制的初始化。其中trap_init()主要是对一些系统保留的中断向量的初始化,而init_IRQ()则主要是用于外设的中断。

34 3.2.3 中断向量表IDT的初始化 void _init trap_init(void) { #ifdef CONFIG_EISA
if (isa_readl(0x0FFFD9) == 'E+('I'<<8)+('S'<<16)+('A'<<24)) EISA_bus = 1; #endif set_trap_gate(0,÷_error); set_trap_gate(1,&debug); set_intr_gate(2,&nmi); set_system_gate(3,&int3); //int3-5 can be called from all set_system_gate(4,&overflow);

35 3.2.3 中断向量表IDT的初始化 set_system_gate(5,&bounds);
set_trap_gate(6,&invalid_op) set_trap_gate(7,device_not_available); set_trap_gate(8,&double_fault); set_trap_gate(9,&coprocessor_segment_overrun); set_trap_gate(10,&invalid_TSS); set_trap_gate(11,&segment_not_present); set_trap_gate(12,&stack_segment); set_trap_gate(13,&general_protection); set_trap_gate(14,&page_fault); set_trap_gate(15,&spurious_interrupt_bug); set_trap_gate(16,&coprocessor_error); set_trap_gate(17,&alignment_check); set_trap_gate(18,&machine_check); set_trap_gate(19,&simd_coprocessor_error);

36 3.2.3 中断向量表IDT的初始化 set_system_gate(SYSCALL_VECTOR,&system_call);
// default LDT is a single_entry callgate to lcall7 for iBCS // and a callgate to lcall27 for Solaris/x86 binaries set_call_gate(&default_ldt[0],lcall7); set_call_gate(&default_ldt[4],lcall27); _set_gate(a,12,3,addr); //12 即二进制的1100b,类型码为100,即调用门 } 这些函数都调用同一个子程序_set_gate(),第一个参数用以设置中断描述符表idt_table中的第n项,第二个参数对应于门格式中的D位加类型位段,第三个参数是对应的DPL位段。

37 3.2.3 中断向量表IDT的初始化 #define _set_gate(gate_addr,type,dpl,addr) do {
int _d0,_d1; _asm_ _volatile_("movw %%dx,%%ax") "movw %4,%%dx " "mov1 %%eax,%0 " "mov1 %%edx,%1" :"=m" (*((long * ) (gate_addr))), "=m" (*(1+(long *)(gate_addr))),"=&a" (_d0),"=&d" (_d1) :"i" ((short) (0x8000+(dpl<<13)+(type<<8))), "3" ((char *) (addr)),"2" (_KERNEL_CS << 16)); }while(0)

38 3.2.3 中断向量表IDT的初始化 在第一个“:”到第二个“:”之间为输出部,有4个约束输出,将有4个变量会被改变,分别为%0、%1、%2和%3相结合。其中%0和%1都是内存单元,分别和gate_addr、gate_addr+1结合,%2于局部变量_d0结合,存放在寄存器%%eax中;%3于局部变量_d1结合,存放在寄存器%%edx中。 第二个“:”之后的部分是输入部,输出部已经定义了%0-%3,输入部中的第一个变量为%4,而紧接着的第二、第三个变量分别等价于输出部的%3和%2。输入部中说明的个输入变量地值,包括%3和%2,都会在引用这些变量之前设置好。在执行指令部的代码之前,会先将%%eax设成(_KERNEL_CS<<16),把%%edx设为addr,%4变量设置为(0x8000+(dpl<<13)+type<<8),则%4变量对应于门结构中的第32-47位,其P位为1。

39 3.2.3 中断向量表IDT的初始化 指令部第一条指令“movw %%dx,%%ax”,将%%dx的低16位移入%%ax的低16位,这样,在%%eax中,其高16位为_KERNEL_CS,而低16位为addr的低16位,形成了门结构中的第0-31位。 第二条指令“movx %4 ,%%dx”,将%4放入%%dx中,也就是将门结构中的第32-47位放在%%dx中,而对于%%edx来说,就对应着门结构中的高32位。 第三条指令“mov1 %%eax,%0”,将%%edx写入变量%0中,即*gate_addr。 第4条指令“mov1 %%eax,%1”将%%edx写入变量%1中,即*(gate_addr+1)。 将第三、第4条指令合起来看,就是将整个门结构的内容都写道*gate_addr中去了。

40 3.2.3 中断向量表IDT的初始化 void _inti init_IRQ(void) { int i;
#ifndef CONFIG_X86_VISWS-APIC init_ISA_irqs(); #else init_VISWS_APIC_irqs(); #endif // Cover the whole vector space,no vector can escape // us. (some of these will be overridden and become // 'special' SMP interrupts) for (i=0;i< NR_IRQS;i++) { int vector = FIRST_EXTERNAL_VECTOR + i; if (vector != SYSCALL_VECTOR) set_intr_gate(vector,interrupt[i]); }

41 3.2.3 中断向量表IDT的初始化 #ifdef CONFIG_SMP
// IRQ0 must ve given a fixed assignment and initialized, // because it's used before the I0-APIC is set up. set_intr-gate(FIRST_DEVICE_VECTOR,interrupt[0]); // The reschedule interrupt is a CPU-to-CPU reschedule-helper IPI,driven by wakeup. set_intr_gate(RESCHEDULE_VECTOR,reschedule_interrupt); // IPI for generic function call set_intr_gate(CALL_FUCTION_VECTOR,call_funtion_interrupt); #endif

42 3.2.3 中断向量表IDT的初始化 #ifdef CONFIG_X86_LOCAL_APIC
// IPI vectors for APIC spurious and error interrupts set_intr_gate(SPURIOUS_APIC_VECTOR,spurious_interrupt); set_intr_gate(ERROR_APIC_VECTOR,error_interrupt); #endif // Set the clock to HZ Hz, we already have a valid vector now; outb_p(0x34,0x43); // binary, mode 2, LSB/MSB ,ch 0 outb_p(LATCH & 0xff,0x40);// LSB outb(LATCH >> 8, 0x40); // MSB #ifndef CONFIG_VISWS setup_irq(2, &irq2) // External FPU? Set up irq13 if so,for original braindamaged IBM FERR coupling. if (boot_cpu_data.hard_math && !cpu_has_fpu) setup_irq(13,&irq13);

43 3.2.3 中断向量表IDT的初始化 i386体系支持256个中断向量。扣除了为CPU保留的向量后,很难说剩下的中断向量是否够用。所以,Linux系统中为每个中断向量设置一个队列,根据每个中断源所使用的中断向量,将其中断服务程序挂到相应的队列中。数组irq_desc[]中的每个元素则是这样一个队列头部以及控制结构。当中断发生时,首先执行中断向量相对应的一段总服务程序,根据具体中断源的设备号在其所属队列中找到特定的服务程序加以执行。 首先对PC的中断控制器8259A的初始化,并初始化了数组iirq_desc[]。接着从FIRST_EXTERNAL_VECTOR开始,设立NR_IRQS个中断向量的 IDT表项。常数FIRST_EXTERNAL_VECTOR定义为 0x20,而NR_IRQS则为224。其中还跳过了用于系统调用的向量0x80。

44 3.2.3 中断向量表IDT的初始化 忽略多处理器SMP结构和SG1工作站的特殊处理,剩下的就是对系统时钟的初始化。在PC中,定时器/计数器芯片8254共有三个通道,通道0是一个产生实时时钟信号的系统计时器,而程序中要设置的也就是通道0。用于控制8254的端口共有4个,前三个分别对应于单个通道的端口,最后一个通道对应于8254的控制字寄存器端口。 outb_p(0x34,0x43);//设置通道的工作方式 //选通通道0,先读写高字节,后读写低字节,工作于方式2,二进制数 outb_p(LATCH&0xff,0x40); //写入低字节 outb(LATCH>>8,0x40); //写入高字节 //设置通道0的记数值

45 3.2.3 中断向量表IDT的初始化 到此已经设置好了IDT,也有了一个中断向量:0号中断时钟中断。虽然该中断服务的入口地址已经设置到中断向量表中,但还没有把0号中断具体的中断服务程序挂到0号中断的队列中去。此时,这些中断地队列都是空的,即使开了中断并产生了时钟中断,也只不过是让它在中断处理的总服务程序中空跑一趟。 设置好了中断向量表,中断队列都还是空的。想要中断程序生效,下一步就要初始化中断请求队列,并把具体的中断服务程序挂到中断队列中去。

46 3.2.4 中断请求队列的初始化 通用中断门是多个中断源共用的,在系统运行的过程中允许动态改变。因此,在IDT的初始化阶段只是为每个中断向量准备一个中断请求队列,即中断请求队列的数组irq_desc[]。中断请求队列头部的数据结构是在include/linux/irq.h中定义的。

47 3.2.4 中断请求队列的初始化 struct hw_interrupt_type {
const char * typename; //赋予控制器的人工可读的名字 unsigned int (*startup)(unsigned int irq); //允许从给定的控制器的IRQ事件发生 void (*shutdown)(unsigned int irq); //禁止从给定的控制器的IRQ事件发生 void (*enable)(unsigned int irq); void (*ack)(unsigned int irq); void (*end)(unsigned int irq); void (*set_affinity)(unsiged int irq,unsigned long mask); };

48 3.2.4 中断请求队列的初始化 typedef struct hw_interupt_type hw_irq_controller;
unsigned int status; // IRQ status hw_irq_controller *handler; struct irqaction *action; //IRQ action list unsigned int depth; // nested irq disables //irq_desc_t 当前用户的个数。用于保证在事件处理过程中IRQ不会被禁止 spinlock_t lock; }_cacheline_aligned irq_desc_t; extern irq_desc_t irq_desc[NR_IRQS];

49 3.2.4 中断请求队列的初始化 每个队列头中,用指针action来维持一个由中断服务程序描述项构成的单链表,还有一个指针handler指向另一个数据结构hw_interrupt_type。Hw_interrupt_type中的一些函数指针,用于该队列的,而不是用于具体的中断源的服务。 这些函数都是在init_ISA_irqs中设置好的。 void_init init //ISA_irqs中设置好的 { int i; init_8259A(0); for (i=0;i< } irq_desc[i].handler="&i8259A_irq_type;" * demand on in filled IRQs PCI *?high? { else interrupts; INTA-cycle old-style 16 (i

50 3.2.4 中断请求队列的初始化 先对8259A进行初始化,将开头16个中断请求队列的handler指针设置成指向数据结构i8259A_irq_type。 struct irqcation { void (*handler)(int,void *,struct pt_regs *); //指向具体中断服务程序 unsigned long flags; unsigned long mask; const char *name; void *dev_id; struct irqaction *next; };

51 3.2.4 中断请求队列的初始化 IDT表初始化之初,每个中断服务队列都是空的。真正的中断服务要到具体设备的初始化程序将其中断服务程序通过reques_irq()向系统“登记”,挂进某个中断请求队列。 int request_irq(unsigned int irq, void (*handler)(int,void*, struct pt_regs *), unsigned long irqflags, const char * devname, void *dev_id) 参数表中的irq为中断请求队列的序号,对应中断控制器的一个通道。这个中断请求号和CPU所用的中断向量是不同的,中断请求号0相当于中断向量0x20。Ireflags 是一些标志位,其中的SA_SHIRQ标志与其它中断源共用该中断请求通道。此时,必须提供一个dev_id以供区别。 在request_irq中分配并设置了irqaction结构,便调用setup_irq将其链入响应的中断请求队列。

52 3.3 Linux2.4 的软中断处理机制 根据与软件相关或和硬件相关,Linux的中断可分为软中断和硬中断两种。软中断是一种“信号机制”,Linux通过信号来产生对进程的各种中断操作,现在知道的信号共有31个。 一般来说,软中断是由内核机制的触发事件引起的(例如进程运行超时),但是不可忽视有大量的软中断也是由于和硬件有关的中断引起的,例如当打印机端口产生一个硬件中断时,会通知和硬件相关的硬中断,硬中断就会产生一个软中断并送到操作系统内核里,内核就会根据这个软中断唤醒在打印机任务队列中的睡眠进程。

53 3.3 Linux2.4 的软中断处理机制 软中断是利用硬件中断的概念,用软件方式进行模拟,实现宏观上的异步执行效果。硬中断是外部设备对CPU的中断,软中断通常是硬中断服务程序对内核的中断,信号则是由内核(或其它进程)对某个进程的中断。软中断的一种典型应用是所谓的“下半部”(bottom half),它的得名来自于将硬件中断处理分离成“上半部”和“下半部”两个阶段的机制:上半部在屏蔽中断的上下文中运行,用于完成关键性的处理动作;而下半部则处理相对不是非常紧急的,比较耗时的动作。因此,下半部由系统自行安排运行时机,不在中断服务上下文中执行。bottom half的应用也是激励内核发展出目前的软中断机制的原因。软中断是Linux系统原“底半处理”的升级,在原有的基础上发展的新的处理方式,以适应多CPU 、多线程的软中断处理。要了解软中断,必须要先了解原来的底半处理的处理机制。

54 底半处理 在Linux内核中,bottom half通常用"bh"表示,最初用于在特权级较低的上下文中完成中断服务的非关键耗时动作,现在也用于一切可在低优先级的上下文中执行的异步动作。最早的bottom half实现是借用中断向量表的方式,在目前的2.4.x内核中仍然可以看到: static void (*bh_base[32])(void);// kernel/softirq.c 系统定义了一个函数指针数组,共有32个函数指针,采用数组索引来访问,与此相对应的是一套函数: void init_bh(int nr,void (*routine)(void)); //为第nr个函数指针赋值为routine void remove_bh(int nr); //动作与init_bh()相反,卸下nr函数指针 void mark_bh(int nr); //标志第nr个bottom half可执行了

55 3.3.2 底半处理 由于历史的原因,bh_base各个函数指针位置大多有了预定义的意义,在v2.4.x内核里有这样一个枚举:
底半处理 由于历史的原因,bh_base各个函数指针位置大多有了预定义的意义,在v2.4.x内核里有这样一个枚举: enum { TIMER_BH = 0, TQUEUE_BH, DIGI_BH, SERIAL_BH, RISCOM8_BH, SPECIALIX_BH, AURORA_BH, ESP_BH, SCSI_BH, IMMEDIATE_BH, CYCLADES_BH, CM206_BH, JS_BH, MACSERIAL_BH, ISICOM_BH };

56 底半处理 并约定某个驱动使用某个bottom half位置,比如串口中断就约定使用SERIAL_BH,现在用得多的主要是TIMER_BH、TQUEUE_BH和IMMEDIATE_BH,但语义已经很不一样了,因为整个bottom half的使用方式已经很不一样了,这三个函数仅仅是在接口上保持了向下兼容,在实现上一直都在随着内核的软中断机制在变。在2.4.x内核里,它用的是tasklet机制。

57 3.3.2 与底半处理相关的数据结构: 某些特殊时刻并不能在内核中执行操作。例如中断处理过程中。当中断发生时,处理器将停止当前正在执行的指令, 操作系统将中断发送到相应的设备驱动程序上去处理。此时系统中其他程序都不能运行, 因此,在这段时间内,设备驱动程序要以最快的速度完成中断处理,设备驱动的中断处理过程不宜过长。Linux 内核利用底层处理过程帮助实现中断的快速处理。对于在中断处理过程之外进行的其他大部分工作,Linux底层部分处理机制可以让设备驱动和Linux内核其他部分将这些工作进行排序以延迟执行。

58 3.3.2 与底半处理相关的数据结构: 系统中最多可以有32个不同的底层处理过程;bh_base是指向这些过程入口的指针数组。而bh_active和 bh_mask用来表示那些处理过程已经安装以及那些处于活动状态。如果bh_mask的第N位置位则表示bh_base的 第N个元素包含底层部分处理例程。如果bh_active的第N位置位,则表示第N个底层处理过程例程,可在调度器认为合适的时刻调用。这些索引被定义成静态的;定时器底层部分处理例程具有最高优先级(索引值为0), 控制台底层部分处理例程其次(索引值为1)。典型的底层部分处理例程含有与之相连的任务链表。例如 immediate底层部分处理例程通过立即任务队列(tq_immediate)执行需要被立刻执行的任务。

59 3.3.2 与底半处理相关的数据结构: 图3.6给出了一个与底层部分处理相关的内核数据结构。bh_base 代表的指针数组中可包含 32 个不同的底层处理过程。bh_mask 和 bh_active 的数据位分别代表对应的底层处理过程是否安装和激活。如果 bh_mask 的第 N 位为 1,则说明 bh_base 数组的第 N 个元素包含某个底层处理过程的地址;如果 bh_active 的第 N 位为 1,则说明必须由调度程序在适当的时候调用第 N 个底层处理过程。

60 3.3.2 与底半处理相关的数据结构: 图3.6 底半处理数据结构

61 3.3.2 与底半处理相关的数据结构: bh_base数组的索引是静态定义的,定时器的底半处理过程的地址保存在第 0 个元素中,控制台底半处理过程的地址保存在第 1 个元素中,等等。当 bh_mask 和 bh_active 表明第 N 个底半处理过程已被安装且处于活动状态,则调度程序会调用第 N 个底半处理过程,该底半处理过程最终会处理与之相关的任务队列中的各个任务。因为调度程序从第 0 个元素开始依次检查每个底半处理过程,因此,第 0 个底半处理过程具有最高的优先级,第 31 个底半处理过程的优先级最低。 内核中的某些底半处理过程是和特定设备相关的,而其它一些则更一般一些。表3.1列出了内核中通用的底半处理过程。

62 3.3.2 与底半处理相关的数据结构: 当某个设备驱动程序,或内核的其它部分需要将任务排队进行处理时,它将任务添加到适当的系统队列中(例如,添加到系统的定时器队列中),然后通知内核,表明需要进行底半处理。为了通知内核,只需将 bh_active 的相应数据位置为 1。例如,如果驱动程序在 immediate 队列中将某任务排队,并希望运行 IMMEDIATE 底半处理过程来处理排队任务,则只需将 bh_active 的第 8 位置为 1。在每个系统调用结束并返回调用进程之前,调度程序要检验 bh_active 中的每个位,如果有任何一位为 1,则相应的底半处理过程被调用。每个底半处理过程被调用时,bh_active 中的相应为被清除。bh_active 中的置位只是暂时的,在两次调用调度程序之间 bh_active 的值才有意义,如果 bh_active 中没有置位,则不需要调用任何底半处理过程。

63 3.3.2 与底半处理相关的数据结构: 表3.1 Linux 中通用的底半处理过程 TIMER_BH(定时器)
在每次系统的周期性定时器中断中,该底半处理过程被标记为活动状态,并用来驱动内核的定时器队列机制。 CONSOLE_BH(控制台) 该处理过程用来处理控制台消息。 TQUEUE_BH(TTY 消息队列) 该处理过程用来处理 tty 消息。 NET_BH(网络) 用于一般网络处理,作为网络层的一部分 IMMEDIATE_BH(立即) 这是一个一般性处理过程,许多设备驱动程序利用该过程对自己要在随后处理的任务进行排队。

64 3.3.2 与底半处理相关的数据结构: bh_mask_count:计数器。对每个enable/disable请求嵌套对进行计数。这些请求通过调用enable_bh和disable_bh实现。每个禁止请求都增加计数器;每个使能请求都减小计数器。当计数器达到0时,所有未完成的禁止语句都已经被使能语句所匹配了,因此下半部分最终被重新使能(其定义见kernel/softirq.c)。 bh_mask和bh_active:它们共同决定下半部分是否运行。它们两个都有32位,而每一个下半部分都占用一位。当一个上半部分(或者一些其它代码)决定其下半部分需要运行时,通过设置bh_active中的一位来标记下半部分。不管是否做这样的标记,下半部分都可以通过清空bh_mask中的相关位来使之失效。因此,对bh_mask和bh_active进行位AND运算就能够表明应该运行哪一个下半部分。如果位与运算的结果是0,就没有下半部分需要运行。

65 3.3.2 与底半处理相关的数据结构: bh_base:是一组简单的指向下半部分处理函数的指针。
bh_base代表的指针数组中可包含 32 个不同的底半处理程序。bh_mask 和 bh_active 的数据位分别代表对应的底半处理过程是否安装和激活。如果 bh_mask 的第 N 位为 1,则说明 bh_base 数组的第 N 个元素包含某个底半处理过程的地址;如果 bh_active 的第 N 位为 1,则说明必须由调度程序在适当的时候调用第 N 个底半处理过程。 

66 3.3.2 与底半处理相关的数据结构: 由此可见,没有必要每次中断都调用下半部分;只有bh_mask 和 bh_active的对应位的与为1时,才必须执行下半部分(do_botoom_half)。所以,如果在上半部分中(也可能在其它地方)决定必须执行对应的半部分,那么可以通过设置bh_active的对应位,来指明下半部分必须执行。当然,如果bh_active的对应位被置位,也不一定会马上执行下半部分,因为还必须具备另外两个条件:首先是bh_mask的相应位也必须被置位,另外,就是处理的时机,如果下半部分已经标记过需要执行了,现在又再次标记,那么内核就简单地保持这个标记;当情况允许的时候,内核就对它进行处理。如果在内核有机会运行其下半部分之前给定的设备就已经发生了100次中断,那么内核的上半部分就运行100次,下半部分运行1次。

67 3.3.3 任务队列 原始的bottom half机制有几个很大的局限,主要的是个数限制在32个以内,随着系统硬件越来越多,软中断的应用范围越来越大,这个数目显然是不够用的。此外,每个bottom half上只能挂接一个函数,也是不够用的。因此,在2.0.x内核里已经在用task queue(任务队列)的办法对其进行了扩充,下面介绍的是2.4.x中的实现。 task queue是在系统队列数据结构的基础上建立的,下面就是task queue的数据结构,其定义位于include/linux/tqueue.h中:

68 3.3.3 任务队列 struct tq_struct { struct list_head list;  // 链表结构 unsigned long sync; // 初始为0,入队时原子的置1,以避免重复入队 void (*routine)(void *); // 激活时调用的函数 void *data; // routine(data) }; typedef struct list_head task_queue;

69 3.3.3 任务队列 在使用时,按照下列步骤进行: DECLARE_TASK_QUEUE(my_tqueue);
// 定义一个my_tqueue,实际上就是一个以tq_struct为元素的list_head队列 //说明并定义一个tq_struct变量my_task; queue_task(&my_task,&my_tqueue); // 将my_task注册到my_tqueue中 run_task_queue(&my_tqueue); // 在适当的时候手工启动my_tqueue 大多数情况下,都没有必要调用DECLARE_TASK_QUEUE()来定义自己的task queue,因为系统已经预定义了三个task queue: tq_timer,由时钟中断服务程序启动; tq_immediate,在中断返回前以及schedule()函数中启动; tq_disk,内存管理模块内部使用。 一般使用tq_immediate就可以完成大多数异步任务了。

70 3.3.3 任务队列 run_task_queue(task_queue *list)函数可用于启动list中挂接的所有task,可以手动调用,也可以挂接在上面提到的bottom half向量表中启动。以run_task_queue()作为bh_base[nr]的函数指针,实际上就是扩充了每个bottom half的函数句柄数,而系统预定义的tq_timer和tq_immediate分别挂接在TQUEUE_BH和IMMEDIATE_BH上(注意,TIMER_BH没有这样使用,但TQUEUE_BH也是在do_timer()中启动的),从而可以扩充bottom half的个数。因此,并不需要手工调用run_task_queue()(这原本就不合适),而只需调用mark_bh(IMMEDIATE_BH),让bottom half机制在合适的时候调度它。

71 tasklet 上面介绍的task queue是以bottom half为基础;而在Linux2.4.x中,bottom half则以新引入的tasklet为实现基础。引入tasklet最主要的考虑是为了更好的支持SMP(多CPU),提高SMP的利用率:不同的tasklet可以同时运行于不同的CPU上。在tasklet的源码注释中还说明了几点特性,可归结为一点,即同一个tasklet只会在一个CPU上运行。

72 3.3.4 tasklet tasklet其实就是一个函数。它的结构如下: struct tasklet_struct {
struct tasklet_struct *next; // 队列指针 unsigned long state; // tasklet的状态,按位操作,目前定义了两个位的含义: //TASKLET_STATE_SCHED(第0位)或TASKLET_STATE_RUN(第1位) atomic_t count; // 引用计数,通常用1表示disabled void (*func)(unsigned long); // 函数指针 unsigned long data;// func(data) };

73 tasklet 把上面的结构与tq_struct比较,可以看出,tasklet扩充了一点功能,主要是state属性,用于CPU间的同步。 tasklet的使用相当简单: void my_tasklet_func(unsigned long); //定义一个处理函数 DECLARE_TASKLET(my_tasklet,my_tasklet_func,data); //定义一个tasklet结构my_tasklet,与//my_tasklet_func(data)函数相关联,相当于DECLARE_TASK_QUEUE() tasklet_schedule(&my_tasklet); //登记my_tasklet,允许系统在适当的时候进行调度运行,相当于//queue_task(&my_task,&tq_immediate)和mark_bh(IMMEDIATE_BH)

74 tasklet 可见tasklet的使用比task queue更简单,而且tasklet还能更好的支持SMP结构,因此,在Linux 2.4.x内核中,tasklet是推荐的异步任务执行机制。除了以上提到的使用步骤外,tasklet机制还提供了另外一些调用接口: DECLARE_TASKLET_DISABLED(name,function,data); //和DECLARE_TASKLET()类似,不过即使被调度到也不会马上运行,必须等到enable tasklet_enable(struct tasklet_struct *); // tasklet使能 tasklet_disble(struct tasklet_struct *); // 禁用tasklet,只要tasklet还没运行,则会推迟到它被enable tasklet_init(struct tasklet_struct *,void (*func)(unsigned long),unsigned long); // 类似DECLARE_TASKLET() tasklet_kill(struct tasklet_struct *); // 清除指定tasklet的可调度位,即不允许调度该tasklet,但不做//tasklet本身的清除

75 tasklet 在Linux 2.4.x内核中, bottom half则是利用tasklet机制实现的,它表现在所有的bottom half动作都以一类tasklet的形式运行,这类tasklet与一般使用的tasklet不同。 在Linux 2.4.x内核里,系统定义了两个tasklet队列的向量表,每个向量对应一个CPU(向量表大小为系统能支持的CPU最大个数,2.4.x的SMP为32),组成一个tasklet链表: struct tasklet_head tasklet_vec[NR_CPUS] __cacheline_aligned; struct tasklet_head tasklet_hi_vec[NR_CPUS] __cacheline_aligned; 另外,对于32个bottom half,系统也定义了对应的32个tasklet结构: struct tasklet_struct bh_task_vec[32];

76 tasklet 在软中断子系统初始化时,这组tasklet的动作被初始化为bh_action(nr),而bh_action(nr)就会去调用bh_base[nr]的函数指针,从而与bottom half的语义挂钩。mark_bh(nr)被实现为调用tasklet_hi_schedule(bh_tasklet_vec+nr),在这个函数中,bh_tasklet_vec[nr]将被挂接在tasklet_hi_vec[cpu]链上(其中cpu为当前CPU编号,也就是说哪个CPU提出了bottom half的请求,则在哪个CPU上执行该请求),然后激发HI_SOFTIRQ软中断信号,从而在HI_SOFTIRQ的中断响应中启动运行。 tasklet_schedule(&my_tasklet)将把my_tasklet挂接到tasklet_vec[cpu]上,激发TASKLET_SOFTIRQ,在TASKLET_SOFTIRQ的中断响应中执行。HI_SOFTIRQ和TASKLET_SOFTIRQ是softirq子系统中的术语。

77 3.3.5 软中断 从前面的讨论可以看出,task queue基于bottom half,bottom half基于tasklet,而tasklet则基于softirq。可以说,softirq沿用的是最早的bottom half思想,但在这个“bottom half”机制之上,已经实现了一个更加庞大和复杂的软中断子系统。 struct softirq_action { void(*action)(struct softirq_action *); void*data; }; static struct softirq_action softirq_vec[32] __cacheline_aligned; 这个softirq_vec[ ]仅比bh_base[]增加了action()函数的参数,在执行上,softirq比bottom half的限制更少。

78 3.3.5 软中断 和bottom half类似,系统也通过以下枚举预定义了几个softirq_vec[ ]结构的用途: enum {
HI_SOFTIRQ=0, NET_TX_SOFTIRQ, NET_RX_SOFTIRQ, TASKLET_SOFTIRQ };

79 3.3.5 软中断 HI_SOFTIRQ被用于实现bottom half,TASKLET_SOFTIRQ用于公共的tasklet使用,NET_TX_SOFTIRQ和NET_RX_SOFTIRQ用于网络子系统的报文收发。在软中断子系统初始化(softirq_init())时,调用了open_softirq()对HI_SOFTIRQ和TASKLET_SOFTIRQ做了初始化: void open_softirq(int nr, void (*action)(struct softirq_action*), void *data) open_softirq()会填充softirq_vec[nr],将action和data设为传入的参数。 TASKLET_SOFTIRQ填充为tasklet_action(NULL),HI_SOFTIRQ填充为tasklet_hi_action(NULL)。在do_softirq()函数中,这两个函数会被调用,分别启动tasklet_vec[cpu]和tasklet_hi_vec[cpu]链上的tasklet运行。 static inline void __cpu_raise_softirq(int cpu, int nr) 这个函数用来激活软中断,实际上就是第cpu号CPU的第nr号软中断的active位置1。在do_softirq()中将判断这个active位。tasklet_schedule()和tasklet_hi_schedule()都会调用这个函数。

80 3.3.5 软中断 do_softirq()有4个执行时机,分别是:
(1) 从系统调用中返回(arch/i386/Kernel/entry.S::ENTRY(ret_from_sys_call)) (2) 从异常中返回(arch/i386/Kernel/entry.S::ret_from_exception标号) (3) 调度程序中(kernel/sched.c::schedule()) (4) 以及处理完硬件中断之后(kernel/irq.c::do_IRQ())。 它将遍历所有的softirq_vec,依次启动其中的action()。需要注意的是,软中断服务程序,不允许在硬中断服务程序中执行,也不允许在软中断服务程序中嵌套执行,但允许多个软中断服务程序同时在多个CPU上并发。

81 3.3.6 使用示例 softirq作为一种底层机制,很少由内核程序员直接使用。因此,下面仅对其余几种软中断机制给出使用范例。
1.bottom half 原有的bottom half用法在drivers/char/serial.c中还能看到,包括三个步骤: init_bh(SERIAL_BH,do_serial_bh); //在串口设备的初始化函数rs_init()中,do_serial_bh()是处理函数 mark_bh(SERIAL_BH); //在rs_sched_event()中,这个函数由中断处理例程调用 remove_bh(SERIAL_BH);   //在串口设备的结束函数rs_fini()中调用

82 3.3.6 使用示例 尽管逻辑上还是这么三步,但在do_serial_bh()函数中的动作却是启动一个task queue:run_task_queue(&tq_serial),而在rs_sched_event()中,mark_bh()之前调用的则是queue_task(...,&tq_serial),也就是说串口bottom half已经结合task queue使用了。而那些更通用一些的bottom half,比如IMMEDIATE_BH,更是必须要与task queue结合使用,而且一般情况下,task queue也很少独立使用,而是与bottom half结合,这在下面的task queue使用示例中可以清楚地看到。

83 3.3.6 使用示例 2.task queue 一般来说,程序员很少自己定义task queue,而是结合bottom half,直接使用系统预定义的tq_immediate等,尤以tq_immediate使用最多。下节是选自drivers/block/floppy.c的代码段: static struct tq_struct floppy_tq; //定义一个tq_struct结构变量floppy_tq,不需要作其它初始化动作 static void schedule_bh( void (*handler)(void*) ) { floppy_tq.routine = (void *)(void *) handler; //指定floppy_tq的调用函数为handler,不需要考虑floppy_tq中的其它域 queue_task(&floppy_tq, &tq_immediate); //将floppy_tq加入到tq_immediate中 mark_bh(IMMEDIATE_BH); //激活IMMEDIATE_BH,由上所述可知,这实际上将引发一个软中断来执行 // tq_immediate中挂接的各个函数 }

84 3.3.6 使用示例 当然,也可以定义并使用自己的task queue,而不用tq_immediate,在drivers/char/serial.c中提到的tq_serial就是串口驱动自己定义的: static DECLARE_TASK_QUEUE(tq_serial); 此时就需要自行调用run_task_queue(&tq_serial)来启动其中的函数,因此并不常用。

85 3.3.6 使用示例 3.tasklet 这是比task queue和bottom half更加强大的一套软中断机制,使用上也相对简单,见下面代码段: 1:void foo_tasklet_action(unsigned long t); 2:unsigned long stop_tasklet; 3:DECLARE_TASKLET(foo_tasklet, foo_tasklet_action, 0); 4:void foo_tasklet_action(unsigned long t) 5:{ 6://do something 7: 8://reschedule 9:if(!stop_tasklet) 10:tasklet_schedule(&foo_tasklet);

86 3.3.6 使用示例 11:} 12:void foo_init(void) 13:{ 14:stop_tasklet=0;
15:tasklet_schedule(&foo_tasklet); 16:} 17:void foo_clean(void) 18:{ 19:stop_tasklet=1; 20:tasklet_kill(&foo_tasklet); 21:}

87 3.3.6 使用示例 这是个利用一个反复执行的tasklet来完成一定的工作的比较完整的代码段。首先在第3行定义foo_tasklet与相应的动作函数foo_tasklet_action相关联,并指定foo_tasklet_action()的参数为0。虽然此处以0为参数,但也同样可以指定有意义的其它参数值,但需要注意的是,这个参数值在定义的时候必须是有固定值的变量或常数(如上例),也就是说可以定义一个全局变量,将其地址作为参数传给foo_tasklet_action(),例如: int flags; DECLARE_TASKLET(foo_tasklet,foo_tasklet_action,&flags); void foo_tasklet_action(unsigned long t) { int flags=*(int *)t; ... }

88 3.3.6 使用示例 这样就可以通过改变flags的值将信息带入tasklet中。直接在DECLARE_TASKLET处填写flags,gcc会报"initializer element is not constant"错。 第9、10行是一种RESCHEDULE的技术。知道,一个tasklet执行结束后,它就从执行队列里删除了,要想重新让它转入运行,必须重新调用tasklet_schedule(),调用的时机可以是某个事件发生的时候,也可以是像这样在tasklet动作中。而这种reschedule技术将导致tasklet永远运行,因此在子系统退出时,应该有办法停止tasklet。stop_tasklet变量和tasklet_kill()就是干这个的。

89 3.3.6 使用示例 下面,进入软中断处理部份(softirq.c):
由softirq.c的代码阅读中,可以知道,在系统的初始化过程中(softirq_init()),它使用了两个数组:bh_task_vec[32],softirq_vec[32]。其中,bh_task_vec[32]填入了32个bh_action()的入口地址,但soft_vec[32]中,只有softirq_vec[0],和softirq_vec[3]分别填入了tasklet_action()和tasklet_hi_action()的地址。其余的保留它用。

90 3.3.6 使用示例 当发生软中断时,系统并不急于处理,只是将相应的CPU的中断状态结构中的active 的相应的位置位,并将相应的处理函数挂到相应的队列,然后等待调度时机来临(如:schedule(), 系统调用返回异常时,硬中断处理结束时等),系统调用do_softirq()来测试active位,再调用被激活的进程在这处过程中,软中断的处理与底半处理有了差别,active 和mask不再对应bh_base[nr], 而是对应softirq_vec[32]。在softirq.c中,只涉及了softirq_vec[0]、softirq_vec[3]。这两者分别调用了tasklet_action()和tasklet_hi_action()来进行后续处理。这两个过程比较相似,大致如下: (1) 锁CPU的tasklet_vec[cpu]链表,取出链表,将原链表清空,解锁,还给系统。 (2) 对链表进行逐个处理。 (3) 有无法处理的,(task_trylock(t)失败,可能有别的进程锁定),插回系统链表。至此,系统完成了一次软中断的处理。

91 3.3.7 讨论 (1) bh_base[]依然存在,但应在何处调用? (2) tasklet_vec[cpu]队列是何时挂上的?
再次考察softirq.c 的bh_action()部份,发现有两个判断语句: (1)if(!spin_trylock(&global_bh_lock))goto:rescue 指明如果global_bh_lock 不能被锁上(已被其它进程锁上),则转而执行rescue,将bh_base[nr]挂至tasklet_hi_vec[cpu]队列中。等候中断调度。 (2)if(!hardirq_trylock(cpu)) goto tescue unlock 此时有硬中断发生,放入队列推迟执行。若为空闲,现在执行。

92 3.3.7 讨论 这部分对应底半处理的程序,bh_base[ ]的延时处理正是底半处理的特点。如果没有其它函数挂入tasklet_hi_vec[cpu]队列,那tasklet_hi_vec[cpu]就完全对应着bh_base[ ]底半处理。 在bh_action()中,把bh_ation()挂入tasklet_hi_vec[cpu]的是mark_bh()。在整个源码树中查找,发现调用mark_bh()的函数很多。可以理解为,在软中断产生之时,相关的函数会调用mark_bh(),将bh_action挂上tasklet_hi_vec队列,而bh_action()的作用是在发现bh_base[nr]暂时无法处理时重返队列。 由此,可推测tasklet_vec队列的挂接应与此相似,查看interrupt.h,找到tasklet_schedule()函数:

93 3.3.7 讨论 157 static inline void tasklet_schedule(struct tasklet_struct *t) 158 { 159 if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) { 160 int cpu = smp_processor_id(); 161 unsigned long flags; 162 163 local_irq_save(flags); 164 t->next = tasklet_vec[cpu].list; 165 tasklet_vec[cpu].list = t; /*插入队列。 166 __cpu_raise_softirq(cpu, TASKLET_SOFTIRQ); 167 local_irq_restore(flags); 168 } 169 }

94 3.3.7 讨论 它为tasklet_vec[cpu]队列的建立起着重要的作用,在源码树中,它亦被多个模块调用。
至此,可以描绘一幅完整的软中断处理图了。 下面讨论do_softirq()的softirq_vec[32],在interrupt.h中有如下定义: 56 enum 57 { 58 HI_SOFTIRQ=0, 59 NET_TX_SOFTIRQ, 60 NET_RX_SOFTIRQ, 61 TASKLET_SOFTIRQ 62 };

95 3.3.7 讨论 这4个变量都是softirq_vec[]的下标,do_softirq()也将会处理NET_TX_SOFTIRQ和NET_RX_SOFTIRQ。 在hi_tasklet(也就是一般用于bh的)的处理中,在处理完当前的队列后,会将补充的队列重新挂上,然后标记(不管是否补充队列里面有tasklet): local_irq_disable(); t->next = tasklet_hi_vec[cpu].list; tasklet_hi_vec[cpu].list = t; __cpu_raise_softirq(cpu, HI_SOFTIRQ); local_irq_enable();

96 3.3.7 讨论 因此,对mark_bh不需要设置这个active位。对于一般的tasklet也一样:
local_irq_disable(); t->next = tasklet_vec[cpu].list; tasklet_vec[cpu].list = t; __cpu_raise_softirq(cpu, TASKLET_SOFTIRQ); local_irq_enable(); 其它的设置,可以检索上面的__cpu_raise_softirq : bottom half, softirq, tasklet, tqueue [bottom half] bh_base[32] | \/ bh_action(); bh_task_vec[32]; | mark_bh(), tasklet_hi_schedule()

97 3.3.7 讨论 task_hi_action bh_base对应的是32个函数,这些函数在bh_action()中调用 static void bh_action(unsigned long nr) { int cpu = smp_processor_id(); if (!spin_trylock(&global_bh_lock)) goto resched; if (!hardirq_trylock(cpu)) goto resched_unlock; if (bh_base[nr]) bh_base[nr](); hardirq_endlock(cpu); spin_unlock(&global_bh_lock); return; resched_unlock: spin_unlock(&global_bh_lock); resched: mark_bh(nr); }

98 3.3.7 讨论 在软中断初始化时,将bh_action()放到bh_task_vec[32]中,bh_task_vec[32]中元素的类型是tasklet_struct,系统使用mark_bh()或task_hi_schedule()函数将它挂到task_hi_vec[]的对列中,在系统调用do_softirq()时执行。

99 3.3.7 讨论 static inline void mark_bh(int nr) { tasklet_hi_schedule(bh_task_vec+nr); } static inline void tasklet_hi_schedule(struct tasklet_struct *t) { if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) { int cpu = smp_processor_id(); unsigned long flags; local_irq_save(flags); t->next = tasklet_hi_vec[cpu].list; tasklet_hi_vec[cpu].list = t; __cpu_raise_softirq(cpu, HI_SOFTIRQ); local_irq_restore(flags); } } [softirq] softirq_vec[32]; struct softirq_action { void (*action)(struct softirq_action *); void *data; };

100 3.3.7 讨论 软中断对应一个softirq_action的结构,在do_softirq()中调用相应的action()做处理。 软中断初始化时只设置了0,3两项,对应的action是task_hi_action和task_action. 1: task_hi_action /\ | tasklet_hi_vec[NR_CPU] struct tasklet_head tasklet_hi_vec[NR_CPUS] __cacheline_aligned; struct tasklet_head { struct tasklet_struct *list; } __attribute__ ((__aligned__(SMP_CACHE_BYTES))); task_hi_action处理的对象是一个tasklet的队列,每个CPU都有一个对应的tasklet队列, 它在tasklet_hi_schedule中动态添加。

101 3.3.7 讨论 3: task_action /\ | tasklet_vec[NR_CPU]
[tasklet] struct tasklet_struct { struct tasklet_struct *next; unsigned long state; atomic_t count; void (*func)(unsigned long); unsigned long data; };

102 3.3.7 讨论 从上面的分析来看,tasklet只是一个调用实体,在do_softirq()中被调用。softirq的组织和结构才是最重要的。

103 3.4 中断处理全过程 对于0-31号中断向量,被保留用来处理异常事件;0x80中断向量用来作为系统调用的总入口点;而其它中断向量,则用来处理外部设备中断;这三者的处理过程都是不一样的。

104 3.4.1 异常的处理全过程 Intel公司保留0-31号中断向量用来处理异常事件,异常的处理程序由操作系统提供。并在初始化时把处理程序的入口等级在对应的中断向量表项中。当产生一个异常时,处理机就会自动把控制转移到相应的处理程序的入口,运行相应的处理程序,进行相应的处理后,返回原中断处。对于这32个处理异常的中断向量,Linux只提供了表3.2中的0-17号中断向量的处理程序;17-31号中断向量空着未用。

105 3.4.1 异常的处理全过程 表3.2 中断向量和异常事件对应表 中断向量号 异常事件 Linux的处理程序 除法错误
异常事件   Linux的处理程序 除法错误 Divide_error 1 调试异常 Debug 2 NMI中断 Nmi 3 单字节,int 3 Int3 4 溢出 Overflow 5 边界监测中断 Bounds 6 无效操作码 Invalid_op 7 设备不可用 Device_not_available 8 双重故障 Double_fault 9 协处理器段溢出 Coprocessor_segment_overrun 10 无效TSS Incalid_tss 11 缺段中断 Segment_not_present 12 堆栈异常 Stack_segment 13 页异常 General_protection 14 一般保护异常 Page_fault 15 Spurious_interrupt_bug 16 协处理器出错 Coprocessor_error 17 对齐检查中断 Alignment_check

106 3.4.2 外部设备中断处理的全过程 对于0-31号和0x80之外的中断向量,主要用来处理外部设备中断;在系统完成初始化后,其中断处理过程如下: 外部中断是外部硬件(如时钟)引起的中断,当外部设备需要处理机进行中断服务时,它就会通过中断控制器要求处理机进行中断服务。如果 CPU 这时可以处理中断,CPU将根据中断控制器提供的中断向量号和中断描述符表(IDT)中的登记的地址信息,自动跳转到相应的interrupt[i]地址,即中断入口;在进行一些简单的但必要的处理后,最后都会调用函数do_IRQ , do_IRQ函数调用 do_8259A_IRQ 而do_8259A_IRQ在进行必要的处理后,将调用已与此IRQ建立联系irqaction中的处理函数,以进行相应的中断处理。最后处理机将跳转到ret_from_intr进行必要处理后,整个中断处理结束返回。

107 3.4.2 外部设备中断处理的全过程 一个完整的外部中断包括4件工作: (1) 将外部设备和中断芯片相应的管脚接上;
(2) 对中断芯片设置,使得特定管脚的中断能映射到CPU IDT特定的位置; (3) 在程序中包含这些中断入口; (4) 在中断向量表里设置向量相应的入口地址。 这些工作需要在外部中断流程里描述。 由于硬件设备可能共用一个中断,在统一的函数中会有相应的结构来处理,也就是有16个结构分别处理相应的16个中断,特定的硬件驱动需要将自己的处理函数挂接到特定的结构上。

108 3.4.2 外部设备中断处理的全过程 因为PCI的设备驱动程序总是知道它们的中断号,在PCI结构中不会有问题。但对于ISA结构来说,一个设备驱动程序可能不知道设备将使用哪一个中断。Linux系统通过允许设备驱动程序探测自己的中断来解决这个问题。 首先,设备驱动程序使得设备产生一个中断。然后,允许系统中所有没有指定的中断,这意味着设备挂起的中断将会通过中断控制器传送。Linux 系统读取中断状态寄存器然后将它的值返回到设备驱动程序。一个非0 的结果意味着在探测期间发生了一个或者多个的中断。设备驱动程序现在可以关闭探测,这时所有还未被指定的中断将继续被禁止。

109 3.4.2 外部设备中断处理的全过程 某个ISA设备驱动程序知道了它的中断号以后,就可以请求对中断的控制了。PCI 结构的系统中断比I S A 结构的系统中断要灵活得多。ISA设备使用中断插脚经常使用跳线设置,所以在设备驱动程序中是固定的。但PCI 设备是在系统启动过程中PCI初始化时由PCI BIOS或PCI子系统分配的。每一个PCI 设备都有可能使用A、B、C或者D这4 个中断插脚中的一个。默认情况下设备使用插脚A。 每个PCI插槽的PCI中断A、B、C和D是通过路由选择连接到中断控制器上的。所以PCI插槽4的插脚A可能连接到中断控制器的插脚6 ,PCI 插槽4 的插脚B 可能连接到中断控制器的插脚7,以此类推。

110 3.4.2 外部设备中断处理的全过程 PCI的设置代码将中断控制器的插脚号写入到每个设备的PCI设置头中。PCI的设置代码根据所知道的PCI中断路由拓扑结构、PCI设备使用的插槽,以及正在使用的PCI中断的插脚号来决定中断号,也就是IRQ号。 系统中可以有很多的PCI中断源,例如当系统使用了PCI-PCI桥时。这时,中断源的数目可能超过了系统可编程中断控制器上插脚的数目。在这种情况下,某些PCI设备之间就不得不共享一个中断,也就是说,中断控制器上的某一个插脚可以接收来自几个设备的中断。在Linux系统中,第一个中断源请求者应宣布它使用的中断是否可以被共享,以实现中断在几个设备之间的共享。中断共享使得irq_action数组中的同一个入口指向几个设备的irqaction结构。当一个共享的中断有中断发生时,Linux系统将会调用和此中断有关的所有中断处理程序。在任何时候,即使自身没有中断需要处理,所有可以共享中断的设备驱动程序的中断处理程序都可能被调用。

111 3.4.3 后续处理 后续处理有一个重要的下半部分,即时钟中断的下半部分。后续处理主要完成下面的工作: (1) bottom_half
在开中断的状态下,它们继续完成中断处理。因此中断中的处理函数需要在一个32位变量中设置特定的bit来告诉do_softirq要执行哪个bottom_half(或可将该32位数想像成一个新的中断向量表,设置bit相当于产生中断,下半部分相当于handler)bottom_half有的时候需要借助task_queue 结构来完成更多的工作,

112 3.4.3 后续处理 (2) 进程是否能切换 task_queue:task_queue 是一个链表,每个节点是一个函数指针,这样,一 个 bottom_half 就可以执行一个链表上的函数列了。当然 task_queue 不一定只在 bottom_half 中应用, 在一些驱动中也直接调用 run_task_queue 来执行一个特定的队列。 2.4.x版的内核增加了softirq_action tasklet。如果内核需要在某个中断产生后执行它的函数,只需要在它下半部分调用的 task_queue 上挂上它的函数。 (3) 是否需要进行切换 因为 Linux是非抢占的,所以如果返回的代码段是内核级的话,就不允许进行切换。如果能切换判断一下是否需要切换, 如果是就切换。

113 3.4.3 后续处理 (4)信号处理 看是否有信号要处理,如果要调用 do_signal 时钟中断的下半部分。
在上面许多的外部中断中,有一个特殊的中断的处理 timer_interrupt, 它的下半部分主要处理:时间计算和校准定时器工作。

114 练习与思考 1.CPU在什么情况下才响应中断?中断处理过程一般包括那些步骤? 2. 8086的中断系统分为哪几种类型的中断?其优先顺序如何?
2.  8086的中断系统分为哪几种类型的中断?其优先顺序如何? 3.  8086系统中,若端口地址为02C0H,02C2H的8259A是单片、全嵌套工作方式、非特殊屏蔽和非特殊结束方式、中断请求信号边沿触发。当中断响应时,8259A输出的中断类型号为08H。试给出8259A初始化程序。 4.假设你正在设计一种先进的计算机体系结构,它使用硬件而不是中断来完成进程切换,则CPU需要哪些信息?请描述用硬件完成进程切换的工作过程。 5.目前的计算机上,中断处理程序至少有一小部分用汇编语言编写,为什么?

115 练习与思考 6.可否使每个进程拥有自己的cache? 7. 给出一个框架,来描述一个可禁止中断的操作系统如何实现信号量。
8. 什么是中断向量、中断向量表、向量地址? 9. 中断有哪些类型?它在操作系统中起的作用如何? 10. 为实现CPU与外部设备并行工作,必须引入的基础硬件是什么?


Download ppt "第 3 章 中断和中断处理 硬件中断机制是一个操作系统内核中非常重要的部分。它的设计直接影响到操作系统整体的性能。它与硬件平台和内核的其它部分,如内存管理、进程调度、设备驱动等都有很密切的关系。因此,它也是操作系统中比较复杂的一个模块。 Linux的硬件中断机制的设计有很多独到之处,本章把kernel."

Similar presentations


Ads by Google