0、程序和进程
程序 是包含一系列信息的文件,这些信息描述了如何在运行时创建一个进程。
进程 是正在运行的程序的实例。是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。
1、区别
程序是静态的,进程是动态的,程序是存储在某种介质上的二进制代码,进程对应了程序的执行过程,系统不需要为一个不执行的程序创建进程,一旦进程被创建,就处于不断变化的动态过程中,对应了一个不断变化的上下文环境。
2、联系
进程是程序的一次执行,而进程总是对应至少一个特定的程序。一个程序可以对应多个进程,同一个程序可以在不同的数据集合上运行,因而构成若干个不同的进程。几个进程能并发地执行相同的程序代码,而同一个进程能顺序地执行几个程序。
关于进程和程序的区别,《现代操作系统》中用了一个比喻形象说明:一位有一手好厨艺的计算机科学家正在为他的女儿烘制生日蛋糕。他有做生日蛋糕的食谱,厨房里有所需要的原料,在这个比喻中,做蛋糕的食谱就是程序(即用适当形式描述的算法),计算机科学家就是处理机(CPU),而做蛋糕的各种原料就是输入数据。进程就是厨师阅读食谱,取来各种原料以及烘制蛋糕等一系列动作的总和。
1、并行和并发
2、进程控制块(PCB)—— 进程信息管理
为了管理进程,内核必须对每个进程所做的事情进行清楚的描述。内核为每个进程分配一个
PCB(Processing Control Block)进程控制块,维护进程相关的信息,Linux 内核的进程控制块是
task_struct 结构体。
在 /usr/src/linux-headers-xxx/include/linux/sched.h 文件中可以查看 struct task_struct 结构体定义。
其内部成员有很多,我们只需要掌握以下部分即可:
- 进程id:系统中每个进程有唯一的 id,用 pid_t 类型表示,其实就是一个非负整数
- 进程的状态:有就绪、运行、挂起、停止等状态
- 进程切换时需要保存和恢复的一些CPU寄存器
- 描述虚拟地址空间的信息
- 描述控制终端的信息
- 当前工作目录(Current Working Directory)
- umask 掩码
- 文件描述符表,包含很多指向 file 结构体的指针
- 和信号相关的信息
- 用户 id 和组 id
- 会话(Session)和进程组
- 进程可以使用的资源上限(Resource Limit)
3、进程的状态
进程状态转换
进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。在三态模型中,进程状态分为三个基本状态,即就绪态,运行态,阻塞态。在五态模型中,进程分为新建态、就绪态,运行态,阻塞态,终止态。
- 运行态:进程占有处理器正在运行;
- 就绪态:进程具备运行条件,等待系统分配处理器以便运行。当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行。在一个系统中处于就绪状态的进程可能有多个,通常 将它们排成一个队列,称为就绪队列;
- 阻塞态:又称为等待(wait)态或睡眠(sleep)态,指进程不具备运行条件,正在等待某个事件的完成;
进程处于阻塞态,CPU的资源会释放掉,调度程序调度给别的程序执行。
程序运行时与用户交互(控制台等待输入),调用的就是阻塞函数。
阻塞态无法直接变为运行态,首先转换为就绪态,然后被调用运行(抢CPU资源)才能转为运行态,因为阻塞时会把CPU资源释放掉。
- 新建态:进程刚被创建时的状态,尚未进入就绪队列;
- 终止态:进程完成任务到达正常结束点,或出现无法克服的错误而异常终止,或被操作系统及有终
止权的进程所终止时所处的状态。进入终止态的进程以后不再执行,但依然保留在操作系统中等待
善后。一旦其他进程完成了对终止态进程的信息抽取之后,操作系统将删除该进程。
就绪态可以直接到终止态。
程序终止后,虚拟地址空间中用户区的数据被释放,内核区的数据还保留在系统中,等待善后。
进程相关的命令
ps aux / ajx
a:显示终端上的所有进程,包括其他用户的进程
u:显示进程的详细信息
x:显示没有控制终端的进程
j:列出与作业控制相关的信息
1、ps aux
ps aux 显示当前进程的一个快照(非动态)
- TTY是进程所属的终端,比如ps aux这个进程属于 pts/3 终端。
- STAT是状态,比如ps aux就是R状态,即正在运行。
- COMMAND是命令,比如ps aux。
2、ps ajx
ps ajx 显示当前进程的一个快照(非动态)
PID是进程ID(学生)
PPID是父进程ID
PGID是进程组ID,一个组里有多个进程(教室)
SID是会话ID,一个会话有多个组(学校)
3、top
top 动态的显示进程信息
可以在使用 top 命令时加上 -d 来指定显示信息更新的时间间隔,在 top 命令执行后,可以按以下按键对显示的结果进行排序:
- M 根据内存使用量排序
- P 根据 CPU 占有率排序
- T 根据进程运行时间长短排序
- U 根据用户名来筛选进程
- K 输入指定的 PID 杀死进程
4、kill
杀死进程(kill名并不是去杀死一个进程,而是给进程发送某个信号)
kill [-signal] pid
kill –l 列出所有信号
kill –SIGKILL 进程ID
kill -9 进程ID
killall name 根据进程名杀死进程
进程号相关函数
1、每个进程都由进程号来标识,其类型为 pid_t(整型)
,进程号的范围:0~32767。进程号总是唯一的,但可以重用。当一个进程终止后,其进程号就可以再次使用。
2、任何进程(除 init 进程
)都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号称为父进程号(PPID)。
- 任何进程都有父进程,比如 a.out 的父进程就是 终端。
3、进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID)。默认情况下,当前的进程号会当做当前的进程组号。
进程号和进程组的获取函数:
pid_t getpid(void); //当前进程ID
pid_t getppid(void); //当前进程父进程ID
pid_t getpgid(pid_t pid); //当前进程进程组的ID
4、进程创建 —— fork函数
系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成 进程树结构模型。
fork() 通过复制调用进程来创建一个新进程。 新进程被称为子进程。 调用进程称为父进程。
fork函数,意译为分叉。
man 2 fork
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
函数的作用:用于创建子进程。返回值:fork()的返回值会返回两次。一次是在父进程中,一次是在子进程中。如何区分父进程和子进程:通过fork的返回值。
返回值:
- 成功:返回两次,在子进程中返回 0,在父进程中返回子进程 ID
- 失败:在父进程中返回-1,表示创建子进程失败,并且设置errno
失败的两个主要原因:
- 当前系统的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN
- 系统内存不足,这时 errno 的值被设置为 ENOMEM
测试一下:
// 创建子进程pid_t pid = fork(); //此时会分叉// 判断是父进程还是子进程if (pid > 0){printf("pid : %d\n", pid);// 如果大于0,返回的是创建的子进程的进程号,当前是父进程printf("im parent process, pid: %d, ppid: %d\n", getpid(), getppid());} else if (pid == 0){// 如果等于0,当前是子进程printf("i am child process, pid : %d, ppid : %d\n", getpid(),getppid());}for (int i = 0; i < 5;i++){printf("i : %d , pid : %d\n", i , getpid());sleep(1);}
输出:
petri@XX:~/lesson02/01$ ./fork
//因为fork分叉,执行了两组代码://父进程部分
pid : 575
im parent process, pid: 574, ppid: 27267
i : 0 , pid : 574//子进程部分
i am child process, pid : 575, ppid : 574
i : 0 , pid : 575
i : 1 , pid : 574
i : 1 , pid : 575
i : 2 , pid : 575
i : 2 , pid : 574
i : 3 , pid : 574
i : 3 , pid : 575
i : 4 , pid : 575
i : 4 , pid : 574
从子进程的输出情况看出,父进程和子进程是交替执行的。
ps aux 查找父进程的 ppid: 27267,发现是一个 bash 进程(如下图):
父子进程用户区数据读写
父子进程虚拟地址空间:
刚才在fork函数中也看到,创建子进程会复制一个新的地址空间:
Description:
The child process and the parent process run in separate memory spaces. At the time of fork() both memory spaces have the same content. Memory writes, file mappings (mmap(2)), and unmappings (munmap(2)) performed by one of the processes do not affect the other.
子进程和父进程在不同的内存空间中运行。 在执行 fork() 时,两个内存空间的内容相同。 其中一个进程执行的内存写入、文件映射(mmap(2))和解除映射(munmap(2))操作不会影响另一个进程。
在上面代码中初始化一个num=10,分别在子进程和父进程中操作num,两者的操作互不影响,效果如下:
注意:
实际上,更准确来说,Linux 的 fork() 使用是通过写时拷贝 (copy- on-write) 实现。
写时拷贝是一种可以推迟甚至避免拷贝数据的技术。
面试题: fork()函数会复制整个进程的地址空间吗?
答:内核并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间,只有在需要写入的时候才会复制地址空间。
fork函数返回值是两个位于父子进程的栈空间的局部变量。
总结:读时共享,写时拷贝!
GDB多进程调试
(gdb) show follow-fork-mode
Debugger response to a program call of fork or vfork is "parent".(gdb) show detach-on-fork
Whether gdb will detach the child of a fork is on.
如果设置 detach-on-fork 为 off,则子进程会挂起。
查看调试的进程:
‘ * ’ 表示当前调试的进程。
切换当前的进程:
使进程脱离GDB调试:
会把detach inferiors的进程继续执行完。
5、exec函数族
exec 函数族: 一系列功能相同或相似的函数,类似C++函数重载。
exec 函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就
是在调用进程内部执行一个可执行文件。
exec 函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被
新的内容取代,只留下进程 ID 等一些表面上的信息仍保持原样,颇有些神似“三十六计”中的“金蝉脱
壳”。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回 -1,从原程序的调
用点接着往下执行。
比如,内核区调用了exec函数,当前用户区的内容会被a.out的用户去内容替换掉:
常用exec函数:
int execl(const char *path, const char *arg, .../* (char *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
//linux系统函数man 2 execve,其余的函数都是在这个函数上的封装 man 2 execl
int execve(const char *filename, char *const argv[], char *const envp[]);
execl
int execl(const char *path, const char *arg, …);
#include <unistd.h>int execl(const char *path, const char *arg, ...);- 参数:- path:需要 指定的执行的文件的路径 或者 名称a.out /home/nowcoder/a.out 推荐使用绝对路径./a.out hello world- arg:是执行可执行文件所需要的参数列表第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称从第二个参数开始往后,就是程序执行所需要的的 参数列表。参数最后需要以NULL结束(哨兵)- 返回值:只有当调用失败,才会有返回值,返回-1,并且设置errno如果调用成功,没有返回值 (内容替换成功)。
#include <unistd.h>
#include <stdio.h>int main() {// 创建一个子进程,在子进程中执行exec函数族中的函数pid_t pid = fork();if(pid > 0) {// 父进程printf("i am parent process, pid : %d\n",getpid());sleep(1);}else if(pid == 0) {// 子进程execl("hello","hello",NULL);// execl("/bin/ps", "ps", "aux", NULL);//执行shell命令,ps aux 查看当前进程信息//execl("ps", "ps", "aux", NULL);//execl: No such file or directory//execl("hello", "ps", "aux", NULL); //hello, worldperror("execl");printf("i am child process, pid : %d\n", getpid());}for(int i = 0; i < 3; i++) {printf("i = %d, pid = %d\n", i, getpid());}return 0;
}
输出:
i am parent process, pid : 8873
hello, world
i = 0, pid = 8873
i = 1, pid = 8873
i = 2, pid = 8873
可以看到执行子进程时,execl用hello把它替换了,子进程后续内容不再执行。
execlp
int execlp(const char *file, const char *arg, … );
#include <unistd.h>int execlp(const char *file, const char *arg, ... );- 会到 环境变量 中查找指定的可执行文件,如果找到了就执行,找不到就执行不成功。- 参数:- file:需要执行的可执行文件的文件名a.outps- arg:是执行可执行文件所需要的参数列表第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称从第二个参数开始往后,就是程序执行所需要的的参数列表。参数最后需要以NULL结束(哨兵)- 返回值:只有当调用失败,才会有返回值,返回-1,并且设置errno如果调用成功,没有返回值。int execv(const char *path, char *const argv[]);argv是需要的参数的一个字符串数组char * argv[] = {"ps", "aux", NULL};execv("/bin/ps", argv);
#include <unistd.h>
#include <stdio.h>int main() {// 创建一个子进程,在子进程中执行exec函数族中的函数pid_t pid = fork();if(pid > 0) {// 父进程printf("i am parent process, pid : %d\n",getpid());sleep(1);}else if(pid == 0) {// 子进程execlp("ps", "ps", "aux", NULL);printf("i am child process, pid : %d\n", getpid());}for(int i = 0; i < 3; i++) {printf("i = %d, pid = %d\n", i, getpid());}return 0;
}
输出:
i am parent process, pid : 10233
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 9868 656 ? Ssl 08:57 0:00 /init
root 108 0.0 0.0 9968 324 tty2 Ss 08:57 0:00 /init
petri 109 0.0 0.0 10656 680 tty2 S 08:57 0:00 sh -c "$VSCODE_WSL_EXT_LOCATION/scripts/wslServer.sh" e170252f762678dec6ca2cc69aba1570769a5d39 stable code-server .vscode-server --host=127.0.0.1 --port=0 --connection-token=3276537263
petri 110 0.0 0.0 10656 728 tty2 S 08:57 0:00 sh /mnt/c/Users/17335/.vscode/extensions/ms-vscode-remote.remote-wsl-0.88.0/scripts/wslServer.sh e170252f762678dec6ca2cc69aba1570769a5d39 stable code-server .vscode-server --host=127.0
petri 115 0.0 0.0 10656 720 tty2 S 08:57 0:00 sh /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/bin/code-server --host=127.0.0.1 --port=0 --connection-token=3276537263-925675798-3382388098-1089837822 --use
petri 119 0.1 0.3 995220 111568 tty2 Sl 08:57 0:07 /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/node /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/out/server-main.js --host=127.0.0.1
root 202 0.0 0.0 9880 324 tty3 Ss 08:57 0:00 /init
petri 203 0.0 0.1 614816 39208 tty3 Sl 08:57 0:00 /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/node -e const net = require('net'); process.stdin.pause(); const client = net.createConnection({ host: '127.0.0.
root 210 0.0 0.0 9880 324 tty4 Ss 08:57 0:00 /init
petri 211 0.0 0.1 609696 34860 tty4 Sl 08:57 0:00 /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/node -e const net = require('net'); process.stdin.pause(); const client = net.createConnection({ host: '127.0.0.
petri 218 0.5 0.4 1035192 146836 tty2 Sl 08:57 0:29 /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/node --dns-result-order=ipv4first /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/out/bo
petri 229 0.0 0.1 857652 37300 tty2 Sl 08:57 0:01 /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/node /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/out/bootstrap-fork --type=fileWatch
petri 244 0.0 0.1 658344 44016 tty2 Rl 08:57 0:00 /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/node /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/out/bootstrap-fork --type=ptyHost -
petri 260 0.0 0.1 133412 55900 tty2 Sl 08:57 0:02 /home/petri/.vscode-server/extensions/ms-vscode.cpptools-1.19.9-linux-x64/bin/cpptools
petri 285 0.0 0.0 17140 2724 pts/0 Ss 08:57 0:00 /bin/bash --init-file /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/out/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh
petri 10155 0.3 0.0 4280760 8244 tty2 Sl 10:31 0:00 /home/petri/.vscode-server/extensions/ms-vscode.cpptools-1.19.9-linux-x64/bin/cpptools-srv 260 {AB84DDF0-E8B0-49F3-B906-82F973B6A78D}
petri 10224 0.0 0.0 10656 680 tty2 S 10:31 0:00 /bin/sh -c "/home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/out/vs/base/node/cpuUsage.sh" 285
petri 10225 0.0 0.0 16664 1748 tty2 S 10:31 0:00 /bin/bash /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/out/vs/base/node/cpuUsage.sh 285
petri 10228 0.0 0.0 15276 816 tty2 S 10:31 0:00 sleep 1
petri 10233 0.0 0.0 10536 576 pts/0 S 10:31 0:00 ./execlp
petri 10234 0.0 0.0 18660 1896 pts/0 R 10:31 0:00 ps aux
i = 0, pid = 10233
i = 1, pid = 10233
i = 2, pid = 10233
总结一下exec后缀的作用:
int execl(const char *path, const char *arg, .../* (char *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
//linux系统函数man 2 execve,其余的函数都是在这个函数上的封装 man 2 execl
int execve(const char *filename, char *const argv[], char *const envp[]);
6、进程控制
进程退出
#include <stdlib.h>
void exit(int status);#include <unistd.h>
void _exit(int status);status参数:是进程退出时的一个状态信息。父进程回收子进程资源的时候可以获取到。
exit和_exit的区别:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main() {printf("hello\n"); // 带\n printf可以自动刷新I/O缓冲区,把hello打印出来,它是标准C库的函数printf("world");// exit(0);_exit(0);return 0;
}
_exit(0)只输出 hello,因为\n
会把hello放到缓冲区里并刷新到控制台上,但是 world 因为_exit(0)不自带刷新缓冲区的功能,所以就不显示了。如果两个printf都不带\n
的话,就什么也不会显示了。
所以平时用 exit 比较多一些。
孤儿进程
父进程运行结束,但子进程还在运行(未运行结束),这样的子进程就称为孤儿进程(Orphan Process)。
每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init (pid=1)
,而 init 进程会循环地 wait()
它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init 进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。
孤儿进程并没有什么危害。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>int main() {// 创建子进程pid_t pid = fork();// 判断是父进程还是子进程if(pid > 0) {printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());} else if(pid == 0) {sleep(20);// 当前是子进程printf("i am child process, pid : %d, ppid : %d\n", getpid(),getppid());}// for循环// for(int i = 0; i < 3; i++) {// printf("i : %d , pid : %d\n", i , getpid());// }return 0;
}
输出:
i am parent process, pid : 15056, ppid : 285
petri@XX:~/lesson02/03$ i am child process, pid : 15057, ppid : 1
注意看,子进程打印到下一个终端上了,而且 ppid (父亲进程)是 1
,即 init 的 pid。
僵尸进程
每个进程结束之后, 都会释放自己地址空间中的用户区数据,内核区的 PCB(进程控制块) 没有办法自己释放掉,需要父进程去释放。
进程终止时,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
操作系统的进程号是有限的。
僵尸进程不能被 kill -9
杀死,这样就会导致一个问题,如果父进程不调用 wait()
或 waitpid()
的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>int main() {// 创建子进程pid_t pid = fork();// 判断是父进程还是子进程if(pid > 0) {while(1) {printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());sleep(1);}} else if(pid == 0) {// 当前是子进程printf("i am child process, pid : %d, ppid : %d\n", getpid(), getppid());}// for循环for(int i = 0; i < 3; i++) {printf("i : %d , pid : %d\n", i , getpid());}return 0;
}
输出:
i am parent process, pid : 21018, ppid : 285
i am child process, pid : 21019, ppid : 21018
i : 0 , pid : 21019
i : 1 , pid : 21019
i : 2 , pid : 21019
i am parent process, pid : 21018, ppid : 285
i am parent process, pid : 21018, ppid : 285
i am parent process, pid : 21018, ppid : 285
i am parent process, pid : 21018, ppid : 285
i am parent process, pid : 21018, ppid : 285
因为父进程一直在循环,并不会释放子进程的PCB,导致子进程变为僵尸进程。
ps auxpetri 21018 0.0 0.0 10536 576 pts/0 S 12:17 0:00 ./zombie
petri 21019 0.0 0.0 0 0 pts/0 Z 12:17 0:00 [zombie] <defunct>
petri 21044 0.0 0.0 17140 2640 pts/1 Ss 12:17 0:00 /bin/bash --init-file /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/out/vs/workbench/contrib/terminal/browser/media/shellIntegration-bas
petri 21199 0.0 0.0 18660 1900 pts/1 R 12:17 0:00 ps aux
pid = 21019 的为僵尸进程,此时 kill -9 21019
并不会杀死该僵尸进程,除非kill -9 父进程 21018,子进程变为孤儿进程,然后 被 pid = 1 的 init 进程 做善后处理。
因此,需要 wait() waitpid() 这两个函数。
进程回收
在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。
但是仍然为其保留一定的信息,这些信息主要主要指进程控制块PCB的信息(包括进程号、退出状态、运行时间等)
父进程可以通过调用 wait
或 waitpid
得到它的退出状态同时彻底清除掉这个进程。
wait() 和 waitpid() 函数的功能一样,区别在于,wait() 函数会阻塞,waitpid() 可以设置不阻塞,
waitpid() 还可以指定等待哪个子进程结束。
**注意:**一次 wait 或 waitpid 调用只能清理一个子进程,清理多个子进程应使用循环
。
wait()
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);功能:等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收子进程的资源。参数:int *wstatus进程退出时的状态信息,传入的是一个int类型的地址,传出参数。这个int类型的地址可以输入宏函数查看进程状态。返回值:- 成功:返回被回收的子进程的id- 失败:-1 (所有的子进程都结束,调用函数失败)调用wait函数的进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒(相当于继续往下执行)
如果没有子进程了,函数立刻返回,返回-1;如果子进程都已经结束了,也会立即返回,返回-1.
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>int main() {// 有一个父进程,创建5个子进程(兄弟)pid_t pid;// 创建5个子进程for(int i = 0; i < 5; i++) {pid = fork();if(pid == 0) {//不可能只产生5个子进程。为了让子进程不再产生进程,加入一个pid == 0来判断,让子进程终止for循环break;}}if(pid > 0) {// 父进程while(1) {printf("parent, pid = %d\n", getpid());int ret = wait(NULL);// 没有子进程后 wait 函数返回 -1,结束循环int ret = wait(&st);if(ret == -1) {break;}printf("child die, pid = %d\n", ret);sleep(1);}} else if (pid == 0){// 子进程while(1) {printf("child, pid = %d\n", getpid()); sleep(1); }exit(0);}return 0; // exit(0)
}
父进程只打印了一次,因为wait函数使其阻塞,此时 kill -9 27501
这个子进程:
父进程被唤醒(相当于继续往下执行),继续执行循环,然后又碰到wait函数,进入阻塞状态。
修改下父进程代码:
if(pid > 0) {// 父进程while(1) {printf("parent, pid = %d\n", getpid());// int ret = wait(NULL);int st;int ret = wait(&st);if(ret == -1) {break;}if(WIFEXITED(st)) {// 是不是正常退出printf("退出的状态码:%d\n", WEXITSTATUS(st));}if(WIFSIGNALED(st)) {// 是不是异常终止printf("被哪个信号干掉了:%d\n", WTERMSIG(st));}printf("child die, pid = %d\n", ret);sleep(1);}
新建一个bash,输入:
kill -9 5647
child, pid = 5648
child, pid = 5647
被哪个信号干掉了:9
child die, pid = 5647
child, pid = 5649
child, pid = 5646
waitpid()
#include <sys/types.h>
#include <sys/wait.h>pid_t waitpid(pid_t pid, int *wstatus, int options);功能:回收指定进程号的子进程,可以设置是否阻塞。参数:- pid:pid > 0 : 某个子进程的pidpid = 0 : 回收当前 进程组 的所有子进程 (ps ajx 可以查看各个进程所属的组)pid = -1 : 回收所有的子进程,相当于 wait() (最常用)pid < -1 : 某个进程组的组id的绝对值(负值?),回收指定进程组中的子进程 (比如 -765)- options:设置阻塞或者非阻塞0 : 阻塞WNOHANG : 非阻塞 (宏值,如果没有子进程结束(还有子进程),立刻返回)- 返回值:> 0 : 返回子进程的id= 0 : options=WNOHANG, 表示还有子进程= -1 :错误,或者没有子进程了
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>int main() {// 有一个父进程,创建5个子进程(兄弟)pid_t pid;// 创建5个子进程for(int i = 0; i < 5; i++) {pid = fork();if(pid == 0) {break;}}if(pid > 0) {// 父进程while(1) {printf("parent, pid = %d\n", getpid());sleep(1);int st;// int ret = waitpid(-1, &st, 0);int ret = waitpid(-1, &st, WNOHANG);if(ret == -1) {break;} else if(ret == 0) {// 说明还有子进程存在continue;} else if(ret > 0) {if(WIFEXITED(st)) {// 是不是正常退出printf("退出的状态码:%d\n", WEXITSTATUS(st));}if(WIFSIGNALED(st)) {// 是不是异常终止printf("被哪个信号干掉了:%d\n", WTERMSIG(st));}printf("child die, pid = %d\n", ret);}}} else if (pid == 0){// 子进程while(1) {printf("child, pid = %d\n",getpid());sleep(1);}exit(0);}return 0;
}
child, pid = 16913
child, pid = 16914
child, pid = 16915
child, pid = 16916
parent, pid = 16912
child, pid = 16917
child, pid = 16913
child, pid = 16914
child, pid = 16915
child, pid = 16916
parent, pid = 16912
child, pid = 16917
child, pid = 16913
设置的非阻塞 WNOHANG,此时如果不进行任何操作,ret == 0,所以parent会一直循环输出。