Re:Linux 系统篇(十七):进程篇(六):环境变量深度解析|命令行参数、环境变量表、4 种获取方式与继承机制万字详解

上一篇我们讲完了 Linux O (1) 调度算法,搞懂了进程如何被调度上 CPU 运行。
但一个进程启动之后,除了自己的代码、数据,操作系统还会给它附带两份 “随身资料”:命令行参数 和 环境变量。
很多人天天敲命令,gcc、g++、sudo、su,却很少思考:
为什么 gcc 可以自动找到系统的库文件?为什么 shell 可以在 PATH 里面搜索命令?为什么子进程可以拿到父进程的环境信息?
本篇我们从 main 函数讲起,逐层拆解命令行参数、环境变量底层结构、4 种代码获取方式、继承原理,同时讲环境变量的安全边界。
前言:命令行参数
1.1 重新认识 main 函数
很多 C/C++ 初学者以为,main()就是程序的真正入口,其实不是。
1.1.1 main 函数不是程序的开始
真正的程序入口是_start函数,这个函数属于 C 运行时库(crt0)。
当操作系统把程序加载进内存,CPU 跳转到_start执行:
- 做程序运行前的初始化工作(栈、标准 IO、全局变量初始化)
- 解析命令行参数、解析环境变量表
- 调用我们写的
main()函数 - main 返回后,
_start调用exit()完成进程退出回收
main 只是我们写业务逻辑的入口,不是整个程序的底层入口。
1.1.2 main 函数也有参数
我们平时写的 main 有两种写法:
// 写法1,不接收参数
int main()
// 写法2,接收命令行参数,argc + argv
int main(int argc, char *argv[])
argc(argument count):命令行参数的个数argv(argument vector):字符串指针数组,保存每一个命令行字符串,数组末尾以NULL标记结束。
示例代码:
// arg_demo.c
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("argc = %d\n", argc);
for(int i = 0; i < argc; i++)
{
printf("argv[%d] = %s\n", i, argv[i]);
}
return 0;
}
编译运行:
gcc arg_demo.c -o arg_demo
./arg_demo hello linux 123
输出:
argc = 4
argv[0] = ./arg_demo
argv[1] = hello
argv[2] = linux
argv[3] = 123
注意:
argv[0]永远是程序自身的名字。
1.2 命令行参数的切分与表结构
shell 在敲回车执行命令的时候,会做一件事:把你输入的整行字符串,按空格切分成一个个字符串,组装成argv数组。
内存布局:
argv[0] → "./arg_demo"
argv[1] → "hello"
argv[2] → "linux"
argv[3] → "123"
argv[4] → NULL
这个数组,存放在进程用户地址空间的栈区高地址,和环境变量挨在一起。
1.3 命令行参数的应用演示
命令行参数最典型的用途:程序不用重新编译,启动的时候传参数改变行为。
比如ls -l,gcc -o test test.c,-l、-o test test.c全部都是命令行参数。
一、引入环境变量
1.1 基本概念
1.1.1 环境变量的概念
环境变量就是操作系统提供给进程的一组特殊全局变量,用来记录系统和用户的运行信息。
就像人出门会随身带证件、通讯录,进程启动的时候,操作系统也会给它附带一整套环境变量。
里面记录的信息包括:
- 当前是谁登录用户(
USER) - 主机名字(
HOSTNAME) - 命令去哪里找(
PATH) - 动态库去哪里找(
LD_LIBRARY_PATH) - 历史命令最多存多少条(
HISTSIZE)
重点:环境变量最大的特性:子进程会继承父进程的环境变量。
这就解释了:为什么gcc、g++、sudo、su这些工具都可以读取环境变量。
- gcc/g++ 继承 shell 的环境变量,读取
LD_LIBRARY_PATH去寻找动态库,读取PATH寻找编译工具链。- su/sudo 继承 shell 的环境变量,可以读取系统信息,完成切换用户、查找命令等操作。
1.1.2 常见环境变量
表格
| 变量名 | 作用 |
|---|---|
| PATH | 命令搜索路径,shell 在这里找可执行程序 |
| HOME | 当前用户家目录 |
| USER | 当前会话用户名 |
| LOGNAME | 登录用户名 |
| HOSTNAME | 主机名 |
| LD_LIBRARY_PATH | 动态链接库搜索路径 |
| HISTSIZE | shell 历史命令保存条数 |
1.1.3 怎么看环境变量
# 打印全部环境变量
env
printenv
# 打印单个环境变量
echo $PATH
echo $HOME
1.1.4 环境变量和本地变量的设置和导入
shell 里面有两种变量:
- shell 本地变量:只在当前 shell 生效,子进程看不到。
- 环境变量:通过
export导出,子进程可以继承。
1.1.4.1 核心概念对比
# 定义shell本地变量(子进程不可见)
MY_VAR="hello world"
# 导出为环境变量(子进程可以继承)
export MY_VAR
1.1.4.2 变量的操作指令
export 变量名=值 # 设置环境变量
unset 变量名 # 删除变量
⚠️注意:直接写
MY_VAR=xxx,只是 shell 局部变量;不加 export,子进程拿不到这个变量。
1.2 main 函数的第三个参数
很多人不知道 main 函数还有第三个隐藏参数,用来接收环境变量表:
int main(int argc, char *argv[], char *envp[])
envp是字符串指针数组,每一个元素格式为KEY=VALUE,数组末尾以NULL结束。
示例代码:
// env_main.c
#include <stdio.h>
int main(int argc, char *argv[], char *envp[])
{
for(int i = 0; envp[i] != NULL; i++)
{
printf("%s\n", envp[i]);
}
return 0;
}
编译运行,就会打印出全部环境变量。
注意:这个第三个参数是 GCC 的扩展,标准 C 不推荐直接使用,标准推荐使用
getenv()或者全局environ指针。
1.3 为什么执行自己的程序必须加 ./
./myprogram
为什么不能直接写myprogram?
shell 执行命令的时候,会去PATH环境变量记录的目录里面挨个找可执行文件。
当前目录.默认不在 PATH 里面,所以 shell 找不到你的程序,必须用./显式告诉 shell:在当前目录找这个程序。
如果你把
.加入 PATH,就可以直接写程序名运行,但这存在安全风险,不建议生产环境这么做。
二、环境变量的底层表结构与溯源
2.1 环境变量表的组织形式
环境变量表本质就是char *envp[]指针数组:
envp[0] → "PATH=/usr/bin:/bin"
envp[1] → "HOME=/home/ubuntu"
envp[2] → "USER=ubuntu"
envp[3] → NULL
每一条字符串格式固定为键=值,数组末尾用NULL标记结束。
这块内存位于进程地址空间的用户栈高地址,和命令行参数挨在一起。
2.2 环境变量的内存级和磁盘级修改
- 内存级修改(临时):shell 执行
export,只对当前 shell 会话生效,关闭终端就消失。 - 磁盘级修改(永久):修改配置文件,登录 shell 的时候自动加载环境变量。
- 用户级别:
~/.bashrc、~/.profile,只对当前用户生效 - 系统级别:
/etc/profile,对所有用户生效
- 用户级别:
修改完磁盘配置文件,需要执行source ~/.bashrc,让配置立刻生效。
2.3 怎么在 windows 下修改环境变量
Windows 环境变量逻辑和 Linux 类似:
- 此电脑 → 右键属性 → 高级系统设置 → 环境变量
- 分为用户环境变量(当前用户)、系统环境变量(全部用户)
- 修改后新开 cmd 窗口生效,旧窗口不会自动更新。
三、获取环境变量的三种核心代码手段
注意:main 第三个参数、getenv、environ、子进程继承,一共 4 种方式。
3.1 方法一:通过 main 函数的第三个参数隐式获取
就是上面写的char *envp[],直接遍历数组读取环境变量。
缺点:标准 C 不支持,属于编译器扩展,可移植性差。
3.2 方法二:通过系统调用 API getenv 获取(✅最推荐)
C 标准库提供getenv(),输入环境变量 key,返回对应的 value 字符串。
// getenv_demo.c
#include <stdio.h>
#include <stdlib.h>
int main()
{
char *path = getenv("PATH");
if(path != NULL)
{
printf("PATH = %s\n", path);
}
char *user = getenv("USER");
printf("USER = %s\n", user);
return 0;
}
编译运行:
gcc getenv_demo.c -o getenv_demo
./getenv_demo
优点:标准 C 库函数,跨平台,代码简洁。
3.3 方法三:通过全局第三方指针environ显式获取
内核给我们提供一个全局的二级指针environ,不需要 main 第三个参数,直接使用。
需要手动声明
extern char **environ;
// environ_demo.c
#include <stdio.h>
extern char **environ;
int main()
{
for(int i = 0; environ[i] != NULL; i++)
{
printf("%s\n", environ[i]);
}
return 0;
}
environ全局指针,直接指向进程的环境变量表的起始地址。
3.4 方法四:子进程继承父进程获取环境变量
3.4.1 核心原理
当我们调用fork()创建子进程的时候:
子进程会拷贝父进程的完整地址空间,环境变量表也完整复制一份。
所以子进程天然拥有父进程全部环境变量。
后续调用exec加载新程序,默认保留环境变量,这就是为什么gcc、sudo这些子进程可以拿到 shell 的环境变量。
exec 也可以使用
execle(),传入自定义的环境变量表,不继承父进程。
3.4.2 示例代码
// fork_env.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid = fork();
if(pid == 0)
{
// 子进程,读取PATH
printf("[子进程] PATH = %s\n", getenv("PATH"));
}
else
{
printf("[父进程] PATH = %s\n", getenv("PATH"));
}
return 0;
}
运行可以看到父子进程打印出一模一样的 PATH,证明继承生效。
四、其他环境变量
4.1 所有环境变量罗列
执行env命令,就可以列出当前进程全部环境变量。
4.1.1 USER 和 LOGNAME 的区别
LOGNAME:记录登录终端的原始用户。USER:记录当前会话的用户。
举例子:用 ubuntu 账号登录终端,然后执行sudo su root切换 root。
此时LOGNAME仍然是 ubuntu,而USER变成 root。
4.1.2 历史命令与 HISTSIZE
HISTSIZE这个环境变量控制 shell 历史命令最多保存多少条。
echo $HISTSIZE
修改这个值,就可以调整 shell 历史命令的存储上限。
4.2 环境绑定与权限控制用法
环境变量可以用来给程序传递配置,不需要修改源代码,只需要设置环境变量就改变程序行为。但是也存在安全风险。
4.2.1 核心逻辑与安全防范
⚠️安全坑点:如果程序是 root 权限运行,环境变量来自普通用户,恶意用户可以篡改环境变量,实现攻击,比如 PATH 劫持。
所以高权限程序,很多会主动清空或者过滤环境变量。
4.2.1.1 示例代码
// secure_env.c
#include <stdio.h>
#include <stdlib.h>
int main()
{
// 高权限程序,不要无条件信任环境变量
char *debug = getenv("DEBUG");
if(debug != NULL)
{
printf("DEBUG模式开启\n");
}
return 0;
}
4.2.3 基于自定义环境变量的控制逻辑
业务程序可以自定义环境变量,作为配置开关。
4.2.3.1 示例代码
// custom_env.c
#include <stdio.h>
#include <stdlib.h>
int main()
{
char *mode = getenv("RUN_MODE");
if(mode == NULL)
{
printf("默认生产模式\n");
}
else if(strcmp(mode, "debug") == 0)
{
printf("调试模式\n");
}
return 0;
}
运行测试:
# 生产模式
./custom_env
# 调试模式
export RUN_MODE=debug
./custom_env
不用改代码,只设置环境变量,就切换程序运行模式。
全文总结
- 命令行参数:shell 把命令切割成字符串数组,传递给 main 函数的
argc/argv,用来给程序传递启动参数。 - 环境变量:操作系统给进程的一组特殊全局变量,保存系统、用户信息,格式为
KEY=VALUE。 - shell 本地变量必须用
export导出,才会变成环境变量被子进程继承。 - 4 种获取环境变量的方式:
- main 第三个参数 envp(可移植差)
getenv()标准库函数(推荐)- 全局
environ指针 - fork 子进程继承父进程环境变量
- 继承原理:
fork拷贝地址空间,环境变量表复制;exec默认保留环境变量。 - 为什么 gcc/g++、sudo 可以读取环境变量:它们都是 shell 的子进程,继承 shell 的环境变量。
- 环境变量可以做配置控制,但高权限程序要警惕环境变量带来的安全风险。

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)