深入分析Linux内核源代码-中断基本知识(中)

每天十五分钟,熟读一个技术点,水滴石穿,一切只为渴望更优秀的你!
————零声学院

中断描述符表的初始化

通过上面的介绍,我们知道了 Intel 微处理器对中断和异常所做的工作。下面,我们从
操作系统的角度来对中断描述符表的初始化给予描述。

Linux 内核在系统的初始化阶段要进行大量的初始化工作,其与中断相关的工作有:初
始化可编程控制器 8259A;将中断向量 IDT 表的起始地址装入 IDTR 寄存器,并初始化表中的
每一项。这些操作的完成将在本节进行具体描述。

用户进程可以通过 INT 指令发出一个中断请求,其中断请求向量在 0~255 之间。为了
防止用户使用 INT 指令模拟非法的中断和异常,必须对 IDT 表进行谨慎的初始化。其措施之
一就是将中断门或陷阱门中的 DPL 域置为 0。如果用户进程确实发出了这样一个中断请求,
CPU 会检查出其 CPL(3)与 DPL(0)有冲突,因此产生一个“通用保护”异常。
但是,有时候必须让用户进程能够使用内核所提供的功能(比如系统调用),也就是说
从用户空间进入内核空间,这可以通过把中断门或陷阱门的 DPL 域置为 3 来达到。

1 外部中断向量的设置

前面我们已经提到,Linux 把向量 0~31 分配给异常和非屏蔽中断,而把 32~47 之间的
向量分配给可屏蔽中断,可屏蔽中断的向量是通过对中断控制器的编程来设置的。前面介绍
了 8259A 中断控制器,下面我们通过对其初始化的介绍,来了解如何设置中断向量。

8259A 通过两个端口来进行数据传送,对于单块的 8259A 或者是级连中的 8259A_1 来说,
这两个端口是 0x20 和 0x21。对于 8259A_2 来说,这两个端口是 0xA0 和 0xA1。8259A 有两种
编程方式,一是初始化方式,二是工作方式。在操作系统启动时,需要对 8959A 做一些初始
化工作,这就是初始化方式编程。

先简单介绍一下 8259A 内部的 4 个中断命令字(ICW)寄存器的功能,它们都是用来启
动初始化编程的。
• ICW1:初始化命令字。
• ICW2:中断向量寄存器,初始化时写入高 5 位作为中断向量的高五位,然后在中断
响应时由 8259 根据中断源(哪个管脚)自动填入形成完整的 8 位中断向量(或叫中断
类型号)。
• ICW3:8259 的级连命令字,用来区分主片和从片。
• ICW4:指定中断嵌套方式、数据缓冲选择、中断结束方式和 CPU 类型。
8259A 初始化的目的是写入有关命令字,8259A 内部有相应的寄存器来锁存这些命令字,
以控制 8259A 工作。有关的硬件知识笔者就不详细描述了,请读者查阅有关可编程中断控制
器 的 资 料 , 我 们 只 具 体 把 Linux 对 8259A 的 初 始 化 讲 解 一 下 , 代 码 在
/arch/i386/kernel/i8259.c 的函数 init_8259A()中:
outb(0xff, 0x21); /* 送数据到工作寄存器 OCW1(又称中断屏蔽字),
屏蔽所有外部中断, 因为此时系统尚未初始化完毕,
outb(0xff, 0xA1); 不能接收任何外部中断请求 /
outb_p(0x11, 0x20); /送 0x11 到 ICW1(通过端口 0x20),启动初始化编
程。0x11 表示外部中断请求信号为上升沿有效,系统中有多片 8295A 级连,还表示要向 ICW4
送数据
outb_p(0x20 + 0, 0x21); /
送 0x20 到 ICW2,写入高 5 位作为中断向量的
高 5 位,低 3 位根据中断源(管脚)填入中断号 0~7,因此把 IRQ0-7 映射到向量 0x20-0x27
/
outb_p(0x04, 0x21); /
送 0x04 到 ICW3,ICW3 是 8259 的级连命令字, 0x04
表示 8259A-1 是主片 /
outb_p(0x11, 0xA0); /
用 ICW1 初始化 8259A-2 /
outb_p(0x20 + 8, 0xA1); /
用 ICW2 把 8259A-2 的 IRQ0-7 映射到 0x28-0x2f
/
outb_p(0x02, 0xA1); /
送 0x04 到 ICW3。表示 8259A-2 是从片,并连接在 8259A_1 的 2
号管脚上
/
outb_p(0x01, 0xA1); /
把 0x01 送到 ICW4 */
最后一句有 4 方面含义:①中断嵌套方式为一般嵌套方式。当某个中断正在服务时,本
级中断及更低级的中断都被屏蔽,只有更高级的中断才能响应。注意,这对于多片 8259A 级
连的中断系统来说,当某从片中一个中断正在服务时,主片即将这个从片的所有中断屏蔽,
所以此时即使本片有比正在服务的中断级别更高的中断源发出请求,也不能得到响应,即不
能中断嵌套。②8259A 数据线和系统总线之间不加三态缓冲器。一般来说,只有级连片数很
多时才用到三态缓冲器;③中断结束方式为正常方式(非自动结束方式)。即在中断服务结
束时(中断服务程序末尾),要向 8259A 芯片发送结束命令字 EOI(送到工作寄存器 OCW2 中),
于是中断服务寄存器 ISR 中的当前服务位被清 0,EOI 命令字的格式有多种,在此不详述;④
CPU 类型为 x86 系列。

