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

2024-09-02 22:28

本文主要是介绍【读书笔记-《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)。如果此时中断处理还没完成,开启中断,可能会有下一个中断进来,这样就会导致程序出错。

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

这篇关于【读书笔记-《30天自制操作系统》-14】Day15的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1131236

相关文章

30常用 Maven 命令

Maven 是一个强大的项目管理和构建工具,它广泛用于 Java 项目的依赖管理、构建流程和插件集成。Maven 的命令行工具提供了大量的命令来帮助开发人员管理项目的生命周期、依赖和插件。以下是 常用 Maven 命令的使用场景及其详细解释。 1. mvn clean 使用场景:清理项目的生成目录,通常用于删除项目中自动生成的文件(如 target/ 目录)。共性规律:清理操作

业务中14个需要进行A/B测试的时刻[信息图]

在本指南中,我们将全面了解有关 A/B测试 的所有内容。 我们将介绍不同类型的A/B测试,如何有效地规划和启动测试,如何评估测试是否成功,您应该关注哪些指标,多年来我们发现的常见错误等等。 什么是A/B测试? A/B测试(有时称为“分割测试”)是一种实验类型,其中您创建两种或多种内容变体——如登录页面、电子邮件或广告——并将它们显示给不同的受众群体,以查看哪一种效果最好。 本质上,A/B测

2024网安周今日开幕,亚信安全亮相30城

2024年国家网络安全宣传周今天在广州拉开帷幕。今年网安周继续以“网络安全为人民,网络安全靠人民”为主题。2024年国家网络安全宣传周涵盖了1场开幕式、1场高峰论坛、5个重要活动、15场分论坛/座谈会/闭门会、6个主题日活动和网络安全“六进”活动。亚信安全出席2024年国家网络安全宣传周开幕式和主论坛,并将通过线下宣讲、创意科普、成果展示等多种形式,让广大民众看得懂、记得住安全知识,同时还

Linux操作系统 初识

在认识操作系统之前,我们首先来了解一下计算机的发展: 计算机的发展 世界上第一台计算机名叫埃尼阿克,诞生在1945年2月14日,用于军事用途。 后来因为计算机的优势和潜力巨大,计算机开始飞速发展,并产生了一个当时一直有效的定律:摩尔定律--当价格不变时,集成电路上可容纳的元器件的数目,约每隔18-24个月便会增加一倍,性能也将提升一倍。 那么相应的,计算机就会变得越来越快,越来越小型化。

PMP–一、二、三模–分类–14.敏捷–技巧–看板面板与燃尽图燃起图

文章目录 技巧一模14.敏捷--方法--看板(类似卡片)1、 [单选] 根据项目的特点,项目经理建议选择一种敏捷方法,该方法限制团队成员在任何给定时间执行的任务数。此方法还允许团队提高工作过程中问题和瓶颈的可见性。项目经理建议采用以下哪种方法? 易错14.敏捷--精益、敏捷、看板(类似卡片)--敏捷、精益和看板方法共同的重点在于交付价值、尊重人、减少浪费、透明化、适应变更以及持续改善等方面。

《C++标准库》读书笔记/第一天(C++新特性(1))

C++11新特性(1) 以auto完成类型自动推导 auto i=42; //以auto声明的变量,其类型会根据其初值被自动推倒出来,因此一定需要一个初始化操作; static auto a=0.19;//可以用额外限定符修饰 vector<string> v;  auto pos=v.begin();//如果类型很长或类型表达式复杂 auto很有用; auto l=[] (int

2021-8-14 react笔记-2 创建组件 基本用法

1、目录解析 public中的index.html为入口文件 src目录中文件很乱,先整理文件夹。 新建components 放组件 新建assets放资源   ->/images      ->/css 把乱的文件放进去  修改App.js 根组件和index.js入口文件中的引入路径 2、新建组件 在components文件夹中新建[Name].js文件 //组件名首字母大写

2021-08-14 react笔记-1 安装、环境搭建、创建项目

1、环境 1、安装nodejs 2.安装react脚手架工具 //  cnpm install -g create-react-app 全局安装 2、创建项目 create-react-app [项目名称] 3、运行项目 npm strat  //cd到项目文件夹    进入这个页面  代表运行成功  4、打包 npm run build

用Python实现时间序列模型实战——Day 14: 向量自回归模型 (VAR) 与向量误差修正模型 (VECM)

一、学习内容 1. 向量自回归模型 (VAR) 的基本概念与应用 向量自回归模型 (VAR) 是多元时间序列分析中的一种模型,用于捕捉多个变量之间的相互依赖关系。与单变量自回归模型不同,VAR 模型将多个时间序列作为向量输入,同时对这些变量进行回归分析。 VAR 模型的一般形式为: 其中: ​ 是时间  的变量向量。 是常数向量。​ 是每个时间滞后的回归系数矩阵。​ 是误差项向量,假

c++习题30-求10000以内N的阶乘

目录 一,题目  二,思路 三,代码    一,题目  描述 求10000以内n的阶乘。 输入描述 只有一行输入,整数n(0≤n≤10000)。 输出描述 一行,即n!的值。 用例输入 1  4 用例输出 1  24   二,思路 n    n!           0    1 1    1*1=1 2    1*2=2 3    2*3=6 4