首页 > 其他分享 >【读书笔记-《30天自制操作系统》-14】Day15

【读书笔记-《30天自制操作系统》-14】Day15

时间:2024-09-03 20:54:31浏览次数:13  
标签:读书笔记 int 30 timer mt Day15 任务 切换 tss

本篇内容开始讲解多任务。本篇内容结构很简单,先讲解任务切换的原理,再讲解任务切换的代码实践。但是涉及到的知识不少,理解上也有些难度。

在这里插入图片描述

1. 任务切换与多任务原理

1.1 多任务与任务切换

所谓多任务,指的是操作系统同时运行多个任务。但是这种说法实际上是不准确的。如果只有一个CPU,是无法事实上实现同时运行多个任务的。而之所以给用户以多个任务在同时运行的错觉,其实是因为多个任务之间在快速地切换。

为了造成这种错觉,切换的间隔时间不能很长;但同时,过于频繁地切换又会严重消耗CPU的处理能力。二者平衡来看,一般的操作系统选择每0.01s进行一次切换,这样消耗在切换过程的CPU处理能力大概是1%,就可以忽略不计了。

讲清楚了多任务与任务切换的关系,下面来讲任务切换的过程。

1.2 任务切换过程

CPU接收到任务切换指令时,会将所有寄存器的值保存在内存中。这是为了以后切换回来时可以从中断的地方继续运行。接下来,为了运行下一个程序,CPU又会从内存中取出另一组寄存器的值,完成一次切换。而切换所需的时间,实际上就是从内存读写寄存器的时间。

1.3 TSS

寄存器中的内容如何写入内存呢?这里引入一种数据结构TSS(Task status segment,任务状态段)。TSS也是内存段的一种,需要在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;
}

TSS中的内容有26个int成员,共104字节。第一行的内容与任务设置相关,可以暂时忽略;第二行是32位寄存器,第三行是16位寄存器。EIP是“extended instruction pointer”的缩写,扩展指令指针寄存器。E表示是32位的寄存器,16位的版本就是IP。EIP中存放的是CPU下一条需要执行指令的地址。每执行一条指令,EIP寄存器中的值会自动累加,保证一直指向下一条需要执行的指令。
实际上JMP指令也利用了EIP寄存器。JMP 0x1234实际执行了向EIP赋值,改变EIP的值后,下一条指令就从新的地址取出,也就实现了跳转。
将EIP的值保存下来,切换回来的时候CPU就知道从哪里开始继续执行了。

第四行的ldtr和iomap也是与任务设置相关的部分,需要正确赋值。这里暂时将ldtr设置位0,将iomap设置为0x40000000。

1.4 任务切换实践

TSS讲解完了,继续来看任务切换的过程。进行任务切换实际上还是需要用到JMP指令。JMP指令分为两种:只改写EIP的称为near模式,同时改写EIP和CS的称为far模式。CS是代码段寄存器,修改了CS就表示要跳转到其他的段了。
如果一条JMP指令所指定的目标地址段不是可执行的代码,而是TSS,那么CPU就不会执行通常的改写CS与EIP的操作,而是将这条指令理解为任务切换。

1.4.1 切换前的任务设置

接下来实践一下,准备两个任务A和B,做从A切换到B的操作。
首先创建两个任务的TSS:

struct TSS32 tss_a, tss_b;

给他们的ldtr和iomap赋值为合适的值:

tss_a.ldtr = 0;
tss_a.iomap = 0x40000000;
tss_b.ldtr = 0;
tss_b.iomap = 0x40000000;

此外还要注册到GDT中:

	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);

将tss_a定义为gdt的3号,段长限制为103字节,tss_b也采用类似的定义。

TR(task register)寄存器存放的是当前执行的任务,进行任务切换的时候,TR寄存器的值也会发生变化。我们给TR寄存器赋值为3*8,即GDT的3号,因为给TR寄存器赋值需要将GET编号乘以8。给TR寄存器赋值需要通过汇编语言的LTR指令:

load_tr(3 * 8);

_load_tr:		; void load_tr(int tr);
		LTR		[ESP+4]			; tr
		RET

1.4.2 任务切换过程

接下来还要执行far模式的跳转指令,这里还是需要用汇编语言进行编写。

_taskswitch4:	; void taskswitch4(void);
		JMP		4*8:0
		RET

通常情况下,JMP指令后面的RET指令是没有意义的。但是对于用作任务切换的JMP指令,重新返回这个任务时,程序会从这条JMP指令之后继续运行。这里就是执行RET,从汇编语言函数返回C语言主程序。

