linux系统中,进程是资源调度的最小单位,进程的管理关乎着你使用linux系统的体验。

进程类型

Linux 系统里有几种不同类型的进程:用户进程(User processes)、守护进程(Deamon processes)和内核进程(Kernel processes)。

用户进程

系统里大多数进程都是用户进程。用户进程由通常的用户账户启动,并在用户空间(user space)当中执行。在没有获得额外许可的情况下,通常用户进程无法对处理器进行特殊访问,或是访问启动进程的用户无权访问的文件。

守护进程

守护进程通常是后台程序,它们往往由一些持续运行的服务来管理。守护进程可以用来监听请求,而后访问某些服务。举例来说,httpd这一守护进程监听访问网络页面的请求。守护进程也可以用来自行启动一些任务。例如,crond 这一守护进程会在预设的时间点启动计划任务。

尽管用于管理守护进程的服务通常是 root 用户启动的,但守护进程本身往往以非 root 用户启动。这种启动方式,符合「只赋予进程运行所必须的权限」之要求,因而能使系统免于一些攻击。举例来说,若是黑客骇入了 httpd 这一由 Apache 用户启动的守护进程,黑客仍然无法访问包括 root 用户在内的其他用户的文件,或是影响其他用户启动的守护进程。

守护进程通常由系统在启动时拉起,而后一直运行到系统关闭。当然,守护进程也可以按需启动和终止,以及让守护进程在特定的系统运行级别上执行,或是在运行过程中触发重新加载配置信息。

内核进程

内核进程仅在内核空间(kernel space)当中执行。内核进程与守护进程有些相似,它们之间主要的不同在于:

  1. 内核进程对内核数据结构拥有完全的访问权限。
  2. 内核进程不如守护进程灵活:修改配置文件并触发重载即可修改守护进程的行为;但对于内核进程来说,修改行为则需要重新编译内核本身。

进程状态

linux是一个多用户、多任务的系统,可以同时运行多个用户的多个程序,就必然会产生很多的进程,而每个进程会有不同的状态。同一时间同一CPU上只能运行一个进程,其他进程只能等待,因此我们可以宽泛地将进程状态分为:

  • 在CPU上执行,此时进程正在运行
  • 不在CPU上执行,此时进程不在运行

进一步来讲,未在运行的进程也可能处于不同的状态:

  • TASK_RUNNING
  • TASK_INTERRUPTIBLE
  • TASK_UNINTERRUPTIBLE
  • TASK_STOPPED/TASK_TRACED
  • TASK_DEAD - EXIT_ZOMBIE
  • TASK_DEAD - EXIT_DEAD

R(TASK_RUNNING,可执行状态)

只有在该状态的进程才可能在CPU上运行。而同一时刻可能有多个进程处于可执行状态,这些进程的task_struct结构(进程控制块)被放入对应CPU的可执行队列中(一个进程最多只能出现在一个CPU的可执行队列中)。进程调度器的任务就是从各个CPU的可执行队列中分别选择一个进程在该CPU上运行。很多操作系统教科书将正在CPU上执行的进程定义为RUNNING状态、而将可执行但是尚未被调度执行的进程定义为READY状态,这两种状态在linux下统一为TASK_RUNNING状态。

S(TASK_INTERRUPTIBLE,可中断状态)

这个状态的进程因为等待某事件的发生(比如等待socket连接、等待信号量等)而被挂起,然后当这些事件发生或完成后,对应的等待队列中的一个或多个进程将被唤醒。一般情况下,系统中的大部分进程都处于这个状态。因为系统的CPU数量是有限的,而系统的进程数量是非常多的,所以大部分进程都处于睡眠状态。

D(TASK_UNINTERRUPTIBLE,不可中断状态)

TASK_INTERRUPTIBLE状态类似,进程处于睡眠状态,但是此刻进程是不可中断的。不可中断,指的并不是CPU不响应外部硬件的中断,而是指进程不响应异步信号。

绝大多数情况下,进程处在睡眠状态时,总是应该能够响应异步信号的。否则你将惊奇的发现,kill -9竟然杀不死一个正在睡眠的进程了!因此我们也很好理解ps命令看到的进程几乎不会出现TASK_UNINTERRUPTIBLE状态,而总是TASK_INTERRUPTIBLE状态。

