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

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

在这里插入图片描述

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]			; trRET

1.4.2 任务切换过程

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

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

通常情况下,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:0RET

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

1.5 多任务实践

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

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

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

使用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)。如果此时中断处理还没完成,开启中断,可能会有下一个中断进来,这样就会导致程序出错。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/415056.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【python因果推断库2】使用 PyMC 模型进行差分-in-差分(Difference in Differences, DID)分析

目录 使用 PyMC 模型进行差分-in-差分(Difference in Differences, DID)分析 导入数据 分析 使用 PyMC 模型建模银行业数据集 导入数据 分析 1 - 经典 22 差分-in-差分 (DiD) 分析 2 - 具有多个干预前后观测值的差分-in-差分 (DiD) 分析 使用 PyMC…

设计模式之生成器方法

一、生成器模式概念 Builder模式也叫建造者模式或者生成器模式,是由GoF提出的23种设计模式中的一种。Builder模式是一种对象创建型模式之一,用来隐藏复合对象的创建过程,它把复合对象的创建过程加以抽象,通过子类继承和重载的方式…

MySQL:约束

目录 一、概述二、创建测试三、外键约束3.1 数据准备3.2 添加外键3.3 删除外键3.4 增加外键约束 一、概述 约束主要用于作用在表中字段上的规则,用于限制存储在表中的数据。 保证数据库中数据的正确性、有效性和完整性。 约束描述关键字主键约束非空并且唯一PRIMARY…

代码随想录算法训练营第三十四天| 62.不同路径 63. 不同路径 II

62.不同路径 题目: 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。 问总共有多少…

ESD防静电监控系统助力电子制造行业转型升级

在电子制造行业中,静电危害不容小觑。ESD 防静电监控系统的出现,为行业转型升级带来强大助力。电子元件对静电极为敏感,微小的静电放电都可能损坏元件,影响产品质量。ESD 防静电监控系统能够实时监测生产环境中的静电状况&#xf…

rknntoolkitlite2环境搭建

目录 前言 0、要下载的软件包 一、环境搭建步骤 1.1 安装Miniconda 1.2创建RKNN虚拟环境 1.3 安装rknntoolkitlite2软件包 1.4 安装opencv 前言 RKNN Toolkit Lite2 工具支持运行在 RK3568: Debian10/Debian11(aarch64)、Ubuntu20/22(…

Java分布式架构知识体系及知识体系图

Java分布式架构整体知识体系是一个庞大而复杂的领域,它涵盖了多个方面,旨在帮助开发者构建高性能、高可用、可扩展的分布式系统。以下是对Java分布式架构整体知识体系的概述: 一、分布式理论基础 CAP理论: 一致性(Con…

线性代数 第五讲:线性方程组_齐次线性方程组_非齐次线性方程组_公共解同解方程组_详解

线性方程组 文章目录 线性方程组1.齐次线性方程组的求解1.1 核心要义1.2 基础解系与线性无关的解向量的个数1.3 计算使用举例 2. 非齐次线性方程的求解2.1 非齐次线性方程解的判定2.2 非齐次线性方程解的结构2.3 计算使用举例 3.公共解与同解3.1 两个方程组的公共解3.2 同解方程…

三(五)子棋实现

设计一个小游戏其实是对自己掌握一门编程语言的一个升华,几百行代码分项目进行这种很让人着迷的感觉哦! 与五子棋游戏其实本质区别只不过是判输赢的条件不同,这里我打算写写三子棋小游戏。 代码的最后我将所有源代码整理了,大家急…

物联网之MQTT

一,MQTT 及其在物联网中的应用 MQTT(Message Queuing Telemetry Transport)是一种轻量级的消息传输协议,设计用于低带宽、延迟高、不稳定的网络环境,特别适合物联网(IoT)应用。它采用了发布/订…

pet薄膜高速度视觉软件丝印应用

卷对卷生产的PET薄膜,以其优异的物理、化学性能及尺寸稳定性,在塑料薄膜行业中占据重要地位。它透明度高、光泽度好,强韧性出色,抗张强度和抗冲击强度远高于一般薄膜,且具有良好的耐热、耐寒、耐油和耐酸性。这些特性使…

(二)、软硬件全开源智能手表,可全面高精度采集生命体征数据,进行健康检测。(HealthyPi Move)

HealthyPi Move是一款开放式硬件设备,可让您高精度地跟踪所有生命体征。它不仅仅是另一款带有心率监测器的智能手表,它还是手腕上的完整生命体征监测和记录设备,可以测量心电图(ECG)、光电容积脉搏波 (PPG)、SpO₂、血压(基于手指)、EDA/GSR、…

scikit-learn:一个强大的机器学习Python库

我是东哥,一个热衷于用Python解决实际问题的技术爱好者。今天,我要和大家分享一个强大的机器学习库——scikit-learn。你是否曾经对机器学习充满好奇,却觉得它高深莫测?scikit-learn库将帮你轻松入门,让你在机器学习的…

《TSMaster开发从入门到精通》——创作者背后的故事...

背后的故事 由汽车行业畅销书作者杨金升老师牵头,同星智能研发团队和应用支持团队全力参与的《TSMaster开发从入门到精通》书籍已由清华大学出版社印付。此书一经上架,就获得汽车行业人士的一致认可和好评(京东自营100%好评率,并…

基于DPU与SmartNIC的K8s Service解决方案

1. 方案背景 1.1. Kubernetes Service介绍 Kubernetes Service是Kubernetes中的一个核心概念,它定义了一种抽象,用于表示一组提供相同功能的Pods(容器组)的逻辑集合,并提供了一种方式让这些Pods能够被系统内的其他组…

python-uinput虚拟输入

文章目录 python-uinput虚拟输入背景库简介:什么是python-uinput?安装指南:如何获取这个强大的工具?快速上手:五个核心函数的介绍与使用1. 创建虚拟设备2. 模拟键盘输入3. 模拟鼠标移动4. 模拟鼠标点击5. 模拟触摸屏操…

嵌入式全栈开发学习笔记---Linux系统编程(进程间通信)

目录 进程间通信概述 进程通信目的 进程间通信的发展 进程间通信分类 管道通信 无名管道 有名管道mkfifo() 信号 发送信号kill & raise 忽略信号signal() 发送信号alarm() 消息队列 消息队列使用的步骤 创建消息队列msgget() 读写消息队列msgrcv()/msgsnd()…

【C语言】十六进制、二进制、字节、位、指针、数组

【C语言】十六进制、二进制、字节、位 文章目录 [TOC](文章目录) 前言一、十六进制、二进制、字节、位二、变量、指针、指针变量三、数组与指针四、指针自加运算五、二维数组与指针六、指向指针的指针七、指针变量作为函数形参八、函数指针九、函数指针数组十、参考文献总结 前…

高经费打造的史诗级视觉盛宴,惊叹于每一帧的奢华

8月29日,备受期待的《指环王:力量之戒》第二季终于上线了。这一季一上架就放出了三集,立刻引发了影迷们的热烈讨论。 自从2022年首季首播以来,《指环王:力量之戒》就一直备受瞩目。尽管首季受到了不少争议,…

【C++ Primer Plus习题】9.4

问题: 解答: main.cpp #include <iostream> #include "sales.h" using namespace std; using namespace SALES;int main() {Sales s1, s2;double de[QUARTERS] { 12.1,32.1,42.1,51.1 };setSales(s1, de, QUARTERS);showSales(s1);cout << endl;setSal…