Linux内核探讨-- 第二章

      本文是个人分析《Linux内核设计与实现》而写的总结,欢迎转载,请注明出处:

                                                                                 http://blog.csdn.net/dlutbrucezhang/article/details/12185125

      第二章 --进程调度

      上一章已经详细的介绍了进程和进程管理的概念,这一章的主题在操作系统能否“完美”的工作方面占有最主要的地位。很明显,根据名字我们就能看出来,就是内核去调度进程的执行。怎样去调度进程?为什么会是那样?好吧,这篇文章就是介绍这些主题的。

1.多任务

      现在的操作系统都是多任务的,也就说多个任务(进程)能够并发的执行。注意,这里用到的词是“并发”,而不是并行。并发:是指多个任务轮流占用资源执行,但是它们之间的切换是快速的,看起来就好像是同时执行一样。由于处理器的限制,即使在内存中现在有100个进程,而且都不阻塞,同一时间,它们也只能有一个进程处于运行状态,其余的是就绪状态。
      多任务操作系统分为两种:抢占式多任务和非抢占式多任务。由于非抢占式多任务效率极其低下,特别是平衡不了实时进程和普通进程之间的关系,几乎退出了现在的市场。我们平时用的操作系统普遍支持抢占。因为,这样不仅使得计算机的反应加快,更能保证高吞吐量的实现。它的含义也就是字面上的解释,即一个进程在执行的过程中,另一个“有资格”的进程打断它的执行(称之为抢占),变成运行状态的过程。

2.I/O消耗型和处理器消耗性的进程

      在操作系统运行的过程中,存在两种表现不同的进程。I/O消耗性表现为,大部分时间是在等待I/O传输的完成,很少一部分时间是在使用CPU,这里最经典的例子就是输入法;处理器消耗性进程是指很少的时间等待I/O的完成,大部分时间是利用CPU的运算,这里经典的例子是图像处理程序和Matlab计算程序。
      这样也就会存在一些矛盾的地方。应该给谁多一些的处理器时间,怎样分配是比较合理的。也许你会说,既然处理器消耗性的进程需要更多的处理器时间,应该给他分配更多的CPU,但是,如果不给I/O消耗型的进程分配更多的处理器时间,那么它的反应会相当的慢,举一个夸张的例子,当你向键盘输入一个字符之后,一秒之后才在显示器上显示,这显然是我们不能接受的。相反,计算程序即使是多一秒或者甚至是多十秒完成,我们似乎都是可以接受的,毕竟我们在等待计算完成时已经等了很久。所以,在Linux中实现的策略是给I/O消耗性的进程分配更多的处理器时间。

3.进程优先级

      不同的进程是有不同的优先级的,这似乎很正常。因为,在不同的时间里,肯定有进程要求尽快能得到执行的机会。这里的优先级也是分为两种的。
      3.1 实时优先级
              它的优先级范围从 0 -- 99,数字越大,优先级越高,能尽快得到处理的机会就会越到,而且通常这意味着它也能获得更多的处理器时间。
      3.2 nice值
               我们可能在程序中见过这样的语句 nice(5); 这里就是给当前进程的优先级计数加上5,注意,这里并不是提高了它的优先级,反而,这样会降低当前进程的优先级。nice值的范围是 -20 -- 19。默认情况下,创建的普通进程优先级为 0 。

4.时间片

      其实,在2.6以后的内核中,就不存在时间片的概念了。不过,由于这是Unix中的经典定义,所以,需要解释下。时间片就是由进程的优先级而分配的可用处理器时间。一个进程如果时间片为0,它就必须放弃执行。
      在现在的内核中,操作系统利用CFS(完全公平调度算法)算法调度进程的执行,同时为进程分配处理器时间。这里用到的概念是处理器使用比。例如,当一个进程分配了10ms的运行时间,但是它只是执行了5ms,就被其他的进程抢占了,这时,我们说,它用到的使用比是 50%。下面,我们会看到,这里的使用比是怎样在进程调度方面发挥作用的。
 

5.进程调度策略

      这里,我们首先给出两个不同种类的普通进程。一个是I/O消耗性的进程--文字处理程序;另一个是处理器消耗性的进程--视频编码程序。假如现在的系统中,这是仅有的两个进程,为了方便起见,它们用于相同的 nice 值,所以,它们理所当然的被分配了相同的处理器使用比。
      由于文字处理程序大部分时间是在等待用户的输入,所以,分配给他的处理器时间它用到的少之又少(相比视频编码程序)。所以,它剩余的处理器使用比就会很多。相反,视频编码程序剩余的处理器使用比就会很少。
      这时,我们给出调度的条件,如果消耗的使用比比当前执行进程的小,那么新的进程立即投入运行,抢占当前的进程。当然,我们同样需要考虑优先级的原则。优先级低的进程肯定不能完成抢占,除非是当前进程阻塞或者是资源放弃处理器。

6.CFS调度器

      CFS调度器是现在Linux系统中常用的调度器类。它利用当前系统中进程的个数(可运行状态)和优先级分配给每个进程不同的处理器使用比。
      当然,如果系统中进程的个数区域无限,那么进程获得的处理器时间将是一个预先设定的极值,一般是 1ms。

