INTERRUPT

中断是硬件和软件交互的一种机制,可以说整个操作系统,整个架构都是由中断来驱动的。中断的机制分为两种,中断和异常,中断通常为 多处理器下的中断机制_初始化 设备触发的异步事件,而异常是 多处理器下的中断机制_初始化_02 执行指令时发生的同步事件。本文主要来说明 多处理器下的中断机制_初始化 外设触发的中断,总的来说一个中断的起末会经历设备,中断控制器,CPU 三个阶段:设备产生中断信号,中断控制器翻译信号,CPU 来实际处理信号

本文用 多处理器下的中断机制_寄存器_04 的实例来讲解多处理器下的中断机制,从头至尾的来看一看,中断经历的三个过程。其中第一个阶段设备如何产生信号不讲,超过了操作系统的范围,也超过了我的能力范围。各种硬件外设有着自己的执行逻辑,有各种形式的中断触发机制,比如边沿触发,电平触发等等。总的来说就是向中断控制器发送一个中断信号,中断控制器再作翻译发送给 多处理器下的中断机制_初始化_02多处理器下的中断机制_初始化_02

中断控制器

说到中断控制器,是个什么东西?中断控制器可以看作是中断的代理,外设是很多的,如果没有一个中断代理,外设想要给 多处理器下的中断机制_初始化_02 发送中断信号来处理中断,那只能是外设连接在 多处理器下的中断机制_初始化_02 的管脚上,多处理器下的中断机制_初始化_02 的管脚是很宝贵的,不可能拿出那么多管脚去连接外设。所以就有了中断控制器这个中断代言人,所有的 多处理器下的中断机制_初始化

中断控制器有很多,前文讲过 PICPIC 只用于单处理器,对于如今的多核多处理器时代,PIC 无能为力,所以出现了更高级的中断控制器 APICAPIC(多处理器下的中断机制_初始化_11) 高级可编程中断控制器,APIC 分成两部分 LAPICIOAPIC,前者 LAPIC 位于 多处理器下的中断机制_操作系统_12 内部,每个 多处理器下的中断机制_操作系统_12 都有一个 LAPIC,后者 IOAPIC 与外设相连。外设发出的中断信号经过 IOAPIC 处理之后发送给 LAPIC,再由 LAPIC 决定是否交由 多处理器下的中断机制_初始化_02

多处理器下的中断机制_初始化_15

可以看出每个 多处理器下的中断机制_初始化_02 上有一个 LAPICIOAPIC 是系统芯片组一部分,各个中断消息通过总线发送接收。关于 APIC 的内容很多也很复杂,详细描述的可以参考 多处理器下的中断机制_#define_17 开发手册卷三,本文不探讨其中的细节,只在上层较为抽象的层面讲述,理清 APIC 模式下中断的过程。

计算机启动的时候要先对 APIC 进行初始化,后续才能正确使用,下面来看看 APIC 在一种较为简单的工作模式下的初始化过程:

IOAPIC

初始化 IOAPIC 就是设置 IOAPIC 的寄存器IOAPIC 寄存器一览:

多处理器下的中断机制_#define_18

所以有了以下定义:

#define REG_ID     0x00  // Register index: ID
#define REG_VER    0x01  // Register index: version
#define REG_TABLE  0x10  // Redirection table base  重定向表

但是这些寄存器是不能直接访问的,需要通过另外两个映射到内存的寄存器来读写上述的寄存器

内存映射的两个寄存器

多处理器下的中断机制_初始化_19

这两个寄存器是内存映射的,IOREGSEL,地址为 多处理器下的中断机制_#define_20IOWIN,地址为 多处理器下的中断机制_#define_21IOREGSEL 用来指定要读写的寄存器,然后从 IOWIN 中读写。也就是常说的 index/data 访问方式,或者说 多处理器下的中断机制_操作系统_22,用 index 端口指定寄存器,从 data 端口读写寄存器,data 端口就像是所有寄存器的窗口。

而所谓内存映射,就是把这些寄存器看作内存的一部分,读写内存,就是读写寄存器,可以用访问内存的指令比如 mov 来访问寄存器。还有一种是 IO端口映射,这种映射方式是将外设的 IO端口(外设的一些寄存器) 看成一个独立的地址空间,访问这片空间不能用访问内存的指令,而需要专门的 in/out 指令来访问

通过 IOREGSELIOWIN 既可以访问到 IOAPIC 所有的寄存器,所以结构体 多处理器下的中断机制_初始化_23

struct ioapic {
  uint reg;       //IOREGSEL
  uint pad[3];    //填充12字节
  uint data;      //IOWIN
};

填充 多处理器下的中断机制_寄存器_24 字节是因为 IOREGSEL多处理器下的中断机制_#define_20,长度为 4 字节,IOWIN多处理器下的中断机制_操作系统_26,两者中间差了 $1$2 字节,所以填充 多处理器下的中断机制_寄存器_24

通过 IOREGSEL 选定寄存器,然后从IOWIN中读写相应寄存器,因此也能明白下面两个读写函数:

static uint ioapicread(int reg)
{
  ioapic->reg = reg;    //选定寄存器reg
  return ioapic->data;  //从窗口寄存器中读出寄存器reg数据
}

static void ioapicwrite(int reg, uint data)
{
  ioapic->reg = reg;    //选定寄存器reg
  ioapic->data = data;  //向窗口寄存器写就相当于向寄存器reg写
}

这两个函数就是根据 多处理器下的中断机制_#define_28 来读写 IOAPIC 的寄存器。下面来看看 IOAPIC 寄存器分别有些什么意义,了解了之后自然就知道为什么要这样那样的初始化了。下面只说 多处理器下的中断机制_寄存器_04

IOAPIC 寄存器

ID Register

  • 索引为 0
  • 多处理器下的中断机制_寄存器_30:ID

Version Register

  • 索引为 1
  • 多处理器下的中断机制_#define_31
  • 多处理器下的中断机制_初始化_32

重定向表项

IOAPIC 有 24 个管脚,每个管脚都对应着一个 64 位的重定向表项(也相当于 64 位的寄存器),保存在 多处理器下的中断机制_寄存器_33,重定向表项的格式如下所示:

多处理器下的中断机制_#define_34

多处理器下的中断机制_寄存器_35

多处理器下的中断机制_#define_36

这是 多处理器下的中断机制_初始化_37 大佬在他的 多处理器下的中断机制_初始化_38

IOAPIC 初始化