outb_p()函数就是把第一个操作数拷贝到由第二个操作数指定的 I/O 端口,并通过一个
空操作来产生一个暂停。

这里介绍了 8259A 初始化的主要工作。最后要说明的是:IBM PC 机的 BIOS 中固化有对
中断控制器的初始化程序段,在计算机加电时,这段程序自动执行,读者感兴趣可以查阅资
料看看它的源代码。典型的 PC 机将外部中断的中断向量分配为:08H0FH,70H77H。但是
Linux 对 8259A 作了重新初始化,修改了外部中断的中断向量的分配(20H~2FH),使中断向
量的分配更加合理。

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

当计算机运行在实模式时,IDT 被初始化并由 BIOS 使用。然而,一旦真正进入了 Linux
内核,IDT 就被移到内存的另一个区域,并进行进入实模式的初步初始化。

1.中断描述表寄存器 IDTR 的初始化
用汇编指令 LIDT 对中断向量表寄存器 IDTR 进行初始化,其代码在
arch/i386/boot/setup.S 中:
lidt idt_48 # load idt with 0,0

idt_48:
.word 0 # idt limit = 0
.word 0, 0 # idt base = 0L

2.把 IDT 表的起始地址装入 IDTR
用汇编指令 LIDT 装入 IDT 的大小和它的地址(在 arch/i386/kernel/head.S 中):
#define IDT_ENTRIES 256
.globl SYMBOL_NAME(idt)
lidt idt_descr

idt_descr:
.word IDT_ENTRIES*8-1 # idt contains 256 entries
SYMBOL_NAME(idt):
.long SYMBOL_NAME(idt_table)
其中 idt 为一个全局变量,内核对这个变量的引用就可以获得 IDT 表的地址。表的长度
为 256×8=2048 字节。

3.用 setup_idt()函数填充 idt_table 表中的 256 个表项
我们首先要看一下 idt_table 的定义(在 arch/i386/kernel/traps.c 中):
struct desc_struct idt_table[256] attribute((section(".data.idt"))) = { {0,
0}, };

