zl程序教程

您现在的位置是:首页 >  其他

当前栏目

深入分析Linux内核源代码阅读笔记 第三章

2023-03-07 09:08:37 时间

我的GIS/CS学习笔记:https://github.com/yunwei37/ZJU-CS-GIS-ClassNotes <一个浙江大学本科生的计算机、地理信息科学知识库 >

第三章 中断机制

中断控制是计算机发展中一种重要的技术,最初它是为克服对 I/O 接口控制采用程序查询所带来的处理器低效率而产生的。

  • 外部中断(或硬件中断)
  • 内部中断(或叫异常)

本章主要讨论在 Intel i386 保护模式下中断机制在 Linux 中的实现。

中断基本知识

实模式和保护模式最本质的差别就是在保护模式引入的中断描述符表。

中断向量:

Intel x86 系列微机共支持 256 种向量中断。

  • 异常:既不使用中断控制器,又不能被屏蔽
    • 故障(Fault)
    • 陷阱(Trap)
  • 中断
    • 外部可屏蔽中断(INTR)
    • 外部非屏蔽中断(NMI)

Linux 对 256 个向量的分配如下:

  • 从 0~31 的向量对应于异常和非屏蔽中断
  • 32~47 的向量(即由 I/O 设备引起的中断)分配给屏蔽中断。
  • 剩余的从 48~255 的向量用来标识软中断。
  • 128 用来实现系统调用。
  • 外设可屏蔽中断: Intel x86 通过两片中断控制器 8259A 来响应 15 个外中断源。 中断控制器 8259A 执行如下操作:
    1. 监视中断线,检查产生的中断请求(IRQ)信号。
    2. 如果在中断线上产生了一个中断请求信号。
    3. 把接受到的 IRQ 信号转换成一个对应的向量。
    4. 把这个向量存放在中断控制器的一个 I/O 端口,从而允许 CPU 通过数据总线读此向量
    5. 把产生的信号发送到 CPU 的 INTR 引脚——即发出一个中断
    6. 等待,直到 CPU 确认这个中断信号,然后把它写进可编程中断控制器(PIC)的一个 I/O 端口;此时,清 INTR 线
    7. 返回到第一步
  • 异常及非屏蔽中断 异常就是 CPU 内部出现的中断,也就是说,在 CPU 执行特定指令时出现的非法情况。非屏蔽中断就是计算机内部硬件出错时引起的异常情况。Intel 把非屏蔽中断作为异常的一种来处理。 当某个异常被响应后,CPU 清除 eflag 的中 IF 位,禁止任何可屏蔽中断。 Intel x86 处理器发布了大约 20 种异常(具体数字与处理器模式有关)。每个异常都由专门的异常处理程序来处理。
  • 中断描述符表 在实地址模式中,CPU 把内存中从 0 开始的 1K 字节作为一个中断向量表。 改叫做中断描述符表 IDT,其中的每个表项叫做一个门描述符(Gate Descriptor)
    • 任务门(Task gate)
    • 中断门(Interrupt gate):关中断
    • 陷阱门(Trap gate):不关中断
    • 系统门(System gate):这是 Linux 内核特别设置的,用来让用户态的进程访问 Intel 的陷阱门

    CPU 中增设了一个中断描述符表寄存器 IDTR,用来存放中断描述符表在内存的起始地址

相关汇编指令:

  • 调用过程指令 CAL
  • 调用中断过程指令 INT
  • 调用中断过程指令 INT
  • 中断返回指令 IRET
  • 加载中断描述符表的指令 LIDT

中断描述符表的初始化

Linux 内核在系统的初始化阶段要进行大量的初始化工作,其与中断相关的工作有:

  • 初始化可编程控制器 8259A
  • 将中断向量 IDT 表的起始地址装入 IDTR 寄存器,并初始化表中的每一项。

外部中断向量的设置:

8259A 中断控制器的初始化

