环境变量:
概念:指在操作系统中用来指定操作系统运行环境的一些参数,如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但 是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。
常见环境变量:
PATH
: 定义了系统查找命令的目录列表。当你输入 ls
时,系统会按照 PATH
中的目录顺序去查找名为 ls
的可执行文件。
HOME
: 当前用户的家目录路径,例如 /home/username
。
USER
或 USERNAME
: 当前登录的用户名。
SHELL
: 当前用户使用的默认 shell 程序路径,例如 /bin/bash
。
PWD
: 当前工作目录的路径。
查看环境变量的方法:
echo $NAME //NAME:你的环境变量名称
printenv //显示所有环境变量
设置环境变量(临时):
在终端中直接使用 export
命令设置的变量是临时的,只在当前 shell 会话及其子进程中有效。关闭终端或开启新窗口后就会失效。
语法: export VARIABLE_NAME=value(注意:
等号两边不能有空格。值如果包含空格,需要用单引号或双引号括起来。)
示例:
# 设置一个变量 MY_VAR
export MY_VAR="Hello World"
# 查看是否设置成功
echo $MY_VAR
# 设置 PATH,注意通常是在原有 PATH 上追加
export PATH=$PATH:/my/custom/path
取消设置环境变量(临时):使用unset命令
语法:unset VARIABLE_NAME
示例:
unset MY_VAR
测试HOME差异:在root用户和普通用户中分别使用echo $HOME;root
用户和普通用户的 HOME
变量值是不同的。区别如下
在root用户下执行echo $HOME命令时会显示/root,root
用户的家目录是 /root
,这是一个特殊目录,只有 root
用户有权限访问。/root
通常用于存放 root
用户的个人配置文件。
而在普通用户(假设普通用户名为sanye)下执行echo $HOME命令时会显示/home/sanye。普通用户的家目录是 /home/用户名
(如 /home/sanye
)。普通用户只能访问自己的家目录,不能访问 /root
或其他用户的家目录(除非有权限)。
和环境变量相关的命令:
-
echo: 显示某个环境变量值
-
export: 设置一个新的环境变量
-
env: 显示所有环境变量
-
unset: 清除环境变量 HOME 的关系
-
set: 显示本地定义的shell变量和环境变量
本地变量:在 Linux Shell(如 Bash)中,本地变量是仅在当前 Shell 进程内部有效的变量,不会被继承到子进程。它们通常用于临时存储数据、控制脚本逻辑或函数内部操作。
本地变量与环境变量的区别:
定义方式:
# 本地变量(仅当前 Shell 有效)
local_var="I am local"
# 环境变量(可被子进程继承)
export global_var="I am global"
常见用途:
(1)存储临时数据
count=0 # 本地变量
for file in *.txt; do
((count++))
done
echo "找到 $count 个文本文件"
内建命令和常规命令:
在Linux中大部分指令是bash的子进程,但不是所有的指令都需要创建子进程(如echo),这一类命令我们称为内建命令。
常规命令:
(1)特点:
独立程序:存储在文件系统中的二进制可执行文件(如 /bin/ls
)。
运行方式:Shell 通过创建子进程(fork()
+ exec()
)执行。
继承关系:子进程继承父 Shell 的环境变量,但无法修改父 Shell 的状态(如工作目录、变量)。
(2)常见常规命令:
ls, grep, cat, python, mkdir, cp
内建命令:
(1)特点:
Shell 自带功能:直接内置于 Shell 解释器中(如 cd
、export
)。
运行方式:不创建子进程,直接在当前 Shell 进程中执行。
用途:修改 Shell 自身状态(如变量、目录、环境)。
常见内建命令:
cd, export, unset, echo, source, exit, alias
环境变量的组织方式:

每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以'\0'结尾的环境字符串。
通过代码如何获取环境变量:
(1)通过命令行的第三个参数:
#include <stdio.h>
int main(int argc, char *argv[], char *env[]) {
printf("=== 使用 main 参数 ===\n");
for (int i = 0; envp[i] != NULL; i++) {
printf("%d: %s\n", i, envp[i]);
}
return 0;
}
(2)通过第三方变量environ获取
#include <stdio.h>
#include <unistd.h>
extern char **environ; // 声明外部环境变量指针
int main() {
printf("=== 使用 environ 指针 ===\n");
for (char **env = environ; *env != NULL; env++) {
printf("%s\n", *env);
}
return 0;
}
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明。
(3)通过getenv获取特定的环境变量
#include <stdio.h>
#include <stdlib.h>
int main() {
// 获取单个环境变量的值
char *home = getenv("HOME");
char *user = getenv("USER");
char *path = getenv("PATH");
if (home) printf("HOME: %s\n", home);
if (user) printf("USER: %s\n", user);
if (path) printf("PATH: %s\n", path);
return 0;
}
环境变量通常具有全局属性,可以被子进程继承下去(环境变量的全局性是指 对当前进程及其所有子进程可见,而非整个操作系统所有用户共享。)
#include <stdio.h>
#include <stdlib.h>
int main()
{
char *env = getenv("MYENV");
if(env){
printf("%s\n", env);
}
return 0;
}
直接查看,发现没有结果,说明该环境变量根本不存在。导出环境变量 export MYENV="hello world" 再次运行程序,发现结果有了!说明:环境变量是可以被子进程继承下去的!
程序地址空间:

这是各个区的变量地址,可以自行去自己的编译器查看:
#include <stdio.h>
#include <stdlib.h>
// 全局变量(数据段)
int global_init = 100;
int global_uninit;
void print_memory_layout() {
int stack_var = 10;
static int static_var = 200;
char *heap_var = (char*)malloc(100);
char *env_var = getenv("MYENV");
extern char **environ;
printf("【代码段】\n");
printf("函数地址(main): %p\n", main);
printf("函数地址(print_memory_layout): %p\n\n", print_memory_layout);
printf("【数据段】\n");
printf("已初始化全局变量: %p\n", &global_init);
printf("静态变量: %p\n", &static_var);
printf("未初始化全局变量(BSS): %p\n\n", &global_uninit);
printf("【堆区】\n");
printf("动态分配内存: %p\n", heap_var);
printf("堆变量指针: %p\n\n", &heap_var);
printf("【栈区】\n");
printf("局部变量(stack_var): %p\n", &stack_var);
printf("环境变量指针(env_var): %p\n\n", &env_var);
printf("【环境变量区域】\n");
if (env_var) {
printf("MYENV值地址: %p\n", env_var);
printf("MYENV值内容: %s\n", env_var);
} else {
printf("MYENV值地址: (未设置)\n");
}
if (environ && *environ) {
printf("环境变量表地址: %p\n", environ);
printf("第一个环境变量: %p -> %s\n", *environ, *environ);
printf("\n前5个环境变量地址:\n");
int count = 0;
for (char **env = environ; *env != NULL && count < 5; env++, count++) {
printf("env[%d]: %p -> %s\n", count, *env, *env);
}
}
free(heap_var);
}
int main() {
if (getenv("MYENV") == NULL) {
setenv("MYENV", "test_environment_value", 0);
}
print_memory_layout();
return 0;
}
当我们在父进程中创建子进程时
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 0;
}
else if(id == 0)
{ //child
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
else
{ //parent
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
我们会发现输出的变量和地址是一摸一样的
parent[2995]: 0 : 0x80497d8
child[2996]: 0 : 0x80497d8
但是当我们对上述代码修改一部分时:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 0;
}
else if(id == 0)
{
g_val=100;
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}else
{
sleep(3);
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
child[3046]: 100 : 0x80497e8
parent[3045]: 0 : 0x80497e8
我们会发现输出结果父子进程的地址是一样的,但是输出的内容却不一样,难道它们用同一份地址输出了两份内容?其实结果不是这样的,其实编译器显示的地址不是物理地址,而是虚拟地址。我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理。OS必须负责将 虚拟地址 转化成 进程地址空间 物理地址。这就是我们将要引入的进程地址空间:
在引入进程空间之前,我们需要先简单弄清楚硬件中的系统总线和I/O总线和高低电平的概念:
高级语言 → 机器指令 → CPU执行→总线传输 → 硬件操作
↓ ↓ ↓ ↓ ↓
getenv() → mov指令 → 总线访问 → 内存读取 → 电平信号
在总线中将CPU与系统连接起来的叫做系统总线,内存与外设连接起来的叫做I/O总线。而在传输数据中也是通过这些总线中的高低电平来传输不同的二进制信号以此来输送数据。
总线与地址空间的硬件关联:
当程序访问进程地址空间中的某个地址(如 mov eax, [0x08049000]
)时:
地址总线:CPU 将虚拟地址 0x08049000
发送到 MMU(内存管理单元)
MMU 转换:通过页表将虚拟地址转为物理地址(如 0x2000F000
)
控制总线:发送读/写信号(高电平=读,低电平=写)
数据总线:传输实际数据(如从内存读取的值)
在32位系统中最大寻址空间为4GB
环境变量在地址空间中的硬件访问:环境变量位于栈区的高地址端(靠近内核空间),其物理内存通过总线访问
物理内存:
CPU → 地址总线发送 environ 地址 → 内存控制器返回指针值 → 数据总线传输字符串
电平信号与进程内存:
(1)内存单元的电平表示:每个内存单元由电容存储电荷(高电平=1,低电平=0):
地址 0x2000F000: [高][低][高][低]... → 二进制 1010... → 数据 0xA...
(2)总线传输的电平序列
CPU 读取内存时:地址周期:地址总线发送 0x2000F000
(32位电平信号,数据周期:数据总线返回 0xA5
(8位电平信号:H L H L L H L H
)
虚拟地址空间:虚拟地址空间是操作系统为每个进程提供的"幻觉",让每个进程都认为自己独享整个内存空间。分页机制则是实现这种幻觉的关键技术。
那么页表是什么?接下来给大家解释一下页表的概念:页表是操作系统用于实现虚拟内存的关键数据结构,它建立了虚拟地址与物理地址之间的映射关系。(核心作用:1.地址转换:将进程看到的虚拟地址转换为实际的物理内存地址。2.内存保护:控制进程对内存的访问权限(读、写、执行)。3.共享内存:允许多个进程安全地共享同一物理内存页)
页表工作原理:
虚拟地址 → [页表查询] → 物理地址
↓ ↓ ↓
进程虚拟空间 MMU 物理内存

所以这就是为什么上面父子进程地址相同但是内容不同的原因。
父进程虚拟地址 --[页表]--> 物理内存的父进程g_val区域
子进程虚拟地址 --[页表]--> 物理内存的子进程g_val区域
惰性加载:将资源分配推迟到实际需要使用时才进行,而不是在初始阶段就完成所有加载。