《30天自制操作系统》笔记(12)——多任务入门

《30天自制操作系统》笔记(12)——多任务入门

进度回顾

上一篇介绍了设置显示器高分辨率的方法。本篇讲一下操作系统实现多任务的方法。

什么是多任务

对程序员来说,也许这是废话,不过还是说清楚比较好。

多任务就是让电脑同时运行多个程序(如一边写代码一边听音乐一边下载电影)。

电脑的CPU只有固定有限的那么一个或几个,不可能真的同时运行多个程序。所以就用近似的方式,让多个程序轮换着运行。当轮换速度够快(0.01秒),给人的感觉就是"同时"运行了。

多任务之不实用版

我们首先从最基本的想法开始,做一个不实用版的多任务作为例子。在学习这个例子的过程中引入真正的多任务必须的TSS、TR、far模式JMP的概念,为后续内容打基础。

当你向CPU发出任务切换的指令时,CPU会先把寄存器中的值全部写入内存某处;然后,从内存另一位置把所有寄存器的值读取出来。这就完成了一次任务切换。

任务切换消耗的时间就是读写内存消耗的时间,大概为0.0001秒。

任务状态段TSS

存取全部寄存器的值这件事,当然需要有一个数据结构,这就是"任务状态段"(Task Status Segment)简称TSS。

1 struct TSS32
2 {
3     int backlink, esp0, ss0, esp1, ss1, esp2, ss2, cr3;
4     int eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi;
5     int es, cs, ss, ds, fs, gs;
6     int ldtr, iomap;
7 };

TSS32中第一行(从backlink到cr3)暂时不用理会。

第二、三行(从eip到gs)都是寄存器。其中EIP是CPU用来记录下一条需要执行的指令位于内存中的地址的寄存器,因此被称为"指令指针"。实际上JMP指令就是修改了EIP的值。

第四行也不用理会。

