任务切换

多任务(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]            ; trRET_taskswitch4:    ; void taskswitch4(void);JMP        4*8:0RET

我们将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;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;

eip中定义在切换到这个任务时,需要从哪开始执行。

task_b_esp是专门为任务B所定义的栈。

最后给CS寄存器设置为GDT的2号,其他寄存器都设置为GDT的1号,这里使用了和bootpack.c相同的地址段,当然用其他地址段也可以。

bootpack.c节选:

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

HariMain节选:

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

这里我们为任务B的栈分配了64KB的内存,并计算出栈底的内存地址。

费了不少功夫呀,我们来运行一下make run,在窗口里输值,输到一半就停住了,好了任务切换完成~

任务切换改进

现在只实现了从任务A切换到任务B,接下来我们尝试从任务B切回到任务A。我们设定切到任务B的5秒后,切回任务A。

改写一下task_b_main函数,bootpack.c节选:

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) { /* 超时时间为5秒 */taskswitch3(); /* 返回任务A */}}}
}

接着创建taskswitch3,naskfunc.nas节选:

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

这里测试一下,经过10s后光标停止闪烁,鼠标和键盘没有反应,过了5s,光标又重新闪烁,又可以从键盘输入值了。

简单的多任务

接下来我们要实现多个任务快速的来回切换,让多个任务看起来像同时运行一样。之前A、B两个任务都分别写了一个函数,这种的写法不太好,比如有100个任务,就要写100个函数了。这肯定不是我们希望的,为了解决这个问题,我们来创建一个通用的函数:

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

“JMP FAR”指令的功能是执行far跳转,在这个指令中指定一个内存地址,CPU会从指定的内存地址中读取4个字节存入EIP寄存器,再继续读2个字节存入CS寄存器。

调用这个函数时,比如farjmp(eip,cs);其中[ESP+4]的位置存放了eip的值,[ESP+8]的位置存放了cs的值,这样就可以实现跳转了。

我们将需要调用的部分改写为:

现在我们来缩短切换的间隔,我们在任务A和任务B中分别准备一个timer_ts变量,每隔0.02s执行一次任务切换。为了能直观看出切换是否成功,我们让任务B来记个数吧。

void HariMain(void)
{(略)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);} else if (256 <= i && i <= 511) { /* 键盘数据 */(略)} else if (512 <= i && i <= 767) { /* 鼠标数据 */(略)} else if (i == 10) { /* 10秒计时器 */putfonts8_asc_sht(sht_back, 0, 64, COL8_FFFFFF, COL8_008484, "10[sec]", 7);} else if (i == 3) { /* 3秒计时器 */putfonts8_asc_sht(sht_back, 0, 80, COL8_FFFFFF, COL8_008484, "3[sec]", 6);} else if (i <= 1) { /* 光标用计时器 */(略)}}}
}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);}}}
}

到这里我们来运行一下——现在计数器在计时的同时,也可以通过键盘在窗口输入信息。看起来两者是同时运行,实际上是任务切换的很快,我们看不出来。

真正的多任务

这里有同学要说了,我们现在做的多任务,都是在HariMain和task_b_main里写入任务切换的代码实现的,不能算真正的多任务。真正的多任务,应该是在程序本身不知道的情况下进行任务切换的。

那接下来我们就来添加多任务吧,先来创建一下mt_int函数:

mtask.c节选:

struct TIMER *mt_timer;
int mt_tr;void mt_init(void)
{mt_timer = timer_alloc();/* 这里没有必要使用timer_init */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_int函数的作用是初始化mt_timer和mt_tr,并将计时器设置为0.02s之后。mt_tr表示TR寄存器,不需要使用timer_init是因为发生超时时不需要向FIFO缓冲区写入数据。

mt_taskswitch函数的作用是按照当前的mt_tr变量的值,计算出下一个mt_tr的值,并将计时器重新设置为0.02s之后并进行任务切换。

接下来我们改造一下inthandler20,如果产生超时的计时器是mt_time的话,不向FIFO写入数据,只将ts置为1;最后判断如果ts的值不为0,就调用mt_taskswitch进行任务切换:

timer.c节选:

void inthandler20(int *esp)
{char ts = 0;(略)for (;;) {/* timers的计时器全部在工作中,因此不用确认flags/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; /* 将下一个计时器的地址赋给timer */}timerctl.t0 = timer;timerctl.next = timer->timeout;if (ts != 0) {mt_taskswitch();}return;
}

之后在HariMain函数中,把任务切换部分删掉就可以了。

现在make run一下——运行成功,看起来和之前没什么差别。不过目前的程序,就算在程序中不进行任务切换,也会实现任务切换的。

好了今天的任务就到这了,明天继续哈~。

https://gitee.com/mint1993/myos.git

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

  1. 30天自制操作系统:第十天 叠加处理

    1 内存管理(续)(harib07a) memman_alloc 和 memman_free 函数能够以1字节为单位进行内存管理,这种方式虽然不错,但是有一点不足:在反复进行内存分配和内存释放之后,内 ...

  2. 30天自制操作系统第五天

    操作系统实验日志5 第5天:结构体.文字显示与GDT/IDT初始化 30天自制操作系统第五天 操作系统实验日志5 一.实验主要内容 1. 内容1:接收启动信息 2. 内容2:使用结构体 3. 内容3: ...

  3. 30天自制操作系统——第五天

    第五天 参考<30天自制操作系统>GDT&IDT - 谷月轩 - 博客 梳理一下文件 现在我们拥有这么9个文件: ipl10.nas InitialProgramLoader, 占 ...

  4. 由《30天自制操作系统》引发的漫画创作

    大家可还记得<30天自制操作系统>的封面上的那只猫吗?记得当时,在果壳网有人问,为何这只猫长了两只尾巴呢,延着这条线,我把这本书捧上了展示的舞台.事隔四个多月,我又重提此书. 这本经我手宣 ...

  5. 30天自制操作系统——第二十三天窗口操作

    窗口及输入切换 我们先来实现用键盘切换窗口,按下F11键,将最下面的窗口移动到最上面,这里F11按键的编码为0x57. bootpack.c节选: void HariMain(void) {(略)fo ...

  6. 30天自制操作系统-初体验

    最近在图书馆翻阅关于操作系统的书籍,看到川和秀实的自制操作系统决定也动手尝试一下,这本书书名就叫做30天自制操作系统.首先还是附上光盘镜像的获取地址吧.30天自制操作系统光盘镜像ISO完整版下载 - ...

  7. 《30天自制操作系统》第9天

    第九天 内存管理 1.整理源文件 这一节只是进行了代码整理,把鼠标键盘相关的内容转移到了特定的文件里. 2.内存容量检查(1) 要做内存管理,首先得知道内存的容量,怎么知道内存的容量呢?BIOS可以告 ...

  8. 30天自制操作系统U盘启动

    最近从某宝上看了一本书,叫<30天自制操作系统>,上网翻了一下pdf,感觉还可以,唯一的美中不足就是软盘启动,这2018年了,谁家还有软盘软驱啊?!于是翻了一下网上的资料,发现没什么可以用 ...

  9. 为什么《30天自制操作系统》封面中的猫是两只尾巴

    刚刚在一社区,发了一贴,被指出一问题,询一高人,得一答案.这便是我没有关注到的封面上的那只猫,我想这也是很多读者没有关注到的.因为在我微博的200转发贴中,并没有人提到封面中的猫为何有两只尾巴.于是咨 ...

最新文章

  1. 推荐8个私藏已久的实用网站,每一款都能带来惊喜!
  2. shell--局部变量
  3. StudentManager-java+mysql学生管理系统
  4. 无忧PHP企业网站内容管理系统源码v2.8 标准版
  5. 白板推导系列Pytorch-逻辑回归
  6. Java 实战篇-JDK9新特性
  7. 旋转式光电编码器的设计动向
  8. [Scala]正则表达式——去除特殊字符,只保留中英文和数字以及下划线
  9. 决策树(手写代码+隐形眼镜项目)
  10. 只要5分钟!学会自己打造多系统合集的DVD安装光盘
  11. 苹果关闭 iOS 14.4.2 系统验证通道
  12. 让IE6 IE7 IE8 IE9 IE10 IE11支持Bootstrap的解决方法
  13. laravel voyager 安装
  14. 鼠标之父:恩格尔巴特于2013年7月3日去世
  15. js获取url后的参数
  16. 着色器(Shader)之像素着色器
  17. 机器学习识别系统的度量指标
  18. 凹透镜类毕业论文文献包含哪些?
  19. 共享单车算不算共享经济
  20. 故障模块名称kernelbase.dll_2020广东松下200W马达回收回收价格公道西门子模块-聚力...

热门文章

  1. 华三H3C设备 公司网络外网变慢分析
  2. 微凉大大,教你一步一步在linux中正确的安装Xcache加速php。
  3. SQL Server 维护计划(数据库备份)
  4. python基础学习2020.6.23-条件、循环和其他语句
  5. 2020年浙江理工大学新生赛 D DD_BOND看到的hcy
  6. B端与C端产品有何不同?
  7. DOS命令操作大全和计算机运行命令(初次写请多多关照)
  8. 《自动驾驶行业交流(微信)群》及公约
  9. 你是弱者,又有什么了不起
  10. Mac OS 安装PHP7