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

从exec到Shell:深度解析Linux进程等待,程序替换与自主Shell实现

一、进程等待

1. 进程等待的必要性

这个前面是说过的哦。

1、回收子进程资源

2、获取子进程的退出信息

2. 什么是进程等待

让父进程通过等待的方式,回收子进程的PCB,Z,如果需要,获取子进程的退出信息

3. 怎么做

//等待任意一个子进程,成功的话返回子进程的pid,失败返回-1
pid_t wait(int* status);//这里status暂时设置为NULL,不关心

父进程调用wait,表示父进程等待任意一个子进程

1、如果子进程没有退出,父进程wait的时候,就会阻塞

2、如果子进程退出了,父进程wait的时候,wait就会返回了,让系统自动解决子进程的僵尸问题

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

通过这段代码,就可以验证父进程等待子进程是否成功,成功的话子进程的僵尸状态就没有了。子进程会从阻塞状态变为僵尸状态,最后被父进程回收。

上面只回收了一个子进程,如何回收多进程呢?

在这里插入图片描述

多进程中,父进程往往最先创建,最后退出。fork之前,只有一个父进程,fork之后,父进程需要对所有的子进程进行回收。

//pid为-1,表示等待任意进程,pid > 0,等待进程id与pid相等的进程
//status输出型参数,目的是为了带出一些数据
//options为0,阻塞等待,options为WNOHANG,非阻塞等待
//等待成功,返回子进程的pid,等待失败,返回0
//既没有等待成功也没有失败返回0(子进程在运行,暂时未退出)
pid_t waitpid(pid_t pid, int* status, int options);

在这里插入图片描述
在这里插入图片描述

子进程的退出码为1,可是status为什么是256呢?这里的status不仅仅是退出码。

status是一个int(整型),32个bit,高16位不考虑。剩下的16位,次8位是子进程的退出码,所以,进程的退出码取值范围是[0,255]低7位是终止信号,还有一位是core dump(这个后面再讲)。

在这里插入图片描述

进程正常终止,终止信号为0,子进程退出码为1,但是后面有8个0,所以status为256。要想拿到子进程的退出码exit_code = (status >> 8) & 0xFF

在这里插入图片描述

上面所述都是基于进程正常终止的情况。那么,如果是进程异常呢?我们都知道,进程异常了,退出码是没有意义的。因此,我们的重点应该是进程为什么会异常

是因为程序出现了问题,导致OS给你的进程发送信号了

kill -l //查看所有的进程信号

在这里插入图片描述

退出信号没有0号信号。所以我们是如何判断进程是否是正常运行结束呢?status->信号的数字 == 0

在这里插入图片描述

我们用两个数字来表示子进程的执行情况

1.进程的退出码

2.进程的退出信号

那我们要如何拿到进程的退出信号呢?status低7位表示的是进程信号,exit_signal = status & 0x7F

但是获取进程退出码,进程退出信号的方式太麻烦,所以操作系统提供了两个宏,WIFEXITED(status),WEXITSTATUS(status),分别用来表示进程是否正常退出(正常退出返回非零值,反之,返回0)和获取进程退出状态码

在这里插入图片描述

不知道大家有没有疑问呢。waitpid(pid_t pid, int* status, int options)中 pid > 0表示的是等待与pid相等的子进程,是因为父进程可以拿pid找到该子进程

可是,如果是-1呢?父进程怎么知道要等待哪一个子进程。那是因为父进程的task_struct里有对于子进程进行管理,等待子进程就看哪一个是僵尸状态就可以了

我们可以通过控制代码来验证进程等待的效果。

在这里插入图片描述

上面所说都是阻塞等待。现在,该解释什么是非阻塞等待了。

像scanf函数就是典型的阻塞等待,资源没有就绪,就会一直卡住。而非阻塞等待,就是资源没有就绪的情况,它并不会一直卡在哪里,会去做别的事情的同时也会继续等待资源就绪

例子:明天就是C语言考试,你的好朋友李四是一个学霸,你需要借助李四的笔记来复习应对明天的考试,但是李四也要用笔记来复习。但是他说等他复习好了,就可以把笔记借给你。这时候你是很开心的,但是你也很慌张,因为平时你没有好好学习。所以你就不停的给李四打电话,问他复习好了吗?他说没有,这时候你就把电话挂断了,你就去刷抖音了,刷大学生期末挂科应该怎么办?过了一会儿,你又给李四打电话,李四说还没好呢,你又去做自己的事情了。

