# 进程
# 程序
- 程序(Program)是一组可以被 CPU 执行的计算机指令。
- 比如在 Windows 系统上双击启动一个软件、在 Linux 系统上执行一条命令,都是启动一个程序。
- 程序被 CPU 执行时,主要分为以下几个阶段:
- 启动 :开始执行。
- 运行 :正在执行。
- CPU 不一定会一直执行同一个程序,可能暂停执行它,转去执行其它程序,一段时间后再回来继续执行它。
- 结束 :终止执行(不是暂停执行),并返回一个退出码。也称为程序退出、终止。
- 程序终止的几种情况:
- 执行完所有指令,正常退出。此时退出码为 0 。
- 调用 exit() 函数,提前退出。此时退出码由程序决定。
- 接收到 SIGTERM 等信号,被杀死。
- 操作系统一般要求程序结束执行时返回一个整数值,称为退出码(Exit Code)、返回码(Return Code),用于表示程序的执行结果。
- Shell 中程序退出码的取值范围为 0~255 ,通常取值为 0 时表示正常退出,取值为非 0 时表示异常退出。
# 进程
- 进程(Process)是程序运行、分配系统资源的最小单位。
- 启动一个程序时,至少要创建一个进程,来执行程序指令。
- 当进程执行程序指令时,实际上是由进程中的线程,在 CPU 中执行。
- 每个进程(称为父进程)可以创建任意个其它进程(称为子进程)。
- 父进程及其所有子进程属于同一个进程组。
- 当父进程终止时,Linux 并不会自动杀死其子进程。如果子进程依然运行,则称为孤儿进程(Orphan Process)。
- 出现孤儿进程时,Linux 会将它的 PPID 改为 1 ,成为 init 进程的子进程,被 init 进程管理。
# 进程组
:Process Group ,包含一个进程,或多个具有父子关系的进程。
- 每个进程组中有且仅有一个 Leader 进程,是其它进程的父进程。
# 进程会话
:Process Session ,包含一个进程组,或多个具有父子关系的进程组。
- 会话中的进程组又称为 job ,用于完成某种任务。
- 一个 Session 中有且仅有一个 Leader 进程,是其它进程、进程组的根父进程。
- 当 Session Leader 终止时,系统会给该 Session 的所有进程发送 SIGHUP 信号来终止它们。当 Session 中的所有进程都终止时,系统就会删除该 Session 。
- 例如:用户登录时会创建一个 login shell ,还会创建一个 Session ,以 login shell 作为 Session Leader 。
- 在该 Session 中,只有一个进程组能工作在前台,其它进程组都只能工作在后台。
- 当用户登出时,属于该 Session 的所有进程组都会被系统终止。
- 相关函数:
#include <unistd.h> pid_t setsid(void); // 创建一个新的进程会话,然后返回其 SID // 创建的新会话中,由当前进程担任 Group Leader 和 Session Leader // 如果当前进程本来就是当前进程组的 Group Leader ,则不允许创建,避免与当前进程组的其它进程处于不同的会话中
# 守护进程
- 前台进程
- :绑定到当前终端的 stdin ,因此会阻塞当前终端。
- 普通方式启动的进程默认会绑定到当前终端的 stdin、stdout、stderr 。
- 后台进程
- :没有绑定到终端的 stdin ,但可能绑定了 stdout、stderr 。
- 前台进程、后台进程都是当前终端的子进程。如果用户关闭当前终端,系统就会给这些进程发送 SIGHUP 信号,终止它们。
- 守护进程(daemon)
- :一种特殊的后台进程。运行在一个独立的 Process Session 中。因此当用户关闭当前终端之后,也会继续运行。
- 系统服务程序通常以守护进程的方式运行。
# 僵尸进程
类 Unix 系统中,进程退出的流程:
- 一个进程终止运行(包括主动退出、被杀死的情况)。
- 内核回收该进程占用的内存、文件等资源,并向其父进程发送 SIGCHLD 信号,通知它有一个子进程退出了。
- 父进程调用 wait() 或 waitpid() 获取该进程的退出状态。
- 内核在进程表中删除该进程,删除其
/proc/<PID>/
目录,使该进程完全消失。
进程在终止运行之后、完全消失之前,状态为 terminated ,称为僵尸进程(Zombie Process)。
- 如果僵尸进程越来越多,可能占用内核的所有 PID ,导致不能创建新进程。
- 僵尸进程不能被 kill 命令杀死,因为它已经终止运行了。
Windows 系统中,进程退出时会被立即回收、删除,因此不会产生僵尸进程。
init 进程会不断调用 wait() 获取其子进程的退出状态,避免产生僵尸进程。
- 因此,孤儿进程没有危害,会被 init 进程管理,不会变成僵尸进程。
避免产生僵尸进程的几种措施:
- 子进程终止时通知父进程,让父进程立即回收它。
- 比如向父进程发送 SIGCHLD 信号,不过一般的进程会忽略该信号。
- 父进程不断轮询子进程是否终止。
- 终止父进程,让子进程变为孤儿进程,从而被 init 进程管理。
- 比如先让当前进程 fork 一个子进程,再让子进程 fork 一个孙进程,然后立即回收子进程。这样孙进程就变为了孤儿进程。
- 子进程终止时通知父进程,让父进程立即回收它。
例:
- 在 Python 解释器中执行
p = subprocess.Popen('sleep 10', shell=True)
创建子进程。 - 执行 ps 命令查看:
29457 root 20 0 147.2m 8.8m 3.4m S 0.0 0.1 0:00.09 `- python3 29860 root 20 0 105.5m 0.3m 0.3m S 0.0 0.0 0:00.00 `- sleep 10
- 当子进程执行完之后,就会变成僵尸进程:
29457 root 20 0 147.2m 8.8m 3.4m S 0.0 0.1 0:00.09 `- python3 29860 root 20 0 0.0m 0.0m 0.0m Z 0.0 0.0 0:00.00 `- [sleep] <defunct>
- 在 Python 解释器中执行
p.wait()
,僵尸进程就会消失。- 另外,创建新的子进程时,Python 会自动清理所有僵尸态的子进程。
- 在 Python 解释器中执行
# 进程属性
# 标识符
Linux 系统会给每个进程、线程分配一个标识符(ID),包括:
- PID :进程的 ID ,在整个系统中唯一。
- PPID :父进程的 ID 。
- TID :线程的 ID ,在其线程组中唯一。
- PGID :进程组(Process Group)的 ID ,等于其主进程的 ID 。
- TGID :线程组(Thread Group)的 ID ,等于其主线程的 ID 。
- SID :Process Session 的 ID ,等于其主进程的 PID 。
# 进程类型
- s :该进程是 Session Leader 。
- + :该进程属于前台进程。
- < :high-priority (not nice to other users)。
- N :low-priority (nice to other users)。
- L :已锁定内存中的 page 。
- l :是多线程的。
# 运行状态
- R :Running
- 进程处于 Running 状态时才会占用 CPU 。
- S :Sleeping
- 此时进程处于可中断的睡眠状态,被 CPU 挂起,等到某一时刻或满足某些条件时再继续运
- 例如,HTTP 服务器通常一直处于 Sleeping 状态,收到 HTTP 请求时才有一瞬间切换到 Running 状态。
- D :Disk Sleep
- 此时进程处于不可中断的睡眠状态,不会响应异步信号,因此不能被 kill -9 杀死。
- 例如,进程等待磁盘 IO 时,会进入短暂的 D 状态。
- I :Idle ,空闲状态。
- Z :Zombie ,僵尸进程。
- T :Stopped ,暂停状态。
- t :Traced ,比如进程被断点调试时处于被跟踪状态。
- X :进程正在终止,这是一个很短暂的状态。
# 进程间通信
Linux 系统上,进程间通信(Inter Process Communication ,IPC)的主要方式如下:
# 信号
信号(signal)可能来源于硬件(比如键盘信号),也可能来源于软件(比如用 kill 命令)。信号产生后,会被内核发送给进程,相当于软件层的模拟中断。
Linux 定义了多种信号,常见的如下:
编号 宏定义名 默认动作 1 SIGHUP 终止进程 2 SIGINT 终止进程 3 SIGQUIT 终止进程 9 SIGKILL 终止进程 10 SIGUSR1 终止进程 12 SIGUSR2 终止进程 15 SIGTERM 终止进程 17 SIGCHLD 表示子进程终止,默认无动作 19 SIGSTOP 暂停进程 - 在不同的平台上,信号的编号可能有差异,因此最好通过宏定义名来指定信号。
- SIGINT 通常由键盘中断(Ctrl+C)引发。
- 向某个进程发出一个普通的终止信号时,进程可能立即终止,也可能做完清理工作之后再终止(比如释放占用的资源),甚至不终止。
- SIGKILL、SIGSTOP 两种信号不能被进程忽略或捕捉,因此一定会立即执行。
- SIGUSR1、SIGUSR2 两种信号常用于被用户绑定自定义的信号处理函数。
- SIGUSR1 的默认动作是终止进程,不过 Apache、Nginx 等很多程序收到 SIGUSR1 信号之后会进行复位操作,比如刷新缓存、重新加载配置文件、重新打开日志文件,接近于重启进程。
相关 API :
#include <signal.h> int kill(pid_t pid, int sig); // 向进程发送信号,发送成功则返回 0
- pid
- 如果为 n > 0 ,则选中 PID 等于 n 的进程。
- 如果为 0 ,则选中当前进程组的所有进程。
- 如果为 -1 ,则选中当前进程有权限发送信号的所有进程。
- 如果为 -n < -1 ,则选中 PGID 等于 n 的进程组中的所有进程。
- 特别地,内核只允许将已注册 handler 的信号发送给 PID 为 1 的进程(通常是 init、systemd 等),会忽略 SIGKILL、SIGSTOP 等信号,避免主机宕机。不过此时调用 kill() 依然会返回 0 。
- 因此,在主机上执行命令
kill -9 1
不会杀死 1 号进程。
- 因此,在主机上执行命令
- sig
- 可以填 int 值,也可以填 SIGTERM 等宏定义名。
- 如果为 0 ,则不发送信号,但依然会检测目标进程是否存在、是否有权限发送信号。
- pid
当进程收到一个信号时,有三种处理方式:
- 执行信号的默认动作
- 忽略信号
- 捕捉信号:进程将自己的信号处理函数传给内核,与一个信号绑定。当该信号发生时,内核就会执行该函数,从而实现该进程自定义的动作。如下:
#include <stdio.h> #include <signal.h> static void sig_handle(int sig_no) // 定义信号处理函数 { if(sig_no == SIGUSR1) printf("Received SIGUSR1\n"); else printf("Received signal %d\n", sig_no); } int main(void){ if(signal(SIGUSR1, sig_handle) == SIG_ERR) // 绑定信号处理函数 printf("Can not catch SIGUSR1\n"); // 如果不能绑定即不能捕捉,则报错 return 0; }
# 信号量
- :semophore ,一个非负整数,用于记录某个资源的可用数量。为 0 时表示资源不可用。
# 套接字
# 管道文件
- 进程可以创建一个管道文件(pipe),和另一个进程同时连接到它,从中读写数据。
- 采用半双工通信,当一个进程写数据时,另一个进程只能读数据。
- 匿名管道(PIPE):保存在内存中,没有文件描述符,只能用于父子进程之间的通信。比如管道符 | 。
- 命名管道(FIFO):保存为文件系统中的一个文件,常用于两个独立进程之间的通信。
# 消息队列
- :message queue ,一个链表结构,允许多个进程从中读写数据。
# 共享内存
- :shared memory ,一块内存空间,允许被多个进程同时访问。
# 相关 API
# PCB
Linux 内核会在内存中保存一个进程表(process table),记录当前存在的所有进程,包括其 PID、PCB 。
- 用户可以通过
/proc/<PID>/
目录获取进程的 PCB 信息。
- 用户可以通过
每个进程用一个 task_struct 结构体记录其信息,称为进程控制块(Process Control Block ,PCB)、进程描述符。如下:
struct task_struct { volatile long state; // 进程的运行状态 void *stack; // 进程的内核栈 atomic_t usage; // 进程描述符的使用计数 unsigned int flags; // 进程的状态标志 int prio; // 进程的 CPU 调度优先级 unsigned int policy; // 进程的 CPU 调度策略 cpumask_t cpus_allowed; // 进程可以在 CPU 的哪些核上执行 int exit_state; // 进程的退出码 pid_t pid; // 进程的标识符 struct task_struct *parent; // 一个指针,指向父进程 struct task_struct *group_leader; // 一个指针,指向当前进程组的主进程 struct list_head children; // 一个链表,其中每个元素表示一个子进程 cputime_t utime, stime; // 进程占用的用户态、内核态 CPU 时长 struct timespec start_time; // 进程的启动时刻 struct mm_struct *mm; // 记录进程的虚拟内存空间,比如 code、data 区域的起始地址、结束地址 struct mm_struct *active_mm; ... }
- 关于 task->mm :
- 同一个进程的多个线程,共用一个虚拟内存空间,因此 task->mm 取值相同。
- 内核线程没有虚拟内存空间,因此 task->mm 取值为 null 。而 task->active_mm 会用于记录最近运行的一个用户态进程的 task->mm ,从而能访问其中的数据。
- 关于 task->mm :
用户可以给进程设置 Nice 优先级,而内核运行进程时会转换成 Priority 优先级。
- Nice
- 取值范围为 -20~19 ,取值越小表示优先级越高。默认为 0 。
- Nice 也表示友好度。如果一个进程增加其 Nice ,就是降低优先级,对其它进程更友好。
- Priority
- 取值范围为 -100~39 ,取值越小表示优先级越高。
- 对于普通进程,其 Priority = Nice + 20 ,取值范围为 0~39 。
- 对于 RT 类型的进程,其 Priority = -1 - rt_prior ,取值范围为 -100~-1 。
- rt_prior 取值范围为 0~99 ,取值越大,优先级越高。
- Nice
进程的 CPU 调度策略分为多种:
SCHED_OTHER # 对于多个进程尽量平均分配时间,因此占用 CPU 时间较短的进程会被优先调度 SCHED_BATCH # 批处理 SCHED_IDLE # 运行低优先级的后台 job SCHED_FIFO # 按先来后到的顺序执行进程,直到进程主动释放 CPU ,或被更高优先级的进程抢占 CPU SCHED_RR # 与 SCHED_FIFO 类似,但是给每个进程分配时间片,如果耗尽,则轮到相同优先级的其它进程
- Linux v2.6 开始,默认采用 CFS(Completely Fair Scheduler)作为 CPU 调度算法。
- CPU 调度时,是以线程为单位。
- SCHED_FIFO、SCHED_RR 采用实时(Real Time ,RT)调度类,对应的进程称为 RT 类型。
# 创建进程
关于创建进程:
#include <unistd.h> pid_t fork(void); // 拷贝当前进程,创建一个子进程 // 如果创建成功,则在父进程中返回子进程的 PID ,在子进程中返回 0 // 如果创建失败,则返回 -1
- 调用 fork() 创建的子进程,与父进程几乎完全相同,比如:
- 拷贝父进程的虚拟内存空间。
- 拷贝父进程打开的文件描述符。
- 调用 fork() 创建的子进程,是一个独立的新进程,与父进程存在少量差异,比如:
- 拥有不同的 PID 。
- 将资源使用率、CPU 使用时长重置为零,重新计算。
- 将待处理的信号集清空。
- 调用 fork() 创建的子进程,与父进程几乎完全相同,比如:
关于执行程序:
#include <unistd.h> int execve(const char *pathname, char *const argv[], char *const envp[]); // 执行 pathname 对应的二进制文件,并传入参数 argv 、环境变量 envp
- 调用 execve() 时,是执行另一个程序,替换当前程序。
- 这会覆盖当前进程的数据段、堆栈。不过进程的 PID 不变,已经打开的文件描述符会保留。
- 例如:在终端执行
ls -l
命令时,是先调用fork()
创建子进程,然后子进程再执行execve("/usr/bin/ls", ["ls", "-l"], 0x7ffc0e3d0910 /* 29 vars */)
。
- 调用 execve() 时,是执行另一个程序,替换当前程序。
# 终止进程
关于终止进程:
#include <stdlib.h> void exit(int status); // 使当前进程退出,且退出码取值为 status
关于等待进程退出:
#include <sys/wait.h> pid_t wait(int *status); // 阻塞当前线程,直到任意一个子进程退出,然后返回其 PID // 如果没有子进程,或执行失败,则返回 -1 pid_t waitpid(pid_t pid, int *status, int options); // 阻塞当前线程,直到指定 PID 的子进程的运行状态改变(默认是等待变为 terminated 状态)
- pid
- 如果为 n > 0 ,则等待 PID 等于 n 的子进程。
- 如果为 0 ,则等待当前进程的任一子进程。
- 如果为 -1 ,则等待任一子进程。
- 如果为 -n < -1 ,则等待 PGID 等于 n 的进程组中的任一子进程。
- 例:
wait(NULL); // 相当于 waitpid(-1, NULL, 0); while (wait(NULL) > 0); // 阻塞当前线程,直到所有子进程都退出
- 例:运行结果:
#include <stdio.h> #include <unistd.h> #include <sys/wait.h> int main(){ pid_t pid; pid = fork(); // 此时拷贝了一个子进程,往下的代码是主进程、父进程同时各执行一份 if (pid < 0) printf("Failed to fork"); else if (pid == 0) printf("This is child process: [%d] . Its parent process is [%d] \n", getpid(), getppid()); else{ printf("This is parent process: [%d] . It just created a child process: [%d] \n", getpid(), pid); pid = wait(NULL); // 等待子进程终止 printf("The child process [%d] has terminated.\n", getpid(), getppid(), pid); } return 0; }
This is parent process: [9961] . It just created a child process: [9962] This is child process: [9962] . Its parent process is [9961] The child process [9961] has terminated.
- pid