Linux_进程终止_进程等待_进程替换

进程终止

不知道大家想过没有,我们写的main()函数的返回值是返回给谁的呢?其实是返回给父进程或者系统的。

int main()
{
    std::cout << "hello" << std::endl;
    return 10;
}

运行该代码,输入hello,没问题!当我们在命令行使用echo $? 发现结果是10。echo $?的作用是获得最近一个程序退出时的退出码。这个退出码通常用来表明错误的原因:返回0,说明程序执行成功;返回非0,程序执行错误。

当返回不同的非0数字时,约定或者表明不同出错的原因。其实系统是为我们提供了一批错误码的,通过函数errno就可获取,但是呢,这个错误码只是一个数字,系统可以看懂,但我们看不懂啊,所以我们通过strerror()获取对应错误码的错误信息!下面是,两个函数的函数原型和使用举例

#include <iostream>
#include <string>
#include <cstdio>
#include <string.h>
#include <errno.h>
int main()
{
    // 正常执行
    printf("before: errno:%d, errstring: %s\n",errno, strerror(errno));

    FILE* fp = fopen("./log.txt","r"); // 本路径下没有log.txt文件,所以打开一定会失败
    if(fp == nullptr)
    {
        printf("after: errno:%d, errstring: %s\n",errno, strerror(errno));
        return errno; 
    }
    return 10;
}

进程终止的方式

main函数的return

对于这种终止方式,大家都比较熟悉,这里不再解释

exit() - 最常见的终止方式

先看函数原型和代码举例

void fun(){
    std::cout << "hello world" << std::endl;
    exit(100);
}

int main()
{
    fun();
    std::cout << "进程正常退出" << std::endl;
}

上述代码运行之后,将语句exit(100);改为 return 100;下面是两次运行的结果对比

从结果我们可以发现,使用exit函数之后,该程序就终止了,不再执行后面的语句!!所以return表示函数结束而在代码的任何地方调用exit()函数,都表示进程结束

_exit()

_exit与exit非常的类似,在用法上参考exit。下面我们主要谈谈它们的区别

  1. _exit不会刷新缓冲区,exit会刷新缓冲区。
  2. exit属于3号手册,属于语言级别;_exit属于2号手册,属于系统级别(系统调用)
  3. exit与_exit是上下层的关系。

结合以上3点,我们可以得出结论:我们之前认为的缓冲区一定不在操作系统内部,这个缓冲区叫做语言级缓冲区,由C / C++提供。

进程等待

为什么要有进程等待?

  1. 当子进程退出,如果父进程不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
  2. 进程一旦变成僵尸状态,那就变得刀枪不入,就算是“杀人不眨眼”的kill -9 也无能为力,毕竟谁也没有办法杀死一个已经死去的进程。
  3. 父进程派给子进程的任务完成的如何,我们需要知道。如:子进程运行完成,结果对还是不对,或者是否正常退出。

结合以上三点,我们可以得出结论:父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

进程等待的方法

wait方法与waitpid方法

来看函数原型

wait():返回值:成功就返回被等待进程pid,失败则返回-1。status为输出型参数,获取子进程退出状态,如果不关心则可以设置成为nullptr。

<重点>waitpid():

返回值:

  • 当正常返回的时候waitpid返回收集到的子进程的进程ID;
  • 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
  • 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

参数:

  •  pid:

        Pid = -1,等待任一个子进程。与wait等效。

        Pid > 0.等待其进程ID与pid相等的子进程。

  • status:

        WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出,即退出信号

        WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码

  • options:

        WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

对于我们创建出来的子进程,作为父进程,必须得等待子进程,直到子进程结束。即对子进程负责。如果子进程不退,父进程就要阻塞在wait函数内部(类比scanf())。我们来认识一下

获取子进程status
 

  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
  • 如果传递nullptr,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

status不能简单的当作整形来看待,它由32个比特位构成,其中8 - 15位为退出码。可以把它当作位图来看待,具体细节如下图(只研究status低16比特位):

重谈进程退出
  1. 代码跑完,结果对,return 0
  2. 代码跑完,结果不对,return !0
  3. 进程异常了,OS提前使用信号终止了你的进程

上述的前两种情况,通过退出码就可判定。但第三种情况只有在进程的退出信息中的退出信号里才可以判定。

一般我们想知道一个进程运行结果是否正确,前提条件是这个信号的 退出信号 值为0(证明代码是正常跑完的),但 结果对还是不对 是通过退出码来进一步判断