这就是非阻塞等待。有时候你会听到非阻塞等待比阻塞等待更高效就是这个原因

在这里插入图片描述

二、进程程序替换

fork之后,父子进程各自执行父进程代码的一部分,那如果子进程就想执行一个全新的程序呢

进程的程序替换来完成这个功能

程序替换是通过特定的接口,加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的地址空间中

1. 初识程序替换函数

//pathname 你想执行的程序(路径+文件名)
//arg 我们要执行程序的程序名称
//... 给程序传递的命令行选项,必须以NULL结尾(参数包)
//失败了,返回-1
int execl(const char* pathname, const char* arg, ...);

在这里插入图片描述
在这里插入图片描述

可以看到,程序是替换成功了的。但是,有一个问题,execl 结束之后,为什么没有打印后续的 printf 呢

那是因为你的进程已经执行另一个程序的代码了,你自己的代码已经没有了

总结程序替换函数,一旦调用成功,后续代码不再执行,因为已经没有了

那如果失败呢?

在这里插入图片描述

程序替换,如果成功,不需要也不会有返回值,失败返回 -1

也就是说exe系列的函数,只要返回,必然失败

那么,如果我们想用子进程进行程序替换呢?父子进程代码是共享的,数据以写时拷贝的方式各自私有。如果用子进程程序替换,是不是也影响到了父进程呢?不是说好进程之间具有独立性吗

我们可以认为,fork之后,父子进程的代码和数据都以写时拷贝的方式各自私有。这样就不会影响父进程了,子进程会加载新的代码和数据(物理内存上新开一段空间),更改页表与物理内存的映射关系

2. 关联linux历史知识

1、命令行上的命令是bash的子进程,那它和bash是共享代码和数据的,但是不同的命令有不同的功能。这是为什么呢

我们已经知道,子进程是由父进程fork创建出来的,那么父进程不就可以对子进程进行程序替换,进程等待...等操作了吗

2、二进制文件,先加载程序到内存,为什么呢

由冯诺依曼体系结构决定的

进程 = PCB(内核数据结构)+ 自己的代码和数据

我们都知道,进程是先有数据结构的,在加载代码和数据,甚至是需要的时候在加载(惰性加载),那么你的程序,是如何加载到内存的呢?

加载的本质是为了变成进程,那么是怎么加载的呢?我们使用的exe系列的函数不就是相当于一种“加载器”吗?程序从磁盘上拷贝到内存,不就是硬件到硬件吗,只有操作系统有这个权利,所以加载一定要调用系统调用或者是对系统调用做封装

3. 详解程序替换函数

//pathname 依旧是路径+文件名
//argv[] 与execl后半部分的参数一致,只不过是用数组组织起来
int execv(const char* pathname, char* const argv[]);

在这里插入图片描述

子进程执行程序替换,我们不需要自己的可执行程序名,因此从命令行参数表下标为1的参数开始

在这里插入图片描述

//执行指定的命令,需要让execlp自己在环境变量PATH中寻找指定的程序
int execlp(const char* file, const char* arg, ...);

在这里插入图片描述
在这里插入图片描述

int execvp(const char* file, char* const argv[]);

在这里插入图片描述
在这里插入图片描述

上面执行的都是系统命令,那么,可不可以执行我们自己的命令呢?

在这里插入图片描述

execl 执行 mycmd的时候,之所以第二个参数不用带./是因为第一个参数已经表明了路径系统已经能够找到该文件了

//argv[]命令行参数表
//envp[]环境变量表
//这两张表可以是系统的,也可以自定义
int execvpe(const char* file, char* const argv[], char* const envp[]);

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

execvpe函数传递环境变量表,会默认摒弃旧的环境变量,使用你自己设置的全新的环境变量表,如果你要用系统提供的,就传入系统的环境变量表。

那如果我们既想使用系统的环境变量表又想使用自定义的呢?

//成功返回0,失败返回非0
int putenv(char* string);//默认的环境变量中新增一项

在这里插入图片描述
在这里插入图片描述

程序替换函数一共有7个。这六个都是execve衍生出来的。剩下的就不多做介绍了。
在这里插入图片描述
在这里插入图片描述

