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

Linux第十四节 — 环境变量和进程地址空间

一、并发和并行

首先,我们引入两个概念:

  • 并发:多个进程在多个CPU下分别运行,称为并发;
  • 并行:多个进程在1个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称为并行;

接下来我们介绍一种并行情况下,CPU基于进程切换和时间片轮转的调度算法:

        每个进程执行固定的时间片的时间(通常为10ms~100ms),此时若该进程未被执行完,则会被强制挂起,然后到等待队列当作排队!

        此时我们会有一个疑问:如果该进程的优先级为60,那么执行完时间片后此时再次排队还是轮到它,会被一直调用吗?

        答案:不会!因为对于进程调用结束后,此时会去另一个等待队列当中排队,当运行队列中所有的进程都被执行结束后,才会再次调用该进程!

CPU当作存在不少的寄存器!

接下来我们拓展两个方面的问题:

  • 为什么函数的返回值,会被外界拿到呢?

实际上我们是将返回值保存到寄存器中,然后再从寄存器中取数据!

在32位环境下,返回值通常存放在eax寄存器,64位则是eax和edx。

  • 系统如何得知我们的进程当前执行到哪行代码了?

这里因为CPU内有程序计数器pc,也叫eip:作用是记录当前进程执行指令的下一行指令的地址!

程序计数器本质是一个存储指令地址的寄存器,它始终指向下一条待执行的机器指令地址。

接下来我们介绍一些常见的寄存器:

这些寄存器的作用是什么?

        答案:提高效率,将进程相关的高频数据存放到寄存器中!(距离CPU越近,存储的效率越高!)

也就是说寄存器存放的是和进程相关的数据!(这些数据可能随时被CPU进行修改或者访问的!)

因此,可以得出相关结论:CPU中存放的是进程的临时数据,也就是进程的上下文!

什么是上下文?(deepseek)

进程的上下文数据主要包括CPU寄存器中的临时值,具体分为:

  • 通用寄存器(如eax、erx):存储运算产生的中间数据 。
  • 指令相关寄存器
    • eip(扩展指令指针):指向下一条待执行指令的地址(即程序计数器PC) 。
    • ir(指令寄存器):暂存当前正在执行的指令 。
  • 状态寄存器(如PS程序状态字):记录处理机的硬件状态(如中断标志、运算结果状态等)。
  • 堆栈指针(如ESP):指向当前栈顶位置 。

此外,上下文还包含与进程相关的内存映射信息(如页表)、打开的文件描述符等系统级数据。

        需要注意:进程在从CPU离开的时候,要将自己的上下文数据保存好,甚至带走!(否早下一个进程来执行的时候会将代码和数据进行覆盖!)

保存的目的都是为了将来再次调用的时候恢复数据!

因此进程切换的时候包含两个重要步骤:

  • 保存上下文;
  • 回复上下文;

那么上下文是保存到哪里的?

实际上进程的上下文是保存在对应的PCB里面!(暂时可以这样认为)

就例如上面的,此时task_struct里面包含一个结构体对象reg_info,用来包含上下文的信息;

上下文存放到哪里?(deepseek)

上下文数据存储在进程的PCB(进程控制块)中。例如:

  • Linux的PCB是task_struct结构体,其中的tss_struct(任务状态段)专门保存寄存器上下文 。
  • 上下文数据在进程被切换出CPU时会被更新到PCB,下次调度时再加载到CPU寄存器以恢复执行 。
     

二、环境变量引入

系统指令搜索路径

        当我们运行我们的程序的时候,我们一般需要./a.out来进行运行,其中需要标注当前程序的所在路径,但是为什么系统的指令,例如ls,pwd等不需要路径呢?

        这是因为系统的只能都被包含在了系统路径中!

        例如当我们通过echo $PATH即可查看系统路径,其中上面的路径通过:来区分;

        也就是当,当我们执行指令的时候,系统会在PATH查找对应的路径,如果不存在此时就需要自己添加路径!

