linux进程的管理和调度 --- 调度相关

Posted 流水灯

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux进程的管理和调度 --- 调度相关相关的知识,希望对你有一定的参考价值。

进程调度含义

进程调度决定了将哪个进程进行执行,以及执行的时间。操作系统进行合理的进程调度,使得资源得到最大化的利用。

在单片机上,常常使用的方式是:系统初始化---->while(1)。(当然,单片机也可以跑类似 FreeRTOS,也可以有进程切换)

在带操作系统的 CPU 上跑的逻辑是,允许多个进程(其实就是程序) ”同时” 跑。比如,你可以在操作鼠标的同时,进行音乐播放,文字编辑等。宏观上看上去是多个任务并行执行,事实的本质是 CPU 在不断的调度每一个进程,使得每个进程都得以响应,与此同时,还要兼顾不同场景下的响应效率(进程的执行时间)。

进程调度器的任务就是合理分配CPU时间给运行的进程,创造一种所有进程并行运行的错觉。这就对调度器提出了要求:

1、调度器分配的CPU时间不能太长,否则会导致其他的程序响应延迟,难以保证公平性。

2、调度器分配的时间也不能太短,每次调度会导致上下文切换,这种切换开销很大。

而调度器的任务就是:1、分配时间给进程 2、上下文切换

所以具体而言,调度器的任务就明确了:用一句话表述就是在恰当的实际,按照合理的调度算法,选择进程,让进程运行到它应该运行的时间,切换两个进程的上下文。

 

I/O 消耗型和 CPU 消耗型

运行的进程如果大部分来进行 I/O 的请求或者等待的话,这个进程称之为 I/O 消耗型,比如键盘。这种类型的进程经常处于可以运行的状态,但是都只是运行一点点时间,绝大多数的时间都在处于阻塞(睡眠)的状态。

如果进程的绝大多数都在使用 CPU 做运算的话,那么这种进程称之为 CPU 消耗型,比如开启 Matlab 做一个大型的运算。没有太多的 I/O 需求,从系统响应的角度上来讲,调度器不应该经常让他们运行。对于处理器消耗型的进程,调度策略往往是降低他们的执行频率,延长运行时间。

Linux 系统为了提升响应的速度,倾向于优先调度 I/O 消耗型。

 

进程的优先级

调度算法中比较基本的就是靠进程的优先级来进行进程的调度,比如 FreeRTOS,靠 task 的优先级来进行进程的抢占。

一、普通进程

在 Linux 中普通进程依赖称之为 nice 值 的东东来进行进程的优先级描述。nice 值的范围是 [-20, 19]。默认的 default 值为 0;越低的 nice 值,代表着越高的优先级,反之,越高的 nice 值代表着越低的优先级。

越高优先级的 普通进程 有着越高的执行时间(注意,这里值的越高的执行时间,指的是在一小段观察时间内,每个可执行的进程都执行一遍的情况,这里的描述可能产生一些歧义,稍安勿躁,接着看)。可以通过 ps -el 查看系统中进程列表

二、实时进程

实时优先级是可配置的默认情况下的范围是 0~99,与 nice 值相反,越高的实时优先级数值代表着越高的优先级。与此同时,任何实时进程的优先级都高于普通进程的优先级。

—— 小结

实时进程优先级:value 越高,优先级越大

普通进程优先级:nice值越高,普通进程的优先级越小

任何实时进程的优先级 > 普通进程

Linux 调度算法

Linux 中有一个总的调度结构,称之为 调度器类(scheduler class),它允许不同的可动态添加的调度算法并存,总调度器根据调度器类的优先顺序,依次去进行调度器类的中的进程进行调度,挑选了调度器类,再在这个调度器内,使用这个调度器类的算法(调度策略)进行内部的调度

调度器的优先级顺序为:

Scheduling Class 的优先级顺序为 Stop_ask > Real_Time > Fair > Idle_Task,开发者可以根据己的设计需求,來把所属的Task配置到不同的Scheduling Class中。其中的 Real_time 和 Fair 是最最常用的,下面主要聊聊着两类。

