Linux相关概念和易错知识点(16)(Shell原理、进程属性和环境变量表的联系)

Shell原理及其模拟实现

认识进程exec系列函数、命令行参数列表、环境变量之后,我们可以尝试理解一下Shell的原理,将各方知识串联起来,让Shell跑起来才能真正理解这些概念。我会以模拟Shell执行的原理模拟一个Shell。途中配上相关讲解。

1.Shell进程的创建

Shell是用户启动后系统为我们启动的第一个进程,用于启动CLI程序,为后面我们的命令行操作做准备,在Linux中,这个Shell具体程序是bash。

bash在/usr/bin/bash下,每一个用户登陆后都会先在内存中创建PCB,再从硬盘中将代码和数据读到内存中来,就和其他进程一样。

2.环境变量的配置

我们从上面那张图中就能发现,bash并不是在每个用户的home目录下,而是在公共区域。同一个程序却能生成针对不同用户登陆的bash,这和环境变量的配置有直接关系。当创建bash读取硬盘数据时,bash会从登陆用户的home目录下读取.bash_profile和.bashrc,里面有环境变量配置的相关信息。但是注意,配置文件里面并不包含所有的环境变量,它是局部的,还有一些环境变量是从bash的父进程继承下来的或是后面生成的。

下面是配置文件里面的一些内容,这些环境变量都会被bash导入到内存中形成一份临时环境变量表,在代码或命令行中修改环境变量就是修改的内存中的环境变量,只要不改硬盘文件,就不会修改默认环境变量。

环境变量是进程中最基础的部分,所有进程启动时都要导入一份环境变量

对于Shell进程,方式有:父进程继承、读取配置文件、后续自动生成(HOME -> cwd -> PWD)

对于其它父子进程而言方式有:父进程继承

当然我们都可以手动添加环境变量。

我们要模拟Shell进程,这意味着我们要完成父进程继承、读取配置文件、后续自动生成。这显然超出了我们的能力,把上面的实现了基本上我们就写出了一个系统。但我们可以模拟普通进程的导入环境变量,拷贝一份环境变量表做模拟。

这里有个易错点,一定要注意。main函数的三个参数(int argc, char** argv, char** env)的env是一个局部变量,且只有我们写int main(int argc, char** argv, char** env)时env才会被导入到函数栈帧中(写了参数调用的main函数就不一样了),后续如果要用env这个变量就要自己手动传参。env里面就存着环境变量。但程序中还有一个变量char** environ,它是全局的,且我们只要extern之后就可以在任何地方调用。

我们最终采用environ方案。

对于这个模拟的Shell程序来讲,我们手动显式实现了一个环境变量表,而我们需要搞清楚这个程序本身还有一个环境变量,那才是该程序真正的环境变量表,我们environ本质就是从该程序的本身的变量表拷贝过来的。

3.对环境变量表、命令行参数列表、进程属性之间的关系解读

在我们的模拟代码中,环境变量表存在于Shell程序的全局区域,可以被任意调用,在进程角度看来环境变量表属于代码和数据部分,本质上是存在于mm_struct管理的映射区域的,这和进程属性直接存在PCB中有本质区别。命令行参数argc和argv都是属于代码和数据的。

(1)对mm_struct进一步解释

进程的mm_struct里面管理进程地址空间及其映射的一部分物理空间,这部分空间里面存的是进程的代码和数据,而除了代码和数据是不会存在于进程地址空间里的(比如进程属性),因此我们在栈区、堆区等地方是找不到进程的属性struct mm_struct* mm或者是cwd的,因为它们是PCB的属性,直接存在物理内存中,没有页表映射。操作系统直接管理,用户永远无法也无需直接获取它们的地址。

综上,进程属性本质上是属于PCB,由Kernel直接管理的,我们永远拿不到它们的物理地址。而相比之下,环境变量属于进程的代码和数据,存在于mm_struct管理的物理空间,对应进程地址空间的位置是高于栈区的。

(2)环境变量PWD和进程属性cwd的关系

在这里我们需要了解一些后续生成的环境变量是如何来的。其中最重要的就是PWD和cwd的关系!

