LINUX进程调度机制及其堆栈切换分析精.docx
- 文档编号:15919245
- 上传时间:2023-07-09
- 格式:DOCX
- 页数:28
- 大小:109.31KB
LINUX进程调度机制及其堆栈切换分析精.docx
《LINUX进程调度机制及其堆栈切换分析精.docx》由会员分享,可在线阅读,更多相关《LINUX进程调度机制及其堆栈切换分析精.docx(28页珍藏版)》请在冰点文库上搜索。
LINUX进程调度机制及其堆栈切换分析精
LINUX2.4.18进程调度机制及其堆栈切换分析
张华强
2004-08
1LINUX调度机制概述
LINUX的进程调度机制的设计主要是从下面三个方面来考虑的。
调度的时机,LINUX的进程调度分为主动调度和被动的调度。
其中主动的调度在内核空间通过调用schedule(函数来启动一次调度。
在用户空间则是通过调用延时、睡眠、退出等主动放弃运行权的系统调用接口进入内核空间并最终通过调用schedule(函数来是启动一次调度。
而被动的调度则发生每次处理中断或异常后从系统空间切换到用户空间的前夕,或者经过其他的系统调用返回到用户空间的前夕。
调度的策略,为了适应各种不同的应用,LINUX在从UNIX上继承下来的基于优先级的调度策略的基础上实现了三种调度策略:
SCHED_FIFO、SCHED_RR和SCHED_OTHER。
其中的SCHE_FIFO和SCHED_RR都是实时调度的策略,他们在系统中被赋予了至少1000的优先权值。
二者不同的地方在于对待同等优先权值的进程的处理方式上的差别。
当系统中有多个同等优先级的SCHED_FIFO进程就绪时,按照先入先出的原则只有当前进程运行到自动放弃CPU的权限时才会让同等优先级的另一个进程调度起来运行。
而对于SCHED_RR的进程,则会在每次时钟中断到来时检查先前进程的时间片是否用完,如果用完则剥夺当前进程的CPU使用权调度出同等优先级的另一个SCHED_RR进程运行。
SCHED_OTHER则为传统的调度策略,主要用于交互式应用。
调度的方式:
LINUX内核的调度采用有条件的可剥夺方式,当进程在用户空间运行时,一旦有必要,例如发生了外部中断或者进程的时间片到时,内核都可以剥夺当前进程的运行权而调度其他的进程运行,因而说这种调度方式是可剥夺的,但当进入内核态时,即使内核知道应该调度了,但这种调度并不会立即发生,只有当前进程回到用户空间的前夕这种调度才会发生。
根据上面的分析可以得出以下几点结论:
1、LINUX的进程调度与中断是密不可分的(即使是系统调用也是通过软件中断INT0x80来实现的。
2、由于不论是主动的调度还是被动的调度最终都是通过在内核空间中调用schedule函数来实现的,因此LINUX的进程切换一定是在系统空间中完成的,在下面的分析中还可以看到这个过程实际上就是在schedule函数中完成的。
2LINUX2.4.18调度时的堆栈切换分析
基于以上两点我们假设系统中目前有两个进程,进程1和进程2,进程1正在运行。
下面我们就分别分析在被动调度(中断发生了和主动调度(调用nanosleep时系统的堆栈切换情况和进程的调度实现。
2.1被动调度时的堆栈切换分析
首先我们分析被动调度的情况。
假设进程1正在用户空间运行的过程中来了一个外设的中断请求。
根据CPU对中断的响应规则,CPU会在进程的当前指令执行完毕后相应中断。
CPU响应中断的过程如下描述。
由于在LINUX中只使用了CPU的0和3这两个运行级别,因此在以下的讨论中涉及到CPU的运行级别时只考虑这两个运行级。
首先CPU从中断控制器中取得中断向量,然后根据该向量重IDT中取出一个初始化好的中断门并试图穿过该中断门进入相应得中断服务程序中。
在穿过中断门的过程中,根据INTELIA-32体系结构中设计的硬件机制,CPU将作如下工作:
首先CPU将其当前运行级别与中断门中要求的运行级别进行比较,如果发现CPU的当前运行级别低于中断门的描述符运行级别(即CPL=3,中断门的DPL=0时,CPU首先会将由TR寄存器选择的TSS段中的SS0和ESP0(运行级别0的堆栈装入CPU的SS和ESP中,从后面的分析可知,SS0和ESP0一定对应的是当前运行任务的系统堆栈指针。
通过这样就完成了用户空间堆栈到系统空间堆栈的切换。
堆栈切换完成之后,CPU紧接着就将当前进程的用户空间堆栈的SS和ESP值压入新的堆栈中,然后在将当前的EFLAGS值和CS、EIP值,这些就对应当前任务被中断时的CPU标志寄存器值和中断处理完成后返回当前进程的地址。
如果发现CPU的当前运行级和描述符的运行级一致则不进行堆栈切换而直接将EFLAGS、CS、EIP的值压入堆栈中。
不管上述哪种情况,穿过中断门后的堆栈都会是当前任务的系统空间堆栈。
由于中断发生在用户空间,因此中断发生时的CPL=3,因而必然会发生堆栈切换。
因此进入中断服务入口处时系统堆栈情况如图1所示。
图1进入中断服务程序入口处的系统堆栈
从后面的分析可知由于CPU每次使用内核堆栈时对其所作的操作都是均衡的,因此每次从用户空间堆栈切换到系统空间堆栈时,该堆栈都是空的。
穿过中断门后系统进入中断服务程序的入口。
在LINUX系统中所有中断服务程序的总入口都是由gcc预生成的,具有如下形式:
asmlinkagevoidIRQ0xYY_interrupt(;
__asm__(
"\n"
“IRQ_0xYY_interrupt:
\n\t"
"pushl$"#nr"-256\n\t"\
"jmpcommon_interrupt";
上述代码中的YY代表外部中断0-254的16进制值(其中系统调用0x80除外,它是由系统另外单独初始化的。
根据上述代码可以看出所有的外部中断都是统一进入common_interrup的地方进行处理的。
同样该段由gcc预处理出来的代码如下:
asmlinkagevoidcall_do_IRQ(void;
__asm__(
"\n"__ALIGN_STR"\n"
"common_interrupt:
\n\t"
SAVE_ALL
"call_do_IRQ:
\n\t"
"calldo_IRQ"\n\t"
"jmpret_from_intr\n";
上述代码的主要作用就是通过SAVE_ALL宏将CPU的当前寄存器内容入栈。
SAVE_ALL宏的定义如下:
#defineSAVE_ALL\
"cld\n\t"\
"pushl%es\n\t"\
"pushl%ds\n\t"\
"pushl%eax\n\t"\
"pushl%ebp\n\t"\
"pushl%edi\n\t"\
"pushl%esi\n\t"\
"pushl%edx\n\t"\
"pushl%ecx\n\t"\
"pushl%ebx\n\t"\
"movl$"STR(__KERNEL_DS",%edx\n\t"\
"movl%edx,%ds\n\t"\
"movl%edx,%es\n\t"
保护完CPU的寄存器,就通过一条CALL指令调用do_IRQ进入真正的中断服务程序中。
这样CPU在进入中断服务程序do_IRQ时系统的堆栈情况如图2所示:
图2进入中断服务程序do_IR时的中断
从图2可知,当系统处理完中断从do_IRQ(函数返回时将跳转到ret_from_intr处开始执行代码。
(由于系统设计使得在中断服务过程中不会调用schedule(函数产生任务切换。
因而中断服务结束后一定会返回到图2中的返回点。
图3中断返回后的堆栈
其中的ret_from_intr处的代码如下:
ret_from_intr:
GET_CURRENT(%ebx
ret_from_exception:
movlEFLAGS(%esp,%eax#mixEFLAGSandCS
movbCS(%esp,%al
testl$(VM_MASK|3,%eax#returntoVM86modeornon-supervisor?
jneret_from_sys_call
jmprestore_all
在上述代码中,系统首先取出当前进程(进程1的task_struct结构指针放入ebx寄存器中。
然后在判断中断是发生在用户空间还是系统空间,如果发生在系统空间,则直接跳转到restore_all处中断返回。
由于本例中中断发生在进程1的用户空间中,因而程序跳转到ret_from_syscall处执行,该段代码如下:
ret_from_sys_call:
cli#need_reschedandsignalsatomictest
cmpl$0,need_resched(%ebx
jnereschedule
cmpl$0,sigpending(%ebx
jnesignal_return
restore_all:
RESTORE_ALL
到达ret_from_syscall后,系统将判断当前进程(进程1的need_resched字段值是否为零,如果非零则调用schedule(函数进行任务切换。
在这里我们假设在中断服务程序中唤醒了进程2,则从中断服务程序do_IRQ返回时进程1的need_resched字段值即为1。
因而系统将跳转到reschedule处运行,该段代码如下:
reschedule:
callSYMBOL_NAME(schedule#test
jmpret_from_sys_call
由此可见系统在此处将直接调用schedule(函数进行进程切换。
Schedule(函数的实现入下:
asmlinkagevoidschedule(void
{
structschedule_data*sched_data;
structtask_struct*prev,*next,*p;
structlist_head*tmp;
intthis_cpu,c;
if(!
current->active_mmBUG(;
need_resched_back:
prev=current;
this_cpu=prev->processor;
if(unlikely(in_interrupt({
printk("Schedulingininterrupt\n";
BUG(;
}
sched_data=&aligned_data[this_cpu].schedule_data;
if(unlikely(prev->policy==SCHED_RR
if(!
prev->counter{
prev->counter=NICE_TO_TICKS(prev->nice;
move_last_runqueue(prev;
}
switch(prev->state{
caseTASK_INTERRUPTIBLE:
if(signal_pending(prev{
prev->state=TASK_RUNNING;
break;
}
default:
del_from_runqueue(prev;
caseTASK_RUNNING:
;
}
prev->need_resched=0;
repeat_schedule:
next=idle_task(this_cpu;
list_for_each(tmp,&runqueue_head{
p=list_entry(tmp,structtask_struct,run_list;
if(can_schedule(p,this_cpu{
intweight=goodness(p,this_cpu,prev->active_mm;
if(weight>c
c=weight,next=p;
}
}
/*Doweneedtore-calculatecounters?
*/
if(unlikely(!
c{
structtask_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;
gotorepeat_schedule;
}
sched_data->curr=next;
task_set_cpu(next,this_cpu;
if(unlikely(prev==next{
/*Wewon'tgothroughthenormaltail,sodothisbyhand*/prev->policy&=~SCHED_YIELD;
gotosame_process;
}
kstat.context_swtch++;
/*
*thereare3processeswhichareaffectedbyacontextswitch:
*
*prev==....==>(last=>next
*
*It'sthe'muchmoreprevious''prev'thatisonnext'sstack,
*butprevissetto(thejustrun'last'processbyswitch_to(.
*Thismightsoundslightlyconfusingbutmakestonsofsense.
*/
prepare_to_switch(;
{
structmm_struct*mm=next->mm;
structmm_struct*oldmm=prev->active_mm;
if(next->active_mmBUG(;
next->active_mm=oldmm;
atomic_inc(&oldmm->mm_count;
enter_lazy_tlb(oldmm,next,this_cpu;
}else{
if(next->active_mm!
=mmBUG(;
switch_mm(oldmm,mm,next,this_cpu;
}
if(!
prev->mm{
prev->active_mm=NULL;
mmdrop(oldmm;
}
}
/*
*Thisjustswitchestheregisterstateandthe
*stack.
*/
switch_to(prev,next,prev;
__schedule_tail(prev;
same_process:
reacquire_kernel_lock(current;
if(current->need_resched
gotoneed_resched_back;
return;
}
上述代码摘自LINUX2.4.18内核源代码,其中我们忽略了预SMP(对称多处理器有关的内容。
进入schedule函数后先取出当前进程的task_struct指针,此处我们主要关注进程的切换,因此我们省去对mm的分析过程,这样程序就运行到need_resched_back处,由于在中断服务程序中不会发生对schehule(的调用,因此运行到此处时unlikely(in_interrupt(返回真值,程序继续往下运行。
接下来系统就判断当前进程的调度策略是否是SCHED_RR的方式,如果是则应将其移动到运行队列尾,这样才能保证系统组同等优先级的SCHED_RR进程得到轮转调度,如果系统发现当前进程的运行状态不是TASK_RUNNING状态或者在TASK_INTERRUPTIBLE状态下收到信号这两种情况之一就要将当前进程从运行队列中删除。
做完上述工作后,以下就即将进行进程切换的工作,为了表明对当前进程的切换请求已经响应,系统将当前进程(进程1的need_resched的字段置为0。
接着进程运行repeat_schedule处开始进行切换调度。
切换前系统首先遍历运行队列并依次调用goodness(函数计算进程的权值并从中取出权值最大者作为将要运行的进程。
goodness(计算的权值与进程的调度策略有关,当进程的调度策略为SCHED_YIELD时直接将其权值置为-1返回,如果为SCHED_OTHER时则以进程的counter值返回,该counter值反映了普通进程占用CPU的时间,它会在每个CPU时间片减1,因此对于普通进程即使
优先级很高,但随着进程的运行,其权值也会逐渐减少从而使其他普通进程得到调度的机会,体现了一种公平调度的原则。
对于SCHED_RR和SCHED_FIFO的实时调度进程则直接将其优先级加上1000作为权值返回。
因此在系统中实时进程是优先得到调度的。
从上面的计算原则可见,当系统中没有实时进程时,计算出的最高权值有可能为零,此时就需要重新设置运行队列中所有进程的counter值,设置的原则就是将进程的当前counter值承2加上有进程nice值通过NICE_TO_TICK宏转换过来的一个值。
然后在重新回到repeat_reschedule处选出将要切换的进程。
在本例中假设通过上述的步骤选出了进程2作为将要运行的进程。
接着schedule(函数就开始真正进行进程切换的工作了,首先是进行两个进程的内存页面的切换,这部分内容是内存管理的内容我们可以不用深究。
切换完内存后,schedule(函数调用switch_to(实现进程的运行切换。
switch_to(函数宏的定义如下:
#defineswitch_to(prev,next,lastdo{\
asmvolatile("pushl%%esi\n\t"\
"pushl%%edi\n\t"\
"pushl%%ebp\n\t"\
"movl%%esp,%0\n\t"/*saveESP*/\
"movl%3,%%esp\n\t"/*restoreESP*/\
"movl$1f,%1\n\t"/*saveEIP*/\
"pushl%4\n\t"/*restoreEIP*/\
"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的prev和last都是当前进程(进程1的任务控制块指针,next为将要运行的进程(进程2的指针。
从上述代码中我们可以看到进入switch_to后首先用三个入栈语句将进程1的esi,edi和ebp入栈。
然后将当前CPU的堆栈寄存器的内容也就是当前进程(进程1的系统堆栈指针保存到当前进程的thread_struct结构的esp中,在此处没有保存堆栈段寄存器SS,原因我们将在后面分析。
保存当前的ESP之后,就通过将将要运行的进程(进程2的thread_struct结构的esp赋给CPU的ESP寄存器而将当前进程(进程1的系统堆栈切换到进程2的系统堆栈中,前面我们已经知道在LINUX下所有的进程调度最终都是通过在系统空间中调用schedule函数来实现的,因此,所有进程的系统堆栈的切入和切出都应该发生在switch_to的堆栈切换的地方,分析这点上堆栈切换情况是分析整个调度的关键。
为此我们将堆栈切换前后的系统堆栈情况分析如下。
切换前的系统堆栈为当前正在运行的进程(进程1的系统堆栈,内容入图4所示。
图4堆栈切换前夕当前进程的系统堆栈情况
我们假设进程2上一次也是在中断的情况下从系统中被切换出去的,则通过堆栈切换后当前的堆栈就应该为进程2的系统堆栈,它应该具有和图3一样的形式。
唯一不同的是栈中的相关寄存器和变量的内容是为近程2保存的。
堆栈切换后switch_to接着往下运行,通过一条赋值语句首先将当前进程(进程1的返回地址(指向switch_to中标号为1处保存到其任务控制块的thread_struct结构的eip字段中,当任务再次被调度时将从这个地方开始运行。
接着将将要运行的进程(进程2的任务控制块中的trhead_struct结构中先前保存的eip值压入进程2的系统堆栈中。
然后通过jmp__switch_to(跳转到__switch_to(函数处执行。
进入该函数后进程2的系统空间堆栈在图3的基础上形成了如图5所示的情况。
图5进入__switch_to(时的系统栈
由于进程2先前切换出去时的流程和进程1一致,因此在此处压入堆栈中的返回地址也应该指向switch_to中的标号为1处。
此处只压入EIP是因为在调__switch_to函数用了一条近调用的CALL指令,因而从__switch_to函数中遇到RET指令返回时只需要将当前栈中的内容POP到EIP中即可(该工作由CPU完成。
由此可见当从__switch_to(中返回时系统已经切换到进程2上运行。
由此可见此处通过巧妙地利用一条jmp指令跳转到__switch_to函数中执行,再利用编译器为该函数加入的返回指令的作用实现了进程运行代码的切换。
因此switch_to中的jmp__switch_to语句为整个LINUX进程调度的进程切换点。
在__switch_to还为进程切换做了以下的辅助工作:
在__swith_to(中做的主要工作是将tss段中的ss0和esp0置为将要运行的进程(进程2的ss0和esp0的,这要才能保证进程2从用户空间通过系统调用或者其他方式进入系统空间时CPU能够正确地将堆栈从进程2的用户堆栈切换到其系统堆栈中。
LINUX正是利用这种方式达到了用一个tss段为系统中所有的进程服务的目的。
从__switch_to(返回后进程2即开始运行,根据前面的堆栈情况,进程2应该从schedule(函数中标号为1的地方开始运行,标号为1的地方开始的是三条POP指令因此进程2在这个地方首先将其在上一次堆栈切换前
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- LINUX 进程 调度 机制 及其 堆栈 切换 分析