
🎬 个人主页:MSTcheng · CSDN 🌱 代码仓库 :MSTcheng · Gitee
🔥 精选专栏: 《C语言》《数据结构》 《算法学习》 《C++由浅入深》
《Linux操作系统》
《Mysql数据库》
《白话大模型》
💬座右铭:
路虽远行则将至,事虽难做则必成!
上一篇文章中,我们给大家介绍了Linux进程,讲解了Linux进程到底是怎么组织起来的,其中还讲了进程调度,学习了进程调度以后进程就能在脑子里“跑起来”了!本篇文章我们就进入到命令行参数和环境变量的学习。
文章目录
- 前置知识:从开机到 Bash,进程如何一路继承
-
- Linux 开机后的第一站:systemd
- 从图形界面到终端:GDM → gnome-shell → bash
- 子进程继承自父进程:命令行参数与环境变量的由来
- 一、命令行参数
-
- 1.1 问题一:main 函数可以有参数吗?
- 1.2 实验:打印 argc 与 argv
- 1.3 命令行参数的本质:shell 切割字符串
- 1.4 为什么要有命令行参数?(本质应用)
- 1.5 用命令行参数理解 `cat` 命令
- 二、进一步理解命令行参数,引出环境变量
-
- 2.1 我们自己实现一个命令
- 2.2 为什么必须写 `./mycmd`
- 2.3 PATH 的本质
- 2.4 如何把当前目录加入 PATH?
- 2.5 内建命令为什么不受影响
- 三、深入认识环境变量
-
- 3.1 `env`(environment) 命令
- 3.2 常见环境变量
- 3.3 灵魂拷问:bash 的环境变量,到底是从哪来的?
- 3.4 cd – 与 OLDPWD(上一次所在目录)
- 3.5 专题:cd ~ 与 HOME
- 3.6 SSH_TTY 与"一切皆文件"
- 3.7 环境变量像什么?
- 3.8 C 程序获取环境变量的三种方式
- 3.9 安全案例:用 getenv 做"用户白名单"
- 四、核心:环境变量到底存在哪里
-
- 4.1 两张表:argv 表与环境变量表
- 4.2 是"内存级"的,不是磁盘上的
- 4.3 bash 也是进程
- 4.4 为什么子进程不传参也能拿到环境变量
- 4.5 进程树:bash 是"祖宗"
- 4.6 只读共享与写时拷贝
- 4.7 结论:环境变量是父进程给的
- 五、bash 的环境变量从哪来:配置文件
-
- 5.1 登录时发生了什么(完整流程)
-
- 5.2 配置文件的调用链
- 5.3 为什么每个用户必须有自己的家目录
- 5.4 "载入进程上下文"是什么意思
- 5.5 验证实验:配置文件真的被读了吗
- 5.6 用 ps 观察 bash 进程
- 5.7 完整因果链
- 六、本地变量与环境变量
-
- 6.1 自己造一个变量
- 6.2 本地变量 vs 环境变量
- 6.3 export 的本质
- 6.4 子进程继承验证
- 6.5 unset:删除变量
- 6.6 set:查看所有变量
- 6.7 本地变量的用途
- 6.8 PS1 与 PS2
- 七、持久化:写进配置文件(呼应 3.3 的"复原"问题)
-
- 7.1 写入 ~/.bash_profile
- 7.2 系统级持久化
- 7.3 安全提醒
- 八、总结
前置知识:从开机到 Bash,进程如何一路继承
在正式介绍命令行参数与环境变量之前,先带大家站在更高的视角看一眼:我们平时写的程序,到底是在一条怎样的“进程链条”上运行的。
Linux 开机后的第一站:systemd
当 Linux 加电启动后,内核完成自身初始化,随后会创建第一个用户态进程——systemd,它的 PID 恒为 1。systemd 是整个用户态进程的“老祖宗”,随后它会拉起一大批系统服务进程和用户会话相关的进程。
从图形界面到终端:GDM → gnome-shell → bash
在众多被 systemd 拉起的进程中,有一个负责图形登录的进程叫 GDM。GDM 在用户登录成功后,会进一步创建桌面环境的核心进程 gnome-shell,把我们看到的图形界面跑起来。
当我们打开一个终端窗口时,事情还没有结束:gnome-shell 会读取 bashrc 配置文件,并据此创建出一个 bash 进程。这个 bash,正是我们平时敲命令、写代码、跑程序的最直接“上级”。
子进程继承自父进程:命令行参数与环境变量的由来
理解了上面的进程链,就很容易明白下面要讨论的核心事实——子进程会继承父进程的很多东西:
所以,接下来要学习的命令行参数与环境变量,讨论的核心就是:子进程 main 的参数继承自 bash,环境变量也同样继承自 bash。理解了这条继承链,再看 main 函数的参数 argc、argv 以及环境变量的具体规则,就会顺畅很多。
了解完上面这这个完整链路之后,下面就来具体的看看命令行参数和环境变量!
一、命令行参数
1.1 问题一:main 函数可以有参数吗?
很多人误以为main函数是没有参数的,是因为以前写的是语言层的代码用不到,当是如果写系统层的代码就会用到了。main函数其实是有参数的!
示例:
int main(int argc, char *argv[]) {
// …
return 0;
}
参数含义:
| argc | argument count | 命令行参数 个数 |
| argv | argument vector | 命令行参数 列表(指针数组) |
- char *argv[] 是一个指针数组:数组的每个元素是一个指针,指向一个字符串;
- argv[0]、argv[1]、argv[2]……分别指向不同的字符串。
1.2 实验:打印 argc 与 argv
代码:
#include <stdio.h>
int main(int argc, char *argv[]) {
for (int i = 0; i < argc; i++) {
printf("argv[%d]: %s\\n", i, argv[i]);
}
printf("argc = %d\\n", argc);
return 0;
}
| ./commendTest | 1 | [0]=./code |
| ./commendTest a | 2 | [0]=./code, [1]=a |
| ./commendTest a b c d | 5 | [0]=./code, [1..4]=a b c d |
| … |