当你创建出来一个子进程,让它帮你完成任务。如果你不关心子进程做的怎么样,那么你可以不用status(即将status设为nullptr);当你想要获取子进程的退出结果等信息,此时我们必须要通过status来获取子进程的退出信息。

// 使用宏,获取进程的退出信息
if(WIFEXITED(status))     // 退出信号不为空,进程正常执行
{
    printf("wait sub process success, rid: %d, status code: %d\n"
                                    ,rid, WEXITSTATUS(status));
}
else            
{
    printf("child process quit error!\n");
}

阻塞与非阻塞

waitpid()中的第三个参数options就是与阻塞相关的。当options的值为0时,为阻塞等待;当options的值为WNOHANG时,为非阻塞等待。在非阻塞等待时,由父进程循环调用非阻塞接口,完成轮询检测,以完成更多的事情。

// 非阻塞测试代码
typedef std::function<void()> task_t;
void LoadTask(std::vector<task_t> &tasks){
    tasks.push_back(PrintLog);
    tasks.push_back(Download);
    tasks.push_back(Backup);
}

int main()
{
    std::vector<task_t> tasks;
    LoadTask(tasks);

    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        while(true)
        {
            printf("我是子进程, pid : %d\n", getpid());
            sleep(1);
        }
        exit(0);
    }
    while(true)
    {
        pid_t rid = waitpid(id, nullptr, WNOHANG); 
        if(rid > 0)
        {
            printf("等待子进程%d成功\n",rid);    // 返回目标子进程的pid
            break;
        }
        else if(rid < 0)
        {
            printf("等待子进程失败\n");
            break;
        }
        else
        {
            printf("子进程尚未退出\n");

            // 在等待子进程期间,父进程可以做自己的事情
            for(auto &task : tasks)
            {
                task();
            }
        }
    }
}

当waitpid()的返回值 > 0 表示等待成功,返回目标子进程的pid;

当waitpid()的返回值 == 0 表示等待成功,但是子进程没有退出;

当waitpid()的返回值 < 0 表示等待失败。

进程程序替换

什么是程序替换

我们在使用fork()系统调用之后,创建出来的子进程是对父进程的复制,也就是说子进程和父进程执行的是相同的程序,虽然说父子进程可能执行的是不同的代码分支(if else语句),但是程序流程是一样。所以我们要想让新创建的子进程中执行其他程序,就需要子进程调用一种exec函数来达到执行另一个程序的目的。

当进程调用一种exec函数的时候,该进程的用户空间代码数据全部被新程序替换掉,从新程序的启动例程开始执行。需要注意的是,调用exec并不会创建新进程,而是一种进程替换,所以调用exec前后,进程本身的pid不会改变。        

程序替换的原理

// myexec.cc文件
#include <unistd.h>
int main()
{
    execl("/bin/ls", "ls", "-l", "-a", nullptr);

    return 0;
}

 当我们 ./myexec.cc 之后该文件就变为一个进程,拥有自己的PCB,页表等。磁盘里还存在另一个程序,当我们在代码里调用execl函数,磁盘里另一个程序覆盖当前进程的数据段和代码段!这就叫做程序替换。哪一个进程调用execl,哪一个进程的代码和数据就会execl中参数的相关信息覆盖。

由上图可知,execl并不会创建新的进程,只是把代码和数据替换了!但是不会影响命令行参数和环境变量,虽然它们也是数据。

 exec函数族

 exec函数族一共有6种,下面是函数原型 和 需要包含的头文件

#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[]);
  • 函数参数

        path:可执行程序路径。

        file:要执行的程序名

        arg:参数列表,最后需要一个nullptr作为结尾,这个nullptr实际上就是一个哨兵,来告诉程序参数列表到此结束。另外参数arg是从arg[0]开始的,而arg[0]是这个程序本身,所以在写参数列表的时候需要先写一个程序本身来占位(实际上是个占位参数)。

  • 返回值

        成功的时候是没有返回值的;只有失败了才会返回-1。由此我们可以得出结论:只要返回,就说明exec失败了!(exit函数也不需要考虑返回值)

exec函数族介绍

exec函数族的命令是有一定的规律的,l表示list,就是参数列表的意思;p代表PATH,所以带p的参数都是file,不带p的参数都是path;e代表环境变量,我们可以设置这个环境变量,比如execle()有一个参数envp[]就是设置环境变量的;v表示vector,我们可以把参数放到一个数组中(我们就不需要一个一个写参数了),然后把数组传给execv()。结合下图理解可能更清晰一点

exec函数族本质就相当于把可执行程序加载到内存。 