TASK_UNINTERRUPTIBLE状态存在的意义就在于,内核的某些处理流程是不能被打断的。如果响应异步信号,程序的执行流程中就会被插入一段用于处理异步信号的流程(这个插入的流程可能只存在于内核态,也可能延伸到用户态),于是原有的流程就被中断了。在进程对某些硬件进行操作时(比如进程调用read系统调用对某个设备文件进行读操作,而read系统调用最终执行到对应设备驱动的代码,并与对应的物理设备进行交互),可能需要使用TASK_UNINTERRUPTIBLE状态对进程进行保护,以避免进程与设备交互的过程被打断,造成设备陷入不可控的状态。这种情况下的TASK_UNINTERRUPTIBLE状态总是非常短暂的,通过ps命令基本上不可能捕捉到。

linux系统中也存在容易捕捉的TASK_UNINTERRUPTIBLE状态。执行vfork系统调用后,父进程将进入TASK_UNINTERRUPTIBLE状态,直到子进程调用exitexec
通过下面的代码就能得到处于TASK_UNINTERRUPTIBLE状态的进程:

1
2
3
4
5
6
#include   
void main() {  
if (!vfork()) {
sleep(100);  
}
}

T (TASK_STOPPED or TASK_TRACED),暂停状态或跟踪状态

向进程发送一个SIGSTOP信号,它就会因响应该信号而进入TASK_STOPPED状态(除非该进程本身处于TASK_UNINTERRUPTIBLE状态而不响应信号)。(SIGSTOPSIGKILL信号一样,是非常强制的。不允许用户进程通过signal系列的系统调用重新设置对应的信号处理函数。) 向进程发送一个SIGCONT信号,可以让其从TASK_STOPPED状态恢复到TASK_RUNNING状态。

当进程正在被跟踪时,它处于TASK_TRACED这个特殊的状态。“正在被跟踪”指的是进程暂停下来,等待跟踪它的进程对它进行操作。比如在gdb中对被跟踪的进程下一个断点,进程在断点处停下来的时候就处于TASK_TRACED状态。而在其他时候,被跟踪的进程还是处于前面提到的那些状态。

对于进程本身来说,TASK_STOPPEDTASK_TRACED状态很类似,都是表示进程暂停下来。而TASK_TRACED状态相当于在TASK_STOPPED之上多了一层保护,处于TASK_TRACED状态的进程不能响应SIGCONT信号而被唤醒。只能等到调试进程通过ptrace系统调用执行PTRACE_CONTPTRACE_DETACH等操作(通过ptrace系统调用的参数指定操作),或调试进程退出,被调试的进程才能恢复TASK_RUNNING状态。

Z(TASK_DEAD - EXIT_ZOMBIE),退出状态,进程成为僵尸进程

进程在退出的过程中,处于TASK_DEAD状态。在这个退出过程中,进程占有的所有资源将被回收,除task_struct结构(以及少数资源)以外。于是进程就只剩下task_struct这么个空壳,故称为僵尸。

之所以保留task_struct,是因为task_struct里面保存了进程的退出码、以及一些统计信息。而其父进程很可能会关心这些信息。比如在shell中,$?变量就保存了最后一个退出的前台进程的退出码,而这个退出码往往被作为if语句的判断条件。

当然,内核也可以将这些信息保存在别的地方,而将task_struct结构释放掉,以节省一些空间。但是使用task_struct结构更为方便,因为在内核中已经建立了从pidtask_struct查找关系,还有进程间的父子关系。释放掉task_struct,则需要建立一些新的数据结构,以便让父进程找到它的子进程的退出信息。

父进程可以通过wait系列的系统调用(如wait4、waitid)来等待某个或某些子进程的退出,并获取它的退出信息。然后wait系列的系统调用会顺便将子进程的尸体(task_struct)也释放掉。子进程在退出的过程中,内核会给其父进程发送一个信号,通知父进程来“收尸”。这个信号默认是SIGCHLD,在通过clone系统调用创建子进程时,可以设置这个信号。

X (TASK_DEAD - EXIT_DEAD),退出状态,进程即将被销毁

进程在退出过程中也可能不会保留它的task_struct。比如这个进程是多线程程序中被detach过的进程(进程?线程?参见《linux线程浅析》)。或者父进程通过设置SIGCHLD信号的handler为SIG_IGN,显式的忽略了SIGCHLD信号。(这是posix的规定,尽管子进程的退出信号可以被设置为SIGCHLD以外的其他信号。)
此时,进程将被置于EXIT_DEAD退出状态,这意味着接下来的代码立即就会将该进程彻底释放。所以EXIT_DEAD状态是非常短暂的,几乎不可能通过ps命令捕捉到。

