Open ustcanycall opened 11 years ago
多任务"multitask",简单来说就是在操作系统中,多个应用程序同时运行的状态。如果我们的电脑装了多个CPU,那么运行多个程序倒也是顺理成章的,但实际上,只有一个CPU,操作系统照样可以实现多任务。 其实说穿了,这些程序根本没有在同时运行,只不过看上去好像在同时运行一样:程序A运行一会儿,接下来程序B运行一会儿,再接下来轮到程序C,然后再回到程序A.......如此反复。如果切换速度非常快(几个毫秒切换一次),那么在人看来多个程序就是同时运行的。
首先,每个任务创建时,都会在内存为任务分配一段空间。该空间来储存任务的运行状态信息。 当CPU收到任务切换指令时,CPU会先把寄存器中的值全部写入内存,这样做的目的是为了当以后切换回这个任务时,可以从中断的地方继续运行。接下来,为了运行下一个程序,CPU会把下一个程序所有寄存器的值从对应的内存地址中读取出来,这样就完成了一次任务切换。
接下来我们将用最简单的方式实现两个任务的切换。为了简化模型,我们做了如下的假设:
main()
tss_b_main()
用C语言实现两个任务切换涉及到以下几个方面:
任务状态段(task status segment),简称TSS,是指在操作系统进程管理中,任务切换时的任务现场信息。TSS的格式如下图所示:
因此我们可以定义如下的结构体:
struct TSS32{ int backlink,esp0,ss0,esp1,ss1,esp2,ss2,cr3; int eip,eflags,edx,ecx,edx,ebx,esp,ebp,esi,edi; int es,cs,ss,ds,fs,gs; int ldtr,iomap; };
TSS共包括26个int成员,总计104个字节,按照作用可以分为四个部分。 第一行从backlink到cr3为止的几个成员,保存任务设置相关的信息。其中backlink保存前一个任务TSS描述符的选择子,esp0到ss2这六个成员与任务的特权级有关,cr3跟分页机制相关。 第二行的成员是32位的寄存器。 第三行的成员是16位的段寄存器。 第四行和第一行一样,是任务设置相关部分。ldtr为局部描述符表寄存器,I/Omap为I/O许可位图。
struct TSS32 tss_a,tss_b;
TSS是内存段的一种,因此需要在GDT(GlobalDescriptorTable)中进行定义后再使用。 GDT是全局描述描述符表,是操作系统采用分段机制的一种体现。GDT由8192个段描述符所组成,每一个段描述符记录了段基址,_段限长_以及段类型。将TSS注册到GDT中就可以为TSS分配内存地址。我们将任务A注册到GDT的第3号段,将任务B注册到GDT的第4号段。
struct SEGMENT_DESCRIPOT *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT; set_segmendesc(gdt+3,103,(int)&tss_a,AR_TSS32); set_segmendesc(gdt+4,103,(int)&tss_b,AR_TSS32);
#define AR_TSS32 0x0089 /*段类型*/ #define ADR_GDT 0x00270000 /*GDT的基址*/ struct SEGMENT_DESCRIPTOR{ /*段描述符的结构体,limit_low+limit_hight形成段限长,base_low+base_middle+base_high形成段基址, access_right表示段类型*/ short limit_low,base_low; char base_mid,access_right; char limit_high,base_high; }; void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd,unsigned int limit, int base,int ar){ /*函数功能为将段描述符注册到GDT中,参数1为需要注册的段描述符,参数2为段限长, 参数3为段基址,参数为段类型*/ if(limit>0xfffff){ /*如果段限长超过1M,则采用分页机制*/ ar|=0x8000; /*G_bit>1*/ limit/=0x1000; } sd->limit_low=limit&0xffff; sd->base_low=base&0xffff; sd->base_mid=(base>>16)&0xff; sd->access_right=ar&0xff; sd->limit_high=((limit>>16)&0x0f)|((ar>>8)&0xf0); sd->base_high=(base>>24)&0xff; return; }
由于任务切换是一种段与段之间的跳转,需要用汇编的JMP FAR指令来实现。很遗憾,C语言对于JMP FAR无能为力,因此只能依靠汇编来实现。汇编代码如下:
GLOBAL _farjmp _farjmp: ;void farjmp(int eip,int cs); JMP FAR [ESP+4] RET
那么由任务3切换到任务4可以写farjmp(0,4<<3); 由任务4切换到任务3可以写成farjmp(0,3<<3);
farjmp(0,4<<3);
farjmp(0,3<<3);
farjmp(eip,cs)
最终CS与EIP的值被更新,这样就实现了FAR JMP。
为了能够让任务自动进行切换,可以考虑用定时器来实现。定时器是定时中断的一种应用,我们可以考虑每过1ms发生一次定时中断,让任务进行切换。 由于篇幅的限制,定时器的实现部分我就不再本篇中进行赘述。
至此,有关如何用C语言实现两个任务的切换已经介绍完毕。由于任务切换涉及到GDT以及定时器的概念,如果大家有编写GDT和定时器的基础,那么我相信任务切换其实并难不倒大家。如果大家对GDT以及定时器的实现有疑问,没有关系,我会在后续中慢慢介绍。
在上述的介绍中,我们已经实现了两个任务的互相切换,但是这样还不够完善。如果我们要运行三个或者三个以上的任务,管理起来就会非常的麻烦。为了方便管理,我们对结构体进行如下的扩充。
#define MAX_TASKS 1000 #define TASK_GDT0 3 struct TSS32{ int backling,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; }; struct TASK{ int sel; /*sel用来存放GDT编号*/ int flags; /*用来记录TASK的状态:未使用,休眠中,正在运行*/ struct TSS32 tss; }; struct TASKCTL{ int running; /*正在运行的任务数量*/ int now; /*记录正在运行的是哪个任务*/ struct TASK* tasks[MAX_TASKS]; struct TASK tasks0[MAX_TASKS]; };
上述三个结构体的关系如下: 1.struct TSS32为任务状态段,用来描述任务的基本信息。 2.struct TASK定义为一个任务,在struct TSS32的基础上添加了两条额外的信息。 3.struct TASKCTL定义为一个任务管理器,用来管理多个任务。 接下来我们定义如下四个函数用来帮助我们管理信息:
struct TSS32
struct TASK
struct TASKCTL
struct TASK* task_init(struct MEMMAN *memman); /*初始化任务管理器*/ struct TASK* task_alloc(void); /*创建一个新的任务*/ void task_switch(void); /*任务自动切换程序,用定时器实现*/ void task_run(struct TASK *task); /*运行某个程序*/ void task_sleep(struct TASK *task); /*让某个程序休眠*/
struct MEMMAN
在操作系统中有一些处理,即使牺牲其他任务的性能也必须完成,否则会引起用户的不满。比如对鼠标的处理,键盘处理,网络处理,音乐播放器等等。 如果要用C语言实现优先级其实并不是非常的困难,只需要在struct TASK这个结构体中加入int priority这个变量即可。
int priority
struct TASK{ int sel; /*sel用来存放GDT编号*/ int flags; /*用来记录TASK的状态:未使用,休眠中,正在运行*/ int priority; /*设置任务的优先级*/ struct TSS32 tss; };
那么如何具体实现优先级呢?其实只需要控制每一个任务的定时器即可。优先级高的任务,定时器的时间也就越紧凑,CPU在单位时间内将会更多的执行该任务。
按照刚才的设定,优先级相同的两个任务同时运行,优先哪个就全凭运气了,任务切换先轮到谁就谁赢了,运气好的任务可以消耗更多的时间来完成它的工作。而另一个同样优先级的任务就只能够等待了。比如拿鼠标与音乐播放器比,两者都是优先级很高的任务,理论上来说音乐播放器的优先级更高,但是不幸如果两者优先级被定义成为相同,那么就会出现非常糟糕的情况,音乐播放器可能变得一团糟。 因此我们需要设计一种架构,使得即便高优先级的任务同时运行,也能够区分哪个更加优先。 其实也没有那么复杂,就是创建了几个struct TASKCTL,个数随意,我们先从创建3个开始讲解。 这种架构的工作原理是,最上层的LEVEL0只要存在哪怕一个任务,则完全忽略LEVEL1和LEVEL2的任务,只在LEVEL0的任务中进行切换。当LEVEL0的任务全部完成或者休眠,或者全部降到下层LEVEL时,接下来开始LEVEL1的任务,最后轮到LEVEL2. 需要创建的结构体如下所示
#define MAX_TASKS_LV 100 /*每一层LEVEL中最多任务数*/ #define MAX_TASKLEVELS 10 /*能够设置的最大任务层数*/ #define TASK_GDT0 3 struct TSS32{ int backling,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; }; struct TASK{ int sel,flags; /*sel用来存放GDT编号*/ int level; /*level记录任务层数*/ int priority; /*priority记录优先级*/ struct TSS32 tss; }; struct TASKLEVEL{ int running; /*正在运行的任务数量*/ int now; /*记录正在运行的是哪个任务*/ struct TASK* tasks[MAX_TASKS_LV]; }; struct TASKCTL{ int now_lv; /*现在活动中的LV*/ char lv_change; /*在下次任务切换时是否需要改变level*/ struct TASKLEVEL level[MAX_TASKLEVELS]; struct TASK tasks0[MAX_TASKS]; };
上述四个结构体的关系如下:
struct TASKLEVEL
上面有关用C语言实现多任务其实有许多不完善的地方。由于在这过程中牵扯到GDT,定时器,内存管理,中断等概念,因此不能完全讲述清楚。后续我会逐步补上用C实现GDT,定时器,内存管理等这些内容。 周徐达 2013/9/21
参考资料 《30天自制操作系统》 -【日】 川合秀实
写的不错~~~但有几点需要说明:
...
1. 2. 3. ....
已经给你修正部分问题! Enjoy Markdown!
关于代码高亮,GFM 使用 Linguist 来对代码语言进行检测和智能高亮,在代码块的第一个 ``` 后面加上代码的语言关键字,如 c、python、java、bash等等,Linguist 支持上百种语言,具体关键字详见这里 !
多任务总述
多任务"multitask",简单来说就是在操作系统中,多个应用程序同时运行的状态。如果我们的电脑装了多个CPU,那么运行多个程序倒也是顺理成章的,但实际上,只有一个CPU,操作系统照样可以实现多任务。 其实说穿了,这些程序根本没有在同时运行,只不过看上去好像在同时运行一样:程序A运行一会儿,接下来程序B运行一会儿,再接下来轮到程序C,然后再回到程序A.......如此反复。如果切换速度非常快(几个毫秒切换一次),那么在人看来多个程序就是同时运行的。
任务切换时CPU的工作
首先,每个任务创建时,都会在内存为任务分配一段空间。该空间来储存任务的运行状态信息。 当CPU收到任务切换指令时,CPU会先把寄存器中的值全部写入内存,这样做的目的是为了当以后切换回这个任务时,可以从中断的地方继续运行。接下来,为了运行下一个程序,CPU会把下一个程序所有寄存器的值从对应的内存地址中读取出来,这样就完成了一次任务切换。
用C语言实现两个任务的切换
接下来我们将用最简单的方式实现两个任务的切换。为了简化模型,我们做了如下的假设:
main()
函数,将任务B定义为tss_b_main()
。用C语言实现两个任务切换涉及到以下几个方面:
描述任务信息的结构体-TSS
任务状态段(task status segment),简称TSS,是指在操作系统进程管理中,任务切换时的任务现场信息。TSS的格式如下图所示:
因此我们可以定义如下的结构体:
TSS共包括26个int成员,总计104个字节,按照作用可以分为四个部分。 第一行从backlink到cr3为止的几个成员,保存任务设置相关的信息。其中backlink保存前一个任务TSS描述符的选择子,esp0到ss2这六个成员与任务的特权级有关,cr3跟分页机制相关。 第二行的成员是32位的寄存器。 第三行的成员是16位的段寄存器。 第四行和第一行一样,是任务设置相关部分。ldtr为局部描述符表寄存器,I/Omap为I/O许可位图。
为TSS分配内存地址
TSS是内存段的一种,因此需要在GDT(GlobalDescriptorTable)中进行定义后再使用。 GDT是全局描述描述符表,是操作系统采用分段机制的一种体现。GDT由8192个段描述符所组成,每一个段描述符记录了段基址,_段限长_以及段类型。将TSS注册到GDT中就可以为TSS分配内存地址。我们将任务A注册到GDT的第3号段,将任务B注册到GDT的第4号段。
用JMP FAR指令实现任务切换
由于任务切换是一种段与段之间的跳转,需要用汇编的JMP FAR指令来实现。很遗憾,C语言对于JMP FAR无能为力,因此只能依靠汇编来实现。汇编代码如下:
那么由任务3切换到任务4可以写
farjmp(0,4<<3);
由任务4切换到任务3可以写成farjmp(0,3<<3);
farjmp(eip,cs)
,首先CPU会把eip压入栈,然后把cs压入栈,如下图所示。最终CS与EIP的值被更新,这样就实现了FAR JMP。
farjmp(0,4<<3);
这条指令中第二个参数需要向左平移3位,则跟段描述符与GDT有关,具体可以自行百度。用定时器实现任务切换
为了能够让任务自动进行切换,可以考虑用定时器来实现。定时器是定时中断的一种应用,我们可以考虑每过1ms发生一次定时中断,让任务进行切换。 由于篇幅的限制,定时器的实现部分我就不再本篇中进行赘述。
小结
至此,有关如何用C语言实现两个任务的切换已经介绍完毕。由于任务切换涉及到GDT以及定时器的概念,如果大家有编写GDT和定时器的基础,那么我相信任务切换其实并难不倒大家。如果大家对GDT以及定时器的实现有疑问,没有关系,我会在后续中慢慢介绍。
任务管理自动化
在上述的介绍中,我们已经实现了两个任务的互相切换,但是这样还不够完善。如果我们要运行三个或者三个以上的任务,管理起来就会非常的麻烦。为了方便管理,我们对结构体进行如下的扩充。
上述三个结构体的关系如下: 1.
struct TSS32
为任务状态段,用来描述任务的基本信息。 2.struct TASK
定义为一个任务,在struct TSS32
的基础上添加了两条额外的信息。 3.struct TASKCTL
定义为一个任务管理器,用来管理多个任务。 接下来我们定义如下四个函数用来帮助我们管理信息:struct MEMMAN
是一个有关内存管理的结构体,有关如何用C语言实现内存管理则会在后续的内容中进行介绍。由于struct TASKCTL
这个结构体异常庞大,因此必须调用内存管理模块对其进行管理。设置任务的优先级
在操作系统中有一些处理,即使牺牲其他任务的性能也必须完成,否则会引起用户的不满。比如对鼠标的处理,键盘处理,网络处理,音乐播放器等等。 如果要用C语言实现优先级其实并不是非常的困难,只需要在
struct TASK
这个结构体中加入int priority
这个变量即可。那么如何具体实现优先级呢?其实只需要控制每一个任务的定时器即可。优先级高的任务,定时器的时间也就越紧凑,CPU在单位时间内将会更多的执行该任务。
更加完善的任务管理器
按照刚才的设定,优先级相同的两个任务同时运行,优先哪个就全凭运气了,任务切换先轮到谁就谁赢了,运气好的任务可以消耗更多的时间来完成它的工作。而另一个同样优先级的任务就只能够等待了。比如拿鼠标与音乐播放器比,两者都是优先级很高的任务,理论上来说音乐播放器的优先级更高,但是不幸如果两者优先级被定义成为相同,那么就会出现非常糟糕的情况,音乐播放器可能变得一团糟。 因此我们需要设计一种架构,使得即便高优先级的任务同时运行,也能够区分哪个更加优先。 其实也没有那么复杂,就是创建了几个
struct TASKCTL
,个数随意,我们先从创建3个开始讲解。 这种架构的工作原理是,最上层的LEVEL0只要存在哪怕一个任务,则完全忽略LEVEL1和LEVEL2的任务,只在LEVEL0的任务中进行切换。当LEVEL0的任务全部完成或者休眠,或者全部降到下层LEVEL时,接下来开始LEVEL1的任务,最后轮到LEVEL2. 需要创建的结构体如下所示上述四个结构体的关系如下:
struct TSS32
为任务状态段,用来描述任务的基本信息。struct TASK
定义为一个任务,在struct TSS32
的基础上添加了四条额外的信息。struct TASKLEVEL
一个任务层的任务管理中枢。struct TASKCTL
由多个struct TASKLEVEL
组合,用来管理多层任务。总结
上面有关用C语言实现多任务其实有许多不完善的地方。由于在这过程中牵扯到GDT,定时器,内存管理,中断等概念,因此不能完全讲述清楚。后续我会逐步补上用C实现GDT,定时器,内存管理等这些内容。 周徐达 2013/9/21