linux0.11进程调度详解

    • 内核调度初始化
      • sched_init()
        • set_tss_desc() & set_ldt_desc()
        • ltr() & lldt()
        • LATCH
    • 进程调度流程
      • 中断处理函数_timer_interrupt
      • do_timer()
      • 调度函数schedule()
      • 难点——任务切换switch_to()
        • 跳转
        • 切换回来

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


内核调度初始化

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//清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)

进程调度流程

10ms触发中断 –> 进入中断处理函数_timer_interrupt –> _do_timer –> schedule()调度函数 –> 任务切换switch_on()

中断处理函数_timer_interrupt

定义在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();
}

调度函数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);
}

难点——任务切换switch_to()

在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寄存器指向的那条指令开始运行

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

你可能感兴趣的:(Linux0.11学习笔记)