读完这篇文章,你将彻底搞懂环境变量是什么、怎么用,以及它背后的进程继承机制。适合零基础小白,也适合想补齐操作系统基础的开发者。


一、从一个问题说起:为什么敲 ls 系统就认识?

你有没有想过,当你在终端敲下 ls 并回车的那一刻,系统是怎么找到 ls 这个程序的?

它为什么不会说"命令未找到"?系统到底去哪里找的?

答案就藏在环境变量里——更具体地说,藏在 PATH 里。


二、什么是环境变量?

环境变量(Environment Variable)是操作系统级别的变量,由"变量名=变量值"的键值对组成,具有全局属性。

这句话拆开来看有三层意思:

  1. 它是变量:和编程语言里的变量一样,有一个名字,存一个值。
  2. 它是系统级的:它不属于某一个程序,而是操作系统维护的一份"全局配置表"。
  3. 它具有全局性:一个进程设置了环境变量,它的子进程也能"继承"到(这一点后面会展开讲,是理解环境变量的关键)。

你可以把环境变量理解为操作系统给所有程序准备的一份"全局备忘录"——程序启动时,系统会把这份备忘录塞到程序手里,程序需要什么信息自己去里面翻。


三、最重要的环境变量:PATH

3.1 PATH 是什么?

echo $PATH

输出可能长这样:

/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/maisui/.local/bin:/home/maisui/bin

这一串由冒号(:)分隔的路径列表,就是 命令搜索路径

当你输入一个命令(比如 ls),Shell 会按照 PATH 中指定的路径,从左到右依次查找,找到第一个匹配的可执行文件就运行它。

3.2 如果我把 PATH 改了会怎样?

# 把 PATH 直接覆盖成自定义路径
PATH=/home/maisui/lesson_envir_var

这样一来,你的 Shell 只会在 /home/maisui/lesson_envir_var 这个目录下找命令。原来的 /usr/bin/usr/local/bin 全都不认了——此时你连 ls 都用不了。

这是内存级的修改,重启 Shell 就会恢复。因为改的是当前进程的环境变量,没有动配置文件。后面我们会解释为什么。

3.3 正确姿势:追加而不是覆盖

# 在保留原 PATH 的基础上追加新路径
PATH=$PATH:/home/maisui/lesson_envir_var

这样原来的路径一个不少,还加上了你自己的目录。这也是各种开发环境配置(比如 Java 的 JAVA_HOME、Android SDK 等)的通用操作模式。

3.4 Windows 也有环境变量

Windows 同样有环境变量这个概念,PATH 的作用和 Linux 完全一致——指定命令/可执行文件的搜索路径。只不过 Windows 下路径分隔符是分号(;)而不是冒号(:),配置入口在"系统属性 → 高级 → 环境变量"。


四、你必须认识的常用环境变量

打好基础,我们先快速过一遍。这里列出的每一个变量都在日常开发中非常实用。

环境变量 含义 示例
PATH 命令搜索路径 /usr/local/bin:/usr/bin:...
HOME 当前用户的主目录(登录后的默认目录) /home/whb
SHELL 当前用户默认使用的 Shell 解释器 /bin/bash
USER 当前登录的用户名 whb
PWD 当前所在的工作目录 /home/whb/projects
OLDPWD 上一次所在的工作目录 /
HOSTNAME 主机名 bite-alicloud
TERM 终端类型(决定颜色、光标等行为) xterm
HISTSIZE 内存中最多保存多少条历史命令 1000

下面挑几个值得展开的详细说说。

4.1 HOME —— 你的"家目录"

echo $HOME
# /home/whb

当你登录 Linux 时,系统默认把你放到这个目录。cd ~ 中的 ~ 展开后就是 $HOME 的值。很多程序(比如 gitsshnpm)都会去 $HOME 下找配置文件(.gitconfig.ssh/config.npmrc 等),所以这个变量比你想象的更重要。

4.2 SHELL —— 你在用哪个 Shell?

echo $SHELL
# /bin/bash

它记录的是当前用户默认使用的 Shell 解释器。Linux 下有多种 Shell(bash、zsh、fish、dash 等),这个变量告诉你现在用的是哪个。注意:它显示的是默认 Shell,并不代表你当前一定在用这个(你可以从 bash 里启动 zsh)。

4.3 HISTSIZE —— 你的命令历史

echo $HISTSIZE
# 1000

HISTSIZE 控制 bash 在内存中最多保存多少条执行过的命令。你敲 history 看到的记录条数就受这个值限制。

注意区分:HISTSIZE 控制内存中的历史条数,而 HISTFILESIZE 控制磁盘上 .bash_history 文件保存的条数。你注销后,内存中的历史会写入磁盘文件,下次登录再读回来。

4.4 OLDPWD —— 一键回到上一个目录

echo $OLDPWD
# /

cd - 命令就是读取这个变量,跳回上一次所在的工作目录。这个变量由 Shell 自动维护。