#define IOAPIC  0xFEC00000   // Default physical address of IO APIC

void ioapicinit(void)
{
  int i, id, maxintr;

  ioapic = (volatile struct ioapic*)IOAPIC;      //IOREGSEL的地址
  maxintr = (ioapicread(REG_VER) >> 16) & 0xFF;  //读取version寄存器16-23位,获取最大的中断数
  id = ioapicread(REG_ID) >> 24;      //读取ID寄存器24-27 获取IOAPIC ID
  if(id != ioapicid)
    cprintf("ioapicinit: id isn't equal to ioapicid; not a MP\n");

  // Mark all interrupts edge-triggered, active high, disabled,
  // and not routed to any CPUs.  将所有的中断重定向表项设置为边沿,高有效,屏蔽状态
  for(i = 0; i <= maxintr; i++){   
    ioapicwrite(REG_TABLE+2*i, INT_DISABLED | (T_IRQ0 + i));  //设置低32位,每个表项64位,所以2*i,
    ioapicwrite(REG_TABLE+2*i+1, 0);   //设置高32位
  }
}

宏定义 多处理器下的中断机制_初始化_39 是个地址值,这个地址就是 IOREGSEL 寄存器在内存中映射的位置,通过 多处理器下的中断机制_#define_28 方式读取 ID,支持的中断数等信息。

多处理器下的中断机制_寄存器_41多处理器下的中断机制_初始化_42 中有记录,关于 多处理器下的中断机制_寄存器_43 我们在 @@@@@@@@@@@ 一文中提到过,简单来说,多处理器下的中断机制_寄存器_43 有各种表项,记录了多处理器下的一些配置信息,计算机启动的时候可以从中获取有用信息。多处理器下的计算机启动@@@@一文只说明了处理器类型的表项,有多少个处理器类型的表项表示有多少个处理器。IOAPIC 同样的道理,然后每个 多处理器下的中断机制_#define_45 类型的表项中有其 多处理器下的中断机制_操作系统_46。关于 多处理器下的中断机制_寄存器_43 咱们就点到为止,有兴趣的可以去公众号后台获取 多处理器下的中断机制_初始化_48

接着就是一个 多处理器下的中断机制_初始化_49

  • 多处理器下的中断机制_寄存器_50,这个表示中断向量号,一个中断向量号就表示一个中断。表明此重定向表项处理 多处理器下的中断机制_寄存器_50
  • 多处理器下的中断机制_寄存器_52,设置此位来屏蔽与该重定向表项相关的中断,也就是说当硬件外设向 IOAPIC 发送中断信号时,IOAPIC 直接屏蔽忽略。
  • 设置 多处理器下的中断机制_寄存器_53多处理器下的中断机制_#define_54
  • 设置 多处理器下的中断机制_初始化_55 为 0 表示 多处理器下的中断机制_操作系统_56,设置高 8 位的 多处理器下的中断机制_操作系统_57 为 0。多处理器下的中断机制_操作系统_58 模式下,多处理器下的中断机制_操作系统_59 字段就表示 多处理器下的中断机制_寄存器_60多处理器下的中断机制_寄存器_60 又唯一标识一个 多处理器下的中断机制_寄存器_62,所以 多处理器下的中断机制_操作系统_59 就表示此中断会路由到该 多处理器下的中断机制_寄存器_62,交由该 多处理器下的中断机制_寄存器_62

因此这里初始化将所有重定向表项设置为边沿触发,高电平有效,所有中断路由到 多处理器下的中断机制_操作系统_66,但又将所有中断屏蔽的状态。 多处理器下的中断机制_寄存器_04 的注释描述的是不会将中断路由到任何处理器,这里我认为是有误的,虽然屏蔽了所有中断,但是根据 多处理器下的中断机制_初始化_68 字段来看应该是路由到 多处理器下的中断机制_操作系统_66

另外为什么要加上一个 多处理器下的中断机制_操作系统_70 呢, 多处理器下的中断机制_操作系统_70 是个宏,值为 32,前 32 个中断向量号分配给了一些异常或者保留,后面的中断向量号 32~255 才是一些外部中断或者 INT n 指令可以使用的

上述 IOAPIC 初始化的时候直接将管脚对应的中断全都给屏蔽了,那总得有开启的时候吧,不然无法工作也就无意义了,“开启”函数如下所示:

void ioapicenable(int irq, int cpunum)
{
  // Mark interrupt edge-triggered, active high,
  // enabled, and routed to the given cpunum,
  // which happens to be that cpu's APIC ID.     调用此函数使能相应的中断
  ioapicwrite(REG_TABLE+2*irq, T_IRQ0 + irq);
  ioapicwrite(REG_TABLE+2*irq+1, cpunum << 24);  //左移24位是填写 destination field字段
}

多处理器下的中断机制_操作系统_72

多处理器下的中断机制_#define_73 为 CPU 的编号,多处理器下的中断机制_操作系统_74 文件中定义了关于 多处理器下的中断机制_初始化_02 的全局数组,存放着所有 多处理器下的中断机制_初始化_02 的信息。多处理器下的中断机制_寄存器_04 里面,这个数组的索引是就是 多处理器下的中断机制_#define_73 也是 多处理器下的中断机制_寄存器_79,可以来唯一标识一个 多处理器下的中断机制_初始化_02。初始化的时候 多处理器下的中断机制_操作系统_81 为 0,调用此函数没有改变该位,所以还是 0,为物理模式,所以将 多处理器下的中断机制_#define_73 写入 多处理器下的中断机制_初始化_68 字段表示将中断路由到该 多处理器下的中断机制_初始化_02

来做个简单测试,在磁盘相关代码文件 多处理器下的中断机制_#define_85 中函数 多处理器下的中断机制_操作系统_86 调用了 多处理器下的中断机制_操作系统_87

ioapicenable(IRQ_IDE, ncpu - 1);     //让这个CPU来处理硬盘中断

根据上述讲的,这说明使用最后一个 多处理器下的中断机制_初始化_02 来处理磁盘中断,下面我们来验证,验证方式很简单,在中断处理程序当中打印 多处理器下的中断机制_初始化_02

首先在 多处理器下的中断机制_寄存器_90 中将 多处理器下的中断机制_初始化_02

ifndef CPUS
CPUS := 4
endif

接着在 多处理器下的中断机制_操作系统_92 文件中添加 多处理器下的中断机制_寄存器_93

