正式开始之前先解释一些概念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 的关系名字全称位置shBourne ShellUnix 原版最古老bashBourne Again SHellLinux 默认兼容 shzshZ ShellmacOS 默认更炫dashDebian 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 环境变量$ MYVARhello ← 本地 Shell 变量只在当前 Shell 能看 $ export MYVARhello ← 环境变量当前 Shell 所有子进程都能看Shell 变量 贴在 Shell 自己身上的便签别人看不到。环境变量 放进户口本里的子进程继承。你之前做的实验就是在验证这个区别本地变量 → bash 子进程里拿不到 export 后 → bash 子进程里能拿到4. 内核 ≠ Shell内核KernelShell属性操作系统的核心一个普通的用户程序作用管理硬件、调度进程、分配内存接收用户命令、启动其他程序是否必须存在是没有内核系统起不来不是嵌入式设备可能没 Shell能换吗可以但极其困难随便换bash / zsh / fishShell 本身不做任何管理的事——它不分配内存、不调度进程、不读写磁盘。它只是把用户请求转发给内核。5. 控制台Consolevs 终端Terminal这两个词日常混用但历史上不一样控制台终端历史直连主机的键盘显示器通过网络/串口远程连的今天基本一个意思基本一个意思特殊Linux 里CtrlAltF1~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 ├── LANGen_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/bash4-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 PATHxxx会将原来的 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 $HOME4-4 相关命令命令作用例子echo $变量名查看某个环境变量echo $PATHexport 变量值设置一个新的环境变量export MYNAMEhelloenv列出所有环境变量envunset 变量名删除一个环境变量unset MYNAMEset列出所有变量含环境变量 本地 shell 变量set区分 env 和 set$ MYLOCALhello # 在 shell 里定义了一个本地变量没 export $ echo $MYLOCAL # shell 里能访问 hello $ env | grep MYLOCAL # 但 env 看不到它不是环境变量 (没输出) $ set | grep MYLOCAL # set 能看到所有变量环境变量 本地变量 MYLOCALhelloexport的作用就是把本地变量升级为环境变量让子进程也能继承到$ MYLOCALhello # 本地变量 $ bash # 启动一个子 shell $ echo $MYLOCAL # 子 shell 里看不到 (空白) $ exit # 退出子 shell $ export MYLOCALhello # 升级为环境变量 $ bash # 再启子 shell $ echo $MYLOCAL # 子 shell 能看到 hello4-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] │ ───→ USERroot\0 │ envp[4] │ ───→ LANGen_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 → USERroot\0 地址 0x4000 → LANGen_US.UTF-8\0 ... 数组envp / environ ┌──────────┬──────────┬──────────┬──────────┬──────┬──────────┐ │ 0x1000 │ 0x2000 │ 0x3000 │ 0x4000 │ .... │ NULL │ │ 指向SHELL │ 指向HOME │ 指向USER │ 指向LANG │ │ 终止标记 │ └──────────┴──────────┴──────────┴──────────┴──────┴──────────┘就是一个指针数组每个指针指到一条键值\0字符串数组末尾是 NULL。和前面进程管理怎么串起来task_structPCB ┌──────────────┐ │ mm_struct │ ─→ 进程的地址空间 │ │ ┌─────────────────┐ │ │ │ 栈局部变量 │ │ │ │ 堆malloc │ │ │ │ 数据段全局变量 │ ← environ 数组在这里 │ │ │ 文本段代码 │ │ │ └─────────────────┘ │ │ │ 命令行参数 argv ─→ [mycmd, NULL] │ 环境表 envp ─→ [SHELL..., HOME..., ..., NULL] │ │ └──────────────┘环境表就是进程地址空间里的一块数据。内核启动进程时把环境变量字符串拷贝到进程的地址空间里然后把 envp 指针交给 main 函数。环境变量的读和写C 标准库提供了操作函数#include stdlib.h char *path getenv(PATH); // 查一条环境变量 putenv(MYVARhello); // 加一条 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(KEYval)设置老版写法常用getenv和putenv函数来访问特定的环境变量。4-8 环境变量通常是具有全局属性的• 环境变量通常具有全局属性可以被子进程继承下去。全局属性 可以被子进程继承。父 ShellBash ├── 环境变量 MYENVhello world ├── 启动了子进程 ./getmyenv │ 子进程继承了 MYENVhello world │ getenv(MYENV) 能查到实验验证 $ export MYENVhello world # 设置 export $ ./getmyenv # 子进程 MYENVhello world # 能拿到 ✓为什么能继承因为fork()创建子进程时内核把父进程的地址空间整页复制给子进程环境表也在里面。子进程的environ数组指向的是自己地址空间里的一份拷贝。父进程 ─── fork() ───→ 子进程 environ environ ├─ PATH... ├─ PATH... ← 拷贝 ├─ MYENVhello ├─ MYENVhello ← 拷贝 └─ NULL └─ NULL4-9 补充只赋值不 export → Shell 本地变量子进程拿不到$ MYENVhello world # 只是当前 Shell 的本地变量 $ ./getmyenv # 子进程 MYENV not found # 拿不到 ✗因为 Shell 本地变量不存进环境表environ只存在 Shell 自己的内部数据结构里。fork不会复制 Shell 的内部变量只复制环境表。动作Shell 内部变量环境表子进程能拿到MYENVhello✓ 有✗ 没写✗ 不能export MYENVhello✓ 有✓ 写入了✓ 能~/.bashrc 和 ~/.bash_profile为什么需要文件级环境变量终端关掉重开之前export的全丢了。需要写入配置文件每次启动自动加载。Bash 启动方式不同读的配置文件也不同 非 login ShellVSCode 终端通常是这样 启动时只读 ~/.bashrc Login Shellssh 登录、云服务器登录 启动时读 ~/.bash_profile → .bash_profile 里会调 .bashrc所以最好只改~/.bashrc让两种方式都能读到# 在 ~/.bashrc 末尾加一行 export MYENVhello world改完后重新加载不用重登source ~/.bashrc所有新建的终端窗口都会自动带上这个变量。这就是环境变量从一次性变成永久的方法。