desc_struct 结构定义为:
struct desc_struct {
unsigned long a,b }
对 idt_table 变量还定义了其属性(attribute),__section__是汇编中的“节”,
指定了 idt_table 的起始地址存放在数据节的 idt 变量中,如上面第“2.把 IDT 表的起始地
址装入 IDTR”所述。
在对 idt_table 表进行填充时,使用了一个空的中断处理程序 ignore_int()。因为现在
处于初始化阶段,还没有任何中断处理程序,因此用这个空的中断处理程序填充每个表项。
ignore_int()是一段汇编程序(在 head.S 中):
ignore_int:
cld #方向标志清 0,表示串指令自动增长它们的索引寄存器(esi 和
edi)
pushl %eax
pushl %ecx
pushl %edx
pushl %es
pushl %ds
movl $(__KERNEL_DS),%eax
movl %eax,%ds
movl %eax,%es
pushl $int_msg
call SYMBOL_NAME(printk)
popl %eax
popl %ds
popl %es
popl %edx
popl %ecx
popl %eax
iret
int_msg:
.asciz “Unknown interrupt\n”
ALIGN
该中断处理程序模仿一般的中断处理程序,执行如下操作:
• 在栈中保存一些寄存器的值;
• 调用 printk()函数打印“Unknown interrupt”系统信息;
• 从栈中恢复寄存器的内容;
• 执行 iret 指令以恢复被中断的程序。
实际上,ignore_int()处理程序应该从不执行。如果在控制台或日志文件中出现了
“Unknown interrupt”消息,说明要么是出现了一个硬件问题(一个 I/O 设备正在产生没有
预料到的中断),要么就是出现了一个内核问题(一个中断或异常未被恰当地处理)。
最后,我们来看 setup_idt()函数如何对 IDT 表进行填充:
/*

  • setup_idt
  • sets up a idt with 256 entries pointing to
  • ignore_int, interrupt gates. It doesn’t actually load
  • idt - that can be done only after paging has been enabled
  • and the kernel moved to PAGE_OFFSET. Interrupts
  • are enabled elsewhere, when we can be relatively
  • sure everything is ok.
    /
    setup_idt:
    lea ignore_int,%edx /计算 ignore_int 地址的偏移量,并将其装入%edx/
    movl $(__KERNEL_CS << 16),%eax /
    selector = 0x0010 = cs /
    movw %dx,%ax
    movw $0x8E00,%dx /
    interrupt gate - dpl=0, present /
    lea SYMBOL_NAME(idt_table),%edi
    mov $256,%ecx
    rp_sidt:
    movl %eax,(%edi)
    movl %edx,4(%edi)
    addl $8,%edi
    dec %ecx
    jne rp_sidt
    ret
    这段程序的理解要对照门描述符的格式。8 个字节的门描述符放在两个 32 位寄存器 eax
    和 edx 中,如图 3.4 所示,从 rp_sidt 开始的那段程序是循环填充 256 个表项。
    • 从栈中恢复寄存器的内容;
    • 执行 iret 指令以恢复被中断的程序。
    实际上,ignore_int()处理程序应该从不执行。如果在控制台或日志文件中出现了
    “Unknown interrupt”消息,说明要么是出现了一个硬件问题(一个 I/O 设备正在产生没有
    预料到的中断),要么就是出现了一个内核问题(一个中断或异常未被恰当地处理)。
    最后,我们来看 setup_idt()函数如何对 IDT 表进行填充:
    /
  • setup_idt
  • sets up a idt with 256 entries pointing to
  • ignore_int, interrupt gates. It doesn’t actually load
  • idt - that can be done only after paging has been enabled
  • and the kernel moved to PAGE_OFFSET. Interrupts
  • are enabled elsewhere, when we can be relatively
  • sure everything is ok.
    /
    setup_idt:
    lea ignore_int,%edx /计算 ignore_int 地址的偏移量,并将其装入%edx/
    movl $(__KERNEL_CS << 16),%eax /
    selector = 0x0010 = cs /
    movw %dx,%ax
    movw $0x8E00,%dx /
    interrupt gate - dpl=0, present */
    lea SYMBOL_NAME(idt_table),%edi
    mov $256,%ecx
    rp_sidt:
    movl %eax,(%edi)
    movl %edx,4(%edi)
    addl $8,%edi
    dec %ecx
    jne rp_sidt
    ret
    这段程序的理解要对照门描述符的格式。8 个字节的门描述符放在两个 32 位寄存器 eax
    和 edx 中,如图 3.4 所示,从 rp_sidt 开始的那段程序是循环填充 256 个表项。
    在这里插入图片描述

中断向量表的最终初始化