7.Linux调度的实现

      7.1 时间记账
             首先,我们需要看一个结构--调度器实体
struct sched_entity {
    struct load_weight    load;        /* for load-balancing */
    struct rb_node        run_node;
    struct list_head    group_node;
    unsigned int        on_rq;

    u64            exec_start;
    u64            sum_exec_runtime;
    u64            vruntime;
    u64            prev_sum_exec_runtime;

    u64            nr_migrations;

#ifdef CONFIG_SCHEDSTATS
    struct sched_statistics statistics;
#endif

#ifdef CONFIG_FAIR_GROUP_SCHED
    struct sched_entity    *parent;
    /* rq on which this entity is (to be) queued: */
    struct cfs_rq        *cfs_rq;
    /* rq "owned" by this entity/group: */
    struct cfs_rq        *my_q;
#endif
};


        我们可以看到,调度器实体中存在一个字段--vruntime,这代表的是虚拟时间,也就是新的概念--处理器使用比。在进程描述符内,我们可以找到指向这个实体的指针。
      7.2 进程选择
             这是最重要的主题之一,这里它的作用是选择一个最合适的调度器类,并从它的运行队列中选择一个最合适的进程。这个最合适的进程当然是 vruntime 值最小的那个进程。为了能更深刻的理解,这里给出一段代码:
/* 
 * Pick up the highest-prio task: 
 */  
static inline struct task_struct *  
pick_next_task(struct rq *rq)  
{  
    const struct sched_class *class;  
    struct task_struct *p;  
  
    /* 
     * Optimization: we know that if all tasks are in 
     * the fair class we can call that function directly: 
     */  
    if (likely(rq->nr_running == rq->cfs.nr_running)) { //如果nr_running==cfs.nr_running,则说明当前rq队列中是没有rt任务的,  
                                                            //rt任务不在cfs队列中,它的优先级的设置和cfs不一样。  
        p = fair_sched_class.pick_next_task(rq); //在cfs队列中挑选即将被切换进去的进程,核心函数,  
        if (likely(p))  
            return p;  
    }  
  
    for_each_class(class) {   
        p = class->pick_next_task(rq);  
        if (p)  
            return p;  
    }  
  
    BUG(); /* the idle class will always have a runnable task */  
}

           这里我们可以看到内核中对挑选调度器类的时候做了一些优化。由于系统中几乎都是普通的进程,所以,一旦进程的总数等于CFS可运行队列中进程的总数时,那么可以直接调用CFS调度器。如果不是,那么选择优先级最高的那个调度器类。每个调度器都定义了自己的 pick_next_task() 函数,且拥有自己的可运行队列,所以,总是可以选出最合适的需要调度的进程。

       7.3 通知调度
               当需要执行进程调度的时候,操作系统是怎样知道的呢?难道需要内核执行函数一个一个进程的“询问”吗?那样显然是太低效了,所以,为了方便通知内核需要实现调度,在进程描述符中有这样一个字段 -- need_resched ,一旦这个标志被设置,那么内核就知道此刻需要进程调度了。关于,什么时候实现检查这个标志的操作,在下文中会有详细的介绍。

8.抢占的实现

      抢占也是分为两种的:
             用户抢占和内核抢占
      用户抢占:内核在返回用户空间的时候,如果 need_resched 标志被设置,会导致 schedule() 被调用,此时就会发生用户抢占。内核在中断返回或者是系统调用返回时,会检查标志是否被设置,如果被设置,那么执行进程切换,如果没有,则继续执行原先的那个进程。
      所以,用户抢占发生在以下两种情况下:
                        从系统调用返回用户空间中
                         从中断处理程序返回用户空间中
       内核抢占:Linux支持内核抢占,即进程在内核中执行的时候,也是可以被抢占的,当然,这也是需要有条件保证的,那就是,此时,抢占是安全的。这里唯一需要保证的是进程此时没有持有锁。
      为了支持内核抢占,每个进程的 thread_info 字段中增加了 preempt_count 字段,这个标志代表的是进程持有锁的个数,初始时,这个值为0,一旦进程持有锁,那么计数加1,再次拥有其他的锁时,继续增加这个数值。当进程中的 preempt_count 和 need_resched,字段都被设置时,此时可以实现内核抢占。
       内核抢占发生的时机如下:
                      中断处理程序正在执行(处于中断上下文中),且返回内核空间之前
                      内核中的任务阻塞
                      内核中进程显示的调用 schedule() 函数

9.实时进程的调度策略

      实时进程,其实就是指那些优先级比较高的进程。它们的调度策略也有两种,分别是 SCHED_FIFO,SCHED_RR
      SCHED_FIFO:如果实时进程使用这种方式执行,那么当前的实时进程将会一直执行,除非是它自己显示的放弃处理器或者是被阻塞,或者是被优先级更高的进程抢占。
     SCHED_RR:它被称为是轮转调度算法,实时进程采用这种方法调度时,它有一个执行时间,当用完这个时间后,它就被挂起,接着选择下一个进程运行,依次轮转运行队列中的进程,直到所有的进程都执行完毕。
      这两种调度算法实现的都是静态优先级,内核不为它们计算动态优先级,这样能够保证给定优先级的进程能够抢占比它优先级低的进程。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值