一、Fair 调度使用的是 CFS 的调度算法,即完全公平调度器

对于一个普通进程,CFS 调度器调度它执行(SCHED_NORMAL),需要考虑两个方面维度:

1. 如何挑选哪一个进程进入运行状态?

—— 在 CFS 中,给每一个进程安排了一个虚拟时钟 vruntime(virtual runtime),这个变量并非直接等于他的绝对运行时间,而是根据运行时间放大或者缩小一个比例,CFS 使用这个 vruntime 来代表一个进程的运行时间。如果一个进程得以执行,那么他的 vruntime 将不断增大,直到它没有执行。没有执行的进程的 vruntime 不变。调度器为了体现绝对的完全公平的调度原则,总是选择 vruntime 最小的进程,让其投入执行。他们被维护到一个以 vruntime 为顺序的红黑树 rbtree 中,每次去取最小的 vruntime 的进程来投入运行。实际运行时间到 vruntime 的计算公式为:

[ vruntime = 实际运行时间 * 1024 / 进程权重 ]

这里的1024代表nice值为0的进程权重。所有的进程都以nice为0的权重1024作为基准,计算自己的vruntime。上面两个公式可得出,虽然进程的权重不同,但是它们的 vruntime增长速度应该是一样的 ,与权重无关。既然所有进程的vruntime增长速度宏观上看应该是同时推进的,那么就可以用vruntime来选择运行的进程,vruntime值较小就说明它以前占用cpu的时间较短,受到了“不公平”对待,因此下一个运行进程就是它。这样既能公平选择进程,又能保证高优先级进程获得较多的运行时间,这就是CFS的主要思想。

2. 挑选的进程进行运行了,它运行多久?

进程运行的时间是根据进程的权重进行分配。

[ 分配给进程的运行时间 = 调度周期 *(进程权重 / 所有进程权重之和) ]

CFS 调度器实体结构作为一个名为 se 的 sched_entity 结构,嵌入到进程描述符 struct task_struct 中

二、实时调度策略

对于实时调度策略分为两种:SCHED_FIFO 和 SCHED_RR:

这两种进程都比任何普通进程的优先级更高(SCHED_NORMAL),都会比他们更先得到调度。

SCHED_FIFO : 一个这种类型的进程出于可执行的状态,就会一直执行,直到它自己被阻塞或者主动放弃 CPU;它不基于时间片,可以一直执行下去,只有更高优先级的 SCHED_FIFO 或者 SCHED_RR 才能抢占它的任务,如果有两个同样优先级的 SCHED_FIFO 任务,它们会轮流执行,其他低优先级的只有等它们变为不可执行状态,才有机会执行。

SCHED_RR : 与 SCHED_FIFO 大致相同,只是 SCHED_RR 级的进程在耗尽其时间后,不能再执行,需要接受 CPU 的调度。当 SCHED_RR 耗尽时间后,同一优先级的其他实时进程被轮流调度。

上述两种实时算法都是静态的优先级。内核不为实时优先级的进程计算动态优先级,保证给定的优先级的实时进程总能够抢占比他优先级低的进程。

Linux 调度时机

一、进程切换

从进程的角度看,CPU是共享资源,由所有的进程按特定的策略轮番使用。一个进程离开CPU、另一个进程占据CPU的过程,称为进程切换(process switch)。进程切换是在内核中通过调用schedule()完成的。

发生进程切换的场景有以下三种:

1、进程运行不下去了:

比如因为要等待IO完成,或者等待某个资源、某个事件,典型的内核代码如下:

//把进程放进等待队列,把进程状态置为TASK_UNINTERRUPTIBLE
prepare_to_wait(waitq, wait, TASK_UNINTERRUPTIBLE);
//切换进程
schedule();

2、进程还在运行,但内核不让它继续使用CPU了:

比如进程的时间片用完了,或者优先级更高的进程来了,所以该进程必须把CPU的使用权交出来;

3、进程还可以运行,但它自己的算法决定主动交出CPU给别的进程:

用户程序可以通过系统调用sched_yield()来交出CPU,内核则可以通过函数cond_resched()或者yield()来做到。

进程切换分为自愿切换(Voluntary)和强制切换(Involuntary),以上场景1属于自愿切换,场景2和3属于强制切换。

  • 自愿切换发生的时候,进程不再处于运行状态,比如由于等待IO而阻塞(TASK_UNINTERRUPTIBLE),或者因等待资源和特定事件而休眠(TASK_INTERRUPTIBLE),又或者被debug/trace设置为TASK_STOPPED/TASK_TRACED状态;
  • 强制切换发生的时候,进程仍然处于运行状态(TASK_RUNNING),通常是由于被优先级更高的进程抢占(preempt),或者进程的时间片用完了。

注意:进程可以通过调用sched_yield()主动交出CPU,这不是自愿切换,而是属于强制切换,因为进程仍然处于运行状态。有时候内核代码会在耗时较长的循环体内通过调用 cond_resched()或yield() ,主动让出CPU,以免CPU被内核代码占据太久,给其它进程运行机会。这也属于强制切换,因为进程仍然处于运行状态。

进程自愿切换(Voluntary)和强制切换(Involuntary)的次数被统计在 /proc/<pid>/status 中,其中voluntary_ctxt_switches表示自愿切换的次数,nonvoluntary_ctxt_switches表示强制切换的次数,两者都是自进程启动以来的累计值。

也可以用 pidstat -w 命令查看进程切换的每秒统计值:

pidstat -w 1
Linux 3.10.0-229.14.1.el7.x86_64 (bj71s060)     02/01/2018      _x86_64_       (2 CPU)
 
12:05:20 PM   UID       PID   cswch/s nvcswch/s  Command
12:05:21 PM     0      1299      0.94      0.00  httpd
12:05:21 PM     0     27687      0.94      0.00  pidstat

自愿切换和强制切换的统计值在实践中有什么意义呢?
大致而言,如果一个进程的自愿切换占多数,意味着它对CPU资源的需求不高。如果一个进程的强制切换占多数,意味着对它来说CPU资源可能是个瓶颈,这里需要排除进程频繁调用sched_yield()导致强制切换的情况。

二、调度时机

自愿切换意味着进程需要等待某种资源,强制切换则与抢占(Preemption)有关。

抢占(Preemption)是指内核强行切换正在CPU上运行的进程,在抢占的过程中并不需要得到进程的配合,在随后的某个时刻被抢占的进程还可以恢复运行。发生抢占的原因主要有:进程的时间片用完了,或者优先级更高的进程来争夺CPU了。

抢占的过程分两步,第一步触发抢占,第二步执行抢占,这两步中间不一定是连续的,有些特殊情况下甚至会间隔相当长的时间:

  1. 触发抢占:给正在CPU上运行的当前进程设置一个请求重新调度的标志(TIF_NEED_RESCHED),仅此而已,此时进程并没有切换。
  2. 执行抢占:在随后的某个时刻,内核会检查TIF_NEED_RESCHED标志并调用schedule()执行抢占。

抢占只在某些特定的时机发生,这是内核的代码决定的。

 

触发抢占的时机

每个进程都包含一个TIF_NEED_RESCHED标志,内核根据这个标志判断该进程是否应该被抢占,设置TIF_NEED_RESCHED标志就意味着触发抢占。

直接设置TIF_NEED_RESCHED标志的函数是 set_tsk_need_resched();
触发抢占的函数是resched_task()。

TIF_NEED_RESCHED标志什么时候被设置呢?在以下时刻:

周期性的时钟中断

时钟中断处理函数会调用scheduler_tick(),这是调度器核心层(scheduler core)的函数,它通过调度类(scheduling class)的task_tick方法 检查进程的时间片是否耗尽,如果耗尽则触发抢占:

/*
 * This function gets called by the timer code, with HZ frequency.
 * We call it with interrupts disabled.
 */
void scheduler_tick(void)

    ...
    curr->sched_class->task_tick(rq, curr, 0);
    ...

 

唤醒进程的时候