如何修改环境变量?

如果我们直接进行:PATH=路径;

此时相当于全部进行覆盖!

但是如果我们此时:

PATH=$PATH:路径名 

此时会在后面新增!(注意PATH左右不能+空格!)

上面我们更改的环境变量是内存级别的,也就是说如果我们不小心覆盖了环境变量,重启一下系统即可!

export 的作用:

  • 核心功能:将变量设置为环境变量,使该变量对当前 Shell 进程及其所有子进程可见。
  • 对比:如果直接写 PATH=$PATH:hello(不加 export),变量 PATH 仅对当前 Shell 进程有效,子进程(如新启动的终端、脚本等)无法继承这个修改。

接下来我们介绍几个其他的环境变量相关的指令:

env:显示当前环境下所有的环境变量!

  • HOSTNAME:表示当前环境下的主机名;
  • HISTSIZE:存储的历史指令的条数(1000条);(history可以查看历史所用到的指令)
  • USER:代表当前的账户;
  • LS_COLORS:代表当前的配色方案;
  • LOGNAME:表示当前的登录用户;
  • OLDPWD: 表示当前路径的上一个路径(cd OLDPWD相当于cd ~);

当我们从root切换到普通用户的时候,USER和LOGNAME也会变成普通用户!

获取环境变量的方式

如果我们不想通过指令env查看获取环境变量,还有没有其他方式?

答案:我们可以使用系统提供的环境变量的系统调用接口来获取!

getenv() --- 需要包含头文件#include<stdlib.h>;

例如下面所示代码:

接下来我们可以给出定义:什么是环境变量?

        环境变量是系统提供的一组name = value形式的变量,不同的环境变量有不同的用户,通常具有全局属性;

引入:命令行参数

我们在命令行输入的参数,实际上对于bash来说是一个字符串!然后通过空格进行分割!

命令行参数以空格为分隔符!

当前对于命令行的两个参数:

  • argc表示参数的个数,也就是字符指针数组中的元素的个数;
  • argv[]表示一个字符指针数组;

以下面的代码为例,我们做一个例子:

这里我们./mycmd -a的本质和ls -a的本质是一样的! 

因此命令行参数的一个重要功能:为指令、工具、选项等提供命令行选项的支持!

那么main除了上面这两个参数,还有其他参数吗?

答案是:有的,main函数的第三个参数是char* env;

因此,当我们执行一个程序的时候,最重要的是里面存在两张向量表:

  • 命令行参数表;
  • 环境向量表;

下面是来自deepseek的解释:

一、main函数参数

main函数的标准形式有两种,其中第二种支持命令行参数和环境变量表:

int main(int argc, char *argv[]); // 命令行参数表

int main(int argc, char *argv[], char *envp[]); // 包含环境变量表(非标准但常见)

  1. argc(参数个数)

    • 作用:记录命令行参数的个数,包括程序名称本身。
    • 示例:执行./a.out arg1 arg2时,argc=3(程序名+2个参数) 。
  2. argv(参数向量)

    • 结构:指针数组,每个元素指向一个命令行参数的字符串。
    • 特点
      • argv[0]:程序名称(如./a.out)。
      • argv[1]argv[argc-1]:用户输入的参数。
      • argv[argc]NULL指针,表示参数结束。
    • 示例

      // 打印所有命令行参数 for (int i = 0; i < argc; i++) { printf("argv[%d] = %s\n", i, argv[i]); }

      执行./a.out hello world输出:

      argv[0] = ./a.out argv[1] = hello argv[2] = world ``` [3](@ref) [4](@ref)。

  3. envp(环境变量表,可选)

    • 作用:存储程序运行时的环境变量,形式为name=value字符串数组,以NULL结尾。
    • 访问方式
      • 通过main的第三个参数char *envp[](非标准扩展)。
      • 全局变量extern char **environ(POSIX标准)。
    • 示例:遍历环境变量表:

      extern char **environ; for (int i = 0; environ[i] != NULL; i++) { printf("%s\n", environ[i]); } ``` [5](@ref) [7](@ref)。


