linux 进程与进程调度
一、调度器设计与实现
1.1 调度器需要满足的需求
- 满足交互式应用的需求。这种应用,这种应用着重于系统的响应速度。
- 满足批处理应用的需求。这种应用要求的是平均速度即效率
- 实时应用。这种应用不但考虑进程执行的平均速度,还要考虑即使速度。也就是考虑响应的速度,以及可预测性。
1.2 考虑的问题
为了满足上述三种应用的需求,在设计调度器时需要考虑以下几个问题:
- 调度的时机:什么情况下,什么时候进行调度。
- 调度的策略:根据什么准则挑选下一个进入运行的进程。
- 调度的方式: 是可剥夺,还是不可剥夺。
1.3 linux的实际实现
1.3.1 调度时机
自愿的调度随时都可以进行,进程可以选择主动放弃CPU
在内核里面,一个进程可以通过schedule()启动一次调度,如果在调用该函数之前,将本进程的状态设置成为TASK_INTERRUPTIBLE 或者TASK_UNINTERRUPTIBLE就可以暂时放弃运行进入睡眠。调度可以非自愿地,在每次从系统调用返回的前夕进行,或者在中断或异常处理返回到用户空间的前夕进行
返回用户空间意味着,只有在用户空间发生的异常或者中断才会引起调度。这一点也是让Linux无法胜任实时应用的原因。假如:在实时应用中,在某个中断的发生,不但要求迅速的中断服务,还需要能够迅速地调度有关进程进入运行。但,如果这样的中断发生在内核中时,本次中断的返回不会引起调度,而是要等到最初使CPU从用户空间陷入内核的那次系统调用或者中断返回用户空间时,才会发生调度。如果内核中这段代码恰好需要较长的时间才能完成的华,调度将会被过分推迟。
注: 不是每次每次从内核返回用户空间的时候都会进行调度。只有再满足了“当前进程应当放弃CPU”的诸多条件的时候,才会引起调度。
1.3.2 调度的方式
linux的调度方式称之为“有条件的可剥夺调度”。当进程在用户空间运行时,不管自愿与否,一旦有必要,内核会暂时剥夺其运行权转而调度其它进程进入运行状态。但,如果进程处于内核空间时,尽管内核有了调度的必要,但实际上调度并不会发生,一直等到返回用户空间前夕才会进行调度。所以linux的调度方式是可剥夺的,但实际运行的时候,由于受到调度时机的限时,变成了有条件的可剥脱调度。
注:有条件可剥夺调度的调度时机也是在从内核空间返回用户空间前夕发生
1.3.3 调度的策略
内核为系统中的每个进程计算出一个反应其运行资格的权值,然后挑选全职最高的投入运行。在运行过程中,当前进程的权值随着时间的流逝而递减。当所有进程的权值都为0的时候,就重新计算一次所有进程的权值。
为了适应上述三种应用的需要,内核在权值计算的基础上实现了三种不同的调度策略。分别是:
- SCHED_FIFO
- SCHED_RR
- SCHED_OTHER
每个进程都可以通过sched_setscheduler()设定自己的调度策略。SCHED_FIFO适合实时性较强,但每次运行时间很短的进程。SCHED_RR适合提及较大,每次运行时间都较长的进程。而SCHED_OTHER适合那些交互式分时进程。这三种调度的策略是在权值计算策略的基础上实现的,实际上这三种调度策略的进程在内核计算运行权值的时候,会考虑进程拥有的调度策略,并且根据不同的调度策略用不同的方式去计算得出权值。这好比在高考时符合某些特殊条件的考生可以获得加分一样。
在调度相同优先级进程的情况下,三种调度策略是有所区别的。对于SCHED_FIFO策略来说,一旦调度策略为SCHED_FIFO的进程投入运行之后,就要一直运行直到该进程资源放弃CPU或者被更高优先级的进程抢占为止(在这一点上与ucos的调度策略是一致的),所以这种策略只适合运行时间短的进程,一旦运行时间过长,就不公平了,因为相同优先级的进程会迟迟得不到调度。而对于SCHED_RR来说,实行的是时间偏轮番调度,这种策略也是基于优先级调度的,但每个进程都会有一个时间片,一旦时间片用完,无论如何,该进程的运行权都会被剥夺。
1.3.4 主动调度的代码分析
在进程退出时,会调用exit系统调用,其内核实现为do_exit()函数。该函数的最后意见事情就是调动schedule()主动放弃CPU的占有权。这里代码如下:
[kernel/sched.c]
526 asmlinkage void schedule(void)
527 {
528 struct schedule_data * sched_data;
529 struct task_struct *prev, *next, *p;
530 struct list_head *tmp;
531 int this_cpu, c;
532
533 /*进程在运行时active_mm一定不是0*/
534 if (!current->active_mm) BUG();
535 need_resched_back:
536 prev = current;
537 this_cpu = prev->processor;
538
539 /*在中断中不能进行调度*/
540 if (in_interrupt())
541 goto scheduling_in_interrupt;
542
543 release_kernel_lock(prev, this_cpu);
544
545 /* Do "administrative" work here while we don't hold any locks */
546 /*每次系统调用返回前夕都要检查软中断*/
547 if (softirq_active(this_cpu) & softirq_mask(this_cpu))
548 goto handle_softirq;
549 handle_softirq_back:
550
551 /*
552 * 'sched_data' is protected by the fact that we can run
553 * only one process per CPU.
554 */
555 /*
556 * 取得下一个将要被装入CPU的进程的struct_task。
557 * typedef struct{task_struct *curr; cycles_t last_schedule;}sched_data;
558 * */
559 sched_data = & aligned_data[this_cpu].schedule_data;
560
561 spin_lock_irq(&runqueue_lock);
562
563 /* move an exhausted RR process to be last.. */
564 /*如果调度策略为SCHED_RR,则进行一些预先的处理*/
565 if (prev->policy == SCHED_RR)
566 goto move_rr_last;
567 move_rr_back:
568
569 /*检查进程的意愿状态,即想要变为哪种状态。
570 * 如果进程将要以TASK_INTURRUPTIBLE状态放弃
571 * CPU的使用权,则先检查是否有信号需要处理,
572 * 如果有信号需要处理,则转而处理信号然后
573 * 直接退出*/
574 switch (prev->state) {
575 case TASK_INTERRUPTIBLE:
576 if (signal_pending(prev)) {
577 prev->state = TASK_RUNNING;
578 break;
579 }
580 default:
581 del_from_runqueue(prev);
582 case TASK_RUNNING:
583 }
584 prev->need_resched = 0;
585
586 /*
587 * this is the scheduler proper:
588 */
589
590 repeat_schedule:
591 /*
592 * Default process to select..
593 */
594 /*按照策略选择等待调度的进程中的最佳候选进程*/
595 next = idle_task(this_cpu);
596 c = -1000;
597 if (prev->state == TASK_RUNNING)
598 goto still_running;
599
600 still_running_back:
601 /*遍历所有处于就绪状态的进程*/
602 list_for_each(tmp, &runqueue_head) {
603 p = list_entry(tmp, struct task_struct, run_list);
604 if (can_schedule(p, this_cpu)) {
605 /*遍历到的每个进程都计算其权值*/
606 int weight = goodness(p, this_cpu, prev->active_mm);
607 if (weight > c)
608 /*比较取得最大权值的那个进程*/
609 c = weight, next = p;
610 }
611 }
612
613 /* Do we need to re-calculate counters? */
614 /*如果全部就绪进程的权值都已经降到0了,那就开始重新计算每个进程的权值
615 * */
616 if (!c)
617 goto recalculate;
618 /*
619 * from this point on nothing can prevent us from
620 * switching to the next task, save this fact in
621 * sched_data.
622 */
623 sched_data->curr = next;
624 #ifdef CONFIG_SMP
625 next->has_cpu = 1;
626 next->processor = this_cpu;
627 #endif
628 spin_unlock_irq(&runqueue_lock);
629
630 if (prev == next)
631 goto same_process;
632
633 #ifdef CONFIG_SMP
634 /*
635 * maintain the per-process 'last schedule' value.
636 * (this has to be recalculated even if we reschedule to
637 * the same process) Currently this is only used on SMP,
638 * and it's approximate, so we do not have to maintain
639 * it while holding the runqueue spinlock.
640 */
641 sched_data->last_schedule = get_cycles();
642
643 /*
644 * We drop the scheduler lock early (it's a global spinlock),
645 * thus we have to lock the previous process from getting
646 * rescheduled during switch_to().
647 */
648
649 #endif /* CONFIG_SMP */
650
651 kstat.context_swtch++;
652 /*
653 * there are 3 processes which are affected by a context switch:
654 *
655 * prev == .... ==> (last => next)
656 *
657 * It's the 'much more previous' 'prev' that is on next's stack,
658 * but prev is set to (the just run) 'last' process by switch_to().
659 * This might sound slightly confusing but makes tons of sense.
660 */
661 prepare_to_switch();
662 {
663 struct mm_struct *mm = next->mm;
664 struct mm_struct *oldmm = prev->active_mm;
665 /*如果将要被投入运行的进程是一个内核进程,则没有用户空间
666 * 也就是说mm = NULL,这时候就向前一个进程借用一个active_mm,
667 * 这个active_mm是内核空间映射。因为每个进程的内核空间映射都是
668 * 一致的,所以可用之*/
669 if (!mm) {
670 if (next->active_mm) BUG();
671 next->active_mm = oldmm;
672 atomic_inc(&oldmm->mm_count);
673 enter_lazy_tlb(oldmm, next, this_cpu);
674 } else {
675 /*如果进程拥有自己的mm结构,那必然等于active_mm结构*/
676 if (next->active_mm != mm) BUG();
677 /*进行进程空间映射的切换。
678 *进程切换时,分两部分,一部分是地址映射的切换,
679 * 另一部分是运行现场的切换。地址的切换仅仅是把
680 * mm中的映射表物理地址装入CR3。
681 * */
682 switch_mm(oldmm, mm, next, this_cpu);
683 }
684
685 if (!prev->mm) {
686 prev->active_mm = NULL;
687 mmdrop(oldmm);
688 }
689 }
690
691 /*
692 * This just switches the register state and the
693 * stack.
694 */
695 /*现场切换*/
696 switch_to(prev, next, prev);
697 __schedule_tail(prev);
698
699 same_process:
700 reacquire_kernel_lock(current);
701 if (current->need_resched)
702 goto need_resched_back;
703
704 return;
705
706 recalculate:
707 {
708 struct task_struct *p;
709 spin_unlock_irq(&runqueue_lock);
710 read_lock(&tasklist_lock);
711 for_each_task(p)
712 p->counter = (p->counter >> 1) + NICE_TO_TICKS(p->nice);
713 read_unlock(&tasklist_lock);
714 spin_lock_irq(&runqueue_lock);
715 }
716 goto repeat_schedule;
717
718 still_running:
719 c = goodness(prev, this_cpu, prev->active_mm);
720 next = prev;
721 goto still_running_back;
722
723 handle_softirq:
724 do_softirq();
725 goto handle_softirq_back;
726
727 move_rr_last:
728 if (!prev->counter) {
729 /*counter是RR调度策略的时间片,在每个systick中断中都会递减
730 * 一旦时间片用完,就将时间片按照优先级换算重新设置时间片,
731 * 然后移动到RR队列的队尾,每次调度的时候,队列前面的相同
732 * 优先级进程具有优先权。也就是说,在调度时,先遍历RR运行
733 * 队列,找到优先级最高的进程投入运行,但对于相同优先级
734 * 的进程,排在队列前头的进程具有优先权,将会得到运行权*/
735 prev->counter = NICE_TO_TICKS(prev->nice);
736 move_last_runqueue(prev);
737 }
738 goto move_rr_back;
739
740 scheduling_in_interrupt:
741 printk("Scheduling in interrupt\n");
742 BUG();
743 return;
744 }
二、总结:
linux进程的切换,总共分为两步:
- 查找权值最大的进程
- 投入运行,即进程切换
查找权值最大的进程,就是计算权值和比较的过程,权值计算与时间片剩余值couter、nice值,rt_priority有关。事实上,couter值也是由nice值计算出来的,所以与时间片有关的调度策略SCHED_RR和SCHED_OTHE都与nice值有关。对于SCHED_OTHER策略的进程来说,它的权值只与couter和nice值有关,其权值等于
进程切换,实际上是包含地址空间的切换和现场切换两部分。地址空间的切换,就是将进程的mm结构中的pgd也就是映射表的物理地址装入CR3寄存器即可。但值得注意的是,如果该进程是一个内核线程,就要向上一个进程借用一下active_mm结构,充当该进程的mm结构。因为每个进程的内核空间映射都是一致的,而内核线程只需要用到内核空间,所以借用过来的mm结构也是可以使用的。