case T_IRQ0 + IRQ_IDE:    //如果是磁盘中断
    ideintr();            //调用磁盘中断程序
    lapiceoi();           //处理完写EOI表中断完成
    cprintf("ide %d\n", cpuid());  //打印CPU编号
    break;

这个函数我们后面会讲到,这里提前看一看,有注释应该还是很好理解的,来看看结果:

多处理器下的中断机制_初始化_94

多处理器下的中断机制_初始化_02 的数量为 4,处理磁盘中断的 多处理器下的中断机制_初始化_02 编号为 3,符合预期,多处理器下的中断机制_初始化_39 的初始化就说到这里,下面来看 多处理器下的中断机制_寄存器_98

LAPIC

LAPIC 要比 IOAPIC 复杂的多,放张总图:

多处理器下的中断机制_初始化_99

多处理器下的中断机制_寄存器_04 不会涉及这么复杂,其主要功能是接收 IOAPIC 发来的中断消息然后交由 多处理器下的中断机制_操作系统_12 处理,再者就是自身也能作为中断源产生中断发送给自身或其他 多处理器下的中断机制_初始化_02。同样的初始化 LAPIC 就是设置相关寄存器,但是 LAPIC 的寄存器实在太多了,本文只是说明 xv6 涉及到的寄存器,其他的可以参考前文@@@@@@@@@@@,或者文末的链接。

LAPIC 的寄存器在内存中都有映射,起始地址一般默认为 多处理器下的中断机制_初始化_103,但这个地址不是自己设置使用的,起始地址在 多处理器下的中断机制_操作系统_104 中可以获取,详见文末链接@@@@@@@@@@,所以可以如下定义和获取 多处理器下的中断机制_#define_105

/*lapic.c*/
volatile uint *lapic;  // Initialized in mp.c

/*mp.c*/
lapic = (uint*)conf->lapicaddr;  //conf就是MP Table Header,其中记录着LAPIC地址信息

多处理器下的中断机制_#define_105 也可以看作是 多处理器下的中断机制_操作系统_107 型的数组,一个元素 4 字节,所以计算各个寄存器的索引的时候要在偏移量的基础上除以 4。举个例子,ID 寄存器相对 多处理器下的中断机制_#define_105 基地址偏移量为 多处理器下的中断机制_初始化_109,那么 ID 寄存器在 多处理器下的中断机制_#define_105

因为是 LAPIC 的寄存器是内存映射,所以设置寄存器就是直接读写相应内存,因此读写寄存器的函实现是很简单的:

static void lapicw(int index, int value)   //向下标为index的寄存器写value
{
  lapic[index] = value;
  lapic[ID];  // wait for write to finish, by reading  
}

这里看着是写内存,但是实际上这部分地址已经分配给了 LAPIC,对硬件的写操作一般要停下等一会儿待写操作完成,可以去看看磁盘键盘等硬件初始配置的时候都有类似的等待操作,这里直接采用读数据的方式来等待写操作完成。

LAPIC 初始化

有了读写 LAPIC 寄存器的函数,接着就来看看 LAPIC 如何初始化的,初始化函数为 多处理器下的中断机制_初始化_111,我们分开来看:

lapicw(SVR, ENABLE | (T_IRQ0 + IRQ_SPURIOUS));

#define SVR     (0x00F0/4)   // Spurious Interrupt Vector
  #define ENABLE     0x00000100   // Unit Enable

SVR 伪中断寄存器,多处理器下的中断机制_初始化_02 每响应一次 多处理器下的中断机制_#define_113(可屏蔽中断),就会连续执行两个 多处理器下的中断机制_#define_114 周期。在 多处理器下的中断机制_初始化_48 中有描述,当一个中断在第一个 多处理器下的中断机制_#define_114 周期后,第二个 多处理器下的中断机制_#define_114 周期前变为无效,则为伪中断,也就是说伪中断就是中断引脚没有维持足够的有效电平而产生的。这主要涉及到电气方面的东西,我们了解就好。

多处理器下的中断机制_初始化_118 中的字段还有其他作用,多处理器下的中断机制_#define_119 置 1 表示使能 LAPICLAPIC 需要在使能状态下工作。

lapicw(TDCR, X1);   //设置分频系数
lapicw(TIMER, PERIODIC | (T_IRQ0 + IRQ_TIMER));  //设置Timer的模式和中断向量号
lapicw(TICR, 10000000);  //设置周期性计数的数字

#define TICR    (0x0380/4)   // Timer Initial Count
#define TDCR    (0x03E0/4)   // Timer Divide Configuration

#define TIMER   (0x0320/4)   // Local Vector Table 0 (TIMER)
  #define X1         0x0000000B   // divide counts by 1
  #define PERIODIC   0x00020000   // Periodic

LAPIC 自带可编程定时器,可以用这个定时器来作为时钟,触发时钟中断。这需要 多处理器下的中断机制_初始化_120多处理器下的中断机制_寄存器_121、以及 多处理器下的中断机制_寄存器_122 配合使用,其实还有一个 多处理器下的中断机制_寄存器_123多处理器下的中断机制_寄存器_04

多处理器下的中断机制_#define_125

这几个寄存器表示 多处理器下的中断机制_寄存器_126 本地中断,LAPIC 除了可以接收 IOAPIC 发来的中断之外,自己也可以产生中断,就是上述列出来的这几种。

从上图可以看出 多处理器下的中断机制_操作系统_127 寄存器 多处理器下的中断机制_操作系统_128 设置 多处理器下的中断机制_寄存器_129多处理器下的中断机制_寄存器_04 设置为 多处理器下的中断机制_寄存器_131多处理器下的中断机制_#define_132

这个数设置在 多处理器下的中断机制_#define_133 寄存器,多处理器下的中断机制_寄存器_04 设置的值是 多处理器下的中断机制_初始化_135

递减得有个频率,这个频率是系统的总线频率再分频,分频系数设置在 多处理器下的中断机制_初始化_136 寄存器,多处理器下的中断机制_寄存器_04

另外 多处理器下的中断机制_操作系统_138 是时钟中断的向量号,设置在 多处理器下的中断机制_操作系统_127

关于时钟中断的设置就是这么多,每个 多处理器下的中断机制_初始化_02 都有 多处理器下的中断机制_寄存器_98,所以每个 多处理器下的中断机制_初始化_02 上都会发生时钟中断,不像其他中断,指定了一个 多处理器下的中断机制_初始化_02

