【Linux 系统篇(十八)】进程(六) :环境变量

能从冰封的土地里
培育出十万朵怒放的蔷薇
Linux 系统篇(十八) —— 环境变量
文章目录
一、命令行参数
1.1 重新认识 main 函数
1.1.1 main 函数不是程序的开始
很多教材都说“main 是程序的入口”,这句话对程序员来说没错,但从底层看,main 并不是程序启动后执行的第一行代码。
我们可以查看汇编代码来验证。编写一个最简单的 C 程序:
#include <stdio.h>
int main()
{
printf("hello world!\n");
return 0;
}
编译后生成可执行文件,用 objdump -d 或 gdb 查看入口点,你会发现程序并不是从 main 开始的。在 Linux 上,程序的入口点是 _start,在 Windows 上则是 _crt_start(或类似)。这个入口函数由 C 运行时库(CRT)提供,它负责初始化进程环境、准备参数,然后才调用 main。
_start 内部的大致逻辑如下(伪代码):
void _start()
{
int ret;
int arg_count = /* 分析 main 函数签名,得到参数个数 */;
if (arg_count == 0)
ret = main();
else if (arg_count == 2)
ret = main(argc, argv);
else
ret = main(argc, argv, env);
exit(ret);
}
编译器在编译时会对 main 函数进行词法、语法分析,判断它声明了几个参数,然后生成不同的调用代码。因此,main 函数可以有 0 个、2 个或 3 个参数,分别对应无参、命令行参数、命令行参数+环境变量。
1.1.2 main 函数的参数
在初学 C/C++ 时,我们大多使用无参的 main。但在 Linux 系统编程中,main 函数最多可以有三个参数:
int main(int argc, char *argv[], char *env[]);
argc:命令行参数的个数(argument count)。argv:字符串指针数组,保存各个参数(argument vector)。env:环境变量字符串指针数组(environment vector)。
其中 argc 和 argv 是最常见的,env 则与环境变量有关。这三个参数在程序启动时由操作系统(更准确说是父进程和内核)自动传递。
有时我们在代码中声明了参数但未使用,编译器可能会警告。为了避免警告,可以显式地“使用”一下,比如:
int main(int argc, char *argv[], char *env[])
{
(void)argc;
(void)argv;
(void)env;
// 其他代码
return 0;
}
1.2 命令行参数的切分与表结构
当我们在终端输入一条命令,比如:
ls -a -l /home
按下回车后,Shell(如 bash)会先接收这一整行字符串,然后以空格为分隔符将其切分成多个子字符串。这些子字符串会被放入一个指针数组中,数组末尾追加一个 NULL 指针作为结束标记。
以 ./code a b c 为例,系统内部构建的 argv 结构如下:

此时 argc 的值是 4(包括程序名本身)。注意,argv[0] 始终是程序本身的路径(可能是相对路径或绝对路径),后面的才是用户传入的参数。
1.3 命令行参数的应用演示
为什么要费这么大劲传递命令行参数?目的是让同一个程序通过不同的选项实现不同功能。比如 ls -a 和 ls -l 就是通过解析不同的选项来输出不同格式的目录信息。
下面是一段 C 演示代码,展示如何遍历参数和根据选项执行不同逻辑:
1 #include<stdio.h>
2 #include<string.h>
3
4 int main(int argc,char *argv[])
5 {
6 //遍历打印所有的命令行参数
7 //for(int i = 0;i < argc;i++)
8 //{
9 // printf("argv[%d]: %s\n",i,argv[i]);
10 //}
11
12 if(argc != 2)
13 {
14 printf("Usage: %s [-a|-b|-c]\n",argv[0]);
15 return 1;
16 }
17
18 const char* arg = argv[1];
19
20 if(strcmp(arg,"-a")==0)
21 printf("这是功能1\n");
22 else if(strcmp(arg,"-b")==0)
23 printf("这是功能2\n");
24 else if(strcmp(arg,"-c")==0)
25 printf("这是功能3\n");
26 else
27 printf("Usage: %s [-a|-b|-c]\n",argv[0]);
28
29 return 0;
30 }
编译运行:

通过命令行参数,我们可以轻松实现类似 ls 这样的多选项程序。
二、环境变量初识:概念与常见变量
2.1 环境变量的概念
环境变量(Environment Variables)是操作系统中用来指定运行环境的一些参数。它们以 名称=值 的形式存在,例如 PATH=/usr/bin:/bin。环境变量通常具有全局属性,可以被当前 Shell 及其启动的所有子进程继承。
一个典型的例子:我们在编译 C/C++ 代码时,链接器需要找到动态库或静态库的位置,但我们通常不需要手动指定库路径,为什么?因为系统通过环境变量(如 LIBRARY_PATH、LD_LIBRARY_PATH 等)提供了默认搜索路径,编译器会自动在这些路径中查找所需的库文件。
2.2 常见环境变量
| 变量名 | 含义 |
|---|---|
PATH | 指定可执行程序的搜索路径,多个路径用冒号 : 分隔 |
HOME | 当前用户的主工作目录(登录后默认所在目录) |
SHELL | 当前使用的 Shell 类型,通常为 /bin/bash |
USER | 当前登录的用户名 |
LOGNAME | 登录用户名(与 USER 基本相同,但某些情况下不同) |
PWD | 当前工作目录 |
OLDPWD | 上一次所在的工作目录(用于 cd -) |
HOSTNAME | 主机名 |
TERM | 终端类型 |
LANG | 系统语言和编码 |
HISTSIZE | 历史命令保存的最大条数 |
SSH_CONNECTION | SSH 连接信息(客户端 IP、端口、服务端 IP、端口) |
2.3 查看环境变量的方法
2.3.1 echo:查看单个变量
echo $HOME
echo $PATH
注意 $ 用于引用变量的值。
2.3.2 env:查看所有环境变量
env
输出会很长,可以配合 grep 过滤:
env | grep PATH
2.3.3 set:查看所有变量(包括本地变量和环境变量)
set
set 会输出当前 Shell 中定义的所有变量,包括环境变量和本地变量。
2.3.4 export:不带参数时也可查看环境变量列表
export
输出结果与 env 类似,但可能包含一些额外的变量(如 Shell 函数等)。
2.4 本地变量与环境变量的区别与转换
在 Shell 中,变量分为本地变量(local variable)和环境变量(environment variable)。
- 本地变量:仅在当前 Shell 进程中有效,子进程无法继承。例如在终端中执行
myvar=100,这个变量只在当前 Shell 中存在。- 环境变量:当前 Shell 及其所有子进程都能访问。通过
export命令可以将本地变量导出为环境变量。
2.4.1 定义本地变量
my_var="hello world"
i=1
注意:等号两边不能有空格;如果值包含空格,需要用双引号或单引号包裹。
2.4.2 导出为环境变量
# 先定义后导出
var1="ubuntu"
export var1
# 或者直接定义并导出
export var2="centos"
2.4.3 删除变量
unset var1
unset var2
删除后变量就不存在了,可以通过 echo $var1 验证(输出为空)。
2.5 实验:为什么执行自己的程序必须加 ./?
假设我们写了一个简单的程序 hello,编译后生成可执行文件。如果直接输入 hello,会提示 command not found,但输入 ./hello 就能运行。这是为什么呢?
原因在于 PATH 环境变量。当我们输入一个命令时,Shell(bash)会在 PATH 变量指定的目录列表中从左到右搜索与该命令同名的可执行文件。如果找到,就执行;否则报错。
查看 PATH 的内容:

可以看到,系统默认只搜索这些目录。我们自己编译的程序通常放在当前目录或自定义目录,而这些目录并没有包含在 PATH 中,所以直接输入 hello 时 Shell 找不到它。
解决办法有几种:
- 使用相对路径
./hello或绝对路径/path/to/hello,显式告诉 Shell 程序的位置。- 将程序所在目录添加到
PATH中(临时或永久)。- 将程序复制或链接到
PATH中的某个目录(如/usr/local/bin)。
实验:将程序路径加入 PATH
# 假设 hello 程序位于 /home/user/mybin 目录下
export PATH=$PATH:/home/user/mybin
然后直接输入 hello 就能运行了。注意这种修改只在当前终端会话有效,重新打开终端后会失效(因为 PATH 是从配置文件加载的)。
实验:对比 root 和普通用户的 HOME
# root 用户
$ echo $HOME
/root
# 普通用户(如 student)
$ echo $HOME
/home/student
HOME 变量决定了用户登录后默认进入的目录。执行 cd ~ 实际上就是切换到 $HOME 指定的目录。
三、环境变量的底层组织
3.1 环境变量表的结构
每个进程在启动时都会收到一张环境变量表(environment table)。这张表是一个字符指针数组,每个指针指向一个以 '\0' 结尾的字符串,字符串格式为 NAME=value。数组的末尾是一个 NULL 指针,作为结束标记。
3.2 环境变量的内存级修改与磁盘级修改
3.2.1 内存级修改(临时修改)
在终端中直接修改变量的值,只影响当前 Shell 进程及其后续子进程,不会改变配置文件,因此重新打开终端后修改就会消失。
例如,清空 PATH:

可以看到,清空 PATH 后,连 ls 都找不到了(因为 Shell 不知道去哪个目录找 ls)。但注意,一些 Shell 内建命令(如 cd、echo、export)仍然可以使用,因为它们不是通过 PATH 查找的外部程序,而是 Shell 自身提供的。
恢复 PATH 需要重新登录或手动设置。
追加式修改更常见,例如:
export PATH=$PATH:/home/user/mybin
这样就在原有 PATH 基础上添加了一个新目录,不会丢失原有内容。
3.2.2 磁盘级修改(永久修改)
如果希望修改永久生效,需要修改配置文件。Linux 系统中,当用户登录时,Shell 会读取一些初始化脚本,设置环境变量。最重要的两个用户级配置文件是:
~/.bash_profile(或~/.profile)~/.bashrc
通常,~/.bash_profile 会调用 ~/.bashrc,而 ~/.bashrc 又会调用系统级的 /etc/bashrc。在这些文件中,我们可以使用 export 命令来设置环境变量。
例如,在 ~/.bashrc 末尾添加:
export MY_CUSTOM_VAR="hello"
export PATH=$PATH:/home/user/mybin
保存后,重新登录或执行 source ~/.bashrc 使修改立即生效。
不建议随意修改系统级配置文件,尤其是 PATH,因为错误的修改可能导致系统命令无法使用。一般只在用户级配置文件中添加自定义路径。
3.3 环境变量的全局属性与继承
环境变量之所以称为“全局”,是因为它可以被子进程继承。当一个进程通过 fork() 创建子进程时,子进程会复制父进程的环境变量表。这意味着父进程设置的环境变量,子进程可以直接访问。
实验:验证环境变量的继承
编写一个程序,打印自定义环境变量 MYENV 的值:
#include <stdio.h>
#include <stdlib.h>
int main()
{
char *env = getenv("MYENV");
if (env)
printf("MYENV = %s\n", env);
else
printf("MYENV not found\n");
return 0;
}
编译后,先直接运行:

因为当前环境中没有这个变量。
然后导出环境变量再运行:

这说明子进程(我们的程序)继承了父进程(bash)的环境变量。
注意:本地变量不能被继承
如果只是定义本地变量而不导出:

因为 MYENV 只是 bash 的本地变量,不会传递给子进程。
四、在代码中获取和操作环境变量
4.1 通过 main 函数第三个参数获取
main 函数的完全体可以有三个参数,第三个参数就是环境变量表:
#include <stdio.h>
int main(int argc, char *argv[], char *env[])
{
for (int i = 0; env[i] != NULL; i++)
printf("env[%d]: %s\n", i, env[i]);
return 0;
}
这种方法可以遍历所有环境变量,但实际开发中很少使用,因为它要求修改 main 签名,而且不方便获取单个变量。
4.2 通过 getenv 函数获取指定变量
C 标准库提供了 getenv 函数,用于获取指定名称的环境变量:
#include <stdio.h>
#include <stdlib.h>
int main()
{
char *path = getenv("PATH");
if (path)
printf("PATH = %s\n", path);
else
printf("PATH not found\n");
return 0;
}
getenv 返回指向环境变量值的指针,如果变量不存在则返回 NULL。注意:不要修改返回的字符串,也不要 free,因为它指向的是进程环境表中的内存。
4.3 通过全局变量 environ 获取
C 库提供了一个全局二级指针 environ,它指向环境变量表的首地址。使用前需要手动声明:
#include <stdio.h>
extern char **environ;
int main()
{
for (int i = 0; environ[i] != NULL; i++)
printf("%s\n", environ[i]);
return 0;
}
这种方式与 main 第三个参数类似,但不需要修改 main 签名。
4.4 通过子进程继承获取环境变量
前三种方法都是在当前进程内读取环境变量。而环境变量的全局性体现在子进程自动继承父进程的环境变量表。我们可以利用这一特性,在父进程中设置环境变量,然后 fork 创建子进程,子进程就能直接读取。
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
// 父进程设置环境变量
setenv("MY_TEST_VAR", "Hello_Linux_Process", 1);
printf("[父进程] 已设置环境变量 MY_TEST_VAR\n");
fflush(NULL); // 刷新缓冲区,防止子进程重复打印
// 创建子进程
pid_t id = fork();
if (id == 0) {
// ===== 子进程 =====
printf("[子进程] 开始运行\n");
char *val = getenv("MY_TEST_VAR");
if (val != NULL)
printf("[子进程] 继承成功,值为: %s\n", val);
else
printf("[子进程] 没找到这个变量\n");
} else {
// ===== 父进程 =====
wait(NULL); // 等待子进程结束
printf("[父进程] 子进程已结束\n");
}
return 0;
}
4.5 设置环境变量的函数:setenv 和 putenv
除了在 Shell 中使用 export,在 C 代码中也可以设置环境变量,常用函数有:
int setenv(const char *name, const char *value, int overwrite);- 若
overwrite非 0,则覆盖已存在的同名变量;否则不覆盖。
- 若
int putenv(char *string);- 参数形式为
"NAME=value",直接修改环境表,使用需谨慎(字符串必须保持有效)。
- 参数形式为
这些函数会影响当前进程及其后续子进程,但不会影响父进程(如 Shell)。
五、其他重要环境变量与实用技巧
5.1 所有环境变量罗列与解释
下面是一个典型 Linux 系统中 env 输出的变量列表,以及它们的含义(部分):
| 变量名 | 含义 |
|---|---|
SHELL | 当前 Shell 解析器,如 /bin/bash |
HISTCONTROL | 历史命令控制,如 ignoredups 忽略连续重复命令 |
HISTSIZE | 历史命令保存的最大条数,如 1000 |
HOSTNAME | 主机名 |
PWD | 当前工作目录 |
LOGNAME | 登录用户名 |
XDG_SESSION_TYPE | 会话类型,如 tty |
HOME | 用户主目录 |
LANG | 系统语言编码,如 en_US.UTF-8 |
LS_COLORS | ls 命令的颜色配置 |
SSH_CONNECTION | SSH 连接信息 |
TERM | 终端类型 |
USER | 当前用户名 |
PATH | 可执行程序搜索路径 |
MAIL | 用户邮箱路径 |
OLDPWD | 上一次所在目录 |
_ | 上一个执行的命令或程序的路径 |
5.2 USER 与 LOGNAME 的区别
大多数情况下,USER 和 LOGNAME 的值相同,都表示当前用户名。但它们在特定场景下可能不同:
LOGNAME是用户登录时确定的用户名,通常不可更改。USER是当前进程的有效用户名,可以通过su或sudo切换用户而改变。