在对中断描述符表进行预初始化后, 内核将在启用分页功能后对 IDT 进行第二遍初始化,
也就是说,用实际的陷阱和中断处理程序替换这个空的处理程序。一旦这个过程完成,对于
每个异常,IDT 都由一个专门的陷阱门或系统门,而对每个外部中断,IDT 都包含专门的中断
门。
1.IDT 表项的设置
IDT 表项的设置是通过_set_gaet()函数实现的,这与 IDT 表的预初始化比较相似,但这
里使用的是嵌入式汇编,因此,理解起来比较困难。在此,我们给出函数源码(在 traps.c
中)及其解释:
#define _set_gate(gate_addr,type,dpl,addr) \
do { \
int __d0, __d1; \
asm volatile (“movw %%dx,%%ax\n\t” \
“movw %4,%%dx\n\t” \
“movl %%eax,%0\n\t” \
“movl %%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)
这是一个带参数的宏定义,其中,gate_addr 是门的地址,type 为门类型,dpl 为请求
特权级,addr 为中断处理程序的地址。对这段嵌入式汇编代码的说明如下:
• 输出部分有 4 个变量,分别与%1、%2、%3 及%4 相结合,其中,%0 与 gate_addr
结合,1%与(gate_aggr+1)结合,这两个变量都存放在内存;2%与局部变量__d0 结合,
存放在 eax 寄存器中;3%与__d1 结合,存放在 edx 寄存器中。
• 输入部分有 3 个变量。由于输出部分已定义了 0%~3%,因此,输入部分的第一个变
量为 4%,其值为“0x8000+(dpl<<13)+(type<<8”,而后面两个变量分别等价于输出部分
的%3(edx)和 2%(eax),其值分别为“addr”和“__KERNEL_CS << 16”
• 有了参数的这种对照关系,再参考前面的 set_idt()函数,就不难理解那 4 条 mov 语句
了。
下面我们来看如何调用_set_get()函数来给 IDT 插入门:
void set_intr_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,14,0,addr);
}
在第 n 个表项中插入一个中断门。这个门的段选择符设置成代码段的选择符
(__KERNEL_CS),DPL 域设置成 0,14 表示 D 标志位为 1 而类型码为 110,所以 set_intr_gate()
设置的是中断门,偏移域设置成中断处理程序的地址 addr。
static void __init set_trap_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,15,0,addr);
}
在第 n 个表项中插入一个陷阱门。这个门的段选择符设置成代码段的选择符,DPL 域设
置成 0,15 表示 D 标志位为 1 而类型码为 111,所以 set_trap_gate()设置的是陷阱门,偏移
域设置成异常处理程序的地址 addr。
static void __init set_system_gate(unsigned int n, void addr)
{
_set_gate(idt_table+n,15,3,addr);
}
在第 n 个表项中插入一个系统门。这个门的段选择符设置成代码段的选择符,DPL 域设
置成 3,15 表示 D 标志位为 1 而类型码为 111,所以 set_system_gate()设置的也是陷阱门,
但因为 DPL 为 3,因此,系统调用在用户空间可以通过“INT0X80”顺利穿过系统门,从而进
入内核空间。
2.对陷阱门和系统门的初始化
trap_init()函数就是设置中断描述符表开头的 19 个陷阱门,如前所说,这些中断向量
都是 CPU 保留用于异常处理的:
set_trap_gate(0,&divide_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);
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_intr_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);
set_system_gate(SYSCALL_VECTOR,&system_call);
在对陷阱门及系统门设置以后,我们来看一下中断门的设置。
3.中断门的设置
下面介绍的相关代码均在 arch/I386/kernel/i8259.c 文件中,其中中断门的设置是由
init_IRQ( )函数中的一段代码完成的:
for (i = 0; i< NR_IRQS; i++) {
int vector = FIRST_EXTERNAL_VECTOR + i;
if (vector != SYSCALL_VECTOR)
set_intr_gate(vector, interrupt[i]);
其含义比较明显:从 FIRST_EXTERNAL_VECTOR 开始,设置 NR_IRQS 个 IDT 表项。常数
FIRST_EXTERNAL_VECTOR 定义为 0x20,而 NR_IRQS 则为 224,即中断门的个数。注意,必须
跳过用于系统调用的向量 0x80,因为这在前面已经设置好了。
这里,中断处理程序的入口地址是一个数组 interrupt[],数组中的每个元素是指向中
断处理函数的指针。我们来看一下编码的作者如何巧妙地避开了繁琐的文字录入,而采用统
一的方式处理多个函数名。
#define IRQ(x,y) \
IRQ##x##y##_interrupt
#define IRQLIST_16(x) \
IRQ(x,0), IRQ(x,1), IRQ(x,2), IRQ(x,3), \
IRQ(x,4), IRQ(x,5), IRQ(x,6), IRQ(x,7), \
IRQ(x,8), IRQ(x,9), IRQ(x,a), IRQ(x,b), \
IRQ(x,c), IRQ(x,d), IRQ(x,e), IRQ(x,f)
void (*interrupt[NR_IRQS])(void) = IRQLIST_16(0x0)
其中,“##”的作用是把字符串连接在一起。经过 gcc 预处理,IRQLIST_16(0x0)被
替换为 IRQ0x00_interrupt,IRQ0x01_interrupt,IRQ0x02_interrupt…IRQ0x0f_interrupt。
到此为止,我们已经介绍了 15 个陷阱门、4 个系统门和 16 个中断门的设置。内核代码
中还有对其他中断门的设置,在此就不一一介绍。

每日分享15分钟技术摘要选读,关注一波,一起保持学习动力!
在这里插入图片描述

第一章 走进linux 1.1 GNU与Linux的成长 1.2 Linux的开发模式和运作机制 1.3走进Linux内核 1.4 分析Linux内核的意义 1.5 Linux内核结构 1.6 Linux内核源代码 1.7 Linux内核源代码分析工具 第二章 Linux运行的硬件基础 2.1 i386的寄存器 2.2 内存地址 2.3 段机制和描述符 2.4 分页机制 2.5 Linux的分页机制 2.6 Linux的汇编语言 第三章中断机制 3.1 中断基本知识 3.2中断描述符表的初始化 3.3异常处理 3.4 中断处理 3.5中断的后半部分处理机制 第四章 进程描述 4.1 进程和程序(Process and Program) 4.2 Linux的进程概述 4.3 task_struct结构描述 4.4 task_struct结构在内存的存放 4.5 进程组织的方式 4.6 内核线程 4.7 进程的权能 4.8 内核同步 第五章进程调度 5.1 Linux时间系统 5.2 时钟中断 5.3 Linux的调度程序-Schedule( ) 5.4 进程切换 第六章 Linux内存管理 6.1 Linux的内存管理概述 6.2 Linux内存管理的初始化 6.3 内存的分配和回收 6.4 地址映射机制 6.5 请页机制 6.6 交换机制 6.7 缓存和刷新机制 6.8 进程的创建和执行 第七章 进程间通信 7.1 管道 7.2 信号(signal) 7.3 System V 的IPC机制 第八章 虚拟文件系统 8.1 概述 8.2 VFS的数据结构 8.3 高速缓存 8.4 文件系统的注册、安装与拆卸 8.5 限额机制 8.6 具体文件系统举例 8.7 文件系统的系统调用 8 .8 Linux2.4文件系统的移植问题 第九章 Ext2文件系统 9.1 基本概念 9.2 Ext2的磁盘布局和数据结构 9.3 文件的访问权限和安全 9.4 链接文件 9.5 分配策略 第十章 模块机制 10.1 概述 10.2 实现机制 10.3 模块的装入和卸载 10.4 内核版本 10.5 编写内核模块 第十一章 设备驱动程序 11.1 概述 11.2 设备驱动基础 11.3 块设备驱动程序 11.4 字符设备驱动程序 第十二章 网络 12.1 概述 12.2 网络协议 12.3 套接字(socket) 12.4 套接字缓冲区(sk_buff) 12.5 网络设备接口 第十三章 启动系统 13.1 初始化流程 13.2 初始化的任务 13.3 Linux 的Boot Loarder 13.4 进入操作系统 13.5 main.c的初始化 13.6 建立init进程 附录: 1 Linux 2.4内核API 2.1 驱动程序的基本函数 2.2 双向循环链表的操作 2.3 基本C库函数 2.4 Linux内存管理Slab缓冲区 2.5 Linux的VFS 2.6 Linux的连网 2.7 网络设备支持 2.8 模块支持 2.9 硬件接口 2.10 块设备 2.11 USB 设备
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值