【Linux系统】进程终止、进程等待与进程替换的概念与实现


各位读者大佬好,我是落羽!一个坚持不断学习进步的学生。
如果您觉得我的文章还不错,欢迎多多互三分享交流,一起学习进步!

也欢迎关注我的blog主页: 落羽的落羽

文章目录

一、进程终止

1. 进程的退出码

一个进程终止,本质上是释放系统资源,也就是释放进程相关内核数据结构和对应代码和数据。

而进程退出时,无非就是以下三种情况:

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止(被信号终止了)

而进程执行的结果状态,可以用两个数字表示出来,即退出码终止信号int exit_code, int exit_signal

  • 当代码运行完毕且结果正确时,终止信号为0,退出码为0
  • 当代码运行完毕且结果不正确,终止信号为0,退出码不为0
  • 当代码异常终止时,终止信号不为0,退出码无意义

这两个数字不用由我们维护,OS会把进程退出的详细信息写到进程的task_struct中

所以,进程需要僵尸状态维持自己的退出状态!

话说回来,为什么自此开始学习编程后,main函数总是要return 0这就是因为,main函数的返回值就是这个进程的退出码,而一般规定退出码0代表进程结果正确!

当一个进程正常终止后,在Linux系统中我们可以用命令echo $?查看上一个进程的退出码!

可以使用strerror函数获取退出码对应的描述:

2. 进程退出的方法

进程常见的退出方法有:

  • 代码执行完毕,正常终止的方法:
    • main函数return
    • 使用exit函数或_exit系统调用
  • 代码执行异常终止方法:
    • ctrl c、发送信号终止

exit是一个C库函数,作用是使当前进程终止,参数是想要返回的退出码

_exit是一个系统调用,它和exit的唯一区别是exit终止会强制刷新缓冲区,但_exit不会。
而本质上,exit的实现也是封装了_exit。这代表刷新缓冲区的操作,一定不是系统内核中的,而是由C/C++维护的!

写一段代码验证一下:

c 复制代码
#include<stdio.h>
#include<stdlib.h>

void test()
{
    exit(1);
}

int main()
{
    test();
    return 0;
}

在非main函数中,要注意return和exit的使用区别------return仅退出当前子函数、向函数调用处返回值、程序继续执行;exit 直接终止整个进程、程序彻底停止!

二、进程等待

之前已经讲过,子进程退出,父进程如果不管,就会导致子进程一直处于僵尸状态,而造成内存泄漏!除此之外,父进程还可能需要知道子进程的任务完成如何,也就是需要获取子进程的退出信息。

所以,父进程需要通过进程等待的方式,回收子进程资源,获取子进程退出信息!

1. 进程等待的方法

实现进程等待主要依靠系统调用waitwaitpid

wait可以等待任意一个子进程,且是阻塞等待。它的返回值是:如果等待成功,返回等待的子进程pid;等待失败则返回-1。
至于它的参数,是用于获取子进程退出信息的,下面讲

waitpid的功能比wait更丰富,返回值规则与wait类似,它有三个参数:

  • 参数pid_t pid:表示等待特定的子进程pid。若传-1则表示等待任意一个子进程。如果调用中出错,如传的pid不是自己子进程的pid,waitpid返回-1,程序的error会被设置成相应的错误码。
  • 参数int options传0,表示阻塞等待,这是默认情况;传WNOHANG(一个宏),表示非阻塞等待。阻塞等待时,父进程不会继续执行代码,直到等到了子进程退出;非阻塞等待时,若指定pid的子进程还没有退出,waitpid返回0,不予等待。

可以看出,如果waitpid第一个参数传-1,第三个参数传0,效果就和wait一样。

  • wait的参数和waitpid第二个参数int* status,是要传一个int变量的地址,这个变量用于接收子进程的退出信息;若不想获取子进程退出信息,则可以传NULL

写一个程序验证一下:

c 复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        printf("我是子进程, pid是%d, 终止信号是%d, 退出码是%d\n", getpid(), 0, 1);
        exit(1);
    }
    else
    {
        int status = 0;
        pid_t retpid = waitpid(id, &status, 0); // 子进程退出前,父进程一直阻塞在这里
        printf("我是父进程, 子进程%d已回收, status是%d\n", retpid, status);
    }

    return 0;
}

其他地方没有问题,可是status是256怎么来的??

其实,status不能当做简单的int看待,而是一个位图
status有32个比特位,只用后16个比特位记录信息。在后16个比特位中,低8位表示子进程的终止信号,高8位表示子进程的退出码!
所以,在上面的例子中,子进程终止信号为0,即00000000;子进程退出码为1,即00000001。那么status的32比特位为:00000000000000000000(前16位不用)00000001(退出码)00000000(终止信号),转为十进制就是256!

换句话说,想从status中得到子进程的具体信息,还需要这样位运算:

  • 终止信号 = status & 0x7F退出码 = (status >> 8) & 0x7F

但其实并不需要我们手动运算,系统中已经为我们提供了相应的宏函数:

  • WIFEXITED(status):如果是正常终止的子进程,返回真
  • WEXITSTATUS(status):如果WIFEXITED(status)是真,返回子进程退出码

