正式开始之前先解释一些概念:

Shell、Bash、终端 概念辨析


一、终端(Terminal)

是什么: 一个输入输出设备,或者模拟这个设备的软件窗口。

干什么的: 只负责两件事——显示文字给你看,把你的键盘输入传给 Shell。它不解析命令,不执行程序。

历史演变:

过去(物理设备)                      现在(软件模拟)
┌────────────┐                   ┌────────────────┐
│  键盘 + 显示器  │               │ VSCode 下方黑框    │
│  一台真机器   │      →         │ Windows Terminal │
│  叫"终端机"   │               │ 叫"终端模拟器"     │
└────────────┘                   └────────────────┘
                                 本质上是个软件,装作自己
                                 是当年的那台物理终端机

你现在看到 VSCode 里那个黑框,是一个终端模拟器,它用软件模拟了一台真终端机的行为。


二、Shell

是什么: 一个命令行解释器程序。

干什么的: Shell 是真正"干活"的那个。你敲命令,Shell 去解析和执行。

你敲 ls -l /home
      │
      ▼
   Shell 解析这串字符:
   1. "ls" → 一个命令,去 PATH 里找 /bin/ls
   2. "-l" → 选项参数
   3. "/home" → 目标路径
      │
      ▼
   Shell 启动 /bin/ls 这个程序,把参数传给它
   程序输出结果 → Shell 打印到终端
      │
      ▼
   打印提示符 $ ,等你下一条命令

Shell 本质上也是一个普通的 C 程序。流程就是:读命令 → 解析 → 执行 → 输出 → 等下一行 → 循环


三、Bash

是什么: Shell 的一种具体实现。全称 Bourne Again SHell。

和其他 Shell 的关系:

名字 全称 位置
sh Bourne Shell,Unix 原版 最古老
bash Bourne Again SHell Linux 默认,兼容 sh
zsh Z Shell macOS 默认,更炫
dash Debian Almquist Shell 轻量,Ubuntu 里 /bin/sh 指向它

Linux 里 /bin/sh/bin/bash 是两个不同的文件,但 Bash 兼容 sh 的语法。 平时你可以互换理解。


四、三者关系图

┌─────────────────────────────────────────┐
│         终端模拟器(VSCode 黑框)           │  ← 你看到的那个窗口
│  ┌───────────────────────────────────┐  │
│  │          Bash(/bin/bash)         │  │  ← 窗口里跑着的 Shell 程序
│  │   读命令 → 解析 → 执行 → 输出     │  │
│  │  ┌───────────────────────────┐    │  │
│  │  │  ls, gcc, gdb, mycmd...   │    │  │  ← Shell 帮你调用的那些程序
│  │  └───────────────────────────┘    │  │
│  └───────────────────────────────────┘  │
└─────────────────────────────────────────┘

一句话:终端是容器,Shell 是解释器。你通过终端和 Shell 交互。


五、初学者容易混淆的概念

1. 终端 ≠ Shell
终端 Shell
是什么 窗口(硬件或软件模拟器) 程序
作用 显示文字、接收键盘 解析命令、执行程序
能否替换 换一个窗口软件 换一个 Shell 程序

关掉终端窗口 → Shell 也跟着死。 因为终端是 Shell 的父进程。


2. Shell ≠ 命令行
Shell = 命令行解释器
命令行 = 你正在敲的这行命令 (command line)

ls -l 是命令行,Shell 是解析这一行命令的程序。


3. Shell 环境和 Shell 变量 vs 环境变量
$ MYVAR=hello           ← 本地 Shell 变量,只在当前 Shell 能看
$ export MYVAR=hello    ← 环境变量,当前 Shell + 所有子进程都能看

Shell 变量 = 贴在 Shell 自己身上的便签,别人看不到。 环境变量 = 放进"户口本"里的,子进程继承。

你之前做的实验就是在验证这个区别:

本地变量 → bash 子进程里拿不到
export 后 → bash 子进程里能拿到

4. 内核 ≠ Shell
内核(Kernel) Shell
属性 操作系统的核心 一个普通的用户程序
作用 管理硬件、调度进程、分配内存 接收用户命令、启动其他程序
是否必须存在 是,没有内核系统起不来 不是,嵌入式设备可能没 Shell
能换吗 可以但极其困难 随便换,bash / zsh / fish

Shell 本身不做任何"管理"的事——它不分配内存、不调度进程、不读写磁盘。它只是把用户请求转发给内核。


