30天自制操作系统——第十五天实现多任务(一)

本文详细介绍了如何在操作系统中实现多任务。从任务切换的概念出发,解释了进程、线程的区别,以及多任务在单CPU环境下的实现原理。通过使用任务状态段(TTS)和JMP指令,演示了任务切换的具体步骤,并逐步改进,实现从任务A到任务B的切换,以及快速的任务循环切换。最后,通过添加mt_int和mt_taskswitch函数,实现了即使在程序中不进行任务切换,系统也能自动进行任务切换,从而达到真正的多任务运行效果。
摘要由CSDN通过智能技术生成

任务切换

多任务(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<
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值