任何子进程,在退出的情况下,一般必须要被父进程进行等待。进程在退出的时候,如果父进程不管不顾,退出进程,状态Z(僵尸状态),内存泄漏
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
函数中,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
会被设计成如下所示的位字段结构:
WIFEXITED
、WIFSIGNALED
和 WIFSTOPPED
等宏检查)。用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[]);
命名理解:
让进程用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");
execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
execl
是 exec
系列函数之一,用于替换当前进程的映像为一个新的可执行文件。"/usr/bin/ls"
指定了要执行的程序的绝对路径。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[]);
代码中,使用 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