5. 控制台(Console)vs 终端(Terminal)

这两个词日常混用,但历史上不一样:

控制台 终端
历史 直连主机的键盘+显示器 通过网络/串口远程连的
今天 基本一个意思 基本一个意思
特殊 Linux 里 Ctrl+Alt+F1~F6 叫虚拟控制台 图形界面里的黑框叫终端模拟器

现在日常使用中两者完全混同,不再刻意区分。


总结

你坐在电脑前
    │
    ▼
终端(VSCode 黑框)  ← 一个软件窗口,负责显示和接收键盘
    │
    ▼
Bash(/bin/bash)     ← 一个 Shell 程序,负责解析和执行命令
    │
    ▼
内核                 ← 操作系统核心,真正管理硬件
    │
    ▼
硬件

终端是脸,Shell 是脑。你看到的是终端,帮你干活的是 Shell。

4. 环境变量


4-1 基本概念

环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数,环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性

环境变量就是操作系统给每个进程配的一本全局参数字典

操作系统
    │
    ├── PATH=/usr/bin:/bin:/usr/local/bin    ← 去哪找可执行程序
    ├── HOME=/root                            ← 你家目录在哪
    ├── SHELL=/bin/bash                       ← 当前用的哪个shell
    ├── LANG=en_US.UTF-8                      ← 用什么语言编码
    └── ... 还有很多 ...

每个进程启动时,操作系统会把这份字典复制一份塞进进程自己的地址空间里。进程想用的时候直接查。

跟之前学的描述 + 组织套路一致:

// task_struct 里这样管环境变量(简化):
struct task_struct {
    // ...
    struct mm_struct *mm;  // mm 里面存着环境变量的地址
    // ...
};

// 环境变量在内存里就是一堆字符串,按 键=值 的格式排着:
// "PATH=/usr/bin\0HOME=/root\0SHELL=/bin/bash\0"
// 进程要查 PATH,就遍历这些字符串找到 "PATH=" 开头的那个

环境变量具有全局性——父进程的环境变量会被子进程继承。


4-2 常见环境变量

变量名 内容 作用
PATH 一堆目录路径,用 : 隔开 告诉 shell 到哪些目录找可执行程序
HOME 当前用户的家目录 cd ~ 就去那,程序存配置也用
SHELL 当前 shell 的路径 通常是 /bin/bash

4-3 查看环境变量:echo $变量名

echo $HOME        # /root
echo $SHELL       # /bin/bash
echo $PATH        # /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:...
测试 PATH——为什么 ls 能直接跑,而你的程序不能
$ ls                           # 直接敲,能跑
$ ./mycmd                      # 必须加 ./,为什么?

因为 ls 在 PATH 列出的某个目录里:

$ which ls
/usr/bin/ls          ← ls 在 /usr/bin 里,/usr/bin 在 PATH 里

$ which mycmd
(没输出)              ← mycmd 不在 PATH 的任何目录里

Shell 找命令的逻辑:

你敲了一个命令 "ls"
    │
    ▼
Shell 拿着 "ls",去 PATH 的每个目录挨个找:
    /usr/local/sbin/ls  → 没有
    /usr/local/bin/ls   → 没有
    /usr/sbin/ls        → 没有
    /usr/bin/ls         → 找到了!执行它
    /bin/ls             → (不找了)

你的 mycmd 不在这些目录里,所以 shell 找不到。./mycmd 的意思是"别去 PATH 里搜了,就当前目录这个文件"。

两种方法让它也能直接跑

记住:凡是改 PATH,永远写成 export PATH=$PATH:新目录,别写成 export PATH=新目录,export PATH=xxx 会将原来的 PATH 覆盖掉!!

方法一:把程序所在目录加入 PATH

$ export PATH=$PATH:/root/workspace/linux_c++_redis/test
$ mycmd                          # 现在直接敲就能跑了

方法二:把程序拷贝到 PATH 已有的目录

$ sudo cp mycmd /usr/local/bin/
$ mycmd                          # /usr/local/bin 本来就在 PATH 里
测试 HOME
# root 用户
$ echo $HOME
/root

# 普通用户
$ echo $HOME
/home/用户名

# cd ~ 就是去 HOME 指向的位置
$ cd ~; pwd
/root               ← 等价于 cd $HOME

4-4 相关命令