回到 LAPIC 的初始化上面来:

// Disable logical interrupt lines.
lapicw(LINT0, MASKED);
lapicw(LINT1, MASKED);

多处理器下的中断机制_初始化_144连接到了 多处理器下的中断机制_操作系统_145多处理器下的中断机制_#define_146,但实际上只连接到了 多处理器下的中断机制_初始化_147(最先启动的 多处理器下的中断机制_初始化_02),只有 多处理器下的中断机制_初始化_147 能接收这两种中断。一般对于 多处理器下的中断机制_初始化_147 如果有 多处理器下的中断机制_寄存器_151 模式(兼容多处理器下的中断机制_#define_152) 多处理器下的中断机制_#define_153 设置为 多处理器下的中断机制_操作系统_154 模式,多处理器下的中断机制_#define_155 设置为 多处理器下的中断机制_#define_146 模式。如果是 多处理器下的中断机制_操作系统_157 直接设置屏蔽位将两种中断屏蔽掉。多处理器下的中断机制_寄存器_04 简化了处理,只使用 APIC 模式,所有的 LAPIC 都将两种中断给屏蔽掉了。

if(((lapic[VER]>>16) & 0xFF) >= 4)
	lapicw(PCINT, MASKED);

// Map error interrupt to IRQ_ERROR.
lapicw(ERROR, T_IRQ0 + IRQ_ERROR);

// Clear error status register (requires back-to-back writes).
lapicw(ESR, 0);
lapicw(ESR, 0);

#define VER     (0x0030/4)   // Version
#define ERROR   (0x0370/4)   // Local Vector Table 3 (ERROR)
#define PCINT   (0x0340/4)   // Performance Counter LVT
#define ESR     (0x0280/4)   // Error Status

Version Register多处理器下的中断机制_#define_159多处理器下的中断机制_初始化_160

ERROR Register,设置这个寄存器来映射 多处理器下的中断机制_操作系统_161 中断,当 $APIC $检测到内部错误的时候就会触发这个中断,中断向量号是 多处理器下的中断机制_初始化_162

多处理器下的中断机制_寄存器_163

lapicw(EOI, 0);
#define EOI     (0x00B0/4)   // EOI

EOI(多处理器下的中断机制_寄存器_164),中断处理完成之后要写 EOI 寄存器来显示表示中断处理已经完成。重置初始化后的值应为 0.

lapicw(ICRHI, 0);
lapicw(ICRLO, BCAST | INIT | LEVEL);
while(lapic[ICRLO] & DELIVS)
	;

#define ICRHI   (0x0310/4)   // Interrupt Command [63:32]
#define TIMER   (0x0320/4)   // Local Vector Table 0 (TIMER)
//ICR寄存器的各字段取值意义
  #define INIT       0x00000500   // INIT/RESET
  #define STARTUP    0x00000600   // Startup IPI
  #define DELIVS     0x00001000   // Delivery status
  #define ASSERT     0x00004000   // Assert interrupt (vs deassert)
  #define DEASSERT   0x00000000
  #define LEVEL      0x00008000   // Level triggered
  #define BCAST      0x00080000   // Send to all APICs, including self.
  #define BUSY       0x00001000
  #define FIXED      0x00000000

ICR(多处理器下的中断机制_操作系统_165)中断指令寄存器,当一个 多处理器下的中断机制_操作系统_12 想把中断发送给另一个 多处理器下的中断机制_操作系统_12 时,就在 ICR 中填写相应的中断向量和目标 LAPIC 标识,然后通过总线向目标 LAPIC 发送消息。因为同样是向另一个 LAPIC 发送中断消息,所以ICR 寄存器的字段和 IOAPIC 重定向表项较为相似,都有 多处理器下的中断机制_初始化_168

多处理器下的中断机制_寄存器_169

多处理器下的中断机制_操作系统_170. 结合 多处理器下的中断机制_#define_17 手册,作用为将所有 多处理器下的中断机制_初始化_02APIC多处理器下的中断机制_#define_173 设置为初始值 多处理器下的中断机制_初始化_174

关于 Arb,引用 多处理器下的中断机制_寄存器_175

Arb,Arbitration Register,仲裁寄存器。该寄存器用 4 个 bit 表示 0~15 共 16 个优先级(15 为最高优先级),用于确定 LAPIC 竞争 APIC BUS 的优先级。系统 RESET 后,各 LAPIC 的 Arb 被初始化为其 LAPIC ID。总线竞争时,Arb 值最大 的 LAPIC 赢得总线,同时将自身的 Arb 清零,并将其它 LAPIC 的 Arb 加一。由 此可见,Arb 仲裁是一个轮询机制。Level 触发的 INIT IPI 可以将各 LAPIC 的 Arb 同步回当前的 LAPIC ID。

// Enable interrupts on the APIC (but not on the processor).
lapicw(TPR, 0);
#define TPR     (0x0080/4)   // Task Priority

任务优先级寄存器,确定当前 CPU 能够处理什么优先级别的中断,CPU 只处理比 TPR 中级别更高的中断。比它低的中断暂时屏蔽掉,也就是在 IRR 中继续等到

上述就是 多处理器下的中断机制_寄存器_04 里面对 LAPIC 的一种简单的初始化方式,其实也不简单,涉及了挺多东西。接下来应该是 CPU 来处理中断的部分,在这之前先来看看 多处理器下的中断机制_操作系统_177

int lapicid(void)   //返回 CPU/LAPIC ID
{
  if (!lapic)
    return 0;
  return lapic[ID] >> 24;
}

这个函数用来返回 多处理器下的中断机制_寄存器_79ID 寄存器 多处理器下的中断机制_寄存器_179 位后表示 多处理器下的中断机制_寄存器_79因为 多处理器下的中断机制_初始化_02LAPIC 一一对应,所以这也相当于返回 多处理器下的中断机制_初始化_182,同样也是 多处理器下的中断机制_初始化_02 数组中的索引。而前面在 多处理器下的中断机制_初始化_39 一节中出现的 多处理器下的中断机制_初始化_185

void lapiceoi(void)
{
  if(lapic)
    lapicw(EOI, 0);
}

EOI 表中断完成,这个函数在中断服务程序中会经常用到用到,下面再来看看 LAPIC 中两个比较重要的寄存器:

  • IRR 中断请求寄存器,256 位,每位代表着一个中断。当某个中断消息发来时,如果该中断没有被屏蔽,则将 IRR 对应的 bit 置 1,表示收到了该中断请求但 CPU 还未处理
  • ISR 服务中寄存器 ,256 位,每位代表着一个中断。当 IRR 中某个中断请求发送给 CPU 时,ISR 对应的 bit 上便置 1,表示 CPU 正在处理该中断