试验一下:

c 复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        printf("我是子进程, pid是%d, 终止信号是%d, 退出码是%d\n", getpid(), 0, 10);
        exit(10);
    }
    else
    {
        int status = 0;
        pid_t retpid = waitpid(id, &status, 0); // 子进程退出前,父进程一直阻塞在这里
        printf("我是父进程, 子进程%d已回收, status是%d\n", retpid, status);
        printf("子进程是否正常终止:%d\n", WIFEXITED(status));
        printf("子进程退出码:%d\n", WEXITSTATUS(status));
    }

    return 0;
}

没有问题!

三、进程替换

fork之后,父子进程各自执行当前程序的代码的一部分,如果我们想让子进程执行一个全新的程序怎么办呢?进程的程序替换来满足这个需求!

进程替换是指,通过特定的接口,加载磁盘上的一个全新程序,加载到调用进程的地址空间中!

1. 程序替换的方法

实现进程替换,主要依靠exec系列库函数:

这六个函数的功能都是替换一个程序,但是参数上有所区别,观察发现它们的参数总共是这几种:

  • const char* path:代表要传一个路径名字符串,可以是相对路径或绝对路径,如"/usr/bin/ls"
  • const char* arg, ...:代表要传若干个字符串,最后一个必须是NULL。你想替换的程序在命令行中怎么执行,这里就怎么传,如"ls", "-a", "-l", NULL
  • const char* file:代表要传一个文件名字符串,不用写路径,程序会从环境变量PATH中的寻找这个文件。如ls
  • char* const envp[]:代表要传一张环境变量表,这套环境变量会被新程序继承使用,覆盖原来的环境变量,数组元素也需要以NULL结尾;如果不想覆盖原环境变量表,则这个参数可以传environ
  • char* const argv[]:代表要传一张命令行参数表,其实就是将上面的const char* arg, ...内容写进数组再传递,数组元素也需要以NULL结尾。本质上,程序的命令行参数,都是通过父进程使用程序替换函数传递给子进程的!

不难想到,exec系列函数通过上面不同的参数组合,有了六种函数,以应对不同的使用场景:

比如:想使用int execlp(const char* file, const char* arg, ...)函数,想把当前进程替换为执行ls。在当前进程中调用

c 复制代码
execlp("ls", "ls", "-a", "-l", NULL); // 可以省略一个"ls",但是不建议,因为还要额外记忆

比如:想使用int execv(const char* file, char* const argv[])函数,想把当前进程替换为执行touch test.c。在当前进程中调用

c 复制代码
char* argv[] = {"touch", "test.c", NULL};
execv("/usr/bin/touch", argv); 
// 或execv(argv[0], argv);

这几个函数,如果程序替换成功,则没有返回值;如果调用出错则返回-1。所以exec函数只有出错的返回值,而没有成功的返回值。

在之前的学习中,我们fork创建子进程后,子进程和父进程执行的还是同一个程序,只是进入了不同的代码分支。
程序替换,没有创建新的进程 ,而是直接覆盖原有程序继续执行。所以,通常是父进程创建子进程后,让子进程替换成别的程序。程序替换后,原程序后面的代码就不再执行了。

替换的本质是:代码和数据拷贝到内存中。而只有OS有权做IO过程,所以进程替换要依靠系统调用!

真正的系统调用函数是:

上面说的exec系列函数,底层都是调用它。

我们来举个栗子完成一次程序替换:

c 复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        // child
        execlp("ls", "ls", "-a", "-l", NULL);
        
        //一般而言,替换的新程序中正常执行完就会在它内部退出终止子进程,不会执行这一句
        //而如果代码执行到了这里,就说明程序替换失败了
        exit(1);
    }
    else
    {
        waitpid(id, NULL, 0);
    }
    return 0;
}

结果符合预期,子进程替换成了ls -a -l命令!

本篇完,感谢阅读。

相关推荐
小年糕是糕手2 小时前
【C++】模板初阶
java·开发语言·javascript·数据结构·c++·算法·leetcode
脏脏a2 小时前
C++ 字符串处理利器:STL string 保姆级入门教程
开发语言·c++
李昊哲小课3 小时前
深度学习进阶教程:用卷积神经网络识别图像
人工智能·深度学习·cnn
AndrewHZ3 小时前
【AI分析进行时】AI 时代软件开发新范式:基于斯坦福CS146S课程分析
人工智能·llm·软件开发·斯坦福·cs146s·能力升级·代码agent
玖日大大3 小时前
Seedream-4.0:新一代生成式 AI 框架的技术深度与实践落地
人工智能
七夜zippoe3 小时前
告别API碎片化与高成本 - 用AI Ping打造下一代智能编程工作流
人工智能·架构·大模型·智能编程·ai ping·模型聚合
qq_479875434 小时前
C++ 网络编程中的 Protobuf 消息分发 (Dispatcher) 设计模式
网络·c++·设计模式
Tandy12356_4 小时前
手写TCP/IP协议——IP层输出处理
c语言·网络·c++·tcp/ip·计算机网络
博语小屋4 小时前
实现简单日志
linux·服务器·数据库·c++