eslab2013-ustc / RTFSC_Linux_Kernel

USTC 嵌入式系统实验室2013级研究生阅读内核记录
64 stars 55 forks source link

在内核态用C语言实现多任务 #6

Open ustcanycall opened 11 years ago

ustcanycall commented 11 years ago

多任务总述

多任务"multitask",简单来说就是在操作系统中,多个应用程序同时运行的状态。如果我们的电脑装了多个CPU,那么运行多个程序倒也是顺理成章的,但实际上,只有一个CPU,操作系统照样可以实现多任务。 其实说穿了,这些程序根本没有在同时运行,只不过看上去好像在同时运行一样:程序A运行一会儿,接下来程序B运行一会儿,再接下来轮到程序C,然后再回到程序A.......如此反复。如果切换速度非常快(几个毫秒切换一次),那么在人看来多个程序就是同时运行的。 4

任务切换时CPU的工作

首先,每个任务创建时,都会在内存为任务分配一段空间。该空间来储存任务的运行状态信息。 当CPU收到任务切换指令时,CPU会先把寄存器中的值全部写入内存,这样做的目的是为了当以后切换回这个任务时,可以从中断的地方继续运行。接下来,为了运行下一个程序,CPU会把下一个程序所有寄存器的值从对应的内存地址中读取出来,这样就完成了一次任务切换。 1

用C语言实现两个任务的切换

接下来我们将用最简单的方式实现两个任务的切换。为了简化模型,我们做了如下的假设:

  1. 将任务A定义为程序的main() 函数,将任务B定义为 tss_b_main()
  2. 只用最简单的结构体表示任务A与任务B的任务现场信息。
  3. 任务A与任务B均在内核态下运行,且不考虑特权级的变化。

用C语言实现两个任务切换涉及到以下几个方面:

  1. 如何描述任务的信息
  2. 如何分配存储任务信息的地址
  3. 用什么命令进行任务切换
  4. 用定时器实现任务切换 下面将从这四个方面逐步实现两个任务切换

    描述任务信息的结构体-TSS

任务状态段(task status segment),简称TSS,是指在操作系统进程管理中,任务切换时的任务现场信息。TSS的格式如下图所示: 5

因此我们可以定义如下的结构体:

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分配内存地址

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指令实现任务切换

由于任务切换是一种段与段之间的跳转,需要用汇编的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);

最终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 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);                          /*让某个程序休眠*/

在操作系统中有一些处理,即使牺牲其他任务的性能也必须完成,否则会引起用户的不满。比如对鼠标的处理,键盘处理,网络处理,音乐播放器等等。 如果要用C语言实现优先级其实并不是非常的困难,只需要在struct TASK这个结构体中加入int priority这个变量即可。

struct TASK{
    int sel;        /*sel用来存放GDT编号*/
        int flags;            /*用来记录TASK的状态:未使用,休眠中,正在运行*/
        int priority;         /*设置任务的优先级*/
    struct TSS32 tss;
};

那么如何具体实现优先级呢?其实只需要控制每一个任务的定时器即可。优先级高的任务,定时器的时间也就越紧凑,CPU在单位时间内将会更多的执行该任务。

更加完善的任务管理器

按照刚才的设定,优先级相同的两个任务同时运行,优先哪个就全凭运气了,任务切换先轮到谁就谁赢了,运气好的任务可以消耗更多的时间来完成它的工作。而另一个同样优先级的任务就只能够等待了。比如拿鼠标与音乐播放器比,两者都是优先级很高的任务,理论上来说音乐播放器的优先级更高,但是不幸如果两者优先级被定义成为相同,那么就会出现非常糟糕的情况,音乐播放器可能变得一团糟。 因此我们需要设计一种架构,使得即便高优先级的任务同时运行,也能够区分哪个更加优先。 其实也没有那么复杂,就是创建了几个struct TASKCTL,个数随意,我们先从创建3个开始讲解。 2 这种架构的工作原理是,最上层的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];
};

上述四个结构体的关系如下:

  1. struct TSS32为任务状态段,用来描述任务的基本信息。
  2. struct TASK定义为一个任务,在struct TSS32的基础上添加了四条额外的信息。
  3. struct TASKLEVEL一个任务层的任务管理中枢。
  4. struct TASKCTL 由多个struct TASKLEVEL组合,用来管理多层任务。

    总结

上面有关用C语言实现多任务其实有许多不完善的地方。由于在这过程中牵扯到GDT,定时器,内存管理,中断等概念,因此不能完全讲述清楚。后续我会逐步补上用C实现GDT,定时器,内存管理等这些内容。 周徐达 2013/9/21

参考资料 《30天自制操作系统》 -【日】 川合秀实

hazirguo commented 11 years ago

写的不错~~~但有几点需要说明:

  1. 行内代码使用一对反单引号(...),块内代码使用三个反单引号对(...)且单引号要独占一行;
  2. 数字编号 1. 2. 3. .... 在数字1 一定要与上一行之间空一行,否则不解析为编号;
  3. 标题中的# 号要与第一个字符之间有至少一个空格,否则不解析为标题;
  4. 为了更好地归类,请加上 label。

已经给你修正部分问题! Enjoy Markdown!

hazirguo commented 11 years ago

关于代码高亮,GFM 使用 Linguist 来对代码语言进行检测和智能高亮,在代码块的第一个 ``` 后面加上代码的语言关键字,如 c、python、java、bash等等,Linguist 支持上百种语言,具体关键字详见这里 !