1 / 115

第 3 章 中断和中断处理

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

misu
Download Presentation

第 3 章 中断和中断处理

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

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年,芬兰赫尔辛基大学一位名叫LinusTorvalds的学生开始了开放源代码的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(瞬时)作为系统时间的测量基准,所有的时间都从 1970.1.1 0:00:00 开始计算,系统启动时,将 CMOS 中记录的时间转化为从 1970.1.1 0:00:00 算起的 jiffies 值。Linux 内核中没有任何时区的概念,Linux 内核中的时间以格林尼治时间记录,将格林尼治时间转换为本地时间的任务则由应用程序负责。 • Linux 的 jiffies 值由两部分组成,分别用 32 位无符号整数记录自 1970.1.1 00: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) • #endif • // 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; • typedef struct { • 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; • };

More Related