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

news2025/1/19 3:16:40

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;
        else
            lastcode = 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.coloradmin.cn/o/2224796.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

信息安全工程师(72)网络安全风险评估概述

前言 网络安全风险评估是一项重要的技术任务&#xff0c;它涉及对网络系统、信息系统和网络基础设施的全面评估&#xff0c;以确定存在的安全风险和威胁&#xff0c;并量化其潜在影响以及可能的发生频率。 一、定义与目的 网络安全风险评估是指对网络系统中存在的潜在威胁和风险…

《Python游戏编程入门》注-第3章3

《Python游戏编程入门》的“3.2.4 Mad Lib”中介绍了一个名为“Mad Lib”游戏的编写方法。 1 游戏玩法 “Mad Lib”游戏由玩家根据提示输入一些信息&#xff0c;例如男人姓名、女人姓名、喜欢的食物以及太空船的名字等。游戏根据玩家输入的信息编写出一个故事&#xff0c;如图…

基于SSM的汽车客运站管理系统【附源码】

基于SSM的汽车客运站管理系统&#xff08;源码L文说明文档&#xff09; 目录 4 系统设计 4.1 设计原则 4.2 功能结构设计 4.3 数据库设计 4.3.1 数据库概念设计 4.3.2 数据库物理设计 5 系统实现 5.1 管理员功能实现 5.1.1 管理员信息 5.1.2 车…

详细解读Movie Gen(2):个性化视频训练

Diffusion Models专栏文章汇总:入门与实战 前言:Meta最近重磅发布了视频生成30B的基础模型Movie Gen,长达93页的技术报告中干货满满,博主将详细解读Movie Gen的核心网络结构、个性化视频微调方法、视频编辑等方面。虽然大部分人没有直接预训练30B模型的机会,但是可以从中获…

C++游戏开发中的多线程处理是否真的能够显著提高游戏性能?如果多个线程同时访问同一资源,会发生什么?如何避免数据竞争?|多线程|游戏开发|性能优化

目录 1. 多线程处理的基本概念 1.1 多线程的定义 1.2 线程的创建与管理 2. 多线程在游戏开发中的应用 2.1 渲染与物理计算 3. 多线程处理的性能提升 3.1 性能评估 3.2 任务分配策略 4. 多线程中的数据竞争 4.1 数据竞争的定义 4.2 多线程访问同一资源的后果 4.3 避…

视频剪辑新手必备:四款热门电脑视频剪辑软件评测

现在真的是一个视频流量的时代&#xff0c;不得不说&#xff0c;我都已经开始刷视频小说了&#xff01;如果你和我一样&#xff0c;是个对电脑视频剪辑充满好奇的新手&#xff0c;那么你一定想知道哪款软件最适合我们这些初学者。今天&#xff0c;我就来和大家分享一下我使用过…

gin入门教程(10):实现jwt认证

使用 github.com/golang-jwt/jwt 实现 JWT&#xff08;JSON Web Token&#xff09;可以有效地进行用户身份验证,这个功能往往在接口前后端分离的应用中经常用到。以下是一个基本的示例&#xff0c;演示如何在 Gin 框架中实现 JWT 认证。 目录结构 /hello-gin │ ├── cmd/ …

医院信息化与智能化系统(10)

医院信息化与智能化系统(10) 这里只描述对应过程&#xff0c;和可能遇到的问题及解决办法以及对应的参考链接&#xff0c;并不会直接每一步详细配置 如果你想通过文字描述或代码画流程图&#xff0c;可以试试PlantUML&#xff0c;告诉GPT你的文件结构&#xff0c;让他给你对应…

详解Pectra升级:如何影响以太坊价值及利益相关者

Pectra很可能是最后几个会直接影响用户和ETH持有者的升级之一。 原文&#xff1a;Galaxy Research&#xff1b;编译&#xff1a;Golem&#xff1b;编辑&#xff1a;郝方舟 出品 | Odaily星球日报&#xff08;ID&#xff1a;o-daily&#xff09; 编者按&#xff1a;以太坊 Pectr…