规律:有几个"以空格分隔的字符串",argc 就是几(含程序名本身)
1.3 命令行参数的本质:shell 切割字符串
我们在命令行里输入的这一行东西本质是一个字符串。这个字符串被拿到了之后,会以空格作为分隔符,把这一个字符串转化成若干个字符串。"
关键点:
- 我们输入的 ./commandTest a b c d(含空格)——本质上是一个字符串;
- shell(未来的内容)把它按空格切割成若干子串;
- 子串的个数 = argc;每个子串依次由 argv 的元素指向。
这个工作是 shell 做的,后面会手搓一个shell(bash)。
注意:多个连续空格会被当作一个分隔符,不会产生空的 argv 元素。
所以为什么要切割字符串?
命令行参数的本质应用,是为了能够实现一个命令可以根据不同的选项,实现不同的子功能!也是Linux中所有命令选项功能的实现方式!!(只有把字符串切割了,才能根据切割后的内容匹配不同的选项执行不同的内容!)
1.4 为什么要有命令行参数?(本质应用)
下面有一段看起来莫名其妙的代码演示:根据不同的选项(-a / -b / -c / -d)执行不同的功能。
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("请这样使用: %s -a|-b|-c|-d\\n", argv[0]);
return 1;
}
if (strcmp(argv[1], "-a") == 0) {
printf("执行功能一\\n");
} else if (strcmp(argv[1], "-b") == 0) {
printf("执行功能二\\n");
} else if (strcmp(argv[1], "-c") == 0) {
printf("执行功能三\\n");
} else {
printf("默认功能\\n");
}
return 0;
}
结论:
命令行参数的本质应用,是为了实现 一个命令可以根据不同的选项实现不同的子功能,这就是 Linux 中所有命令选项的来源。
- ls 是命令它是 C 写的,它有 main 函数、有参数,参数传进去后自己判断,根据判断实现不同功能。
对于命令行参数有三个细节要注意:
① 细节一:argc ≥ 1,argv[0] 一定是程序名
- 命令行参数至少是1,即argc>=1,所以argv[o]一定会有元素,其所指向的就是层序名!!例如只输入ls或pwd命令argv[0]就指向ls或pwd.
②细节二:选项是"以空格分隔的字符串"
- -a、-b 是一个个的字符串(哪怕只有一个字符);
- 我们上面只是"打印"它,若加逻辑判断(如 strcmp),一个程序就能实现多种功能。
③细节三:argv 最后一个元素永远指向 NULL
- 如果有argc个命令行参数,那么argv[argc-1]是最后一个,argv[argc]=NULL。
- 因此可以这样遍历(更简洁):
for (int i = 0; argv[i] != NULL; i++) { // i < argc 等价写法
printf("%s\\n", argv[i]);
}
1.5 用命令行参数理解 cat 命令
cat 后面跟文件名,它是怎么把文件名拿到的?
答案: cat是也命令!catfile.txt为什么呢能打印文件内容?cat命令也是C语言写的,所以它有 main 函数参数(argc/argv),能从命令行参数里获取文件名,那么cat命令内部就可以使用fopen以读的形式打开,然后再全部读取打印出来。所以本质就是命令行参数获取到文件内容,最后再打印文件内容!
cat 打印文件内容的本质流程:
① 通过命令行参数获取文件名(argv[1])
│
▼
② 以只读方式打开文件(open / fopen)
│
▼
③ 读取文件内容(read / fread)
│
▼
④ 打印到显示器(stdout)
cat 打印文件内容的本质就是——通过命令行参数获取文件名,打开文件,读取文件,打印。
关联旧知识:之前讲过的输入重定向(cat < file)也是从文件读取(即cat识别到了<(符号)和file(文件)知道是从文件中读取)即将来<和file都会传入到cat程序的mian函数中的argv[]中,谁传的?—bash! 终于从"命令行参数"角度把它讲透了。
二、进一步理解命令行参数,引出环境变量
2.1 我们自己实现一个命令
#include <stdio.h>
int main(void)
{
printf("我是一个命令,自己实现的\\n");
return 0;
}
编译:
gcc mycmd.c -o mycmd
运行:
./mycmd
输出:
我是一个命令,自己实现的
结论:命令不是什么神秘东西,就是“特定路径下的一个可执行文件”。 系统自带的 ls 本质就是 /usr/bin/ls 这个文件。
2.2 为什么必须写 ./mycmd

