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[]); // 包含环境变量表(非标准但常见)
-
argc
(参数个数)- 作用:记录命令行参数的个数,包括程序名称本身。
- 示例:执行
./a.out arg1 arg2
时,argc=3
(程序名+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)。
-
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
)
-
传递方式
- 命令行输入:参数以空格分隔,含空格的参数需用引号包裹(如
"hello world"
) 。 - IDE设置:在开发工具(如Visual Studio)中通过项目属性添加参数 。
- 应用场景:配置文件路径、运行模式(如调试模式
-d
)、输入输出文件名等 。
- 命令行输入:参数以空格分隔,含空格的参数需用引号包裹(如
-
实际应用示例(文件拷贝)
#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
即可完成文件复制。
三、环境变量表(environ
或envp
)
-
常见环境变量
PATH
:可执行文件搜索路径。HOME
:用户主目录。USER
:当前用户名。LANG
:系统语言设置 。
-
操作函数
- 获取变量值:
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)。
- 获取变量值:
-
环境变量表结构
- 每个元素是
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内有效,不会被继承!
本地变量的特点
-
作用域限制
- 仅当前Shell进程有效:本地变量仅在定义它的Shell进程内有效,无法被其他进程或子进程直接访问 。
- 不继承性:子进程无法继承父进程的本地变量(除非通过
export
转换为环境变量) 。
- 示例:在脚本中定义的本地变量,其他脚本或子Shell无法直接读取。
- 仅当前Shell进程有效:本地变量仅在定义它的Shell进程内有效,无法被其他进程或子进程直接访问 。
-
生命周期
- 临时性:本地变量随当前Shell进程的终止而销毁。若需持久化,需手动写入Shell配置文件(如
~/.bashrc
) 。
- 示例:在终端直接定义的变量(如
var=123
)关闭终端后失效。
- 临时性:本地变量随当前Shell进程的终止而销毁。若需持久化,需手动写入Shell配置文件(如
-
存储位置
- Shell内存中:本地变量直接存储在Shell进程的内存中,而非系统或用户配置文件中 。
- 查看方式:通过
set
命令查看所有本地变量(包括环境变量) 。
- Shell内存中:本地变量直接存储在Shell进程的内存中,而非系统或用户配置文件中 。
-
用途
- 脚本内部逻辑:常用于脚本中临时存储中间值、循环控制等。
- 环境隔离:避免全局污染,例如在脚本中临时修改
PATH
而不影响其他进程 。
-
定义与修改
- 直接赋值:通过
变量名=值
定义(注意等号两侧无空格)。 - 无需特殊命令:默认定义的变量为本地变量,除非显式用
export
提升为环境变量。
- 直接赋值:通过
接下来我们提出一个问题:
既然本地变量不会被继承,那么我们通过bash创建子进程,此时子进程通过echo获取到变量值,为什么它能获取到?
因为echo是内建命令!
Linux下常有两种命令:
- 常规命令:通过创建子进程来完成的;
- 内建命令:bash不创建子进程,而是由自己亲自执行,类似bash调用了自己写的,或者是系统提供的函数!
因此这里的echo也是内建指令,因为本地变量是直接保存在bash里面!所以这里没有创建子进程,而是通过接口直接在bash里面进行获取!
接下来我们给出deepseek定义的内建命令的概念:
Linux中的内建命令(Built-in Commands)是直接集成在Shell解释器内部的命令,执行时无需创建子进程,因此效率更高。它们通常用于Shell环境管理、变量操作、流程控制等场景。以下是内建命令的特点和常见示例:
内建命令的特点
- 无需外部程序:内建命令是Shell自身的一部分,无需通过
/bin
或usr/bin
中的外部程序执行。 - 高效执行:不创建子进程,执行速度快 。
- 影响当前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;
}
注意的是:使用该变量之前我们需要对其进行声明!