三、进程概念(操作系统与进程(3))
本文系统解析了终端、Shell与Bash的核心概念及相互关系:终端是输入输出界面(硬件/软件模拟),Shell是命令解释器程序(如Bash),Bash是Shell的一种具体实现。重点剖析了环境变量的本质——操作系统为进程配置的全局参数字典,包括其存储结构(键值字符串数组)、继承机制(fork时复制)和操作方法(export/getenv等)。通过PATH、HOME等实例演示了环境变量的作用,并区分
正式开始之前先解释一些概念:
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
所有新建的终端窗口都会自动带上这个变量。这就是环境变量从"一次性"变成"永久"的方法。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐






所有评论(0)