Linux----环境变量
环境变量
环境变量就是操作系统为进程提供的一组“键值对”形式的配置信息。
形式上像这样:
也就是说,环境变量是一种:
-
全局性的
-
键值对形式的
-
可被进程读取的配置参数
环境变量的作用
环境变量主要用于:
-
控制程序行为(如
LANG
控制语言环境) -
指定系统路径(如
PATH
决定可执行文件搜索路径)
使用getenv函数可以得到对应字段的值:
与echo指令输出的一样:
也可以为PATH变量加一些内容:注意要使用$PATH:连接后面的内容,如果直接PATH=.....就会导致覆盖原来PATH的内容。这个修改只是当前 shell 临时生效,所以你关闭当前终端、重新打开新的终端,PATH 会恢复成登录时的默认值。
同样,在linux下可以打印USER环境变量,每一个用户对应一个USER,对应一个家目录
举个例子 👇
当你在终端输入:
python3
系统其实会去找 PATH
环境变量:
PATH=/usr/local/bin:/usr/bin:/bin
它会依次在 /usr/local/bin
、/usr/bin
、/bin
目录下找 python3
文件,找到就运行。
三、环境变量与进程的关系
在 Linux(或类 Unix 系统)中:
每个 进程都有自己的一份环境变量表。
-
当你登录一个用户时,系统会根据该用户的配置文件加载一组环境变量(如
.bashrc
、.profile
)。 -
当你运行一个新程序时,父进程(如 shell)会拷贝一份自己的环境变量表传给子进程。
也就是说:
不是全系统共享一个表,而是每个进程都有自己的一份拷贝。
四、用户与环境变量的关系
你问得非常好:“一个用户对应一组环境变量吗?”
回答是:
✅ 是的,一个用户登录后,系统会为他加载特定的一组环境变量。
但这组变量只是该用户的默认配置,不同进程可以修改自己的副本。
举例说明:
用户 | 环境变量 HOME | 环境变量 PATH |
---|---|---|
userA | /home/userA | /usr/local/bin:/usr/bin:/bin |
userB | /home/userB | /usr/local/bin:/usr/bin:/bin |
-
用户 A 登录 → 拥有属于自己的环境变量表
-
用户 B 登录 → 也有自己的环境变量表
-
二者互不影响
五、环境变量在进程切换中的情况
当操作系统在 并发执行多个进程(比如 A 和 B)时:
-
每个进程保存自己的一份环境变量;
-
在进程切换时,操作系统会恢复该进程的寄存器状态、内核栈、内存映射等;
-
这份环境变量表也属于进程用户空间的数据;
-
切换回该进程时,它仍能看到自己那份变量表(比如
$HOME
,$PATH
等)。
环境变量是进程上下文的一部分(位于用户空间),随进程独立存在。
导入新的环境变量
可以为环境变量导入新的值:
注意不要写成:这样是会使VALUE变成shell的本地变量,不是环境变量,用set本地变量可以查看。set可以查看所有 shell 中定义的变量(本地变量 + 环境变量 + 函数定义)。
本地变量与环境变量的对比:最重要的一点就是环境变量可继承,而本地变量不可继承。
对比项 | 本地变量(Local Variable) | 环境变量(Environment Variable) |
---|---|---|
定义方式 | A=10 | export A=10 或 A=10; export A |
可见范围 | 当前 shell | 当前 shell + 子进程 |
存放位置 | shell 的本地符号表 | shell 的环境表(可继承) |
查看命令 | set | env |
是否自动传递给子进程 | ❌ 否 | ✅ 是 |
删除方式 | unset A | unset A |
还有一个有意思的点,就是定义同名的变量,本地变量与环境变量定义先后的区别:
本地变量先定义:ABCD会变成环境变量
去掉环境变量ABCD后:调用set后出现_=ABCD。在 Bash 中,_
是一个特殊变量,用来保存 最近一次执行命令的最后一个参数或最近使用的变量名。
环境变量先定义:此后再使用ABCD赋值,仅仅是对ABCD修改值,并不会影响它环境变量的地位
取消ABCD环境变量地位:
操作 | 结果 | 是否传递给子进程 |
---|---|---|
A=1 | 创建本地变量 | ❌ 否 |
export A=1 | 创建环境变量 | ✅ 是 |
A=2 (在已 export 后) | 修改环境变量的值 | ✅ 是 |
export -n A | 取消 export 标记,变成本地变量 | ❌ 否 |
在 Shell 中,变量名唯一,环境变量只是“带 export 标志”的普通变量。如果你重新定义它,它会被覆盖,但仍然保持 export 状态。
命令行参数
第一个被执行的程序:启动程序(启动器)
在大多数操作系统(如 Linux)中:
-
操作系统内核在用户按下运行命令或 shell 调用程序时,比如./a.out时,会:
-
加载可执行文件到内存;
-
创建进程控制块(PCB);
-
初始化堆栈、堆、寄存器等。
-
-
**启动代码(CRT,C Runtime)**被执行:
-
初始化程序执行环境
-
设置栈、堆、全局变量、静态变量;
-
初始化
.bss
段(未初始化全局变量置 0); -
初始化
.data
段(已初始化全局变量)。
-
-
处理命令行参数和环境变量
-
把操作系统提供的命令行参数和环境变量整理成
argc
、argv
、envp
形式。
-
-
调用构造函数(C++)
-
如果是 C++ 程序,会调用全局对象的构造函数(
__libc_init_array
)。
-
-
调用用户的 main 函数
-
传入
argc
、argv
、envp
; -
获取返回值。
-
-
Linux 下 CRT 的典型实现
在 Linux(glibc)下,启动过程通常是:
程序入口 _start → crt1.o → crti.o → crtn.o → main()
-
_start
-
汇编实现,是程序真正的入口(ELF 文件入口地址);
-
做最底层的初始化,比如栈指针、寄存器。
-
-
crt1.o / crti.o / crtn.o
-
链接时加入,包含初始化代码;
-
调用
_libc_start_main
。
-
-
_libc_start_main(glibc 提供)
-
调用所有初始化函数;
-
然后调用用户的
main(argc, argv, envp)
; -
再调用
exit(main返回值)
。
-
main函数中的三个参数
int main(int argc, char *argv[], char *envp[])
| 参数名 | 全称 | 类型 | 作用 |
| ------ | ------------------- | ------- | ------------- |
| `argc` | argument count | int | 命令行参数的数量 |
| `argv` | argument vector | char*[] | 命令行参数字符串数组 |
| `envp` | environment pointer | char*[] | 环境变量字符串数组(可选) |
argc和argv
运行这段代码,可以看的出来,argv的第一参数是程序名字,后面的依次是选项。
让我们联想到平时使用的指令,不同的选项可以有不同的功能,我们来模拟实现一下。
可以做一个类似指令的程序:
可以做的更逼真一些:
所以,main的这两个参数的意义就是为指令、工具、软件等提供命令行选项的支持!
envp
envp
参数的意义就是 让 C/C++ 程序在启动时直接访问子进程的环境变量表,它是操作系统传给程序的环境快照。
如何理解上面这句话,看如下例子:在当前 shell 中执行 ./a
时,环境变量到底是怎么从父进程传给子进程的。
一、执行 ./a
时的参与者
假设你现在在 bash 里:
$ ./a
涉及的两个进程:
-
bash —— 父进程
-
./a —— 子进程(由 bash 创建并执行)
二、bash 执行命令的内部步骤
当你在 shell 输入 ./a
时,bash 主要经历以下过程:
阶段 | 调用 | 说明 |
---|---|---|
1️⃣ 创建子进程 | fork() | bash 调用 fork() 创建一个新进程,复制自身所有内存空间(包括环境变量) |
2️⃣ 执行新程序 | execve() | 子进程调用 execve("./a", argv, envp) 启动新的可执行文件 |
3️⃣ 载入程序 | 内核接手 | 内核加载 ELF 程序到内存,并把 argc 、argv[] 、envp[] 压栈传给它 |
4️⃣ 程序运行 | _start → main() | C 运行时初始化,调用用户的 main 函数 |
三、环境变量的传递过程详解
(1)bash 保存了所有环境变量
bash 进程中有一个环境变量表,比如:
PATH=/usr/bin:/bin
HOME=/home/wzz
LANG=en_US.UTF-8
它存在 bash 进程的用户空间中(在 environ
全局变量中)。
(2)fork() 阶段
fork()
复制整个进程的地址空间:
子进程获得了一份 bash 的环境变量副本。
验证一下,子进程的环境变量来自父进程:
在 bash 中设置环境变量TEST,./c.out执行时 bash fork + execve("./c.out", argv, environ),bash 调用 fork()
→ 创建一个子进程;子进程调用 execve("./c.out", argv, environ)
;这里 environ
就是 bash 的环境变量表,包括 TEST=521
;内核把这份表复制到新程序的栈中。所以子进程的环境变量来自父进程。
内建命令和常规命令
一、基本定义
类型 | 名称 | 执行位置 | 举例 |
---|---|---|---|
内建命令 | Built-in command | 由 Shell 自身实现并在当前进程中执行 | cd , echo , export , pwd , set , exit , alias |
外部命令(常规命令) | External / Regular command | 由文件系统中的独立可执行文件执行(通过 fork + exec) | ls , cat , grep , cp , mv , python , gcc |
二、区别详解
对比项 | 内建命令 | 外部命令 |
---|---|---|
实现位置 | Shell 程序本身实现(如 bash 的源码里) | 独立的可执行文件,通常在 /bin 、/usr/bin 等目录下 |
执行方式 | 在 当前 Shell 进程 中执行(不创建新进程) | Shell 会 fork 一个子进程 再 exec 对应的程序 |
执行效率 | 无需新进程,速度更快 | 需要系统调用 fork/exec,略慢 |
对环境变量的影响 | 可以直接修改当前 Shell 环境(例如 cd , export ) | 只影响自己的子进程环境,对当前 Shell 无影响 |
是否可被替换/卸载 | 不可卸载(属于 Shell 的一部分) | 可以替换、删除或重命名 |
查找顺序 | Shell 优先检查是否为内建命令 | 若不是内建命令,再在 PATH 中查找外部命令 |
三、为什么 cd
必须是内建命令?
举个例子:
cd /home/user
如果 cd
是外部命令,那么执行时会创建子进程,
这个子进程改变自己的工作目录没问题,但它一退出,父进程(当前 Shell)仍然在原来的目录。
所以你表面上执行了 cd
,实际上没效果。
模拟实现一下:chdir可以改变当前进程的工作目录。
使用ls观察该进程的工作目录,cwd就是当前进程的工作目录。proc存放着当前运行的进程。可以看到30秒后,切换到了家目录。
因此,cd
、export
、alias
、unset
等必须是 内建命令,因为它们需要改变 当前 Shell 环境。
四、如何区分内建与外部命令
可以用以下命令:
type 命令名
示例:
$ type cd
cd is a shell builtin$ type ls
ls is /bin/ls