• Linux进程优先级的处理--Linux进程的管理与调度(二十二)


    1. linux优先级的表示

    1.1 优先级的内核表示

    linux优先级概述

    在用户空间通过nice命令设置进程的静态优先级, 这在内部会调用nice系统调用, 进程的nice值在-20~+19之间. 值越低优先级越高.

    setpriority系统调用也可以用来设置进程的优先级. 它不仅能够修改单个线程的优先级, 还能修改进程组中所有进程的优先级, 或者通过制定UID来修改特定用户的所有进程的优先级

    内核使用一些简单的数值范围0~139表示内部优先级, 数值越低, 优先级越高。

    从0~99的范围专供实时进程使用, nice的值[-20,19]则映射到范围100~139

    linux2.6内核将任务优先级进行了一个划分, 实时优先级范围是0到MAX_RT_PRIO-1(即99),而普通进程的静态优先级范围是从MAX_RT_PRIO到MAX_PRIO-1(即100到139).

    优先级范围 描述
    0——99 实时进程
    100——139 非实时进程

    内核的优先级表示

    内核表示优先级的所有信息基本都放在include/linux/sched/prio.h中, 其中定义了一些表示优先级的宏和函数.

    优先级数值通过宏来定义, 如下所示,

    其中MAX_NICE和MIN_NICE定义了nice的最大最小值

    而MAX_RT_PRIO指定了实时进程的最大优先级,而MAX_PRIO则是普通进程的最大优先级数值

    /*  http://lxr.free-electrons.com/source/include/linux/sched/prio.h?v=4.6#L4 */
    #define MAX_NICE        19
    #define MIN_NICE        -20
    #define NICE_WIDTH      (MAX_NICE - MIN_NICE + 1)
    
    /* http://lxr.free-electrons.com/source/include/linux/sched/prio.h?v=4.6#L24  */
    #define MAX_PRIO        (MAX_RT_PRIO + 40)
    #define DEFAULT_PRIO        (MAX_RT_PRIO + 20)
    
    描述
    MIN_NICE -20 对应于优先级100, 可以使用NICE_TO_PRIO和PRIO_TO_NICE转换
    NICE_WIDTH 40 nice值得范围宽度, 即[-20, 19]共40个数字的宽度
    MAX_RT_PRIO, MAX_USER_RT_PRIO 10
    0
    实时进程的最大优先级
    MAX_PRIO 14
    0
    普通进程的最大优先级
    DEFAULT_PRIO 12
    0
    进程的默认优先级, 对应于nice=0
    MAX_DL_PRIO 0 使用EDF最早截止时间优先调度算法的实时进程最大的优先级

    而内核提供了一组宏将优先级在各种不同的表示形之间转移

    //  http://lxr.free-electrons.com/source/include/linux/sched/prio.h?v=4.6#L27
    /*
     * Convert user-nice values [ -20 ... 0 ... 19 ]
     * to static priority [ MAX_RT_PRIO..MAX_PRIO-1 ],
     * and back.
     */
    #define NICE_TO_PRIO(nice)      ((nice) + DEFAULT_PRIO)
    #define PRIO_TO_NICE(prio)      ((prio) - DEFAULT_PRIO)
    
    /*
     * 'User priority' is the nice value converted to something we
     * can work with better when scaling various scheduler parameters,
     * it's a [ 0 ... 39 ] range.
     */
    #define USER_PRIO(p)            ((p)-MAX_RT_PRIO)
    #define TASK_USER_PRIO(p)       USER_PRIO((p)->static_prio)
    #define MAX_USER_PRIO           (USER_PRIO(MAX_PRIO))
    

    还有一些nice值和rlimit值之间相互转换的函数nice_to_rlimit和rlimit_to_nice, 这在nice系统调用进行检查的时候很有用, 他们定义在include/linux/sched/prio.h, L47中, 如下所示

    /*
     * Convert nice value [19,-20] to rlimit style value [1,40].
     */
    static inline long nice_to_rlimit(long nice)
    {
        return (MAX_NICE - nice + 1);
    }
    
    /*
     * Convert rlimit style value [1,40] to nice value [-20, 19].
     */
    static inline long rlimit_to_nice(long prio)
    {
        return (MAX_NICE - prio + 1);
    }
    

    DEF最早截至时间优先实时调度算法的优先级描述

    此外新版本的内核还引入了EDF实时调度算法, 它的优先级比RT进程和NORMAL/BATCH进程的优先级都要高, 关于EDF的优先级的设置信息都早内核头文件include/linux/sched/deadline.h

    因此内核将MAX_DL_PRIO设置为0, 可以参见内核文件include/linux/sched/deadline.h

    #define MAX_DL_PRIO             0
    

    此外也提供了一些EDF优先级处理所需的函数, 如下所示, 可以参见内核文件include/linux/sched/deadline.h

    static inline int dl_prio(int prio)
    {
        if (unlikely(prio < MAX_DL_PRIO))
                return 1;
        return 0;
    }
    
    static inline int dl_task(struct task_struct *p)
    {
        return dl_prio(p->prio);
    }
    
    static inline bool dl_time_before(u64 a, u64 b)
    {
        return (s64)(a - b) < 0;
    }
    

    1.2 进程的优先级表示

    struct task_struct
    {
        /* 进程优先级
         * prio: 动态优先级,范围为100~139,与静态优先级和补偿(bonus)有关
         * static_prio: 静态优先级,static_prio = 100 + nice + 20 (nice值为-20~19,所以static_prio值为100~139)
         * normal_prio: 没有受优先级继承影响的常规优先级,具体见normal_prio函数,跟属于什么类型的进程有关
         */
        int prio, static_prio, normal_prio;
        /* 实时进程优先级 */
        unsigned int rt_priority;
    }
    

    动态优先级 静态优先级 实时优先级

    其中task_struct采用了三个成员表示进程的优先级:prio和normal_prio表示动态优先级, static_prio表示进程的静态优先级.

    为什么表示动态优先级需要两个值prio和normal_prio

    调度器会考虑的优先级则保存在prio. 由于在某些情况下内核需要暂时提高进程的优先级, 因此需要用prio表示. 由于这些改变不是持久的, 因此静态优先级static_prio和普通优先级normal_prio不受影响.

    此外还用了一个字段rt_priority保存了实时进程的优先级

    字段 描述
    static_prio 用于保存静态优先级, 是进程启动时分配的优先级, ,可以通过nice和sched_setscheduler系统调用来进行修改, 否则在进程运行期间会一直保持恒定
    rt_priority 用于保存实时优先级
    normal_prio 表示基于进程的静态优先级static_prio和调度策略计算出的优先级. 因此即使普通进程和实时进程具有相同的静态优先级, 其普通优先级也是不同的, 进程分叉(fork)时, 子进程会继承父进程的普通优先级
    prio 保存进程的动态优先级

    实时进程的优先级用实时优先级rt_priority来表示

    2 进程优先级的计算

    前面说了task_struct中的几个优先级的字段

    静态优先级 实时优先级 普通优先级 动态优先级
    static_prio rt_priority normal_prio prio

    但是这些优先级是如何关联的呢, 动态优先级prio又是如何计算的呢?

    2.1 normal_prio函数设置普通优先级normal_prio

    静态优先级static_prio(普通进程)和实时优先级rt_priority(实时进程)是计算的起点

    因此他们也是进程创建的时候设定好的, 我们通过nice修改的就是普通进程的静态优先级static_prio

    首先通过静态优先级static_prio计算出普通优先级normal_prio, 该工作可以由nromal_prio来完成, 该函数定义在kernel/sched/core.c#L861

    /*
     * __normal_prio - return the priority that is based on the static prio
     * 普通进程(非实时进程)的普通优先级normal_prio就是静态优先级static_prio
     */
    static inline int __normal_prio(struct task_struct *p)
    {
        return p->static_prio;
    }
    
    /*
     * Calculate the expected normal priority: i.e. priority
     * without taking RT-inheritance into account. Might be
     * boosted by interactivity modifiers. Changes upon fork,
     * setprio syscalls, and whenever the interactivity
     * estimator recalculates.
     */
    static inline int normal_prio(struct task_struct *p)
    {
        int prio;
    
        if (task_has_dl_policy(p))              /*  EDF调度的实时进程  */
                prio = MAX_DL_PRIO-1;
        else if (task_has_rt_policy(p))       /*  普通实时进程的优先级  */
                prio = MAX_RT_PRIO-1 - p->rt_priority;
        else                                              /*  普通进程的优先级  */
                prio = __normal_prio(p);
        return prio;
    }
    
    进程类型 调度器 普通优先级normal_prio
    EDF实时进程 EDF MAX_DL_PRIO-1 = -1
    普通实时进程 RT MAX_RT_PRIO-1 - p->rt_priority = 99 - rt_priority
    普通进程 CFS __normal_prio(p) = static_prio

    普通优先级normal_prio需要根据普通进程和实时进程进行不同的计算, 其中__normal_prio适用于普通进程, 直接将普通优先级normal_prio设置为静态优先级static_prio. 而实时进程的普通优先级计算依据其实时优先级rt_priority.

    3.1.1 辅助函数task_has_dl_policy和task_has_rt_policy

    定义在kernel/sched/sched.h#L117

    其本质其实就是传入task->policy调度策略字段看其值等于SCHED_NORMAL, SCHED_BATCH, SCHED_IDLE, SCHED_FIFO, SCHED_RR, SCHED_DEADLINE中的哪个, 从而确定其所属的调度类, 进一步就确定了其进程类型

    static inline int idle_policy(int policy)
    {
        return policy == SCHED_IDLE;
    }
    static inline int fair_policy(int policy)
    {
        return policy == SCHED_NORMAL || policy == SCHED_BATCH;
    }
    
    static inline int rt_policy(int policy)
    {
        return policy == SCHED_FIFO || policy == SCHED_RR;
    }
    
    static inline int dl_policy(int policy)
    {
            return policy == SCHED_DEADLINE;
    }
    static inline bool valid_policy(int policy)
    {
            return idle_policy(policy) || fair_policy(policy) ||
                    rt_policy(policy) || dl_policy(policy);
    }
    
    static inline int task_has_rt_policy(struct task_struct *p)
    {
            return rt_policy(p->policy);
    }
    
    static inline int task_has_dl_policy(struct task_struct *p)
    {
            return dl_policy(p->policy);
    }
    

    2.1.2 关于rt_priority数值越大, 实时进程优先级越高的问题

    我们前面提到了数值越小, 优先级越高, 但是此处我们会发现rt_priority的值越大, 其普通优先级越小, 从而优先级越高.

    因此网上出现了一种说法, 优先级越高?这又是怎么回事?难道有一种说法错了吗?

    实际的原因是这样的,对于一个实时进程,他有两个参数来表明优先级——prio 和 rt_priority,

    prio才是调度所用的最终优先级数值,这个值越小,优先级越高;

    而rt_priority 被称作实时进程优先级,他要经过转化——prio=MAX_RT_PRIO - 1- p->rt_priority;

    MAX_RT_PRIO = 100, ;这样意味着rt_priority值越大,优先级越高;

    而内核提供的修改优先级的函数,是修改rt_priority的值,所以越大,优先级越高。

    所以用户在使用实时进程或线程,在修改优先级时,就会有“优先级值越大,优先级越高的说法”,也是对的。

    2.1.3 为什么需要__normal_prio函数

    我们肯定会奇怪, 为什么增加了一个__normal_prio函数做了这么简单的工作, 这个其实是有历史原因的: 在早期的o(1)调度器中, 普通优先级的计算涉及相当多技巧性地工作, 必须检测交互式进程并提高其优先级, 而必须”惩罚”非交互进程, 以便是得系统获得更好的交互体验. 这需要很多启发式的计算, 他们可能完成的很好, 也可能不工作

    2.2 effective_prio函数设置动态优先级prio

    可以通过函数effective_prio用静态优先级static_prio计算动态优先级prio, 即·

    p->prio = effective_prio(p);
    

    该函数定义在kernel/sched/core.c, line 861

    /*
     * Calculate the current priority, i.e. the priority
     * taken into account by the scheduler. This value might
     * be boosted by RT tasks, or might be boosted by
     * interactivity modifiers. Will be RT if the task got
     * RT-boosted. If not then it returns p->normal_prio.
     */
    static int effective_prio(struct task_struct *p)
    {
        p->normal_prio = normal_prio(p);
        /*
         * If we are RT tasks or we were boosted to RT priority,
         * keep the priority unchanged. Otherwise, update priority
         * to the normal priority:
         */
        if (!rt_prio(p->prio))
                return p->normal_prio;
        return p->prio;
    }
    

    我们会发现函数首先effective_prio设置了普通优先级, 显然我们用effective_prio同时设置了两个优先级(普通优先级normal_prio和动态优先级prio)

    因此计算动态优先级的流程如下

    • 设置进程的普通优先级(实时进程99-rt_priority, 普通进程为static_priority)
    • 计算进程的动态优先级(实时进程则维持动态优先级的prio不变, 普通进程的动态优先级即为其普通优先级)

    最后, 我们综述一下在针对不同类型进程的计算结果

    进程类型 实时优先级rt_priority 静态优先级static_prio 普通优先级normal_prio 动态优先级prio
    EDF调度的实时进程 rt_priority 不使用 MAX_DL_PRIO-1 维持原prio不变
    RT算法调度的实时进程 rt_priority 不使用 MAX_RT_PRIO-1-rt_priority 维持原prio不变
    普通进程 不使用 static_prio static_prio static_prio

    2.2.1 为什么effective_prio使用优先级数值检测实时进程

    t_prio会检测普通优先级是否在实时范围内, 即是否小于MAX_RT_PRIO.参见include/linux/sched/rt.h#L6

    static inline int rt_prio(int prio)
    {
        if (unlikely(prio < MAX_RT_PRIO))
            return 1;
        return 0;
    }
    

    而前面我们在normal_prio的时候, 则通过task_has_rt_policy来判断其policy属性来确定

    policy == SCHED_FIFO || policy == SCHED_RR;
    

    那么为什么effective_prio重检测实时进程是rt_prio基于优先级数值, 而非task_has_rt_policy或者rt_policy?

    对于临时提高至实时优先级的非实时进程来说, 这个是必要的, 这种情况可能发生在是哦那个实时互斥量(RT-Mutex)时.

    2.3 设置prio的时机

    • 在新进程用wake_up_new_task唤醒时, 或者使用nice系统调用改变其静态优先级时, 则会通过effective_prio的方法设置p->prio

    wake_up_new_task(), 计算此进程的优先级和其他调度参数,将新的进程加入到进程调度队列并设此进程为可被调度的,以后这个进程可以被进程调度模块调度执行。

    • 进程创建时copy_process通过调用sched_fork来初始化和设置调度器的过程中会设置子进程的优先级

    2.4 nice系统调用的实现

    nice系统调用是的内核实现是sys_nice, 其定义在kernel/sched/core.c#L7498

    它在通过一系列检测后, 通过set_user_nice函数, 其定义在kernel/sched/core.c#L3497

    关于其具体实现我们会在另外一篇博客里面详细讲

    2.5 fork时优先级的继承

    在进程分叉处子进程时, 子进程的静态优先级继承自父进程. 子进程的动态优先级p->prio则被设置为父进程的普通优先级, 这确保了实时互斥量引起的优先级提高不会传递到子进程.

    可以参照sched_fork函数, 在进程复制的过程中copy_process通过调用sched_fork来设置子进程优先级, 参见sched_fork函数

    /*
     * fork()/clone()-time setup:
     */
    int sched_fork(unsigned long clone_flags, struct task_struct *p)
    {
        /*  ......  */
        /*
         * Make sure we do not leak PI boosting priority to the child.
         * 子进程的动态优先级被设置为父进程普通优先级 
         */
        p->prio = current->normal_prio;
    
        /*
         * Revert to default priority/policy on fork if requested.
         * sched_reset_on_fork标识用于判断是否恢复默认的优先级或调度策略
    
         */
        if (unlikely(p->sched_reset_on_fork))  /*  如果要恢复默认的调度策略, 即SCHED_NORMAL  */
        {
            /*   首先是设置静态优先级static_prio
             *   由于要恢复默认的调度策略
             *   对于父进程是实时进程的情况, 静态优先级就设置为DEFAULT_PRIO
             *
             *   对于父进程是非实时进程的情况, 要保证子进程优先级不小于DEFAULT_PRIO
             *   父进程nice < 0即static_prio < 的重新设置为DEFAULT_PRIO的重新设置为DEFAULT_PRIO
             *   父进程nice > 0的时候, 则什么也没做
             *   */
            if (task_has_dl_policy(p) || task_has_rt_policy(p))
            {
                p->policy = SCHED_NORMAL;           /*  普通进程调度策略  */
                p->static_prio = NICE_TO_PRIO(0);   /*  静态优先级为nice = 0 即DEFAULT_PRIO*/
                p->rt_priority = 0;                             /*  实时优先级为0  */
            }
            else if (PRIO_TO_NICE(p->static_prio) < 0)  /*  */
                p->static_prio = NICE_TO_PRIO(0);   /*  */
    
            /*  接着就通过__normal_prio设置其普通优先级和动态优先级
              *  这里做了一个优化, 因为用sched_reset_on_fork标识设置恢复默认调度策略后
              *  创建的子进程是是SCHED_NORMAL的非实时进程
              *  因此就不需要绕一大圈用effective_prio设置normal_prio和prio了 
              *  直接用__normal_prio设置就可  */
            p->prio = p->normal_prio = __normal_prio(p); /*  设置*/
    
            /*  设置负荷权重  */
            set_load_weight(p);
    
            /*
             * We don't need the reset flag anymore after the fork. It has
             * fulfilled its duty:
             */
            p->sched_reset_on_fork = 0;
        }
        /*  ......  */
    }
    

    3 总结

    task_struct采用了四个成员表示进程的优先级:prio和normal_prio表示动态优先级, static_prio表示进程的静态优先级. 同时还用了rt_priority表示实时进程的优先级

    字段 描述
    static_prio 用于保存静态优先级, 是进程启动时分配的优先级, ,可以通过nice和sched_setscheduler系统调用来进行修改, 否则在进程运行期间会一直保持恒定
    prio 进程的动态优先级, 这个有显示才是调度器重点考虑的进程优先级
    normal_prio 普通进程的静态优先级static_prio和调度策略计算出的优先级. 因此即使普通进程和实时进程具有相同的静态优先级, 其普通优先级也是不同的, 进程分叉(fork)时, 子进程会继承父进程的普通优先级, 可以通过normal_prio来计算(非实时进程用static_prIo计算, 实时进程用rt_priority计算)
    rt_priority 实时进程的静态优先级

    调度器会考虑的优先级则保存在prio. 由于在某些情况下内核需要暂时提高进程的优先级, 因此需要用prio表示. 由于这些改变不是持久的, 因此静态优先级static_prio和普通优先级normal_prio不受影响.

    此外还用了一个字段rt_priority保存了实时进程的优先级静态优先级static_prio(普通进程)和实时优先级rt_priority(实时进程)是计算的起点, 通过他们计算进程的普通优先级normal_prio和动态优先级prio.

    内核通过normal_prIo函数计算普通优先级normal_prio

    通过effective_prio函数计算动态优先级prio

    参考

    进程调度之sys_nice()系统调用

    linux调度器源码研究 - 概述(一)

    深入 Linux 的进程优先级

  • 相关阅读:
    C#中的“装箱”与“拆箱”[转贴]
    C#中的委托和事件[转帖]
    ASP.NET的四种错误机制[转帖]
    读技术书的技巧
    《AdvancED ActionScript 3.0 Animation》读书笔记(2) —— 2.5d
    读《JavaScript大师Nicholas C. Zakas谈TypeScript》有感,也谈编译js问题
    《游戏人工智能编程》读书笔记 —— 向量的归一和点乘
    浏览器图形渲染性能测试
    对对碰游戏demo
    A星寻路demo
  • 原文地址:https://www.cnblogs.com/linhaostudy/p/9930245.html
Copyright © 2020-2023  润新知