命令 作用 例子
echo $变量名 查看某个环境变量 echo $PATH
export 变量=值 设置一个新的环境变量 export MYNAME=hello
env 列出所有环境变量 env
unset 变量名 删除一个环境变量 unset MYNAME
set 列出所有变量(含环境变量 + 本地 shell 变量) set
区分 env 和 set
$ MYLOCAL=hello          # 在 shell 里定义了一个本地变量(没 export)
$ echo $MYLOCAL          # shell 里能访问
hello

$ env | grep MYLOCAL     # 但 env 看不到,它不是环境变量
(没输出)

$ set | grep MYLOCAL     # set 能看到所有变量(环境变量 + 本地变量)
MYLOCAL=hello

export 的作用就是把本地变量"升级"为环境变量,让子进程也能继承到:

$ MYLOCAL=hello           # 本地变量
$ bash                    # 启动一个子 shell
$ echo $MYLOCAL           # 子 shell 里看不到
(空白)

$ exit                    # 退出子 shell
$ export MYLOCAL=hello    # 升级为环境变量
$ bash                    # 再启子 shell
$ echo $MYLOCAL           # 子 shell 能看到
hello

4-5 环境变量的组织方式


一块内存 + 一个数组

每个进程启动时,内核会把父进程的环境变量复制一份,塞进新进程的地址空间。具体的存放格式:

环境表(char *environ[])—— 一个字符指针数组

      ┌─────────┐
      │ envp[0] │ ───→ "PATH=/usr/local/bin:/usr/bin:/bin\0"        ← 一个完整字符串
      │ envp[1] │ ───→ "HOME=/root\0"
      │ envp[2] │ ───→ "SHELL=/bin/bash\0"
      │ envp[3] │ ───→ "USER=root\0"
      │ envp[4] │ ───→ "LANG=en_US.UTF-8\0"
      │  ....   │
      │ envp[N] │ ───→  NULL                          ← 用 NULL 做结尾标记
      └─────────┘

就是一堆字符串,每个字符串是 键=值 的格式,最后放一个 NULL 表示结束。极其朴素。


4-6 通过代码获取环境变量

系统提供两种方式拿到这张环境表:

方式一:main 函数的第三个参数

#include <stdio.h>

int main(int argc, char *argv[], char *envp[])  // envp 就是环境表
{
    int i = 0;
    while (envp[i] != NULL) {          // 遍历到 NULL 结束
        printf("%s\n", envp[i]);       // 每行打印一条 "键=值"
        i++;
    }
    return 0;
}

方式二:用全局变量 environ

#include <stdio.h>

extern char **environ;               // 声明外部变量

int main()
{
    int i = 0;
    while (environ[i] != NULL) {
        printf("%s\n", environ[i]);
        i++;
    }
    return 0;
}

两种方式拿到的是一样的东西——同一个字符指针数组


验证一下

#include <stdio.h>
int main(int argc, char *argv[], char *envp[])
{
    int i = 0;
    while (envp[i] != NULL) {
        printf("[%d] %s\n", i, envp[i]);
        i++;
    }
    printf("total: %d\n", i);
    return 0;
}

内存里长什么样

上面跑出来的结果,在内存里就是这个结构:

内存某处(只读数据段 / 栈 / 堆)
        
地址 0x1000 → "SHELL=/bin/bash\0"
地址 0x2000 → "HOME=/root\0"
地址 0x3000 → "USER=root\0"
地址 0x4000 → "LANG=en_US.UTF-8\0"
...

数组(envp / environ):
┌──────────┬──────────┬──────────┬──────────┬──────┬──────────┐
│ 0x1000   │ 0x2000   │ 0x3000   │ 0x4000   │ .... │  NULL    │
│ 指向SHELL │ 指向HOME  │ 指向USER  │ 指向LANG  │      │ 终止标记  │
└──────────┴──────────┴──────────┴──────────┴──────┴──────────┘

就是一个指针数组,每个指针指到一条 "键=值\0" 字符串,数组末尾是 NULL。


和前面进程管理怎么串起来

task_struct(PCB)
┌──────────────┐
│ mm_struct    │ ─→ 进程的地址空间
│              │    ┌─────────────────┐
│              │    │ 栈(局部变量)      │
│              │    │ 堆(malloc)       │
│              │    │ 数据段(全局变量)   │  ← environ 数组在这里
│              │    │ 文本段(代码)      │
│              │    └─────────────────┘
│              │
│  命令行参数 argv   ─→ ["mycmd", NULL]
│  环境表   envp    ─→ ["SHELL=...", "HOME=...", ..., NULL]
│              │
└──────────────┘