上述就是 APIC 的初始化和一些重要函数的讲解,有了这些了解之后,来总体的看一看 APIC 部分的中断过程:

  1. 外设触发中断,发送中断信号给 IOAPIC
  2. IOAPIC 根据 多处理器下的中断机制_#define_186 表将中断信号翻译成中断消息,然后发送给 多处理器下的中断机制_操作系统_57 字段列出的 多处理器下的中断机制_操作系统_188
  3. LAPIC 根据消息中的 多处理器下的中断机制_初始化_189多处理器下的中断机制_操作系统_57,自身的寄存器 ID 来判断自己是否接收该中断消息,设置 IRR 相应的 多处理器下的中断机制_#define_191
  4. 多处理器下的中断机制_操作系统_12 在可以处理下一个中断时,从 IRR 中挑选优先级最大的中断,相应位置 0,ISR 相应位置 1,然后送 多处理器下的中断机制_操作系统_12
  5. 多处理器下的中断机制_操作系统_12
  6. 中断处理完成后写 EOI 表示中断处理已经完成,写 EOI 导致 ISR 相应位置 0,对于 多处理器下的中断机制_寄存器_195 触发的中断,还会向所有的 IOAPIC 发送 EOI 消息,通知中断处理已经完成。

上述的过程只是一个很简单的大致过程,没有涉及到不可屏蔽中断,一些特殊的中断,中断嵌套等等,只是来简单认识一下 APIC 在中断时是如何工作的,接下来重点看看 多处理器下的中断机制_初始化_02

CPU 部分

上述就是 多处理器下的中断机制_初始化_197 的初始化部分,被 多处理器下的中断机制_#define_198 中的 多处理器下的中断机制_#define_199 调用,是计算机启动时环境初始化的一部分。下面来看 多处理器下的中断机制_初始化_02 处理中断的部分。先来复习一下 多处理器下的中断机制_初始化_02

  • 多处理器下的中断机制_操作系统_12
  • 根据中断向量号去 多处理器下的中断机制_#define_203 索引门描述符,根据门描述符中的段选择子去 多处理器下的中断机制_#define_204
  • 这期间 多处理器下的中断机制_操作系统_12 会进行特权级检查,如果特权级有变化,如用户态进入内核态,压入原栈 多处理器下的中断机制_#define_206多处理器下的中断机制_初始化_207 到内核栈,如果没有变化则不用压入。之后压入 多处理器下的中断机制_#define_208多处理器下的中断机制_操作系统_209多处理器下的中断机制_初始化_210,该中断有错误码的话还需要压入错误码。
  • 根据段描述符中的段基址和中断描述符中的偏移量取得中断服务程序的地址
  • 执行中断服务程序,这期间会压入寄存器等资源,保存上下文
  • 执行完成后恢复上下文,写 EOI 表中断完成

所以在中断正式处理之前就压入一些寄存器,栈中情况如下:

多处理器下的中断机制_初始化_211

接下来便就是去 IDTGDT 中索引门描述符和段描述符,寻找中断服务程序,本文主要讲述中断,所以只来看看 IDTGDT 相关内容我在 @@@@@@@@@有所讲述,可以参考参考。

构建 IDT

IDT多处理器下的中断机制_#define_212,中断描述符表,我们得先有这么一个表,多处理器下的中断机制_初始化_02 才能使用中断控制器发送来的向量号去 多处理器下的中断机制_初始化_214

所以得构建一个 IDT,构建 IDT 就是构建一个个中断描述符,一般称作门描述符,IDT 里面可以存放几种门描述符,如调用门描述符,陷阱门描述符,任务门描述符,中断门描述符。大多数中断都使用中断门描述符,来看看中断门描述符的格式:

多处理器下的中断机制_寄存器_215

其实上述也可以作为陷阱门描述符,两者几乎一模一样,只有 多处理器下的中断机制_初始化_216

struct gatedesc {
  uint off_15_0 : 16;   // low 16 bits of offset in segment
  uint cs : 16;         // code segment selector
  uint args : 5;        // # args, 0 for interrupt/trap gates
  uint rsv1 : 3;        // reserved(should be zero I guess)
  uint type : 4;        // type(STS_{IG32,TG32})
  uint s : 1;           // must be 0 (system)
  uint dpl : 2;         // descriptor(meaning new) privilege level
  uint p : 1;           // Present
  uint off_31_16 : 16;  // high bits of offset in segment
};
  • 多处理器下的中断机制_寄存器_217:中断服务程序在目标代码段中的偏移量 0~15 位
  • 多处理器下的中断机制_#define_218:中断服务程序所在段的段选择子
  • 多处理器下的中断机制_#define_219:中断门的 多处理器下的中断机制_初始化_220
  • 多处理器下的中断机制_#define_221:S 字段为 0 表示系统段,各种门结构都是系统段,意为这是硬件需要的结构,反之软件需要的则是非系统段,包括平常所说的数据段和代码段,这不是硬件必须的,为非系统段。
  • 多处理器下的中断机制_#define_222多处理器下的中断机制_#define_223,描述符特权级,进入中断时会用来特权级检查。
  • 多处理器下的中断机制_寄存器_224多处理器下的中断机制_寄存器_225
  • 多处理器下的中断机制_寄存器_226:中断服务程序在内核代码段中的偏移量 16~31 位

从上面部分字段代表的意义可以看出,构建中断门描述符还需要中断服务程序的地址信息,所以咱们首先还得准备好各个中断服务程序,取得它们的地址信息。在 多处理器下的中断机制_寄存器_04

IDT 中支持 256 个表项,支持 256 个中断,所以要有 256 个入口程序,入口程序所做的工作是类似的,所以 多处理器下的中断机制_寄存器_04 使用了 多处理器下的中断机制_初始化_229 脚本来批量产生代码。脚本文件是 多处理器下的中断机制_寄存器_230,生成的代码如下所示:

.globl alltraps  

.globl vector0   #向量号为0的入口程序
vector0:
  pushl $0
  pushl $0
  jmp alltraps
#############################
.globl vector8
vector8:
  pushl $8
  jmp alltraps
##############################
.globl vectors  #入口程序数组
vectors:
  .long vector0
  .long vector1
  .long vector2