TSS中的信息会存储到内存某处(记为X),而X的地址会注册到GDT中。(不知道什么是GDT?请查看这里

寄存器TR

寄存器TR是作用是让CPU记住当前在运行哪个任务。其存储的值是"当前任务所在的段号*8"。只需在操作系统启动时对其赋值一次,以后进行任务切换时,CPU会自动调整TR的值。给TR赋值只能用汇编实现。

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

LTR指令只是改变TR的值,不会发生任务切换。所以我感觉TR像是一个标识变量。正是由于这一点我才有了后文的猜想。

切换任务就是执行JMP指令

JMP指令分两种,即"只改写EIP的near模式"与"同时改写EIP和CS的far模式"。CS是代码段寄存器(code segment)。

平时使用的都是near模式。

在asmhead.nas中跳转到bootpack.c中的主函数用的是far模式,即

1 JMP DWORD 2 * 8: 0x0000001b

这条指令在向EIP写入0x1b时,也向CS写入2*8(即16)。

像这样在JMP目标地址中带冒号(:)的,就是far模式。

切换任务时,我们使用far模式的JMP指令。

CPU执行far模式的JMP指令前,会根据GDT中注册的TSS情况,判断JMP的目标地址是可执行代码还是TSS。如果是可执行代码,那么CPU就认为这只是一个普通的far模式的JMP;如果是TSS,则认为这是一个任务切换指令,会切换到目标地址指定的TSS所记录的任务中,也就是JMP到另一个任务那里去了。

所以普通的far模式的JMP和任务切换的JMP指令,其机器码是同一个。

Demo:两个任务切换

我们把操作系统启动时运行的程序记作任务A,即如下代码。

 1 void HariMain(void)
 2 {
 3     /* 略 */
 4     timer_ts = timer_alloc();
 5     timer_init(timer_ts, &fifo, 2);
 6     timer_settime(timer_ts, 2);
 7     /* 略 */
 8     for (;;) {
 9         io_cli();
10         if (fifo32_status(&fifo) == 0) {
11             io_stihlt();
12         } else {
13             i = fifo32_get(&fifo);
14             io_sti();
15             if (i == 2) {
16                 farjmp(0, 4 * 8);
17                 timer_settime(timer_ts, 2);
18             } else if (256 <= i && i <= 511) { /* 键盘数据 */
19                 /* 略 */
20             } else if (512 <= i && i <= 767) { /* 鼠标数据 */
21                 /* 略 */
22             } else if (i == 10) { /* 10秒计时器 */
23                 putfonts8_asc_sht(sht_back, 0, 64, COL8_FFFFFF, COL8_008484, "10[sec]", 7);
24             } else if (i == 3) { /* 3秒计时器 */
25                 putfonts8_asc_sht(sht_back, 0, 80, COL8_FFFFFF, COL8_008484, "3[sec]", 6);
26             } else if (i <= 1) { /* 光标用计时器 */
27                 /* 略 */
28             }
29         }
30     }
31 }

任务A:操作系统启动时程序

下面是任务B执行的函数。

 1 void task_b_main(void)
 2 {
 3     struct FIFO32 fifo;
 4     struct TIMER *timer_ts;
 5     int i, fifobuf[128];
 6
 7     fifo32_init(&fifo, 128, fifobuf);
 8     timer_ts = timer_alloc();
 9     timer_init(timer_ts, &fifo, 1);
10     timer_settime(timer_ts, 2);
11
12     for (;;) {
13         io_cli();
14         if (fifo32_status(&fifo) == 0) {
15             io_sti();
16             io_hlt();
17         } else {
18             i = fifo32_get(&fifo);
19             io_sti();
20             if (i == 1) { /* 任务切换 */
21                 farjmp(0, 3 * 8);
22                 timer_settime(timer_ts, 2);
23             }
24         }
25     }
26 }

任务B

任务A执行0.02秒后就进入farjmp(0, 4 * 8);,从而自行切换到任务B,任务B执行0.02秒后就进入farjmp(0, 3 * 8);,从而自行切换到任务A。周而复始。

像这种在应用代码中编写任务切换的方式,明显不实用。不过用于研究多任务还是很方便的。

多任务截图没有意义,就此作罢。

真正的多任务

大体上说,实现多任务的方法就是利用前面提到的定时器PIT(Programmable Interval Timer)能够定时产生中断的功能,在其中断处理函数中实现任务切换的目的。

时间片轮转调度算法是一种最基本的任务调度算法,它让每个任务依次执行相同的一段时间(如0.01秒)。在此基础上,可以为任务添加"休眠"、"优先级"等功能和属性,根据属性值调整执行时间和执行顺序。

 1 void inthandler20(int *esp)
 2 {
 3     /* 略 */
 4     char ts = 0;
 5     for (;;) {
 6         /* timers的计时器全部在工作中,因此不用确认flags */
 7         if (timer->timeout > timerctl.count) {
 8             break;
 9         }
10         /* 超时 */
11         timer->flags = TIMER_FLAGS_ALLOC;
12         if (timer != mt_timer) {
13             fifo32_put(timer->fifo, timer->data);
14         } else {
15             ts = 1; /* mt_timer超时 */
16         }
17         timer = timer->next; /* 将下一个计时器的地址赋给timer */
18     }
19     timerctl.t0 = timer;
20     timerctl.next = timer->timeout;
21     if (ts != 0) {
22         mt_taskswitch();
23     }
24     return;
25 }
26 void mt_taskswitch(void)
27 {/* demo演示只有两个固定任务的切换过程 */
28     if (mt_tr == 3 * 8) {
29         mt_tr = 4 * 8;
30     } else {
31         mt_tr = 3 * 8;
32     }
33     timer_settime(mt_timer, 2);
34     farjmp(0, mt_tr);
35     return;
36 }

为了简化非核心代码,我用demo版的mt_taskswitch(void)代替了有复杂数据结构的真实版本,这样方便理解整个代码的原理。其中的farjmp是用汇编实现的。

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

根据C语言编译器的规则,调用这个farjmp函数时,在[ESP+4]处存放了EIP的值,在 [ESP+8]处存放了CS的值。给eip赋值0,给cs赋值要切换到的任务所在的段号(乘8),就可以正确调用farjmp。

一般发生JMP后,不会执行后面的RET指令了。但是,执行任务切换的JMP后,再返回这个任务的时候,程序会从JMP指令之后的地方恢复运行,也就是JMP后面这个RET指令会被执行。因此这里的RET必不可少。

任务切换的时机

我提出一个问题,任务切换是在farjmp中执行JMP FAR [ESP+4]时发生的吗?

从不实用版的代码看来,答案应该是"是"。因为确实在任务A执行了farjmp中的JMP FAR [ESP+4]指令后切换到了任务B,之后切换回任务A时,又从JMP FAR [ESP+4]指令后面的RET指令开始执行了。

但是在真正的多任务中,CPU调用中断处理函数,在inthandler20中执行了farjmp中的JMP FAR [ESP+4]。如果答案是"是",那么此时就会从中断处理函数中切换到另一个任务A中了。

可是,还会不会切换回中断处理函数inthandler20呢?

如果会,那么中断处理函数不就也成了一个任务吗?

如果会,那么中断处理函数函还没有return不就中断了吗?此时再来一个中断的话,会怎么样呢?栈就乱套了。

如果不会,那么中断处理函数最前面用汇编写的PUSH各种寄存器的指令就没有相应的POP指令了呀,时间一长栈就溢出了呀。这明显不对。

所以,我猜想只有一种情况是可行的。那就是:farjmp中的JMP FAR [ESP+4]指令并没有完成任务切换,它只是让CPU记录了一个标识(比如上文的寄存器TR的作用),标识应该运行的任务是X。然后,当CPU完成中断处理函数,再次执行某个任务A中的指令时,它会发现"现在应该执行任务X"中的指令了,所以它就切换到任务X中去。任务切换实际上此时才完成。

我查了一些资料,只能暂时作此猜想。

总结

操作系统利用CPU的far模式的JMP指令、寄存器TR、GDT、TSS和PIT中断这些功能实现了多任务,可见CPU在设计时就考虑到了计算机要具有多任务处理的能力。也就是说,CPU、PIC等硬件支持什么功能,操作系统才能实现什么功能。这又肯定了硬件为操作系统提供API的看法。

本篇完成了基本的多任务功能,下一篇就对此进行优化。

《30天自制操作系统》笔记(12)——多任务入门,布布扣,bubuko.com

时间: 2024-10-05 04:40:47

《30天自制操作系统》笔记(12)——多任务入门的相关文章

30天自制操作系统笔记(第四天)

这一节讲的最出彩的地方是c语言的地址. 而要理清c语言地址,又必须追根溯源,看看汇编里内存地址的使用. MOV AL,0X15 MOV [1024],AL MOV BYTE[1024],0X15 这两种指令效果相同,都是在这个内存地址里存入一个数据,而学过汇编的我们知道,直接往内存某地址存入数据时,要说明填入的数据大小,或者说数据类型,不然机器不知道怎么填入该数据,到底是按照8位填入,还是十六位填入.因此,这个byte必不可少,而前面的指令,由于AL已经明确了是八位,因此不用说明. 好了,接下来

《30天自制操作系统》笔记(04)——显示器256色

<30天自制操作系统>笔记(04)--显示器256色 进度回顾 从最开始的(01)篇到上一篇为止,已经解决了开发环境问题和OS项目的顶层设计问题. 本篇做一个小练习:设置显卡显示256色. 原理 设置显卡模式 调用BIOS中断命令INT 0x10,设置显卡模式为VGA图形模式,320*200*8位彩色模式,调色板模式.代码如下. 1 MOV AL,0x13 ; VGA图形模式,320*200*8位彩色模式 2 MOV AH,0x00 3 INT 0x10 设置调色板 256色的调色板是这样一个

《30天自制操作系统》读书笔记(2)hello, world

让系统跑起来 要写一个操作系统,我们首先要有一个储存系统的介质,原版书似乎是06年出版的,可惜那时候没有电脑,没想到作者用的还是软盘,现在的电脑谁有软驱?不得已我使用一张128M的SD卡来代替,而事实上你用的是U盘还是软盘对我们的操作系统没有影响,缺点是你的U盘刷入系统后容量只能是1440 MB,即当年流行的3.5英寸软盘的大小,当然不用担心,再格式化一次(用DiskGeniu),就可以恢复. 我做事情的话,总是怕自己的努力的结果白费了,害怕辛辛苦苦看完这本书但是发现做出来的东西现在根本没法用,

《30天自制操作系统》读书笔记(4) 绘图

暑假果然是滋生懒散的温床. (╯‵□′)╯︵┻━┻ 好久不动都忘记之前做到哪里了, 上次好像做到了C语言的引入, 这一节所做的东西都相当轻松, 将会绘制出操作系统的基本界面. 绘图的原理 按照书中所说, 将值写入到显存中就能在屏幕上显示相应的像素, 在asmhead.nas 中有这一段: 1 CYLS EQU 0x0ff0 ; 设定启动区 2 LEDS EQU 0x0ff1 3 VMODE EQU 0x0ff2 ; 关于颜色数目的信息,颜色的位数 4 SCRNX EQU 0x0ff4 ; 分辨率

《30天自制操作系统》笔记(08)——叠加窗口刷新

<30天自制操作系统>笔记(08)--叠加窗口刷新 进度回顾 上一篇中介绍了内存管理的思路和算法,我们已经可以动态申请和释放内存了.这不就是堆(Heap)么.在此基础上,本篇要做一段程序,一并解决窗口和鼠标的叠加处理问题. 问题 在之前的<<30天自制操作系统>笔记(05)--启用鼠标键盘>篇,已经能够移动鼠标了.但是遗留了如下图所示的一个小问题. 我们希望的情形是这样的: 实际上,当前版本的OS还没有窗口图层的东西.本篇要做一段程序,一并解决窗口和鼠标的叠加处理问题.

《30天自制操作系统》笔记(01)——hello bitzhuwei’s OS!

<30天自制操作系统>笔记(01)--hello bitzhuwei's OS! 最初的OS代码 1 ; hello-os 2 ; TAB=4 3 4 ORG 0x7c00 ; 指明程序的装载地址 5 6 ; 以下这段是标准FAT32格式软盘专用的代码 7 8 JMP entry 9 DB 0x90 10 DB "HELLOIPL" ; freeparam 启动区的名称可以是任意的字符串(8字节) 11 DW 512 ; 每个扇区(sector)的大小(必须为512字节)

《30天自制操作系统》笔记(06)——CPU的32位模式

<30天自制操作系统>笔记(06)--CPU的32位模式 进度回顾 上一篇中实现了启用鼠标.键盘的功能.屏幕上会显示出用户按键.点击鼠标的情况.这是通过设置硬件的中断函数实现的,可以说硬件本身的设计就具有事件驱动的性质,所以软件层面上才有基于事件的消息机制. 但上一篇没有说明中断的来龙去脉,本篇就从头到尾描述一下CPU与此相关的设置问题. Segment 32位的CPU使用32条地址线,能区分232=4G个内存地址.每个内存地址都有1Byte的内容. 分段,就是将4GB的内存分成很多块(blo

《30天自制操作系统》笔记(02)——导入C语言

<30天自制操作系统>笔记(02)--导入C语言 进度回顾 在上一篇,记录了计算机开机时加载IPL程序(initial program loader,一个nas汇编程序)的情况,包括IPL代码(helloos.nas).编译生成helloos.img文件.用虚拟机QEMU加载helloos.img.制作U盘启动盘和用物理机加载helloos.img. 计算机启动时会自动加载和执行IPL程序,但IPL程序只能占用512字节.若直接用IPL写OS,空间不够用.所以IPL程序一般用于将真正的OS程序

《30天自制操作系统》读书笔记(3) 引入C语言

这一次的学习相当曲折, 主要是因为粗心, Makefile里面的错误导致了文件生成出现各种奇奇怪怪的问题, 弄得心力交瘁, 因此制作过程还是尽量按着作者的路子来吧. 作者提供的源码的注释在中文系统下是乱码, 而且代码的分隔用了两个Tab, 在这里要处理一下: :%s/;.*//g 删除所有的注释; :%s/\t\t/\t 把两个Tab替换为一个Tab; 要让作者的nas文件和asm文件拥有相同的语法规则, 在_vimrc文件的最后一行添加 au BufNewFile,BufRead *.nas