至此,我们已经理解了X86架构如何在硬件层面如何处理中断和异常,那么接下来,我们看看Linux内核管理这些中断和异常。
同所有的设备一样,我们在使能硬件之前,必须先初始化其相关的数据结构。而Linux使用中断描述符表IDT记录管理所有的中断和异常。那么,首先,Linux内核应该把IDT的起始地址写入idtr寄存器,然后初始化所有的表项。这一步在初始化系统时完成。
因为汇编指令int
允许用户进程发送任意编号的中断(0-255)。为此,IDT的初始化必须考虑阻止由用户进程int指令引发的非法中断和异常。可以通过将中断描述符表中的DPL域设为0来实现。如果用户进程试图发送非法中断信号,CPU控制单元比较CPL和DPL的值,发出常规保护
的异常。
但是,大部分时候,用户态进程必须能够发送可编程异常。那么把相应的中断或陷阱门描述符的DPL域设为3即可。比如系统调用。
让我们看看Linux如何实现这种策略。
中断、陷阱和系统门
在之前的文章中,我们已经介绍过,Intel提供了三种类型的中断描述符:任务,中断和陷阱门描述符。Linux的分类有些不同,它们如下所示:
- 中断门
和Intel的中断门相同。所有的用户进程不能访问(该门的DPL设为0)。所有Linux的中断处理程序都是通过中断门激活的,也就是说只能在内核态访问。 - 系统门
属于Intel的陷阱门,可以被用户态进程访问(该门的DPL设为3)。三个Linux异常处理程序对应的中断号分别是4、5和128,分别使用into
、bound
和int $0x80
三条汇编指令发出对应的中断信号。 - 系统中断门
属于Intel的中断门,用户态进程可以访问(该门的DPL域设为3)。中断号为3的异常处理程序通过系统中断门激活,可以使用在用户态使用int3指令实现。 - 陷阱门
属于Intel陷阱门,不能被用户态程序访问(该门的DPL设为0)。用来访问大部分的异常处理程序。 - 任务门
属于Intel任务门,用户态进程不能访问(该门的DPL设为0)。专门访问处理Double fault
异常的处理程序。
对应上面的5种分类,分别有相应的函数可以初始化IDT(这些函数与硬件架构息息相关),如下所示:
- set_intr_gate(n,addr)
插入中断门。该门内的端选择器设为内核态代码所在的段。Offset被设为addr,就是中断处理程序的地址。DPL域设为0。 - set_system_gate(n,addr)
插入系统门。其余描述与上面的函数相同。 - set_system_intr_gate(n,addr)
插入系统中断门。该门内的端选择器设为内核态代码所在的段。Offset被设为addr,就是异常处理程序的地址。DPL域设为3。 - set_trap_gate(n,addr)
插入陷阱门,DPL被设为3。其余与上面函数相同。 - set_task_gate(n,gdt)
插入任务门。段选择器设为要执行的函数所在的段。Offset设为0,而DPL设为3。
IDT第一次初始化
其实,IDT被初始化两次。第一次是在BIOS程序中,此时CPU还工作在实模式下。一旦Linux启动,IDT会被搬运到RAM的受保护区域并被第二次初始化,因为Linux不会使用任何BIOS程序。
IDT结构被存储在idt_table表中,包含256项。idt_descr变量存储IDT的大小和它的地址,在系统的初始化阶段,内核用来设置idtr寄存器,专用汇编指令是lidt。
内核初始化的时候,汇编函数setup_idt()用相同的中断门填充idt_table表的所有项,都指向ignore_int()中断处理函数:
setup_idt: lea ignore_int, %edx movl $(__KERNEL_CS << 16), %eax movw %dx, %ax /* = 0x0010 = cs */ movw $0x8e00, %dx /* 中断门,DPL=0 */ lea idt_table, %edi /* 加载idt表的地址到寄存器edi中 */ mov $256, %ecx rp_sidt: movl %eax, (%edi) /* 设置中断处理函数 */ movl %edx, 4(%edi) /* 设置段描述符 */ addl $8, %edi /* 跳转到IDT表的下一项 */ dec %ecx /* 自减 */ jne rp_sidt ret
中断处理函数ignore_int()
,也是一个汇编语言编写的函数,相当于一个null函数,它执行:
- 保存一些寄存器到堆栈中。
- 调用printk()函数打印
Unknown interrupt
系统消息`。 - 从堆栈中恢复寄存器的内容。
- 执行iret指令回到调用处。
正常情况下,此时的中断处理函数ignore_int()是不应该被执行的。如果在console或者log日志中出现Unknown interrupt
的消息,说明发生硬件错误或者内核错误。
完成这次IDT表的初始化之后,内核还会进行第二次初始化,用真正的trap或中断处理函数代替刚才的null函数。一旦这两步初始化都完成,IDT表就包含具体的中断、陷阱和系统门,用以控制每个中断请求。
对于IDT表的第二次初始化过程,我们将分别以异常和中断的视角分开阐述。请参考后面的文章。