Linux 基础 01:终端、Shell,和命令是怎么被找到的

发布于 2022-04-09 20:30 更新于 2022-04-09 22:10 1388 字 7 min read ... 访问量 手写 每个字都是我自己敲的。

Linux 基础 25:收尾——自查清单与继续深入的方向Linux 基础 24:启动流程与救援——UEFI、GRUB、initramfs 与 emergency modeLinux 基础 23:容器与内核边界——namespace、cgroup 与 docker exec 背后Linux 基础 22:日志与故障排查方法论——从现象到根因的六步Linux 基础 21:定时任务——crontab 的坑与 systemd timerLinux 基础 20:Shell 脚本(下)——trap、日志、幂等与并发锁Linux 基础 19:Shell 脚本(上)——引号、条件与退出码Linux 基础 18:性能观测与 strace——机器到底卡在哪Linux 基础 17:防火墙——iptables、nftables、firewalld 与 ufw 的关系Linux 基础 16:SELinux——当权限都对,服务还是被拒绝Linux 基础 15:权限进阶——sudoers 的通配陷阱、ACL 与 capabilitiesLinux 基础 14:SSH 与远程运维——密钥、权限与三种隧道Linux 基础 13:网络基础——自下而上的排错顺序Linux 基础 12:磁盘与文件系统——从块设备到挂载点Linux 基础 11:systemd——unit、systemctl 与 journalctl 的正确用法Linux 基础 10:包管理——apt 到底做了什么,以及四个高频报错Linux 基础 09:用户与组——passwd、shadow、sudoers 三件套Linux 基础 08:环境变量与 shell 启动文件——为什么 ssh 跑命令找不到你的 PATHLinux 基础 07:进程与信号——TERM 和 KILL 到底差在哪Linux 基础 06:管道与重定向——三条流、一个顺序问题Linux 基础 05:文本处理——把 grep/sort/uniq 变成你的分析工具Linux 基础 04:权限与属主——rwx 在文件和目录上不是一回事Linux 基础 03:ls、cp、mv、rm 的真实语义,以及那个由 shell 展开的 *Linux 基础 02:/ 底下为什么是这些目录,inode 里到底存了什么Linux 基础 01:终端、Shell,和命令是怎么被找到的
先把三个东西分清楚:终端不是 shell,shell 不是内核。再搞明白 PATH 的搜索顺序、builtin 与外部命令的差别,以及为什么 cd 会"找不到命令"。

这一节要解决什么

第一次 ssh 登录一台机器,屏幕上出现 root@vm:~# 这行东西,很容易产生一个错觉:我在跟 Linux 说话。

其实中间隔着好几层。分不清这几层,后面很多现象就只能靠死记:为什么 cd 会“找不到命令”、为什么改了 PATH 在另一个终端里却不生效、为什么 echo $SHELL 和当前真正跑的 shell 不是一回事。

一、四个角色,一条链路

键盘 → 终端(pty) → shell → fork/exec 子进程 → 内核 → 磁盘/网络/CPU
  • 内核(kernel):真正干活的那个。进程调度、内存、文件系统、网络都是它管。它不认识中文,也不认识 ls,只认系统调用。
  • shell:一个普通的用户态程序,职责只有一条——读一行,解析,执行。bash、zsh、dash 都是 shell。
  • 终端模拟器:你眼前的那个窗口(iTerm2、GNOME Terminal)。它负责显示字符、接收键盘。
  • pty(伪终端):程序与窗口之间的一根线。ssh 登录后,远端分配的也是 pty。

一次 ls 的真实过程:shell 从 pty 读到 ls\n → 按空格切词 → 在 PATH 里找名为 ls 的可执行文件 → fork() 出一个子进程 → 子进程 execve("/usr/bin/ls", …) → ls 自己调 open / readdir / write → 内核读目录、返回数据 → 输出回 pty → 你看到字。

shell 不负责“执行”,它只负责找程序、起进程、把管道接好。真正执行的是被它 exec 出来的那个程序 —— 所以 shell 是命令解释器,不是执行器。

二、命令是怎么被找到的:PATH 的顺序

echo "$PATH" | tr ':' '\n' | nl

