建站提交历史文章,原文写作时间 2023 年 2 月前后。

进程基本控制

  • 进程创建、进程查看、进程重载、进程调试、进程回收

进程状态命令

查看与设置进程资源限制

1
2
3
$ ulimit -a  // 查看全部进程资源限制
// About more of shell command: "ulimit --help" or "help ulimit"
// About more of linux api: "man 2 getrlimit" or "man 2 setrlimit"

查看进程状态快照

1
$ ps aux  // 或 ps ajx
  • (默认):显示当前终端进程及其子进程的粗略信息。

  • a:显示终端上的所有进程,包括其他用户进程。

  • x:显示无终端的所有进程,不包括其他用户进程。

  • u:显示进程详细信息。多为用户关注信息,如:CPU占用、内存占用、STAT状态。

  • j:显示进程详细信息。多为控制相关信息,如:PPIDGIDSTAT状态。

STAT状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
状态:
R 运行或就绪
S 可中断的睡眠
D 不可中断的睡眠 (usually IO)
Z 僵尸进程 (nearly dead)
X 已死 (should never be seen)
I 空闲的内核线程
T 停止或被追踪
t 追踪时被调试器停止

其他:
< 高优先级 (not nice to other users)
N 低优先级 (nice to other users)
s 拥有子进程
l 拥有多线程
+ 前台进程

监视实时进程状态

1
2
$ top
$ top -d 1 // 设置刷新时间间隔(秒)

按键操作:

  • h:相关帮助。

  • P:按CPU占用排序。

  • M:按内存占用排序。

  • T:按运行时长排序。

  • U:按用户名筛选。

  • k:杀死进程,输入无效数据退出。

  • q:退出top

杀死进程

1
2
kill <pid>  // 杀死进程(默认通过 15 号信号)
kill -9 <pid> // 通过 9 号信号强制杀死进程(自杀, 杀死暂停进程), "kill -l" 查看所有信号。

查看与设置进程号

  • getpidgetppid
  • 关于进程组会话相关更多内容见2.4.1 进程、进程组、会话章节。
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <sys/types.h>
#include <unistd.h>
// get process id
// return value:
// return process id of current process, or -1 for error
pid_t getpid(void);
// get parent process id
// return value:
// return parent process id of current process, or -1 for error
pid_t getppid(void);

// About more:
// $ man 2 getpid

创建子进程

创建进程分叉

  • 进程分叉将创建当前进程(父进程)的拷贝。进程分叉初始,父进程子进程具有完全相同的状态:两者共享相同代码,运行到代码的相同位置,具有完全相同的数据集。当然,两者的进程标识pidppid是不同的。
  • 返回值:进程分叉将有两个返回值,一个来自父进程,一个来自子进程
    • 父进程中,返回子进程pid
    • 子进程中,返回0
    • 如果进程创建异常,不会产生子进程,返回-1
1
2
3
4
#include <sys/types.h>
#include <unistd.h>
// create a child fork process
pid_t fork(void);

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main() {
printf("Fork test:\n");

int pid = fork();

if (pid == -1) {
perror("fork");
return -1;
} else if (pid) {
// parent process
printf("This is parent process, pid = %d, ppid = %d\n", getpid(), getppid());
sleep(1);
} else {
// child process
printf("This is child process, pid = %d, ppid = %d\n", getpid(), getppid());
sleep(1);
}

for (int i = 1; i <= 3; i++) {
printf("%d\t%d\n", getpid(), i);
}

return 0;
}

输出:

1
2
3
4
5
6
7
8
9
Fork test:
This is parent process, pid = 10001, ppid = 1506 // 假设父进程 id 为 10001
This is child process, pid = 10086, ppid = 10001 // 假设子进程 id 为 10086
10001 1
10086 1
10001 2
10086 2
10086 3 // 无法预判父子进程谁先谁后
10001 3

注:由于fork直译为分叉,所以该进程创建又称为进程分叉。

进程分叉内存管理原理

  • fork函数将进程进行拷贝,生成一个完全相同的进程。
  • 由于fork函数生成一个完全相同的进程,因此子进程将与父进程有相同的虚拟地址映射表。换言之,子进程父进程虚拟地址映射到相同的物理地址,但是两者在数据上应该具有独立性,这导致了子进程父进程对当前数据只有读的权限,不具备写的权限。如果进程对物理地址的值进行修改,将导致两者的数据同时发生修改,这不是期望发生的。
  • 进程分叉运用了写时拷贝的技术,具备了写的权限,同时为进程分叉节约了大量的时间与空间。在进程分叉的初始状态,子进程父进程的虚拟内存指向相同的真实内存空间,当子进程父进程的任意一方需要修改时,修改部分的虚拟内存会指向一片新的真实内存,并赋予这个空间新的值。这样做的好处是,例如代码和一些无需更改的数据,将不会重复占用真实内存。
  • 以上过程均由内核自动完成。

退出进程

  • 进程退出的方法有三种:_exit(0)exit(0)(推荐)、main函数中的return 0
  • main函数中的return 0实际上是调用了exit(0)
  • exit函数是标准C库函数,是对Linux函数_exit的封装。
1
2
3
4
5
6
7
8
9
10
11
#include <unistd.h>
// exit a process
// status:
// exit status, 0~255
void _exit(int status);

#include <stdlib.h>
// exit a process
// status:
// exit status
void exit(int status);

GDB 多进程调试

  1. 关闭detach-on-fork才能在调试中切换进程。
1
2
(gdb) show detach-on-fork  // 显示 detach-on-fork 选择
(gdb) set detach-on-fork off // 关闭 detach-on-fork 选项, on / off
  1. 设置follow-fork-mode跟踪起始进程。
