linux 进程与进程调度

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值有关,其权值等于

weigth=counter+(20nice)
对于SCHED_RR和SCHED_FIFO策略来说,它的权值计算仅仅与rt_priority有关。计算公式为
weigth=1000+rtpriority
但nice值会影响SCHED_RR策略进程的时间片配额,因为时间片是根据nice值计算得出的。
进程切换,实际上是包含地址空间的切换和现场切换两部分。地址空间的切换,就是将进程的mm结构中的pgd也就是映射表的物理地址装入CR3寄存器即可。但值得注意的是,如果该进程是一个内核线程,就要向上一个进程借用一下active_mm结构,充当该进程的mm结构。因为每个进程的内核空间映射都是一致的,而内核线程只需要用到内核空间,所以借用过来的mm结构也是可以使用的。

猜你喜欢

转载自blog.csdn.net/u013298300/article/details/51214243