目录树为什么长这样:/bin 和 /usr/bin 到底有什么关系

难度 ★☆☆☆☆ · 前置:第一次打开终端:提示符那行字在说什么 | 一句话结论:目录树不是设计出来的,是 1994 年那份 FHS 约定的产物。/bin 和 /usr/bin 现在指同一处,是后来合并(UsrMerge)的结果;理解这一点,/sbin 为什么在 PATH 里、/usr 为什么不是 user,就都有答案了。

刚接触 Linux 那阵,我照着教程敲 ls /,屏幕上滚出二十来个目录,我一个都不认识,也没有一个名字能望文生义。

更迷惑的是接下来这一步,教程让我装个工具,说文件在 /usr/bin,另一篇说在 /bin,我两边都 ls 了一下,发现 ls、cat、bash 在两个地方都有,第一反应是系统装了两份,还认真算过删掉一份能省多少空间,因为那块虚拟磁盘总共只有 20G。于是我打开文件管理器的树形视图,一个文件一个文件地对,比大小、比日期、比权限位,发现两份完全一样之后反而更慌了,因为完全一样通常意味着其中一份是冗余的,而冗余在一块这么小的盘上是要命的事,我甚至已经想好先删 /usr/bin 里那几个占地方大的,留着 /bin 保底,连先删哪个后删哪个的顺序都排过一遍,就等哪天磁盘满了动手。

(现在回头看,这大概是我离弄坏系统最近的一次,差一点就把一个完全错误的结论当成维护操作执行下去。)

后来跑了一条命令,谜底就开了。

1
ls -ld /bin
1
lrwxrwxrwx 1 root root 7 Aug 30 12:02 /bin -> usr/bin

/bin 根本不是目录,是个指向 usr/bin 的符号链接(符号链接就是快捷方式,你访问它,等于访问它指向的那个东西),我看到的所谓两份,从头到尾就是同一份,那一行开头的 l 就写着答案,只是我当时连权限位有几位、第一位代表什么都不知道,看过去就是一片 rwx 混着几个短横,那一串是权限位,决定谁能读、谁能改、谁能执行,讲权限那篇会拆开讲。但这又带出两个新问题,既然一样,为什么还要留这个链接,以及最开始为什么要分成两个地方,答案分别在 1994 年和 2012 年。

1994 年那版约定,考虑的是小硬盘

这套目录结构叫 FHS,文件系统层次结构标准,它不强制,但几乎所有发行版都照着来,所以你的部署脚本换个发行版大多还能跑。1994 年那版 FHS 的出发点非常具体,那时候 / 可能只有几百 MB,/usr 常常单独挂在另一块盘上,可能更大、更慢,甚至是网络盘,那个年代在几台机器之间共享一份 /usr 是省钱的做法,十几台工作站只留一张盘放系统,省下来的磁盘钱比网费多。于是有个要命的问题,系统开机时如果 /usr 还没挂上,ls 在哪,bash 在哪,没有这些,你连修都没法修。

目录 装什么 为什么这么分
/bin 启动和修复必需的二进制 /usr 没挂时也得能用
/sbin 同样必需,偏系统管理(fsck、mount) 同上
/lib /bin、/sbin 依赖的共享库 同上
/usr 非必需的用户态程序,占大头 可以单独挂、可只读共享
/etc 配置 每台机器各管各的
/var 会一直变的数据:日志、数据库、缓存 会持续长大
/tmp 临时文件 谁都能写
/home 用户家目录 单独挂一块盘最省事
/root root 的家目录 放 /home 下的话,/home 挂了 root 就登不进去
/dev /proc /sys 内核导出的虚拟文件 根本不在磁盘上
/opt 第三方软件整包安装 一个软件一个目录,好整体删

规矩就是从这张表长出来的,启动和修复必需的东西必须放在 / 上,不能放 /usr,这条规矩硬到连 mount 自己都得遵守,因为它正是那个负责把 /usr 挂上去的程序。

所以 /bin 和 /usr/bin 最初的分工是,一份救命,一份日常。

2012 年前后,这份担心不成立了

硬盘变大了,更关键的是 initramfs 普及了,内核启动时先挂一个内存里的迷你根文件系统,要什么带什么,等真正的根挂好再切过去,这个过渡只有几秒钟,但足够把 mount、fsck 和必要的驱动模块塞进去,于是那条 /usr 没挂上就完蛋的前提从根上就没了。于是有人提了个提案,叫 UsrMerge,既然 /bin 和 /usr/bin 里的东西高度重复,打包时还得反复纠结往哪边放,不如全搬到 /usr 下,老路径留个符号链接,一个软件的安装位置从此只有一种写法,打包脚本里那些判断要不要拆 bin 的分支全可以删掉。

Debian 12、Ubuntu 20.04、Fedora 17 及之后的版本都做了这件事(Ubuntu 在 20.04 就完成了,18.04 里 /bin、/sbin 还是真目录),合并之后这四个老路径全都变成指向 usr 下面对应目录的链接。

1
2
3
4
/bin    -> usr/bin
/sbin -> usr/sbin
/lib -> usr/lib
/lib64 -> usr/lib64

没合并的系统里,/bin 还是实打实的目录,ls -ld 显示 drwxr-xr-x,CentOS 7 是很典型的一个,很多老 Debian 也是,判断一台机器合没合并,不用去查文档,一行就看出来了。

1
ls -ld /bin /sbin /lib /lib64