进程树

每一个进程都是被别的进程启动的,或者说是复刻(Fork)的。当系统刚刚启动的时候,有一个非常特别的根进程 init ,它就是是直接被操作系统内核启动的。

这样一来,这个系统中运行的所有进程集合就构成了一颗以init进程为根节点的进程树,所有的其他进程都有一个父进程,也有可能有多个子进程。

比方说,每次我们在bash命令行提示符下执行一个命令的时候,bash 会复刻一个新的进程来执行这个命令,这时这个进程就变成了bash的子进程了。

相似地,当我们看见一个「登录」提示符时,这其实是login命令在运行着。如果我们成功的登录了,login命令会复刻一个新的进程来执行登录用户选择的shell

我们可以使用ps auxf命令来查看树形结构的进程列表,像下面这样:

1
2
3
4
5
6
7
8
9
10
-+= 00001 root /sbin/launchd
 |--= 00085 root /usr/libexec/logd
 |--= 00086 root /usr/libexec/UserEventAgent (System)
 |--= 00089 root /System/Library/PrivateFrameworks/Uninstall.framework/Resource
 |--= 00090 root /System/Library/Frameworks/CoreServices.framework/Versions/A/F
 |--= 00091 root /System/Library/PrivateFrameworks/MediaRemote.framework/Suppor
 |-+= 00093 root /usr/sbin/systemstats --daemon
 | \--- 00359 root /usr/sbin/systemstats --logger-helper /private/var/db/system
 |--= 00095 root /usr/libexec/configd
 |--= 00096 root endpointsecurityd

进程归属权

每一个进程都归属于某个特定的用户,归属于该用户的进程有权限像该用户直接登录了一样执行所有该用户可以执行的所有命令。

比方说,假如有一个进程归shinerio用户所有,那么这个进程就可以做所有shinerio用户能做的事情了:编辑shinerio用户home目录下的文件,启动一个归属于shinerio用户的新进程,等等。

系统进程比如initlogin归属于root用户,而且当一个根进程复刻一个新进程的时候,它可以改变这个子进程的归属。

所以,当我们登录后, login命令会复刻一个新的进程我运行我们的shell,但是新的进程是所属于成功登陆的那个用户的。接下来所有的后续命令都会以该用户的名义执行,所启动的进程都归属于他。

默认情况下,只有 root 进程可以像这样改变归属权。

Init System

操作系统内核在初始化进程中所做的最后一件事情就是启动「init system」,也就是执行 /sbin/init命令。「init system」有很多种,但它们都有相同的职责:

  1. 控制哪些服务在系统启动时跟随启动
  2. 提供可以开启、停止服务的工具,并且给出服务的状态信息总览
  3. 提供一个可以编写新的服务的框架

这里的服务涵盖了从web服务器到用来管理登录的系统级服务器在内的所有服务。基本上,一个「init system」的工作就是让所有面向用户(即非内核)的程序和服务运行起来。

例如,Ubuntu和centos都使用systemd作为默认的「init system」。根据 Linux 惯例,字母 d 是守护进程(daemon)的缩写。 Systemd这个名字的含义,就是它要守护整个系统

(1)-(3) 中设计的特定命令和工具会因不同的「init system」而各有不同。Linux系统历史上最通用的一个「init system」叫做「System V Init」,它是以极具影响力的UNIX SYSTEM V来命名的。在现在Linux系统中,同时被CentOSRedHadDebianUbuntu等等主流发行版本所采用的「init system」叫做「systemd init system」。

有两点需要铭记:

  1. 不同的 Linux 发行版本可以使用不同的「init system」
  2. 同一 Linux 发型版本的不同版本号可以使用不同的「init system」

PS命令

ps命令是process status的简称,用于显示当前运行的进程的信息。在不使用任何标识的情况下,ps会显示所有当前用户启动的进程,比如:

1
2
3
4
$ ps
  PID TTY          TIME CMD
 9961 pts/0    00:00:00 bash
 9981 pts/0    00:00:00 ps
  • PID: 进程的ID号
  • TTY: 终端名称缩写
  • TIME: CPU时间,即进程使用CPU的总时间
  • CMD: 所执行的命令名称。

参数

