Linux进程间关系与优先级设置简介
2020-03-11 22:35:36 阿炯

在配合开发调试过程中,发现进程因使用基于FIFO的实时优先级,而另一个与之交互的进程是使用普通调度的优先级,两个进程是互相通信的,它们在工作一段时间后都不能正常工作。初步分析为进程优先级的设定问题,分析问题过程中查找了一些资料,以加强自己的理解。

Linux进程调度的三种策略
(1)、SCHED_OTHER,分时调度策略
(2)、SCHED_FIFO,实时调度策略,先到先服务
(3)、SCHED_RR,实时调度策略,时间片轮转

由于涉及相关内容比较多,很难讲的清楚,关于进程调度更详细的内容建议读Linux内核相关的书籍。

前面遇到问题的进程就是使用SCHED_FIFO调度策略的,而另进程没有经过设置,默认是SCHED_OTHER。

通常意义上来讲,我们平时说优先级也正是指的上面字面意义上的优先级,代表对 cpu 的优先使用级别,高优先级的进程先使用,低优先级的进程后使用。对于没有时间片限制的一些调度算法来说,只有最高优先级的进程主动让出 cpu(完成它自己的工作),次低一点优先级的进程才可以获取 cpu 使用权。而对于有时间片限制的调度算法,优先级高的进程则代表它优先、较多地享有 cpu 使用权,注意:优先、较多。它意味着可能不等到高优先级任务主动出让 cpu,就会被强制切换到更低一点优先级的进程,但是总体来说,高优先级的进程会有比其它进程更多的 cpu 使用时间。

Linux 系统的调度类

实时调度类
Linux 上面的实时调度不是硬实时的,而是尽可能的实时,比如 SCHED_RR、SCHED_FIFO 两种,采用 RT 调度算法。但是估计是这种实时调度策略只能够在某些特定的场景下发挥作用,而有一些对周期要求比较严格的场景,比如多媒体数据流,则无法满足。于是后续出台了更硬的实时调度策略,它就是 SCHED_DEADLINE,采用 EDF 调度算法。

非实时调度类
就是大名鼎鼎的完全公平调度 CFS(Completely fair schedule)。在内核定义中是 SCHED_OTHER 或者 SCHED_NORMAL,这两个代表同样的含义。还有 SCHED_BATCH 与 SCHED_IDLE 这两种,不过不是很常用,不过他们同属于 CFS 调度算法,也就是说它们的优先级概念是一样的。

Linux 上面的调度类主要就是这几个了,还有一类叫做 SCHED_ISO,不过在内核里面作为保留字段,并没有实现。其中实时调度类使用 RT 调度算法,不过实时类的 SCHED_DEADLINE 调度类是 EDF 调度算法,非实时的调度类就是 CFS 调度算法了,可以推知,在 Linux 系统上面有三种优先级的概念,它们分别归属于不同的调度算法上,如下图所示:


在Linux中,调度器是基于线程的调度策略(scheduling policy)和静态调度优先级(static scheduling priority)来决定那个线程来运行。对于下面三种调度策略SCHED_OTHER, SCHED_IDLE, SCHED_BATCH,其调度优先级sched_priority是不起作用的,即可以看成其调度优先级为0;调度策略SCHED_FIFO和SCHED_RR是实时策略,他们的调度值范围是1到99,数值越大优先级越高,另外实时调度策略的线程总是比前面三种通常的调度策略优先级更高。通常,调度器会为每个可能的调度优先级(sched_priority value)维护一个可运行的线程列表,并且是以最高静态优先级列表头部的线程作为下次调度的线程。所有的调度都是抢占式的:如果一个具有更高静态优先级的线程转换为可以运行了,那么当前运行的线程会被强制进入其等待的队列中。下面介绍几种常见的调度策略:

SCHED_OTHER:该策略是是默认的Linux分时调度(time-sharing scheduling)策略,它是Linux线程默认的调度策略。SCHED_OTHER策略的静态优先级总是为0,对于该策略列表上的线程,调度器是基于动态优先级(dynamic priority)来调度的,动态优先级是跟nice中相关(nice值可以由接口nice,setpriority,sched_setattr来设置),该值会随着线程的运行时间而动态改变,以确保所有具有SCHED_OTHER策略的线程公平运行。在Linux上,nice值的范围是-20到+19,默认值为0;nice值越大则优先级越低,相比高nice值(低优先级)的进程,低nice值(高优先级)的进程可以获得更多的处理器时间。使用命令ps -el查看系统的进程列表,其中NI列就是进程对应的nice值;使用top命令,看到的NI列也是nice值。运行命令的时候可用nice –n xx cmd来调整cmd任务的nice值,xx的范围是-20~19之间。

SCHED_FIFO:先入先出调度策略(First in-first out scheduling)。该策略简单的说就是一旦线程占用cpu则一直运行,一直运行直到有更高优先级任务到达或自己放弃。

SCHED_RR:时间片轮转调度(Round-robin scheduling)。该策略是SCHED_FIFO基础上改进来的,他给每个线程增加了一个时间片限制,当时间片用完后,系统将把该线程置于队列末尾。放在队列尾保证了所有具有相同优先级的RR任务的调度公平。使用top命令,如果PR列的值为RT,则说明该进程采用的是实时策略,即调度策略是SCHED_FIFO或者为SCHED_RR,而对于非实时调度策略(比如SCHED_OTHER)的进程,该列的值是NI+20,以供Linux内核使用。可以通过命令:
ps -eo state,uid,pid,ppid,rtprio,time,comm

来查看进程对应的实时优先级(位于RTPRIO列下),如果有进程对应的列显示'-',则说明它不是实时进程。注意任何实时策略进程的优先级都高于普通的进程,也就说实时优先级和nice优先级处于互不相交的两个范畴。

调度类的优先级
在内核里面,调度类的优先级顺序如下所示:

SCHED_DEADLINE > SCHED_FIFO/SCHEDRR > SCHED_NORMAl/SCHED_OTHER/SCHED_BATCH > SCHED_IDLE

调度类内核归一化优先级范围user 可设置优先级/NICE范围设置函数调度算法
SCHED_OTHER/NORMAL100~139-20~19sched_setpriorityCFS
SCHED_IDLE100~139-20~19sched_setpriorityCFS
SCHED_BATCH100~139-20~19sched_setpriorityCFS
SCHED_RR0~9899~1sched_setparamRT
SCHED_FIFO0~9899~1sched_setparamRT
SCHED_DEADLINE-1不可设置sched_setparamEDF


如何设置为实时进程

查找资料的时候发现有个链接,为什么设置FIFO策略,但和预想的不一致。可以通过top命令查看进程是否成功,如果为"rt"表示是实时进程了。如果不成功,可能是权限问题,需要roo权限。如果不调整调度策略,也可以提升进程优先级,使得进程得到更多的CPU,特别是交互式程序,用户体检更好。代码很简单,只需要调用nice(int n)函数即可。n的有效范围是-20~19,数值越小表示优先级越高。

用户空间的 NI,PRI

除了内核空间里面繁杂、错综的优先级概念,在用户空间也有一些容易混淆的概念,它们之间有着某种联系,这里主要就说用户空间使用 top 命令看到的 NI 与 PRI 。

NI 就是 nice 值,顾名思义,就是这个进程的友好程度,越友好的程序 NI 值越大(取值范围 -20~19),也就越慷慨,它们会使用相对较少的 cpu 时间,所谓优先级低;而不那么友好的进程则使用相对较多的 cpu 时间,所谓优先级高。由此可见,nice 值代表了可以使用的 cpu 资源的比例。

PRI 就是优先级,这个优先级不对应内核里面任何一个优先级的概念,可以看到它的值跟随 NI 值不断变化,在 CFS 调度时关系看起来就像是:PRI = 20+NI。实际上:PRI(new)=PRI(old)+nice,用户空间的进程刚初始化是默认都是 PRI=20,NI=0,当 NI 的值改变的时候,PRI 的值就显而易见。

