linux0.11进程调度详解

匿名 (未验证) 提交于 2019-12-02 21:59:42

文章写的有些长,把相关的、用到的函数都列出来了,看完应该能对进程调度相关的代码有一定了解


sched_init()

kernel/sched.c : 385

该函数只有36~40行与进程调度有关,只想了解进程调度的同学可以忽略该函数其他部分

void sched_init(void) {     int i;     struct desc_struct * p;      if (sizeof(struct sigaction) != 16)         panic("Struct sigaction MUST be 16 bytes");      //以下两行初始化init任务(任务0)的任务状态段描述符和局部数据表描述符     //详细介绍见下文     set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));     set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));      p = gdt+2+FIRST_TSS_ENTRY;//指向gtd表中init任务的后一个任务的任务状态段描述符     for(i=1;i<NR_TASKS;i++) { //清gdt表中init任务后所有描述符         task[i] = NULL;         p->a=p->b=0;         p++;         p->a=p->b=0;         p++;     }  /* Clear NT, so that we won't have troubles with that later on */ //这行内联汇编利用堆栈做中间变量将标志寄存器的NT位清除,以屏蔽任务切换 //1.首先用pushfl指令将标志寄存器压栈 //2.然后通过栈顶指针sp来修改刚才压栈的标志寄存器 //3.然后将栈顶的内容弹出到标志寄存器中,完成修改     __asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl");      //加载任务寄存器和局部描述符表寄存器     //详细介绍见下文     ltr(0);     lldt(0);  //下面的代码用于对8253定时器初始化操作,使其每10ms发出一个定时中断     outb_p(0x36,0x43);          //模式设置     outb_p(LATCH & 0xff , 0x40);//设置定时值的低字节(LATCH见下文)     outb(LATCH >> 8 , 0x40);    //定时值的高字节     set_intr_gate(0x20,&timer_interrupt);//设置定时中断处理函数,中断处理函数_timer_interrupt下文中有详解     outb(inb_p(0x21)&~0x01,0x21);        //修改中断控制器屏蔽码以开启时钟中断      set_system_gate(0x80,&system_call);  //设置系统调用中断门,与本文无关 }

set_tss_desc() & set_ldt_desc()

在include/asm/system.h中最后两行定义
set_tss_desc()和set_ldt_desc()是两个宏函数,该宏函数把传入的gdt的偏移地址转换成(char*)型后,逐字节设置描述符
源码如下

#define _set_tssldt_desc(n,addr,type) \ __asm__ ("movw $104,%1\n\t" \     "movw %%ax,%2\n\t" \     "rorl $16,%%eax\n\t" \     "movb %%al,%3\n\t" \     "movb $" type ",%4\n\t" \     "movb $0x00,%5\n\t" \     "movb %%ah,%6\n\t" \     "rorl $16,%%eax" \     ::"a" (addr), "m" (*(n)), "m" (*(n+2)), "m" (*(n+4)), \      "m" (*(n+5)), "m" (*(n+6)), "m" (*(n+7)) \     )  #define set_tss_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x89") #define set_ldt_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x82")

ltr() & lldt()

在include/linux/sched.h : 153定义
1. 将传入的任务号计算出该任务的任务状态段/局部描述符表在gdt中的偏移后
2. 用 ltr/lldt 汇编指令将任务号对应的 任务状态段/局部描述符表 加载到对应的 任务状态寄存器TR/局部描述符表寄存器LDT

#define FIRST_TSS_ENTRY 4 #define FIRST_LDT_ENTRY (FIRST_TSS_ENTRY+1) #define _TSS(n) ((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3)) #define _LDT(n) ((((unsigned long) n)<<4)+(FIRST_LDT_ENTRY<<3)) #define ltr(n) __asm__("ltr %%ax"::"a" (_TSS(n))) #define lldt(n) __asm__("lldt %%ax"::"a" (_LDT(n)))

LATCH

在kernel/sched.c : 46行定义
设置时钟芯片8253的定时初值
linux0.11是通过时钟中断来进行任务调度的
Linux希望的中断频率是100,即10ms发出一次时钟中断

 //HZ在include/linux/sched.h定义为100,1.193180MHZ为定时芯片8253的输入时钟频率  #define LATCH (1193180/HZ)

定义在kernel/system_call.s : 176

_timer_interrupt:     push %ds        # save ds,es and put kernel data space     push %es        # into them. %fs is used by _system_call     push %fs     pushl %edx      # we save %eax,%ecx,%edx as gcc doesn't     pushl %ecx      # save those across function calls. %ebx     pushl %ebx      # is saved as we use that in ret_sys_call     pushl %eax     #以上将寄存器压栈,保护现场     #以下5行,将ds、es指向内核数据段,将fs指向局部数据段     movl $0x10,%eax     mov %ax,%ds     mov %ax,%es     movl $0x17,%eax     mov %ax,%fs      incl _jiffies       #系统启动后的时钟滴答值+1     movb $0x20,%al     # EOI to interrupt controller #1     outb %al,$0x20      #发送EOI以结束硬件中断      #以下三行取当前特权级别,并压栈作为调用_do_timer的参数     movl CS(%esp),%eax     andl $3,%eax       # %eax is CPL (0 or 3, 0=supervisor)     pushl %eax     call _do_timer      # 'do_timer(long CPL)' does everything from     addl $4,%esp       # task switching to accounting ...     jmp ret_from_sys_call

do_timer()

定义在kernel/sched.c : 305

void do_timer(long cpl) {     extern int beepcount;     extern void sysbeepstop(void);      //蜂鸣器     if (beepcount)         if (!--beepcount)             sysbeepstop();      //根据当前特权级,将相应的运行时间递增。     if (cpl)         current->utime++;     else         current->stime++;      //如果有定时器正在使用,则将定时器链表的第一个定时器的定时值减一,如果定时值为0,则执行其处理程序并删除该定时器     if (next_timer) {         next_timer->jiffies--;         while (next_timer && next_timer->jiffies <= 0) {             void (*fn)(void);//利用函数指针临时保存当前定时器的处理函数              fn = next_timer->fn;             next_timer->fn = NULL;             next_timer = next_timer->next;             (fn)();          //执行该定时器的处理函数         }     }     //软盘相关,与本文无关     if (current_DOR & 0xf0)         do_floppy_timer();     //如果当前进程时间片不为0,则退出继续执行当前进程     if ((--current->counter)>0) return;     current->counter=0;     //如果当前特权级表示发生中断时正在内核态运行,则返回(内核任务不可被抢占)     if (!cpl) return;     //执行调度函数     schedule(); }

定义在kernel/sched.c : 104

void schedule(void) {     int i,next,c;     struct task_struct ** p;  /* check alarm, wake up any interruptible tasks that have got a signal */ //遍历任务数组,如果任务设置过定时值alarm并且已经超时,则把信号位图中的SIGALRM置位 //LAST_TASK和FIRST_TASK在include/linux/sched.h第7、8行定义,分别指向任务数组的最后一个和第一个元素     for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)         if (*p) {             if ((*p)->alarm && (*p)->alarm < jiffies) {                     (*p)->signal |= (1<<(SIGALRM-1));                     (*p)->alarm = 0;                 }             //如果信号位图中有已经置位的信号,并且任务处于可中断状态,则把任务置位就绪态             if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&             (*p)->state==TASK_INTERRUPTIBLE)                 (*p)->state=TASK_RUNNING;         }  /* this is the scheduler proper: */      while (1) {         c = -1;         next = 0;    //保存选出的任务的任务号         i = NR_TASKS;//include/linux/sched.h : 4 将该宏定义为64(任务数组长度最大64)         p = &task[NR_TASKS];         //遍历任务数组,选出就绪态的、时间片最大的任务         while (--i) {             if (!*--p)                 continue;             if ((*p)->state == TASK_RUNNING && (*p)->counter > c)                 c = (*p)->counter, next = i;         }         //如果所有任务时间片都是0,则执行下面代码为所有任务重新分配时间片,否则跳出当前while(1)循环         if (c) break;         //遍历任务数组重新分配时间片,新分配的时间片为counter/2+优先级,所以优先级越高分配到的时间片越大         for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)             if (*p)                 (*p)->counter = ((*p)->counter >> 1) +                         (*p)->priority;     }     switch_to(next); }

在include/linux/sched.h : 171定义
该函数将当前函数切换到所传参数任务号对应的任务数组中的任务。

#define switch_to(n) {\ struct {long a,b;} __tmp; \ __asm__("cmpl %%ecx,_current\n\t" \         //ecx传入的任务号对应的任务是否为当前任务     "je 1f\n\t" \                           //若是,则跳出     "movw %%dx,%1\n\t" \                    //新任务TSS选择符赋值给第一个参数__tmp.b     "xchgl %%ecx,_current\n\t" \            //交换指令,交换后ecx为被切换的任务,current为要切换到的任务     "ljmp %0\n\t" \                         //跳转至__tmp(新任务的段选择符),切换到该TSS对应的进程(__tmp.a实际上没用),实际上的指令是ljmp __tmp.b低字节:__tmp.a;a做偏移地址为0      "cmpl %%ecx,_last_task_used_math\n\t" \ //以下为再次切换回来后检查是否使用过协处理器     "jne 1f\n\t" \     "clts\n" \     "1:" \     ::"m" (*&__tmp.a),"m" (*&__tmp.b), \    //     "d" (_TSS(n)),"c" ((long) task[n])); \  //_TSS(n)传入给dx,任务号n对应的任务传入给ecx }

以下为对switch_on()函数的详细介绍
该函数一共两个难点
1. 怎么跳转到新任务
2. 任务切换后,怎么执行到协处理器检测代码

璺宠浆

  • 首先进入函数后定义了一个八字节结构体__tmp,我们只用到了其中的六个字节
  • 成员a值为0,成员b的低二字节在第五行被赋值为要切换到任务的TSS 段选择符
  • 跳转到TSS段选择符即会造成任务切换到该TSS对应的任务
  • 第七行的跳转代码相当于,跳转到__tmp.b对应的TSS选择符的0偏移处
  • 在任务切换时CPU会保存寄存器现场
  • 当前被切换任务的寄存器现场将保存在该任务的TSS结构中
  • 需注意在切换前,IP寄存器将指向切换指令的下一条指令,同样IP寄存器也将被CPU保存到结构中
  • 当该备切换的任务再一次被内核调度运行时,它将从IP寄存器指向的那条指令开始运行

刚开始写博客,写的不好大家见谅
有写的不对的地方希望大家不吝赐教
有疑惑也欢迎大家共同探讨

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!