当前位置: 首页 > article >正文

【linux】进程等待与进程替换

Alt

🔥个人主页Quitecoder

🔥专栏linux笔记仓

Alt

目录

    • 01.进程等待
      • 系统调用
      • 获取子进程status
      • 常用宏
      • 使用示例
    • 02.进程替换
      • 替换函数
      • 关键点解释:
      • 代码详细分析
      • `execvpe` 函数的使用

01.进程等待

任何子进程,在退出的情况下,一般必须要被父进程进行等待。进程在退出的时候,如果父进程不管不顾,退出进程,状态Z(僵尸状态),内存泄漏

  • 进程一旦变成僵尸状态,kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程
  • 父进程通过等待,解决子进程退出的僵尸问题,回收系统资源(一定要考虑的)
  • 获取子进程的退出信息,知道子进程是因为什么原因退出的(可选的功能)

系统调用

  1. wait()

    • wait() 函数使调用的进程(通常是父进程)暂停执行,直到一个子进程终止或发生一个信号。这个调用通常用于简单的父子进程同步。
    • 函数原型:pid_t wait(int *status);
    • 如果有子进程退出,wait() 返回子进程的 PID,并可通过 status 指针获取子进程的退出状态。
  2. 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 值的相关宏和方法:

常用宏

  1. WIFEXITED(status):

    • 判断子进程是否正常退出(调用 exit 或者返回 main 函数)。
    • 返回非零值表示子进程正常退出,可以通过 WEXITSTATUS(status) 获取退出状态。
  2. WEXITSTATUS(status):

    • WIFEXITED(status) 为真时使用。
    • 获得子进程的退出码(也就是子进程传递给 exit() 的参数或 main() 函数的返回值),这是一个8位的整数。
  3. WIFSIGNALED(status):

    • 判断子进程是否因为未捕获信号而终止。
    • 返回非零值表示子进程被信号终止,可以通过 WTERMSIG(status) 获取导致终止的信号编号。
  4. WTERMSIG(status):

    • WIFSIGNALED(status) 为真时使用。
    • 获得导致子进程终止的信号编号。
  5. WIFSTOPPED(status):

    • 判断子进程是否因信号停止。
    • 返回非零值表示子进程被信号停止,可以通过 WSTOPSIG(status) 获取导致停止的信号编号。
  6. WSTOPSIG(status):

    • WIFSTOPPED(status) 为真时使用。
    • 获得导致子进程停止的信号编号。
  7. 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: 在一些实现中,这些位可以包含信号编号,表示子进程因信号而终止。
  • 特定的位字段:表明子进程是否被信号中止、是否正常退出、是否由信号停止(这些信息是由 WIFEXITEDWIFSIGNALEDWIFSTOPPED 等宏检查)。

在这里插入图片描述

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; // 程序正常结束返回
}

关键点解释:

  1. printf("testexec...begin\n");

    • 这行代码输出 “testexec…begin” 到标准输出,标示程序开始执行。
  2. execl("/usr/bin/ls", "ls", "-l", "-a", NULL);

    • execlexec 系列函数之一,用于替换当前进程的映像为一个新的可执行文件。
    • 第一个参数 "/usr/bin/ls" 指定了要执行的程序的绝对路径
    • 接下来的参数 “ls”, “-l”, “-a” 是传递给 ls 程序的参数,分别代表程序名、长格式列表和显示所有文件(包括以点开头的隐藏文件)。
    • 最后一个参数 NULL 表示参数列表的结束
    • 成功调用 execl 后**,原进程的代码和数据将被 ls 程序替换,原 main 函数之后的代码不会被执行**。
  3. 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;
}
  1. 进程创建 (fork())

    • fork() 创建一个新的子进程,子进程是父进程的一个副本。
    • fork() 在父进程中返回子进程的 PID,在子进程中返回 0。
    • 由于操作系统的调度策略,父进程和子进程之后的执行顺序是不确定的。
  2. 进程替换 (execl())

    • 在子进程中,execl() 用于加载并执行指定的程序(这里是 /usr/bin/ls)。
    • 如果 execl() 成功,它不会返回;如果失败,会返回 -1,并且子进程继续执行后续代码。
  3. 退出处理 (exit())

    • 在子进程中,如果 execl() 调用失败,紧接着调用 exit(1) 来结束子进程,并返回状态码 1
  4. 进程同步 (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

在这里插入图片描述


http://www.kler.cn/a/394597.html

相关文章:

  • 【Linux】HTTP协议和HTTPS加密
  • 微澜:用 OceanBase 搭建基于知识图谱的实时资讯流的应用实践
  • MySQL与Oracle对比及区别
  • JWT深度解析:Java Web中的安全传输与身份验证
  • Linux 函数在多个地方被同时调用时,函数中的变量如何管理,确保互不影响
  • C++面试基础知识:排序算法 C++实现
  • LeetCode343.整数拆分
  • 客户端发送http请求进行流量控制
  • 如何绕过Captcha并使用OCR技术抓取数据
  • 如何轻松导出所有 WordPress URL 为纯文本格式
  • 红黑树
  • 【日常记录-Git】如何为post-checkout脚本传递参数
  • 【SQL】sql常用命令
  • uniapp在app模式下组件传值
  • 【go从零单排】Ticker
  • Python自动化操作JSON文件详解
  • 在Ubuntu 24.04 LTS上安装飞桨PaddleX
  • uniapp隐藏自带的tabBar
  • 【刷题19】队列+bfs专题
  • 生成自签名证书并配置 HTTPS 使用自签名证书
  • uni-app快速入门(四)--maninfest.json及pages.json配置
  • CSS新特性
  • Ai编程从零开始全栈开发一个后台管理系统之用户登录、权限控制、用户管理-前端部分(十二)
  • nacos配置中心入门
  • 【达梦数据库】参数优化脚本主要改什么
  • spark.default.parallelism 在什么时候起作用,与spark.sql.shuffle.partitions有什么异同点?