ps命令支持的参数很多,这里我们只列出常用的一些。

  • a显示当前终端所有进程
  • -A显示系统所有进程
  • e显示每个进程使用的环境变量
  • -e显示所有进程,等价于-A
  • -f显示进程之间关系
  • -H显示树桩结构
  • u显示进程的归属用户及内存的使用情况
  • x显示没有控制终端的进程

常见使用方式

ps -ef

显示所有进程的pid、启动命令和父进程pid,常配合管道符|grep来查找某个特定进程。

1
2
3
4
5
  UID   PID  PPID   C STIME   TTY           TIME CMD
    0     1     0   0 11:36AM ??         1:18.88 /sbin/launchd
    0    85     1   0 11:36AM ??         1:16.91 /usr/libexec/logd
    0    86     1   0 11:36AM ??         0:06.62 /usr/libexec/UserEventAgent (System)
    0    89     1   0 11:36AM ??         0:01.04 /System/Library/PrivateFrameworks/Uninstall.framework/Resources/uninstalld
  • UID:启动用户ID
  • PID:进程ID
  • PPID:父进程 ID
  • C:CPU用于计算执行优先级的因子。数值越大,表明进程是CPU密集型运算,执行优先级会降低;数值越小,表明进程是I/O密集型运算,执行优先级会提高。
  • STIME:进程启动的时间
  • TTY:终端名称缩写
  • TIME:CPU时间,即进程使用CPU的总时间
  • CMD:启动进程所用的命令和参数

ps –aux

适合于需要查看进程更多的详细信息,包括系统资源占用情况、进程状态等。

1
2
3
4
5
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root 1 0.0 0.8 77664 8668 ? Ss 2022 1:08 /sbin/init
root 2 0.0 0.0 0 0 ? S 2022 0:05 [kthreadd]
root 4 0.0 0.0 0 0 ? I< 2022 0:00 [kworker/0:0H]
root 6 0.0 0.0 0 0 ? I< 2022 0:00 [mm_percpu_wq]

  • USER:用户名称
  • PID:进程号
  • %CPU:进程占用CPU的百分比
  • %MEM:进程占用物理内存的百分比
  • VSZ:进程占用的虚拟内存大小(单位:KB)
  • RSS:进程占用的物理内存大小(单位:KB)
  • TTY:终端名称缩写
  • STAT:进程状态,其中 S(Sleep)可中断,s-表示该进程是会话的先导进程。
  • STARTED:进程的启动时间
  • TIME:CPU时间,即进程使用CPU的总时间
  • COMMAND:启动进程所用的命令和参数,如果过长会被截断显示

TOP命令

ps为我们提供了进程的一次性的快照,它所提供的查看结果并不动态连续。如果想对进程时间监控,应该使用TOP命令。

TOP交互式命令:

  • P 按照 CPU 占用来排序
  • M 按照内存占用来排序
  • q 退出

TOP -p pid可以查看指定进程信息。

终止进程

我们可以使用kill命令或killall命令来终止一个进程。常用的kill命令如下:

  • 1 (HUP):重新加载进程。
  • 2 (INT): 中断(同Ctrl + C
  • 3 ( QUIT): 退出(同Ctrl + \
  • 9 (KILL):无条件强制杀死一个进程。
  • 15 (TERM):正常停止一个进程。
  • 18 (CONT): 继续(与STOP相反)
  • 19 (STOP): 暂停(同Ctrl + Z

只有第9种信号(SIGKILL)才可以无条件终止进程,其他信号进程都有权利忽略

默认情况下,killkillall命令会发送TERM信号给特定的进程。TERM信号是一个「优雅」的终止信号,进程收到这个信号时会以合适的方式处理和结束进程。比如,被终止的进程可能想要在终止之前完成当前的任务、或者是清理可能会残留在系统中的临时文件等等。

如果一个进程有漏洞导致它已经不能响应TERM信号了,这种情况下我们就只能发送另一个比较激进的信号了。有两种方法可以发送这个信号:

  1. kill -KILL pid
  2. kill -9 pid

kill -9或者killall -9指令都是非常激进了,粗略地等同于直接拔掉计算机的电源。像这样来终止进程可能会留下一堆麻烦,只不过如果进程真的不响应了,也没啥别的办法。所以,在使用kill -9 PID之前,一定要先尽量尝试使用kill PID才是。

参考链接

  1. # Linux 系统里的进程状态