三、mini shell

myshell.h

#ifndef _SHELL_H_
#define _SHELL_H_#include<cstdio>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/wait.h>
#include<ctype.h>#define MAX 1024#define ARGS 64#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3void InitGlobal();
void PrintCommandLinePrompt();
bool GetUserCommand(char usercommand[], int len);
void CheckRedir(char usercommand[]);
bool PraseUserCommand(char usercommand[]);
bool BuiltInCommandExec();
bool ForkAndExec();
#endif

myshell.cc

#include "myshell.h"int gargc = 0; //解析用户命令存储的数组下标
char* gargv[ARGS] = {NULL};//故意设置为全局的,方便使用,安全起见,保证命令行参数表全局有效
int exit_code = 0;
char pwd[MAX] = {0};
int redir;
std::string filename;void InitGlobal()
{gargc = 0;memset(gargv,0,sizeof(gargv));redir = NONE_REDIR;filename = "";}static std::string GetUserName()
{std::string username = getenv("USER");return username.empty()? "None": username;
}static std::string GetHostName()
{char name[256] = {0};if(gethostname(name, sizeof(name)) == 0){std::string hostname = name;return hostname;}else{perror("gethostname failed");return "None";}
}static std::string GetPwd()
{//std::string pwd = getenv("PWD");//return pwd.empty()? "None": pwd;char temp[MAX];char* ptr = getcwd(temp,sizeof(temp));//将ptr指针里面的内容和PWD=(这四个字符)一起写到pwd数组里,putenv会在环境变量中查找PWD,然后进行覆盖。snprintf(pwd, sizeof(pwd), "PWD=%s", ptr);putenv(pwd);std::string str = temp;std::string delim = "/";int pos = str.rfind(delim);std::string s = str.substr(pos+delim.size());return s.empty()? "/": s;}std::string GetHomePath()
{std::string home = getenv("HOME");return home.empty()? "/": home;
}void PrintCommandLinePrompt()
{std::string username = GetUserName();std::string hostname = GetHostName();std::string pwd = GetPwd();printf("[%s@%s %s]$ ",username.c_str(), hostname.c_str(), pwd.c_str());
}bool GetUserCommand(char usercommand[], int len)
{if(usercommand == NULL || len <= 0)return false;//fgets函数会自动在字符串的末尾加上'\0'char* res = fgets(usercommand, len, stdin);if(res == NULL){perror("fgets failed");return false;}//去掉末尾的换行符(\n)usercommand[strlen(usercommand) - 1] = 0;return strlen(usercommand) == 0? false: true;
}#define TrimeSpace(start) do{\while(isspace(*start)){\start++;\}\
}while(0)//检查是否有重定向
void CheckRedir(char usercommand[])
{char* start = usercommand, *end = usercommand + strlen(usercommand) - 1;while(start <= end){if(*start == '>'){*start = '\0';if(*(start+1) == '>'){redir = APPEND_REDIR;start += 2;TrimeSpace(start);filename = start;break;}else{redir = OUTPUT_REDIR;start += 1;TrimeSpace(start);filename = start;break;}}else if(*start == '<'){redir = INPUT_REDIR;*start = '\0';start++;TrimeSpace(start);filename  = start;break;}else{start++;}}
}bool PraseUserCommand(char usercommand[])
{if(usercommand == NULL)return false;//解析//"ls -l -s" -------->  "ls", "-l", "-a"
#define SEP " " 	gargv[gargc++] = strtok(usercommand, SEP);//最后一次解析失败,会存储NULL,但是我们不需要NULLchar* str = strtok(NULL,SEP);while(str){gargv[gargc++] = str;str = strtok(NULL, SEP);}
//#define DEBUG
#ifdef DEBUGprintf("gargc:%d\n",gargc);for(int i = 0; i < gargc; ++i)printf("gargv[%d]:%s\n",i,gargv[i]);
#endifreturn true;
}bool BuiltInCommandExec()
{std::string cmd = gargv[0];bool ret = false;if(strcmp(cmd.c_str(), "cd") == 0){if(gargc == 2){char* str = gargv[1];if(strcmp(str, "~") == 0){ret = true;const char* home = GetHomePath().c_str();chdir(home);}else{ret = true;chdir(str);}}else if(gargc == 1){ret = true;chdir(GetHomePath().c_str());}else{//TODO}}else if(cmd == "echo"){if(gargc == 2){std::string args = gargv[1];if(args[0] == '$'){if(args[1] == '?'){ret = true;printf("%d\n",exit_code);exit_code = 0;}else{const char* name = &args[1];printf("%s\n",getenv(name));ret = true;}}else{ret = true;printf("%s\n",args.c_str());}}}return ret;
}bool ForkAndExec()
{pid_t id = fork();if(id < 0){perror("fork");return false;}else if(id == 0){//更改文件描述符指向的文件,从而让子进程只需要执行系统命令就可以达到重定向的功能if(redir == OUTPUT_REDIR){int output = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666);dup2(output, 1);}else if(redir == INPUT_REDIR){int input = open(filename.c_str(), O_RDONLY);dup2(input, 0);}else if(redir == APPEND_REDIR){int append = open(filename.c_str(), O_CREAT | O_WRONLY | O_APPEND);dup2(append, 1);}else{//nothing to do}execvp(gargv[0],gargv);exit(0);}else{int status = 0;pid_t rid = waitpid(id, &status, 0);if(rid > 0){//exit_code = WIFEXITED(status);exit_code = WEXITSTATUS(status);}}return true;
}

main.cc

#include"myshell.h"int main()
{char UserCommand[MAX];while(true){//打印命令行提示符PrintCommandLinePrompt();//获取用户输入的命令if(!GetUserCommand(UserCommand, sizeof(UserCommand)))continue;InitGlobal();//printf("echo %s\n",UserCommand);CheckRedir(UserCommand);//解析用户命令PraseUserCommand(UserCommand);//检查内建命令if(BuiltInCommandExec())continue;//执行命令,不能让父进程自己去执行程序替换,否则父进程程序替换之后就结束了,而shell是一个死循环软件,应该让子进程去执行ForkAndExec();}return 0;
}

Makefile

myshell:myshell.cc main.ccg++ -o $@ $^ -g
.PHONY:clean
clean:rm -f myshell

运行结果:

在这里插入图片描述

至此,我们简易版的mini_shell就完成了。觉得不错的小伙伴给个一键三连吧。

http://www.lryc.cn/news/609787.html

相关文章:

  • Assistant API——构建基于大语言模型的智能体应用
  • 在 C++ 中实现类似 Vue 3 的 Pinia 状态管理库
  • 反转字符串中的元音字母:Swift 双指针一步到位
  • 数据在内存中的存储深度解析
  • 【基础完全搜索】USACO Bronze 2019 January - 猜动物Guess the Animal
  • [找出字符串中第一个匹配项的下标]
  • OCR 精准识别验讫章:让登记与校验更智能
  • 嵌入式 - 数据结构:查找至双向链表
  • 用户管理——配置文件和命令
  • 【数据库】使用Sql Server创建索引优化查询速度,一般2万多数据后,通过非索引时间字段排序查询出现超时情况
  • Linux-Shell脚本基础用法
  • 【VSCode】 使用 SFTP 插件实现多服务器同步
  • 随机森林知识点整理:从原理到实战
  • 区块链基础之Merkle树
  • 数据结构——单向链表
  • CMakeLists.txt学习
  • 《JavaScript高级程序设计》读书笔记 35 - 代理捕获器、反射方法以及代理模式
  • React 19 + Next.js 15 中实现混合布局
  • React配置proxy跨域
  • ref和reactive的区别
  • 通过 Flink 和 CDC 从 Oracle 数据库获取增量数据,并将这些增量数据同步到 MySQL 数据库中
  • [GESP202306 四级] 2023年6月GESP C++四级上机题超详细题解,附带讲解视频!
  • Spring Boot + ShardingSphere 实现分库分表 + 读写分离实战
  • AWS VPC Transit Gateway 可观测最佳实践
  • 【物联网】基于树莓派的物联网开发【23】——树莓派安装SQLite嵌入式数据库
  • 16_OpenCV_漫水填充(floodFill)
  • Nginx vs Spring Cloud Gateway:限流功能深度对比与实践指南
  • Spring Cloud Gateway 实现登录校验:构建统一认证入口
  • 图片的放大缩小选择全屏
  • XSS的原型链污染1--原型链解释