看到 lrwxrwxrwx 是合并过,看到 drwxr-xr-x 是没合并,这一行的差别直接决定了你写部署脚本时该用 /bin/bash 还是 /usr/bin/bash,合并之后两个都能用,但没合并的机器上只有前者靠得住。这个坑我在一个 CentOS 7 的部署脚本上踩得很实,本地测得好好的,上了那台机器就报找不到 /usr/bin/bash,我盯着那行报错看了十分钟才反应过来,不是路径写错了,是那台机器压根没做合并。

(改法很简单,把 shebang 写回 /bin/bash,但当时我是把整个脚本复制一份改路径,现在想想挺蠢的。)

链接是怎么生效的

/bin/ls 在合并之后仍然能用,是因为内核在解析路径时会自动穿透符号链接,你敲 /bin/ls,内核按 /bin 找到链接、读出内容 usr/bin、换成 /usr/bin、再拼上 /ls,最后落到 /usr/bin/ls,整个过程对用户完全透明,ls 自己也不知道它是从哪个路径被找到的。这一步很容易被误以为复制了一份,实际不是,不存在两份 ls 占两份空间,也不需要任何同步,find / -type f -name ls 只会找到一个。另一个细节是 /bin -> usr/bin 写的是相对链接,不是 /usr/bin,因为如果哪天你把根挂成只读、或者从救援系统里把它挂到 /mnt 下,绝对链接 /usr/bin 就指到 chroot 外面去了,而相对链接跟着新根一起走,你把整棵树搬到哪儿它都还在树里。

写符号链接的一条经验,目标能相对就相对。

(这条我是用一张救援盘折腾出来的,chroot 进去之后所有命令都找不到,查了半天才发现是链接指到了外面。)

/usr 不是 user

这个误解流传极广,/usr 早期确实被理解成用户目录,但今天的意思更接近 Unix System Resources,非必需的、可共享的、通常只读的系统资源,证据很简单,/usr/bin 里有 sudo,有 systemctl,这些跟用户没什么关系。至于 /sbin 的那点特权,现代 PATH 早就把它打破了,这事比想象中绕,而且取决于你怎么登录,登录 shell 的 PATH 来自 /etc/login.defs 里的 ENV_PATH,Ubuntu 18.04、20.04、22.04 三版一字不差,到今天也不含 /sbin,含 /sbin、/usr/sbin 的是 /etc/environment,那是图形会话在用的,18.04 就已经是这样,所以同一台机器上,SSH 进来的 PATH 和桌面里打开的 PATH 可以不一样(前面那篇讲终端的第一篇里给的那条 PATH 就是图形会话的)。真正决定你能不能跑 fsck 的,是文件权限和 sudo,不是它放在哪个目录,/sbin 现在更多是分类习惯,不再是权限边界。

四行输出,全是 l

合并过的机器,四行输出全都以 l 开头。

1
ls -ld /bin /sbin /lib /lib64
1
2
3
4
lrwxrwxrwx 1 root root  7 Aug 30 12:02 /bin -> usr/bin
lrwxrwxrwx 1 root root 8 Aug 30 12:02 /sbin -> usr/sbin
lrwxrwxrwx 1 root root 7 Aug 30 12:02 /lib -> usr/lib
lrwxrwxrwx 1 root root 7 Aug 30 12:02 /lib64 -> usr/lib64

再验证一下虚拟目录到底真不真,/proc 和 /sys 的容量是 0,因为它们不在任何磁盘上,是内核在内存里现生成的,df 拿到的只是个假数字。

1
df -h / /proc /sys
1
2
3
4
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1 98G 41G 52G 44% /
proc 0 0 0 - /proc
sysfs 0 0 0 - /sys

/tmp 的权限位是另一个故事。

1
ls -ld /tmp
1
drwxrwxrwt 12 root root 4096 Nov 13 10:00 /tmp

注意权限位最后那个 t,这是 sticky bit,意思是这个目录谁都能写,但只能删自己创建的文件,没有它,任何用户都能 rm 掉别人的临时文件,这是二十世纪就修过的漏洞。

/tmp 会不会自动清,取决于发行版和 systemd-tmpfiles 的配置,Ubuntu 在 tmpfiles.d 里有 q /tmp 1777 root root 10d 这样的规则,10 天没动过的东西会被删掉,所以别把要留的东西放 /tmp。

(这条教训我是用一次数据丢失换来的,一个还没提交的 patch 放在那儿过了个周末,回头只剩目录。)

这几个目录名,猜错是常态

/bin 和 /usr/bin 是两套命令?合并之后它们是同一套,合并之前确实是两套,但内容高度重叠,本来就容易让人以为是重复安装。/usr 是用户目录?用户家目录在 /home,/usr 是系统资源。/proc 是个真目录?它大小是 0,大部分写不进去,重启就没了,/proc/1/ 里的 1 是 PID(进程编号,每个跑着的程序都有一个),不是第一号文件。

/opt 和 /usr/local 随便用?惯例是包管理器装的进 /usr,自己编译的进 /usr/local,整包第三方软件(一个大 JRE、一个大 SDK)进 /opt,混着用,卸载时会变成考古现场。

/var 可以随便删?/var/lib 里有数据库和容器运行时数据,/var/log 里有你正需要的现场,rm -rf /var/* 是能把系统删到起不来的。

目录是约定的产物

根目录这套结构是 FHS 定的,最初的出发点就是 /usr 挂不上时系统还得能起来,/bin 到 /usr/bin 是 UsrMerge 留下的符号链接,ls -ld /bin 一行就能看出合没合并,/usr 不是用户目录,/home 才是,/proc、/sys、/dev 不在磁盘上,df 报 0 很正常。

/tmp 的 sticky bit 只防别人删你的文件,不防别人读,敏感内容别放那儿。

目录认全了,下一个问题是,某条命令我不知道怎么用,该去哪问。