在用户空间,我们想要设置某一个进程的 nice 值,那么这个进程一定是 CFS 调度算法下面的调度类,否则无意义,如果想让这个程序尽可能优先的运行,那么通过 nice 值修改可能并不会得到想象中的效果,更好的办法是提升进程的调度类,比如从 SCHED_OTHER 升级到 SCHED_RR。如果想让自己的进程尽可能多地使用 cpu 时间,那么减少 nice 值就是一个很好的选择。


用top或者ps -l查看进程会发现有PR(PRI) NI两个字段:
NI 是优先值,是用户层面的概念, PR是进程的实际优先级, 是给内核(kernel)看(用)的。
一般情况下,PR=NI+20, 如果一个进程的优先级PR是20, 那么它的NI(nice)值就是20-20=0。

进程调度优先级是从-20到19,一共40个级别,数字越大,表示进程的优先级越低。默认时候,进程的优先级是0。查看进程优先级有两个办法:ps和top。top命令显示的NI列的值,或者可以使用ps -efl来查看,也是在ni列表示了进程的优先级。

进程的优先级可以在程序运行的时候设置,也可以在程序运行过程中动态的修改。运行的时候设置进程的优先级可以使用nice命令,比如要使得top命令运行时候的优先级是5而不是默认的0,则可以使用nice -n 5 top,来使得top命令运行在5的优先级别。如果top命令已经在运行,则有两个办法可以动态的调整进程的级别。可以在top中输入r命令,然后按照提示输入top命令对应的进程号,再按照提示输入要调整到哪个级别。另一个方法是使用renice命令,帮助如下:
$ renice --help
usage: renice priority [ [ -p ] pids ] [ [ -g ] pgrps ] [ [ -u ] users ]

此命令使用也很简单,可以调整单个进程,一个用户或者一个组的所有进程的优先级。示例如下:
#renice +10 -u oracle,此命令把oracle用户的所有进程的优先级全部调为10,包括新创建的和已经在运行的oracle用户的所有进程。此处的+10并不是表 示在现有级别上再往上调整10个级别,而是调整到正10的级别,所以多次运行此命令,进程的优先级不会再发生变化,将一直停留在+10级别。

#renice 10 1825  将PID为1825的进程优先级调整为10

注意:如果不是root权限,则只能降调度优先级而不能提高,即使是自己用户的进程,自己把它调高了后,优先级也不能再被调会原来的值了,除非使用root用户来调回去。系统重启后,对进程优先级的调整全部失效,所有进程的调度回到默认的初始级别。

摘抄了部分的E文:

Short Answer

PR is the priority level. The lower the PR, the higher the priority of the process will be.

PR is calculated as follows:
for normal processes: PR = 20 + NI (NI is nice and ranges from -20 to 19)
for real time processes: PR = - 1 - real_time_priority (real_time_priority ranges from 1 to 99)

Long Answer

There are 2 types of processes, the normal ones and the real time For the normal ones (and only for those), nice is applied as follows:

Nice

The "niceness" scale goes from -20 to 19, whereas -20 is the highest priority and 19 the lowest priority. The priority level is calculated as follows:

PR = 20 + NI

Where NI is the nice level and PR is the priority level. So as we can see, the -20 actually maps to 0, while the 19 maps to 39.

By default, a program nice value is 0, but it is possible for the root user to launch programs with a specified nice value by using the following command:

nice -n <nice_value> ./your_program

Real Time

We could go even further. The nice priority is actually used for user programs. Whereas the UNIX/LINUX overall priority has a range of 140 values, nice value enables the process to map to the last part of the range (from 100 to 139). This equation leaves the values from 0 to 99 unreachable which will correspond to a negative PR level (from -100 to -1). To be able to access to those values, the process should be stated as "real time".

There are 5 scheduling policies in a LINUX environment that can be displayed with the following command:
chrt -m

