北川广海の梦

北川广海の梦

浅谈中断与消息机制

2020-03-06

前言: 最近我一直在进行操作系统的学习。 顺便也推荐一本书《30天自制操作系统》,是一位日本作家,川合秀实写的。书的文字浅显易懂,也提供了大量的工具,这本书将教会读者如何一步一步实现自己的操作系统。然而读者也大可不必抱着“我一定要自己写出来!”的心态去阅读,可以只看作者做了哪些步骤,这些步骤的目的是什么,对它的做法有个大概的了解即可。 因为如果想每一步都完全靠自己实现,那要学习的成本实在太高,从计算机组成原理,到CPU指令集、X86汇编,编译原理,到C语言编译器,最重要的是要本身就对操作系统有着非常深刻的理解。所以对咱本科三年级的学生来说,并不现实。 就如同学习JAVA,谁会一上来就抱着JVM啃呢?肯定是Hello World 先~

1

即使是毫无经验的普通人,也都一定知道,CPU有着非常快的运算速度,它的职责就是不断地读取指令,执行指令,如此往复下去。 计算机的一切,都几乎是靠着CPU来进行控制的,而其他设备,例如鼠标,键盘,都是无法直接与CPU交互的,因为CPU只能直接访问内存,操作寄存器。

2

如果只有CPU,那么只能执行一系列命令,和当年的“打纸带”是没啥区别的,所以才有了其他的硬件,而其他硬件想要能与CPU交互,必须有点巧妙的东西。 这里有一句话说的很好“计算机科学的一切问题,都可以通过中间层解决”, 细品一下。 在硬件层面,当年设计电脑的叔叔们也设计出了一个中间层,用来解决和这些设备交互的问题,它就是PIC(Programmable interrupt controller)可编程中断控制器。 Pic与Cpu.jpg 它和CPU之间大概就是这关系。CPU和主PIC相连,然后主PIC与从PIC通过IRQ2相连。 IRQ(Interrupt ReQuest)可中断请求,计算机中,每个独立的硬件都会有一个独立的IRQ,那么有人就会有疑问了,这一共才16个IRQ,怎么够那么多硬件分?一种是通过PCI插卡,也就是通过主板上那长长的PCI接口来扩展,因为PCI总线是共享一个IRQ的。另一种办法就是通过USB的方式来加入硬件,因为USB的控制芯片只占用一个IRQ,而非所有USB设备都会占用一个IRQ。有了PIC,它就可以代理其他设备,像CPU发送消息,CPU也就能够拿到PIC传来的数据。而这个发送的数据,就是中断

通过图片可以知道,PIC相对于CPU来说,属于外部设备。 而CPU只能通过OUT指令,向PIC发送数据。 不了解汇编的同学,可以这样理解,CPU就是一个海岛,海岛上有许许多多的港口,每一个港口,都有自己的号码,这个号码就是端口号,我们向特定的端口发送数据,就如同一艘船从港口出航,驶向数据的终点。OUT指令就是做这样一件事情。这样,我们就能让CPU向PIC发送指令,让它完成对应的初始化工作,也能够从PIC读取各个设备发来的数据。

3

说了这么多,还没说到中断是什么,那现在就来介绍一下它。前面已经说了,CPU是一个只会不断执行指令家伙,而有些事情,并不能一开始就设定好,然后让CPU去执行的。例如键盘按下,鼠标移动等。这些都是需要CPU去处理的,然后才能得到反应。而如何让CPU知道键盘按下了呢?答案就是产生一个中断。中断这个说法也是非常的形象了,就像对CPU说“桥豆麻袋~!” CPU收到这个信号之后,就会停下手中的事情,去处理这个中断。当它处理完成之后,就会继续去完成剩下的事情。 那么怎么才能让中断能够通知到CPU呢? 我们首先必须弄出一个叫IDT(interrupt descriptor table)的东西,中文可以叫做中断记录表,通过将这个中断记录表,放在内存中,才能让CPU能够处理中断信号,这件事情,只能通过汇编来做

_load_idtr:		; void load_idtr(int limit, int addr);
	MOV	AX,[ESP+4]	; limit
	MOV	[ESP+6],AX
	LIDT	[ESP+6]
	RET

以上就是用汇编注册IDT的代码,可以通过特殊的方法(其实是编译器的加持)来被C语言调用,参数包含一个limit,可以理解为IDT的长度(大小),addr则是表示IDT的首地址。完成了这个指令之后,就可以将IDT表注册成功。

void init()
{
	for (i = 0; i <= LIMIT_IDT / 8; i++) {
		set_gatedesc(idt + i, 0, 0, 0);
	}
	load_idtr(LIMIT_IDT, ADR_IDT);
	set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, 0x008e);
}