这是一段汇编代码,所有的中断入口程序都做了相同的三件事或两件事:

  • 压入 0,其实这个位置是错误码的位置,有些中断会产生错误码压入栈中,所以为了统一,没有错误码的中断也压入一个东西:0
  • 压入自己的中断向量号
  • 跳到 多处理器下的中断机制_寄存器_231

第一项 压入 0 只有没有错误码产生的中断/异常才会执行,而错误码主要部分就是选择子,一般不使用。但这是 多处理器下的中断机制_#define_232 架构特性,有错误码的时候会自动压入,所以在 多处理器下的中断机制_初始化_229

if(!($i == 8 || ($i >= 10 && $i <= 14) || $i == 17)){
        print "  pushl \$0\n";

表示向量号为 多处理器下的中断机制_操作系统_234

这 256 个中断入口程序地址写入一个大数组 多处理器下的中断机制_操作系统_235,所以中断门描述符要的地址信息不就来了,因此 IDT 的构建如下:

struct gatedesc idt[256];
extern uint vectors[];  // in vectors.S: array of 256 entry pointers

void tvinit(void)   //根据外部的vectors数组构建中断门描述符
{
  int i;

  for(i = 0; i < 256; i++)
    SETGATE(idt[i], 0, SEG_KCODE<<3, vectors[i], 0);
  SETGATE(idt[T_SYSCALL], 1, SEG_KCODE<<3, vectors[T_SYSCALL], DPL_USER);

  initlock(&tickslock, "time");
}

#define SETGATE(gate, istrap, sel, off, d)                \  //门描述符,是否是陷阱,选择子,偏移量,DPL
{                                                         \
  (gate).off_15_0 = (uint)(off) & 0xffff;                \
  (gate).cs = (sel);                                      \
  (gate).args = 0;                                        \
  (gate).rsv1 = 0;                                        \
  (gate).type = (istrap) ? STS_TG32 : STS_IG32;           \
  (gate).s = 0;                                           \
  (gate).dpl = (d);                                       \
  (gate).p = 1;                                           \
  (gate).off_31_16 = (uint)(off) >> 16;                  \
}

多处理器下的中断机制_操作系统_236

中断服务程序属于内核程序,段选择子为内核代码段,多处理器下的中断机制_寄存器_237 设置为 0,但是系统调用需要特殊处理,多处理器下的中断机制_寄存器_237 字段必须设置为 3。为什么这么设置,原由与特权级检查有关:当前代码段寄存器的 多处理器下的中断机制_操作系统_239多处理器下的中断机制_操作系统_240,也就是 多处理器下的中断机制_#define_241。是不是很绕,没办法,事实就是这样。

作何特权级检查呢?多处理器下的中断机制_寄存器_242 需要大于等于门描述符中选择子的 多处理器下的中断机制_寄存器_237,而对于系统调用 多处理器下的中断机制_寄存器_242 还需要小于等于门描述符的 多处理器下的中断机制_寄存器_237,不然就会触发一般保护性错异常。系统调用特权级肯定是要转移的,也就是从用户态到内核态,用户态下 多处理器下的中断机制_操作系统_246,门描述符 多处理器下的中断机制_寄存器_237 如果还为 0 的话,那特权级检查不能通过,是要触发异常的,所以对于系统调用 多处理器下的中断机制_寄存器_237

这说的有点远了,特权级检查是个很复杂的东西,上面还没有加入 多处理器下的中断机制_#define_249 的检查呢。这里只是稍作了解就好,后面有机会写一篇捋一捋特权级检查,下面回到 IDT 本身上来,IDT 构建好了之后需要将其地址加载到 IDTR 寄存器,如此 多处理器下的中断机制_初始化_02 才晓得去哪儿找 IDT

void idtinit(void)
{
  lidt(idt, sizeof(idt));      //加载IDT地址到IDTR
}

static inline void lidt(struct gatedesc *p, int size)   //构造idtr需要的48位数据,然后重新加载到idtr寄存器
{
  volatile ushort pd[3];

  pd[0] = size-1;
  pd[1] = (uint)p;
  pd[2] = (uint)p >> 16;

  asm volatile("lidt (%0)" : : "r" (pd));
}

IDTR 寄存器有 48 位

  • 多处理器下的中断机制_寄存器_217 表示 IDT 的界限,也就是这个表有好大,表示的最大范围为 多处理器下的中断机制_初始化_252,也就是 多处理器下的中断机制_#define_253,一个门描述符 8 字节,所以描述符最多 多处理器下的中断机制_操作系统_254,但是处理器只支持 256 个中断,也就是 256 个门描述符。
  • 多处理器下的中断机制_操作系统_255 表示 IDT 基地址

上述代码中数组 多处理器下的中断机制_寄存器_256 就是这 48 位数据,先构造这个数据,然后使用内联汇编,指令 多处理器下的中断机制_寄存器_257 将其加载到 IDTR 寄存器,关于内联汇编不多说,可以参考我前面的文章:@@@@@@@@

中断服务程序

多处理器下的中断机制_初始化_214

中断入口程序

中断入口程序主要是保存中断上下文,多处理器下的中断机制_#define_259 数组中记录的入口程序只能算是一部分,这一部分做了三件事:压入 0/错误码,压入向量号,跳到 多处理器下的中断机制_寄存器_231

所以现阶段栈中情况如下:

多处理器下的中断机制_初始化_261

紧接着程序跳到了 多处理器下的中断机制_操作系统_262,来看看这是个什么玩意儿:

.globl alltraps
alltraps:
  # Build trap frame.  构建中断栈帧
  pushl %ds
  pushl %es
  pushl %fs
  pushl %gs
  pushal
  
  # Set up data segments.  设置数据段为内核数据段
  movw $(SEG_KDATA<<3), %ax
  movw %ax, %ds
  movw %ax, %es

  # Call trap(tf), where tf=%esp  调用trap.c()
  pushl %esp
  call trap
  addl $4, %esp

可以看出 多处理器下的中断机制_操作系统_262

  • 建立栈帧,保存上下文
  • 设置数据段寄存器为内核数据段
  • 传参调用 多处理器下的中断机制_初始化_264

1、建立栈帧,保存上下文

建立栈帧保存上下文就是将各类寄存器资源压栈保存在栈中,多处理器下的中断机制_寄存器_04 直接暴力地将所有的寄存器直接压进去。先是压入各段寄存器,再 多处理器下的中断机制_操作系统_266 压入所有的通用寄存器,顺序为 多处理器下的中断机制_寄存器_267

所以现下栈中的情况为:

多处理器下的中断机制_操作系统_268

所以如此定义栈帧:

struct trapframe {
  // registers as pushed by pusha
  uint edi;
  uint esi;
  uint ebp;
  uint oesp;      // useless & ignored esp值无用忽略
  uint ebx;
  uint edx;
  uint ecx;
  uint eax;

  // rest of trap frame
  ushort gs;
  ushort padding1;
  ushort fs;
  ushort padding2;
  ushort es;
  ushort padding3;
  ushort ds;
  ushort padding4;
  uint trapno;       //向量号

  // below here defined by x86 hardware
  uint err;
  uint eip;
  ushort cs;
  ushort padding5;
  uint eflags;

  // below here only when crossing rings, such as from user to kernel
  uint esp;
  ushort ss;
  ushort padding6;
};

可以看出定义的中断栈帧结构体与前面的操作是一一对应的,说明两点:

  • 段寄存器只有 16 位 2 字节,压栈段寄存器时用的 多处理器下的中断机制_寄存器_269,压入了一个双字 4 字节,所以需要 多处理器下的中断机制_操作系统_270 类型的来填充 2 字节。也可以直接将段寄存器定义为 多处理器下的中断机制_初始化_271
  • 多处理器下的中断机制_#define_272 时压入通用寄存器,这些寄存器加上进入中断时 多处理器下的中断机制_操作系统_12 自动压入的值就是中断发生前一刻进程的上下文。这里 多处理器下的中断机制_#define_272 压入的 多处理器下的中断机制_初始化_207

2、设置数据段寄存器为内核数据段

在根据向量号索引门描述符的时候已经进行了特权级检查,将门描述符中的段选择子——内核代码段选择子加载到了 CS,这里就只需要设置数据段寄存器为内核数据段。附加段,附加的数据段,通常与数据段进行一样的设置,在串操作指令中,将附加段作为目的操作数的存放区域,详见前文内联汇编

3、调用中断处理程序

多处理器下的中断机制_#define_276 之后 多处理器下的中断机制_操作系统_277,标准的函数调用方式,先 多处理器下的中断机制_#define_276 参数,再 多处理器下的中断机制_操作系统_277 调用函数。多处理器下的中断机制_操作系统_280,此时的 esp 是中断栈帧的栈顶元素的地址,也就是说传递的参数是中断栈帧的首地址。随后 多处理器下的中断机制_寄存器_281 调用中断处理程序,压入返回地址(多处理器下的中断机制_操作系统_277 指令后面那条指令的地址,也就是 $addl\ $4, %esp$ 语句的地址),之后跳转到 多处理器下的中断机制_初始化_283

此时栈中情况:

多处理器下的中断机制_初始化_284

中断处理程序

上述操作已经将中断处理程序 多处理器下的中断机制_操作系统_285 需要的参数中断栈帧 多处理器下的中断机制_#define_286 的地址压入栈中。其实 多处理器下的中断机制_初始化_283 也像是中断服务程序的入口,整个程序就是由许多条件语句组成,根据 多处理器下的中断机制_#define_286

if(tf->trapno == T_SYSCALL){    //系统调用
    if(myproc()->killed)  //如果当前进程已经被杀死
      exit();             //退出
    myproc()->tf = tf;    //当前进程的栈帧
    syscall();            //系统调用入口
    if(myproc()->killed)  //再次确认进程状态
      exit();
    return;        //返回
  }

如果向量号表示这是一个系统调用,则进行系统调用,这部分放在后面文章讲解。

switch(tf->trapno){
  case T_IRQ0 + IRQ_TIMER:     //时钟中断
    if(cpuid() == 0){
      acquire(&tickslock);
      ticks++;
      wakeup(&ticks);
      release(&tickslock);
    }
    lapiceoi();
    break;
  case T_IRQ0 + IRQ_IDE:  //磁盘中断
    ideintr();
    lapiceoi();
    break;
/*****************************/

如果是时钟中断,并且是 多处理器下的中断机制_操作系统_66 发出的时钟中断,就将滴答数 多处理器下的中断机制_#define_290 加 1。每个 多处理器下的中断机制_初始化_02 都有自己的 LAPIC,也就都有自己的 APIC Timer,都能够触发时钟中断。多处理器下的中断机制_#define_290 记录系统从开始到现在的滴答数,作为系统的时间,发生一次时钟中断其数值就加 1,但是能修改 多处理器下的中断机制_#define_290 的应该只能有一个 CPU,不然如果所有的 多处理器下的中断机制_初始化_02 都能修改 多处理器下的中断机制_#define_290 的值的话,那岂不是乱套了?所以这里就选择 多处理器下的中断机制_操作系统_66 也是 多处理器下的中断机制_初始化_147 来修改 多处理器下的中断机制_#define_290 的值。处理完之后写 EOI 表时钟中断完成

如果是磁盘发出的中断,就调用磁盘中断处理程序,也是磁盘驱动程序的主体,详见前文带你了解磁盘驱动。处理完之后就写 EOI 表中断完成

其他的中断都是这样处理,就不一一举例说明了,其中有一些中断还没有讲到,但所有中断的处理都是如此,根据向量号调用不同的中断处理程序,处理完之后写 EOI 表中断完成

中断退出程序

执行完 多处理器下的中断机制_初始化_283 函数之后,回到汇编程序 多处理器下的中断机制_#define_300

# Call trap(tf), where tf=%esp
  pushl %esp
  call trap
  addl $4, %esp

  # Return falls through to trapret...
.globl trapret    #中断返回退出
trapret:
  popal
  popl %gs
  popl %fs
  popl %es
  popl %ds
  addl $0x8, %esp  # trapno and errcode
  iret

中断退出程序基本上就是中断入口程序的逆操作。

首先从 多处理器下的中断机制_初始化_283 返回之后清理参数占用的栈空间,将 ESP 上移 4 字节。一般系统的源码就是汇编和 C 程序,所以使用 多处理器下的中断机制_初始化_302 调用约定,该约定规定了参数从右往左入栈,EAX,ECX,EDX 由调用者保存,也是调用者来清理栈空间等等。而清理栈空间呢?其实就是为了栈里面的数据正确,显然要当前栈顶指针需要向上移动 4 字节,后面的操作 多处理器下的中断机制_操作系统_303

清理了栈空间之后弹出各个寄存器,到错误码向量号的时候直接将 ESP 上移 8 字节跳过。

栈中变化情况如下:

多处理器下的中断机制_初始化_304

这里说明两点:

  • 多处理器下的中断机制_#define_305 出栈操作并不会实际清理栈空间的内容,只是 多处理器下的中断机制_初始化_207
  • 返回地址什么时候跳过的?一般情况下 多处理器下的中断机制_#define_307多处理器下的中断机制_操作系统_308 是一对儿,多处理器下的中断机制_#define_307 压入返回地址,多处理器下的中断机制_操作系统_308 弹出返回地址,可是没看到 多处理器下的中断机制_操作系统_308 啊?这里是汇编和 多处理器下的中断机制_操作系统_312 语言混合编程,将 多处理器下的中断机制_操作系统_312 代码 多处理器下的中断机制_操作系统_314 编译之后就有 多处理器下的中断机制_操作系统_308 了,所以弹出返回地址就发生在 多处理器下的中断机制_操作系统_316

现在 多处理器下的中断机制_操作系统_317 指向的是 多处理器下的中断机制_操作系统_318,该执行 多处理器下的中断机制_寄存器_319 了,多处理器下的中断机制_寄存器_319 时先检查是否进行了特权级转移,如果没有特权级转移,那么就要弹出 EIP,CS 和 EFALGS,如果有特权级转移则还要弹出 ESP,SS

多处理器下的中断机制_寄存器_321

原任务的所有状态都恢复了原样,则中断结束,继续原先的任务。

中断的总体过程大致就是这样,不只是 多处理器下的中断机制_寄存器_04 如此,所有基于 多处理器下的中断机制_#define_232

下面来看一看过程图:

多处理器下的中断机制_#define_324

这主要是定位中断服务程序的图,至于实际处理中断的过程图就不画了,把握上面的栈的变化就行了,而栈的变化情况上述的图应该描述的很清楚了,所以这里就不再赘述,说起栈,关于栈上述我们还遗留了一些问题,在这儿解答:

栈的问题

最后再来聊一聊栈的问题,栈一直是一个很困惑的问题,我一直认为,操作系统能把栈捋清楚那基本就没什么问题了。在进入中断的时候,如果特权级发生变化,会先将 SS,ESP 先压入内核栈,再压入 CS,EIP,EFLAGS

这句话看着没什么问题,但有没有想过这个问题:怎么找到内核栈的?切换到内核栈之后,ESP 已经指向内核栈,但是我们压入的 ESP 应该是切换栈之前的旧栈栈顶值,所以怎么得到旧栈的值再压入再者 多处理器下的中断机制_操作系统_325 时如果按栈中的寄存器顺序只是简单的先 多处理器下的中断机制_操作系统_326,再 多处理器下的中断机制_初始化_327

首先怎么切换到内核栈的这个问题,硬件架构提供了一套方法。有个寄存器叫做 TR 寄存器,TR 寄存器存放着 TSS 段选择子,根据 TSS 段选择子去 GDT 中索引 TSS 段描述符,从中获取 TSS

那说了半天 TSS 是啥?TSS(多处理器下的中断机制_操作系统_328),任务状态段,它是硬件支持的系统数据结构,各级(包括内核)栈的 SSESP所以当特权级变化的时候就会从这里获取内核栈的 SSESP。这个 TSS 这里我们只是简介,TSS 什么样子的,怎么初始化,还有些什么用处,它的功能都用到了?这些三言两语说不完,也不是本文重点,后面进程的时候会再次讲述。

接着第二个问题,切换到新栈怎么压入旧栈信息,其实这个问题很简单,我先把旧栈信息保存到一个地方,换栈之后再压入不就行了。关于 多处理器下的中断机制_寄存器_319 时弹出栈中信息是一个道理,查看 多处理器下的中断机制_#define_17 手册第二卷可以找到答案,的确也是这样处理的,手册中的伪码明显表示了有 多处理器下的中断机制_初始化_331 来作为中转站。但这个 多处理器下的中断机制_初始化_331

本文中断关于栈还有一个地方值得聊聊,嗯其实也没多大聊的,就是解释一句。建立栈帧的时候 多处理器下的中断机制_寄存器_333 的问题,这个是用来压入和弹出那 8 个通用寄存器的,还记得中断栈帧结构体中关于 ESP 的注释吗?写的是 多处理器下的中断机制_操作系统_334,意思是无用忽略,这是为啥?

这得从 多处理器下的中断机制_操作系统_266 说起,多处理器下的中断机制_操作系统_266 中压入 ESP 的时候压入的是 执行到 多处理器下的中断机制_#define_337非也,压入的是 执行 多处理器下的中断机制_#define_272 前的栈顶值,在执行 多处理器下的中断机制_#define_272 之前先将 ESP 的值保存到 多处理器下的中断机制_操作系统_340,当压入 ESP 的时候执行的时 多处理器下的中断机制_寄存器_341

所以 多处理器下的中断机制_操作系统_303 执行到弹出 多处理器下的中断机制_初始化_331 的时候,就不能将其中的值弹入 ESP,而是直接将 ESP 的值加 4 跳过 多处理器下的中断机制_初始化_331。因为将 多处理器下的中断机制_初始化_331 弹入 ESP 的话等于换了一个栈了,本来只该跳 4 字节的,结果跳过了很多字节,那明显就不对了嘛。

可以来张图看看,红线叉叉表示出错:

多处理器下的中断机制_初始化_346

关于 多处理器下的中断机制_操作系统_347

多处理器下的中断机制_#define_348

中断这一块关于栈方面的问题就是这么多吧,发生中断时有特权级变化就换栈,内核栈地址去 TSS 中找,中断完成后将所有的寄存器信息复原,其中就有 刚进入中断时压入的 SS ESP(有特权级变化的时候),栈也就恢复到了用户态下的栈。当然如果发生中断时就在内核态,那栈就不用变换,当然这只是 多处理器下的中断机制_寄存器_04

当然这只是一个普通外设触发的中断,一些特殊中断,中断嵌套开关中断的内容都没有讲述,中断是个很大的概念,内容也很庞杂,本文利用 多处理器下的中断机制_寄存器_04

https://wiki.osdev.org/APIC#Local_APIC_configuration

https://wiki.osdev.org/IOAPIC

http://blog.chinaunix.net/uid-20499746-id-1663122.html