- 直接输入 mycmd 会报 command not found。
- 原因:shell 只去 PATH 列出的目录里找命令,默认不搜索当前目录 .。
- ./ 表示“当前目录”,./mycmd 是 明确告诉 shell:就执行当前目录下这个文件。
- 也可以用绝对路径:/home/whb/mycmd。
- 我们自己写的命令,系统不应该就一定要认识,系统只认 PATH 里的路径!
2.3 PATH 的本质
查看:
echo $PATH
典型输出(冒号分隔):
/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin
- PATH 是一串目录,冒号 : 分隔。
- shell 查找命令时从左到右依次查找,找到第一个就执行,后面的不再找。
- 这就是“同名命令谁生效”的根源:排在前面的目录优先。
which 可以看到命令最终用的是哪个路径:
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ which ls
/usr/bin/ls
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ which pwd
/usr/bin/pwd
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ which mycmd
👈没反应,因为mycmd是我们自己实现的,不在环境变量PATH里面
2.4 如何把当前目录加入 PATH?
正确追加:
PATH=$PATH:$(pwd)
或规范写法(export 后面会讲):
export PATH=$PATH:$(pwd)
把当前目录导入到PATH这个环境变量中,那么PATH这个环境变量里就有了当前路径了!(记住PATH的本质就是一串目录)
- PATH 是目录列表,bash 按顺序搜索不带路径的外部命令,找到第一个同名可执行文件就执行;优先级由 PATH 先后顺序决定,不是系统命令天然优先。你写的 ls,如果目录不在 PATH 里,bash 就找不到它,只能执行系统 ls; 但如果你把自定义目录加到 PATH前面,你的 ls 就会覆盖系统 ls;所以生产环境一般不建议把自定义目录前置,追加到末尾是安全做法。
//此时我们再which我们自己的命令 它就能查到是在这个目录底下了!!!!!!!!!
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ which myprocess
/home/mstcheng/117/code/linux-code/process/myprocess
//并且不用加./了,因为当前目录已经在环境变量PATH里了,所以我们直接写程序名
//即可运行我们自己写的程序了 就像ls,pwd,find这类命令一样!!!!!!!!
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ myprocess
i am a process pid:406557 ppid:45955
错误覆盖:
PATH=$(pwd)
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ PATH=$(pwd)
//如果改错了,那么PATH里面就只有当前路径了
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ echo $PATH
/home/mstcheng/117/code/linux-code/process
//我们再去执行ls,whoami,这样的命令就执行不了了!!!!!!
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ ls
命令 “ls” 可在以下位置找到
* /bin/ls
* /usr/bin/ls
由于 /usr/bin:/bin 不在 PATH 环境变量中,故无法找到该命令。
ls:未找到命令
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ whoami
命令 “whoami” 可在以下位置找到
* /bin/whoami
* /usr/bin/whoami
由于 /usr/bin:/bin 不在 PATH 环境变量中,故无法找到该命令。
whoami:未找到命令
//那为什么pwd还能执行??因为它是内建命令(后面具体介绍)
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ pwd
/home/mstcheng/117/code/linux-code/process
通过上面的现象结论就是:
- PATH 里只剩当前目录。
- ls、cat、vim 等 外部命令全部失效(找不到)。
- 但 cd、echo、pwd 等 内建命令仍然可用,因为它们不经过 PATH 查找。
恢复方法
- 重新登录(退出当前 shell 再登录)即可恢复。
- 原因后面讲:PATH 的真实来源是配置文件,你改的只是内存。
注意:
- echo PATH 打印的是字面量 PATH;echo $PATH 才是变量值。
- PATH=… 是覆盖,PATH=$PATH:… 才是追加。
- 在 PATH 里放 . 或当前目录有安全风险:别人在你目录下放一个同名恶意程序,你敲命令就可能执行它。
- 修改 PATH 只对当前 shell 及其子进程有效,不会影响其他终端,也不会持久化。
2.5 内建命令为什么不受影响
- cd、echo、pwd、export、unset、set 等是 shell 内建(builtin)。
- 它们由 bash 自己实现并直接执行,不需要 fork 新进程,也不需要去 PATH 找文件。
- 所以 PATH 改坏后,你仍然能用 echo 打印、用 cd 切目录,但没法用 ls 看目录内容。
三、深入认识环境变量
3.1 env(environment) 命令
//直接输入env 你会看见有很多信息
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ env
SHELL=/bin/bash
PWD=/home/mstcheng/117/code/linux-code/process //环境变量里面也存在着当前的工作路径
PATH=/home/mstcheng/.npm-global/bin:/home/mstcheng/........//可以注意到PATH是在环境变量里面存在的
......
打印当前进程能看到的所有环境变量,格式为 NAME=value。
env 是独立可执行程序(外部命令,不是 shell 内置),它做的事情很简单:
- 读取自身进程的环境变量表;
- 逐行打印出来。
3.2 常见环境变量
| PATH | 命令搜索路径 | 冒号分隔,决定命令去哪找 |
| HOSTNAME | 主机名 | 当前机器叫什么 |
| TERM | 终端类型 | 如 xterm,决定终端能力 |
| SHELL | 当前使用的 shell | 通常是 /bin/bash |
| HISTSIZE | 历史命令保存条数 | history | wc -l 验证 |
| SSH_CLIENT | 客户端 IP 和端口 | 记录“谁从哪连过来” |
| SSH_TTY | 当前终端设备 | 如 /dev/pts/1,每次 SSH 登录新建 |
| PWD | 当前工作目录 | cd 后自动更新 |
| OLDPWD | 上一次所在目录 | cd – 的本质 |
| HOME | 用户家目录 | cd ~ / cd 使用 |
| USER / LOGNAME | 当前用户名 | 登录身份 |
| LANG | 语言与字符编码 | 影响中文显示/排序 |
| 邮件存放位置 | 传统邮件程序使用 |
3.3 灵魂拷问:bash 的环境变量,到底是从哪来的?
看完上面这一大堆变量,你肯定想问一个问题:这些环境变量到底是从哪来的?为什么我自己改了,退出重进又复原了?
先做个实验:
# 自己造一个变量并导出
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ export MY_VAR="hello"
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ env | grep MY_VAR
MY_VAR=hello
# 关掉终端,重新登录,再来看——
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ env | grep MY_VAR
# 空!啥都没有了,复原了
答案分两层:
我们使用 export、unset、改 PATH,改的都是当前 bash 进程内存里的那张表,不会自动写回配置文件。所以:当前 shell 立即生效 → 子进程也生效 → 退出重登后恢复原样。
与前置知识呼应:你登录后看到的命令行,是 bash 进程在跑。bash 也是进程——就是前置知识里那条链的末端:内核 → systemd(PID 1) → sshd / GDM → gnome-shell → bash。bash 启动时读取配置文件,把变量载入自己的内存;bash 退出,内存里的表就没了,所以"重登复原"。
验证一下 bash 真的是"一个进程一份内存":
mstcheng@ubuntu-dev:~$ ps axj | grep bash
# 每多开一个 SSH 会话,就多一个 bash 进程
# 每退出一个会话,就少一个
# 全部退出后,系统里就没有你的 bash 了

