Linux:进程模型和进程管理
1 进程与程序
在Linux系统中,执行一个程序或命令就可以触发一个进程,系统会给予这个进程一个ID,称为PID,同时根据触发这个进程的用户与相关属性关系,基于这个PID一组有效的权限设置。如下图所示(图片来自《鸟哥的Linux私房菜》[1]):
举个常见的例子,我们要操作系统的时候通常是利用ssh连接程序或直接在主机上登录,然后获取shell。默认的shell是bash,对应的路径为/bin/bash
,那么同时间的每个人登录都是执行/bin/bash
,不过每个人获取的权限不同,如下图所示:
也就是说,当我们的登录并执行/bin/bash
程序时,系统已经给了我们一个PID,这个PID就是根据登陆者的UID/GID(/etc/passwd
)而来,而所谓用户的权限就是这个bash进程的权限。当这个进程执行其它作业时,比如我们在shell中执行touch
命令时,这个进程触发出来的其它进程也会沿用这个进程的相关权限。
我们对程序与进程做一个总结:
- 程序(program):通常为二进制程序,存储在存储媒介中(如磁盘等),以物理文件的形式存在。
- 进程(process):程序被触发后,执行者的权限与属性、程序的代码与所需数据等都会被加载到内存中,操作系统给予这个内存中的单元一个标识符(PID),可以说进程就是一个正在运行中的程序。
进程彼此之间是有关系的。从下图来看,连续执行两个bash后,第二个bash的父进程就是前一个bash,通过Parent PID(PPID)可获取其父进程的PID:
此外,子进程可以获取父进程的环境变量。
下面这个例子我们会展示子进程和父进程的关系。我们在当前的bash环境下,再触发一次bash,并用ps -l
命令查看进程相关的输出信息,
bash-3.2$ ps -l
UID PID PPID F CPU PRI NI SZ RSS WCHAN S ADDR TTY TIME CMD
501 9892 9827 4006 0 31 0 408650672 1968 - S 0 ttys001 0:00.01 /bin/bash
501 9905 9892 4006 0 31 0 408657840 2736 - S 0 ttys001 0:00.01 /bin/bash
可以看到第一个bash的PID和第二个bash的PPID都是9892,这是因为第二个bash是来自第一个所产生的。
很多常常会发现,“咦,我们们将有问题的进程关闭了(比如Ctrl+C
杀掉),怎么过一阵子它又自动产生了?而且新产生进程的PID还与原先不同这是怎么回事呢?”如果不是crontab计划任务的影响,那么肯定有一个父进程存在,所以我们杀掉子进程后,父进程又会主动再生成一个。那怎么办呢?“擒贼先擒王”。我们用ps auxf
找出那个父进程,然后将它杀掉即可(后文会提到)。
fork and exec:进程调用的流程
子进程和父进程之间的关系比较复杂,最大的复杂点在于进程之间的调用。Linux程序的调用通常称为fork-and-exec流程。进程都会借由父进程以复制(fork)的方式产生一个一模一样的子进程,然后被复制出来的子进程再以exec的方式来执行时机要执行的代码,最终就成为一个子进程。整个流程有点像下面这张图:
- 系统先以fork的方式复制一个与父进程相同的临时进程,这个进程与父进程唯一的差别就是PID不同,但这个临时进程还会多一个PPID参数。
- 然后临时进程开始以exec的方式加载实际要执行的进程。以上图来讲,新的程序名称为
qqq
,最终子进程的进程代码就会变成qqq
了。
系统或网络服务:常驻在内存里的进程
一般的Linux命令(如ls
、touch
、rm
等)都是执行完就结束,也就是说该项命令被触发后所产生的PID很快就会被终止,那么有没有一直在执行的进程呢?
当然有,我们把在后台启动并一直持续不断地运行,也即常驻在内存当中的进程称为守护进程(daemon)。常见的服务包括系统本身所需要的服务(例如crond、atd、rsyslogd等)和负责网络连接的服务(例如apache、named、postfix、vsftpd等)。网络服务比较有趣的地方在于,它会启动一个可以复杂网络监听的端口(port),以提供外部客户端(client)的连接请求。
PS1:在Linux系统中,一般daemon类型的进程都会在文件名后面加上d。
PS2:“守护进程”这个概念由麻省理工学院MAC项目的程序员发明。费南多·柯巴托于1963年在MAC项目任务。根据他的说法,他的团队最早采用daemon这个概念,其灵感来源于麦克斯韦妖——一种物理学和热力学中虚构的介质,能帮助排列分子。他对此表示:“我们别出心裁地开始使用daemon这个词来描述后台进程,它们不知疲倦地处理系统中的杂务。”Unix系统继承了这个术语。作为一种在后台起作用的超自然存在,麦克斯韦妖与古希腊神话中的代蒙一致[2]。关于麦克斯韦妖的更多有趣信息可以参见梅拉妮·米歇尔的《复杂》[3]一书。
2 进程管理
想要查看系统上正在运行中的进程,可以利用静态的ps
或者是动态的top
命令,还可以利用pstree
来查看进程树之间的关系。
ps:将某个时间点的进程运行情况撷取下来
ps aux
可查看系统中所有的进程(注意,没有-
号):
~/Orion-Orion # ps -aux root@qi
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 4504 48 pts/0 Ss 2022 0:01 sh /root/start.sh
root 7 0.0 0.0 65520 420 ? Ss 2022 0:07 /usr/sbin/sshd
root 8 0.0 0.0 20052 264 pts/0 S+ 2022 0:00 /bin/bash
...
root 39717 0.1 0.0 93648 7608 ? Ss 08:40 0:02 sshd: root@notty
root 39727 0.0 0.0 9752 2924 ? Ss 08:40 0:00 bash
root 40140 0.5 0.0 95296 9220 ? Rs 08:54 0:02 sshd: root@notty
root 40150 0.0 0.0 9752 2832 ? Ss 08:54 0:00 bash
ps -l
则可以仅查看自己的bash相关的进程:
~/Orion-Orion # ps -l root@qi
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 0 42331 41909 0 80 0 - 5015 wait pts/312 00:00:00 bash
0 R 0 42379 42331 0 80 0 - 6920 - pts/312 00:00:00 ps
我们可以看到,系统整体运行的进程是非常多的,但使用ps -l
仅会列出与你的操作环境(bash)有关的进程,即最上层的父进程会是你自己的bash而没有扩展到systemd
(后续会介绍)这个进程中。我们接下来来看看ps -l
显示出来的数据有哪些?
- F:表示这个进程标识(process flags),说明这个进程的权限,常见号码有:
- 若为4表示此进程的权限为root。
- 若为表示此进程仅执行复制(fork)而没有实际执行(exec)。
- 若为0表示进程标识没有设置。
- S: 代表这个进程的状态(STAT),主要的状态有:
- R(Running):该进程正在运行中(running) 或是可运行的(runnable)。
- S(Sleep):进程处于可被唤醒(signal)的睡眠状态,也即所谓空闲状态(idle)。这种状态一般是进程主动进入的。
- D:进程处于不可被唤醒的睡眠状态,通常这个进程可能在等待I/O的情况(例如打印)。这种状态一般是进程被动进入的。
- T(Stopped):停止状态,可能是在任务控制(后台暂停)或跟踪(traced)状态。
- Z(Zombie):僵尸状态(即所谓defunct),进程已经终止但却无法被删除至内存外。
- UID/PID/PPID:代表进程被该UID所拥有/进程的PID号码/此进程的父进程PID号码。
- C:代表CPU使用率,单位为百分比。
- PRI/NI:Priority/Nice的缩写,代表此进程被CPU所执行的优先级,数值越小代表该进程越快被CPU执行。详细的PRI与NI将在下一小节说明。
- ADDR/SZ/WCHAN:都与内存相关,ADDR是kernel function,指出该进程在内存的哪个部分,如果是个running的进程,一般就会显示
-
;SZ代表此进程用掉多少内存;WCHAN表示目前进场是否运行,同样的,若为-
表示正在运行中。 - TTY:登录者的终端位置,若为远程登录则使用动态终端接口名称(
pts/n
)。 - TIME:使用CPU的时间,注意是进程实际花费CPU的时间,而不是运行时间。关于这两个时间之间的区别可参见我的博客《Python:对程序做性能分析及计时统计》
- CMD:就是command的缩写,表示触发此进程的命令是什么。
所以你看到的ps -l
输出信息中,它说明的是:bash进程属于UID为0的用户,状态为睡眠(Sleeping),之所以为睡眠,是因为它触发了ps
(状态为Running)。此进程的PID是42331,执行优先顺序为80,执行bash所获取的终端接口为pts/0
。运行状态为等待(wait)。
接下来我们用ps auxf
列出类似进程树的进程显示:
(base) root@qi:~/Orion-Orion# ps -auxf
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 4504 48 pts/0 Ss 2022 0:01 sh /root/start.sh
root 7 0.0 0.0 65520 420 ? Ss 2022 0:07 /usr/sbin/sshd
root 38677 0.0 0.0 93284 7412 ? Ss 07:55 0:00 \_ sshd: root@notty
root 38687 0.0 0.0 9756 2824 ? Ss 07:55 0:00 | \_ bash
root 38765 0.0 0.0 4504 1712 ? S 07:55 0:00 | \_ sh /root/.vscode-server/bin/b7886d7461186a5
root 41898 0.1 0.0 661960 58144 ? Sl 10:38 0:06 | | \_ /root/.vscode-server/bin/b7886d7461
root 42331 0.0 0.0 20060 3788 pts/312 S 10:40 0:00 | | \_ bash
root 43592 0.0 0.0 36276 3248 pts/312 R+ 11:41 0:00 | | \_ ps -auxf
...
root 39717 0.0 0.0 93648 7608 ? Ss 08:40 0:02 \_ sshd: root@notty
root 39727 0.0 0.0 9752 2924 ? Ss 08:40 0:00 | \_ bash
root 43493 0.0 0.0 4376 672 ? S 11:40 0:00 | \_ sleep 180
root 41362 0.0 0.0 93296 7360 ? Ss 10:33 0:01 \_ sshd: root@notty
root 41372 0.0 0.0 9752 2824 ? Ss 10:33 0:00 \_ bash
root 43492 0.0 0.0 4376 700 ? S 11:39 0:00 \_ sleep 180
root 8 0.0 0.0 20052 264 pts/0 S+ 2022 0:00 /bin/bash
因为我是用ssh网络连接进入服务器来执行一些测试的,可以看出进程之间是相关性的。从上面的例子来看,我是通过sshd
提供的网络服务获取的一个进程,该进程提供bash给我使用,而我通过bash再去执行VSCode-Server服务启动脚本,以运行VSCode-Server服务进程,然后该进程再提供给我一个bash, 我通过这个bash再去执行ps auxf
(疯狂套娃哈哈哈)。
这里说一个题外话,
sshd
进程是我们前面提到的deamon,是不能随意杀掉的哦! 杀掉了不仅你会马上断开连接,下次再用ssh连你也连不上了。
除了f
这个选项,我们还可以使用pstree
来完全查看这个进程树。
(base) root@qi:~/Orion-Orion# pstree
sh─┬─bash
└─sshd─┬─sshd───bash─┬─sh───node─┬─node───12*[{node}]
│ │ ├─node─┬─node───10*[{node}]
│ │ │ ├─node───11*[{node}]
│ │ │ ├─python───{python}
│ │ │ └─11*[{node}]
│ │ ├─node───11*[{node}]
│ │ ├─node─┬─bash───pstree
│ │ │ └─11*[{node}]
│ │ └─10*[{node}]
│ └─sleep
└─2*[sshd───bash───sleep]
除此之外,我们必须要知道的是僵尸(zombie) 进程是什么?通常,造成僵尸进程的原因在于该进程应该已经执行完毕,或是应该要终止了,但该进程的父进程却无法完整地将该进程结束掉,而造成该进程一直存在内存中。如果你发现在某个进程的CMD
/COMMAND
后面接上了defunct
时,就代表该进程是僵尸进程,例如:
apache 8683 0.0 0.9 83384 9992 ? Z 14:33 0:00 /usr/sbin/httpd <defunct>
系统不稳定的时候就容易造成所谓的僵尸进程,可能是因为程序写得不好,或是用户的操作习惯不良等所造成的。如果你发现系统中有很多僵尸进程时,记得要找出该进程的父进程,然后好好做个追踪,好好进行主机的环境优化,看看有什么地方需要改善,而不是直接将它kill掉。不然万一它一直产生就麻烦了。
事实上,通常僵尸进程都已经无法管理,而直接交给 systemd
这个进程来负责,偏偏systemd
是系统第一个执行的进程,它是所有进程的父进程。我们是无法杀掉该进程的(杀掉它,系统就死掉了),所以如果产生僵尸进程,而系统过了一阵子还没有办法通过内核非经常性的特殊处理来将该进程删除时,那你只好通过reboot
的方式来将该进程kill掉。
systemd
是目前Linux系统上主要的系统守护进程管理工具,由于init
一方面对于进程的管理是串行化的,容易出现阻塞情况,另一方面init
也仅仅是执行启动脚本,并不能对服务本身进行更多的管理。所以最新系统(RedHat7,CentOS7,Ubuntu15…)大都由systemd取代了init
作为默认的系统进程管理工具。
top:动态查看进程的变化
相对于ps是选取一个时间点的进程状态,top
可以持续监测进程运行的状态,使用方式如下:
top [-d 数字] | top [-bnp]
它的选项与参数如下:
-d
:后面可以接秒数,就是整个进程界面更新的秒数,默认是5秒。-b
:以批量的方式执行top
,还有更多的参数可以使用,通常会搭配数据重定向来将批量的结果输出为文件。-n
:与-b
搭配,意义是需要执行几次top
的输出结果。-p
:指定某些PID来执行查看检测。
在top
的执行过程中可以使用下列的按键命令:
?
:显示在top
中可以输入的按键命令P
:以CPU的使用排序显示。M
:以Memory的使用排序显示。N
:以PID来排序。T
:由该进程使用的CPU时间累积(TIME+
)排序k
:给予某个PID一个信号(signal
)。r
:给予某个PID重新制定一个nice值。q
:退出top
的按键。
接下来我们实际查看一下如何使用top
与top
的界面。比如以下是我们输入top -d 2
命令得到的结果,该命令表示每两秒钟更新一次top,查看整体信息。
top - 13:19:06 up 202 days, 5:00, 3 users, load average: 89.81, 75.65, 68.67
Tasks: 74 total, 1 running, 73 sleeping, 0 stopped, 0 zombie
%Cpu(s): 32.2 us, 4.5 sy, 35.7 ni, 27.5 id, 0.0 wa, 0.0 hi, 0.1 si, 0.0 st
KiB Mem : 52701926+total, 52750784 free, 49904712 used, 42436377+buff/cache
KiB Swap: 8388604 total, 7983868 free, 404736 used. 46639996+avail Mem
<==如果加入k或r时,就会有相关的字样出现在这里。
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
38775 root 20 0 1106596 236232 37596 S 1.0 0.0 1:28.68 node
12191 root 20 0 1662128 61256 11176 S 0.5 0.0 205:36.04 python
1 root 20 0 4504 48 0 S 0.0 0.0 0:01.03 sh
7 root 20 0 65520 420 4 S 0.0 0.0 0:07.19 sshd
...
41787 root 20 0 20060 3700 32 S 0.0 0.0 0:00.01 bash
41898 root 20 0 968200 62276 33216 S 0.0 0.0 0:14.26 node
可见,top
与ps
的静态结果输出不同,top
这个进程可以持续地监测整个系统的进程任务状态。在默认的情况下更新进程资源的时间为5秒,不过可以使用-d
来执行修改。top
主要分为两部分界面,上面的界面为整个系统的资源使用状态,基本上总共有六行。至于top
下半部分的画面,则是每个进程使用的资源情况。
top
默认使用CPU使用率(%CPU
)作为排序的依据,如果你想要使用内存使用率排序,则可以按下M
键,若要恢复则按下P
键即可。如果想要退出top
,则按下q
。
参考
- [1] 鸟哥. 鸟哥的 Linux 私房菜: 基础学习篇(第四版)[M]. 人民邮电出版社, 2018.
- [2] 《维基百科:守护进程》
- [3] 梅拉妮·米歇尔. 复杂[M]. 湖南科学技术出版社, 2018.