环境变量是什么
环境变量(Environment Variable):操作系统提供给进程的键值对。进程启动时继承父进程的整个环境,可用于配置程序行为、传递路径信息等。常见如 HOME、USER、SHELL、LANG。
PATH:最核心的环境变量,告诉 Shell 在哪些目录中按顺序搜索可执行文件。敲一条命令时,Shell 挨个扫 PATH 里的目录——先找到的就执行,后面的不再查。
动态库搜索路径:程序运行时查找共享库(Linux .so、macOS .dylib、Windows .dll)的机制。逻辑与 PATH 类似但机制因平台差异极大,是最容易踩跨平台坑的领域。
我什么时候会用到它
抛开应用程序主动用它传配置不谈,最常用的场景就是手动安装命令行工具。处于某些原因我们不想用包管理器或安装器安装程序时(它们通常会帮你配置好环境变量)而是直接下载一个压缩包的可执行文件使用时,我们要告诉 Shell 在哪里找到我们下载的可执行文件,即配置环境。通常情况下,只需要将可执行文件所在的目录添加到 PATH 环境变量里即可。那么,怎么添加?——
怎么读
环境变量的读取范围分四层,搞不清这个是"设了但不生效"的第一大原因:
| 层级 | 覆盖范围 | 典型用途 | 生效时机 |
|---|---|---|---|
| 系统级 | 所有用户、所有进程 | 公共工具路径、系统级配置 | 需重启/新登录/广播通知 |
| 用户级 | 单个用户的所有进程 | 用户信息、个人工具 | 需新开 Shell 或新登录 |
| Shell级 | 当前 Shell 及其子进程 | 临时测试 | 对当前 Shell 接下来的所有操作生效,关闭 Shell 即消失 |
| 进程级 | 仅当前运行命令 | 一次性操作 | 对当前运行命令生效,当前命令退出即消失 |
同名变量在多层设置时,层级越窄优先级越高: 进程级 > Shell级 > 用户级 > 系统级)
| 层级 | Linux/macOS bash/zsh | Windows CMD/PowerShell |
|---|---|---|
| 系统 | /etc/ 下写文件 | setx /M 或面板 |
| 用户 | ~/ 下写文件 | setx 或面板 |
| Shell | export | set / $env: |
| 进程 | VAR=val cmd | 不支持 |
这里的行为,与其说是平台特定,不如说是 Shell 特定:比如,如果你在 Windows 上用 Bash,那其实更应该参考 Linux 的设置。
为什么写文件可以修改环境变量?这是 Shell 的设计机制:Shell 启动时会按一定规则运行特定的初始化脚本,而作为所有命令的入口,所有的 Shell 都设置了的环境,相当于就是整个用户/系统的环境。
特例:
- Linux
/etc/environment是用户登录时由 PAM 读取的,格式为KEY=VALUE - macOS
/etc/paths只管$PATH,每行一个路径,不是KEY=VALUE格式;/etc/paths.d/*同理(每个文件一行一个路径)
Shell 的 Login / Non-login——Linux/macOS 最大的坑
Shell 启动方式决定它读取哪些配置文件,这直接决定你的环境变量在哪个文件里写才能生效:
| Login Shell | Non-login Shell | |
|---|---|---|
| Interactive | SSH 登录、su -、macOS Terminal.app 默认 | Linux GUI 终端默认、su |
| Non-interactive | bash -l script.sh | bash script.sh、cron 任务 |
bash 读取顺序
| Shell 类型 | 读取文件 |
|---|---|
| Login | /etc/profile → (~/.bash_profile > ~/.bash_login > ~/.profile)(只读找到的第一个) |
| Interactive non-login | /etc/bash.bashrc → ~/.bashrc |
| Non-interactive | 读取 $BASH_ENV 指定的文件(通常不设此变量) |
bash 的 Login Shell 优先级列表 ~/.bash_profile > ~/.bash_login > ~/.profile 诞生于 Unix 历史的“兼容期”,它是为了解决多流派 Shell 用户的迁移问题,而非让你把三个文件都用上。
~/.profile是 Bourne Shell (sh) 的老祖宗标准: 在当年,所有 Unix 系统的默认 Shell 都是sh,大家都把环境变量写在~/.profile里。直到今天,如果你用的是 Ubuntu,它的底层系统默认 Shell 是dash,dash启动时依然只认~/.profile。~/.bash_login是对 C Shell (csh) 的妥协:当年有一大批写~/.login的 csh 用户转投 Bash,为了照顾他们的习惯,Bash 加了这个支持。~/.bash_profile是 Bash 的“亲儿子”:Bash 诞生增加了很多sh不支持的高级语法(比如数组、特殊的字符串处理)。如果用户把这些高级语法写进~/.profile,当系统其他组件用sh或dash读取这个文件时,就会直接报错。
为什么设计为互斥隔离(只读找到的第一个)?
如果 Bash 依次把这三个全读一遍,往往会引发灾难(比如环境变量 PATH=...:$PATH 被重复追加三次、打印了三次登录欢迎语、或者因为语法不兼容报错)。在 Bash 开发者的视角里:
如果你写了专属的
.bash_profile,我就按你的来;如果你没写,我就去看你是不是习惯用.bash_login;如果你连这都没写,那我就去读最普通的.profile保底。千万别读两次。
但这导致了一个巨大的痛点:用户必须在 login 和 non-login 两个文件里维护两套配置。为了解决这个问题,现代的 Linux 发行版(如 Ubuntu, CentOS, Debian)在默认给用户生成家目录(从 /etc/skel/ 拷贝)时,动了一个非常聪明的“手脚”——强制桥接。现代发行版真实的 Login Shell 读取顺序变成了一个嵌套链条:
- bash 加载
/etc/profile-> 遍历并source /etc/profile.d/*.sh(用于配置系统级环境变量)和去读/etc/bash.bashrc - bash 去找并读取 (
~/.bash_profile>~/.bash_login>~/.profile) -> 去读~/.bashrc
然而,想象如下情景:
- 系统默认只有
~/.profile(里面包含了调用bashrc的桥接代码)。 - 你为了配置环境变量,上网随便搜了一篇教程,自己新建了一个
~/.bash_profile文件并在里面写了export PATH=...。 - 灾难发生:由于 Bash 的优先级是
~/.bash_profile>~/.profile,你的 Login Shell 发现了你新建的文件,就不会再去读系统的~/.profile了。 - 结果:你新建的文件里没有那段“桥接代码”,导致你的
~/.bashrc彻底被 Login Shell 抛弃,以前在bashrc里配的所有别名(alias)和变量在 SSH 登录时全部失效。
因此,上网查教程时要特别小心,尤其是你发现它在用>>(流追加符)这种看似无害的东西修改配置文件时,因为你永远不知道它究竟是简单的追加还是默默新建了一个优先级更高的配置。
zsh 读取顺序(macOS Catalina+ 默认)
| Shell 类型 | 读取文件 |
|---|---|
| Login | zshenv → zprofile → zshrc → zlogin(系统+用户各一套) |
| Interactive non-login | zshenv → zshrc |
| Non-interactive | zshenv |
zsh 无论是否 login,都读 ~/.zshrc,所以 zsh 下环境变量只需放在 ~/.zshrc 就够了——比 bash 简单得多。
怎么写
Windows
| 层级 | 设置方法 | 写入位置 | 注意 |
|---|---|---|---|
| 系统级 | setx /M VAR "value"(需管理员权限)或:系统属性 → 高级 → 环境变量 → 系统变量 | 注册表 HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment | 已运行的程序不受影响,需重启或新开程序 |
| 用户级 | setx VAR "value"或:系统属性 → 高级 → 环境变量 → 用户变量 | 注册表 HKCU\Environment | 当前 cmd 窗口不生效,新开窗口才生效 |
| Shell级 | cmd: set KEY=valuePowerShell: $env:KEY = "value" | 仅当前进程内存 | 关闭 Shell 即消失 |
Windows GUI 程序改完环境变量后不会自动感知,需要重启该程序。虽然系统会广播 WM_SETTINGCHANGE 消息,但很多程序忽略此消息。可能可以通过重启程序或重新登录生效,但既然都是 Windows 了,最可靠的方式还是重启系统。
Linux
| 层级 | 文件 | 格式 | 说明 |
|---|---|---|---|
| 系统级 | /etc/environment | KEY=VALUE(纯键值对) | PAM 读取,不能用 Shell 语法(不能写 export、不能 $VAR 引用),对所有用户所有 Shell 生效 |
/etc/profile | Shell 脚本 | login shell 时读取 | |
/etc/profile.d/*.sh | Shell 脚本 | /etc/profile 自动 source 此目录下所有 .sh——加变量新建文件就可以,最好不要直接改 /etc/profile | |
| 用户级 | ~/.profile | Shell 脚本 | login shell 时读取(被 bash/zsh 专属文件优先取代) |
~/.bash_profile | Shell 脚本 | bash login shell 优先读此文件,有它就不读 ~/.profile | |
~/.bashrc | Shell 脚本 | bash interactive non-login 时读取 | |
~/.zshrc | Shell 脚本 | zsh interactive 总是读取——zsh 下放变量的首选位置 | |
| Shell级 | 直接执行 export VAR=value | Shell 命令 | 即时生效,关闭即失 |
为什么有 /etc/profile 还要 /etc/profile.d/?
系统包更新可能覆盖 /etc/profile ,但 /etc/profile.d/ 下的文件独立存在不受影响。/etc/ 下很多配置其实都有 xxx 和 xxx.d/ ——以便分离默认配置和用户配置。
GUI 应用(从桌面环境启动的)不读 ~/.bashrc。
它们的进程由 display manager (如 GDM) 创建,继承的是 systemd 用户会话环境。给 GUI 应用设环境变量的正确方式:
此目录下的 .conf 文件格式同 /etc/environment(纯 KEY=VALUE),由 systemd-user 启动时读取,仅对 GUI 应用生效。
macOS
| 层级 | 文件/机制 | 说明 |
|---|---|---|
| 系统级 | /etc/paths | PATH 专用,每行一个目录(macOS 独有机制) |
/etc/paths.d/* | PATH 模块化扩展,每文件每行一个目录 | |
/etc/profile → /etc/profile.d/*.sh | 其他环境变量(同 Linux) | |
/etc/zshrc、/etc/zprofile | zsh 系统级配置 | |
| 用户级 | ~/.zshrc | zsh interactive 总被读取——放环境变量的首选位置 |
~/.zprofile | zsh login shell 读取(Terminal.app 默认是 login shell) | |
| Shell级 | export VAR=value | 即时生效,关闭即失 |
macOS Catalina (10.15) 起默认 Shell 从 bash 改为 zsh。
旧版升级后,~/.bash_profile 里的配置在新终端中完全不生效。迁移:把 bash 配置搬到 ~/.zprofile(login 级)+ ~/.zshrc(interactive 级)。
macOS 的 PATH 机制独特。
不在 /etc/environment 里(此文件不存在),而是用 /etc/paths + /etc/paths.d/。加系统级 PATH 目录的正确做法:
注意这里用 tee 而非流重定向符,是因为 sudo 只管命令运行权限,而流是由 Shell 处理的——用 sudo 不就是因为 Shell 权限不够嘛。你要用 sudo sh -c 'echo /opt/homebrew/bin >> /etc/paths.d/homebrew' 也是一样的。
GUI 应用(从 Dock/Launchpad 启动)不继承 Shell 环境变量。
macOS 的 GUI 应用由 launchd 启动,launchd 的环境来自系统级配置,不读 ~/.zshrc。给 GUI 应用设环境变量的正确方式:
PATH 设置最佳实践
| 原则 | 为什么 |
|---|---|
| 层级越窄越好 | 个人工具放用户级 PATH,别放系统级影响所有人 |
| 追加而非覆盖 | 设 PATH 时务必保留原有:$PATH:newdir(Linux/macOS)或 %PATH%;newdir(Windows cmd) |
| 顺序就是优先级 | PATH 中靠前的目录先匹配。~/bin 里有个同名程序放在 PATH 前面就会覆盖系统的——可能是你想要的,也可能是灾难 |
永远不放 .(当前目录) | 安全风险:某恶意目录下放个 ls,你 cd 进去敲 ls 就执行了恶意程序。Windows 早期默认把 . 放 PATH 最前面,这是历史安全漏洞 |
Linux/macOS 个人工具放 ~/.local/bin | XDG 标准目录,现代发行版已默认将其加入 PATH。旧惯例 ~/bin 也能用但不是标准 |
各平台追加 PATH 的安全写法
动态库搜索——跨平台差异最大的领域
三平台机制速览
| Windows | Linux | macOS | |
|---|---|---|---|
| 库文件格式 | .dll | .so | .dylib / .framework |
| 环境变量 | PATH(兼管 exe 和 DLL) | LD_LIBRARY_PATH | DYLD_LIBRARY_PATH / DYLD_FRAMEWORK_PATH |
| 系统级配置 | PATH 系统变量 | /etc/ld.so.conf + ldconfig | 无直接等效(用编译时嵌入路径) |
| 编译时嵌入 | 无标准机制 | -Wl,-rpath,/dir | @rpath / @loader_path / @executable_path |
| 设计哲学 | 搜索顺序固定,应用目录优先 | ldconfig 缓存为主,环境变量为辅 | 路径编进库本身,不依赖环境变量 |
Windows DLL 搜索顺序
Windows 的 PATH 同时管可执行文件搜索和 DLL 搜索.
这是独一无二的:PATH 里加个目录,不光影响命令查找,还影响 DLL 加载。PATH 越宽,DLL 搜索范围也越宽,可能加载到错误版本 DLL(DLL Hell 的根源)。这也是为什么 Windows 下按个 Tab 会冒出来一堆不能直接运行的.dll.
SafeDllSearchMode 开启时(Windows Vista+ 默认):
| 顺序 | 搜索位置 |
|---|---|
| 1 | 应用程序所在目录(最高优先——Windows 避免 DLL Hell 的主要机制) |
| 2 | 系统目录 C:\Windows\System32 |
| 3 | 16位系统目录(历史遗留) |
| 4 | Windows 目录 C:\Windows |
| 5 | PATH 中的目录 |
| 6 | 当前工作目录 |
SafeDllSearchMode 关闭时(仅旧系统),当前目录排在 PATH 前面——这是安全隐患。
最佳实践:DLL 放在应用程序自己的目录下——这是搜索顺序第一位,最可靠且不影响其他程序。尽量不要依赖 PATH 来找 DLL。 这也就是为什么 Windows 下 FFmpeg 那么容易出问题:FFmpeg 的协议使其经常没法再分发,只能手动补齐。
Linux 动态库搜索
搜索顺序:
| 顺序 | 来源 | 说明 |
|---|---|---|
| 1 | DT_RPATH(旧) | 在 LD_LIBRARY_PATH 之前搜索,可覆盖——已被 RUNPATH 取代 |
| 2 | LD_LIBRARY_PATH | 环境变量 |
| 3 | DT_RUNPATH(新) | 在 LD_LIBRARY_PATH 之后搜索,不可覆盖——现代工具链默认 |
| 4 | ldconfig 缓存 | /etc/ld.so.cache,由 ldconfig 命令从 /etc/ld.so.conf 构建 |
| 5 | 默认路径 | /lib、/usr/lib、/lib64、/usr/lib64 等 |
设置方法:
| 层级 | 方法 | 说明 |
|---|---|---|
| 系统级 | 在 /etc/ld.so.conf.d/ 加 .conf 文件,然后 sudo ldconfig | 模块化管理——不要直接改 /etc/ld.so.conf |
| 用户级 | 无标准机制 | 可在 ~/.bashrc 里设 LD_LIBRARY_PATH,但不推荐永久使用 |
| Shell级 | export LD_LIBRARY_PATH=/new/dir:$LD_LIBRARY_PATH | 临时使用 |
| 编译时 | gcc -Wl,-rpath,/dir 或 -Wl,-rpath,$ORIGIN/lib | 写进可执行文件,最可靠 |
LD_LIBRARY_PATH 应当只临时使用。
LD_LIBRARY_PATH 不应永久写入 Shell 配置。它会覆盖 ldconfig 缓存和默认路径,可能导致系统工具加载到错误版本的库(比如你的 LD_LIBRARY_PATH 里有个旧版 libssl,系统 curl 就可能加载它然后崩掉)。
RPATH vs RUNPATH 行为不同。
RPATH 在 LD_LIBRARY_PATH 之前搜索(可被 RPATH 覆盖),RUNPATH 在之后搜索(LDLIBRARYPATH 可覆盖 RUNPATH)。现代 binutils 默认用 RUNPATH。如果你需要编译时路径覆盖环境变量,要显式用 -Wl,--disable-new-dtags(强制 RPATH)——但这通常不是你想要的。
$ORIGIN 是一个特殊的 rpath 值,展开为可执行文件所在目录。
用于可移植部署(可执行文件和库放同一目录树),但 $ORIGIN 必须写进 rpath 字面量,不能在 Shell 里展开——gcc 命令中要写成:
macOS 动态库搜索
搜索顺序:
| 顺序 | 来源 | 说明 |
|---|---|---|
| 1 | DYLD_LIBRARY_PATH / DYLD_FRAMEWORK_PATH | 环境变量覆盖(由于SIP, 对系统进程无效) |
| 2 | @rpath 展开 | 可执行文件中 LC_RPATH 指定的目录列表 |
| 3 | @loader_path / @executable_path | 相对于加载者/主程序的目录 |
| 4 | install name 绝对路径 | 库自身声明的路径 |
| 5 | DYLD_FALLBACK_LIBRARY_PATH | 默认 /usr/local/lib:/usr/lib |
macOS 的 @ 路径魔术——这是理解 macOS 动态库的关键:
| 语法 | 展开为 | 用途 |
|---|---|---|
@executable_path | 主程序所在目录 | 主程序找自己的库 |
@loader_path | 加载此库的那个文件所在目录 | 库找自己依赖的库(递归场景) |
@rpath | 可执行文件中 LC_RPATH 列表中的目录 | 最灵活——主程序定义搜索列表,库只需声明"在 rpath 里找我" |
macOS 的设计哲学:库的搜索路径编译进库本身(install name),而不是依赖环境变量。库的 install name 通常形如 @rpath/libfoo.dylib,加载时 dyld 用主程序的 rpath 列表展开 @rpath 为具体目录。
设置方法:
| 层级 | 方法 | 说明 |
|---|---|---|
| 系统级 | 无环境变量机制 | macOS 设计上不支持——靠编译时嵌入 |
| Shell级 | export DYLD_LIBRARY_PATH=/dir | 临时用,SIP会对系统进程剥离此变量 |
| 编译时 | gcc -Wl,-rpath,/dir 或 install_name_tool | 最可靠的方式 |
| 修改已编译库 | install_name_tool -change old_path new_path libfoo.dylib | macOS 独有,改库的 install name 不需要重新编译 |
Homebrew 安装的库使用绝对路径 install name(/opt/homebrew/lib/libfoo.dylib 或 /usr/local/lib/libfoo.dylib),Homebrew 在编译时通过 -rpath 让可执行文件能找到它们。如果你手动编译程序链接 Homebrew 库,必须传:
漏了 -rpath 就能编译但运行时找不到库——这是 macOS 上 dyld: Library not loaded 报错的第一大原因。
install_name_tool 是 macOS 独有的神器:
可以修改已编译 .dylib 的 install name 和已编译可执行文件的 rpath,不需要重新编译:
经验交流:每个平台的环境和动态库问题都有大量隐坑,尤其是 Windows 的 DLL 搜索顺序和 macOS 的 install name 体系——如果你有踩坑经验或补充,欢迎评论区分享。