如果far-JMP指令用于任务切换,则地址段4*8一定要指向TSS,而偏移量则可以忽略,这里写为0即可。

执行切换的函数写好了,我们在主程序中调用就可以实现切换。在哪里调用呢?我们放在超时10s的处理里面:

else if (i == 10) { /* 10s计时器} */
				putfonts8_asc_sht(sht_back, 0, 64, COL8_FFFFFF, COL8_008484, "10[sec]", 7);
				taskswitch4();
			}

这样程序启动10s后,就会执行切换。

到这里切换的过程就完成了吗?其实还没有。运行taskswitch4函数可以切换到任务B,但我们还没有设置好任务B的TSS,这些工作其实是在初始化时完成的。

tss_b.eip = (int) &task_b_main;
	tss_b.eflags = 0x00000202; /* IF = 1; */
	tss_b.eax = 0;
	tss_b.ecx = 0;
	tss_b.edx = 0;
	tss_b.ebx = 0;
	tss_b.esp = task_b_esp;
	tss_b.ebp = 0;
	tss_b.esi = 0;
	tss_b.edi = 0;
	tss_b.es = 1 * 8;
	tss_b.cs = 2 * 8;
	tss_b.ss = 1 * 8;
	tss_b.ds = 1 * 8;
	tss_b.fs = 1 * 8;
	tss_b.gs = 1 * 8;

从后半段寄存器赋值来看,给CS赋值为GDT的2号,其他的寄存器设置为1号,其实是使用了与bootpack.c相同的地址段。使用其他的地址段也没有问题这里只是为了举个例子。
在eip中需要定义好切换到这个任务时从哪里开始运行,于是把task_b_main的地址赋值给eip。task_b_main就是任务B要运行的函数,目前其实什么都没做,只是执行了HLT。

void task_b_main(void)
{
	for (;;) { io_hlt(); }
}

task_b_esp是为任务B定义的栈。切换任务的时候,每个任务都有自己专门的栈。

int task_b_esp;
task_b_esp = memman_alloc_4k(memman, 64 * 1024) + 64 * 1024;

到这里也就切换的过程也就全部完成了。由于任务B只是执行HLT,所以运行的结果是10s之后停住,鼠标和键盘都没有反应了。

完成了切换到任务B,我们再从任务B切换回任务A。

void task_b_main(void)
{
	struct FIFO32 fifo;
	struct TIMER *timer;
	int i, fifobuf[128];

	fifo32_init(&fifo, 128, fifobuf);
	timer = timer_alloc();
	timer_init(timer, &fifo, 1);
	timer_settime(timer, 500);

	for (;;) {
		io_cli();
		if (fifo32_status(&fifo) == 0) {
			io_sti();
			io_hlt();
		} else {
			i = fifo32_get(&fifo);
			io_sti();
			if (i == 1) { /* 超时时间为5s */
				taskswitch3(); /* 返回任务A */
			}
		}
	}
}

_taskswitch3:	; void taskswitch3(void);
		JMP		3*8:0
		RET

改写后的任务B程序与主程序类似,并且定义了一个5s的定时器。超时时间一到,就执行taskswitch3切换回任务A。有了前面的基础,这些修改也不难理解了。

1.5 多任务实践

完成了任务切换的功能,只需要再实现快速交替切换任务,就实现了多任务的目的,也不难做到。

首先将任务切换的函数改写的更加通用一些。

_farjmp:		; void farjmp(int eip, int cs);
		JMP		FAR	[ESP+4]				; eip, cs
		RET

使用JMP FAR指令时,需要指定一个地址。CPU会从指定的地址中读出4字节数据存入EIP,再继续读取2字节数据存入CS。这样我们调用_farjump(eip,cs)时,在[ESP + 4]的位置就存放了EIP的值,[ESP + 8]的位置则存放了CS的值,就可以实现预期的JMP FAR了。
因此taskswitch3就可以改写为farjmp(0, 38),taskswitch4就可以改写成farjmp(0, 48)。

至于缩短时间间隔,我们只需要在任务A和任务B中分别准备一个0.02s的定时器,每隔0.02s就执行一次切换,这样就完成了。

	timer_ts = timer_alloc();
	timer_init(timer_ts, &fifo, 2);
	timer_settime(timer_ts, 2);