那"配置文件"具体是哪些?调用链放到第五节细讲,这里先记住结论。接下来先看几个常见变量。
3.4 cd – 与 OLDPWD(上一次所在目录)
//先进入到/etc这个目录
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ cd /etc
//再打印OLDPWD
mstcheng@ubuntu-dev:/etc$ echo $OLDPWD
/home/mstcheng/117/code/linux-code/process #发现打印出来的是我们上一个路径!
//使用cd -就能迅速回到上一次的路径下
mstcheng@ubuntu-dev:/etc$ cd –
/home/mstcheng/117/code/linux-code/process
每次 cd,shell 会把"旧目录"写进 OLDPWD,把"新目录"写进 PWD。所以:
cd – 不是特殊魔法,本质就是 cd $OLDPWD。
- PWD/OLDPWD 是 shell 维护的,还是系统调用?——shell 在 cd 时更新环境变量;pwd 内建命令也可通过 getcwd() 获取。
3.5 专题:cd ~ 与 HOME
mstcheng@ubuntu-dev:/etc$ cd
mstcheng@ubuntu-dev:~$ pwd
/home/mstcheng
cd 不带参数、cd ~ 都会回到 HOME。root 的 HOME 是 /root,普通用户通常是 /home/用户名。
与前知识置呼应:为什么每个用户必须有自己的家目录?因为配置文件放在各自家目录下(如 /home/mstcheng/.bash_profile)——每个用户登录读的是自己的配置文件,所以不同用户可以有不同环境变量;你改了 mstcheng 的配置,root 登录不受影响。
3.6 SSH_TTY 与"一切皆文件"
每建立一次 SSH 连接,系统就分配一个新的终端设备,如 /dev/pts/1、/dev/pts/2。SSH_TTY 记录当前会话对应的设备文件。
mstcheng@ubuntu-dev:~$ echo $SSH_TTY
/dev/pts/1
# Linux 一切皆文件:终端也是文件,可以向它写数据
mstcheng@ubuntu-dev:~$ echo "hello" > /dev/pts/1
注意我这里演示的是本地的(同一台机器),不是SSH,但是跟SSH是一个效果的!
如果另一个终端正好是 /dev/pts/1,且你有权限,就能在对方终端上看到 hello。
补充(SSH_CLIENT 的精确格式):SSH_CLIENT=192.168.1.100 50000 22是三个字段:客户端 IP、客户端端口、服务器端口(22)。它和 SSH_TTY 正是 sshd 这条链传给 bash 的"证据"。
与前置知识呼应:你看到的 SSH_CLIENT / SSH_TTY 这两个环境变量为什么存在? 因为你是通过 sshd 登录的——systemd → sshd → bash,这两个变量就是 sshd 传下来的。
注意:
- 为什么每次登录 /dev/pts/N 编号会变?——伪终端按需分配。
- "一切皆文件"如何体现?——设备、管道、socket 都可按文件接口操作。
3.7 环境变量像什么?
环境变量就像你的身份信息、你的学籍信息等,这些东西一生都跟着你。
对应到程序:一个进程启动后,父进程给它的环境变量会一直跟着它;它 fork 出来的子进程、孙子进程也都能看到。
所以环境变量具有"全局"属性(准确说是沿进程树继承)。那问题来了:环境变量是谁给你的?——这个答案就是第四节的正片。
3.8 C 程序获取环境变量的三种方式
方式一:main 的第三个参数(main 函数是有参数的!)
#include <stdio.h>
int main(int argc, char *argv[], char *env[])
{
(void)argc;
(void)argv;
for (int i = 0; env[i] != NULL; i++)
printf("env[%d]: %s\\n", i, env[i]);
return 0;
}
第三个参数就是环境变量表,同样以 NULL 结尾。注意:它没有"个数"参数(不像 argv 有 argc),只能靠 NULL 判断结束。
与前置知识呼应:这个 env[] 就是父进程 bash 传给子进程的——前置知识说"子进程 main 的参数继承自 bash",在这里具象化了:argv 和 env 都是 bash 在 fork/exec 时准备好的。
方式二:environ 全局变量
#include <stdio.h>
#include <unistd.h>
extern char **environ;
int main(void)
{
for (int i = 0; environ[i] != NULL; i++)
printf("environ[%d]: %s\\n", i, environ[i]);
return 0;
}
environ 是 libc 提供的全局指针,指向环境变量表;声明在 <unistd.h> 中。它和 main 的第三参是同一张表。
方式三:getenv(最常用、最佳实践)
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char *path = getenv("PATH");
if (path == NULL)
printf("PATH 不存在\\n");
else
printf("PATH = %s\\n", path);
return 0;
}
- 函数原型:char *getenv(const char *name);,头文件 <stdlib.h>;
- 按名字查找,找到返回值指针,找不到返回 NULL;
- 实际开发优先用 getenv:简单、按名取值、不用自己遍历。
编译运行:
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ gcc -o myenv myenv.c
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ ./myenv
env[0]: XDG_SESSION_ID=36981
env[1]: HOSTNAME=ubuntu-dev
env[2]: TERM=xterm
env[3]: SHELL=/bin/bash
env[4]: HISTSIZE=1000
注意:
- getenv 返回的是表内字符串的指针:不要试图 free;也不要长期保存后假设它不变(环境变量表可能被修改)。
- getenv 返回 NULL 必须判断,直接 printf("%s", p) 可能崩溃。
- 环境变量名大小写敏感。
3.9 安全案例:用 getenv 做"用户白名单"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
char *who = getenv("USER");
if (who == NULL)
printf("无法执行,因为我不认识你\\n");
else if (strcmp(who, "root") == 0)
printf("我也不准 root 执行\\n");
else if (strcmp(who, "mstcheng") == 0)
printf("恭喜你,你是合法用户,我让你执行\\n");
printf("who am I: %s\\n", who);
return 0;
}
以 mstcheng 登录运行 → 放行;以 root 登录运行 → 拒绝。
mstcheng@ubuntu-dev:~$ USER=mstcheng ./check
# 程序立刻认为你是 mstcheng,直接放行!
# 或者:
mstcheng@ubuntu-dev:~$ env USER=mstcheng ./check
你看,我们是不是骗过它了?这说明环境变量是可以被改的。
结论(重要)
- 环境变量是用户可控的输入,绝不能用于身份认证 / 权限判断;
- 真实系统做权限校验,必须用内核提供的可信身份:getuid()、geteuid();
- 这是典型的信任边界问题:跨越信任边界的数据都必须校验。
注意注意:
- 为什么不能用环境变量做鉴权?——可被调用者任意设置。
- getuid 与 geteuid 的区别?——真实 UID vs 有效 UID(setuid 程序)。
- 环境变量注入还能导致什么?——PATH 劫持(把恶意目录放 PATH 前面,同名命令被替换)、LD_PRELOAD 注入(预加载恶意共享库)、IFS 篡改等。
- 不能以为"只有我能改我自己的环境变量"——任何能启动进程的人都能给子进程设置环境变量。
- 用 USER 判断身份,属于典型安全漏洞。
四、核心:环境变量到底存在哪里
4.1 两张表:argv 表与环境变量表
把两张表画在一起:命令行参数表 char *argv[] 和环境变量表 char *env[] / char **environ。
它们的本质完全一样:
- 都是指针数组,每个元素是 char *,指向一个字符串;
- 都以 NULL 结尾;
- 都存放在进程的用户空间内存里。