当进程被唤醒的时候,如果优先级高于CPU上的当前进程,就会触发抢占。相应的内核代码中,try_to_wake_up()最终通过check_preempt_curr()检查是否触发抢占。

新进程创建的时候

如果新进程的优先级高于CPU上的当前进程,会触发抢占。相应的调度器核心层代码是sched_fork(),它再通过调度类的 task_fork方法触发抢占:

int sched_fork(unsigned long clone_flags, struct task_struct *p)

        ...
        if (p->sched_class->task_fork)
                p->sched_class->task_fork(p);
        ...

进程修改nice值的时候

如果进程修改nice值导致优先级高于CPU上的当前进程,也会触发抢占。内核代码参见 set_user_nice()。

进行负载均衡的时候

在多CPU的系统上,进程调度器尽量使各个CPU之间的负载保持均衡,而负载均衡操作可能会需要触发抢占。

不同的调度类有不同的负载均衡算法,涉及的核心代码也不一样,比如CFS类在load_balance()中触发抢占:

load_balance()

        ...
        move_tasks();
        ...
        resched_cpu();
        ...

RT类的负载均衡基于overload,如果当前运行队列中的RT进程超过一个,就调用push_rt_task()把进程推给别的CPU,在这里会触发抢占。

 

执行抢占的时机

触发抢占通过设置进程的TIF_NEED_RESCHED标志告诉调度器需要进行抢占操作了,但是真正执行抢占还要等内核代码发现这个标志才行,而内核代码只在设定的几个点上检查TIF_NEED_RESCHED标志,这也就是执行抢占的时机。

抢占如果发生在进程处于用户态的时候,称为User Preemption(用户态抢占);如果发生在进程处于内核态的时候,则称为Kernel Preemption(内核态抢占)。

执行User Preemption(用户态抢占)的时机

1. 从系统调用(syscall)返回用户态时;

源文件:arch/x86/kernel/entry_64.S
sysret_careful:
        bt $TIF_NEED_RESCHED,%edx
        jnc sysret_signal
        TRACE_IRQS_ON
        ENABLE_INTERRUPTS(CLBR_NONE)
        pushq_cfi %rdi
        call schedule
        popq_cfi %rdi
        jmp sysret_check

 

2. 从中断返回用户态时:

retint_careful:
        CFI_RESTORE_STATE
        bt    $TIF_NEED_RESCHED,%edx
        jnc   retint_signal
        TRACE_IRQS_ON
        ENABLE_INTERRUPTS(CLBR_NONE)
        pushq_cfi %rdi
        call  schedule
        popq_cfi %rdi
        GET_THREAD_INFO(%rcx)
        DISABLE_INTERRUPTS(CLBR_NONE)
        TRACE_IRQS_OFF
        jmp retint_check

 

执行Kernel Preemption(内核态抢占)的时机

inux在2.6版本之后就支持内核抢占了,但是请注意,具体取决于内核编译时的选项:

  • CONFIG_PREEMPT_NONE=y

  不允许内核抢占。这是SLES的默认选项。

  • CONFIG_PREEMPT_VOLUNTARY=y

  在一些耗时较长的内核代码中主动调用cond_resched()让出CPU。这是RHEL的默认选项。

  • CONFIG_PREEMPT=y

  允许完全内核抢占。
在 CONFIG_PREEMPT=y 的前提下,内核态抢占的时机是:

1. 中断处理程序返回内核空间之前会检查TIF_NEED_RESCHED标志,如果置位则调用preempt_schedule_irq()执行抢占。preempt_schedule_irq()是对schedule()的包装。

#ifdef CONFIG_PREEMPT
 
        /* Returning to kernel space. Check if we need preemption */
 
        /* rcx:  threadinfo. interrupts off. */
 
ENTRY(retint_kernel)
 
        cmpl $0,TI_preempt_count(%rcx)
 
        jnz  retint_restore_args
 
        bt  $TIF_NEED_RESCHED,TI_flags(%rcx)
 
        jnc  retint_restore_args
 
        bt   $9,EFLAGS-ARGOFFSET(%rsp)  /* interrupts off? */
 
        jnc  retint_restore_args
 
        call preempt_schedule_irq
 
        jmp exit_intr
 
