[修改回复]
删除回复
插入表情:
宋体
楷体
幼圆
黑体
隶书
华文行楷
方正舒体
Arial
Arial Black
Arial Narrow
Century Gothic
Comic Sans MS
#0000FF
#8A2BE2
#DEB887
#5F9EA0
#7FFF00
#000000
#D2691E
#FF7F50
#FF0000
#DC143C
#99ccff
字体颜色
#FFF8DC
#00FFFF
#EE82EE
#F5DEB3
#FFFFFF
#F5F5F5
#FFFF00
#9ACD32
使用帮助
4. 调度器主函数schedule()(kernel/sched.c) schedule()是用来挑选出下一个应该执行的进程,并且完成进程切换的工作,是进程 调度的主要执行者,也是操作系统Kernel很重要的一个函数,它的性能将直接决定操 作系统的性能。 (1) 函数主要流程 两个重要数据:prev和next prev 当前进程,也就是即将被切换出CPU的进程 next 下一个进程,也就是即将被切换进CPU的进程 准备工作 a. 做原子操作方面的检查(主要是检查内核抢占和内核锁的深度是否一致); b. 关闭内核抢占(通过函数preempt_disable(),详见"内核可抢占"一节),因为此 时将要对内核一系列重要数据进行操作,所以必须将内核抢占关闭; c. 将当前进程current赋值给prev,获取当前CPU的运行队列rq,释放prev的内核锁 (因为即将对prev做一系列操作),计算prev的运行时间(如果是交互进程则给予run_time 上的奖励,详见"interactive_credit"一节),给rq上自旋锁(防止其他进程访问rq ); d. 进行内核的数据统计(如上下文切换次数等),如果prev处于可中断状态,而且 有信号等待处理,则将prev状态置为TASK_RUNNING,否则将prev从rq中删除。(这一 部分的代码主要是因为在进程在转入睡眠状态时,需要主动调用schedule()函数); e. 如果rq中就绪进程个数为0,而且系统是SMP,则进行负载均衡的操作(详见"负载 均衡"一节),否则将next置为idle进程,赋值rq->expired_timestamp = 0(具体含 义参见"expired_timestamp"的介绍一节),然后直接进行进程切换。 寻找最高优先级进程 a. 如果rq的active array中进程个数为0,则将active array和expired array进行 切换。具体的过程由以下代码完成: array = rq->active; rq->active = rq->expired; rq->expired = array; rq->expired_timestamp = 0; rq->best_expired_prio = MAX_PRIO; b. 用函数sched_find_first_bit()找到优先级最高的进程队列的偏移量idx,那么queue [idx]->next即为所找的next,可以通过以下三行代码快速完成: idx = sched_find_first_bit(array->bitmap); queue = array->queue + idx; next = list_entry(queue->next, task_t, run_list); c. 如果next是从TASK_INTERRUPTIBLE状态中被唤醒的(actived>0),则将进程从就 绪队列中删除,将进程在就绪队列上的等待时间也加在等待时间里面重新计算进程的 prio(详见"平均等待时间"一节),再根据新的优先级将进程插入相应就绪队列。 进程切换 a. 如果prev!=next,则进行进程切换; b. 进行进程切换前的准备:将当前时间赋给next->timestamp,并且将rq->curr=next ;可见此时的rq->curr与current不再相同; c. 进程切换,包括内存、堆栈切换等。具体过程和Kernel 2.4大致相同,在这里不 再赘述; 完成进程切换后 完成进程切换过后,还需要进行释放prev的mm,给rq解锁,重新给current获得内核 锁(注意在此时current=next=rq->curr),使能内核抢占;最后检查TIF_NEED_RESCHED 位,如果已被置位,则重新开始进行调度,重复上述过程;否则调度结束。 (2) 函数执行时机 schedule()函数何时被调用,如何被调用也是一个非常重要的问题。 在Kernel 2.4里面,schedule()函数可以通过两种方式调用: 一种是主动调度,直接调用函数schedule(),如进程退出,或者进入睡眠状态等。 一种是强制性调度,置位当前进程task_struct里面的need_resched。当是从内核态 返回用户态的时候将检查这个位,如果发现已经被置位,会调用schedule();有以下 三种情况可能会置位need_resched: a. 时钟中断服务程序中,发现进程已经用完自己的时间片,需要被切出CPU; b. 当唤醒一个睡眠进程时,发现该进程比当前占有CPU的进程更有运行资格; c. 一个进程通过系统调用改变调度政策、nice值等。 和主动调度相比,强制性调度有一定的调度延时。 Kernel2.6的调度时机包含了Kernel 2.4的调度时机(不同的就是need_resched变成 了一个bit)同时加入了一个重要的特性--内核可抢占,具体的分析见"内核可抢占" 一节。 5. 进程调度的生与死 这一部分分析了系统调度器开始工作的时机,以及一个进程从创建到灭亡过程中和进 程调度相关的信息和函数。 (1) 系统启动时进程调度的初始化 -- sched_init() 系统进程调度的初始化由sched_init()函数完成,它被init/main.c中函数start_kernel ()调用,该函数主要完成以下工作: a. 对于所有的CPU,完成runqueue的初始化工作; b. 对于SMP,要获取第一个进程的CPU号; c. 调用wake_up_forked_process()(参见下面"wake_up_forked_process"一节)来 唤醒当前进程; d. 初始化timer (2) 创建新进程时的调度信息改变 -- sched_fork(task_t *p) 当当前进程fork出一个新进程的时候,需要改变新进程的调度信息,该函数主要的调 用关系是:kenel/fork.c - do_fork()->copy_process->sched_fork(),函数主要完 成: a. 将进程状态置为TASK_RUNNIG,但并未将它加入runqueue,主要是为了保证没有其 他人运行该程序,并且信号或者其他外部事件都不能将它唤醒; b. 初始化进程的runlist、array、自旋锁(开子进程的自旋锁,直到fork结束,返 回用户态时调用函数sched_tail来解锁),preempt_count赋1; c. 将子进程的first_timeslice置1,标志这是子进程第一次分配到时间片; d. 将父进程时间片的一半赋给子进程,同时父进程的时间片减半(这样是为了防止 一个进程通过不停的fork出子进程来占有CPU);如果父进程的时间片此时变为0,则 将其时间片置为1,相当于此时父进程即将用完其时间片,调用scheduler_tick()来 开始新的调度(具体见"scheduler_tick"一节)。 (3) 初始化新进程的统计信息 -- wake_up_forked_process(task_t * p) 该函数是每一个刚被fork出来的进程必须执行的函数,被kernel/fork.c中的do_fork ()函数调用,函数主要完成一些fork出的新进程统计信息的初始化,主要包括: a. 父进程和子进程sleep_avg的变化(请参照"sleep_avg进程创建"一节); b. 子进程的interactive_credit置为0,重新计算子进程的prio,设置子进程的cpu 号; c. 如果当前进程不在任何active array中(如idle进程),则调用__activate_task (p, rq)将子进程加入到active array里面;否则将父进程的动态优先级赋给子进程 ,并且将子进程添加到父进程的运行队列中去。 (4) 创建进程完毕 -- schedule_tail() 这个函数是在fork()系统调用即将完成,返回用户态之前,经过entry.S时调用的。 函数主要完成一些fork完毕需做的清理工作,如释放上文所说的自旋锁等。 (5) 进程运行过程中 -- scheduler_tick() update_process_time()(被时钟中断服务程序调用)调用该函数来更新当前进程的 时间片,并且根据减小后的结果进行相应的处理。函数主要完成: a. 完成当前进程使用的系统时间、用户时间的统计信息; b. 如果当前进程是实时进程,调度策略是SCHED_RR(调度策略是SCHED_FIFO的进程 不需要重新分配时间片),且已经用完时间片,则重新计算时间片,将(表明该进程 退出时不会把时间片交还给父进程),置位TIF_NEED_RESCHED,将进程放到进程队列 的末尾; c. 如果不是实时进程,且用完时间片: a). 置位TIF_NEED_RESCHED,重新计算进程的动态优先级和时间片,将first_timeslice 置0,记录rq-> expired_timestamp的值(意义参见"expired_timestamp"一节); b). 根据TASK_INTERACTIVE()(宏的意义参见"TASK_INTERACTIVE"一节)判断是否交 互进程,用宏EXPIRED_STARVING(rq)判断expired array是否已经饥饿,将该宏展开 后为: (STARVATION_LIMIT && ((rq)->expired_timestamp&&(jiffies- (rq)->expired_timestamp >= STARVATION_LIMIT * ((rq)->nr_running) + 1))) || ((rq)->curr->static_prio > (rq)->best_expired_prio) 可见如果EXPIRED_STARVING()的是否为真与三个因素有关: a. STARVATION_LIMIT为真; b. (rq)->expired_timestamp为真; c. 若(rq)->expired_timestamp >= STARVATION_LIMIT * ((rq)->nr_running) + 1 )(说明expired array上的进程已经等了足够长的时间)为真,或者((rq)->curr-> static_prio > (rq)->best_expired_prio)(说明当前进程的静态优先级比expired array中最高的优先级低)为真。 c). 如果进程被认为是交互进程,而且EXPIRED_STARVING()为假,则将当前进程重新 插入到active array里面(参见"TASK_INTERACTIVE"一节);否则,将进程插入到expired array。 d) 如果进程尚未用完时间片,该进程是交互式进程,且剩余的时间片是该进程时间 片粒度的整数倍(至少1倍),则强行剥夺该进程CPU使用权,且放到active array里 面运行队列的末尾(实际上是在交互式进程内部实行RR策略了)。 时间片粒度的和两个因素有关: a. sleep_avg sleep_avg越大,粒度越大,因为越大说明该进程是交互进程的可能性 越大,交互式进程的特点就是时间片小,频率高;反之,如果是一个CPU-bound进程 就应该少分片或者不分片(尽量避免cache失配),应该有高的粒度; b. CPU个数 CPU个数越多,运行粒度就越大。 (6) 进程状态的相互转换(sleep和wake up) 这里简单的介绍函数wait_for_completion()和try_to_wake_up()。 wait_for_completion() 该函数是标准的将用户由就绪状态转为睡眠状态的函数,主要经过以下几个步骤: a. 通过DECLARE_WAITQUEUE()创建一个等待队列入口; b. 通过函数__add_wait_queue_tail()将进程加入到等待队列; c. 将进程状态置为TASK_UNINTERRUPTIBLE; d. 调用schedule()函数进行调度; e. 利用循环检查进程等待条件是否满足,如果满足通过函数__remove_wait_queue( e. 利用循环检查进程等待条件是否满足,如果满足通过函数__remove_wait_queue( )将进程从等待队列中删除;否则,继续通过schedule()进行调度。 try_to_wake_up() 函数调用activate_task()将进程加到就绪队列中,如果新唤醒的进程的优先级比rq ->curr高(具体原因请参见"curr"一节),则置位TIF_NEED_RESCHED,最后将进程的 状态置为TASK_RUNNING。 (7) 进程结束,退出调度 -- sched_exit(task_t *p) 被release_task()调用,用于处理进程销毁前调度信息的清理,包括: a. 根据p->first_timeslice来判断是否将时间片交还给父进程;如果first_timeslice 的值为1,则说明p尚未用完fork时从父进程分来的时间片,此时应该将时间片交还父 进程;否则,说明子进程已经重新分配过时间片,无须交还; b. 如果子进程(p)的执行时间过长(p->sleep_avg < p->parent->sleep_avg), 则给予父进程一定的惩罚(稍稍减小父进程的sleep_avg)。
不能为空
不能含有 ` 字符,字数8000以内
(CTRL+ENTER提交)
关闭窗口