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

devtools/2024/10/24 4:09:42/

进程终止

不知道大家想过没有,我们写的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);    // 返回目标子进程的pidbreak;}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执行命令的原理。


http://www.ppmy.cn/devtools/128351.html

相关文章

Jetpack架构组件_LiveData组件

1.LiveData初识 LiveData:ViewModel管理要展示的数据&#xff08;VM层类似于原MVP中的P层&#xff09;&#xff0c;处理业务逻辑&#xff0c;比如调用服务器的登陆接口业务。通过LiveData观察者模式&#xff0c;只要数据的值发生了改变&#xff0c;就会自动通知VIEW层&#xf…

Spring Boot + Vue 前后端分离项目总结:解决 CORS 和 404 问题

Spring Boot Vue 前后端分离项目总结&#xff1a;解决 CORS 和 404 问题 在进行前后端分离的项目开发中&#xff0c;我们遇到了几个关键问题&#xff1a;跨域问题 (CORS) 和 404 路由匹配错误。以下是这些问题的详细分析和最终的解决方案。 问题描述 跨域请求被阻止 (CORS) 当…

11.学生成绩管理系统(Java项目基于SpringBoot + Vue)

目录 1.系统的受众说明 2 总体设计 2.1 需求概述 2.2 软件结构 3 模块设计 3.1 模块基本信息 3.2 功能概述 3.3 算法 3.4 模块处理逻辑 4 数据库设计 4.1 E-R图 4.2 表设计 4.2.1 管理员信息表 4.2.2 课程基本信息表 4.2.3 课程扩展信息表 4.2.4 专业信…

【华为HCIP实战课程十三】OSPF网络中3类LSA及区域间负载均衡,网络工程师

一、ABR SW1查看OSPF ABR为R4而非R3,因为R4连接骨干区域0,R3没有连接到区域0 R6查看OSPF路由: 二、查看3类LSA,由于R6不是ABR因此自身不会产生3类LSA 但是有区域间路由就可以看到3类LSA

sealed class-kotlin中的封闭类

在 Kotlin 中&#xff0c;sealed class&#xff08;密封类&#xff09;是一种特殊的类&#xff0c;用于限制继承的类的数量。密封类可以被用来表示一组有限的类型&#xff0c;通常用于状态管理或表达多种可能的错误类型。 密封类用 sealed 关键字定义&#xff0c;这意味着只能…

LeetCode15 三数之和 - “贪心+双指针: 基于”两数之和“的拓展题“

Leetcode 15&#xff1a; 三数之和 题目链接 发布在LeetCode上的题解 思路 这道题的思路建立在 167.两数之和 的基础上。先来看看”两数之和“的大概题意&#xff1a; 已知一个非递减的数组&#xff0c;找出满足相加之和等于目标数 target 的两个数&#xff0c;假设每个输…

Vue项目中实现拖拽上传附件:原生JS与Element UI组件方法对比

在现代化的Web应用中&#xff0c;文件上传是一个基本功能。随着技术的发展&#xff0c;拖拽上传已经成为提升用户体验的一个重要特性。在Vue项目中&#xff0c;我们可以通过原生JavaScript或使用Element UI组件来实现这一功能。下面我们将分别介绍这两种方法&#xff0c;并对比…

小新学习K8s第一天之K8s基础概念

目录 一、Kubernetes&#xff08;K8s&#xff09;概述 1.1、什么是K8s 1.2、K8s的作用 1.3、K8s的功能 二、K8s的特性 2.1、弹性伸缩 2.2、自我修复 2.3、服务发现和负载均衡 2.4、自动发布&#xff08;默认滚动发布模式&#xff09;和回滚 2.5、集中化配置管理和密钥…