峥嵘岁月

joshua_yu的网络空间

俞峥ID:joshua_yu
99879次访问,排名850(1)好友2人,关注者10
人生总有些阶段,新的起点,新的心情,没有好也没有坏,生活总是辩证而真实地存在,感谢所有人!
joshua_yu的文章
原创 45 篇
翻译 2 篇
转载 42 篇
评论 18 篇
joshua的公告
联系方式: QQ:404271575 MSN:joshua_yu@263.net
最近评论
mohroq:wow gold,
owen870215:好帖
不过和我用的版本的源码还是不一样
继续学习
joshua_yu:对不起,好久没有上来了,才看到你的留言。
编译驱动程序需要用DDK的编译环境,你可能直接在VC当中编译了。
zh517:提示"cannot open file "wbemuuid.lib" "
zh517:大哥.我下了你的代码,可以编译不过去啊!先是提示找不到wbemcli.h文件,后来我从DDK下找到了这个文件放到了James\Exe\inc\下.继续编译,不提示"cannot open file "wbemuuid.lib" "怎么回事啊?我是初学者,是不是VC和DDK需要配置一下啊?
文章分类
收藏
    相册
    08年第一期儿子照片
    过年
    交大新面貌
    我的可爱儿子
    周末烧烤之众生相
    关注的Blog
    EVA的回收站
    joyfire的space
    Kendiv的专栏
    PJF的Blog
    WebCrazy的Blog
    孟言的blog
    野路子(http://wulujia.com)
    铁卷大成天下
    网络收藏夹
    China CISSP论坛(文档保护)
    China Uniix
    developerWorks Linux 专栏
    docshow
    linux伊甸园
    OSR在线论坛
    PKI论坛
    reactos
    rootkit论坛
    Sysinternals论坛
    中国Linux公社
    中国Linux论坛
    协议分析网论坛
    安全焦点
    看雪技术学院论坛
    驱动开发网
    存档
    软件项目交易
    订阅我的博客
    XML聚合  FeedSky
    订阅到鲜果
    订阅到Google
    订阅到抓虾
    订阅到BlogLines
    订阅到Yahoo
    订阅到GouGou
    订阅到飞鸽
    订阅到Rojo
    订阅到newsgator
    订阅到netvibes

    原创 (原创)linux内核进程调度以及定时器实现机制收藏

    新一篇: (原创)Linux内核NAPI机制分析 | 旧一篇: LIDS攻略

    一、2.6版以前内核进程调度机制简介

    Linux的进程管理由进程控制块、进程调度、中断处理、任务队列、定时器、bottom half队列、系统调用、进程通信等等部分组成。

    进程调用分为实时进程调度和非实时进程调度两种。前者调度时,可以采用基于动态优先级的轮转法(RR),也可以采用先进现出算法(FIFO)。后者调度时,一律采用基于动态优先级的轮转法。某个进程采用何种调度算法由改进程的进程控制块中的某些属性决定,没有专门的系统用来处理关于进程调度的相关事宜。Linux的进程调度由schedule()函数负责,任何进程,当它从系统调用返回时,都会转入schedule(),而中断处理函数完成它们的响应任务以后,也会进入schedule()。

     

    1.         进程控制块数据结构

    Linux系统的进程控制块用数据结构task_struct表示,这个数据结构占用1680个字节,具体的内容不在这里介绍,详细内容见《Linux内核2.4版源代码分析大全》第二页。

    进程的状态主要包括如下几个:

    TASK_RUNNING   正在运行或在就绪队列run-queue中准备运行的进程,实际参与进程调度。

    TASK_INTERRUPTIBLE       处于等待队列中的进程,待资源有效时唤醒,也可由其它进程通过信号或定时中断唤醒后进入就绪队列run-queue

    TASK_UNINTERRUPTIBLE         处于等待队列的进程,待资源有效时唤醒,不也可由其它进程通过信号或者定时中断唤醒。

    TASK_ZOMBIE      表示进程结束但尚未消亡的一种状态(僵死),此时,进程已经结束运行并且已经释放了大部分资源,但是尚未释放进程控制块。

    TASK_STOPPED    进程暂停,通过其它进程的信号才能唤醒。

     

    所有进程(以PCB形式)组成一个双向列表。next_taskprev_task就是链表的前后向指针。链表的头尾都是init_taskinit进程)。不过进程还要根据其进程ID号插入到一个hash表当中,目的是加快进程搜索速度。

     

    2.         进程调度

    Linux进程调度由schedule()执行,其任务是在run-queue队列中选出一个就绪进程。

    每个进程都有一个调度策略,在它的task_struct中规定(policy属性),或为SCHED_RR,SCHED_FIFO,或为SCHED_OTHER。前两种为实时进程调度策略,后一种为普通进程调度策略。

    用户进程由do_fork()函数创建,它也是fork系统调用的执行者。do_fork()创建一个新的进程,继承父进程的现有资源,初始化进程时钟、信号、时间等数据。完成子进程的初始化后,父进程将它挂到就绪队列,返回子进程的pid

    进程创建时的状态为TASK_UNINTERRUPTIBLE,在do_fork()结束前被父进程唤醒后,变为TASK_RUNNING。处于TASK_RUNNING状态的进程被移到就绪队列中,当适当的时候由schedule()按CPU调度算法选中,获得CPU

    如果进程采用轮转法,当时间片到时(10ms的整数倍),由时钟中断触发timer_interrupt()函数引起新一轮的调度,把当前进程挂到就绪队列的尾部。获得CPU而正在运行的进程若申请不到某个资源,则调用sleep_on()或interruptible_sleep_on()睡眠,并进入就绪队列尾。状态尾TASK_INTERRUPTIBLE的睡眠进程当它申请的资源有效时被唤醒,也可以由信号或者定时中断唤醒,唤醒以后进程状态变为TASK_RUNNING,并进入就绪队列。

    首先介绍一下2.6版以前的的调度算法的主要思想,下面的schedule()函数是内核2.4.23中摘录的:

    asmlinkage void schedule(void)

    {

    struct schedule_data * sched_data;

    struct task_struct *prev, *next, *p;

    struct list_head *tmp;

    int this_cpu, c;

     

    spin_lock_prefetch(&runqueue_lock);

     

    BUG_ON(!current->active_mm);

    need_resched_back:

           /*记录当前进程和处理此进程的CPU*/

    prev = current;

    this_cpu = prev->processor;

    /*判断是否处在中断当中,这里不允许在中断处理当中调用sechedule()*/

    if (unlikely(in_interrupt())) {

            printk("Scheduling in interrupt\n");

            BUG();

    }

     

    release_kernel_lock(prev, this_cpu);

     

    /*'sched_data' 是收到保护的,每个CPU只能运行一个进程。*/

    sched_data = & aligned_data[this_cpu].schedule_data;

     

    spin_lock_irq(&runqueue_lock);

     

    /*如果当前进程的调度策略是轮转RR,那么需要判断当前进程的时间片是否已经用完,如果已经用完,则重新计算时间片值,然后将该进程挂接到就绪队列run-queue的最后*/

    if (unlikely(prev->policy == SCHED_RR))

            if (!prev->counter) {

                   prev->counter = NICE_TO_TICKS(prev->nice);

                   move_last_runqueue(prev);

            }

    /*假如前进程为TASK_INTERRUPTTIBLE状态,则将其状态置为TASK_RUNNING。如是其它状态,则将该进程转为睡眠状态,从运行队列中删除。(已不具备运行的条件) */

    switch (prev->state) {

            case TASK_INTERRUPTIBLE:

                   if (signal_pending(prev)) {

                          prev->state = TASK_RUNNING;

                          break;

                   }

            default:

                   del_from_runqueue(prev);

            case TASK_RUNNING:;

    }

    /*当前进程不需要重新调度*/

    prev->need_resched = 0;

     

    /*下面是一般的进程调度过程*/

     

    repeat_schedule:

    next = idle_task(this_cpu);

    c = -1000;

    /*遍历进程就绪队列,如果该进程能够进行调度(对于SMP来说就是判断当前CPU未被占用能够执行这个进程,对于非SMP系统则为1),则计算该进程的优先级,如果优先级大于当前进程,则next指针指向新的进程,循环直到找到优先级最大的那个进程*/

    list_for_each(tmp, &runqueue_head) {

            p = list_entry(tmp, struct task_struct, run_list);

            if (can_schedule(p, this_cpu)) {

                   int weight = goodness(p, this_cpu, prev->active_mm);

                   if (weight > c)

                          c = weight, next = p;

            }

    }

     

    /* 判断是否需要重新计算每个进程的时间片,判断的依据是所有正准备进行调度的进程时间片耗尽,这时,就需要对就绪队列中的每一个进程都重新计算时间片,然后返回前面的调度过程,重新在就绪队列当中查找优先级最高的进程执行调度。 */

    if (unlikely(!c)) {

            struct task_struct *p;

     

            spin_unlock_irq(&runqueue_lock);

            read_lock(&tasklist_lock);

            for_each_task(p)

                   p->counter = (p->counter >> 1) + NICE_TO_TICKS(p->nice);

            read_unlock(&tasklist_lock);

            spin_lock_irq(&runqueue_lock);

            goto repeat_schedule;

    }

     

    /*CPU私有调度数据中记录当前进程的指针,并且将当前进程与CPU绑定,如果待调度进程与前面一个进程属于同一个进程,则不需要调度,直接返回。*/

    sched_data->curr = next;

    task_set_cpu(next, this_cpu);

    spin_unlock_irq(&runqueue_lock);

     

    if (unlikely(prev == next)) {

            /* We won't go through the normal tail, so do this by hand */

            prev->policy &= ~SCHED_YIELD;

            goto same_process;

    }

    /*全局统计进程上下文切换次数*/

    kstat.context_swtch++;

    /*如果后进程的mm0 (未分配页),则检查是否被在被激活的页里(active_mm,否则换页。令后进程记录前进程激活页的信息,将前进程的active_mm中的mm_count值加一。将cpu_tlbstate[cpu].state改为 TLBSTATE_LAZY(采用lazy模式) 如果后进程的mm不为0(已分配页),但尚未激活,换页。切换mmswitch_mm)。 如果前进程的mm 0(已失效) ,将其激活记录置空,将mm结构引用数减一,删除该页。 */

    prepare_to_switch();

    {

            struct mm_struct *mm = next->mm;

            struct mm_struct *oldmm = prev->active_mm;

            if (!mm) {

                   BUG_ON(next->active_mm);

                   next->active_mm = oldmm;

                   atomic_inc(&oldmm->mm_count);

                   enter_lazy_tlb(oldmm, next, this_cpu);

            } else {

                   BUG_ON(next->active_mm != mm);

                   switch_mm(oldmm, mm, next, this_cpu);

            }

     

            if (!prev->mm) {

                   prev->active_mm = NULL;

                   mmdrop(oldmm);

            }

    }

     

    /*切换到后进程,调度过程结束*/

    switch_to(prev, next, prev);

    __schedule_tail(prev);

     

    same_process:

    reacquire_kernel_lock(current);

    if (current->need_resched)

            goto need_resched_back;

    return;

    }

     

    3.         进程上下文切换(摘自中国Linux论坛一片文章)

    首先进程切换需要做什么?它做的事只是保留正在运行进程的"环境",并把将要运行的进程的"环境"加载上来,这个环境也叫上下文。它包括各个进程"公用"的东西,比如寄存器。
    下一个问题,旧的进程环境保存在那,新的进程环境从那来,在i386上,有个tss段,是专用来保存进程运行环境的。在Linux来说,在结构task_struct中有个类型为struct thread_struct的成员叫tss,如下:

    struct task_struct {

    。。。

    /* tss for this task */

    struct thread_struct tss;

    。。。

    };

    它是专用来存放进程环境的,这个结构体因CPU而异,你看它就能知道有那些寄存器是需要保存的了。

    最后的问题就是切换了,虽然在i386CPU可以自动根据tss去进行上下文的切换,但是Linux的程序员们更愿意自己做它,原因是这样能得到更有效的控制,而且作者说这和硬件切换的速度差不多,这可是真的够伟大的。

    好了,现在来看源码,进程切换是使用switch_to这个宏来做的,当进入时prev即是现在运行的进程,next是接下来要切换到的进程,

    #define switch_to(prev,next,last) do { \

    asm volatile(

    "pushl %%esi\n\t" \

    "pushl %%edi\n\t" \

    "pushl %%ebp\n\t" \

     

    // 首先它切换堆栈指针,prev->tss.esp = %esp%esp = next->tss.esp,这以后的堆栈已经是next的堆栈了。

    "movl %%esp,%0\n\t" /* save ESP */ \

    "movl %3,%%esp\n\t" /* restore ESP */ \

     

    // 然后使进程prev的指针保存为标号为1的那一个指针,这样下次进程prev可以运行时,它第一个执行的就是pop指令。

    "movl $1f,%1\n\t" /* save EIP */ \

     

    // 把进程next保存的指针推进堆栈中,这句作用是,从__switch_to返回时,下一个要执行的指令将会是这个指针所指向的指令了。

    "pushl %4\n\t" /* restore EIP */ \

     

    // 使用jump跳到__switch_to函数的结果是:调用switch_to函数但不象call那样要压栈,但是ret返回时,仍是要弹出堆栈的,也就是上条指令中推进去的指令指针。这样,堆栈和指令都换了,进程也就被"切换"了。

    "jmp __switch_to\n" \

     

    // 由于上面所说的原因,__switch_to返回后并不会执行下面的语句,要执行到这,只有等进程prev重新被调度了。

    "1:\t" \

    "popl %%ebp\n\t" \

    "popl %%edi\n\t" \

    "popl %%esi\n\t" \

    :"=m" (prev->tss.esp),"=m" (prev->tss.eip), \

    "=b" (last) \

    :"m" (next->tss.esp),"m" (next->tss.eip), \

    "a" (prev), "d" (next), \

    "b" (prev)); \

    } while (0)

     

    最后是__switch_to函数,它虽然是c形式,但内容还都是嵌入汇编。

     

    // 这句跟fpu有关,我并不感兴趣,就略过了。

    unlazy_fpu(prev);

     

    // 这句可能需要你去记起前面第二章中所描述的gdt表的结构了,它的作用是把进程nexttss描述符的type中的第二位清0,这位表示这个描述符是不是当前正在用的描述符,作者说如果不清0就把它loadtss段寄存器的话,系统会报异常(我可没试过)。

    gdt_table[next->tss.tr >> 3].b &= 0xfffffdff;

     

    // 把进程nexttssloadtss段存器中。

    asm volatile("ltr %0": :"g" (*(unsigned short *)&next->tss.tr));

     

    // 保存进程prevfsgs段寄存器

    asm volatile("movl %%fs,%0":"=m" (*(int *)&prev->tss.fs));

    asm volatile("movl %%gs,%0":"=m" (*(int *)&prev->tss.gs));

     

    然后下面就是load进程nextldt,页表,fsgsdebug寄存器。

    因为Linux一般并不使用ldt,所以它们一般会指向一个共同的空的ldt段描述符,这样就可能不需要切换ldt了,如果进程nextprev是共享内存的话,那么页表的转换也就不必要了(这一般发生在clone时)。

     

    二、2.6版内核对进程调度的优化

    1.         新调度算法简介

    2.6版本的Linux内核使用了新的调度器算法,称为O1)算法,它在高负载的情况下执行得非常出色,并在有多个处理器时能够很好地扩展。

    2.4版本的调度器中,时间片重算算法要求在所有的进程都用尽它们的时间片后,新时间片才会被重新计算。在一个多处理器系统中,当进程用完它们的时间片后不得不等待重算,以得到新的时间片,从而导致大部分处理器处于空闲状态,影响SMP的效率。此外,当空闲处理器开始执行那些时间片尚未用尽的、处于等待状态的进程时,会导致进程开始在处理器之间跳跃。当一个高优先级进程或交互式进程发生跳跃时,整个系统的性能就会受到影响。

    新调度器解决上述问题的方法是,基于每个CPU来分布时间片,并取消全局同步和重算循环。调度器使用了两个优先级数组,即活动数组和过期数组,可以通过指针来访问它们。活动数组中包含所有映射到某个CPU且时间片尚未用尽的任务。过期数组中包含时间片已经用尽的所有任务的有序列表。如果所有活动任务的时间片都已用尽,那么指向这两个数组的指针互换,包含准备运行任务的过期数组成为活动数组,而空的活动数组成为包含过期任务的新数组。数组的索引存储在一个64位的位图中,所以很容易找到最高优先级的任务。

     

    新调度器的主要优点包括:

         SMP效率 如果有工作需要完成,所有处理器都会工作。

         等待进程 没有进程需要长时间地等待处理器,也没有进程会无端地占用大量的CPU时间。

         SMP进程映射 进程只映射到一个CPU,而且不会在CPU之间跳跃。

         优先级 非重要任务的优先级低,反之亦然。

         负载平衡 调度器会降低那些超出处理器负载能力的进程的优先级。

         交互性能 即使在高负载的情况下,系统花费很长时间来响应鼠标点击或键盘输入的情况也不会再发生。

     

    2.6版内核中,进程调度经过重新编写,调度程序不需每次都扫描所有的任务,而是在一个任务变成就绪状态时将其放到一个名为当前的队列中。当进程调度程序运行时,只选择队列中最有利的任务来执行。这样,调度可以在一个恒定的时间里完成。当任务执行时,它会得到一个时间段,或者在其转到另一线程之前得到一段时间的处理器使用权。当时间段用完后,任务会被转移到另一个名为过期的队列中。在该队列中,任务会根据其优先级进行排序。

    从某种意义上说,所有位于当前队列的任务都将被执行,并被转移到过期队列中。当这种事情发生时,队列就会进行切换,原来的过期队列成为当前队列,而空的当前队列则变成过期队列。由于在新的当前队列中,任务已经被排列好,调度程序现在使用简单的队列算法,即总是取当前队列的第一个任务进行执行。这个新过程要比老过程快得多。

     

    2.         2.6版新调度算法分析

    下面的schedule