5.3 历史命令与 HISTSIZE
Bash 会记录用户输入过的命令,以便通过上下键或 ctrl+r 快速查找。HISTSIZE 变量指定保存的历史命令最大条数。
常用操作:
history:查看历史命令列表。history | wc -l:查看当前保存的历史命令数量。!v:执行最近一条以v开头的命令。ctrl+r:反向搜索历史命令。
这些功能依赖于 Bash 的历史记录机制,而历史记录文件通常位于 ~/.bash_history(或由 HISTFILE 指定)。
六、总结与面试要点
6.1 环境变量的基本概念
- 什么是环境变量?
环境变量是操作系统中用来指定运行环境的一些参数,以NAME=value形式存储,具有全局属性,可被子进程继承。- 常见环境变量有哪些?
PATH(命令搜索路径)、HOME(用户主目录)、SHELL(当前 Shell)、USER(当前用户)、PWD(当前目录)等。
6.2 操作环境变量的命令
echo $VAR:查看变量值。export VAR=value:设置/导出环境变量。unset VAR:删除变量。env:查看所有环境变量。set:查看所有变量(包括本地变量)。
6.3 为什么执行自己的程序要加 ./?
因为当前目录通常不在 PATH 环境变量中,Shell 找不到程序。加 ./ 显式指定路径即可执行。也可以修改 PATH 加入当前目录。
6.4 环境变量的组织方式
每个进程都有一张环境变量表,是一个字符指针数组,每个元素指向 NAME=value 字符串,以 NULL 结尾。
6.5 在代码中获取环境变量的方法
- main 函数第三个参数
char *env[]。getenv("NAME")获取指定变量。- 外部全局变量
extern char **environ。- 子进程通过继承父进程环境变量获得。
6.6 本地变量和环境变量的区别
本地变量仅在当前 Shell 进程中有效,不能被子进程继承;环境变量可以被所有子进程继承。使用 export 可将本地变量转为环境变量。
6.7 如何让环境变量永久生效?
修改用户级配置文件 ~/.bashrc 或 ~/.bash_profile,添加 export 语句,然后重新登录或 source 文件。
6.8 环境变量的全局性如何理解?
当一个进程创建子进程时(如 fork),子进程会复制父进程的环境变量表,因此环境变量可以传递给所有后代进程。但子进程对环境变量的修改不会影响父进程。
结语:
今天的内容到这里就结束了,希望你能有所收获~
干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)