Linux 环境变量
1. 基本概念
在 Linux 操作系统原理中,环境变量(Environment Variables) 是一类在 操作系统层面定义的全局变量,用于配置操作系统行为和影响进程运行环境。它们通常以 键=值 的形式存在,作用范围可以是用户会话、进程,也可以被继承到子进程。
- 环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数
- 如:我们在编写
C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找 - 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
环境变量的本质
- 本质上是 以字符串形式存储在内存中的一组键值对
- 每个运行中的进程都维护着一份自己的环境变量表,初始化来源是其父进程
2. 认识常见的环境变量
PATH
- PATH: Linux 系统的指令搜索路径
- 为什么我们输入系统的指令 (
ls pwd) 运行时不需要加./,而运行我们自己写的程序运行时需要加./
- 为什么我们输入系统的指令 (
系统当中,针对于指令的搜索,Linux 系统会为我们提供一个环境变量 PATH。
- 环境变量
PATH是系统开机就已经在shell中存在的
查看 PATH
查看 PATH 的内容:
我们查看环境变量,需要借助 echo 指令。
echo PATH:直接输出PATH,PATH默认会被当成字符串处理- 查看
PATH中的内容需要使用$。echo $PATH
查看任何环境变量的值都需要在环境变量名前加上$,一般使用命令 echo $envName
# 查看环境变量的内容
echo $PATH
# $表示取环境变量的内容
修改 PATH
PATH 的内容是一系列路径,路径与路径之前用 : 分隔
- 输入
ls,shell会在这些被冒号分隔开的路径中依次查找 ls 程序。- 找到的话,执行。因为
ls处于路径/usr/bin路径下,路径/usr/bin位于环境变量PATH中,所以系统可以找到直接执行
- 找到的话,执行。因为
- 输入我们自己的程序
mycmd,该程序不在PATH中包含的各个路径下,mycmd程序所处的路径也不在环境变量PATH中,因此找不到,最终提示command not found。这个查找工作一般都是shell来完成的
那么这就意味着,我们自己编写的程序,想要像指令一样不加路径直接运行,有两种方法:
- 将我们的自己的程序放入 PATH 中存在的路径中:
- 以程序
mycmd为例,只需要将其移动到环境变量PATH中的目录即可 - 使用
mv mycmd /usr/bin命令,常见系统的指令通常位于/usr/bin目录下
- 以程序
- 将我们自己的程序所处的路径加入到
PATH环境变量中
接下来我们演示第二种方法,并演示 PATH 的修改
- 环境变量
PATH的访问和修改方法
# 查看环境变量的内容
echo $PATH
# $表示取环境变量的内容
# 修改 PATH=$PATH:newContent # PATH=newPath # 错误行为 会把 PATH 中之前的内容覆盖掉
修改环境变量使用等号 =,这里的等号可以类比为赋值,赋值会把 PATH 中之前的内容覆盖掉。我们需要将指定的路径追加到 PATH 中,因此指令为:PATH=$PATH:newPath,这里的行为依然是覆盖,但是 $PATH 保证了我们取出了 PATH 中已有的内容,并在后面加上 :newPATH,保证了修改是追加新路径
- 通过这种方式修改的
PATH,是一种内存级别的 PATH,在 shell 中保存 - 如果我们错误的修改了
PATH,只需关闭终端后重新登陆shell shell没有启动时,PATH在系统的配置文件中保存。shell启动时,环境变量从系统的配置文件中加载到内存which指令搜索时,也是在环境变量PATH中搜索的
HOME
- HOME : 指定用户的主工作目录 (即用户登陆到 Linux 系统中时,默认的目录)
- 为什么我们在首次登录
Linux时,默认所处的目录是自己的家目录呢? - 为什么默认所处的路径为
/home/userName呢
- 为什么我们在首次登录
原因是,当我们登录时,shell 会识别当前登录的用户名,给当前用户填充 $HOME 环境变量,填充的值为 /home/用户名
当我们使用远程工具登录 Linux 时,命令行解释器会分配命令行解释器,命令行解释器会执行类似 cd $HOME 的命令,因此初次登录时,处于当前用户的家目录
SHELL
- SHELL:当前终端使用的 Shell 程序,它的值通常是/bin/bash
- 如何查看当前系统中使用的是哪一个
shell呢? - 只需要查看环境变量
SHELL中的值,命令为:echo $SHELL
- 如何查看当前系统中使用的是哪一个
# 查看当前系统中正在使用的 shell
echo $SHELL
- 经验证,默认使用的
shell一般是/bin/bash
其他常见环境变量
除了以上环境变量,系统中还有很多其他的环境变量。
环境变量如此多,我们进行查看呢?我们可以使用 env 命令
- env:查看到当前的进程和 bash 进程从系统中继承下来的所有环境变量。
PWD 与 OLDPWD
- 环境变量 PWD:记录当前进程所处的工作路径
- 环境变量 OLDPWD:会记录当前进程所处的工作路径的上个路径
- 我们的
cd -命令会被解析为cd $OLDPWD,因此可以跳转到上次所处的路径中
- 我们的
LOGNAME 与 USER
- 可以看到,当我们在不同用户之间切换时,env 获取到的环境变量值中的
LOGNAME 与 USER也都在同步变化。
SSH_TTY
这里对环境便令 SSH_TTY 简单提一下。SSH_TTY 表示当前终端所使用的字符设备文件。当前 SSH_TTY 的值为 /dev/pts/0
我们再开启一个终端,向该文件中追加内容时,另一个终端中便会显示我们追加的内容。
由环境变量理解权限
使用系统调用获取环境变量
我们可以通过系统调用接口 getenv() 来获取环境变量,需要给该函数传入字符串格式的环境变量名。该函数返回环境变量值,格式为 char 风格的字符串*。
理解权限
- 我们Linux 的权限理解部分,同一个文件,不同的用户在使用时,拥有不同的操作权限
- 那么系统想要给我们限制权限,首先要获取到我们是谁。
- 操作系统可以通过系统调用来获取当前用户是谁
- 不同的用户执行相同的代码时,环境变量获取到的值就不一样,因此可以操作系统可以辨识出当前使用用户的身份
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
// 环境变量,可以让程序获取到当前系统中的用户是谁
int main() {
char who[32];
strcpy(who, getenv("USER"));
if (strcmp(who, "root") == 0) {
printf("你是 root 用户,可以做任何事情\n");
} else {
printf("你是普通用户,受到权限约束\n");
}
return 0;
}
- 有了环境变量,程序内部便可以获取环境变量,从而获知当前是哪个用户正在执行指令的
有了环境变量的存在,系统就有了获知当前是哪个用户正在执行指令的能力,获取后就可以将文件属性中的拥有者、所属组和文件所对应的权限进行比对,进而判定当前用户有无特定的权限
3. 总结什么是环境变量
经过以上对环境变量的熟悉和使用,我们可以得出环境变量的最终概念
环境变量是操作系统提供的一组 name=value 形式的变量,不同的环境变量有不同的用途,通常具有全局属性
环境变量的本质
- 本质上是 以字符串形式存储在内存中的一组键值对
- 每个运行中的进程都维护着一份自己的环境变量表,初始化来源是其父进程。
| 功能 | 示例变量 | 说明 |
|---|---|---|
| 系统路径 | PATH | 指定可执行程序查找路径 |
| 当前用户信息 | USER, HOME | 当前登录用户和主目录 |
| 默认语言 | LANG | 控制系统语言和字符集 |
| 编辑器 | EDITOR | 系统默认文本编辑器 |
| 库路径 | LD_LIBRARY_PATH | 指定动态库搜索路径 |
| shell 类型 | SHELL | 当前 shell 程序路径 |
4. 命令行参数和环境变量的全局性
命令行参数
什么是命令行参数?
- 命令行参数:是指在命令行界面运行程序时,跟随在程序名称后的额外输出参数,用于控制程序的运行或传递数据
我们 C/C++ 的 main 函数是可以有参数的
int main(int argc, char* argv[]) {
return 0;
}
- 其中:
argc是一个整数,argv是一个字符串数组 (指针数组)argv字符串数组的大小由argc决定
main 函数也是函数,因此main 函数可以调用其他函数,同时也可以被其他函数调用。C/C++ 程序运行起来时,int argc 和 char* argv[] 这两个参数会被调用方进行传参。
- 用户层面上:
main函数是被第一个调用的函数 - 系统层面上:第一个调用的函数是Startup() 函数或 CRTStartup() 函数,该函数会调用
main函数,给main函数传参
使用以下代码查看 char* argv[] 中的内容:
int main(int argc, char* argv[]) {
int i = 0;
for (; i < argc; ++i) {
printf("argv[%d]->%s\n", i, argv[i]);
}
return 0;
}
可以看到 char* argv 中的内容,就是我们输入的指令和附带的选项。
argv[0]是./mycmdargv[i]是后面附带的不同选项
为什么会这样呢?
这是因为,我们在 bash 中输入的指令,本质上都是字符串,bash 会把他们以空格为分隔符,分割成子字符串,分割成多少个字符串,就初始化 argc 为多少
打散后,argv 指针数组中,每个空间,存放的都是相应字符串的地址。最后一个空间存放的是空指针 NULL
- 在系统层面上,被打散的字符串的地址存入
char* argv[]中并初始化argc。shell命令行解释器,会把这两个参数传递给程序的main函数。 - 既然
char* argv[]的最后一个位置的值为NULL,那么我们遍历argv[]就有了新的方式:- for 循环的第二个位置表示循环终止条件,当
argv[i]的值为NULL时,循环结束,也就完成了对argv[]的遍历
- for 循环的第二个位置表示循环终止条件,当
int main(int argc, char* argv[]) {
int i = 0;
for (; argv[i]; ++i) {
printf("argv[%d]->%s\n", i, argv[i]);
}
return 0;
}
以上就是命令行参数。以空格为分隔符。将输入的指令分割的过程,就叫做命令行解析
为什么要有 main 函数要有命令行参数?
- 我们
Linux中的指令都是 C 语言写的main函数的命令行参数为指令、工具、软件提供命令行选项的支持- 调用同一个程序时,使用不同的选项,可以完成不同的功能
main 函数的第三个参数
main 函数不只有 argc 和 argv 两个参数,还可以带第三个参数,如下:
int main(int argc, char* argv[], char* env[]) {
return 0;
}
- 这里的第三个参数
char* env[]表示的是从父进程继承过来的环境变量,其内容和env命令输出的内容一模一样- 使用以下代码验证
// C/C++ 程序 两张表 main 函数的第三个参数
// 1. 命令行参数 向量表 2. 环境变量 向量表 这两张表的结构一模一样
int main(int argc, char* argv[], char* env[]) {
int i = 0;
// argv 和 env 的最后一个元素存储的是 NULL,遍历到结尾时,条件自动为假,所以退出循环
for (; env[i]; ++i) {
printf("env[%d]->%s\n", i, env[i]);
}
return 0;
}
- 综上:通过参数和
char* env[]查看的环境变量和env命令查看到的环境变量一模一样char* argv[]和char* env[]的结构一模一样,且最后一个元素都是NULL指针
- 不能简单的认为程序启动时,就是简单的将程序加载到内存。
而是程序在启动时,Startup 函数会调用 main 函数,给 main 函数传递参数,传递两张表。命令行参数表和环境变量表。
环境变量具有全局属性
我们在命令行下所运行的所有进程,都是 bash 的子进程。bash 在启动的时候,会从操作系统的配置文件中读取环境变量信息。子进程会继承父进程交给我的环境变量。子进程可以通过 main 函数的第三个参数 char* env[] 访问环境变量。
- 而子进程再创建子进程,环境变量表就被无穷尽的传递下去了,因此说,环境变量具有全局性
环境变量也是进程的数据,进程具有独立性,父子进程 fork 之后的代码共享,数据,读时共享,写时拷贝
- 一个进程还没有创建子进程时,如果已经创建好了环境变量,再创建子进程,就会被子进程继承下去
- 环境变量的继承有两种方式
main函数传参- 直接继承
5. 环境变量可以被子进程继承
如何验证环境变量可以被子进程继承呢?
我们可以在系统的环境变量中加入一个我们自己的环境变量
增加环境变量命令:
export [name=value]
# env 可以查看全部的环境变量
# echo $PATH 等可以查看特定环境变量的内容
# 那么如何增加环境变量
# 增加该环境变量 MY_VALUE=123456
export MY_VALUE=123456
export增加环境变量后,env命令中就多了我们添加的环境变量export增加环境变量后,我们运行的bash的子进程./mycmd中,获取到的环境变量也有我们新增加的环境变量
这是因为:环境变量可以被子进程继承
- 发源进程只有一个,是
bash,因此bash及其的所有子进程构成的进程树,拥有相同的环境变量
如果今天我想让我的所有进程都遵守一套规则,我可以把这套规则放在 bash 的环境变量中,这样所有的子进程都会有相同的环境变量
系统中的权限,每条指令都应该遵守,这些功能的实现就和环境变量有关
取消环境变量:
# 取消我们刚刚新增的环境变量 MY_VALUE
unset MY_VALUE
bash 进程取消环境变量 MY_VALUE 后,之后的子进程就也没有相应的环境变量了,读者可以自行验证
- 另外,编译器在编译时,会对 main 函数的命令行参数进行检查,会进行条件编译。如果
main函数有参数,会传入相应个数的参数。没有参数,就不传入参数
6. 本地变量与内建命令
本地变量
与环境变量对应的还有本地变量。
什么是本地变量?本地变量就是我们直接在 shell 中定义的变量
# 直接在 bash 命令行中定义
a=1
b=2
c=3
本地变量只会在本 bash 内部有效,不会被子进程继承
set 命令:可以显示系统中所有的变量。包括环境变量和本地变量
本地变量不能被子进程继承
- 我们用本地变量
MYVAL来测试,以下程序查看MYVAL的值
int main(int argc, char* argv[], char* env[]) {
printf("MYVAL: %s\n", getenv("MYVAL"));
return 0;
}
内建命令
-
观察以下输出,思考问题
-
前文提到,
bash命令行中执行的程序,都是bash的子进程。 -
前文提到,本地变量只在本
bash内部有效,不会被子进程继承 -
那么,
echo $MY_val命令,echo也是Bash的子进程,./mycmd也是bash的子进程,无法获取到本地变量。echo也是bash的子进程,为什么能取到本地变量的?
我们纠正之前的结论:命令行中所启动的指令,不一定全都要创建子进程
我们的命令要被分成两类:
- 常规命令:执行时,
bash通过创建子进程执行 - 内建命令:执行时,
bash不创建子进程,而是bash自己亲自执行的。类似于bash 调用了自己写的或者系统提供的函数。已知常见的内建命令如下:echo:echo 是由 bash 执行的,bash 内部有一个函数叫echo,echo $MY_val时,echo 获取到 bash 内部的本地变量输出出来就行了cd:每个进程都有自己的当前工作目录,如果 cd 命令在执行时创建子进程,那么 cd 改变的将是子进程的工作目录,而不会改变主进程的工作目录
结合系统调用体会内建命令 cd
bash 也是一个程序,其内部在实现时,读取到用户输入 cd 时,bash 并不会创建子进程,而是直接调用系统调用 chdir 改变 bash 程序当前的路径。
我们可以用如下代码进行验证:
由于我们执行 mycmd 时,bash 会创建子进程,因此我们无法直接观察到 bash 的工作路径改变,我们可以观察 /proc 目录下进程的 cwd 目录
// 验证 cd 命令
int main(int argc, char* argv[]) {
printf("before change:\n");
sleep(40);
if (argc == 2) {
chdir(argv[1]);
}
printf("change end:\n");
sleep(20);
return 0;
}
- sleep() 是为了给我们输入指令查看进程的 cwd 留出时间
通过 C 语言提供的外部变量获取环境变量
// 通过 C 语言提供的 外部变量 environ 获取环境变量
int main() {
int i = 0;
extern char** environ;
for (; environ[i]; ++i) {
printf("%s\n", environ[i]);
}
return 0;
}
至此,我们已经系统地学习了 Linux 环境变量的方方面面:它们的结构、用途、修改方式、继承机制,以及它们在程序启动和系统权限管理中的关键角色。从 PATH 到 HOME,从命令行参数到环境变量表,从本地变量的局部性到子进程的继承性,环境变量贯穿了整个 Linux 系统的运行脉络。

