【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命令!

本篇完,感谢阅读。

相关推荐
Mintopia几秒前
量子计算会彻底改变 AI 的运算方式吗?一场关于"量子幽灵"与"硅基大脑"的深夜对话 🎭💻
人工智能·llm·aigc
natide1 分钟前
表示/嵌入差异-4-闵可夫斯基距离(Minkowski Distance-曼哈顿距离-欧氏距离-切比雪夫距离
人工智能·深度学习·算法·机器学习·自然语言处理·概率论
再睡一夏就好7 分钟前
多线程并发编程核心:互斥与同步的深度解析及生产者消费者模型两种实现
linux·运维·服务器·jvm·c++·笔记
ulias21211 分钟前
多态理论与实践
java·开发语言·前端·c++·算法
蹦蹦跳跳真可爱58915 分钟前
Python----大模型(GPT-2模型训练,预测)
开发语言·人工智能·pytorch·python·gpt·深度学习·embedding
飞Link28 分钟前
【MySQL】Linux(CentOS7)下安装MySQL8教程
linux·数据库·mysql
llilian_1634 分钟前
时间基准的行业赋能者——北斗卫星授时同步统一设备应用解析 时统 授时同步设备
服务器·网络·单片机
hxcat36 分钟前
AI 提示词测试:在人工智能时代践行“测试左移“理念
软件测试·人工智能·chatgpt
随祥37 分钟前
网络开源工具
linux
居然JuRan40 分钟前
AI自动画界面?Google这个开源神器让前端工程师失业了
人工智能