![6ca6d34c959cf866fa9c6cd950edbd19.png](https://i-blog.csdnimg.cn/blog_migrate/4ec19b5122026a1d245b2e97b23a517d.jpeg)
有了上一章的多任务知识储备,本章就来在我们的操作系统上来实现多任务编程。
一、多任务管理器
对管理器,我们不在陌生。前面我们先后用到了三个管理器:内存管理器、图层管理器、定时器管理器。这些管理器功能的实现,在C语言里面全部都是通过结构体来达到目标的。所以,结构体是C语言编程非常重要的功能,我们通过前面的实践会发现:C语言结构体的重要意义就是用户通过自定义数据结构来实现某一种管理机制,如前面的内存管理、图层管理和定时器管理。
研究这些管理器的共同特点可以发现,通过它们就可以有效管理多重相似单元。所谓多重相似单元指的是管理对象具有同样的属性,但是数量上可以无限延伸,我们就需要对它们进行有效组织。比如:内存管理器就是管理一片一片的内存、图层管理器就是管理一层一层的图层、定时器管理器就是管理一个一个的定时器。
显然,多任务调度就是管理一个一个的任务。因此,我们这里就需要设计一个多任务管理器,同样需要用结构体来实现。要实现这样一个管理器,以下数据结构不可少:
1.TSS数据结构:每个任务的tss数据是一个复杂的逻辑CPU,所以需要定义一个结构体TSS32来存储。
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;
};
2.任务对象数据结构:每个任务是独立的单元,可以认为是一个任务对象,任务对象除了TSS数据不同以外,还需要一些辅助的信息数据,这些辅助信息包括:TSS描述符放在GDT表中的索引位置(可以立即为任务ID),可以用selector来表示。另外,任务在不同时段处于多种状态,比如准备就绪、休眠状态、正在运行等,可以用一个字段flags来表征。综上,任务对象就需要用如下结构体来实现:
struct TASK {
int sel, flags;
struct TSS32 tss;
};
3.任务对象数组:任务对象数据结构只是一个抽象的格式,当系统运行起来之后,每个任务对象就会一个个的实际值,所以需要把所有的任务对象(变量)全部存储起来,这样就需要定义一个放置所有任务对象的数组
struct TASK tasks0[MAX_TASKS]; /*存放所有的任务对象*/
4.任务对象指针:在每个任务对象数据中,有部分数据是静态的,一般初始化之后就不怎么变化,比如TSS数据、selector等,就算TSS里面的一些数据比如每个任务的EIP值肯定会随任务切换实时更新,但那都是CPU硬件自动完成的事情,站在用户层面我们一般是不会去更新TSS数据的。但是也有部分数据是动态变化的,而且需要我们编程进行控制,比如flags,它会在任务切换过程中不断的变换。为了能实现这部分动态数据的修改,我们需要定位每个任务对象,故要为每个任务对象的存放地址都定义一个指针,把这些指针全部都放在一个数组里面,就成了任务对象指针数组。在任务切换过程中,我们还不只是要动态修改flags,我们也会随时要读取任务对象里面的sel数据,通过它才能定位用jmp指令切换任务时,究竟应该跳转到GDT里面的哪个TSS描述符。所以,任务对象的定位---任务指针就太重要了。
struct TASK *tasks[MAX_TASKS]; /*任务对象指针数组*/
5.全局任务管理器:当具备以上4种的基础数据结构之后,操作系统最后就需要一个全局的多任务管理器,通过用它来全面控制和调度多任务切换。比如一共有多少个任务,当前是哪个认为在运行等。最终这个多任务管理器的结构体结构如下:
struct TASKCTL { /*多任务管理器*/
int running; /*正在运行的任务数量*/
int now; /*当前哪个任务在运行*/
struct TASK *tasks[MAX_TASKS]; /*每个任务对象需要一个指针*/
struct TASK tasks0[MAX_TASKS]; /*存放所有的任务对象*/
};
总结一下,我们定义的多任务管理器结构和逻辑如下:
![a6807a96c8ab2677438a3853979e6602.png](https://i-blog.csdnimg.cn/blog_migrate/1679b607367f7bf38fa55c6066638805.jpeg)
二、关键环节
1.新分配任务。task_alloc是新分配一个任务,核心语句就是:task = &taskctl->tasks0[i];将新分配的任务注册进任务对象数组tasks0[]。从此,这个TSS数据结构就专属于这个任务。
struct TASK *task_alloc(void)
{
int i;
struct TASK *task;
for (i = 0; i < MAX_TASKS; i++) {
if (taskctl->tasks0[i].flags == 0) {
task = &taskctl->tasks0[i];
task->flags = 1;
task->tss.eflags = 0x00000202; /* IF = 1; */
task->tss.eax = 0;
task->tss.ecx = 0;
task->tss.edx = 0;
task->tss.ebx = 0;
task->tss.ebp = 0;
task->tss.esi = 0;
task->tss.edi = 0;
task->tss.es = 0;
task->tss.ds = 0;
task->tss.fs = 0;
task->tss.gs = 0;
task->tss.ldtr = 0;
task->tss.iomap = 0x40000000;
return task;
}
}
return 0;
}
2.启用新任务。分配任务之后,需要启动该任务就用task_run函数,它将更新整个任务管理器的状态,并通过核心语句:taskctl->tasks[taskctl->running] = task,来把新分配任务对象的指针注册进任务对象指针数组tasks[]。那么以后凡是要操作该任务对象数据,就可以通过两级指针来实现:taskctl->tasks[i]->sel(flags)。
void task_run(struct TASK *task)
{
task->flags = 2;
taskctl->tasks[taskctl->running] = task;
taskctl->running++;
return;
}
3.休眠任务。我们定义的字段flags就是为了表征一个任务的多种状态,当有键盘、鼠标等外围中断类型的任务时,可以采用在缓存区数据没有输入的时候进行休眠,当检测到有数据输入的时候再来唤醒任务,而唤醒的任务则放在FIFO数据区put数据的时候触发。flags=0表示任务未分配、1表示任务休眠、2表示任务唤醒。
if (fifo32_status(&fifo) == 0)
{
task_sleep(task_a); /*任务休眠*/
io_sti();
}
else {
i = fifo32_get(&fifo);
io_sti();
....
三、任务优先级
用以上数据结构和方法已经可以实现多任务切换了,只不过这些任务之间地位平等。通常操作系统还需要对任务的优先级进行区分和管理:对实时性要求高的任务采用高优先级。所以,我们还必须优化多任务管理器,来支持不同的任务优先级管理和切换:
![672fe416cc3f079a4a9643493d696229.png](https://i-blog.csdnimg.cn/blog_migrate/10daf4650007895a6da1c8bf981ef611.png)
![9384a5f4ef8ba5df4eec5cb9038be430.png](https://i-blog.csdnimg.cn/blog_migrate/b1dd74968bd5e4acc748186b07b81ae4.png)
![38a14bf537be5b29a6996700f0154c5f.png](https://i-blog.csdnimg.cn/blog_migrate/fbbf33a86f1db2d65e75987782f824fa.png)
所以,还需要进一步优化多任务管理的数据结构:
1.任务对象数据结构
struct TASK { /*单个任务对象数据结构*/
int sel, flags; /*sel为GDT中对应任务的索引编号 */
int level, priority; /*优先级层数和层内优先级*/
struct TSS32 tss;
};
2.优先级层数数据结构
struct TASKLEVEL {
int running; /*该优先级层正在运行的任务数量*/
int now; /*当前运行的任务 */
struct TASK *tasks[MAX_TASKS_LV];
};
3.全局任务管理器(附优先级管理)
struct TASKCTL {
int now_lv; /*现在活动中的level*/
char lv_change; /*下次任务切换时是否需要改变level*/
struct TASKLEVEL level[MAX_TASKLEVELS];
struct TASK tasks0[MAX_TASKS];
};
最后,综合以上所有的方法,我们就可以编程来实现多个窗口的并发运行了:
![6ba17da61a0c79515139e6eed6243fab.png](https://i-blog.csdnimg.cn/blog_migrate/e8a442786603e331cdc61c68a7a7bb4e.jpeg)
这个多个"Windows"窗口界面的出现,就标志着:我们的操作系统已经完全实现了多任务切换控制管理。
四、结束语
多任务管理无疑是现代操作系统的核心,所以逻辑相对来说会更加的复杂。而且在优秀的操作系统中,多任务管理还会涉及众多的优化和算法。但是,万变不离其宗,无论怎样它都是建立在处理器支持多任务切换基础之上的,因此我们这里花费了大量的精力和篇幅来学习计算机多任务切换机制,后续再学习操作系统多任务管理就不会感觉到困难了。