date: 2014-10-31 12:16linux
在讨论进程的调度与切换时,咱们关注以下几个问题:安全
那么linux内核的调度机制是怎样的呢?先来看看进程状态转换关系示意图:app
首先自愿的调度随时均可以进行。在内核空间中,一个进程能够随时经过调用schedule来启动一次调度。在用户空间中,进程能够经过系统调用pause来自愿让出cpu从而启动一次调度。从应用的角度来看,只有在用户空间自愿放弃(pause系统调用以及nanosleep系统调用,注意sleep不是系统调用而是C库函数)这一举动是可见的;而进程陷入内核后的自愿放弃行为是不可见的,它隐藏在其余可能受阻的系统调用,好比open、read、write等。进程因这些系统调用而陷入内核,若是这些调用被阻塞,总不能让CPU阻塞在这里啥都不干吧,因而内核就替进程作主,自愿放弃CPU启动一次调度。ide
此外,若是一个进程运行太长时间,调度器可能会进行一次强制调度。非自愿的被强制的调度(发生在每次从系统调用返回到用户空间的前夕,以及每次从中断或异常处理返回到用户空间的前夕。注意这里的“返回到用户空间的前夕”的限定,对系统调用来讲,确定是返回到用户空间了;对中断或异常来讲,它有可能发生在用户空间(当进程在用户空间运行时中断来了),也可能发生在内核空间(即当进程陷入内核后,中断来了),那么中断有可能返回到用户空间也有可能返回到内核空间。有了这个限定之后,只有当在用户空间发生的中断,其返回到用户空间前夕,才会进行一次强制调度;而在内核空间发生的中断,其返回时不会进行强转调度。这就给内核的设计与实现带来了便利。想一想看,若是没有这个限定的话,在内核空间中,当前进程可能由于中断而被强转换出,其正在使用的资源可能会被新运行的进程所修改,这样一来,全部在进程间共享的数据都要经过互斥来保护了,这种多进程共享的数据何其多矣,加不胜加呀。函数
还要指明,强制调度还有一个条件,那就是当前进程task_struct结构的need_resched字段被置1(前面讲fork流程时,父进程将本身的need_resched置1,所以,从fork返回时会发生一次强制性调度),那么谁来设置该字段了,天然只有内核了,用户空间没法访问到task_struct结构的。什么状况下设置该字段呢?其一,在某些系统调用的内核实现中设置,好比系统调用pause、fork中,还有其余调用可能受阻的系统调中;其二在时钟中断服务程序中,发现当前进程运行过久时设置;其三,内核中因某种缘由唤醒一个进程时。this
当进程在用户空间中运行时,无论自愿不自愿,一旦有必要(好比运行太长时间),内核就能够暂时剥夺其运行转而调度其余进程运行。但是,一旦进程进入内核空间,就像进入“安全地带”,这时,尽管内核知道要调度了,也只能干等着,等待进程离开“安全地带”返回用户空间前夕将其剥夺。所以说,linux的调度方式是可剥夺的,但因为剥夺时机的限制而变成有条件可剥夺的了。 那么,剥夺式的调用发生在何时呢?一样是进程从系统空间返回用户空间的前夕。设计
其实,这里讨论“有条件可剥夺”与前面的调度时机是密切相关的,剥夺式的调度即非自愿的强制调度,它剥夺当前进程的运行权利而让其余进程运行。3d
调度政策为以优先级为基础的调度。内核为每一个进程计算出一个反应其运行资格的权值,而后挑选权值最高的进程投入运行。而资格的运算则是以优先级为基础。unix
为了适应不一样的需求,内核实现了三种不一样的政策:SCHED_FIFO、SCHED_RR以及SCHED_OTHER。SCHED_FIFO适应于实时性要求比较强、而每次运行的耗时又比较短的进程;SCHED_RR适用于实时性要求较高但每次运行耗时较长的进程,其中的RR表示“Round Robin”即轮流之意,意即当多个进程具备同一优先级时,轮流调度运行;SCHED_OTHER则为传统的调度政策,适用与交互式的分时应用。 既然每一个进程都有本身使用的调度政策,那么在计算运行资格时涉及到“归一化”的问题,即在计算资格时将政策也考虑进去,就像高考时,给符合某条件的考生加分同样。计算资格的函数为goodness,咱们在后面会详细讲到。指针
在exit一节中,一个即将去世的进程在do_exit中的最后一件事就是调用schedule自愿让出CPU,这是自愿调度的情形;此外,每当系统调用(或者是中断)返回到用户空间的前夕,内核会检查当前进程的need_resched字段,若是该字段非0,则调用schedule()进行一次强制性调度(这部分代码在<arch/i386/kernel/entry.s>中),这是强制调度的情形。
本小节咱们来看看schedule的流程,其定义在<kernel/sched.c>文件中,流程图以下(源代码里用了大量的goto 语句,这里为了描述方便,在不影响流程的状况下,省略对这些跳转描述):
进程的task_struct结构中有两个mm_struct结构指针:一个是mm,指向进程的用户空间,另外一个是active_mm。对于具备用户空间的进程这两个指针是一致的(好比在execve系统调用中会设置成一致,参考exec_mmap函数的详细代码);可是当一个不具有用户空间的内核进程被调度运行时,要求它必须有一个active_mm,因此只好借用一个。问谁借呢,最简单就是为借用当前进程(即将被换出的进程)的active_mm(当前进程也多是是个内核进程,它的active_mm也多是借来的),由于这样能够省去用户空间切换的开销,而在该进程被换成中止运行时,要记得归还它借来的active_mm。
为何必需要有一个active_mm?由于指向页面映射目录表的指针pgd就在这个结构中,内核进程不是没有用户空间吗,它要pgd何用?不要忘了,目录表中除了有用户空间的虚存页面映射,还有内核空间的虚存页面映射,参考第2章第6节。
schedule只能由进程在内核空间中主动调用,或者在当进程从系统空间返回到用户空间前夕被动地调用,而不能在一个中断服务程序内部调用。即便一个中断服务程序有调度的要求,也只能经过设置当前进程的need_resched字段为1来表达这种需求,而不能直接调用schedule。那么怎么判断当前处在中断上下文(即在中断服务程序里还没出来)呢?咱们来看看in_interrupt的定义。
<include/asm/hardirq.h> 20 /* 21 * Are we in an interrupt context? Either doing bottom half 22 * or hardware interrupt processing? 23 */ 24 #define in_interrupt() ({ int __cpu = smp_processor_id(); \ 25 (local_irq_count(__cpu) + local_bh_count(__cpu) != 0); })
在单CPU系统中,__cpu为0。在中断服务的入口和出口出,分别会调用irq_enter()和irq_exit()来递增和递减计数器local_irq_count[__cpu],只要这个计数器非0,就说明CPU在中断服务程序中还未离开。相似的,只要计数器local_bh_count[__cpu]非0就说明CPU在执行某个bh函数。就像停车场,每开入一辆车计算加1,每开出一辆车计数器减1,若是这个计数器非0,则说明停车场内还有车。
/* * This is the function that decides how desirable a process is.. * You can weigh different processes against each other depending * on what CPU they've run on lately etc to try to handle cache * and TLB miss penalties. * * Return values: * -1000: never select this * 0: out of time, recalculate counters (but it might still be selected) * +ve: "goodness" value (the larger, the better) * +1000: realtime process, select this. */ static inline int goodness(struct task_struct * p, int this_cpu, struct mm_struct *this_mm) { int weight; /* * select the current process after every other * runnable process, but before the idle thread. * Also, dont trigger a counter recalculation. */ weight = -1; if (p->policy & SCHED_YIELD) goto out; /* * Non-RT process - normal case first. */ if (p->policy == SCHED_OTHER) { /* * Give the process a first-approximation goodness value * according to the number of clock-ticks it has left. * * Don't do any other calculations if the time slice is * over.. */ weight = p->counter; if (!weight) goto out; /* .. and a slight advantage to the current MM */ if (p->mm == this_mm || !p->mm) weight += 1; weight += 20 - p->nice; goto out; } /* * Realtime process, select the first one on the * runqueue (taking priorities within processes * into account). */ weight = 1000 + p->rt_priority; out: return weight; }
这个函数比较简单,不一样调度政策运行资格的计算可参考以下表格:
回到流程图中,若是遍历完可运行队列中全部的进程,那么候选进程的运行资格c的值有以下几种可能:
为进程从新分配时间配的代码以下:
for_each_task(p) p->counter = (p->counter >> 1) + NICE_TO_TICKS(p->nice);
宏NICE_TO_TICKS的定义以下。参考注释,做者的意图是但愿NICE_TO_TICKS获得的时间片在50ms左右,所以须要根据时钟频率HZ来定义。好比,若是HZ为200,表示每秒中断200次,那么一个滴答tick为5ms,20-(nice)的取值为[1 ,40],平均值为20,将20右移1位即除以2为10,10个滴答即50ms。当时钟频率HZ越高,每一个滴答所表明的时间越短,NICE_TO_TICKS分配的滴答数越多,但最大只是20-(nice)的值左移2位即乘以4,极大值为160,这是没法与实时调度政策中最低运行资格为1000相抗衡的。
/* * Scheduling quanta. * * NOTE! The unix "nice" value influences how long a process * gets. The nice value ranges from -20 to +19, where a -20 * is a "high-priority" task, and a "+10" is a low-priority * task. * * We want the time-slice to be around 50ms or so, so this * calculation depends on the value of HZ. */ #if HZ < 200 #define TICK_SCALE(x) ((x) >> 2) #elif HZ < 400 #define TICK_SCALE(x) ((x) >> 1) #elif HZ < 800 #define TICK_SCALE(x) (x) #elif HZ < 1600 #define TICK_SCALE(x) ((x) << 1) #else #define TICK_SCALE(x) ((x) << 2) #endif #define NICE_TO_TICKS(nice) (TICK_SCALE(20-(nice))+1)
另外,须要说明,在从新计算时间配额时,对全部进程都进行了更新。并且更新是将原有配额除以2再加上NICE_TO_TICKS。那么那些不在可运行队列中的调度政策为SCHED_OTHER的进程,会所以得到较高的时间配额,在未来的调度中会占必定的优点。但这种更新方式也决定了更新后的时间配额不会超过两倍的NICE_TO_TICKS。所以即便调度政策为SCHED_OTHER的进程通过长期的“韬光养晦”,其运行资格也没法超过实时调度政策的进程。
任务切换的核心为switch_to,这是一段嵌入式汇编代码,定义在<include/asm/system.h>文件中:
#define switch_to(prev,next,last) do { \ asm volatile("pushl %%esi\n\t" \ "pushl %%edi\n\t" \ "pushl %%ebp\n\t" \ "movl %%esp,%0\n\t" /* save ESP */ \ "movl %3,%%esp\n\t" /* restore ESP */ \ "movl $1f,%1\n\t" /* save EIP */ \ "pushl %4\n\t" /* restore EIP */ \ "jmp __switch_to\n" \ "1:\t" \ "popl %%ebp\n\t" \ "popl %%edi\n\t" \ "popl %%esi\n\t" \ :"=m" (prev->thread.esp),"=m" (prev->thread.eip), \ "=b" (last) \ :"m" (next->thread.esp),"m" (next->thread.eip), \ "a" (prev), "d" (next), \ "b" (prev)); \ } while (0)
switch_to()有三个参数,schedule()调用它时,第一个和第三个参数传入的是当前进程,即要被调度器换出的进程,设为进程A,第二个参数传入的是候选进程,及要被调度器调度运行的进程,设为进程B,咱们用三步法来分析下这段代码:
;伪寄存器 ; prev->thread.esp --> r0 ; prev->thread.eip --> r1 ; last --> r2 ; next->thread.esp --> r3 ; next->thread.eip --> r4 ;伪寄存器与通用寄存器结合的"建议" ; prev --> eax ; netx --> edx ; prev --> ebx 016 pushl %esi /*在A进程的系统空间堆栈中进行入栈操做*/ 017 pushl %edi 018 pushl %ebp 019 movl %esp, r0 /*保存A进程系统空间堆栈的栈顶指针esp到其task_struct 结构的thread成员中*/ 020 movl r3, %esp /*从B进程task_struct结构的thread成员中恢复B进程的 系统空间堆栈esp,执行该指令以后,系统空间堆栈已经切换 到了B进程的系统空间堆栈 */ 021 move $lf, r1 /*设置A进程下一次调度执行时系统空间eip为标号1的地址 (保存到A进程task_struct结构的thread成员中)*/ 022 push r4 /*B进程系统空间的eip入栈(此时,固然是B进程的系统空间 堆栈) */ 023 jmp __switch_to /*这里经过jmp而不是call调用函数__switch_to,因而 __switch_to函数的返回地址就上上条指令压入的r4,即 进程B系统空间的eip*/ 024 1: 025 popl %ebp /*在B进程的系统空间堆栈中进行出栈操做*/ 026 popl %edi 027 popl %esi
代码的意图请参考注释。这段代码对进程A与进程B的操做以下图所示:
咱们再来逐行说明下:
咱们来看看B进程的执行路径:
25行~27行,寄存器出栈,这与16~18行相对应。因为switch_to()自己是个宏,编译后“内嵌”到schedule()函数中。因此接下来进程B继续在schedule()函数中执行,直到schedule()函数结束后返回。
进程B从schedule()函数返回时,要从B进程的系统空间堆栈弹出返回地址。用动态的眼光来看,B进程当初被暂停运行时,确定也曾调用过schedule(有一个特例,fork系统调用产生的子进程,下文详述),系统空间堆栈中确定保存着schedule()的返回地址。若是进程B当初在内核代码中主动调用schecule(),那么如今将回到schedule()的下一条代码执行;
若是进程B当初是在从系统空间返回用户空间前夕被强制调用了schedule(),那么这时将会回到<arch/i386/kernel/entry.s>中的第289行(见下),调用ret_from_sys_call恢复进程B用户空间的现场(从进程B系统空间堆栈顶部的pt_regs处恢复),进程B就回到它的用户空间继续运行了。
287 reschedule: 288 call SYMBOL_NAME(schedule) # test 289 jmp ret_from_sys_call
咱们再来看看fork产生的子进程首次被调度运行时的运行路线:
第22行,进程B的eip入栈。进程的eip被设置为ret_from_fork();
第23行,经过jmp指令调用__switch_to()函数。__switch_to()函数的返回地址即为ret_from_fork()函数的入口,那么当从__switch_to()函数返回时,直接跳转到ret_from_fork()处执行,继而跳转到ret_from_sys_call处,到达ret_with_reschedule时,因为子进程的need_resched字段为0,那么就直接返回到用户空间了。这部分代码在<arch/i386/kernel/entry.s>中第205行到223行。
fork产生的子进程初次被调度时,没有执行switch_to()的第25行到27行,也不涉及从schedule()函数中返回,它“抄了一段近路”直接到跳转至ret_from_fork(),而后返回到用户空间。
至于函数__switch_to(),那只是内核“应付”intel的“硬”任务切换。intel支持由CPU硬件来实施任务切换(核心是TSS),但linux并不买帐。由于这个“大而全”硬件切换一是缺乏灵活性,二是切换速度不必定快(由于有不少多余的切换动做)。