4.2 是"内存级"的,不是磁盘上的
在前面的3.3节中我们已经铺垫过:export、unset、改 PATH,改的都是当前 bash 进程内存中的表,不会自动写回配置文件。所以:当前 shell 立即生效;所有子进程也生效;退出重登录后恢复原样。
4.3 bash 也是进程
你登录后看到的命令行,是 bash 进程在跑。bash 自己启动时,也要从某处拿到这两张表—— 命令行参数表 + 环境变量表。这两张表就是 bash 进程里的两个全局指针数组。
前置呼应:bash 的这两张表不是凭空来的,是它的父进程(sshd / gnome-shell)传下来的,加上配置文件载入的内容。整条链上每个进程的这两张表都这样。
4.4 为什么子进程不传参也能拿到环境变量
关键在 fork:你在 bash 里执行的每一条外部命令,都是 bash 通过 fork 创建的子进程。fork 之后,子进程拥有父进程地址空间的副本(写时拷贝,后面进程控制说),父进程内存里的两张全局表,子进程天然能看到。所以你的 C 程序 main 不写第三个参数,也能用 environ / getenv——因为表本来就在它继承来的内存里。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
char *argv_tbl[100];
char *env_tbl[100];
int main(void)
{
argv_tbl[0] = "AAA";
argv_tbl[1] = NULL;
env_tbl[0] = "PATH=/usr/bin:/bin";
env_tbl[1] = NULL;
pid_t id = fork();
if (id == 0) {
// 子进程:直接遍历父进程的"全局表"
for (int i = 0; argv_tbl[i]; i++)
printf("argv_tbl[%d] = %s\\n", i, argv_tbl[i]);
for (int i = 0; env_tbl[i]; i++)
printf("env_tbl[%d] = %s\\n", i, env_tbl[i]);
} else {
// 父进程:先不退出,方便观察
sleep(10000);
}
return 0;
}
结论:子进程能直接看到父进程未修改的全局数据。 这就是环境变量"全局"的底层原因。
4.5 进程树:bash 是"祖宗"
//这个是远程登录进程树
systemd / init
└── sshd
└── bash(你登录后的 shell)
├── ls
├── vim
│ └── (vim 再 fork 的子进程)
└── ./myenv
//这是本地 Ptyxis 终端进程树版本
systemd / init
└── gnome-shell # GNOME桌面外壳
└── ptyxis # Ptyxis GUI窗口前端进程
└── ptyxis-agent(4817) # PTY代理、bash的直接父进程
├── bash(pts/0,第一个终端窗口)
│ ├── ls
│ ├── vim
│ └── ./myenv
├── bash(pts/1,第二个终端窗口)
│ └── ...
└── bash(pts/2,第三个终端窗口)
└── ...
从 bash 开始,所有后代进程都能继承环境变量;自己写的进程还可以继续 fork 出"孙子进程",孙子同样能看到。
所以环境变量不是"系统全局",而是"从 bash 往下的整棵子树全局"。
前置呼应:把这条链往上接到前置知识——内核 → systemd(PID 1) → sshd / GDM → gnome-shell → bash → 你的命令,就是一个完整的"祖孙继承"树。systemd 是用户态老祖宗,它的环境变量由内核在启动时直接初始化;此后每一级都是父进程 fork 传下去的。
4.6 只读共享与写时拷贝
子进程只是"读"环境变量时,和父进程共享同一份物理内存(写时拷贝,Copy-On-Write)。一旦子进程要"写",内核才会给它复制一份新的物理页。所以:
- 子进程修改环境变量,不会影响父进程;
- 父进程修改,也不会影响已经启动的子进程(子进程看到的是 fork 那一刻的快照)。
4.7 结论:环境变量是父进程给的
给进程传环境变量的,是父进程,不是操作系统!
系统只负责在你登录时启动第一个 bash,并让 bash 读取配置文件;之后所有进程的环境变量,都是父进程 fork 时传下去的。至于 fork 内部到底怎么复制、怎么传这两张表,进程控制会介绍。
注意:
- 环境变量是"操作系统全局变量",任何进程随便读——实际是继承来的。
- export 后所有已存在进程都能看到——只有之后 fork 的子进程才能看到。
五、bash 的环境变量从哪来:配置文件
5.1 登录时发生了什么(完整流程)
5.2 配置文件的调用链
| 用户级 | ~/.bash_profile | 登录 shell 启动时读取,内部通常 source ~/.bashrc |
| 用户级 | ~/.bashrc | 交互式 shell 配置,常被 .bash_profile 调用 |
| 系统级 | /etc/profile | 所有用户的登录配置 |
| 系统级 | /etc/bashrc(或 /etc/bash.bashrc) | 依发行版而定 |
| 系统级 | /etc/profile.d/*.sh | 按片段拼装系统配置 |
简化调用链:
登录 → ~/.bash_profile → source ~/.bashrc → /etc/bashrc → /etc/profile.d/*
5.3 为什么每个用户必须有自己的家目录
配置文件放在用户家目录下,如 /home/mstcheng/.bash_profile。每个用户登录,读的是自己的配置文件。所以:不同用户可以有不同环境变量;你改了 mstcheng 的配置,root 登录不会受影响。
5.4 "载入进程上下文"是什么意思
配置文件里不是简单的 PATH=… 一行,而是脚本。脚本里可能有 for 循环、if 判断、export、umask 等。bash 执行脚本,把字符串读进来,用 malloc 分配空间、用指针拼成数组,最终形成进程内存中的环境变量表。这个过程就叫"载入进程上下文"。
5.5 验证实验:配置文件真的被读了吗
在 mstcheng 用户的 ~/.bashrc 末尾加:
echo "果然登录时 .bashrc 被执行了"
env
然后:关闭 Xshell → 重新以 mstcheng 登录 → 屏幕上先打印欢迎语,再打印一大堆环境变量 → 说明登录时确实执行了 .bashrc。如果你是本地的虚拟机,那么另起一个窗口就能看到:

再用 root 登录:什么额外输出都没有。因为改的是 mstcheng 的文件,root 读的是 root 自己的配置。
结论:每个用户登录都会启动自己的 bash,并读取自己家目录下的配置文件。
5.6 用 ps 观察 bash 进程
mstcheng@ubuntu-dev:~$ ps axj | grep bash
每多开一个 SSH 会话,就多一个 bash 进程;每退出一个会话,就少一个。全部退出后,系统里就没有你的 bash 了。这个实验在上面演示过!
这再次证明:环境变量是 bash 进程内存里的东西,bash 没了,内存里的表也就没了。
5.7 完整因果链

- 登录 shell 会读哪些文件?——/etc/profile、/.bash_profile、/.bashrc 等(依发行版和 shell 而定)。
- .bash_profile 和 .bashrc 的区别?——前者用于登录 shell,后者用于交互式非登录 shell;很多发行版在 .bash_profile 里 source .bashrc。
- source 和直接执行脚本的区别?——source 在当前 shell 执行,变量留在当前 shell;直接执行是 fork 子 shell,变量不影响父 shell。
注意事项:
- 改了 ~/.bashrc 却不生效:可能你用的是登录 shell,只读 .bash_profile;或没重新登录 / 没 source。
- 在 root 下改配置,普通用户不生效。
- 不同 shell(bash/zsh/fish)配置文件不同,zsh 读 .zshrc。
- 图形界面终端不一定是"登录 shell",读取的文件可能不同。
六、本地变量与环境变量
6.1 自己造一个变量
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ test_env=123456789
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ echo $test_env
123456789
# 但是——
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ env | grep test_env
# 查不到!
# 写个 C 程序去 getenv("test_env"),也拿不到
结论:test_env 只是一个本地变量(shell 变量)。
6.2 本地变量 vs 环境变量
| 定义方式 | name=value | export name=value 或先定义再 export |
| 当前 shell 可见 | 是 | 是 |
| env 能查到 | 否 | 是 |
| 子进程能继承 | 否 | 是 |
| 作用范围 | 仅当前 shell | 当前 shell 及所有后代进程 |
| 类比 C/C++ | 局部变量 | 全局变量 |
6.3 export 的本质
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ export test_env
把已经存在的本地变量写进当前 bash 的环境变量表。之后 env 能看到,子进程也能继承。注意:export 只影响内存,不修改配置文件。也可以一步到位:export AAA=12345(定义 + 导出同时完成)。
6.4 子进程继承验证
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ export TEST_ENV=ABCDEFG
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ ./myenv # 程序里 getenv("TEST_ENV")
ABCDEFG
如果变量不存在:char *p = getenv("NOT_EXIST"); // p == NULL
注意:
- 赋值时等号两边不能有空格:A = 1 会被当成命令 A 加参数。
- $ 只在取值时用:定义时写 A=1,不是 A=$1。
- 单引号内的变量不会展开:echo '$HOME' 输出 $HOME。
- export 后仍然不持久化,重登就没了。
6.5 unset:删除变量
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ unset TEST_ENV
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ unset AAA
从当前 bash 的环境变量表中删除,同样是内存级操作,不影响配置文件。删除后 env 查不到,子进程也拿不到。
6.6 set:查看所有变量
mstcheng@ubuntu-dev:~/117/code/linux-code/process$ set
显示环境变量 + 本地变量 + shell 函数等;env 只显示环境变量。所以:想确认某个变量是不是环境变量 → env | grep 名字;想看全部(含本地)→ set。
补充(内建命令辨析,接上 3.1 的伏笔):
- env 是外部命令(/usr/bin/env),它打印的正是"从 bash 继承来的那张表"——又一次验证继承;
- export / unset / set / cd / pwd 是 bash 内建命令(builtin)——在 bash 进程内部直接执行,不 fork 新进程,直接改 bash 自己的内存表,所以 export 立即生效;
- 这就是为什么"改了配置要 source 才生效":source 也是让 bash 在自己进程里执行脚本。
6.7 本地变量的用途
- 支撑 shell 自身语法:循环变量、条件判断、临时计算:
for i in 1 2 3; do
echo $i
done
- 临时保存信息:比如记住一个时间、一个路径。
- 控制 shell 行为:PS1、PS2、HISTSIZE 等。
6.8 PS1 与 PS2
- PS1:主命令提示符格式。常见组成:\\u(用户名)、\\h(主机名)、\\w(当前路径)、\\$(提示符 $ 或 #)。例:PS1='[\\u@\\h \\w]\\$ ' → [mstcheng@ubuntu-dev ~]$。
- PS2:续行提示符,默认 >。命令没输完(比如引号没闭合、反斜杠续行)时出现。
七、持久化:写进配置文件(呼应 3.3 的"复原"问题)
3.3 问"为什么重登复原"——答案:只改了内存。想不复原,就把 export 写进配置文件。
7.1 写入 ~/.bash_profile
mstcheng@ubuntu-dev:~$ vim ~/.bash_profile
# 加入一行:
export haha="woqu"
保存退出,重新登录:
mstcheng@ubuntu-dev:~$ echo $haha
woqu
原因:每次登录 bash 都会重新读取 ~/.bash_profile 并执行 export。
7.2 系统级持久化
想让所有用户生效:改 /etc/profile、/etc/bashrc 或 /etc/profile.d/*.sh。需要 root 权限。改错可能影响所有用户登录,务必先备份。
7.3 安全提醒
- 不要在配置文件里明文写密码、Token;
- 配置文件权限建议 600,避免其他用户读取;
- 不要随意 source 来路不明的脚本。
注意:
- 写完配置没重新登录,以为没生效——可以 source ~/.bash_profile 立即生效;
- 写到了错误的文件(如改了 .bashrc 但登录 shell 只读 .bash_profile);
- 引号使用错误:export A="hello world" 需要引号,否则空格会
八、总结
一句话:命令行参数是 bash 传给子进程的“指令”,环境变量是 bash 传给子进程的“档案”,都靠 fork 继承、都存在内存里;想持久化,就写进配置文件。