环境表就是进程地址空间里的一块数据。内核启动进程时,把环境变量字符串拷贝到进程的地址空间里,然后把 envp 指针交给 main 函数。


环境变量的读和写

C 标准库提供了操作函数:

#include <stdlib.h>

char *path = getenv("PATH");       // 查一条环境变量
putenv("MYVAR=hello");             // 加一条
setenv("MYVAR", "hello", 1);      // 加/改(第三个参数 1=覆盖, 0=不覆盖)
unsetenv("MYVAR");                // 删一条

echo $PATH 也好,export PATH= 也好,Bash 底层就是在操作这张表。

但是注意:子进程只能改自己的这张表,改不了父进程的环境变量。 所以你在 Bash 里跑的 showenv,改了自己的 environ 并不会影响 Bash。


总结

问题 答案
环境变量存在哪 进程地址空间的数据段里
什么格式 一堆 键=值 的字符串
怎么组织 字符指针数组,末位是 NULL
怎么拿到 main 第三个参数 envp[],或全局变量 environ
怎么操作 getenv / setenv / unsetenv
进程间怎么传递 内核启动新进程时拷贝一份过去

就是一套极其简单粗暴的数据结构——字符串 + 指针数组 + NULL 结尾。没有任何魔法。

4-7 通过系统调用获取或设置环境变量

#include <stdio.h>
#include <stdlib.h>          // getenv 在这个头文件里

int main()
{
    printf("%s\n", getenv("PATH"));   // 查 PATH
    return 0;
}

getenv("PATH") 做的事:遍历环境表(那个字符指针数组),找到 PATH= 开头的字符串,返回 = 后面的值。

C 标准库提供了四个函数:

函数 作用
getenv("KEY") 查某个环境变量的值
setenv("KEY", "val", 是否覆盖) 设置一条
unsetenv("KEY") 删一条
putenv("KEY=val") 设置(老版写法)

常用getenv和putenv函数来访问特定的环境变量。

4-8 环境变量通常是具有全局属性的

• 环境变量通常具有全局属性,可以被子进程继承下去。

全局属性 = 可以被子进程继承。

父 Shell(Bash)
  ├── 环境变量 MYENV=hello world
  ├── 启动了子进程 ./getmyenv
  │    子进程继承了 MYENV=hello world
  │    getenv("MYENV") 能查到
实验验证:

$ export MYENV="hello world"     # 设置 + export
$ ./getmyenv                      # 子进程
MYENV=hello world                 # 能拿到 ✓

为什么能继承?

因为 fork() 创建子进程时,内核把父进程的地址空间整页复制给子进程,环境表也在里面。子进程的 environ 数组指向的是自己地址空间里的一份拷贝。

父进程  ─── fork() ───→  子进程
environ                  environ
  ├─ "PATH=..."           ├─ "PATH=..."    ← 拷贝
  ├─ "MYENV=hello"        ├─ "MYENV=hello" ← 拷贝
  └─  NULL                └─  NULL

4-9 补充

只赋值不 export → Shell 本地变量,子进程拿不到:

$ MYENV="hello world"           # 只是当前 Shell 的本地变量
$ ./getmyenv                      # 子进程
MYENV not found                   # 拿不到 ✗

因为 Shell 本地变量不存进环境表(environ),只存在 Shell 自己的内部数据结构里。fork 不会复制 Shell 的内部变量,只复制环境表。

动作 Shell 内部变量 环境表 子进程能拿到?
MYENV=hello ✓ 有 ✗ 没写 ✗ 不能
export MYENV=hello ✓ 有 ✓ 写入了 ✓ 能

~/.bashrc 和 ~/.bash_profile

为什么需要文件级环境变量? 终端关掉重开,之前 export 的全丢了。需要写入配置文件,每次启动自动加载。

Bash 启动方式不同,读的配置文件也不同:

非 login Shell(VSCode 终端通常是这样):
  启动时只读 ~/.bashrc

Login Shell(ssh 登录、云服务器登录):
  启动时读 ~/.bash_profile → .bash_profile 里会调 .bashrc

所以最好只改 ~/.bashrc,让两种方式都能读到:

# 在 ~/.bashrc 末尾加一行
export MYENV="hello world"

改完后重新加载(不用重登):

source ~/.bashrc

所有新建的终端窗口都会自动带上这个变量。这就是环境变量从"一次性"变成"永久"的方法。

Logo

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

更多推荐