3 中断, 是内核中非常核心的一部份, 有了中断, 分页机制的虚拟内存管理才有了可能。
6 Interrup, 中断, 意为被打断之意。 然在内核的实现中, 起着非常重要的作用, 使得内核可以异步处理一些慢I/O的操作, 如磁盘的读取等, 从而使得CPU不用慢长的等待那些操作的完成, 大大提高了CPU与系统的性能。 为了更好的理解中断, 我举例如下: 假如你正在吃饭, 此时突然手机来电, 你很有可能放下手中的饭碗, 拿起手机接听, 打完之后, 你自然会接着吃(排除出些了紧急情况)。 这就完成了一次中断, 手机是中断源, 而你被电话中断, 之后继续之前被中断的事--吃饭。 是的, 内核中的中断就是基于这个原理实现的。
9 假如你此时订了个餐, 大概半小时之后到(当然, 也有可能更久), 你肯定不会傻坐着干等着餐的到来吧, 所以, 此时一个默认的中断协议已经达成: 在餐来之前, 你可以做很多事情, 如玩电脑,看电视,甚至洗脸刷牙(如果你刚起的话), 然而此时有人在敲门, 接着你开门, 好的, 餐到了。 多好, 在这半小时, 做了很多事, 而且餐也来了。 然而内核的设计也是基于这个原理的。 假如, 你现在边听歌边写论文(是的, 这本是一件很享受的事情), 假设你每打一个字都会立即写入硬盘(但,实际上不是的, 好的系统会尽量延迟磁盘的写操作), 每当CPU收到写命令时, 便告知硬盘控制器, CPU便会马上做其它的事情(如播放歌曲), 等数据写完时, 硬盘控制器便会发一个信号给CPU, 说那一字节我已写了, CPU便又可处理你输入的另一字节, 如此如此循环, 编辑, 放歌, 两不误。 但假如, 没有中断, 每写一个字, CPU就是不断地在等待硬盘写的完成, 注意, 这里是忙等待, 也就是说不处理任何其它的事情, 就相当如下代码:
10 while (disk_write_not_finished) {
11 sleep_a_while(); /* Do nothing, waiting for the finish of disk write */
13 go_on_with_the_next_char();
14 当然, 结果可想而之, 编辑可以顺利进行, 但歌却一直被卡着, 尽管这个操作系统的是多任务的, 但显然不起作用。 这就相当于, 你什么事也不做就干等着, 不停的开门看看送餐有没到来, 我想这会是相当难受的! 可想而知, 如系统没了中断, 一切就会变得很慢。
16 当然, 你也许会想, 有了中断, 上面的代码将会变成什么样。 简单地说, 它可以是这样:
18 -------------- 编辑器的某些代码(实事上, 编辑器不管这些事) ------------
20 * 这个函数的功能将是把这个任务放入一个等待列表, 并将
21 * CPU转让给其它程序, 如player。
23 sleep_on_disk_write();
24 -------------------------------------------------------------------------
29 /* do the write things here */
34 wakeup_sleeper(); /* 这行, 将会使CPU继续执行刚被打断的操作, 使CPU继续处理你输入的字符。
40 本文之前也提到过些中断, 如BIOS int 0x13号磁盘相关的中断。 是的, 中断由int + 中断号触发的。 然其实, 中断的处理也是一段程序, 只不过是不是直接地如以C函数的方式显示的, 而是由中断号定位的。 当然, 你可以把它看着是一个函数。 那如何根据中断号找到那断处理程序呢?
42 在古老的实模式下, 是有一个叫做IVT(Interrupt Vector Table)的表, 它如GDT一样, 也是张表, 也是个数组, 每一项有4字节, 前两字节存放的是CS段基址, 后两字节存放的是IP地址, 也就是OFF值。 合起来就是实模式下的CS:IP。 而这个地址就自然存放着相应中断号所对应的中断处理程序。
44 然而在, 在32位保护模式下, 情况稍微有点不同而且比较复杂, 它的这张不是IVT, 而是IDT(Interrupt Descriptor Table), 每项如GDT一样, 也是有8个字节。中断描述符(Interrupt Descriptor)有三种, 为了简单起见, 我这里只列出了几个重要的域:
46 ------------------+----------------+ 8
47 | Offset 16 - 31 | xxxx |
48 ------------------+----------------+ 4
49 |Segment Selector | Offset 00 - 15 |
50 ------------------+----------------+ 0
51 比如说我们执行int 0x0, 它就会在IDT中找第一个项, 取出Segment Selector, 就如第三章所讲, 它将找到该段的基址, 比如为int_base, 再抽出offset, init_base + offset便是0号中断程序所对应的地址了。
53 也正如GDT, IDT也有一个6字节对应的描述符, 如:
55 unsigned short lenght;
56 unsigned long long *addr;
57 } __attribute__((packed));
59 注意, 在操作系统的开发过程中, 有些结构体得严格打包, 不然, 很可能会被编译器等做些优化而破坏了某些数据。 如上面的结构体, 应该严格按照如下的内存布局:
61 -----------------------------------------------------------------
63 -------------------------------------------------
65 ------------------------------------------------------------------
67 -----------------------------------------------------------------
68 是的, 编译很可能把你的代码优化成第二种格式, 但这显然跟idt描述述所要的不同, 所以, 也就不能正常工作。
72 好的, 有了上面的背景, 那么接下来的安装也就简单多了。 假设我们现在要安装前16个中断(现暂且不管它们都是些什么中断),那么代码可以这样:
79 set_idt_entry(i, &reserved);
82 是的, 就这么简单, 说白了也就是一个数组的初始化。 它的功能也很简单, 也就是把所有的中断处理程序都指向reserverd函数, 而该函数所要做的,也只要打印一些信息, 如下:
85 * The interrupt handler for all the interrups for now
87 static void reserved(void)
89 printk("A interrupt occured!");
92 是的, 还有一个函数, set_idt_entry, 真正做‘安装’的才是这个函数, 也很简单, 简写如下:
94 unsigned long long idt[256]; /* 作为全局变量 */
96 static void set_idt_entry(int int_num, void (*handler)(void))
98 unsigned short *idt_entry = (unsigned short *)&idt[int_num];
100 idt_entry[0] = handler && 0xffff;
101 idt entry[1] = 0x0008; /* the CS segment selector */
102 idt_entry[2] = 0x0000; /* Ignored for now */
103 idt_entry[3] = handler >> 16;
105 好的, 这就完成了。 当我们执行int 0x00(0-0xf都行), 屏幕上将输出"A interrupt occured"。
108 好的, 我们的前16号中断都能正常工作了(实际并没有, 因为它们并有做好相关的处理, 只是打印一条信息说中断已发生, 但甚至没有给出是哪个中断发生了)。 现在稍微说下那16个中断都是干什么的。 这16个中断都是intel CPU上预留的, 但实际上intel预留了前32个中断,现只介绍几个:
110 第00号中断: int 0x00, 是除0错误, 当我们除0时, 便会引发这个中断。
111 第03号中断: int 0x03, 中断, 多用于些调试软件的实现当中, 断点调试。
112 第06号中断: int 0x06, 无效操作码(或叫机器码)
113 第14号中断: int 0x0e, 缺页中断, 用于虚拟内存管理系统的实现
115 关于更多的中断及其细节, 请参考Intel System Programming Guide 3A卷手册。
117 但是这些中断是远远不够的, 如这里没有任何关于磁盘读取写入的中断, 时钟中断等等。 不过, 还好, 8259A[PIC spec]提供了这些中断, 只不过, 我们得做些初始化, 始得8259A能正常工作, 让它指向我们所需要的中断号与中断处理程序。 代码如下:
120 void ipc_install(void)
122 /* 0x11 表示初始化命令开始,是 ICW1 命令字 */
123 outb_p(0x11, 0x20); /* 发送到 8259A 主芯片 */
124 outb_p(0x11, 0xa0); /* 发送到 8259A 从芯片 */
126 outb_p(0x20, 0x21); /* 发送主芯片 ICW2字, 告诉其开始中断号, 这里是0x20 */
127 outb_p(0x28, 0xa1); /* 从芯片起始中断号, 0x28 */
129 outb_p(0x04, 0x21); /* 发送ICW3命令字, 主芯片的IR2连从芯片INT */
130 outb_p(0x02, 0xa1); /* 从芯片的INT连到主芯片的IR2引脚上 */
132 outb_p(0x01, 0x21); /* 发送ICW4命令字, 表示初始化结束, 芯片就绪 */
135 outb_p(0x11, 0x21); /* 屏蔽所有中断请求 */
138 这样, 8259A提供的中断就从0x20开始编号, 如时钟中断所对应的中断号就为0x20, 我们若要使时钟中断能正常工作, 可以在简写如下时钟的init函数如下:
145 set_idt_entry(0x20,timer_interrupt);
147 outb(inb(0x21)&0xfe, 0x21);
150 类似的, 软盘驱动, 键盘驱动亦可以这么写。 关于8259A就介绍到这, 关于其更详细的信息可参考相关的说明文档与书籍。
154 上面已经把中断大多来龙去脉都讲了个大概, 唯一有点遗憾的是没有给出一个中断处理程序的实例, 现在将详细讲述中断处理程序的实现作为本章的结尾。
156 关于中断处理程序的实现, 之前讲到的函数调用栈很重要, 若之前的都能理解,那么中断程序实现起来也就不难以理解了, 只不过, 仍是稍微有些复杂。 中断大概可以为分两种, 一种是带出错码的, 另一种是没带出错码的, 这两种的栈结构如分别如下:
188 所以, 有没这两种的中断处理程序稍微有点不同, 分别举例如下:
191 void divide_error_handler(void)
193 printk("You shouldn't divide with ZERO\n");
195 /* You can add your code here to do what you want */
201 void general_protection_handler(unsigned int err_code)
203 printk("The err_code is %08x\n", err_code);
205 /* You can add more specific code here like this */
208 /* Tell ther user what this error is */
220 至此, 关于中断的全部内容就大概讲这些, 由于篇幅所限,讲的很笼统, 有很多也只是一笔带过, 若想了解更多关于中断方面的内容, 你可以参考Intel官方文档, 以及Thunix, linux源代码。 毕竟源代码是最好的学习资料。
222 最后, 做个整理, 贴出interrpt_init函数的代码:
229 unsigned short length;
230 unsigned long long *address;
231 } __attribute__((packed)) idt_descr = {256*8-1, idt};
235 set_idt_entry(0,÷_error);
236 set_idt_entry(1,&debug);
237 set_idt_entry(2,&nmi);
238 set_idt_entry(3,&int3);
239 set_idt_entry(4,&overflow);
240 set_idt_entry(5,&bounds);
241 set_idt_entry(6,&invalid_op);
242 set_idt_entry(7,&device_not_available);
243 set_idt_entry(8,&double_fault);
244 set_idt_entry(9,&coprocessor_segment_overrun);
245 set_idt_entry(10,&invalid_TSS);
246 set_idt_entry(11,&segment_not_present);
247 set_idt_entry(12,&stack_segment);
248 set_idt_entry(13,&general_protection);
249 set_idt_entry(14,&page_fault);
250 set_idt_entry(15,&reserved);
251 set_idt_entry(16,&coprocessor_error);
253 for (i=17;i < 48;i++)
254 set_idt_entry(i,&reserved);
258 __asm__ ("lidt %0\n\t"::"m"(idt_descr)); /* 最后别忘了要加载idt描述符 */