当读取配置文件后,HOME被配置好了,之后cwd会使用chdir修改自己的当前工作目录,再之后会借助cwd里面的数据使用putenv这个函数创建一个新的环境变量PWD。PWD环境变量是借助cwd这个进程属性来初始化的。

注意,只有Shell进程创建时会干这事。其余进程创建时都会使用写时拷贝的技术继承父进程的环境变量表,不会有任何读取文件或是自动添加环境变量操作。

3.打印命令行提示符

命令行提示符的格式是:[用户名(USER)@主机名(HOSTNAME) 当前路径(PWD)]$/#

这部分主要就是字符串处理相关的知识。获取的环境变量是从我们自己拷贝得到的环境变量表中取,而不是在该进程原本的环境变量表中取。

注意C/C++混编的情况下string有着避免野指针的优势,我们可以在函数里定义string,然后返回它,这样做不会出现和数组那样的野指针问题,会自动初始化一个新的string。当然我们要注意C/C++之间的转换,如c_str()这种接口要熟悉

4.获取并分析用户命令

这是一个非常容易犯错误的地方。全局的argv和argc、获取用户命令的CommandBuffer每次都要清空数据,之后用fgets安全读入一行,strtok分割字符串并存入argv中。

注意回车符也会被读入,读入后的下一个字符被标记为'\0',读取结束,因此我们要手动处理字符串中有回车的情况,将它改成'\0'。

5.执行命令

(1)子进程执行命令

借助execvp和argv,我们可以实现子进程执行程序,父进程wait子进程。这样做的好处是子进程执行失败完全不会影响父进程的安全性,如果直接让父进程执行所有命令,那么如果出了一个较严重的错误,父进程就直接被挂掉了,这显然不是我们希望看到的。

(2)内建命令

内建命令用于修改当前进程环境变量的值或者要访问只有该进程能访问的数据。如echo能访问本地变量,cd要修改本进程的环境变量。因为子进程执行指令没办法访问父进程的数据,进程之间的独立性决定了这类命令只能直接由父进程执行。

思路就是穷举法,将需要的命令手动在父进程处理。注意chdir修改的是进程属性cwd,进程属性和环境变量之间在进程运行时是各改个的,只有在Shell创建时的初始化时才存在关联。它们之间的同步需要手动维护。

我们从一个更加底层的角度上来想,进程属性属于PCB、系统直接管理对象,而环境变量是程序代码和数据的一部分。当修改一边时,另一边理所应当保持独立,只不过Shell内部维护导致我们大部分看上去是同步的,但事实上修改cwd后PWD依然不变,cwd和PWD都只是数据而已,并没什么大不了的,因此我们需要手动putenv。 

在有的时候会发现存在不同步的情况。比如在Shell进程内切换用户,环境变量表会变,因为切换用户会重新读取配置文件,但进程始终是同一个,进程创建者不变。

注意getenv和putenv我们都要自己实现,因为系统给我们的这些函数都只会到进程自带的环境变量表中查找,我们要实现到自己的环境变量中查找、添加等,都要自己写。

最后可以使用全局的lastcode存储退出码,同样使用内建命令处理echo来获得退出码,底层逻辑是一样的,也很简单,这里就不再讲述了。唯一需要注意的是当没有找到命令时,错误信息和退出码是要在exec函数后面更新的,exec函数执行成功就不会执行下面的语句,执行失败就要。

总结:

我们通过Shell的原理和模拟实现主要是为了搞清进程属性和环境变量表、命令行参数表的关系。我们发现,一个进程 = PCB + 代码和数据,PCB可以管理这些代码和数据。其中进程属性直接是PCB的成员,被系统直接管理。而环境变量表、退出码、命令行参数列表本质都是存在代码和数据中的,也可以叫程序的上下文中。正是这样的差异导致了环境变量和进程属性的独立性,也帮助我们理解环境变量表是如何传给子进程的,命令行参数从读取到传给argv的过程是怎样的,以及退出码是如何读取到的。整个Shell的知识点都被串联起来了,很值得我们消化。

全部代码:


#include <sys/wait.h>
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <unistd.h>
using namespace std;const int basenum = 100;
const int basesize = 1024;//命令行参数列表和环境变量表放在Shell的代码和数据中
char* genv[basenum];
char* gargv[basesize];
int gargc;//存储即将更新的PWD,维护环境变量的更新,保持cwd和PWD的同步
int posPWD = 0;//随时存储PWD的位置,方便后续找到
char newPWD[basesize];int lastcode;//存储退出码void InitEnv()//初始化环境变量表,一般子进程是这样继承的,Shell进程是从文件读配置文件的,这里简略一点
{extern char** environ;for(int curi = 0; environ[curi]; curi++){genv[curi] = (char*)malloc(strlen(environ[curi]) + 1);//不用sizeof,它会按指针大小算memcpy(genv[curi], environ[curi], strlen(environ[curi]) + 1);}
}string GetEnv(string oneEnv)//用string防止不必要全局字符串,值拷贝避免野指针
{for(size_t i = 0; i < basenum; i++){for(size_t j = 0; j < oneEnv.size(); j++){if(oneEnv[j] != genv[i][j])//有不一样的就说明不匹配break;if(j == oneEnv.size() - 1)//全部相等{string User;for(size_t k = oneEnv.size() + 1; genv[i][k]; k++)User += genv[i][k];posPWD = i;//存储PWD信息,方便后续找到return User;}}}return nullptr;
}void PrintCommandLine()
{string GetUser = GetEnv("USER");string GetPwd = GetEnv("PWD");string GetHostName = GetEnv("HOSTNAME");if((GetPwd = string(GetPwd.begin() + GetPwd.rfind('/'), GetPwd.end())).size() > 1)//表达式的返回值是string,如果返回值不是根目录,都要把/删掉GetPwd.erase(0, 1);printf("[%s@%s %s]%c ", GetUser.c_str(), GetHostName.c_str(), GetPwd.c_str(), GetUser == "root" ? '#' : '$');
}void GetCommandLine(char* CommandBuffer)//使用值拷贝string防止出现野指针
{memset(CommandBuffer, '\0', basesize);//每次都初始化读取用的字符串fgets(CommandBuffer, basesize, stdin);CommandBuffer[strlen(CommandBuffer) - 1] = '\0';
}void ParseCommandLine(char* CommandBuffer)
{gargc = 0;//每次解析命令前先把上一个命令的信息删除memset(gargv, '\0', basesize);if(strlen(CommandBuffer) == 0)return;gargv[gargc++] = strtok(CommandBuffer, " ");while((bool)(gargv[gargc++] = strtok(nullptr, " ")));gargc--;
}void ExecuteCommand()//没有指令的情况走不到这个函数
{pid_t ret = fork();if(ret == 0){execvpe(gargv[0], gargv, genv);printf("-bash: %s: command not found\n", gargv[0]);lastcode = 1;}else{int status = 0;waitpid(ret, &status, 0);if(!WIFEXITED(status))lastcode =  1;elselastcode = WEXITSTATUS(status);}
}bool CheckAndExecBuiltCommand()
{if(gargc == 0)//如果没有指令,直接进行下一轮循环,通过return true省的走下一个函数return true;if(strcmp("cd", gargv[0]) == 0){if(gargc == 2){memset(newPWD, '\0', basesize);chdir(gargv[1]);snprintf(newPWD, basesize, "PWD=%s", gargv[1]);genv[(GetEnv("PWD"), posPWD)] = newPWD;//每次调用GetEnv都会刷新posPWD的位置,用逗号表达式的特性实现lastcode = 0;return true;}return false;}if(strcmp("export", gargv[0]) == 0){if(gargc == 2){int curi = 0;while(genv[curi]) curi++;genv[curi] = (char*)malloc(strlen(gargv[1]) + 1);memcpy(genv[curi], gargv[1], strlen(gargv[1]) + 1);lastcode = 0;return true;}return false;}if(strcmp("env", gargv[0]) == 0){if(gargc == 1){for(int i = 0; genv[i]; i++)printf("%s\n", genv[i]);lastcode = 0;return true;}return false;}if(strcmp("echo", gargv[0]) == 0){if(gargc == 2 && strcmp("$?", gargv[1]) == 0)printf("%d\n", lastcode);lastcode = 0;return true;}return false;
}int main()
{char CommandBuffer[basesize] = { 0 };InitEnv();while(true){PrintCommandLine();//打印命令行提示符GetCommandLine(CommandBuffer);//读取命令ParseCommandLine(CommandBuffer);//解析命令至argc和argv中if(CheckAndExecBuiltCommand()) continue;ExecuteCommand();}return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Mybatis-03.入门-配置SQL提示

一.配置SQL提示 目前的Springboot框架在mybatis程序中编写sql语句并没有给到任何的提示信息&#xff0c;这对于开发者而言是很不友好的。因此我们需要配置SQL提示。 配置SQL提示 这样再去写SQL语句就会有提示了。 但是会发现指定表名时并没有给出提示。这是因为&#xff1a…

用kali入侵 DarkHole_2测试

进入kali系统调出root交互式界面 netdiscover -r 000.000.000.000/24 -------局域网探测IP工具 nmap 设备端口扫描 发现两个攻击点一个是80端口的Http 一个是22端口的ssh 发现有许多GIT文件 可能会出现git源码泄露 使用githack URL 命令还原git源文件 打开面板控制命令行 输入…

2024数学分析【南昌大学】

计算极限 lim ⁡ n → ∞ 2024 n ( 1 − cos ⁡ 1 n 2 ) n 3 1 + n 2 − n \mathop {\lim }\limits_{n \to \infty } \frac{{\sqrt[n]{{2024}}\left( {1 - \cos \frac{1}{{{n^2}}}} \right){n^3}}}{{\sqrt {1 + {n^2}} - n}} n→∞lim​1+n2 ​−nn2024 ​(1−cosn21​)n3​ …

【Vulnhub靶场】DC-4

DC-4靶场下载地址https://www.five86.com/downloads/DC-4.zip 本机IP&#xff1a;192.168.118.128 靶机IP&#xff1a;192.168.118.0/24 信息收集 扫描主机存活&#xff0c;扫描端口&#xff0c;扫描服务 第一步扫描出主机ip为192.168.118.141 nmap -sP 192.168.118.0/24 nm…

通过rancher2.7管理k8s1.24及1.24以上版本的k8s集群

目录 初始化实验环境 安装Rancher 登录Rancher平台 通过Rancher2.7管理已存在的k8s最新版集群 文档中的YAML文件配置直接复制粘贴可能存在格式错误&#xff0c;故实验中所需要的YAML文件以及本地包均打包至网盘. 链接&#xff1a;https://pan.baidu.com/s/1oYX4eGoBtW_R-7i…

canvas-editor首行缩进

canvas-editor中渲染部分的源码都在Draw.ts里&#xff0c;能找到computeRowList方法中并没有实现首行缩进相关的逻辑&#xff0c;但是实现了element.type ElementType.TAB的缩进&#xff0c;如图&#xff1a; 因此我们可以基于tab进行首行缩进的逻辑编写&#xff0c;在main.ts…

GoogleChrome和Edge浏览器闪屏问题

GoogleChrome和Edge浏览器闪屏问题 文章目录 GoogleChrome和Edge浏览器闪屏问题 买了电脑半年, GoogleChrome和edge浏览器出现了一个令人头疼的问题–闪屏, 就是打开这两个浏览器之后, 就会出现电脑屏幕一闪一闪的, 过一会就看不见了, 跟黑夜里的闪电一样, 遇到这种情况我都会直…

《 C++ 修炼全景指南:十七 》彻底攻克图论!轻松解锁最短路径、生成树与高效图算法

摘要 1、引言 1.1、什么是图&#xff1f; 图&#xff08;Graph&#xff09;是计算机科学和离散数学中一种重要的数据结构&#xff0c;用来表示一组对象之间的关系。一个图由顶点&#xff08;也称为节点&#xff0c;Vertex&#xff09;和边&#xff08;Edge&#xff09;组成。…

【Python爬虫实战】Selenium自动化网页操作入门指南

#1024程序员节&#xff5c;征文# &#x1f308;个人主页&#xff1a;易辰君-CSDN博客 &#x1f525; 系列专栏&#xff1a;https://blog.csdn.net/2401_86688088/category_12797772.html ​ 目录 前言 一、准备工作 &#xff08;一&#xff09;安装 Selenium 库 &#xff0…

VMware Workstation Pro 16 搭建 android-x86过程问题罗列

1、搭建完成后&#xff0c;app安装显示软件包解析失败或app打开闪退 测试了android-x86_64-9.0-r2这个版本&#xff0c;发现按照网上部署arm库方法没有成功&#xff0c;最后使用android-x86-7.1-r5版本解决了问题 2、android-x86网络连接不通 虚拟机网络设置选择桥接模式 安…

低代码平台如何通过AI赋能,实现更智能的业务自动化?

引言 随着数字化转型的加速推进&#xff0c;企业在日常运营中面临的业务复杂性与日俱增。如何快速响应市场需求&#xff0c;优化流程&#xff0c;并降低开发成本&#xff0c;成为各行业共同关注的核心问题。低代码平台作为一种能够快速构建应用程序的工具&#xff0c;因其可视化…

Springboot 使用EasyExcel导出Excel文件

Springboot 使用EasyExcel导出Excel文件 Excel导出系列目录&#xff1a;引入依赖创建导出模板类创建图片转化器 逻辑处理controllerservice 导出效果遗留问题 Excel导出系列目录&#xff1a; 【Springboot 使用EasyExcel导出Excel文件】 【Springboot 使用POI导出Excel文件】 …

如何提高游戏的游戏性

改进游戏玩法是一个动态的过程&#xff0c;需要深入了解是什么让玩家保持参与、挑战和兴奋&#xff0c;以获得更多。优秀游戏的核心是平衡——乐趣和难度的无缝结合&#xff0c;让玩家在不感到沮丧的情况下为自己的技能感到奖励。微调这种平衡通常涉及调整难度曲线&#xff0c;…

HTML+JavaScript案例分享: 打造经典俄罗斯方块,详解实现全过程

在本文中,我们将深入探讨如何使用 JavaScript 实现经典的俄罗斯方块游戏。俄罗斯方块是一款广为人知的益智游戏,通过操纵各种形状的方块,使其在游戏区域内排列整齐,以消除完整的行来获得分数。 效果图如下: 一、游戏界面与布局 我们首先使用 HTML 和 CSS 来创建游戏的界面…

力扣283-- 移动零

开始做梦的地方 力扣283 &#xff1a; 给定一个数组 nums&#xff0c;编写一个函数将所有 0 移动到数组的末尾&#xff0c;同时保持非零元素的相对顺序。请注意 &#xff0c;必须在不复制数组的情况下原地对数组进行操作。 何解&#xff1f; 1&#xff0c;暴力枚举&#xff1a…

Vue前端开发2.2 数据绑定

文章目录 一、初识数据绑定&#xff08;一&#xff09;数据绑定概述&#xff08;二&#xff09;数据绑定构成1、定义数据2、输出数据 &#xff08;三&#xff09;数据绑定案例演示1、创建单文件组件2、切换页面显示组件 &#xff08;四&#xff09;将Vue引入HTML页面中1、概述2…

高效文本编辑与导航:Vim中的三种基本模式及粘滞位的深度解析

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

sheng的学习笔记-AI基础-正确率/召回率/F1指标/ROC曲线

AI目录&#xff1a;sheng的学习笔记-AI目录-CSDN博客 分类准确度问题 假设有一个癌症预测系统&#xff0c;输入体检信息&#xff0c;可以判断是否有癌症。如果癌症产生的概率只有0.1%&#xff0c;那么系统预测所有人都是健康&#xff0c;即可达到99.9%的准确率。 但显然这样的…

Linux中级(DNS域名解析服务器)

一。产生原因1.IP地址&#xff1a;是互联网上计算机唯一的逻辑地址&#xff0c;通过IP地址实现不同计算机之间的相互通信&#xff0c;每台联网计算机都需要通过IP地址来互相联系和分别&#xff0c;但由于IP地址是由一串容易混淆的数字串构成&#xff0c;人们很难记忆所有计算机…

MySql中的锁的分类

锁的分类 MySQL锁可以按模式分类为&#xff1a;乐观锁与悲观锁。按粒度分可以分为全局锁、表级锁、页级锁、行级锁。按属性可以分为&#xff1a;共享锁、排它锁。按状态分为&#xff1a;意向共享锁、意向排它锁。按算法分为&#xff1a;间隙锁、临键锁、记录锁。 二、全局锁、表…