exec函数族的详细说明

execl函数的第一个参数是一条路径,后面是可变参数,也就是说你在命令行怎样输出命令,在这里就怎么写。

举个例子:

 execl函数与execv函数没有本质区别,execv只是把在execl中需要传递的参数,放在了一个vector里面,直接传vector就可以了。所有函数名中带p的第一个参数只需要传可执行程序的名字

// 部分exec函数的使用举例 -- 以ls为例
execl("/bin/ls", "ls", "-l", "-a", nullptr);
execv("/bin/ls", argv);
execlp("ls", "ls", "-l", "-a", nullptr);     
execvp("ls", argv);  
关于环境变量

环境变量我们可以不传,使用默认的;也可以使用execvpe函数自主决定要传什么样的环境变量,就是使file使用全新的环境变量。

  1. 可以让子进程继承父进程全部的环境变量
  2. 如果要传递全新的环境变量(需要自己定义,自己传递)
  3. 新增环境变量需要用putenv函数

 exec函数族的调用关系 

事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。

#include <unistd.h>

int execve(const char *filename, char *const argv[], char *const envp[]);

上述函数只有在传参方式上有明显差别,是为了满足不同的应用场景。

shell进程执行命令的原理

exec函数族的作用是用来进行进程替换的,但是exec函数有个特点:一旦执行成功就不会再返回了。如果我们shell需要执行某种功能,直接对shell进程进行替换,成功后就直接执行该功能。很好,没毛病,但是我执行完该功能,我想再返回shell进程,这时候怎么办?

实际上shell是先使用fork()创建一个子进程,然后让子进程使用exec函数进行进程替换,从而完成某种功能。这两个进程互不干扰,既解决了上面的返回问题,又解决了执行某种功能的问题。这才是真实的exec函数的应用场景,也就是说exec函数族是和fork()函数一块使用的。实际上这就是shell执行命令的原理。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/898932.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

I\O进程线程(Day32)

一、学习内容 进程之间的通信(nterprocess communication) 信号通信 概念 1> 信号通信中&#xff0c;多个进程只起到通知作用&#xff0c;没有数据传输的功能 2> 所谓信号通信&#xff0c;就是软件模拟的硬件的中断请求 3>原理图 信号处理方式 默认&#xff08;SIG_DF…

SpringBoot1~~~

目录 快速入门 依赖管理和自动配置 修改自动仲裁/默认版本号 starter场景启动器 自动配置 修改默认扫描包结构 修改默认配置 读取application.properties文件 按需加载原则 容器功能 Configuration Import ​编辑 Conditional ImportResource 配置绑定Configur…

华为云购买弹性云服务器(教程)

配置弹性云服务器 基础配置 实例 操作系统

『完整代码』坐骑召唤

创建一个按钮 作为召唤/消失坐骑的开关 将预制体放入指定文件夹 命名为Mount01 创建脚本并编写&#xff1a;CallMount.cs using UnityEngine; using UnityEngine.UI; public class CallMount : MonoBehaviour{public Button callBtn;GameObject mountPrefab;GameObject mountIn…

信息搜集 --子域名

1.证书查询 通过ssl证书指纹在crt.sh |证书搜索网站搜索 这些就是证书一样的 2.fofa等空间测绘平台查询 3.dns查询 https://dnsdumpster.com/ 4.威胁情报中心 360 微步等等 5.枚举 暴力破解 工具推荐&#xff1a;oneforall GitHub - shmilylty/OneForAll: OneForAll是一款…

windows 上面交叉编译 适合arm架构上的linux内核系统的qt 版本,源码编译

1. 在机器上确认系统信息 cat /proc/cpuinfomodel name : ARMv7 Processor rev 5 (v7l) arm 32位 BogoMIPS : 57.14 Features : swp half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 CPU implementer : 0x41 CPU architecture: 7 …

【Linux】线程互斥与同步,生产消费模型(超详解)

目录 线程互斥 进程线程间的互斥相关背景概念 数据不一致问题 锁 深度理解锁 原理角度理解&#xff1a; 实现角度理解&#xff1a; 线程同步 条件变量 测试代码 生产消费模型 生产消费模型概念 编写生产消费模型 BlockingQueue &#xff08;1&#xff09;创建生产…

双十一宠物空气净化器哪款吸毛好而且噪音低?希喂、IAM、有哈真实测评

家人们谁懂啊&#xff0c;家里怎么会有一只这么爱掉毛的小猫咪啊&#xff0c;看着香香软软的&#xff0c;谁知道掉起毛来六亲不认啊&#xff0c;搞得我这个老母亲筋疲力尽啊&#xff0c;每天只想着清理它掉下来的浮毛&#xff0c;主要是还特别难清理。 所以后面入手了能吸毛的…