4.5 SSH 相关变量(远程登录专属)

如果你是通过 SSH 连接到服务器的,你会看到这些额外变量:

SSH_CLIENT=113.132.215.158 17705 22
SSH_TTY=/dev/pts/0
变量 含义
SSH_CLIENT 格式:客户端公网IP 客户端端口 服务器SSH端口。这里是你的电脑从 17705 端口连接服务器的 22 端口
SSH_TTY SSH 会话绑定的虚拟伪终端设备,pts 就是远程登录用的虚拟终端。每个 SSH 连接分配一个 pts 设备

这两个变量仅在 SSH 远程登录时自动设置。如果你直接在服务器本地插显示器键盘登录,是不会有的。

4.6 XDG_SESSION_ID —— 登录会话编号

XDG_SESSION_ID=36981

由 systemd 生成,用来标记当前这次登录会话。不管是 SSH 远程登录还是图形桌面登录,都会分配一个唯一 ID。系统管理员排查问题时经常用到。


五、操作环境变量的常用命令

5.1 查看环境变量

# 方法一:查看单个变量
echo $变量名
echo $PATH

# 方法二:查看所有环境变量
env
# 或者
printenv

# 方法三:查看所有变量(包括环境变量 + Shell 本地变量)
set

三者的区别很重要:

命令 显示范围
env / printenv 只显示环境变量
set 显示环境变量 + Shell 本地变量

关键区别:环境变量可以被子进程继承,Shell 本地变量不能被继承。set 能看到本地变量,env 看不到——因为本地变量只在当前 Shell 内部有效。

5.2 创建和修改变量

# 创建本地变量(只在当前 Shell 有效,子进程拿不到)
TEST_ENV=12342

# 把已有的本地变量"升级"为环境变量(子进程可以继承了)
export TEST_ENV

# 一步到位:直接创建环境变量
export aaa=12345

5.3 删除变量

unset aaa
unset TEST_ENV

5.4 验证变量是否存在

echo $TEST_ENV    # 有值就说明存在
echo $aaa         # 空行说明不存在或为空

六、底层原理:环境变量到底是怎么传递的?

这是本文的重头戏。如果你能理解这一节,你对环境变量的认知就超越了 90% 的开发者。

6.1 环境变量存在哪里?

环境变量并不是存在某个"全局数据库"或"注册表"里——它本质上是存在进程的内存空间中的一张表

每个进程启动时,操作系统会把一份环境变量表"塞"给它。这张表和命令行参数表一样,都是内存级的、临时的

6.2 子进程会继承父进程的环境变量

核心规则:子进程可以拿到父进程的环境变量。

为什么?因为你在终端里运行的所有命令,都是当前 Shell 进程(比如 bash)的子进程。bash 通过 fork() 创建子进程时,子进程会继承父进程(bash)的环境变量表。

用一张图来理解:

系统配置文件(磁盘)
      ↓ 读取
   bash 进程(内存中,持有环境变量表)
      ↓ fork() + exec()
   你的程序(作为 bash 的子进程,继承了环境变量表)

6.3 为什么重启 Shell 后 PATH 修改会丢失?

现在我们来解释前面留下的那个问题:

PATH=$PATH:/home/maisui/lesson_envir_var   # 改了 PATH
# 关闭终端,重开...
echo $PATH                                  # PATH 又恢复了!

原因很简单:你用 export 修改的是当前 bash 进程的内存中的环境变量,你并没有写磁盘上的配置文件。bash 进程一死,这些修改就跟着消失了。下次启动新 bash 时,它重新从配置文件里读,拿到的当然是原来的值。

修改环境变量(export)= 改内存 → 临时生效,重启丢失

修改配置文件(.bashrc 等)= 改磁盘 → 永久生效

6.4 用代码验证继承关系

光说不练假把式,下面用 C 代码来验证。

验证一:遍历环境变量表(通过 main 的第三个参数)

#include <stdio.h>

int main(int argc, char *argv[], char *env[]) {
    (void)argc;
    (void)argv;

    int i = 0;
    for (; env[i]; i++) {
        printf("env[%d]: %s\n", i, env[i]);
    }
    printf("我是一个命令,自己实现的!\n");
    return 0;
}

main 函数其实可以有三个参数,第三个 char *env[] 就是父进程传给它的环境变量表。编译运行后你会看到所有环境变量都被打印了出来。

验证二:使用全局变量 environ

#include <stdio.h>
#include <unistd.h>

int main() {
    extern char **environ;  // 系统提供的全局变量
    int i = 0;
    for (; environ[i]; i++) {
        printf("environ[%d] -> %s\n", i, environ[i]);
    }
    return 0;
}

char **environ 是系统默认给定的全局变量,和 env[] 指向的是同一张表。

