linux24调度系统分析.docx
- 文档编号:8649595
- 上传时间:2023-02-01
- 格式:DOCX
- 页数:19
- 大小:37.91KB
linux24调度系统分析.docx
《linux24调度系统分析.docx》由会员分享,可在线阅读,更多相关《linux24调度系统分析.docx(19页珍藏版)》请在冰豆网上搜索。
linux24调度系统分析
Linux2.4调度系统分析
一.前言
在开源操作系统中,Linux的发展最为显著,到目前为止,它在低端服务器市场已经占据了相当大的份额。
从最新的Linux2.6系统来看,Linux的发展方向主要有两个:
嵌入式系统和高端计算领域。
调度系统对于操作系统的整体性能有着非常重要的影响,嵌入式系统、桌面系统和高端服务器对于调度器的要求是很不一样的。
Linux调度器的特点主要有两个:
∙核心不可抢占;
∙调度算法简单有效。
由于Linux适用于多种平台,本文所指缺省为i386下的SMP系统。
二.相关数据结构
在Linux中,进程用task_struct表示,所有进程被组织到以init_task为表头的双向链表中(见[include/linux/sched.h]SET_LINKS()宏),该链表是全系统唯一的。
所有CPU被组织到以schedule_data(对界后)为元素的数组之中。
进程与所运行的CPU之间可以相互访问(详见下)。
所有处于运行态的进程(TASK_RUNNING)被组织到以runqueue_head为表头的双向链表之中,调度器总是从中寻找最适合调度的进程。
runqueue_head也是全系统唯一的。
下面分别介绍这些与调度器工作相关的数据结构。
1.init_tss
TSS,TaskStateSegment,80x86平台特有的进程运行环境,尽管Linux并不使用TSS,但将TSS所需要描述的信息保存在以cpu号为索引的tss_struct数组init_tss中,进程切换时,其中的值将获得更新。
2.task_struct
在Linux中,线程、进程使用的是相同的核心数据结构,可以说,在2.4的内核里只有进程,其中包含轻量进程。
一个进程在核心中使用一个task_struct结构来表示,包含了大量描述该进程的信息,其中与调度器相关的信息主要包括以下几个:
i.state
Linux的进程状态主要分为三类:
可运行的(TASK_RUNNING,相当于运行态和就绪态);被挂起的(TASK_INTERRUPTIBLE、TASK_UNINTERRUPTIBLE和TASK_STOPPED);不可运行的(TASK_ZOMBIE),调度器主要处理的是可运行和被挂起两种状态下的进程,其中TASK_STOPPED又专门用于SIGSTP等IPC信号的响应,而TASK_ZOMBIE指的是已退出而暂时没有被父进程收回资源的"僵尸"进程。
ii.need_resched
布尔值,在调度器中用于表示该进程需要申请调度(详见"调度器工作流程")。
iii.policy
在Linux2.4中,进程的调度策略可以有三种选择:
SCHED_FIFO(先进先出式调度,除非有更高优先级进程申请运行,否则该进程将保持运行至退出才让出CPU)、SCHED_RR(轮转式调度,该进程被调度下来后将被置于运行队列的末尾,以保证其他实时进程有机会运行)、SCHED_OTHER(常规的分时调度策略)。
另外,policy中还包含了一个SCHED_YIELD位,置位时表示主动放弃CPU。
iv.rt_priority
用于表征实时进程的优先级,从1-99取值,非实时进程该项应该为0。
这一属性将用于调度时的权值计算(详见"就绪进程选择算法")。
v.counter
该属性记录的是当前时间片内该进程还允许运行的时间(以CPU时钟tick值为单位,每个进程的counter初值与nice值有关,nice越小则counter越大,即优先级越高的进程所允许获得的CPU时间也相对越多),并参与"就绪进程选择算法"。
在Linux2.4中,每个(非SCHED_FIFO实时)进程都不允许运行大于某一时间片的时间,一旦超时,调度器将强制选择另一进程运行(详见"调度器工作流程")
vi.nice
用户可支配的进程优先级,将参与"就绪进程选择算法",同时该值也决定了该进程的时间片长度(详见下)。
vii.cpus_allowed
以位向量的形式表示可用于该进程运行的CPU(见"调度器工作流程")。
viii.cpus_runnable
以位向量的形式表示当前运行该进程的CPU(相应位为1)。
如果不在任何CPU上运行,则为全1。
这一属性和cpus_allowed属性结合,可以迅速判断该进程是否能调度到某一CPU上运行(位"与")。
ix.processor
本进程当前(或最近)所在CPU编号。
x.thread
用于保存进程执行环境(各个寄存器的值以及IO操作许可权映射表),内容与TSS相近。
因为TSS以CPUid为索引,而Linux无法预测被替换下来的进程下一次将在哪个CPU上运行,所以这些信息不能保存在TSS中。
3.current
核心经常需要获知当前在某CPU上运行的进程的task_struct,在Linux中用current指针指向这一描述符。
current的实现采用了一个小技巧以获得高效的访问速度,这个小技巧与Linux进程task_struct的存储方式有关。
在Linux中,进程在核心级运行时所使用的栈不同于在用户级所分配和使用的栈。
因为这个栈使用率不高,因此仅在创建进程时分配了两个页(8KB),并且将该进程的task_struct安排在栈顶。
(实际上这两个页是在分配task_struct时申请的,初始化完task_struct后即将esp预设为页尾作为进程的核心栈栈底,往task_struct方向延伸。
)
因此,要访问本进程的task_struct,只需要执行以下简单操作:
__asm__("andl%%esp,%0;":
"=r"(current):
"0"(~8191UL));
此句将esp与0x0ffffe0作"与"运算,获得核心栈的首页基址,此即为task_struct的地址。
4.schedule_data
task_struct是用于描述进程的数据结构,其中包含了指向所运行CPU的属性。
在Linux中,另有一个数据结构对应于CPU,可以利用它访问到某CPU上运行的进程,这个数据结构定义为schedule_data结构,包含两个属性:
curr指针,指向当前运行于该CPU上的进程的task_struct,通常用cpu_curr(cpu)宏来访问;last_schedule时间戳,记录了上一次该CPU上进程切换的时间,通常用last_schedule(cpu)宏来访问。
为了使该数据结构的访问能与CPU的Cacheline大小相一致,schedule_data被组织到以SMP_CACHE_BYTES为单位的aligned_data联合数组中,系统中每个CPU对应数组上的一个元素。
5.init_tasks
调度器并不直接使用init_task为表头的进程链表,而仅使用其中的"idle_task"。
该进程在引导完系统后即处于cpu_idle()循环中(详见"其他核心应用的调度相关部分"之"IDLE进程")。
SMP系统中,每个CPU都分别对应了一个idle_task,它们的task_struct指针被组织到init_tasks[NR_CPUS]数组中,调度器通过idle_task(cpu)宏来访问这些"idle"进程(详见"调度器工作流程")。
6.runqueue_head
以runqueue_head为表头的链表记录了所有处于就绪态的进程(当前正在运行的进程也在其中,但idle_task除外),调度器总是从中选取最适合调度的进程投入运行。
回页首
三.进程切换过程
从一个进程的上下文切换到另一个进程的上下文,因为其发生频率很高,所以通常都是调度器效率高低的关键。
在Linux中,这一功能是以一段经典的汇编代码实现的,此处就着力描述这段代码。
这段名为switch_to()的代码段在schedule()过程中调用,以一个宏实现:
/*节选自[include/asm-i386/system.h]*/
#defineswitch_to(prev,next,last)do{\
asmvolatile("pushl%%esi\n\t"\
"pushl%%edi\n\t"\
"pushl%%ebp\n\t"\保存esi、edi、ebp寄存器
"movl%%esp,%0\n\t"\esp保存到prev->thread.esp中
"movl%3,%%esp\n\t"\从next->thread.esp恢复esp
"movl$1f,%1\n\t"
\在prev->thread.eip中保存"1:
"的跳转地址,
\当prev被再次切换到的时候将从那里开始执行
"pushl%4\n\t"
\在栈上保存next->thread.eip,__switch_to()返回时将转到那里执行,
\即进入next进程的上下文
"jmp__switch_to\n"\跳转到__switch_to(),进一步处理(见下)
"1:
\t"\
"popl%%ebp\n\t"\
"popl%%edi\n\t"\
"popl%%esi\n\t"\先恢复上次被切换走时保存的寄存器值,
\再从switch_to()中返回。
:
"=m"(prev->thread.esp),\%0
"=m"(prev->thread.eip),\%1
"=b"(last)\ebx,
\因为进程切换后,恢复的栈上的prev信息不是刚被切换走的进程描述符,
\因此此处使用ebx寄存器传递该值给prev
:
"m"(next->thread.esp),\%3
"m"(next->thread.eip),\%4
"a"(prev),"d"(next),\eax,edx
"b"(prev));\ebx
}while(0)
进程切换过程可以分成两个阶段,上面这段汇编代码可以看作第一阶段,它保存一些关键的寄存器,并在栈上设置好跳转到新进程的地址。
第二阶段在switch_to()中启动,实现在__switch_to()函数中,主要用于保存和更新不是非常关键的一些寄存器(以及IO操作许可权映射表ioperm)的值:
∙unlazy_fpu(),如果老进程在task_struct的flags中设置了PF_USEDFPU位,表明它使用了FPU,unlazy_fpu()就会将FPU内容保存在task_struct:
:
thread中;
∙用新进程的esp0(task_struct:
:
thread中)更新init_tss中相应位置的esp0;
∙在老进程的task_struct:
:
thread中保存当前的fs和gs寄存器,然后从新进程的task_struct:
:
thread中恢复fs和gs寄存器;
∙从新进程的task_struct:
:
thread中恢复六个调试寄存器的值;
∙用next中的ioperm更新init_tss中的相应内容
switch_to()函数正常返回,栈上的返回地址是新进程的task_struct:
:
thread:
:
eip,即新进程上一次被挂起时设置的继续运行的位置(上一次执行switch_to()时的标号"1:
"位置)。
至此转入新进程的上下文中运行。
在以前的Linux内核中,进程的切换使用的是farjmp指令,2.4采用如上所示的手控跳转,所做的动作以及所用的时间均与farjmp差不多,但更利于优化和控制。
回页首
四.就绪进程选择算法
Linuxschedule()函数将遍历就绪队列中的所有进程,调用goodness()函数计算每一个进程的权值weight,从中选择权值最大的进程投入运行。
进程调度权值的计算分为实时进程和非实时进程两类,对于非实时进程(SCHED_OTHER),影响权值的因素主要有以下几个:
1.进程当前时间片内所剩的tick数,即task_struct的counter值,相当于counter越大的进程获得CPU的机会也越大,因为counter的初值与(-nice)相关,因此这一因素一方面代表了进程的优先级,另一方面也代表了进程的"欠运行程度";(weight=p->counter;)
2.进程上次运行的CPU是否就是当前CPU,如果是,则权值增加一个常量,表示优先考虑不迁移CPU的调度,因为此时Cache信息还有效;(weight+=PROC_CHANGE_PENALTY;)
3.此次切换是否需要切换内存,如果不需要(或者是同一进程的两个线程间的切换,或者是没有mm属性的核心线程),则权值加1,表示(稍微)优先考虑不切换内存的进程;(weight+=1;)
4.进程的用户可见的优先级nice,nice越小则权值越大。
(Linux中的nice值在-20到+19之间选择,缺省值为0,nice()系统调用可以用来修改优先级。
)(weight+=20-p->nice;)对于实时进程(SCHED_FIFO、SCHED_RR),权值大小仅由该进程的rt_priority值决定(weight=1000+p->rt_priority;),1000的基准量使得实时进程的权值比所有非实时进程都要大,因此只要就绪队列中存在实时进程,调度器都将优先满足它的运行需要。
如果权值相同,则选择就绪队列中位于前列的进程投入运行。
除了以上标准值以外,goodness()还可能返回-1,表示该进程设置了SCHED_YIELD位,此时,仅当不存在其他就绪进程时才会选择它。
如果遍历所有就绪进程后,weight值为0,表示当前时间片已经结束了,此时将重新计算所有进程(不仅仅是就绪进程)的counter值,再重新进行就绪进程选择(详见"调度器工作流程")。
回页首
五.调度器
Linux的调度器主要实现在schedule()函数中。
1.调度器工作流程
schedule()函数的基本流程可以概括为四步:
1).清理当前运行中的进程
2).选择下一个投入运行的进程
3).设置新进程的运行环境
4).执行进程上下文切换
5).后期整理
其中包含了一些锁操作:
就绪队列锁runquque_lock,全局核心锁kernel_flag,全局中断锁global_irq_lock,进程列表锁tasklist_lock。
下面先从锁操作开始描述调度器的工作过程。
A.相关锁
∙runqueue_lock,定义为自旋锁,对就绪队列进行操作之前,必须锁定;
∙kernel_flag,定义为自旋锁,因为很多核心操作(例如驱动中)需要保证当前仅由一个进程执行,所以需要调用lock_kernel()/release_kernel()对核心锁进行操作,它在锁定/解锁kernel_flag的同时还在task_struct:
:
lock_depth上设置了标志,lock_depth小于0表示未加锁。
当发生进程切换的时候,不允许被切换走的进程握有kernel_flag锁,所以必须调用release_kernel_lock()强制释放,同时,新进程投入运行时如果lock_depth>0,即表明该进程被切换走之前握有核心锁,必须调用reacquire_kernel_lock()再次锁定;
∙global_irq_lock,定义为全局的内存长整型,使用clear_bit()/set_bit()系列进行操作,它与global_irq_holder配合表示当前哪个cpu握有全局中断锁,该锁挂起全局范围内的中断处理(见irq_enter());
∙tasklist_lock,定义为读写锁,保护以init_task为头的进程列表结构。
B.prev
在schedule中,当前进程(也就是可能被调度走的进程)用prev指针访问。
对于SCHED_RR的实时进程,仅当该进程时间片结束(counter==0)后才会切换到别的进程,此时将根据nice值重置counter,并将该进程置于就绪队列的末尾。
当然,如果当前就绪队列中不存在其他实时进程,则根据前面提到的goodness()算法,调度器仍将选择到该进程。
如果处于TASK_INTERRUPTIBLE状态的进程有信号需要处理(这可能发生在进程因等待信号而准备主动放弃CPU,在放弃CPU之前,信号已经发生了的情况),调度器并不立即执行该进程,而是将该进程置为就绪态(该进程还未来得及从就绪队列中删除),参与紧接着的goodness选择。
如果prev不处于就绪态,也不处于上面这种有信号等待处理的挂起态(prev为等待资源而主动调用schedule()放弃CPU),那么它将从就绪队列中删除,此后,除非有唤醒操作将进程重新放回到就绪队列,否则它将不参与调度。
被动方式启动调度器工作时,当前进程的need_resched属性会置位(见下"调度器工作时机")。
在schedule()中,该位会被清掉,表示该进程已经在调度器中得到了处理(当然,这一处理并不意味着该进程就一定获得了CPU)。
C.goodness
调度器遍历就绪队列中的所有进程,只要它当前可被调度(cpus_runnable&cpus_allowed&(1< next指针用来指向权值最大的进程,缺省指向idle_task,如果就绪队列为空,就使用缺省的idle_task作为next。 正如前面所提到的,如果遍历结束后的最大权值为0,则表示当前所有可被调度的就绪进程的时间片都用完了,这时调度器将需要重新设置所有进程(包括就绪的和挂起的)的counter值,未完成时间片的进程(例如当前被挂起的进程或者当前正在其他CPU上运行的进程),其剩下的时间片的一半将叠加到新的时间片中。 将选中的进程设置为在本CPU上运行(task_set_cpu())之后,runqueue_lock就可以解开了,接下来就将对next进行配置。 D.next 选取的新进程可能刚好就是需要替换出去的老进程,此时因为实际上不需要进行进程切换,所以可以跳过配置next以及下面的"switch"和"schedule_tail"两个阶段。 新进程的运行环境实际上主要就是指内存。 在task_struct中有两个与调度器相关的内存属性: mm和active_mm,前者指向进程所拥有的内存区域,后者则指向进程所实际使用的内存。 对于大多数进程,mm和active_mm是相同的,但核心线程没有自主的内存,它们的mm指针永远为NULL。 我们知道,任何进程的虚页表中,核心空间永远映射到了虚存的高端固定位置,所以,核心线程所使用的内存无论对于哪个进程空间都是一样的,所以也就没有必要切换进程的内存。 在调度器中,只要判断一下next->mm是否为空就能知道该进程是不是核心线程,如果是,则继续使用prev的active_mm(next->active_mm=prev->active_mm),并通过设置cpu_tlbstate[cpu].state为TLBSTATE_LAZY,告诉内存管理部件不要刷新TLB;否则就调用switch_mm()函数进行内存的切换(具体过程牵涉到内存管理模块的知识,这里就从略了)。 实际上,在switch_mm()中还会对prev->active_mm和next->mm判断一次,如果两值相等,说明两个进程是同属于一个"进程"的两个"线程"(实际上是轻量进程),此时也不需要执行内存的切换,但这种情况TLB还是需要刷新的。 设置好next的内存环境以后,就可以调用mmdrop()释放掉prev的内存结构了。 所有不在运行中的进程,其active_mm属性都应该为空。 E.switch 进程切换的过程在上文中已经描述得比较详细了。 F.schedule_tail 完成切换后,调度器将调用__schedule_tail()。 这一函数对于UP系统基本没什么影响,对于SMP系统,如果被切换下来的进程(用p表示)仍然处于就绪态且未被任何CPU调度到,__schedule_tail()将调用reschedule_idle(),为p挑选一个空闲的(或者是所运行的进程优先级比p低的)CPU,并强迫该CPU重新调度,以便将p重新投入运行。 进程从休眠状态中醒来时也同样需要挑选一个合适的CPU运行,这一操作是通过在wake_up_process()函数中调用reschedule_idle()实现的。 挑选CPU的原则如下: ∙p上次运行的CPU目前空闲。 很显然,这是最佳选择,因为不需要抢占CPU,CPUCache也最有可能和p吻合。 不过,既然p可运行,调度器就不可能调度到idle_task,所以这种情况只会发生在wake_up_process()的时候。 ∙所有空闲的CPU中最近最少活跃(last_schedule(cpu)最小)的一个。 该CPU中的Cache信息最有可能是无用的,因此这种选择方式可以尽最大可能减少抢占CPU的开销,同时也尽可能避免频繁抢占。 值得注意的是,在使用支持超线程技术的CPU的SMP平台上,一旦发现一个物理CPU的两个逻辑CPU均空闲,则该CPU的其中一个逻辑CPU立即成为p候选的调度CPU,而不需要继续寻找最近最少活跃的CPU。 ∙CPU不空闲,但所运行的进程优先级比p的优先级低,且差值最大。 计算优先级时使用的是goodness()函数,因为它所包含的信息最多。 找到合适的CPU后,reschedule_idle()就会将目标进程(正在该CPU上运行的进程,可能是idle_task)的need_resched置为1,以便调度器能够工作(见"调度器工作时机")。 同时,因为idle_task很多情况下都使cpu处于停机(halt)状态以节电,所以有必要调用smp_send_reschedule(cpu)向cpu发RESCHEDULE_VECTOR中断(通过IPI接口),以唤醒该cpu。 注: 对于目标进程是idle_task的情况,还要判断它的need_resched标志位,仅当它为0的时候才会启动调度,因为非0状态的idle_task本身一直都在检查need_resched值,它自己会启动schedule()(见下"IDLE进程")。 G.clear 调度器工作的结果有两种: 发生了切换、没有发生切换,但调度器退出前的清理工作是一样的,就是恢复新进程的状态。 主要包含两个动作: ∙清被切换走的进程的SCHED_YIELD位(不管它是否置位); ∙如果新进程(p)的lock_depth大于等于0,则重新为核心锁kernel_flag加锁(见上"相关锁")。 2.调度器工作时机 调度器的启动通常有两种方式: A.主动式 在核心应用中直接调用schedule()。 这通常发生在因等待核心事件而需要将进程置于挂起(休眠)状态的时候--这时应该主动请求调度以方便其他进程使用CPU。 下面就是一个主动调度的例子: /*节选自[drivers/input/mousedev.c]mousedev_read()*/ add_wait_queue(&list->mousedev->wait,&wait); current->state=TASK_INTERRUPTIBLE; while(! list->ready){ if(file->f_flags&O_NONBLOCK){
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- linux24 调度 系统分析
![提示](https://static.bdocx.com/images/bang_tan.gif)