Which will show the following list:
1. SCHED_OTHER   the standard round-robin time-sharing policy
2. SCHED_BATCH   for "batch" style execution of processes
3. SCHED_IDLE    for running very low priority background jobs.
4. SCHED_FIFO    a first-in, first-out policy
5. SCHED_RR      a round-robin policy

The scheduling processes could be divided into 2 groups, the normal scheduling policies (1 to 3) and the real time scheduling policies (4 and 5). The real time processes will always have priority over normal processes. A real time process could be called using the following command (The example is how to declare a SCHED_RR policy):
chrt --rr <priority between 1-99> ./your_program

To obtain the PR value for a real time process the following equation is applied:
PR = -1 - rt_prior

Where rt_prior corresponds to the priority between 1 and 99. For that reason the process which will have the higher priority over other processes will be the one called with the number 99.

It is important to note that for real time processes, the nice value is not used.

To see the current "niceness" and PR value of a process by top command.


Set Default Nice Value Of a Specific User's Processes

You can set the default nice value of a particular user or group in the /etc/security/limits.conf file. Its primary function is to define the resource limits for the users logged in via PAM.

The syntax for defining a limit for a user is as follows (and the possible values of the various columns are explained in the file):
#<domain>   <type>  <item>  <value>

Now use the syntax below where hard – means enforcing hard links and soft means – enforcing the soft limits.
<username>  <hard|soft>  priority  <nice value>

Alternatively, create a file under /etc/security/limits.d/ which overrides settings in the main file above, and these files are read in alphabetical order.

# vim /etc/security/limits.d/freeoa-priority.conf
freeoa  hard  priority  10
@users      -       priority    10
username    -       priority    19

Save and close, From now on, any process owned by freeoa will have a nice value of 10 and PR of 30.


前后台进程与孤儿进程和守护(Daemon类)进程的关系

前、后台进程和进程的关系

前台进程是占用当前终端的进程,只有该进程执行完成或被终止之后,才会释放终端并将终端交还给shell进程。

例如:
$ sleep 30

执行该命令后,将创建sleep进程,sleep进程是当前bash进程(假如当前的shell为bash)的子进程:

$ pstree -p | grep sleep
     |-bash(31207)---sleep(31800)

在30秒内,sleep进程将占用终端,所以此时的sleep称为前台进程。当睡眠30秒之后,前台进程sleep退出,终端控制权交还给当前shell进程,shell进程可继续向下运行命令或等待用户输入新命令。如果给命令加上一个&符号,该命令将在后台运行。

$ sleep 30 &

此时,sleep仍然是当前bash的子进程,但是它不会占用终端,而是在后台默默地运行,并且在30秒之后默默的退出。如果是在一个子Shell环境中运行一个前台进程呢?例如:
$ ( sleep 30 )

执行这个命令时,小括号会开启一个子Shell环境,这相当于当前的bash进程隔离了一个bash运行时环境。sleep进程将在这个新的子Shell环境中运行,sleep仍然是当前bash的子进程。由于它会占用当前的终端,所以它是前台进程。

30秒之后,sleep进程退出,它将释放终端,与此同时,子Shell环境也会随着sleep进程的终止而关闭。

如果不了解子Shell,也可以通过shell脚本来理解,或程序内部使用system()来理解,它们都是提供了一种执行外部命令的运行环境。例如:bash -c 'sleep 30',sleep进程将在该bash进程提供的环境下运行,它是该bash进程的子进程。

再例如shell脚本:
#!/bin/bash
sleep 30

sleep将在这个bash脚本进程提供的环境下运行,它是该脚本进程的子进程。

再例如Perl脚本:
#!/bin/perl
system('sleep 30')

sleep将在这个Perl脚本进程提供的环境下运行。

需注意,编程语言(如Perl)可能提供多种调用外部程序的方式,比如system('sleep 30')和system('sleep',30),这两种方式有区别:
system('sleep 30')会调用sh,并将sleep 30 作为sh -c的参数来运行,等价于sh -c 'sleep 30',所以sleep进程是sh进程的子进程,而sh进程是Perl进程的子进程:
1.因为调用了sh,所以允许shell金含行的解析,比如使用重定向、.管道符号等;
2.如果调用的命令是简单命令,没有涉及任何需要Shell解析的符号和逻辑,将优化为直接调用命令,而不通过sh来调用。