二、命令行参数表(argv

  1. 传递方式

    • 命令行输入:参数以空格分隔,含空格的参数需用引号包裹(如"hello world") 。
    • IDE设置:在开发工具(如Visual Studio)中通过项目属性添加参数 。
       
    • 应用场景:配置文件路径、运行模式(如调试模式-d)、输入输出文件名等 。
       
  2. 实际应用示例(文件拷贝)

    #include <stdio.h> int main(int argc, char *argv[]) { if (argc != 3) { printf("用法:%s 源文件 目标文件\n", argv[0]); return -1; } FILE *src = fopen(argv[1], "r"); FILE *dst = fopen(argv[2], "w"); // 文件拷贝逻辑... fclose(src); fclose(dst); return 0; }

    执行./copy input.txt output.txt即可完成文件复制。


三、环境变量表(environenvp

  1. 常见环境变量

    • PATH:可执行文件搜索路径。
    • HOME:用户主目录。
    • USER:当前用户名。
    • LANG:系统语言设置 。
       
  2. 操作函数

    • 获取变量值char *getenv(const char *name)

      char *path = getenv("PATH"); printf("PATH: %s\n", path); ``` [5](@ref) [7](@ref)。

    • 设置/修改变量int setenv(const char *name, const char *value, int overwrite)

      setenv("MY_VAR", "123", 1); // 设置或覆盖环境变量 ``` [5](@ref) [7](@ref)。

  3. 环境变量表结构

    • 每个元素是name=value格式的字符串。
    • 示例输出:

                PATH=/usr/bin:/bin HOME=/home/user USER=alice ``` [5](@ref) [7](@ref)。

        所以当我们运行程序的时候,不仅仅是将程序加载到内存中,更重要的是当我们调用main函数的时候,有人将命令行参数表和环境变量表,这两张表传递进来!

        接下来我们可能会发现一种现象:当我们在执行程序之前,通过env指令就可以查看当前的环境变量,执行程序后也可以通过main函数进行查看环境变量,并且两种途径查询得到的环境变量的结果是一样的!

因此我们可以得出一个结论:

  •         我们所运行的进程,都是子进程,bash本身在启动的时候,会从操作系统的配置文件中读取环境变量的信息,子进程会继承父进程交给我们的环境变量!
  •         因此,一定我们定义好了环境变量,子进程会继承我们之前定义的环境变量,这也解释了为什么环境变量具有全局属性!

        环境变量也算数据,当我们创建子进程之后,如果通过子进程对环境变量进行修改,此时不能影响父进程!(写时拷贝);

如何定义一个环境变量?

export MYENV="hello"

直接通过关键字export即可!

如何取消一个环境变量?

unset MYENV="hello"

通过关键字unset即可取消!

引入本地变量和内建命令

什么是本地变量?

我们可以直接在命令行对其进行定义:

如果我们想要查询系统中所有的变量(包含本地变量和环境变量),我们应该怎么做?

通过指令set即可!(set内包含各种本地变量,可用于bash命令行的指令的格式输入)

本地变量的特点:不会被子进程继承!只在本bash内部有效!

将本地变量导成环境变量:

export MYVAL

这里的MYVAL是本地变量;

本地变量的特点:本地变量只会在本BASH内有效,不会被继承!

本地变量的特点

  1. 作用域限制

    • 仅当前Shell进程有效:本地变量仅在定义它的Shell进程内有效,无法被其他进程或子进程直接访问 。
       
    • 不继承性:子进程无法继承父进程的本地变量(除非通过export转换为环境变量) 。
       
    • 示例:在脚本中定义的本地变量,其他脚本或子Shell无法直接读取。
  2. 生命周期

    • 临时性:本地变量随当前Shell进程的终止而销毁。若需持久化,需手动写入Shell配置文件(如~/.bashrc) 。
       
    • 示例:在终端直接定义的变量(如var=123)关闭终端后失效。
  3. 存储位置

    • Shell内存中:本地变量直接存储在Shell进程的内存中,而非系统或用户配置文件中 。
       
    • 查看方式:通过set命令查看所有本地变量(包括环境变量) 。
  4. 用途

    • 脚本内部逻辑:常用于脚本中临时存储中间值、循环控制等。
    • 环境隔离:避免全局污染,例如在脚本中临时修改PATH而不影响其他进程 。
  5. 定义与修改

    • 直接赋值:通过变量名=值定义(注意等号两侧无空格)。
    • 无需特殊命令:默认定义的变量为本地变量,除非显式用export提升为环境变量。

接下来我们提出一个问题:

既然本地变量不会被继承,那么我们通过bash创建子进程,此时子进程通过echo获取到变量值,为什么它能获取到?

因为echo是内建命令!

Linux下常有两种命令:

  • 常规命令:通过创建子进程来完成的;
  • 内建命令:bash不创建子进程,而是由自己亲自执行,类似bash调用了自己写的,或者是系统提供的函数!

        因此这里的echo也是内建指令,因为本地变量是直接保存在bash里面!所以这里没有创建子进程,而是通过接口直接在bash里面进行获取!

接下来我们给出deepseek定义的内建命令的概念:

        Linux中的内建命令(Built-in Commands)是直接集成在Shell解释器内部的命令,执行时无需创建子进程,因此效率更高。它们通常用于Shell环境管理、变量操作、流程控制等场景。以下是内建命令的特点和常见示例:

内建命令的特点

  1. 无需外部程序:内建命令是Shell自身的一部分,无需通过/binusr/bin中的外部程序执行。
  2. 高效执行:不创建子进程,执行速度快 。
  3. 影响当前Shell环境:例如修改工作目录(cd)或设置环境变量(export)等操作会直接影响当前Shell进程 。

        如果上面的代码是实现bash的接口,那么对于其他指令我们需要通过fork创建子进程来完成,但是对于cd指令,我们可以直接通过在函数体内调用chdir来实现!(即cd指令是bash进程自己实现的内置命令!) 

        除了上面两种获取环境变量的方法,我们还可以通过第三方变量environ来获取!

#include <stdio.h>
int main(int argc, char *argv[])
{
 extern char **environ;
 int i = 0;
 for(; environ[i]; i++){
 printf("%s\n", environ[i]);
 }
 return 0;
}

注意的是:使用该变量之前我们需要对其进行声明!

 


http://www.kler.cn/a/561172.html

相关文章:

  • Mysql 主从集群同步延迟问题怎么解决?
  • JVM之JVM的组成
  • 从零开始的网站搭建(以照片/文本/视频信息通信网站为例)
  • 安全面试3
  • uni-app 开发app 时 ios上传图片失败的问题
  • 解读 Uniapp: 从入门到精通的底层原理
  • 大语言模型:从开发到运行的深度解构
  • C语言(13)------------>do-while循环
  • ros面试准备
  • STM32基础篇(三)------滴答定时器
  • [Android]App生命周期
  • 渗透小记--Docker Registry未授权访问
  • java23种设计模式-外观模式
  • 使用Python爬虫获取淘宝商品详情:API返回值说明与案例指南
  • Python游戏编程之赛车游戏6-5
  • 一文讲解Redis中和本地缓存之间的一致性问题及区别等
  • Spring Boot:Java开发的神奇加速器(一)
  • JSON Web Token在登陆中的使用
  • Lua C API :使用 lua_tonumber 函数从 Lua 栈中提取数值
  • 首届国际文化科技与未来创新发展大会:蓝海彤翔CTO穆凯辉出席并签约