void set_gatedesc(struct GATE_DESCRIPTOR *gd, int offset, int selector, int ar)
{
	gd->offset_low   = offset & 0xffff;
	gd->selector     = selector;
	gd->dw_count     = (ar >> 8) & 0xff;
	gd->access_right = ar & 0xff;
	gd->offset_high  = (offset >> 16) & 0xffff;
	return;
}
_asm_inthandler21:
		PUSH		ES
		PUSH		DS
		PUSHAD
		MOV		EAX,ESP
		PUSH		EAX
		MOV		AX,SS
		MOV		DS,AX
		MOV		ES,AX
		CALL		_inthandler21 
		POP		EAX
		POPAD
		POP		DS
		POP		ES
		IRETD
void inthandler21(int *esp)
{
	unsigned char data;
	io_out8(0x0020, 0x61);	/*通知PIC,IQR-01已经收到通知*/
	data = io_in8(0x0060);
	return;
}

以上的代码大可不必完全看懂,只需要记住,他们做了什么事情:首先对IDT进行分配和注册,然后设置特定的中断用什么函数处理,上面设定的是IRQ-01,也就是键盘产生的中断,我们用汇编写了它的处理程序asm_inthandler21(其实用C语言也可以写,但是中断处理完成后,不能使用RET指令,也就是C语言中的Return,必须用IRETD,所以暂时直接用汇编写出这个处理程序),并且重新调用set_gatedesc(),来注册它,这样一来,每当有中断产生,CPU就会自动调用这个函数。而在这段用汇编写的函数中,我们还通过CALL来调用了用C语言编写的inthandler21()函数,实际上我们的处理逻辑就在这个inthandler()函数中。 在这个函数中我们也先通过io_out()函数,告诉PIC,我们已经收到这个中断通知了,然后我们通过io_in()函数,获取这个中断所携带的数据。io_out和io_in函数其实分别就对应OUTIN指令,分别掌管着输出和输入。

4

做完了以上的事情,每当我们按下按键之后,inthandler21()函数中的data变量,就能够接收到设备产生的数据。虽然实际的操作系统中远没有这么简单(例如键盘内部一次只能发送一个字节数据,但每次按键会产生的数据“键值代码”有两个字节大小,所以一个char类型的data是接收不全的),但差不多都是这个逻辑。 好了,了解了中断之后,我们来讲讲消息机制。 Windows操作系统中,我们按下键盘,移动鼠标,都能看到反馈。这些都是操作系统提供的“炫酷的”视觉效果,而这些信号,是由外部设备直接产生的,并且告诉CPU,CPU再调用操作系统注册的处理函数。如果我们自己为这个操作系统编写程序,是根本无法访问到底层设备产生的信号的。那如何让我们的程序也做出响应呢? 前面提到一句话“计算机科学的一切问题,都可以通过中间层解决”。 Windows也是如此,Windows为每一个运行的进程,都维护了一个消息队列(不是Kafka那种,但却真的很像),当Windows检测到底层的鼠标按下,键盘移动等产生的中断,就将他们转换成特定的消息,投放到消息队列中,然后应用程序不断的从队列中取出消息,并且做出对应的响应。 在很久以前的Windows程序设计中,每一个窗口程序一定会有一个叫做消息循环的东西,它其实是一个死循环,它的作用就是不断的不断地读取消息队列里的消息,然后调用编程者开发的对应的处理函数。没有经历过win32时代的程序员肯定也没用过这东西(我也没用过),但是WinForm程序设计许多人都很熟悉,里面就包含了各种事件,例如鼠标点击什么的,可以推测,winform程序中也一定有着类似的东西,不断的从Windows消息队列中读取消息,然后调用对应的事件处理函数。 除桌面程序之外,在Web应用程序中,也总会有一个线程,会不断地循环,不断地监听某个网络端口,看看有没有发来的请求,如果有请求,则立马进行处理。

5

上面历史类的东西讲完了,后来的开发者受到历史的启发,设计出了一种编程模式:“观察者模式”。就好比一本杂志,它非常受欢迎,但它也非常奇怪,谁也不知道它的下一期会什么时候发布,于是人们每天都去报刊社问一问。这无疑是件非常麻烦的事情,于是报刊社老板想了一个办法,让想看这本杂志的人,把电话号码登记在报刊社,等新杂志一到货,他就给所有想看的人们打电话。于是人们都能解放出来,不必每天都去一趟报刊社,可以专心做自己的事情。

这样的设计方式可以大大降低程序的耦合,可以避免大量的重复代码。在C#中,用event 事件 来让开发者非常方便就能实现这个设计模式。而在其他语言中,例如JavaScript 则是Observeable,一个可订阅对象来实现的。

计算机的世界就是如此,从最底层的CPU中断,到操作系统的消息机制,再到程序设计的中的事件与观察者模式,总是有着非常惊人的相似,让人即觉得计算机的设计是历史的偶然,又认为它是时代发展的必然,实乃妙也~

以上就是本人在学习了中断之后,产生的感悟与个人见解,附件会上传《30天自制操作系统》的电子扫描版和程序压缩包,不过我还是希望看官们能够支持这本书作者。

30天自制操作系统.7z