system('sleep', 30)是直接调用sleep程序,sleep进程是当前perl进程的子进程,因为没有调用中间程序sh,所以不支持Shell命令行的解析,即不能使用类似重定向、管道符号等特殊符号。


举几个例子帮助理解,假设有Perl脚本a.pl,其内三行内容为:
system('sleep',30);    #(1)
system('sleep 30 ; echo hhh');    #(2)
system('sleep 30');    #(3)

对于(1),命令和参数分开,perl将直接调用sleep程序,这时的sleep进程是perl进程a.pl的子进程,且不支持使用管道|、重定向> < >>、&&等等属于Shell支持的符号。

$ pstree -p | grep sleep
     |    `-bash(31696)---a.pl(32707)---sleep(32708)

对于(2),perl将调用sh,并将参数sleep 30; echo hhh作为sh -c的参数运行,等价于sh -c 'sleep 30; echo hhh',所以sh进程将是perl进程的子进程,sleep进程将是sh进程的子进程。

$ pstree -p | grep sleep
     |    `-bash(31696)---a.pl(32747)---sh(32748)---sleep(32749)

另外需要注意的是,(2)中的命令是多条命令,而不是简单的单条命令,因为识别多条命令并运行它们的能力是Shell解析提供的,所以上面涉及了Shell的解析过程。由于会调用sh命令,所以允许命令中使用Shell特殊符号,比如管道符号。

