【linux】进程等待与进程替换
🔥个人主页:Quitecoder
🔥专栏:linux笔记仓
目录
- 01.进程等待
- 系统调用
- 获取子进程status
- 常用宏
- 使用示例
- 02.进程替换
- 替换函数
- 关键点解释:
- 代码详细分析
- `execvpe` 函数的使用
01.进程等待
任何子进程,在退出的情况下,一般必须要被父进程进行等待。进程在退出的时候,如果父进程不管不顾,退出进程,状态Z(僵尸状态),内存泄漏
- 进程一旦变成僵尸状态,kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程
- 父进程通过等待,解决子进程退出的僵尸问题,回收系统资源(一定要考虑的)
- 获取子进程的退出信息,知道子进程是因为什么原因退出的(可选的功能)
系统调用
-
wait()
wait()
函数使调用的进程(通常是父进程)暂停执行,直到一个子进程终止或发生一个信号。这个调用通常用于简单的父子进程同步。- 函数原型:
pid_t wait(int *status);
- 如果有子进程退出,
wait()
返回子进程的 PID,并可通过status
指针获取子进程的退出状态。
-
waitpid()
waitpid()
函数提供更多的控制,允许父进程等待特定的子进程,或者是与父进程有特定关系的任何子进程。- 函数原型:
pid_t waitpid(pid_t pid, int *status, int options);
- 参数:
pid
:指定要等待的子进程的 PID;若为-1
,则等待任何子进程,与wait等效。status
:和wait()
一样,用于存放子进程的终止状态。options
:可以控制waitpid()
的行为,如WNOHANG
(非阻塞),不会等待子进程终止,立即返回。
- 返回值:
- 当正常返回的时候waitpid返回收集到的子进程的进程ID;
- 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
- 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
- 如果不存在该子进程,则立即出错返回
所以说父进程通过等待,解决子进程退出的僵尸问题,回收系统资源
如果子进程没有退出,父进程其实一直在进行阻塞等待!
获取子进程status
在 waitpid
函数中,status
是一个指向整数的指针,用于存储子进程的终止状态信息。这个状态不仅仅是一个简单的退出代码,而是一组位的组合,这些位可以表示子进程的多种状态。
下面是如何解释 status
值的相关宏和方法:
常用宏
-
WIFEXITED(status)
:- 判断子进程是否正常退出(调用
exit
或者返回 main 函数)。 - 返回非零值表示子进程正常退出,可以通过
WEXITSTATUS(status)
获取退出状态。
- 判断子进程是否正常退出(调用
-
WEXITSTATUS(status)
:- 在
WIFEXITED(status)
为真时使用。 - 获得子进程的退出码(也就是子进程传递给
exit()
的参数或main()
函数的返回值),这是一个8位的整数。
- 在
-
WIFSIGNALED(status)
:- 判断子进程是否因为未捕获信号而终止。
- 返回非零值表示子进程被信号终止,可以通过
WTERMSIG(status)
获取导致终止的信号编号。
-
WTERMSIG(status)
:- 在
WIFSIGNALED(status)
为真时使用。 - 获得导致子进程终止的信号编号。
- 在
-
WIFSTOPPED(status)
:- 判断子进程是否因信号停止。
- 返回非零值表示子进程被信号停止,可以通过
WSTOPSIG(status)
获取导致停止的信号编号。
-
WSTOPSIG(status)
:- 在
WIFSTOPPED(status)
为真时使用。 - 获得导致子进程停止的信号编号。
- 在
-
WIFCONTINUED(status)
:- 判断子进程是否由
SIGCONT
信号继续。 - 返回非零值表示子进程接收到
SIGCONT
信号后继续执行,这个宏主要在系统支持WCONTINUED
选项时使用。
- 判断子进程是否由
使用示例
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>int main() {pid_t pid = fork();if (pid == -1) {perror("fork failed");exit(EXIT_FAILURE);} else if (pid == 0) {// 子进程printf("Child process (PID: %d) executing...\n", getpid());exit(42); // 子进程结束并返回状态码42} else {// 父进程int status;pid_t waited = waitpid(pid, &status, 0);if (waited == -1) {perror("waitpid failed");} else {if (WIFEXITED(status)) {printf("Child process (PID: %d) exited with status %d\n", waited, WEXITSTATUS(status));} else if (WIFSIGNALED(status)) {printf("Child process (PID: %d) terminated by signal %d\n", waited, WTERMSIG(status));} else if (WIFSTOPPED(status)) {printf("Child process (PID: %d) stopped by signal %d\n", waited, WSTOPSIG(status));} else if (WIFCONTINUED(status)) {printf("Child process (PID: %d) continued\n", waited);}}}return 0;
}
在上面的代码中,status
变量通过 waitpid
获得子进程的状态。根据不同的状态宏,可以判断子进程是如何退出的,并做相应的处理。这种机制使得父进程能够详细了解子进程的退出原因,而不仅仅是它的退出码。
status不能简单的当作整形来看待,可以当作位图来看待
虽然在不同的 Unix 系统中这个结构可能略有差异,但通常 status
会被设计成如下所示的位字段结构:
- 位 0-7: 子进程的退出代码(如果子进程是正常退出的)。
- 位 8-15: 在一些实现中,这些位可以包含信号编号,表示子进程因信号而终止。
- 特定的位字段:表明子进程是否被信号中止、是否正常退出、是否由信号停止(这些信息是由
WIFEXITED
、WIFSIGNALED
和WIFSTOPPED
等宏检查)。
02.进程替换
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变
替换函数
其实有六种以exec开头的函数,统称exec函数:
#include <unistd.h>`
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回-1
- 所以exec函数只有出错的返回值而没有成功的返回值。
命名理解:
- l(list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p自动搜索环境变量PATH
- e(env) : 表示自己维护环境变量
让进程用exec函数,执行起来新的程序
main
函数演示了如何使用 execl
函数进行进程替换。这段代码旨在在 Unix-like 系统上运行,其中 execl
是用来替换当前进程并执行新的程序。这里,新程序是系统的 ls
命令,用来列出当前目录中的所有文件和目录(包括隐藏文件),并以长格式显示。
以下是每一行代码的具体解释:
int main()
{printf("testexec...begin\n"); // 打印开始消息execl("/usr/bin/ls", "ls", "-l", "-a", NULL); // 替换当前进程,执行 ls 命令printf("testexec...end\n"); // 打印结束消息,理论上不应执行到这里return 0; // 程序正常结束返回
}
关键点解释:
-
printf("testexec...begin\n");
- 这行代码输出 “testexec…begin” 到标准输出,标示程序开始执行。
-
execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
execl
是exec
系列函数之一,用于替换当前进程的映像为一个新的可执行文件。- 第一个参数
"/usr/bin/ls"
指定了要执行的程序的绝对路径。 - 接下来的参数 “ls”, “-l”, “-a” 是传递给
ls
程序的参数,分别代表程序名、长格式列表和显示所有文件(包括以点开头的隐藏文件)。 - 最后一个参数
NULL
表示参数列表的结束。 - 成功调用
execl
后**,原进程的代码和数据将被ls
程序替换,原main
函数之后的代码不会被执行**。
-
printf("testexec...end\n");
- 理论上,这行代码永远不会执行,因为一旦
execl
成功,当前进程的地址空间已经被新程序(这里是ls
)所替换。 - 如果出现这行代码被执行的情况,那意味着
execl
调用失败了。失败的原因可能包括指定的程序不存在,或者进程没有执行该程序的权限等。
- 理论上,这行代码永远不会执行,因为一旦
execl函数的返回值可以不关心了。只要替换成功,就不会向后继续运行只要继续运行了,一定是替换失败了!
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>int main()
{printf("testexec...begin\n");pid_t id=fork();if(id==0){execl("/usr/bin/ls","ls","-l","-a",NULL);exit(1);}int status=0;pid_t rid=waitpid(id,&status,0);if(rid>0){printf("father wait success,child exit code:%d\n",WEXITSTATUS(status));}printf("testexec...end\n");return 0;
}
main
函数展示了如何结合 fork()
和 execl()
进行进程创建和替换,还演示了如何使用 waitpid()
来等待子进程结束并获取子进程的退出状态。这是 Unix-like 系统编程的一个典型示例,通常用于需要同时运行多个程序或监控其他程序执行的情况。
代码详细分析
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>int main()
{printf("testexec...begin\n"); // 输出程序开始执行的标志pid_t id = fork(); // 创建一个新进程if (id == 0) // 子进程执行分支{execl("/usr/bin/ls", "ls", "-l", "-a", NULL); // 子进程中执行 ls 命令exit(1); // 如果 execl 执行失败,退出子进程,返回状态 1}// 父进程执行分支int status = 0;pid_t rid = waitpid(id, &status, 0); // 父进程等待子进程结束if (rid > 0) // waitpid 成功{if (WIFEXITED(status)) { // 判断子进程是否正常退出printf("father wait success, child exit code: %d\n", WEXITSTATUS(status)); // 输出子进程的退出状态}}printf("testexec...end\n"); // 输出程序结束的标志return 0;
}
-
进程创建 (
fork()
):fork()
创建一个新的子进程,子进程是父进程的一个副本。fork()
在父进程中返回子进程的 PID,在子进程中返回 0。- 由于操作系统的调度策略,父进程和子进程之后的执行顺序是不确定的。
-
进程替换 (
execl()
):- 在子进程中,
execl()
用于加载并执行指定的程序(这里是/usr/bin/ls
)。 - 如果
execl()
成功,它不会返回;如果失败,会返回 -1,并且子进程继续执行后续代码。
- 在子进程中,
-
退出处理 (
exit()
):- 在子进程中,如果
execl()
调用失败,紧接着调用exit(1)
来结束子进程,并返回状态码 1。
- 在子进程中,如果
-
进程同步 (
waitpid()
):- 父进程使用
waitpid()
等待子进程结束,并通过status
变量获取子进程的退出状态。 WIFEXITED(status)
检查子进程是否正常结束,WEXITSTATUS(status)
获取子进程的返回码。
- 父进程使用
错误处理和输出
- 子进程在
execl()
调用失败时通过exit(1)
明确指示错误退出。 - 父进程检查
waitpid()
返回值以确认等待是否成功,并从状态码中提取具体的退出信息,正确处理并报告子进程的退出状态。
这个程序结构清晰,展示了进程的创建、执行替换、等待及状态检查的完整流程,是学习 Unix/Linux 系统编程的一个很好的实例。
一旦子进程进行了替换,也要进行写时拷贝
#include <unistd.h>
int main()
{char *const argv[] = {"ps", "-ef", NULL};char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};execl("/bin/ps", "ps", "-ef", NULL);// 带p的,可以使用环境变量PATH,无需写全路径execlp("ps", "ps", "-ef", NULL);// 带e的,需要自己组装环境变量execle("ps", "ps", "-ef", NULL, envp);execv("/bin/ps", argv);// 带p的,可以使用环境变量PATH,无需写全路径execvp("ps", argv);// 带e的,需要自己组装环境变量execve("/bin/ps", argv, envp);exit(0);
}
事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示
我们现在用c语言的文件来替代c++的程序:
修改makefile让其一次性生成两个可执行文件
检测pid:
同理,其他类型的程序我们也可以替换
pid_t id=fork();if(id==0){char *const argv[]={"mypragma",NULL};printf("child pid:%d\n",getpid());sleep(2);execvpe("./mypragma",argv,NULL);// execl("./mypragma","mypragma",NULL); // char const* argv[]={(char*)"ls",(char *)"-l",(char*)"-a",NULL};// execv("/usr/bin/ls",argv);exit(1);}
execvpe
函数的使用
execvpe
的原型如下:
int execvpe(const char *file, char *const argv[], char *const envp[]);
- file: 要执行的程序的名称或路径。
- argv: 指向以 NULL 结尾的字符串数组的指针,这些字符串为要传递给新程序的命令行参数。
- envp: 指向以 NULL 结尾的字符串数组的指针,这些字符串构成了新程序的环境。
代码中,使用 execvpe
来执行 ./mypragma
程序,并将 argv
设置为 {"mypragma", NULL}
。这意味着 mypragma
作为参数0(通常是程序名称)传递给 mypragma
程序。
打印结果:
[dyx@VM-8-13-centos process_test]$ ./myprocess
testexec...begin
child pid:21680
argv[0]:mypragma
-------------------------
env[0]:HAHA=111111
env[1]:HEHE=222222
-------------------------
hello c++,I am a c++ pragma:21680
hello c++,I am a c++ pragma:21680
hello c++,I am a c++ pragma:21680
hello c++,I am a c++ pragma:21680
father wait success,child exit code:0
testexec...end