PATH 从左到右找,第一个命中即用,后面的同名程序永远不会被用到。这也是 ~/bin 放在 PATH 最前面就能“覆盖”系统命令的原因——方便,同时也危险(第三节有实例)。

想知道一个名字到底是什么,别猜,问 shell:

type -a ls        # 列出所有匹配:alias / function / builtin / 磁盘文件
type -a cd
command -v ls     # 只回答“shell 会怎么执行它”

在 Ubuntu 上我这边实测 type -a ls 输出两行:

ls is /usr/bin/ls
ls is /bin/ls

看着像两个程序,其实是同一个文件:现代发行版做了 merged-usr,/bin、/sbin、/lib 都是指向 /usr 下的符号链接;而 PATH 里同时有 /usr/bin 和 /bin,所以同一个文件被列了两次。

type -a cd 的答案不一样:

cd is a shell builtin

cd 之所以“找不到”,是因为它根本不是磁盘上的程序,而是 shell 的内建命令(builtin)。它必须内建:cd 要改变的是当前 shell 进程的工作目录(系统调用 chdir),而外部程序是被 fork 出来的子进程,改了也不影响父进程。

常见 builtin:cd、export、read、source、type、pwd、umask、echo(bash 有内建版,也可以调用 /bin/echo)。

三、我在哪个 shell 里?三个答案可能不一样

echo "$SHELL"                                  # 登录 shell(来自 /etc/passwd 第 7 个字段)
echo "$0"                                      # 当前这个 shell 进程的名字
ps -o pid,ppid,cmd -p $$                       # $$ 是当前 shell 的 PID
grep "^$(whoami):" /etc/passwd
tty; who am i

我的批注:$SHELL 只能当参考,不能当证据。 它是登录时从 /etc/passwd 读出来的,之后你手动敲 zsh 或 bash 都不会改变它。要看“此刻在跑什么”,用 $0 / $$ / ps。

$- 还能告诉你这个 shell 处于什么状态:含 i 是交互式(interactive),含 m 说明 job control 开着(这点在讲信号那节会用到)。

echo "$-"        # 例:himBH

四、踩坑记录:把当前目录写进 PATH

# 千万别这么干
export PATH=.:$PATH

这样 shell 找任何一个命令时,都会先在当前目录里找。哪天你解压了一个包含 ls 或 sudo 同名的可执行文件的压缩包,cd 进去敲 ls,跑的就是别人的程序。这类攻击有名字:PATH 劫持。

第二个坑更隐蔽,也是我真踩过的:改 PATH 忘了导出。

# 只改了变量,没导出 → 当前 shell 有,子进程看不到
PATH="$HOME/bin:$PATH"

# 正确姿势
export PATH="$HOME/bin:$PATH"

验证“子进程到底看不看得到”,用最小实验看得很清楚:

FOO=bar        ; bash -c 'echo "${FOO:-<empty>}"'      # <empty>
export FOO=bar ; bash -c 'echo "${FOO:-<empty>}"'      # bar

五、动手清单(五分钟跑完)

# 1. 面对的是谁
echo "$0 | $SHELL | $$"; ps -o pid,ppid,cmd -p $$

# 2. PATH 的搜索顺序
echo "$PATH" | tr ':' '\n' | nl

# 3. 同一个名字在哪几层被解释
type -a ls; type -a cd; type -a sudo

# 4. shell 起进程的直接证据:父进程就是你的 shell
sleep 300 &
ps -o pid,ppid,cmd -p $!      # $! 是最近一个后台进程的 PID
kill %1                       # 记得收拾干净

第 4 步里 ppid 显示的就是你的 shell —— 这就是“shell 只负责 fork/exec”的可观测证据。

小结

  • 终端 ≠ shell ≠ 内核。shell 只是“读一行、找程序、起进程”的那一层。
  • 命令查找靠 PATH 从左到右,builtin 不参与查找;cd、export 这类必须内建。
  • $SHELL 是登录 shell,不等于当前进程;确认状态看 $0 / $$ / ps / $-。
  • 别把 . 放进 PATH。

下一节预告

下一节讲 / 底下为什么是这些目录,以及 inode 里到底存了什么——顺带解释“磁盘明明还有空间,却写不进文件”这种真实故障。

执笔 手写

喜欢的话,留下你的评论吧~

加载中...