
🔥个人主页:Quitecoder
🔥专栏:linux笔记仓

目录
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.进程替换
替换函数
其实有六种以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
 