#endif

2. 当内核从non-preemptible(禁止抢占)状态变成preemptible(允许抢占)的时候;
在preempt_enable()中,会最终调用 preempt_schedule 来执行抢占。preempt_schedule()是对schedule()的包装。

 

Linux进程管理 实时调度

关键词:RT、preempt_count、RT patch。

 

除了CFS调度器之外,还包括重要的实时调度器,有两种RR和FIFO调度策略。本章只是一个简单的介绍。

更详细的介绍参考《Linux进程管理 (9)实时调度类分析,以及FIFO和RR对比实验》。

同时为了提高Linux的实时性,Linux社区还维护了realtime相关的补丁。这些补丁的介绍在《Linux实时补丁及其分析》。

 

1. 抢占内核

如果Linux内核不支持抢占,那么进程要么主动要求调度,如schedule()或者cond_resched();要么在系统调用、异常处理和中断处理完成返回用户空间前夕

 

在支持可抢占内核中,如果唤醒动作发生在系统调用或者异常处理上下文中,在下一次调用preempt_enable()是会检查是否需要抢占调度;

中断处理返回前夕会检查是否要抢占当前进程,注意这里是中断返回而不是不支持抢占情况的用户空间返回

struct thread_info成员preempt_count计数表示内核是否可以被完全抢占,当preempt_count为0时,表示内核可以被安全抢占;大于0时则禁止抢占。

preempt_count是32bit,低8位用于抢占计数PREEMPT_ACTIVE表示一个很大的抢占计数,通常用于表示抢占调度。

内核提供preempt_disable()来关闭抢占,preempt_count会加1。preempt_enable()函数打开抢占,preempt_count减1后判断是否为0,并检查thread_info的TIF_NEED_RESCHED标志位,如果为0,则用schedule() 完成调度抢占。

#define preempt_disable() \\
do { \\
    preempt_count_inc(); \\----------------------------------------对当前current_thread_info()->preempt_count加1
    barrier(); \\
} while (0)

#define preempt_count_inc() preempt_count_add(1)
#define preempt_count_add(val)    __preempt_count_add(val)
static __always_inline void __preempt_count_add(int val)
{
    *preempt_count_ptr() += val;
}

#define preempt_enable() \\ do { \\ barrier(); \\ if (unlikely(preempt_count_dec_and_test())) \\-----------------prermpt_count减1后为0,且TIF_NEED_RESCHED被置位,则进行schedule()调度抢占。 __preempt_schedule(); \\ } while (0) static __always_inline bool __preempt_count_dec_and_test(void) { return !--*preempt_count_ptr() && tif_need_resched();---------对当前preempt_count减1并判断是否为0,如果为0则检查TIF_NEED_RESCHED } static __always_inline int *preempt_count_ptr(void) { return &current_thread_info()->preempt_count; } #define tif_need_resched() test_thread_flag(TIF_NEED_RESCHED)-----测试TIF_NEED_RESCHED是否置位 #define test_thread_flag(flag) \\ test_ti_thread_flag(current_thread_info(), flag) #define __preempt_schedule() preempt_schedule() asmlinkage __visible void __sched notrace preempt_schedule(void) { if (likely(!preemptible()))-----------------------------------判断当前preempt_count是否为0,并且irq没有被禁止。 return; preempt_schedule_common();------------------------------------__schedule()调度抢占。 }
# define preemptible()	(preempt_count() == 0 && !irqs_disabled())

static void __sched notrace preempt_schedule_common(void)
{
    do {
        __preempt_count_add(PREEMPT_ACTIVE);
        __schedule();
        __preempt_count_sub(PREEMPT_ACTIVE);

        /*
         * Check again in case we missed a preemption opportunity
         * between schedule and now.
         */
        barrier();
    } while (need_resched());
}

 

 

2. 内核实时进展

Linux在提高实时性方面取得一系列进展,具体如下:

主要功能 内核版本 说明
Preemption suport 2.5  
PI Mutexes N/A PI即Priority Inheritance,优先级继承的互斥体
HR Timer 2.6.24 高精度定时器
Preemptive RCU 2.6.25 可抢占RCU
IRQ Threads 2.6.30 中断线程化
Forced IRQ Threads 2.6.39 强制中断线程化
Deadline scheduler 3.14 Deadline调度器
Full Realtime Preemption support rt-patches rt.wiki.kernel.org

 

 

3. 内核延迟调试工具

内核提供了一些接口、工具,使我们得以一窥调度延迟。常用的有个ftrace的调度器preemptirqoff、等,以及工具latencytop、cyclictest等。

3.1 ftrace preemptirqsoff

preemptirqsoff可以跟踪关闭中断并禁止进程抢占代码的延时,同时记录关闭的最大时长。

这些tracer可以在Kernel hacking->Tracers中打开。 

查看/sys/kernel/debug/tracing/available_tracers可以知道当前支持的tracer,里面有preemptirqsoff、preemptoff、irqsoff三种。

更详细的解释参照《Linux ftrace框架介绍及运用》。

下面是一个preemptirqsoff实例,可以看出禁止抢占、屏蔽中断的函数排列。以及最大值的进程信息和发生时的栈信息。

# tracer: preemptirqsoff
#
# preemptirqsoff latency trace v1.1.5 on 4.4.138-rt155-custom
# --------------------------------------------------------------------
# latency: 628 us, #39/39, CPU#0 | (M:preempt VP:0, KP:0, SP:0 HP:0 #P:8)
#    -----------------
#    | task: gnome-shell-1775 (uid:1000 nice:0 policy:0 rt_prio:0)
#    -----------------
#  => started at: schedule
#  => ended at:   migrate_disable
#
#
#                  _--------=> CPU#              
#                 / _-------=> irqs-off          
#                | / _------=> need-resched      
#                || / _-----=> need-resched_lazy 
#                ||| / _----=> hardirq/softirq   
#                |||| / _---=> preempt-depth     
#                ||||| / _--=> preempt-lazy-depth
#                |||||| / _-=> migrate-disable   
#                ||||||| /     delay             
# cmd     pid    |||||||| time   |  caller       
#     \\   /      ||||||||   \\    |  /            
...
gnome-sh-1775    0....21.   41us!: preempt_count_sub <-_raw_spin_unlock_irq
gnome-sh-1775    0....11.  627us : pin_current_cpu <-migrate_disable
gnome-sh-1775    0....111  628us : preempt_count_sub <-migrate_disable
gnome-sh-1775    0....111  628us : migrate_disable <-migrate_disable
gnome-sh-1775    0....111  629us+: trace_preempt_on <-migrate_disable
gnome-sh-1775    0....111  686us : <stack trace>
 => preempt_count_sub
 => migrate_disable
 => rt_spin_lock
 => add_wait_queue
 => __pollwait
 => unix_poll
 => sock_poll
 => do_sys_poll
 => SyS_poll
 => entry_SYSCALL_64_fastpath

 

 

3.2 latencytop

latencytop在内核上下文切换时记录被切换进程的内核栈,然后通过匹配内核栈函数来判断导致上下文切换的原因。

方便判断系统出现哪方面的延迟,还能查看某个进程或者线程的延迟情况。

使用latencytop需要安装libcanberra-gtk-module,并且使能CONFIG_LATENCYTOP(通过Kernel hacking->Latency measuring infrastructure打开)。

执行sudo latencytop,得到如下结果。

整个结果分为三部分,Targets->Cause->Backtrace,分别是进程->问题点->问题点栈回溯。

 

以上是关于linux进程的管理和调度 --- 调度相关的主要内容,如果未能解决你的问题,请参考以下文章

Linux 进程管理之进程调度与切换

Linux(内核剖析):12---进程调度之与调度相关的系统调用

《Linux内核设计与实现》读书笔记- 进程的调度

Linux进程调度策略的发展和演变--Linux进程的管理与调度(十六)

Linux 进程调度

Linux 操作系统原理 — 进程管理 — 进程调度