我是靠谱客的博主 生动棒球,最近开发中收集的这篇文章主要介绍linux 公平 队列_公平调度(fair-share scheduling)的进程调度算法 | 学步园,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

调度程序schedule():

调度程序schedule()是一个非常频繁地执行的函数,因此要将运行效率放在第一位,函数中使用了很多的goto语句。

前面讲过,对schedule()只能由进程在内核中主动

调用,或者在当前进程从系统空间返回用户空间的前夕被动的

发生,而不能在一个中断服务程序的内部发生。即使一个中断服务程序有调度的要求,也只能通过把当前进程的need_resched字段设为1来表达这种要求,而不能直接调用schedule()。所以,如果在某个中断服务程序内部调用了schedule(),那一定是有问题的,所以转向scheduling_in_interrupt.(kernel/sched.c)

asmlinkage void schedule(void)

509 {

510

struct schedule_data * sched_data;

511 struct task_struct *prev, *next,

*p;

512 struct list_head *tmp;

513 int this_cpu, c;

514

515 if

(!current>

active_mm) BUG();

516 need_resched_back:

517 prev =

current;

518 this_cpu = prev>

processor;

519

520 if

(in_interrupt())

521 goto scheduling_in_interrupt

;

522

523

release_kernel_lock(prev, this_cpu);

524

525 /* Do "administrative" work

here while we don't hold any locks */

526 if (softirq_active(this_cpu) &

softirq_mask(this_cpu))

/*检查是否有内核软中断服务请求在等待,若有,就转入handle_softirq为这些请求服务*/

527

goto handle_softirq;

528

handle_softirq_back:

我们来看一下内核对这种问题的响应:

[schedule()]

686

scheduling_in_interrupt:

687   printk("Scheduling in interrupt/n");

688

BUG();

689

return;

内核对此的响应是显示或者在/var/log/messages文件末尾添上一条出错信息,然后执行一个宏操作BUG。

接着往下看schedule():

如果有内核软中断服务请求在等待,那么就转入

handle_softirq:

[schedule()]

675 handle_softirq:

676

do_softirq();

677    goto handle_softirq_back;

执行softirq队列完毕以后继续往下看:

====================

kernel/sched.c 528 541 ====================

[schedule()]

528 handle_softirq_back:

529

530 /*

531 *

'sched_data' is protected by the fact that we can run

532 * only one process

per CPU.

533 */

534 sched_data = &

aligned_data[this_cpu].schedule_data;

535

536

spin_lock_irq(&runqueue_lock);

537

538

/* move an exhausted RR process to be last.. */

539 if (prev>policy == SCHED_RR)

540 goto move_rr_last;

541

move_rr_back:

指针sched_data指向一个schedule_data数据结构,用来保存供下一次调度时使用的信息。此数据结构的定义如下:

====================

kernel/sched.c 91 101 ====================

91 /*

92 * We align

perCPU

scheduling data on cacheline boundaries,

93 * to prevent cacheline

pingpong.

94 */

95 static union {

96   struct schedule_data {

97

struct task_struct * curr;

98     cycles_t

last_schedule;

99    }schedule_data;

100    char __pad [SMP_CACHE_BYTES];

101 }

aligned_data [NR_CPUS

]

__cacheline_aligned = {

{{&init_task,0}}};

这里的cycles_t实际上是无符号整数,用来记录调度发生的时间。这个数据结构是为多处理器SMP结构而设的,因此我们不必关心。数组中的第一个元素,即CPU0的schedule_data结构初始化为{&init_task,0},其余的则全为{0,0}。代码中的__cacheline_aligned表示数据结构的起点应与高速缓存中的缓冲线对齐。

下面就要涉及可执行进程队列了,所以先将这个队列锁住(536行),以防止其他处理器的干扰。从538行开始:如果当前进程prev的调度策略是SCHED_RR,也就是轮换调度,那就要先进行一点特殊的处理(540 : goto move_rr_last;

)。

(对使用SCHED_RR策略的当前进程的处理)

==================== kernel/sched.c 679 685

====================

[schedule()]

679  move_rr_last:

680   if

(!prev>counter) {

681       prev>counter = NICE_TO_TICKS

(prev>nice);

682

move_last_runqueue(prev);

683     }

684 goto move_rr_back;

这里的prev>counter:代表这当前进程的运行时间配额,其数值在每次时钟中断时都要递减(update_process_times()中实现的)。因此,不管一个进程的时间配额有多高,随着运行时间的积累最终总会递减到0。对于调度策略为SCHED_RR的进程,一旦其时间配额降到0,就要从可执行进程队列runqueue

中当前的位置上移动到队列的末尾,同时恢复其最初的时间配额(NICE_TO_TICKS

),以等待下一次的调度。对于具有相同优先级的进程,调度的时候排在前面的进程优先,所以这使队列中具有相同优先级的其他进程有了优势。

宏操作NICE_TO_TICKS根据系统时钟的精度将进程的优先级别换算成可以运行的时间配额。在kernel/sched.c中定义。

将一个进程的task_struct结构从可执行队列中的当前位置移到队列的末尾是由

move_last_runqueue()完成的(kernel/sched.c)。把进程移到可执行进程队列的末尾意味着:如果队列中没有资格更高的进程,但是有一个资格与之相同的进程存在,那么,这个资格虽然相同而排在前面的进程会被选中。

继续看schedule():

====================

kernel/sched.c 541 553 ====================

[schedule()]

541 move_rr_back:

542

543 switch (prev>state

) {

544 case

TASK_INTERRUPTIBLE:

545    if (signal_pending(prev)) {

546

prev>state = TASK_RUNNING;

547       break;

548     }

549

default:

550    del_from_runqueue(prev);

551 case TASK_RUNNING:

552

}

553 prev>need_resched =

0;

当前进程,就是正在执行的进程,当进入schedule()时其状态却不一定是TASK_RUNNING。例如:当前进程如已经在do_exit()中将其状态改成TASK_ZOMBIE,又如当前进程在sys_wait4()中调用schedule()时的状态为TASK_INTERRUPTIBLE。所以,这里的prev>state与其说是当前进程的状态不如说是其意愿。当其意愿既不是继续执行也不是可中断的睡眠时,就要通过del_from_runqueue()把这个进程从可执行队列中撤下来。另一方面,也可以看出TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE两种睡眠状态之间的区别:

前者在进程有信号等待处理时要将其改成TASK_RUNNING,让其处理完这些信号再说,而后者则不受信号的影响。

最后,将prev>need_resched恢复为0,因为所需求的调度已经在进行了。

下面的任务就是要挑选出一个进程来运行了(这一部分是很重要的,通过对就绪进程队列进行扫描)。

====================

kernel/sched.c 555 576 ====================

[schedule()]

555 /*

556 *

this is the scheduler proper:

557 */

558

559 repeat_schedule:

560

/*

561 * Default process to select..

562 */

563 next = idle_task

(this_cpu);

564

c = 1000;

565 if (prev>state ==

TASK_RUNNING

)

566      goto still_running;

567

568

still_running_back:

569      list_for_each

(tmp,

&runqueue_head) {

570          p = list_entry(tmp, struct task_struct,

run_list);

571          if (can_schedule(p, this_cpu)) {

572           int

weight = goodness

(p, this_cpu,

prev>active_mm);

573           if (weight > c

)

574

c = weight, next = p;

575          }

576

}

在这段程序中,next总是指向已知最佳的候选进程,c则是这个进程的综合权值,或者是运行资格。

挑选的过程是从idle进程即0号进程开始,其权值为-1000,这是可能的最低值,表示仅在没有其他进程可以运行时才会让他运行。

然后,遍历可执行队列runqueue中的每个进程(在单CPU系统中can_schedule()的返回值永远是1),也就是一般操作系统书中所称的就绪进程。为每一个就绪进程通过函数goodness

()计算出他当前所具有的权值,然后与当前的最高值c相比。注意这里的条件:weight > c,

这意味着“先入为大”

。也就是说,如果两个进程有相同的权值的话,排在队列前面的进程胜出,优先运行。

这里还有一个小插曲:如果当前进程的意图是继续运行,那么就要先执行一下still_running(kernel/sched.c):

==================== kernel/sched.c 670 674

====================

[schedule()]

670 still_running:

671    c =

goodness(prev, this_cpu, prev>active_mm);

672    next = prev;

673

goto

still_running_back;

674

也就是说,如果当前进程想要继续运行,那么在挑选候选进程时以当前进程此刻的权值开始比较。而且这意味着,对于具有相同权值的其他进程来说,当前进程优先。

那么,进程的当前权值是怎样计算的呢?也就是goodness()是怎样执行的呢?

====================

kernel/sched.c 123 187 ====================

[schedule()>goodness()

]

123

/*

124 * This is the function that decides how desirable a process

is..

125 * You can weigh different processes against each other

depending

126 * on what CPU they've run on lately etc to try to handle

cache

127 * and TLB miss penalties.

128 *

129 * Return values:

130 *

1000:never select this

131 * 0: out of time, recalculate counters (but it

might still be

132 * selected)

133 * +ve: "goodness" value (the larger,

the better)

134 * +1000: realtime process, select this.

135

*/

136

137 static inline int goodness(struct task_struct * p, int

this_cpu, struct mm_struct *this_mm)

138 {

139 int weight;

140

141

/*

142 * select the current process after every other

143 * runnable

process, but before the idle thread.

144 * Also, dont trigger a counter

recalculation.

145 */

146 weight = -1

;

147 if

(p>policy & SCHED_YIELD

)

148 goto

out;

149

150 /*

151 * Non RT process normal case first.

152

*/

153 if (p>policy

== SCHED_OTHER

) {

154 /*

155 * Give the process a

firstapproximation goodness value

156 * according to the number of clockticks

it has left.

157 *

158 * Don't do any other calculations if the time slice

is

159 * over..

160 */

161    weight =

p->counter;

162    if (!weight)

163    goto out;

164

165

#ifdef CONFIG_SMP

166 /* Give a largish advantage to the same processor...

*/

167 /* (this is equivalent to penalizing other processors) */

168 if

(p->processor == this_cpu)

169    weight += PROC_CHANGE_PENALTY;

170

#endif

171

172 /* .. and a slight advantage to the current MM */

173

if (p->mm == this_mm || !p->mm)

174       weight += 1;

175

weight += 20-

p>nice;

176    goto out;

177 }

178

179 /*

180 *

Realtime process, select the first one on the

181 * runqueue (taking

priorities within processes

182 * into account).

183 */

184      weight

= 1000 + p->rt_priority;

185 out:

186      return weight;

187

}

*首先,如果一个进程通过系统调用sched_yield()明确表示了“礼让”后,就将其权值定位-1。这是很低的权值,一般就绪进程的权值至少是0。

*对于没有实时要求的进程

,即调度策略为SCHED_OTHER的进程,其权值主要取决于两个因素:一个是剩下的时间配额p->counter

,如果用完了则权值为0。另一个是进程的优先级nice,这是从早期Unix沿用下来的负向优先级

(越负,优先级越高),其取值范围为19~-20,只有特权用户才能把nice值设置为小于0。所以,综合的权值weight在时间配额尚未用完时基本上是二者之和。此外,如果是内核线程,或者其用户

空间与当前进程的相同,因而无需切换用户空间,则会得到一点小“奖励”,将权值额外加1。

*对于实时进程,即调度策略为SCHED_FIFO或者SCHED_RR的进程,则另有一种正向的优先级

,那就是实时优先级rt_priority

,而权值为1000 +

p->rt_priority。可见,SCHED_FIFO或者SCHED_RR两种有时间要求的策略赋予进程很高的权值(相对于SCHED_OTHER)。这种进程的权值至少是1000。另一方面,rt_priority的值对于实时进程之间的权值比较也起着重要的作用,其数值也是在sched_setscheduler()中与调度策略一起设置的。

从上面可以看出:对于这两种实时调度策略,一个进程已经运行了多久,即时间配额p->counter

的当前值,对权值的计算不起所用。不过,前面讲到,对于使用SCHED_RR策略地进程,当

p->counter

达到

0时会导致将进程移到队列尾部。

实时进程的nice数值与优先级无关,但是对使用SCHED_RR策略地进程的时间配额大小有关(宏操作NICE_TO_TICKS())。由于实时进程的权值有个很大的基数(1000),因此当有实时进程就绪时,非实时进程是没有机会运行的。

由此可见,linux内核中对权值的计算是很简单的,但是goodness()函数并不代表linux调度算法的全部,而要与前面讲到的对SCHED_RR进程的特殊处理

、对意欲继续运行的当前进程的特殊处理

‘以及下面要讲到的recalculate

结合起来分析。

上面still_running_back运行结束后,变量c的值有几种可能:一种可能是一个大于0的正数,此时next指向挑选出来的进程;另一种可能是c的值为0

,发生于就绪队列中所有进程的权值都是0的时候。由于除了init进程和调用了sched_yield()的进程以外,每个进程的权值最低为0,所以只要队列中有其他就绪进程存在就不可能为负数。因此,队列中所有其他进程的权值都已经降到0了,说明这些进程的调度策略都是SCHED_OTHER,即系统中当前没有就绪的实时进程,因为如果有策略为SCHED_FIFO或者SCHED_RR的进程存在,其权值至少也有1000。

let`s go

on:回到schedule()

==================== kernel/sched.c 578 580

====================

[schedule()]

578 /* Do we need to

recalculate

counters? */

579 if (!c)

580 goto

recalculate;

如果当前已经选择的进程(权值最高的进程)的权值为0,那就要重新计算各个进程的时间配额。如上所述,这说明系统中当前没有就绪的实时进程。而且,这种情况已经持续了一段时间,否则SCHED_OTHER进程的权值就没有机会消耗到0。

====================

kernel/sched.c 658 669 ====================

[schedule()]

658

recalculate:

659 {

660     struct task_struct *p;

661

spin_unlock_irq(&runqueue_lock);

662

read_lock(&tasklist_lock);

663     for_each_task

(p)

664

p->counter = (p->counter >> 1) +

NICE_TO_TICKS(p->nice);

665

read_unlock(&tasklist_lock);

666

spin_lock_irq(&runqueue_lock);

667 }

668  goto repeat_schedule;

这里所作的运算是将每个进程的当前的时间配额p->counter除以2,再在上面加上由该进程的nice值换算过来的tick数量。宏操作NICE_TO_TICKS的定义在前面已经见过,显然nice值对于非实时进程既表示优先级也决定着时间配额。

注意:这里的for_each_task()

是对所有进程的循环,而并不是仅对就绪进程队列的循环,对于不再就绪进程队列中的非实时进程

,这里得到了提升其时间配额、从而提升其综合权值的机会。不过,这种对综合权值的提升是很有限的,每次重新计算都将原有的时间配额减半,再与NICE_TO_TICKS(p->nice)相加,这样就决定了重新计算以后的综合权值永远也不可能达到NICE_TO_TICKS(p->nice)的两倍。因此,即使经过很长时间的韬光养晦,也不可能达到可与实时进程竞争的地步,所以只是对非实时进程之间的竞争有意义。

至于实时进程,时间配额的增加并不会提升其综合权值,而且对于SCHED_FIFO进程,时间配额就没有什么意义。

重新计算完权值以后,程序转回

repeat_schedule(跳回前面,再次执行挑选进程)处重新挑选。这样,当再次完成对就绪进程队列的扫描时,变量c的值就应该不为0了,此时next指向挑选出来的进程。

至此,已经挑选好进程了(权值最高的进程)。

还没有结束阿?哈哈

进程挑好之后,接下来要做的就是切换的事情了。

[schedule()]

581

/*

582 * from this point on nothing can prevent us from

583 * switching to

the next task, save this fact in

584 * sched_data.

585 */

586 sched_data>curr =

next;

587 #ifdef CONFIG_SMP

.....

590 #endif

591

spin_unlock_irq(&runqueue_lock);

592

593 if (prev == next

)

594     goto same_process;

595

596 #ifdef

CONFIG_SMP

==================== kernel/sched.c 612 657

====================

612 #endif /* CONFIG_SMP */

613

614

kstat.context_swtch++;

615 /*

616 * there are 3 processes which are

affected by a context switch:

617 *

618 * prev == .... ==> (last =>

next)

620 * It's the 'much more previous' 'prev' that is on next's

stack,

621 * but prev is set to (the just run) 'last' process by

switch_to().

622 * This might sound slightly confusing but makes tons of

sense.

623 */

624 prepare_to_switch

();

625

{

626   struct mm_struct *mm = next->mm;

627   struct mm_struct *oldmm

= prev->active_mm;

628   if (!mm) {

629         if (next>active_mm)

BUG();

630         next>active_mm = oldmm;

631

atomic_inc(&oldmm>mm_count);

632          enter_lazy_tlb(oldmm, next,

this_cpu);

633 } else {

634      if (next>active_mm != mm)

BUG();

635     switch_mm(oldmm, mm, next, this_cpu);

636 }

637

638

if (!prev>mm) {

639       prev>active_mm = NULL;

640

mmdrop(oldmm);

641 }

642 }

643

644 /*

645 * This just switches

the register state and the

646 * stack.

647 */

648 switch_to(prev,

next, prev);

649 __schedule_tail(prev);

650

651 same_process:

652

reacquire_kernel_lock(current);

653     if (current>need_resched)

654

goto need_resched_back;

655

656    return;

跳过对SMP结构的条件编译部分。

首先,如果挑选出来的进程next就是当前进程prev,就不用切换,直接跳转到same_process处就返回了。这里的reacquire_kernel_lock()对于i386单CPU结构而言是空语句。前面已经把当前进程的need_resched清0,如果现在又成了非0,则一定是发生了中断并且情况有了变化,所以转回need_resched_back再调度一次。

否则,如果挑选出来的进程next与当前进程prev不同,那就要切换了。对于i386单CPU结构而言,prepare_to_switch()也是空语句。而649行的__schedule_tail()则只是将当前进程prev的task_struct结构中的policy字段里的SCHED_YIELD标志位清成0。所以实际上只剩下了两件事:对用户虚存空间的处理;进程的切换switch_to()。

最后

以上就是生动棒球为你收集整理的linux 公平 队列_公平调度(fair-share scheduling)的进程调度算法 | 学步园的全部内容,希望文章能够帮你解决linux 公平 队列_公平调度(fair-share scheduling)的进程调度算法 | 学步园所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(42)

评论列表共有 0 条评论

立即
投稿
返回
顶部