OpenAI o1复现:自动构造prm训练数据-OmegaPRM

作者&#xff1a;cmathx 原文&#xff1a;https://zhuanlan.zhihu.com/p/1477078851 openai o1复现中&#xff0c;有个比较关键的问题&#xff0c;怎么样自动化构造prm模型的训练数据&#xff1f;本文主要从代码层面&#xff0c;来解析OmegaPRM原理。 论文 Improve Mathemat…

Discuz | 起尔开发 传奇开服表游戏公益服发布论坛网站插件

Discuz | 起尔开发 传奇开服表游戏公益服发布论坛网站插件 插件下载&#xff1a;源码 - 起尔开发的插件下载 演示地址&#xff1a;discuz.72jz.com 标黄和非标黄自动分开 在标黄时间内显示在上面置顶&#xff0c;标黄过期后自动显示在下面白色区域。 后台可以设置非标黄默认…

四、多线程带来的的⻛险-线程安全

4.1 观察线程不安全 运行以下代码&#xff1a; package demo02;public class Test {private static int count 0;public static void main(String[] args) throws Exception {Thread t1 new Thread(() -> {for (int i 0; i < 50_000; i) {count;}});Thread t2 new …

通过Docker Compose构建自己的Java项目

通过Docker Compose构建自己的Java项目 前置条件 安装了Docker,未安装的请移步:CentOS7 / CentOS8 安装 Docker-ce安装了Docker-Compose,未安装的请移步:在CentOS7、CentOS8系统下安装Docker Compose1. 配置阿里云镜像仓库 为了提高Docker镜像的下载速度,我们可以配置阿…

版本工具报错:Error Unity Version Control

NotConfiguredClientException: Unity VCS client is not correctly configured for the current user:Client config file.

python 爬虫 入门 三、登录以及代理。

目录 一、登录 &#xff08;一&#xff09;、登录4399 1.直接使用Cookie 2.使用账号密码进行登录 可选观看内容&#xff0c;使用python对密码进行加密&#xff08;无结果代码&#xff0c;只有过程分析&#xff09; 二、代理 免费代理 后续&#xff1a;协程&#xff0c;…

TitanIDE:解锁编程教学新范式

在高校软件工程类课程教育中&#xff0c;传统编程教学方式正面临着多重痛点&#xff1a; 环境配置繁琐&#xff1a;软件工程类课程往往需要学生自行配置复杂的开发环境。但是&#xff0c;学校硬件设备条件差异、软件兼容性问题等因素&#xff0c;导致学生学习效率低下&#xf…

热销王西圣H1头戴式耳机—全平台售罄断货:揭秘抢购潮究其原因?

西圣xisem作为国内平价享轻奢的领军品牌&#xff0c;就在今年它家的头戴式蓝牙耳机性价比标杆—西圣H1&#xff0c;凭借其发烧级的千元音质、降噪与满级的旗舰配置性能&#xff0c;不仅惊艳了整个耳机圈&#xff0c;还在仅仅的几个月内&#xff0c;西圣H1头戴式耳机已经火爆断货…

python 使用gradio启动程序报错

问题一&#xff1a;localhost is not accessible 解决办法&#xff1a; export no_proxy"localhost,127.0.0.1,::1"

C#学习笔记(三)

C#学习笔记&#xff08;三&#xff09; 第 二 章 命名空间和类、数据类型、变量和代码规范二、类的组成和使用分析1. 基本概念2. 类的内容组成3. 方法的初步理解 第 二 章 命名空间和类、数据类型、变量和代码规范 二、类的组成和使用分析 1. 基本概念 类是程序的基本单元&a…

PostgreSQL中触发器递归的处理 | 翻译

许多初学者在某个时候都会陷入触发器递归的陷阱。通常&#xff0c;解决方案是完全避免递归。但对于某些用例&#xff0c;您可能必须处理触发器递归。本文将告诉您有关该主题需要了解的内容。如果您曾经被错误消息“超出堆栈深度限制”所困扰&#xff0c;那么这里就是解决方案。…

Javascript算法——二分查找

1.数组 1.1二分查找 1.搜索索引 开闭matters&#xff01;&#xff01;&#xff01;[left,right]与[left,right) /*** param {number[]} nums* param {number} target* return {number}*/ var search function(nums, target) {let left0;let rightnums.length-1;//[left,rig…