LINUX系统-08-环境变量(二)
Linux-环境变量二
- 一、环境变量的组织方式
- 二、通过代码获取环境变量
- 1、带参数的命令行程序
- 2、代码获取环境变量
- 1、直接打印env环境变量
- 2、通过第三方变量environ获取
- 3、系统调用获取环境变量
- 三、环境变量的父子进程继承性
- 1、父传子,子不传父,修改不可回溯
- 2、环境变量的全局性、局部性
- 四、系统级修改环境变量
一、环境变量的组织方式
我画了一幅图去阐述环境变量env和PATHSHELLUSER等等环境变量之间的关系。

之前我们再命令行输入env即可以打印全部的环境变量。
不像查看其他的环境变量必须通过echo $PATH这样的手段去打印,因此可以得知我们输入命令行的env本身是一个外部命令,而不是一个字符串地址,他也会通过PATH环境变量去搜索找到 /usr/bin/env 后,Shell 会通过系统调用(fork 和 exec)把这个文件加载到内存中运行。
而env命令会直接打印一个环境表,环境表是一个字符指针数组(蓝色方框),其中的指针都指向一个以 结尾的环境字符串。
二、通过代码获取环境变量
1、带参数的命令行程序
在很多年前我们学习C语言的时候,基本架构不是下面这样的:
#include
int main()
{
// 代码
return 0;
}
int main():适合写“Hello World”或者做算法练习。程序一运行,结果就出来了,不需要用户干预。
而是这样的:
#include
int main(int argc,char* argv[])
{
// 代码
return 0;
}
这是编写命令行工具 (CLI) 的标准。它让程序具备了“配置”和“交互”的能力。
为什么要这么写程序呢?argc 和argv是什么呢?
这两个参数是 C 语言程序与操作系统交互的桥梁,它们的作用是接收命令行参数。
什么叫接受命令行参数?
场景一,不带参数运行:
[benjiangliu@VM-4-8-centos ~]$ ./hello
场景二,带参数运行:
[benjiangliu@VM-4-8-centos ~]$ ./hello hello world 123
argc和argv是什么?
argc是参数的数量,假如我写的程序需要3个参数,那么argc = 4。
argv是参数的数组(容器),它的类型是char* ,可以看出它是一个字符指针数组,argv是一个二级指针。
假如argc = 4,那么argv[4],即argv这个数组有4个元素,这个数组的第一个元素argv[0]指向的是程序名及路径“./hello”,而从第一个元素开始,每一个元素都是一个字符指针,每一个字符指针都指向一个字符串的起始地址。
具体看图,它与env的结构基本一致:

argc 的值:4 (因为包含了程序名,总共 4 个)
argv 的内容:
argv[0] -> “./hello”
argv[1] -> “hello”
argv[2] -> “world”
argv[3] -> “123”
哨兵位argv[4] -> NULL
为什么要这么写?
如果不写这两个参数,你的程序就是“死”的,它每次运行都只能做完全相同的事。
加上它们,你的程序就变成了“活”的,可以根据用户的输入做出不同的反应。这在 Linux 工具中无处不在:
ls 命令:ls 和 ls -a 行为完全不同。
cp 命令:cp 需要接收源文件名和目标文件名。
举个例子:
#include
#include
int main(int argc,char* argv[])
{
printf("argv[0] = %s
",argv[0]);
if(argc == 1)
{
printf("命令行参数至少有2个,例如:./xxxx -a|-b|-c|-d
");
return 1;
}
// 字符串比较,在 C 语言底层,就是靠 strcmp 一个一个去比对用户输入了什么。
if(strcmp(argv[1],"-a") == 0)
{
printf("执行该命令的第一个功能
");
}
else if(strcmp(argv[1],"-b") == 0)
{
printf("执行该命令的第二个功能
");
}
else if(strcmp(argv[1],"-c") == 0)
{
printf("执行该命令的第三个功能
");
}
else
{
printf("执行该命令的默认功能
");
}
// 注意此处不是不是越界访问
// argv[argc] 并没有越界,它是 C 语言标准特意为你留好的“哨兵”(Sentinel)。
// 在 C 语言标准(C11/C99)中,对于 main 函数的 argv 参数,有明确的规定:
// argv[argc] 的值被保证为 NULL。
// 这意味着,虽然 argv 数组的有效数据是 argv[0] 到 argv[argc-1],但 C �runtime(运行时库)在构建这个数组时,特意多分配了一个指针的空间,并在最后写入了 NULL。
// 所以,argv[argc] 是一个合法的内存访问,它不是野指针,而是标准定义的“结束标志”
if(argv[argc] == NULL)
{
printf("argv[argc] == NULL
");
}
return 0;
}
[benjiangliu@VM-4-8-centos lesson17]$ ./argc_argv -a
argv[0] = ./argc_argv
执行该命令的第一个功能
2、代码获取环境变量
1、直接打印env环境变量
#include
// 参数数量 参数数组 环境变量
int main(int argc,char* argv[],char* env[])
{
(void)argc;
(void)argv;
// for循环的判断条件中,当env[i]为NULL,说明遍历结束,结束循环
for(int i = 0;env[i];i++)
{
printf("env[%d] = %s
",i,env[i]);
}
}
2、通过第三方变量environ获取
1#include <stdio.h>
2 #include <stdlib.h>
3
4 int main(int argc,char* argv[])
5 {
6 (void)argc;
7 (void)argv;
8 extern char** environ;
// 因为libc中定义的全局变量environ指向环境变量表env
// environ没有包含在任何头文件中,所以需要使用extern进程声明
9
10 int i = 0;
11 for(;environ[i];i++)
12 {
13 printf("%s
",environ[i]);
14 }
15 return 0;
16 }
3、系统调用获取环境变量
通过getenv(“变量名称”)获取环境变量
#include
#include
int main()
{
printf("%s
",getenv("PATH"));
return 0;
}
三、环境变量的父子进程继承性

在学习进程的时候,我们知道在OS内核的外围与用户交互的shell层,例如我们使用ssh连接远程服务器(阿里云、腾讯云)服务的时候,就相当于在后来新建了一个shell。
但是这个shell是一个广义的概念,你使用的shell可能不叫shell而是叫bash之类的名称,而他们都是命令行解释器,它的主要职责是接收用户输入的命令(如 ls, cd),解释这些命令并调用内核的功能来执行,最后将结果返回给用户。它就是用户与冰冷的内核之间的“桥梁”。但本质上都是为了实现OS内核与用户交互的中间层。
这本质上与程序通过fork创建子进程一样,bash是一个父进程,而我们通过命令行输入的 类似lscd这些命令被bash执行为子进程。
1、父传子,子不传父,修改不可回溯
在之前的进程学习中我们可以知道,父进程可以继承父进程数据和代码,所以也可以获得bash的环境变量,但是我们修改子进程的数据和代码,不会影响父进程的数据和代码,或者说,我们修改子进程的环境变量,不会影响父进程的环境变量。
[benjiangliu@VM-4-8-centos ~]$ bash # 在当前bash新建一个bash
[benjiangliu@VM-4-8-centos ~]$ export NEWENV="hello world" // 使用export命令导入一个新的环境变量NEWENV
[benjiangliu@VM-4-8-centos ~]$ env
XDG_SESSION_ID=458969
HOSTNAME=VM-4-8-centos
SHELL=/bin/bash
TERM=xterm
HISTSIZE=3000
SSH_CLIENT=220.205.248.254 54201 22
SSH_TTY=/dev/pts/2
USER=benjiangliu
NEWENV=hello world # 我们导入的新的环境变量
HOME=/home/benjiangliu
SHLVL=2 # 这里可以看出我们嵌套了2层bash
LOGNAME=benjiangliu
SSH_CONNECTION=220.205.248.254 54201 10.0.4.8 22
LESSOPEN=||/usr/bin/lesspipe.sh %s
PROMPT_COMMAND=history -a; history -a; history -a; history -a; printf "]0;%s@%s:%s" "${USER}" "${HOSTNAME%%.*}" "${PWD/#$HOME/~}"
XDG_RUNTIME_DIR=/run/user/1001
HISTTIMEFORMAT=%F %T
_=/usr/bin/env
[benjiangliu@VM-4-8-centos ~]$ exit # 退出新建的bash,回到初始的bash
exit
[benjiangliu@VM-4-8-centos ~]$ env # 在初始的bash中,发现我们无法找到之前添加的NEWENV这条环境变量
XDG_SESSION_ID=458969
HOSTNAME=VM-4-8-centos
TERM=xterm
SHELL=/bin/bash
HISTSIZE=3000
SSH_CLIENT=220.205.248.254 54201 22
SSH_TTY=/dev/pts/2
USER=benjiangliu
LD_LIBRARY_PATH=:/home/benjiangliu/.VimForCpp/vim/bundle/YCM.so/el7.x86_64
MAIL=/var/spool/mail/benjiangliu
PATH=/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/benjiangliu/.local/bin:/home/benjiangliu/bin
PWD=/home/benjiangliu
LANG=en_US.utf8
SHLVL=1
HOME=/home/benjiangliu
LOGNAME=benjiangliu
SSH_CONNECTION=220.205.248.254 54201 10.0.4.8 22
LESSOPEN=||/usr/bin/lesspipe.sh %s
PROMPT_COMMAND=history -a; history -a; printf "]0;%s@%s:%s" "${USER}" "${HOSTNAME%%.*}" "${PWD/#$HOME/~}"
XDG_RUNTIME_DIR=/run/user/1001
HISTTIMEFORMAT=%F %T
_=/usr/bin/env
我们在初始bash下新建了一个bash,然后修改新建的这个bash的环境变量,查看新建的环境变量,发现除去我们新添加的那行NEWENV其他的环境变量全部继承至初始bash,这是所谓的父传子,也是fork时的复制机制导致的。
因为这个新建的bash是初始bash的子进程,所以我们修改新建bash的环境变量,不会影响到初始bash的环境变量,这是所谓的子不传父,其本质是写时拷贝保证进程内存空间的独立性。
这就是所谓的父传子,子不传父,修改不可回溯!
2、环境变量的全局性、局部性
所谓环境变量通常具有全局属性,其就是环境变量可以被子进程继承下去,这点我们已经证明了。
当我们使用export命令向环境变量表添加新环境变量时,因为使用export命令,这条新添加的环境变量,就是全局变量,可以被子进程继承,上面已经说过这个例子,不再赘述。
而我们没有使用export命令,添加的就是当前bash的局部变量。
[benjiangliu@VM-4-8-centos ~]$ NEWENV="hello world" # 不加export,NEWENV就是一个当前bash的普通的局部变量
[benjiangliu@VM-4-8-centos ~]$ echo $NEWENV
hello world
[benjiangliu@VM-4-8-centos ~]$ env | grep NEWENV
# 打印为空
但是事实上没有“局部环境变量”这个东西
环境变量: 存在于环境表中,具有全局(继承性)属性,子进程可见。
局部变量: 存在于Shell 的内存中,具有局部属性,仅当前 Shell 进程自己可见。
所以,准确的说法应该是:“局部变量” 和 “环境变量”。
你可以把 Shell(Bash)想象成一个包工头,而子进程(如 ls, vim)是打工仔。
场景 A(不加 export): 你(包工头)在自己的办公室里写了一张便签条 NEWENV=“hello” 贴在自己电脑上。
结果: 这只是你的个人笔记(局部变量)。当你派打工仔(子进程)去干活时,你并没有把这张便签条复印一份给他,所以他根本不知道 NEWENV 的存在。
验证: env 命令查看的是“发给打工仔的装备清单”(环境表),你的个人便签条不在这个清单上,所以查不到。
场景 B(加 export): 你把这张便签条正式录入到项目资料库(环境表)中。
结果: 现在它变成了项目规范(环境变量)。每当你要派打工仔(子进程)出去干活,系统会自动复印一份最新的项目资料库给他。
验证: 打工仔(子进程)启动后,也能查到这份资料。
四、系统级修改环境变量
我们在登录用户的家目录中(home目录)中ls -al可以看到很多文件,其中2个隐藏文件,.bash_profile 和 .bashrc就是环境变量的配置文件
[benjiangliu@VM-4-8-centos ~]$ pwd
/home/benjiangliu
[benjiangliu@VM-4-8-centos ~]$ ls -al
total 180
drwx------ 15 benjiangliu benjiangliu 4096 Jan 27 11:33 .
drwxr-xr-x. 5 root root 4096 Aug 28 11:41 ..
-rw-r--r-- 1 benjiangliu benjiangliu 193 Oct 18 19:08 .bash_profile
-rw-r--r-- 1 benjiangliu benjiangliu 359 Sep 1 18:46 .bashrc
# 可以看到 .bash_profile 和 .bashrc 两个配置文件
# 省略其他文件
这两个文件的关系是“主次分明,各司其职”。
简单来说,.bash_profile 是“入口”,负责登录时的一次性初始化;而 .bashrc 是“功能库”,负责定义交互式操作的细节。为了不让环境配置变得混乱,通常会让 .bash_profile 去调用 .bashrc,实现逻辑复用。
.bash_profile内容
1 # .bash_profile
2
3 # Get the aliases and functions
4 if [ -f ~/.bashrc ]; then
5 . ~/.bashrc
6 fi
7
8 # User specific environment and startup programs
9
10 PATH=$PATH:$HOME/.local/bin:$HOME/bin
11 export PATH
.bashrc内容
1 # .bashrc
2
3 # Source global definitions
4 if [ -f /etc/bashrc ]; then
5 . /etc/bashrc
6 fi
7
8 # Uncomment the following line if you don't like systemctl's auto-paging feature:
9 # export SYSTEMD_PAGER=
10
11 # User specific aliases and functions
12 alias vim='/home/benjiangliu/.VimForCpp/nvim'
13 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:~/.VimForCpp/vim/bundle/YCM.so/el7.x86_64
~
但是这两个环境变量表的初始化文件,为什么看上去和env导出的环境变量表没有关系??
其实.bashrc 和 .bash_profile 只是初始化脚本,而 env 显示的是最终的全量结果。这个结果是由“系统默认值 + 脚本修改 + 父进程继承”共同构成的。以下是导致“少了这么多”的具体原因拆解:
1.、你看到的是“增量”,系统提供的是“基量”
.bash_profile 和 .bashrc 通常只写你想要修改或新增的那部分配置,而不会把操作系统默认生成的几百行配置都写在里面。
系统默认环境: 当你开机或登录时,内核和 init 系统会先建立一个基础的环境(包含 HOME, SHELL, TERM 等)。这些变量在你的 .bashrc 里是找不到的,因为它们是系统自带的。
你的脚本只写“变化”: 你的 .bash_profile 里只写了 PATH=… 和 export PATH,这不代表你的环境里只有 PATH。它只是在系统原有的基础上修改了 PATH。其他没被修改的系统变量(如 HOME)依然存在,但你不需要在文件里写它们。
2.、配置文件被“包含”了(隐藏的代码)
你只看了你自己的 .bash_profile 和 .bashrc,但这两个文件通常会引用系统级的配置文件。
系统级配置: 你的 .bashrc 里有这么一行:. /etc/bashrc(或者 /etc/profile)。
看不见的手: /etc/bashrc 和 /etc/profile 是系统级的配置文件,里面定义了大量的默认环境变量、别名和路径。
结果: env 输出的很多变量其实来自 /etc/profile,而不是你的个人文件。如果你不看那个文件,就会觉得这些变量“凭空出现”了。
新的问题,当我们希望修改环境变量的时候,我们是修改.bashrc还是修改.bash_profile中的内容?
这取决于“你希望这个变量什么时候生效”以及“这个变量是给谁用的”。
简单直接的结论是:绝大多数情况下,写在 .bashrc 里更稳妥;如果你非常确定只在登录时设置一次,且不希望嵌套 Shell 重复设置,才写在 .bash_profile 里。
1、 写在 .bashrc 里(推荐大多数情况)
适用场景:
你修改了 PATH,希望在每一个新打开的终端窗口里都立即生效。
你设置了编译器选项、语言环境(LANG)、或者一些工具的行为变量(如 NODE_ENV)。
你不确定自己改了之后要不要马上开个新终端测试一下。
2、写在 .bash_profile 里(特定场景)
适用场景:
启动任务: 你有一些命令只需要在登录时执行一次,在新建的bash中无法使用。
例子:我希望每次登录bash的时候都输出我的名字
按照之前的说法,我需要再.bashrc中添加一条代码,让每次新建bash重新初始化读取.bashrc的时候,都能够执行一次输出我想要的信息。
1: .bashrc ⮀
1 # .bashrc
2
3 # Source global definitions
4 if [ -f /etc/bashrc ]; then
5 . /etc/bashrc
6 fi
7
8 # Uncomment the following line if you don't like systemctl's auto-paging feature:
9 # export SYSTEMD_PAGER=
10
11 # User specific aliases and functions
12 alias vim='/home/benjiangliu/.VimForCpp/nvim'
13 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:~/.VimForCpp/vim/bundle/YCM.so/el7.x86_64
14 echo "benjiangliu's linux study machine" # 新添加的一行
新建bash时读取.bashrc的 内容
[benjiangliu@VM-4-8-centos ~]$ bash
benjiangliu's linux study machine # 可以看到已经输出了在.bashrc中写的内容
直接通过xshell新建一个ssh连接时
WARNING! The remote SSH server rejected X11 forwarding request.
Last login: Wed Jan 28 12:29:14 2026 from 220.205.248.254
benjiangliu's linux study machine # 可以看到已经输出了在.bashrc中写的内容
[benjiangliu@VM-4-8-centos ~]$
换句话说,如果我写的一条环境变量或者其他指令,在bash初始化的时候,就会直接执行这条代码!