对于(3),perl本该调用sh,并将sleep 30作为sh -c的参数运行。但此处是一个简单命令,不涉及任何Shell解析过程,所以会优化为等价于system('sleep', 30)的方式,即不再调用sh,而是直接调用sleep,也即sleep不再是sh的子进程,而是perl进程的子进程:
$ pstree -p | grep sleep
     |      `-bash(31696)---a.pl(32798)---sleep(32799)

其实子shell中运行命令和system()运行命令的行为是类似的:
# sleep进程是当前shell进程的子进程
$ (sleep 30)

# 当前shell进程会创建一个子bash进程
# sleep进程和echo进程是该子bash进程的子进程
$ (sleep 30 ; echo hhh)

了解以上介绍后,想必能清晰地理解如下结论:
在进程A中通过system('cmd1 ' arg1,arg2...)或system('cmd1')的方式运行一个进程B,进程B将是进程A的子进程:
在Shell环境中直接运行命令,或者子Shell中运行单条简单命令(cmd1),也属于这种情况。

在进程A中骚过system('cmd_with_shell')的方式运行一个进程B,进程B的父进程是sh,sh的父进程是进程A:
在子Shell中运行非简单单条命令(即需要Shell解析的参与)时,也属于这种情况。


孤儿进程和Daemon类进程

如果在进程B退出前,父进程先退出了呢?这时进程B将成为孤儿进程,因为它的父进程已经死了。孤儿进程会被PID=1的systemd进程收养,所以进程B的父进程PPID会从原来的进程A变为PID=1的systemd进程。

注意,孤儿进程会继续保持运行,而不会随父进程退出而终止,只不过其父进程发生了改变。例如,在子Shell中运行后台命令:
$ (sleep 30 &)

因为后台符号&是属于Shell的,所以涉及到shell的解析过程,所以当前bash进程会创建一个子bash进程来解析命令并提供sleep进程的运行环境。

sleep进程将在这个子bash进程环境中运行,但因为它是一个后台命令,所以sleep进程创建成功之后立即返回,由于小括号内已经没有其它命令,子bash进程会立即终止。这意味着sleep将成为孤儿进程:
$ ps -o pid,ppid,cmd $(pgrep sleep)
   PID   PPID CMD
 32843      1 sleep 30
 
再比如,Shell脚本内部运行一个后台命令,并且让Shell脚本在后台命令退出前先退出。
#!/bin/bash
sleep 300 &
echo over

当上述脚本运行时,sleep在后台运行并立即返回,于是立即执行echo进程,echo执行完成后脚本进程退出。

脚本进程退出前,sleep进程的父进程为脚本进程,脚本进程退出后,sleep进程成为孤儿进程继续运行,它会被systemd进程收养,其父进程变成PID=1。

当一个进程脱离了Shell环境后,它就可以被称为后台服务类进程,即Daemon类守护进程,显然Daemon类进程的PPID=1。当某进程脱离Shell的控制,也意味着它脱离了终端:当终端断开连接时,不会影响这些进程。

需特别关注的是创建Daemon类进程的流程:先有一个父进程,父进程在某个时间点fork出一个子进程继续运行代码逻辑,父进程立即终止,该子进程成为孤儿进程,即Daemon类进程。当然,要创建一个完善的Daemon类进程还需考虑其它一些事情,比如要独立一个会话和进程组,要关闭stdin/stdout/stderr,要chdir到/下防止文件系统错误导致进程异常等等。不过最关键的特性仍在于其脱离Shell、脱离终端。

为什么要fork一个子进程作为Daemon进程?为什么父进程要立即退出?

所有的Daemon类进程都要脱离Shell脱离终端,才能不受终端不受用户影响,从而保持长久运行。

在代码层面上,脱离Shell脱离终端是通过setsid()创建一个独立的Session实现的,而进程组的首进程(pg leader)不允许创建新的Session自立山头,只有进程组中的非首进程(比如进程组首进程的子进程)才能创建会话,从而脱离原会话。而Shell命令行下运行的命令,总是会创建一个新的进程组并成为leader进程,所以要让该程序成为长久运行的Daemon进程,只能创建一个新的子进程来创建新的session脱离当前的Shell。

另外,父进程立即退出的原因是可以立即将终端控制权交还给当前的Shell进程。但这不是必须的,比如可以让子进程成为Daemon进程后,父进程继续运行并占用终端,只不过这种代码不友好罢了。

换言之:当用户运行一个Daemon类程序时,总是会有一个瞬间消失的父进程。

前面演示的几个孤儿进程示例已经说明了这一点。为了更接近实际环境,这里再用nginx来论证这个现象。默认配置下,nginx以daemon方式运行,所以nginx启动时会有一个瞬间消失的父进程。

$ ps -o pid,ppid,comm; nginx; ps -o pid,ppid,comm $(pgrep nginx)
   PID   PPID COMMAND
 34126  34124 bash
 34194  34126 ps
   PID   PPID COMMAND
 34196      1 nginx
 34197  34196 nginx
 34198  34196 nginx
 34200  34196 nginx
 34201  34196 nginx

第一个ps命令查看到当前分配到的PID值为34194,下一个进程的PID应该分配为34195,但是第二个ps查看到nginx的main进程PID为34196,中间消失的就是nginx main进程的父进程。

可以修改配置文件使得nginx以非daemon方式运行,即在前台运行,这样nginx将占用终端,且没有中间的父进程,占用终端的进程就是main进程。

$ ps -o pid,ppid,comm; nginx -g 'daemon off;' &
   PID   PPID COMMAND
 34126  34124 bash
 34439  34126 ps     #--> ps PID=34439
[1] 34440            #--> NGINX PID=34440

[~]->$ ps -o pid,ppid,comm $(pgrep nginx)
   PID   PPID COMMAND
 34440  34126 nginx
 34445  34440 nginx
 34446  34440 nginx
 34447  34440 nginx
 34448  34440 nginx

最后,需要区分后台进程和Daemon类进程,它们都在后台运行。但普通的后台进程仍然受shell进程的监督和管理,用户可以将其从后台调度到前台运行,即让其再次获得终端控制权。而Daemon类进程脱离了终端、脱离了Shell,它们不再受Shell的监督和管理,而是接受pid=1的systemd进程的管理。


参考来源

linux进程优先级


浅析Linux线程调度