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

news2024/12/25 3:22:57

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

在这里插入图片描述

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ambari-hdp启动yarn报错Corruption: checksum mismatch

ambari-hdp启动yarn报错Corruption: checksum mismatch 页面报错 Traceback (most recent call last):File "/var/lib/ambari-agent/cache/stacks/HDP/3.0/services/YARN/package/scripts/nodemanager.py", line 102, in <module>Nodemanager().execute()Fil…

万字文档带你走进Python的世界

目录 Python基本使用语法 老生常谈 Python中的注释 Python变量 定义变量 变量类型 Python变量的特点 Python中的输入与输出 Python中的运算符 算术运算符 /和// **运算符 关系运算符 逻辑运算符 赋值运算符 Python运算符优先级 Python分支语句 if语句和if-else语句 if-else if-…

Java | Leetcode Java题解之第386题字典序排数

题目&#xff1a; 题解&#xff1a; class Solution {public List<Integer> lexicalOrder(int n) {List<Integer> ret new ArrayList<Integer>();int number 1;for (int i 0; i < n; i) {ret.add(number);if (number * 10 < n) {number * 10;} els…

Datawhale X 李宏毅苹果书 AI夏令营

文章目录 我认为苹果书是最好的深度学习原理教材 第三章开篇讲的就是为什么深度学习模型会优化失败&#xff0c;这个问题其它在我们训练深度学习模型的过程中是非常常见的一种现象&#xff1a;明明使用了更加深层的结构&#xff0c;但它的表现与之前一样&#xff0c;有时甚至不…

企业IT服务管理(ITSM)的实践与探索

随着信息技术的飞速发展&#xff0c;企业对IT服务管理&#xff08;ITSM&#xff09;的需求也日益增长。在这个背景下&#xff0c;某大型集团&#xff08;以下简称“该机构”&#xff09;逐步构建了完善的IT服务管理体系&#xff0c;其发展历程和实践经验对于广大运维团队而言&a…

OceanBase V4.2解析:如何用迭代器 Generator快速生成任意数据

前言 OceanBase 4.2 版本新增了迭代器 generator 函数。尽管这一功能在数据库领域中已属于通用能力&#xff0c;postgresql 也提供了类似的函数&#xff0c;然而&#xff0c;与MySQL和Oracle数据库在默认情况下是需要用户额外编写函数来实现的。OceanBase 4.2 的这一更新也是满…

鸿蒙(API 12 Beta6版)图形【AR物体摆放】 AR引擎服务

概要 本章节通过AR Engine识别设备周围的平面&#xff0c;并允许用户在平面上放置虚拟物体&#xff0c;实现虚拟和现实的融合。AR物体摆放可用于虚拟家具、数字展厅等应用&#xff0c;给用户提供虚实结合的新体验。通过本示例&#xff0c;您可以学习并掌握如何使用AR Engine开…

刷题记录(2)

1. HWOD机试 - 模拟消息队列(100) package com.yue.test;import org.junit.Test;import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; import java.util.List;/*** Author: 夜雨* Date: 2021-12-08-10:31* Description:* Version 1.0*/ public…

C#编译成32和64位的区别

C#编译成32和64位的区别 背景 C#32位客户端项目在把代码提交到客户端之后&#xff0c;jinkens直接崩掉了。原因是内存占用100%运维同学建议改成64位&#xff0c;理由是电脑内存大&#xff0c;客观条件IT不给扩。那么在同一台电脑上&#xff0c;32位和64位在编译过程中有什么区…

【DEV工具-IDEA】idea的光标变成黑块了?

项目场景&#xff1a; 解决&#xff1a;windows&#xff1a;按一下insert键。

Python获取次幂数据公众号榜单数据

公众号排行榜,wx公众号排行榜,原创排行榜,赞赏排行榜,评论排行榜 教程仅供参考,请勿滥用,由此带来的法律责任,需由自己承担。 一、运行效果 二、程序代码 #!/usr/bin/python # -*- coding: UTF-8 -*- """ @author: Roc-xb """import request…

Java学习第六天

Java进阶知识面向对象 static&#xff1a;是静态的意思&#xff0c;可以修饰成员变量&#xff0c;表示该成员变量在内存中只存储一份&#xff0c;可以被共享访问。 静态成员变量&#xff08;有static修饰&#xff0c;属于类&#xff0c;内存中加载一次&#xff09;&#xff1a…

三元里等你!融合三个经典模型!Transformer-LSTM-SVM多变量时间序列预测(Matlab)

三元里等你&#xff01;融合三个经典模型&#xff01;Transformer-LSTM-SVM多变量时间序列预测&#xff08;Matlab&#xff09; 目录 三元里等你&#xff01;融合三个经典模型&#xff01;Transformer-LSTM-SVM多变量时间序列预测&#xff08;Matlab&#xff09;效果一览基本介…

I2C总线的标准收发代码

结合I2C总线协议的知识&#xff0c;我们可以知道I2C写数据由一下10个步骤组成。 第一步&#xff0c;发送一个起始信号。 第二步&#xff0c;发送7bit从机地址&#xff0c;即OZ9350的地址。此处需要注意&#xff0c;发送数据时&#xff0c;无法发送7bit数据&#xff0c;此处发…

求和放大器(单位/非单位增益加法器+比例加法器)+运算放大器实现积分器和微分器

2024-9-2&#xff0c;星期一&#xff0c;22:00&#xff0c;天气&#xff1a;晴转雨&#xff0c;心情&#xff1a;晴。新的一周开始了&#xff0c;新的一个月又开始啦&#xff0c;希望大家开开心心&#xff0c;以崭新的面貌迎接中秋和十一假期&#xff01;废话不多说&#xff0c…

LinkAI工作流支持广场访问和api调用啦

什么是工作流 LinkAI工作流&#xff08;WorkFlow&#xff09;是一种灵活的智能体搭建方式。可以自由选择「大模型、应用、知识库、插件、意图识别、转人工、渠道消息发送」等多种原子能力&#xff0c;通过可视化拖拉拽的方式进行组合编排&#xff0c;零代码搭出一个业务流程。…

PPT制作加速器:3款工具插件的演示文稿制作更高效

IvyhTools英豪插件 IvyhTools是一款功能强大的PPT插件&#xff0c;主要用于辅助用户进行各种PPT编辑和处理操作。该插件具备以下主要功能&#xff1a; 字体编辑&#xff1a;用户可以对PPT中的字体进行编辑和调整。 动图录制&#xff1a;支持录制动态图像&#xff0c;方便用户在…

深度学习(四)-卷积神经网络

神经网络局限 不考虑数据形状 未考虑数据的“形状”&#xff0c;会破坏数据空间结构。例如&#xff0c;输入数据是图像时&#xff0c;图像通常是高长通道方向上的3维形状。但是&#xff0c;向全连接层输入时&#xff0c;需要将3维数据拉平为1维数据 参数庞大 全连接网络参数…

中小企业怎么选择MES:专用MES、集成MES和可配置MES

专用MES、集成MES和可配置MES是MES&#xff08;制造执行系统&#xff09;在不同发展阶段和应用场景下的三种主要形式。它们各自具有不同的特点和应用优势&#xff0c;下面将分别进行详细介绍。 专用MES 定义与特点&#xff1a; 专用MES是针对特定行业或特定生产流程而设计的…

CCS报错:error: cannot find file “libc.a“+CCS安装包

1、编译工程出现报错以下报错信息&#xff1a; error: cannot find file "libc.a" warning: automatic RTS selection: attempt to automatically link in index library "libc.a" failed; file not found warning: entry-point symbol "_c_int0…