任务切换
多任务(multitask)也可以称作多进程,在windows操作系统中,就是多个应用程序同时运行的状态。
这里来复习一下进程和线程的概念:
进程:进程是执行程序的一次执行过程,它是一个动态的概念,是系统资源分配的单位。可以将运行在内存中的exe文件理解为进程,进程是受操作系统管理的基本运行单元。
线程:线程可以理解成在进程中独立运行的子任务。比如QQ.exe运行时,就有很多子任务同时运行。每一项任务可以理解成“线程”在工作,这样做的优势是最大限度地利用CPU的空闲时间来处理其他任务。
那么多任务是怎么实现的呢?
最简单的方式,电脑里如果有多个CPU,每个应用程序都在不同的CPU上运行,多任务就顺利完成了。
但是事实上我们电脑里只有1个CPU,也可以实现多任务。方法就是尽可能快的切换不同的任务,快到用户察觉不出来就可以了。
一般情况下,这个切换的动作每0.01~0.03秒就会进行一次。
可能有同学会好奇,为什么是0.01秒呢,不应该越快越好么?
事实上,CPU进行任务切换这个动作本身也需要消耗时间,这个时间大约在0.0001s左右(不同操作系统、不同CPU所需时间不同)。如果CPU每隔0.001切换一次任务,那么CPU处理能力的10%都被浪费了。别小看这10%,快10%CPU价格可要高不少呢。因此切换任务的间隔最短也要是0.01s,1%的处理能力浪费,我们认为还是可以接受的。
我们来看如何让CPU处理多任务呢?
当我们向CPU发出任务切换指令时,CPU会先把寄存器中的值全部写入内存,之后为了运行下一个程序,CPU会把寄存器中的全部值从内存中读取出来(这里读取和写入的地址不同)。
那么寄存器中的内容是怎样写入内存中的呢?
我们要用到一种结构,叫TTS(task status segment),即“任务状态段”。TTS也是内存段的一种,它有两个版本,16位版本和32位版本,在GDT中定义后才能使用。
struct TSS32 {
int backlink, esp0, ss0, esp1, ss1, esp2, ss2, cr3;
int eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi;
int es, cs, ss, ds, fs, gs;
int ldtr, iomap;
};
TTS总共包含26个int成员,共分了四行。
第一行保存的是与任务设置相关的信息,除了backlink外在执行任务切换的时候不会被写入。
第二行的成员是32位寄存器,第三行是16位寄存器。
其中,EIP(Extended Instruction Pointer),即扩展指令指针寄存器。扩展即32位的意思,对应的16位寄存器叫作IP。它是用来记录下一条需要执行的指令在内存中位于哪个地址,每执行一条指令,EIP寄存器的值会自动累加,保证一直指向下一条指令所在的内存地址。
在TTS中将EIP寄存器的值记录下来,当返回到这个任务的时候,CPU就知道从哪里开始读取程序了。
第四行也是任务设置相关的信息,在任务切换时不会被CPU写入。这里我们将ldtr置为0,iomap置为0x40000000。
接下来,如何进行任务切换呢?
这里要用到JMP指令,JMP指令分为near模式和far模式两种。其中只改写EIP的成为near模式,同时改写EIP和CS的成为far模式。其中CS(code segment)是代码段寄存器。
我们之前用到都是near模式,那么far模式长什么样呢?
JMP DWORD 2*8:0x0000001b
像上面这样在目标地址中带有“:”的,就是far模式,这条指令向EIP存入了0x1b,同时将CS置为2*8。
当JMP指令指定的目标地址段是TTS,CPU就不会执行改写EIP和CS的操作,而是执行任务切换。
接下来实际做一次任务切换吧,准备两个任务A和B,再准备两个TTS,任务A的TTS和任务B的TTS,并向它们存入相应的值。
HariMain节选:
struct TSS32 tss_a, tss_b;
tss_a.ldtr = 0;
tss_a.iomap = 0x40000000;
tss_b.ldtr = 0;
tss_b.iomap = 0x40000000;
struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT;
set_segmdesc(gdt + 3, 103, (int) &tss_a, AR_TSS32);
set_segmdesc(gdt + 4, 103, (int) &tss_b, AR_TSS32);
每次给TR寄存器赋值时,需要把GDT的编号乘以8,就是这样规定的没有什么原因。刚刚我们把任务定义为GDT的3号,因此向TTS寄存器存入3*8。TR赋值需要用到LTR指令,这个指令只能使用汇编语言。
load_tr(3*8);
LTR指令的作用只是改变TR寄存器的值,要进行任务切换还是要执行far模式 JMP指令。
naskfunc.nas节选
_load_tr: ; void load_tr(int tr);
LTR [ESP+4] ; tr
RET
_taskswitch4: ; void taskswitch4(void);
JMP 4*8:0
RET
我们将taskswitch4()的调用放在HariMain显示“10[sec]”的语句后面。
再来准备tts_b,在任务切换的时候小读取tss_b的内容,需要在TTS中定义好寄存器的初始值。
tss_b.eip = (int) &task_b_main;
tss_b.eflags = 0x00000202; /* IF = 1; */
tss_b.eax = 0<