1
2
(gdb) show follow-fork-mode  // 显示 follow-fork-mode 选择
(gdb) set follow-fork-mode parent // 设置 follow-fork-mode 选项, parent / child
  1. 切换跟踪进程。
1
2
3
4
5
// 请先确保 detach-on-fork 选项关闭
// 切换至的进程将处于暂停状态, 暂停在进程创建位置, 或上次进程切出时位置
// 暂停时, 进程 STAT 为 t, 可使用 "ps" 查看
(gdb) info inferiors // 显示所有进程及其 ID
(gdb) inferior <ID> // 切换跟踪进程

exec 函数簇重载进程

  • execlexeclpexecleexecvexecvpexecvpe
  • exec函数簇将重载进程,直接覆盖当前进程代码,因此exec函数簇通常需要在fork进程中执行。
  • l:参数使用列表传递,传递一系列参数,以NULL参数(哨兵)结尾。
  • p:在环境变量和当前路径中查找file,其他函数仅在当前路径查找。
  • e:传递envp数组指定路径搜索目录,此时哨兵显得尤为重要。
  • v:参数使用argv数组传递,而不是参数列表。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <unistd.h>
int execl(const char *pathname, const char *arg, .../* (char *) NULL */);
int execlp(const char *file, const char *arg, .../* (char *) NULL */);
int execle(const char *pathname, const char *arg, .../*, (char *) NULL, char *const envp[] */);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
// execute a file
// pathname / file:
// the file path to execute
// return value:
// no return on success, or return -1 for error

// About more
// man 3 exec
  • 注:根据习惯,第一个参数为文件名,即本身。如ps aux参数为psaux
  • 注:如果执行成功没有返回值,因为当前进程代码已覆盖,无法继续执行。
  • 注:system 函数实际是对 fork 函数与 exec 函数簇的封装,通常使用 system 函数可以更方便的满足需求。

回收子进程

孤儿进程与僵尸进程

  • 孤儿进程父进程结束、子进程尚未结束,此时子进程孤儿进程init进程PID1)代替父进程托管子进程孤儿进程虽然脱离父进程控制,但是会自然结束,通常不具有危害。
  • 僵尸进程子进程结束、父进程尚未结束,如果父进程未处理子进程的结束信息,未回收子进程的内核区数据,此时子进程僵尸进程。在进程概述中讲到,子进程结束不会自动释放内核区数据,等待父进程接收并处理其结束信息。如果处于将死状态的子进程一直存在,将不断占用内核区内存和PID,大量积累具有危害。
  • 父进程不处理僵尸进程,在其结束后,init进程会为其处理僵尸进程。但是如果父进程是长期运行的进程(如服务器进程),危害将持续积累。
  • wait函数与waitpid函数用于处理和回收子进程内核区数据。

wait 函数回收子进程

  • wait函数与waitpid函数用于接收进程状态变化,默认阻塞进程等待任意子进程状态变化。

  • 返回值:当任意进程状态变化,函数返回子进程PID,设置wstatus值表示该子进程状态变化编码(后述情况不设置wstatus);当WNOHANG选项打开,并且存在未结束子进程,返回0(当WNOHANG选项关闭,并且存在未结束子进程,阻塞);当所有子进程已回收,抛出No child process错误,返回-1;当出现其他错误,返回-1

  • pid:分为下述情况:

    • < -1:等待任意组ID-pidpid的绝对值)的子进程状态变化。
    • -1:等待任意子进程状态变化。
    • 0:等待任意组ID与函数启动时父进程组ID相同的子进程状态变化。
    • > 0:等待进程IDpid子进程状态变化。
    • 如果对应进程符号ID要求但不是子进程子孙进程不属于子进程)的,不属于控制范围。
  • wstatus:整型指针,用于接收状态变化编码,可为NULL表示不接收信息。解码宏如下:

    • WIFEXITED(wstatus):返回01整型。是否正常退出(exit为正常退出)。
    • WEXITSTATUS(wstatus):返回整型。如果上宏为真,返回退出状态(exit参数,0~255)。
    • WIFSIGNALED(wstatus):返回01整型。是否异常退出(信号退出为异常退出)。
    • WTERMSIG(wstatus):返回整型。如果上宏为真,返回引发退出的信号。
    • WIFSTOPPED(wstatus):返回01整型。是否进入暂停。
    • WSTOPSIG(wstatus):返回整型。如果上宏为真,返回引发暂停的信号。
    • WIFCONTINUED(wstatus):返回01整型。是否进程继续。
  • options:宏如下:(使用|连接)

    • (默认):等待进程正常退出、进程异常退出(以及被追踪进程暂停)。
    • WNOHANG:不阻塞,立即返回。
    • WUNTRACED:也会等待进程(未被追踪进程)被暂停。
    • WCONTINUED:也会等待进程被继续。
  • wait(wstatus)等价于waitpid(-1, wstatus, 0)

  • 关于更多:$ man 2 wait

1
2
3
4
5
6
7
8
9
#include <sys/types.h>
#include <sys/wait.h>
// wait for the change state
pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);

#include <stdio.h>
// print error message if return value is -1
perror("wait");

信号回收子进程

  • 我们注意到wait函数回收子进程时,需要主动检测是否有子进程状态变化;如果我们希望即使处理子进程,必须阻塞的回收子进程,或者使用独立线程处理子进程。在2.3.4 信号章节中将提到,子进程状态变化时将触发SIGCHLD信号,届时可以考虑使用信号即时回收子进程。
1
2
3
4
5
6
7
8
9
10
11
12
13
// 请先了解 2.3.4 信号 章节后阅读此代码
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
void process_signal(int sig) {
if (sig == SIGCHLD) {
int ret = 1, wstatus;
while (ret > 0) {
waitpid(-1, &wstatus, WNOHANG);
// 业务逻辑
}
}
}