Linux的进程管理由进程控制块、进程调度、中断处理、任务队列、定时器、bottomhalf队列、调用、进程通讯等等部份组成。
进程调用分为实时进程调度和非实时进程调度两种。后者调度时,可以采用基于动态优先级的轮转法(RR),也可以采用先进现出算法(FIFO)。前者调度时,一律采用基于动态优先级的轮转法。某个进程采用何种调度算法由改进程的进程控制块中的个别属性决定,没有专门的系统拿来处理关于进程调度的相关事宜。Linux的进程调度由schedule()函数负责linux内核进程调度,任何进程,当它从系统调用返回时,就会转到schedule(),而中断处理函数完成它们的响应任务之后,也会步入schedule()。
1.进程控制块数据结构
Linux系统的进程控制块用数据结构task_struct表示,这个数据结构占用1680个字节,具体的内容不在这儿介绍linuxvi,详尽内容见《Linux内核2.4版源代码剖析大全》第二页。
进程的状态主要包括如下几个:
TASK_RUNNING正在运行或在就绪队列run-queue中打算运行的进程,实际参与进程调度。
TASK_INTERRUPTIBLE处于等待队列中的进程,待资源有效时唤起,也可由其它进程通过讯号或定时中断唤起后步入就绪队列run-queue。
TASK_UNINTERRUPTIBLE处于等待队列的进程,待资源有效时唤起,不也可由其它进程通过讯号或则定时中断唤起。
TASK_ZOMBIE表示进程结束但仍未衰落的一种状态(僵死),此时,进程早已结束运行而且早已释放了大部份资源,而且仍未释放进程控制块。
TASK_STOPPED进程暂停,通过其它进程的讯号能够唤起。
所有进程(以PCB方式)组成一个单向列表。next_task和prev_task就是数组的前后向表针。数组的头尾都是init_task(init进程)。不过进程还要依照其进程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中节选的:
asmlinkagevoidschedule(void)
structschedule_data*sched_data;
structtask_struct*prev,*next,*p;
structlist_head*tmp;
intthis_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("Schedulingininterrupt/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){
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);
c=-1000;
/*遍历进程就绪队列,假如该进程才能进行调度(对于SMP来说就是判定当前CPU未被占用才能执行这个进程,对于非SMP系统则为1),则估算该进程的优先级,假如优先级小于当前进程,则next表针指向新的进程,循环直至找到优先级最大的那种进程*/
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;
/*判定是否须要重新估算每位进程的时间片,判定的根据是所有正打算进行调度的进程时间片用尽,这时,就须要对就绪队列中的每一个进程都重新估算时间片,之后返回后面的调度过程,重新在就绪队列当中查找优先级最高的进程执行调度。*/
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;
/*CPU私有调度数据中记录当前进程的表针linux防火墙设置,而且将当前进程与CPU绑定,假若待调度进程与后面一个进程属于同一个进程,则不须要调度,直接返回。*/
sched_data->curr=next;
task_set_cpu(next,this_cpu);
spin_unlock_irq(&runqueue_lock);
if(unlikely(prev==next)){
/*Wewon'tgothroughthenormaltail,sodothisbyhand*/
prev->policy&=~SCHED_YIELD;
gotosame_process;
/*全局统计进程上下文切换次数*/
kstat.context_swtch++;
/*倘若后进程的mm为0(未分配页),则检测是否被在被激活的页里(active_mm),否则换页。令后进程记录前进程激活页的信息,将前进程的active_mm中的mm_count值加一。将cpu_tlbstate[cpu].state改为TLBSTATE_LAZY(采用lazy模式)假如后进程的mm不为0(已分配页),但仍未激活,换页。切换mm(switch_mm)。假如前进程的mm为0(已失效),将其激活记录置空,将mm结构引用数减一,删掉该页。*/
prepare_to_switch();
structmm_struct*mm=next->mm;
structmm_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)
gotoneed_resched_back;
return;
3.进程上下文切换(摘自中国Linux峰会一片文章)
首先进程切换须要做哪些?它做的事只是保留正在运行进程的"环境",并把即将运行的进程的"环境"加载上来,这个环境也叫上下文。它包括各个进程"公用"的东西,例如寄存器。
下一个问题,旧的进程环境保存在那,新的进程环境从那来,在i386上,有个tss段,是专用来保存进程运行环境的。在Linux来说,在结构task_struct中有个类型为structthread_struct的成员叫tss,如下:
structtask_struct{
。。。
/*tssforthistask*/
structthread_structtss;
。。。
};
它是专用来储存进程环境的,这个结构体因CPU而异,你看它能够晓得有这些寄存器是须要保存的了。
最后的问题就是切换了,尽管在i386上CPU可以手动按照tss去进行上下文的切换,并且Linux的程序员们更乐意自己做它,缘由是这样能得到更有效的控制,并且作者说这和硬件切换的速率差不多,这而且真的够伟大的。
好了,如今来看源码,进程切换是使用switch_to这个宏来做的,当步入时prev即是现今运行的进程,next是接出来要切换到的进程,
#defineswitch_to(prev,next,last)do{/
asmvolatile(
"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"/*saveESP*//
"movl%3,%%esp/n/t"/*restoreESP*//
//之后使进程prev的表针保存为标号为1的那一个表针,这样上次进程prev可以运行时,它第一个执行的就是pop指令。
"movl$1f,%1/n/t"/*saveEIP*//
//把进程next保存的表针推动堆栈中,这句作用是,从__switch_to返回时,下一个要执行的指令将会是这个表针所指向的指令了。
"pushl%4/n/t"/*restoreEIP*//
//使用jump跳到__switch_to函数的结果是:调用switch_to函数但不象call那样要压栈,然而ret返回时linux内核进程调度,仍是要弹出堆栈的,也就是上条指令中推动去的指令表针。这样,堆栈和指令都换了,进程也就被"切换"了。
"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表的结构了,它的作用是把进程next的tss描述符的type中的第二位清0,那位表示这个描述符是不是当前正在用的描述符,作者说假如不清0就把它load进tss段寄存器的话,系统会报异常(我可没试过)。
gdt_table[next->tss.tr>>3].b&=0xfffffdff;
//把进程next的tss段load到tss段存器中。
asmvolatile("ltr%0"::"g"(*(unsignedshort*)&next->tss.tr));
//保存进程prev的fs和gs段寄存器
asmvolatile("movl%%fs,%0":"=m"(*(int*)&prev->tss.fs));
asmvolatile("movl%%gs,%0":"=m"(*(int*)&prev->tss.gs));
之后下边就是load进程next的ldt,页表,fs,gs,debug寄存器。
由于Linux通常并不使用ldt,所以它们通常会指向一个共同的空的ldt段描述符,这样就可能不须要切换ldt了,若果进程next和prev是共享显存的话,这么页表的转换也就毋须要了(这通常发生在clone时)。
本文原创地址://gulass.cn/ldjckzkjcddz.html编辑:刘遄,审核员:暂无