验证三:用 getenv() 获取单个环境变量

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    char *whoami = getenv("USER");  // 获取 USER 这个环境变量的值

    if (whoami == NULL) {
        printf("无法执行我,因为我不认识你\n");
    } else if (strcmp(whoami, "root") == 0) {
        printf("我不准 root 执行\n");
    } else if (strcmp(whoami, "whb") == 0) {
        printf("你是合法用户,执行吧\n");
    }
    return 0;
}

getenv("USER") 会去环境变量表里查找 USER 对应的值。这个函数在实际开发中非常实用——比如你可以根据环境变量切换开发/生产配置。

6.5 自己造一个"环境变量表"传给子进程

我们甚至可以不依赖 bash,手动构造环境变量表来喂给子进程:

#include <stdio.h>
#include <unistd.h>

char *argv[100];
char *env[100];

int main() {
    argv[0] = (char*)"ls";
    argv[1] = (char*)"-a";

    env[0] = (char*)"PATH=AAAA";  // 自定义环境变量

    pid_t id = fork();
    if (id == 0) {
        // 子进程:打印自己收到的参数和环境变量
        int i = 0;
        for (; argv[i]; i++)
            printf("argv: %s\n", argv[i]);
        for (i = 0; env[i]; i++)
            printf("env : %s\n", env[i]);
    } else {
        // 父进程:不退出,方便观察
        sleep(10000);
    }
    return 0;
}

这个例子说明:环境变量表的传递,本质上就是父进程把自己的那张表复制一份给子进程。不是什么神秘的"全局数据库",就是朴实无华的内存拷贝。


七、本地变量 vs 环境变量:一张表说清楚

特性 本地变量 环境变量
创建方式 VAR=value export VAR=value
查看方式 set 可以看到 env / printenv 可以看到
作用范围 仅在当前 Shell 内部 当前 Shell + 所有子进程
子进程能继承吗? ❌ 不能 ✅ 能
典型用途 脚本内部临时变量 全局配置(PATH、HOME 等)

一句话总结:本地变量是 Shell 的"私有财产",环境变量是"传给下一代的遗产"。export 就是办遗产公证的那个动作。


八、如何让环境变量永久生效?(配置文件)

既然内存级的修改重启就丢了,那怎么让修改永久保存呢?

答案:写到配置文件中

8.1 配置文件在哪里?

每个用户的家目录下都有几个特殊的文件:

文件 作用
~/.bash_profile 登录 Shell 时执行(启动时读一次)
~/.bashrc 每个交互式 Shell 启动时执行(每开一个终端就读一次)
~/.profile 通用 profile,部分发行版使用
/etc/profile 全局配置,对所有用户生效(需要 root 权限)

8.2 如何操作?

# 编辑 .bash_profile(或 .bashrc)
vim ~/.bash_profile

# 在里面加上:
export maisui=shuaige
export PATH=$PATH:/home/whb/my_bin

# 保存退出后,让配置立刻生效:
source ~/.bash_profile

这样设置后,每次重新登录(或重新打开终端),这些变量就会被自动加载。

小建议:个人配置改 ~/.bashrc,系统级全局配置才改 /etc/profile。给每个改动加上注释,半年后你会感谢现在的自己。


九、环境变量的实际应用场景

知道了原理,来看几个你在日常开发中一定会遇到的场景:

  1. 多版本管理(Java/Python/Node.js):通过切换 JAVA_HOMEPYTHON_HOME 等环境变量来切换版本,而不是反复安装卸载。
  2. 开发/生产环境区分:程序启动时读 ENV=production 或 ENV=development,自动切换数据库连接、日志级别等。
  3. 敏感信息管理:数据库密码、API Key 等写在环境变量里,不硬编码到代码中(12-Factor App 的核心原则之一)。
  4. 工具链配置ANDROID_HOMEGOPATHMAVEN_HOME 等各种开发工具都依赖环境变量找到自己的家目录。

十、总结

回顾一下本文的核心脉络:

配置文件(磁盘)→ bash 进程读取(内存)→ fork 子进程(继承)→ 你的程序拿到环境变量
  • 环境变量是操作系统维护的全局键值对,保存在进程的内存空间中。
  • PATH 是最重要的环境变量,决定了你的命令去哪里找可执行文件。
  • env 看环境变量,set 看所有变量(含本地变量)。
  • export 创建环境变量,unset 删除变量,修改是内存级的。
  • 子进程会继承父进程的环境变量——这是环境变量"全局性"的根本原因。
  • 永久生效,需要修改配置文件(~/.bashrc 等),而不仅仅是 export
  • 在 C 程序中,可以通过 char *env[]extern char **environgetenv() 三种方式访问环境变量。

搞懂了环境变量,你就理解了操作系统进程间信息传递的一个基本范式。下次配开发环境时,你就不只是"照教程改几行配置"了——你很清楚每一行在做什么、为什么这样做、不这样做会怎样。


本文涵盖了从基础操作到底层原理的完整知识链,适合作为环境变量的系统性参考。


如果你觉得有帮助,欢迎点赞收藏,也欢迎在评论区交流讨论!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