《30天自制操作系统》笔记(12)——多任务入门
上一篇介绍了设置显示器高分辨率的方法。本篇讲一下操作系统实现多任务的方法。
什么是多任务
对程序员来说,也许这是废话,不过还是说清楚比较好。
多任务就是让电脑同时运行多个程序(如一边写代码一边听音乐一边下载电影)。
电脑的CPU只有固定有限的那么一个或几个,不可能真的同时运行多个程序。所以就用近似的方式,让多个程序轮换着运行。当轮换速度够快(0.01秒),给人的感觉就是"同时"运行了。
多任务之不实用版
我们首先从最基本的想法开始,做一个不实用版的多任务作为例子。在学习这个例子的过程中引入真正的多任务必须的TSS、TR、far模式JMP的概念,为后续内容打基础。
当你向CPU发出任务切换的指令时,CPU会先把寄存器中的值全部写入内存某处;然后,从内存另一位置把所有寄存器的值读取出来。这就完成了一次任务切换。
任务切换消耗的时间就是读写内存消耗的时间,大概为0.0001秒。
任务状态段TSS
存取全部寄存器的值这件事,当然需要有一个数据结构,这就是"任务状态段"(Task Status Segment)简称TSS。
1 struct TSS32 2 { 3 int backlink, esp0, ss0, esp1, ss1, esp2, ss2, cr3; 4 int eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi; 5 int es, cs, ss, ds, fs, gs; 6 int ldtr, iomap; 7 };
TSS32中第一行(从backlink到cr3)暂时不用理会。
第二、三行(从eip到gs)都是寄存器。其中EIP是CPU用来记录下一条需要执行的指令位于内存中的地址的寄存器,因此被称为"指令指针"。实际上JMP指令就是修改了EIP的值。
第四行也不用理会。
TSS中的信息会存储到内存某处(记为X),而X的地址会注册到GDT中。(不知道什么是GDT?请查看这里)
寄存器TR
寄存器TR是作用是让CPU记住当前在运行哪个任务。其存储的值是"当前任务所在的段号*8"。只需在操作系统启动时对其赋值一次,以后进行任务切换时,CPU会自动调整TR的值。给TR赋值只能用汇编实现。
1 _load_tr: ; void load_tr(int tr); 2 LTR [ESP+4] ; tr 3 RET
LTR指令只是改变TR的值,不会发生任务切换。所以我感觉TR像是一个标识变量。正是由于这一点我才有了后文的猜想。
切换任务就是执行JMP指令
JMP指令分两种,即"只改写EIP的near模式"与"同时改写EIP和CS的far模式"。CS是代码段寄存器(code segment)。
平时使用的都是near模式。
在asmhead.nas中跳转到bootpack.c中的主函数用的是far模式,即
这条指令在向EIP写入0x1b时,也向CS写入2*8(即16)。
像这样在JMP目标地址中带冒号(:)的,就是far模式。
切换任务时,我们使用far模式的JMP指令。
CPU执行far模式的JMP指令前,会根据GDT中注册的TSS情况,判断JMP的目标地址是可执行代码还是TSS。如果是可执行代码,那么CPU就认为这只是一个普通的far模式的JMP;如果是TSS,则认为这是一个任务切换指令,会切换到目标地址指定的TSS所记录的任务中,也就是JMP到另一个任务那里去了。
所以普通的far模式的JMP和任务切换的JMP指令,其机器码是同一个。
Demo:两个任务切换
我们把操作系统启动时运行的程序记作任务A,即如下代码。
1 void HariMain(void) 2 { 3 /* 略 */ 4 timer_ts = timer_alloc(); 5 timer_init(timer_ts, &fifo, 2); 6 timer_settime(timer_ts, 2); 7 /* 略 */ 8 for (;;) { 9 io_cli(); 10 if (fifo32_status(&fifo) == 0) { 11 io_stihlt(); 12 } else { 13 i = fifo32_get(&fifo); 14 io_sti(); 15 if (i == 2) { 16 farjmp(0, 4 * 8); 17 timer_settime(timer_ts, 2); 18 } else if (256 <= i && i <= 511) { /* 键盘数据 */ 19 /* 略 */ 20 } else if (512 <= i && i <= 767) { /* 鼠标数据 */ 21 /* 略 */ 22 } else if (i == 10) { /* 10秒计时器 */ 23 putfonts8_asc_sht(sht_back, 0, 64, COL8_FFFFFF, COL8_008484, "10[sec]", 7); 24 } else if (i == 3) { /* 3秒计时器 */ 25 putfonts8_asc_sht(sht_back, 0, 80, COL8_FFFFFF, COL8_008484, "3[sec]", 6); 26 } else if (i <= 1) { /* 光标用计时器 */ 27 /* 略 */ 28 } 29 } 30 } 31 }
下面是任务B执行的函数。