void __init init_8259A(int auto_eoi)
{
	unsigned long flags;

	spin_lock_irqsave(&i8259A_lock, flags);

	outb(0xff, 0x21);	/* mask all of 8259A-1 */
	outb(0xff, 0xA1);	/* mask all of 8259A-2 */

	/*
	 * outb_p - this has to work on a wide range of PC hardware.
	 */
	outb_p(0x11, 0x20);	/* ICW1: select 8259A-1 init */
	outb_p(0x20 + 0, 0x21);	/* ICW2: 8259A-1 IR0-7 mapped to 0x20-0x27 */
	outb_p(0x04, 0x21);	/* 8259A-1 (the master) has a slave on IR2 */
	if (auto_eoi)
		outb_p(0x03, 0x21);	/* master does Auto EOI */
	else
		outb_p(0x01, 0x21);	/* master expects normal EOI */

	outb_p(0x11, 0xA0);	/* ICW1: select 8259A-2 init */
	outb_p(0x20 + 8, 0xA1);	/* ICW2: 8259A-2 IR0-7 mapped to 0x28-0x2f */
	outb_p(0x02, 0xA1);	/* 8259A-2 is a slave on master's IR2 */
	outb_p(0x01, 0xA1);	/* (slave's support for AEOI in flat mode
				    is to be investigated) */

	if (auto_eoi)
		/*
		 * in AEOI mode we just have to mask the interrupt
		 * when acking.
		 */
		i8259A_irq_type.ack = disable_8259A_irq;
	else
		i8259A_irq_type.ack = mask_and_ack_8259A;

	udelay(100);		/* wait for 8259A to initialize */

	outb(cached_21, 0x21);	/* restore master IRQ mask */
	outb(cached_A1, 0xA1);	/* restore slave IRQ mask */

	spin_unlock_irqrestore(&i8259A_lock, flags);
}

中断描述符表 IDT 的预初始化:

  1. 中断描述表寄存器 IDTR 的初始化
  2. 把 IDT 表的起始地址装入 IDTR
  3. 用 setup_idt()函数填充 idt_table 表中的 256 个表项
    • 在对 idt_table 表进行填充时,使用了一个空的中断处理程序 ignore_int()。
    • setup_idt()函数对 IDT 表进行填充

中断向量表的最终初始化:

在对中断描述符表进行预初始化后, 内核将在启用分页功能后对 IDT 进行第二遍初始化,也就是说,用实际的陷阱和中断处理程序替换这个空的处理程序。

  • IDT 表项的设置是通过_set_gate()函数实现的:
    • set_intr_gate
    • set_trap_gate
    • set_system_gate
  • 对陷阱门和系统门的初始化:
    • trap_init()函数就是设置中断描述符表开头的 19 个陷阱门
  • 中断门的设置: 由 init_IRQ( )函数中的一段代码完成的

异常处理

Linux 利用异常来达到两个截然不同的目的:

  • 给进程发送一个信号以通报一个反常情况
  • 处理请求分页

内核对异常处理程序的调用有一个标准的结构,它由以下 3 部分组成:

  • 在内核栈中保存大多数寄存器的内容(由汇编语言实现);
  • 调用 C 编写的异常处理函数;
  • 通过 ret_from_exception()函数从异常退出

进入异常处理程序的汇编指令在 arch/I386/kernel/entry.S 中.

中断请求队列的初始化:

让每个中断源都必须占用一条中断线是不现实的,在 Linux 设计中,专门为每个中断请求 IRQ 设置了一个队列,这就是我们所说的中断 请求队列。

  • 中断线是中断请求的一种物理描述
  • 中断线逻辑上对应一个中断请求号(或简称中断号)
  • 第 n 个中断号(IRQn)的缺省中断向量是 n+32。