for (;;) {
		io_cli();
		if (fifo32_status(&fifo) == 0) {
			io_stihlt();
		} else {
			i = fifo32_get(&fifo);
			io_sti();
			if (i == 2) {
				farjmp(0, 4 * 8);
				timer_settime(timer_ts, 2);
……

可以看出主程序也就是任务A中设置了定时器ts,达到0.02s的超时时间后就执行切换,而切换返回后再执行timer_settime重新设置超时时间。

void task_b_main(void)
{
	struct FIFO32 fifo;
	struct TIMER *timer_ts;
	int i, fifobuf[128], count = 0;
	char s[11];
	struct SHEET *sht_back;

	fifo32_init(&fifo, 128, fifobuf);
	timer_ts = timer_alloc();
	timer_init(timer_ts, &fifo, 1);
	timer_settime(timer_ts, 2);
	sht_back = (struct SHEET *) *((int *) 0x0fec);

	for (;;) {
		count++;
		sprintf(s, "%10d", count);
		putfonts8_asc_sht(sht_back, 0, 144, COL8_FFFFFF, COL8_008484, s, 10);
		io_cli();
		if (fifo32_status(&fifo) == 0) {
			io_sti();
		} else {
			i = fifo32_get(&fifo);
			io_sti();
			if (i == 1) { /* 任务切换 */
				farjmp(0, 3 * 8);
				timer_settime(timer_ts, 2);
			}
		}
	}
}

任务B的程序也与此类似。但如何确认任务B确实在运行呢?这里我们让任务B执行计数功能。不过还存在一个问题,任务B中没有定义sht_back变量,需要在切换的时候传进来。如何传进来呢?这里先比较随便地将sht_back存在一个地址0x0fec中,切换到任务B时再从这个地址中获取。

*((int *) 0x0fec) = (int) sht_back;

sht_back = (struct SHEET *) *((int *) 0x0fec);

这样运行一下,由于切换速度很快,就给人以同时运行的感觉。
在这里插入图片描述
但是通过一个随意的地址来传送sht_back变量肯定是不合适的。从汇编语言的角度考虑,传入的参数就存放在内存地址ESP+4中,因此可以进行如下改写:

task_b_esp = memman_alloc_4k(memman, 64 * 1024) + 64 * 1024 - 8;

*((int*)(task_b_esp+ 4)) = (int)sht_back;

分配的内存地址为64K,假设是从0x01234000开始,则task_b_esp的地址为0x0123ff8,ESP+4的地址即为0x0123ffe。从这里写入4字节,恰好不会超出64KB的空间。而运行B任务时,ESP+4的地址中已经存入了sht_back变量,B任务就会将其作为参数进行处理了。

在task_b_main程序中是不能使用return语句的。因为return语句归根结底是返回函数调用位置的一条JMP指令。由于task_b_mian这个程序不是由其他程序直接调用的,没有确定的调用位置,使用return会使程序无法正常运行。

到这里我们已经实现了一种多任务,但却还不是真正的多任务。因为当前的任务切换函数在任务A和任务B中执行,如果任务自身出了问题,可能会出现无法切换的情况。所谓真正的多任务,是在程序本身没有感知的情况下实现任务切换。

创建这样一个函数:

struct TIMER *mt_timer;
int mt_tr;

void mt_init(void)
{
	mt_timer = timer_alloc();
	timer_settime(mt_timer, 2);
	mt_tr = 3 * 8;
	return;
}

void mt_taskswitch(void)
{
	if (mt_tr == 3 * 8) {
		mt_tr = 4 * 8;
	} else {
		mt_tr = 3 * 8;
	}
	timer_settime(mt_timer, 2);
	farjmp(0, mt_tr);
	return;
}

mt_init函数设置了初始化了mt_tr的值,并设置了一个0.02s的定时器。这里超时后不向fifo中写入数据,因此不需要使用timer_init。mt_tr实际存放了TR寄存器的值,mt_taskswitch则根据当前mt_tr的值确定下一个mt_tr的值,重新设置定时器并且通过farjmp实行切换,还是比较简单的。

这样我们也需要修改一下inthandler20函数。

void inthandler20(int *esp)
{
	struct TIMER *timer;
	char ts = 0;
	io_out8(PIC0_OCW2, 0x60);	
	timerctl.count++;
	if (timerctl.next > timerctl.count) {
		return;
	}
	timer = timerctl.t0;
	for (;;) {
		if (timer->timeout > timerctl.count) {
			break;
		}
		/* 超时 */
		timer->flags = TIMER_FLAGS_ALLOC;
		if (timer != mt_timer) {
			fifo32_put(timer->fifo, timer->data);
		} else {
			ts = 1; /* mt_timer超时 */
		}
		timer = timer->next; 
	}
	timerctl.t0 = timer;
	timerctl.next = timer->timeout;
	if (ts != 0) {
		mt_taskswitch();
	}
	return;
}

如果是mt_timer发生了超时,则将ts变量设置为1,在主程序中判断如果ts变量不为0,则执行mt_taskswitch进行任务切换。

为什么不在中断处理函数inthandler20中直接执行任务切换呢?

原因在于调用mt_taskswitch进行任务切换的过程中,中断允许标志IF的值可能会被重设为1(因为切换任务的同时会切换EFLAGS)。如果此时中断处理还没完成,开启中断,可能会有下一个中断进来,这样就会导致程序出错。

本篇的内容终于完成了。关于任务切换的基本过程,不清楚的知识还真不少,阅读了三遍才算基本理清。下一篇继续硬核的多任务,敬请期待。

标签:读书笔记,int,30,timer,mt,Day15,任务,切换,tss
From: https://blog.csdn.net/Ocean1994/article/details/141802442

相关文章

  • 8.30 ~ 9.8
    8.30返校日。又回到了原来的班(和化奥一个班),一个班有69个人;然后我坐在最角上......
  • 8.30 上午 becoder 模拟赛总结 & 题解
    T1密码当时想到解法了,却依然认为自己不会做,我真是个人才。结论:对于$\foralli\in[1,n)$,满足密码不是$a_i$的因数,且密码是$a_k$的因数,设满足条件的最小值为$g$则答案为$\frac{n}{g}$。一种最好想的做法:枚举$\gcd(a_k,n)$的因数作为$g$,并枚举$i\in[1,n)$,判断是......
  • 电力系统机组组合优化调度(IEEE14节点、IEEE30节点、IEEE118节点)(Matlab代码实现)
     ......
  • 前端如何在30秒内实现吸管拾色器?
    ⭐前言大家好,我是yma16,本文分享前端react——实现浏览器页面的吸管拾色器功能。背景:在chromeweb端快速实现一个页面的取色器功能,分为两个场景固定区域小范围取色当前页面取色⭐canvas实现区域范围的取色器原理使用canvas的createLinearGradient绘制渐变区域监听点击坐标值,使用c......
  • ORA-04030
    TableofContents1.问题描述2.问题分析2.1.查看内存分配2.2.查看MOS3.解决方案3.1.方案1:修改操作系统页面计数3.2.方案2:修改数据库参数3.2.1.Oracle11204版本之前3.2.2.Oracle12.1版本之后4.解决实验1.问题描述现场运维人员反馈,在......
  • 使用同步锁的代码示例30
    使用同步锁的代码示例packageorg.zyf.javabasic.thread.lock.opti;importjava.util.concurrent.locks.ReentrantLock;/***@program:zyfboot-javabasic*@description:使用了ReentrantLock来保护对共享资源(counter)的访问,确保同一时间只有一个线程可以对计数器......
  • P7技术专家30k前端架构-商用级产品架构,业务实现+开发提效双线并进
    P7技术专家30k前端架构-商用级产品架构,业务实现+开发提效双线并进最近部门招聘,很多工程师,包括我在内都参与了内推和面试的过程,经过这次招聘,我发现能够最终拿到offer的人,基本上在看到简历的那一瞬间就已经定下来了,后续的面试只不过是一种验证而已(注意,是验证,而不是走过场),除非你......
  • 30s到0.8s,记录一次接口优化成功案例!
    大家好,我是沐子,推荐一个程序员免费学习的编程网站我爱编程网(www.love-coding.com)**场景**在高并发的数据处理场景中,接口响应时间的优化显得尤为重要。本文将分享一个真实案例,其中一个数据量达到200万+的接口的响应时间从30秒降低到了0.8秒内。这个案例不仅展示了问题诊......
  • 惠海H6900B 12V升压24V 60V 5V升压12V芯片 DC-DC LED恒流驱动IC 300W大功率
    H6900B是一款高效率、稳定可靠的升压型LED恒流驱动芯片,适用于多个高亮度LED灯串的恒流驱动。以下是关于该产品的详细解析:产品特征:内置耐压60V功率NMOS,可直接处理60V电压,无需额外升压电路。输入电压范围2.7V-48V,兼容多种电源类型。效率高达95%以上,降低能耗,延长电池寿命。工作频率1MH......
  • 读书笔记:高效C/C++调试
    高效C/C++调试(美)严琦、卢宪廷目录第1章调试符号和调试器11.1调试符号11.1.1调试符号概览2全局变量文件行号数据类型1.1.2DWARF格式31.2实战故事1:数据类型的不一致141.3调试器的内部结构161.3.1用户界面161.3.2符号管理模块161.3.3目标管理模块......