【SpringCloud】 K8s的滚动更新中明明已经下掉旧Pod,还是会把流量分到了不存活的节点

系列文章目录 文章目录 系列文章目录前言一、初步定位问题二、源码解释1.引入库核心问题代码进一步往下看【这块儿算是只是拓展了&#xff0c;问题其实处在上面的代码】Nacos是如何实现的&#xff1f; 如何解决 总结 前言 背景&#xff1a; 使用了SpringCloudGateWay 和 Sprin…

C++学习路线(二十五)

常见错误总结 错误1&#xff1a;对象const问题 #include <iostream>class Man { public:void walk() {std::cout << "I am walking." << std::endl;} };int main() {const Man man;man.walk();return 0; } 原因是Man man是const对象 但是调用了…

大语言模型的Scaling Law【Power Low】

NLP-大语言模型学习系列目录 一、注意力机制基础——RNN,Seq2Seq等基础知识 二、注意力机制【Self-Attention,自注意力模型】 三、Transformer图文详解【Attention is all you need】 四、大语言模型的Scaling Law【Power Low】 文章目录 NLP-大语言模型学习系列目录一、什么是…

Stable Diffusion视频插件Ebsynth Utility安装方法

一、Ebsynth Utility制作视频的优势&#xff1a; 相比其他视频制作插件&#xff0c;Ebsynth Utility生成的视频&#xff0c;画面顺滑无闪烁&#xff0c;对显存要求相对不高。渲染速度也还可以接受。其基本过程为&#xff1a; 1、将参考视频分解为单个帧&#xff0c;并同时生成…

模型训练识别手写数字(二)

模型训练识别手写数字&#xff08;一&#xff09;使用手写数字图像进行模型测试 一、生成手写数字图像 1. 导入所需库 import cv2 import numpy as np import oscv2用于计算机视觉操作。 numpy用于处理数组和图像数据。 os用于文件和目录操作。 2. 初始化画布 canvas np.z…

GitHub下载参考

1.Git下载 Git下载https://blog.csdn.net/mengxiang_/article/details/128193219 注意&#xff1a;根据电脑的系统配置选择合适的版本&#xff0c;我安装的是64.exe的版本 2.Git右键不出现问题&#xff1a; Git右键不出现https://blog.csdn.net/ling1998/article/details/1…

Java项目实战II基于微信小程序的马拉松报名系统(开发文档+数据库+源码)

目录 一、前言 二、技术介绍 三、系统实现 四、文档参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末 一、前言 马拉松运动…

[SWPUCTF 2022 新生赛]py1的write up

开启靶场&#xff0c;下载附件&#xff0c;解压后得到&#xff1a; 双击exe文件&#xff0c;出现弹窗&#xff1a; 问的是异或&#xff0c;写个python文件来计算结果&#xff1a; # 获取用户输入的两个整数 num1 int(input("Enter the first number: ")) num2 int…

云渲染主要是分布式(分机)渲染,如何使用blender云渲染呢?

云渲染主要是分布式&#xff08;分机&#xff09;渲染&#xff0c;比如一个镜头同时开20-100张3090显卡的机器渲染&#xff0c;就能同时渲染20-100帧&#xff0c;渲染不仅不占用自己电脑&#xff0c;效率也将增加几十上百倍&#xff01; blender使用教程如下&#xff1a; 第一…

基于Django+python的车牌识别系统设计与实现(带文档)

项目运行 需要先安装Python的相关依赖&#xff1a;pymysql&#xff0c;Django3.2.8&#xff0c;pillow 使用pip install 安装 第一步&#xff1a;创建数据库 第二步&#xff1a;执行SQL语句&#xff0c;.sql文件&#xff0c;运行该文件中的SQL语句 第三步&#xff1a;修改源…

软件架构设计学习总结

概述&#xff1b; 如何描述软件架构&#xff1b; 架构的层次结构&#xff1b; 架构设计技能&#xff1a; 需求分析、业务架构、数据架构、应用架构、技术架构、开发架构设计&#xff1b; 层次框架设计&#xff1b; 集成与接口设计&#xff1b; 性能优化&#xff1b; 设计…