对于每个 IRQ,Linux 都用一个 irq_desc_t 数据结构来描述,我们把它叫做 IRQ 描述符;

  • 在系统初始化期间,init_ISA_irqs()函数对 IRQ 数据结构(或叫描述符)的域进行初始化
  • 中断控制器描述符 hw_interrupt_type 包含一组指针,指向与特定中断控制器电路(PIC)打交道的低级 I/O 例程
  • 中断服务例程描述符 irqaction 是为多个设备能共享一条中断线而设置的一个数据结构。
  • 中断服务例程 例如网卡和图形卡分别有其相应的中断服务例程。

在 IDT 表初始化完成之初,每个中断服务队列还为空。因此,在设备驱动程序的初始化阶段,必须通过 request_irq() 函数将对应的中断服务例程挂入中断请求队列。其中主要语句就是对 setup_irq()函数的调用,该函数才是真正对中断请求队列进行初始化的函数。

中断处理

本节要关心的主要内容是如何执行中断处理程序。

中断和异常处理的硬件处理:

  • 在对下一条指令执行前,CPU 先要判断在执行当前指令的过程中是否发生了中断或异常:
  • 如果发生了一个中断或异常,那么 CPU 将做以下事情:
    • 确定所发生中断或异常的向量 i(在 0~255 之间)。
    • 通过 IDTR 寄存器找到 IDT 表,读取 IDT 表第 i 项(或叫第 i 个门)。
    • 分两步进行有效性检查:
      • “段”级检查
      • “门”级检查
    • 检查是否发生了特权级的变化

Linux 对中断的处理:

Linux 把一个中断要执行的操作分为下面的 3 类:

  • 紧急的(Critical)
  • 非紧急的(Noncritical)
  • 非紧急可延迟的(Noncritical deferrable):由一些被称为“下半部分”(bottom halves)的函数来执行

所有的中断处理程序都执行 4 个基本的操作:

  • 在内核栈中保存 IRQ 的值和寄存器的内容;
  • 给与 IRQ 中断线相连的中断控制器发送一个应答,这将允许在这条中断线上进一步发出中断请求
  • 执行共享这个 IRQ 的所有设备的中断服务例程(ISR);
  • 跳到 ret_from_intr( )的地址后终止。

与堆栈有关的常量、数据结构及宏:

  • 常量
  • 存放在栈中的寄存器结构 pt_regs
  • 保存现场的宏 SAVE_ALL
  • 恢复现场的宏 RESTORE_ALL
  • 将当前进程的 task_struct 结构的地址放在寄存器中 GET_CURRENT(reg)

中断处理程序的执行:

假定外设的驱动程序都已完成了初始化工作,并且已把相应的中断服务例程挂入到特定的中断请求队列。又假定当前进程正在用户空间运行(随时可以接受中断),且外设已产生了一次中断请求。

  1. 这个中断请求通过中断控制器 8259A 到达 CPU 的中断请求引线 INTR
  2. CPU 在执行完当前指令后来响应该中断
  3. CPU 从中断控制器的一个端口取得中断向量 I
  4. 根据 I 从中断描述符表 IDT 中找到相应的表项
  5. 从这个中断门获得中断处理程序的入口地址
  6. 进行堆栈的切换
  7. 禁用中断线
  8. IRQn_interrupt
  9. do_IRQ()
  10. handle_IRQ_event()
  11. 中断服务例程

从中断返回:

中断、异常及系统调用的返回是放在一起实现的:

  • ret_from_intr() 终止中断处理程序。
  • ret_from_sys_call( )终止系统调用
  • ret_from_exception( ) 终止除了 0x80 的所有异常

中断的后半部分处理机制

内核的目标就是尽可能快地处理完中断请求,尽其所能把更多的处理向后推迟:内核把中断处理分为两部分:前半部分(top half)和后半部分(bottom half),前半部分内核立即执行,而后半部分留着稍后处理。

  • 软中断(softirq)机制
  • bottom half(简称 bh)机制
  • Tasklet 机制