文件系统结构
Linux 运维基础 · 第二章 目标:理解 Linux 目录布局,掌握路径、inode、链接的核心概念
1. 概述#
Windows 用盘符(C: D: E:)组织文件,插个 U 盘就多一个盘符。Linux 完全不同——一切皆文件,所有东西挂在同一个根 / 下。磁盘、键盘、进程信息、网络连接……统统表示为文件。
本章从运维的角度帮你看清三个关键问题:
/etc和/var分别放什么?万一生产出问题,知道去哪翻。df -h满了但du -sh不大——是不是"幽灵文件"在占空间?- 软链接和硬链接到底有什么区别?删了源文件链接还能用吗?
2. FHS 目录约定#
FHS(Filesystem Hierarchy Standard)规定了各目录该放什么。以下是运维必须记住的核心目录:
2.1 核心目录速查#
| 目录 | 全称 / 含义 | 放什么(一句话) |
|---|---|---|
/ | root | 整个文件系统的起点,所有目录的祖先 |
/etc | Editable Text Configuration | 配置文件。一切服务的配置都在这里(nginx、ssh、systemd 等) |
/var | Variable | 可变数据:日志(/var/log)、缓存、数据库文件、邮件队列等 |
/tmp | Temporary | 临时文件,所有用户可写,重启可能清空 |
/proc | Process | 伪文件系统,存放内核和进程的运行时信息(内存数、CPU 信息) |
/home | Home | 普通用户的家目录,每个用户一个子目录 |
/usr | Unix System Resources | 用户程序、库、文档(现代解释为"系统资源";历史上最初其实是 user 家目录,后演变为只读共享数据区) |
/opt | Optional | 第三方软件的独立安装目录(如 /opt/google/chrome) |
/root | — | root 用户的家目录,不在 /home 下 |
/dev | Device | 设备文件,磁盘、终端、键盘等都以文件形式存在 |
/bin, /sbin | Binary / System Binary | 基础命令(许多发行版已合并到 /usr/bin) |
/boot | Boot | 内核镜像和引导加载器文件 |
/lib, /lib64 | Library | 共享库文件(/lib/modules 是内核模块) |
/mnt, /media | Mount / Media | 临时挂载点 / 自动挂载的 U 盘等 |
/run | Runtime | 系统启动以来的运行时数据(PID 文件、socket 文件等) |
/sys | System | 内核设备信息,与 /proc 类似但更结构化 |
2.2 运维常去的目录#
# 查配置
/etc/nginx/ # Nginx 配置
/etc/systemd/system/ # systemd 服务文件
/etc/ssh/sshd_config # SSH 服务端配置
/etc/fstab # 开机自动挂载表
# 查日志
/var/log/syslog # 系统主日志(Debian/Ubuntu)
/var/log/messages # 系统主日志(RHEL/CentOS)
/var/log/auth.log # 认证日志(登录、sudo 记录)
/var/log/nginx/ # Nginx 日志
# 查运行时状态
/proc/cpuinfo # CPU 详细信息
/proc/meminfo # 内存信息
/proc/loadavg # 系统负载
/var/run/ # PID 文件在这(或 /run/)
# 临时文件
/tmp/ # 注意:不要放重要数据2.3 伪文件系统 /proc /sys /dev /run#
/proc 不是真实磁盘文件,读取它本质是向内核查询系统状态,所以不占磁盘空间:
# 查看 CPU 信息
cat /proc/cpuinfo | grep "model name"
# 查看系统启动了多久
cat /proc/uptime
# 查看进程信息
ls /proc/ # PID 为名的目录就是进程的信息
cat /proc/1234/cmdline # 进程 1234 的启动命令
cat /proc/1234/status # 进程 1234 的状态和内存使用除了 /proc,Linux 还有几个"不占磁盘"的虚拟文件系统。df -hT 里能看到它们的类型是 tmpfs / devtmpfs / proc / sysfs:
| 挂载点 | 类型 | 作用 |
|---|---|---|
/proc | proc | 进程与内核运行时信息(上面讲的) |
/sys | sysfs | 内核设备/驱动/子系统的结构化视图,可读写内核参数(如 /sys/class/net/eth0/mtu) |
/dev | devtmpfs | 设备节点(/dev/sda、/dev/null),内核自动创建维护 |
/run | tmpfs | 运行时数据(PID、socket),存在内存里,重启即清空 |
/dev/shm | tmpfs | 共享内存,供进程间高速交换数据 |
tmpfs 是内存文件系统:往
/run、/dev/shm写东西不落磁盘、重启就没,但会占内存。别拿它当持久存储。
3. 绝对路径 vs 相对路径#
3.1 概念#
| 类型 | 特征 | 示例 |
|---|---|---|
| 绝对路径 | 以 / 开头,从根目录算起 | /home/user/doc/readme.txt |
| 相对路径 | 不以 / 开头,以当前目录为参考 | doc/readme.txt |
两个特殊符号:
| 符号 | 含义 |
|---|---|
. | 当前目录 |
.. | 上级目录 |
3.2 使用场景#
# 绝对路径:写脚本/配置文件时必须用——不依赖"当前在哪"
cat /etc/nginx/nginx.conf # 不管在哪个目录,都能正确读取
# 相对路径:日常交互时省敲键
cd /var/log # 先到日志目录
less syslog # 相对路径,省去 /var/log/syslog
cd ../../home/alice # 向上两级再到 alice 家目录原则:写脚本时永远用绝对路径,或先 cd 到已知位置再操作。避免 cron 任务因为"当前目录不对"而找不到文件。
4. inode 是什么#
4.1 两层结构#
每个文件在磁盘上由两个东西描述:
| 结构 | 存什么 | 存在哪 |
|---|---|---|
| inode(索引节点) | 元数据:权限、大小、时间戳、数据块指针、链接数 | 文件系统的 inode 表 |
| 目录项(dentry) | 文件名 + 对应 inode 编号 | 目录文件的数据块中 |
理解要点:文件名不是文件的一部分,它只是"指向 inode"的标签。 一个 inode 可以有多个文件名(硬链接)。
4.2 inode 中存什么#
inode 编号 (inode number)
文件类型 (普通文件/目录/链接...)
权限 (rwxr-xr-x)
UID / GID(所有者/所属组)
文件大小 (字节数)
时间戳:
- atime:最后访问时间
- mtime:最后修改内容时间
- ctime:最后修改元数据时间
链接数 (link count)
数据块指针:指向真正存数据的磁盘块atime 的现代实情:为省掉"每次读文件都写一次 atime"的开销,现在主流发行版(Rocky/Ubuntu)挂载默认带
relatime——只有当 atime 比 mtime/ctime 旧、或距上次更新超过 24h 时才更新 atime。所以别指望用 atime 精确判断"文件最后一次被读是什么时候"。想彻底关掉可用noatime挂载选项(数据库、高 IO 场景常用)。
4.3 查看 inode#
# -i 显示 inode 编号
ls -li /etc/passwd
# 输出:1234567 -rw-r--r-- 1 root root 2540 Jan 1 12:00 /etc/passwd
# ^ inode号
# 查看 inode 使用情况
df -i
# Filesystem Inodes IUsed IFree IUse% Mounted on
# /dev/sda1 655360 45321 610039 7% /运维要点:df -h 显示空间没满但创建不了文件——很可能是 inode 用完了(小文件太多)。
5. 删文件的本质#
5.1 为什么 rm 不等于删除数据#
# 准备一个普通文件(link count = 1)
echo "hello" > test.log
# 当文件没有被任何进程打开:
rm test.log
→ 1. 删除目录项(文件名消失)
→ 2. inode 的 link count 从 1 变成 0
→ 3. 内核释放 inode 和对应的数据块
# 当文件被进程打开时:
> app.log # 假设有进程正在写这个文件
rm app.log # 文件名消失了
ls -l app.log # 显示:No such file or directory
df -h # 但磁盘空间没有释放!
# 为什么?因为进程还持有文件句柄(file handle)
# inode 的 link count 变 0,但 open count 不是 0
# inode 和数据块会保留到进程关闭文件才真正释放5.2 典型故障场景:磁盘满了但找不到大文件#
运维经典故障:
df -h
# /dev/sda1 50G 50G 0 100% / ← 磁盘满了
du -sh /* | sort -rh | head
# 加起来才 30G,那 20G 去哪了?
# 真相:有进程持有已删除文件
lsof | grep deleted | awk '{print $1, $7}' | sort -nrk2 | head
# mysqld 1052672000 ← MySQL 还在写一个已删除的日志
# java 524288000 ← Java 写一个已删除的日志
# 解决方法:重启对应进程(不推荐直接杀)
systemctl restart mysqld # 重启后空间就回来了要点:被进程打开的文件,即使删了目录项,空间要等进程关闭文件才释放。
6. 软链接 vs 硬链接#
6.1 概念对比#
# 创建硬链接
ln source.txt hard_link.txt
# 创建软链接(符号链接)
ln -s source.txt soft_link.txt
# 查看
ls -li
# 12345 -rw-r--r-- 2 user group 0 Jul 1 10:00 hard_link.txt ← inode=12345, link count=2
# 12345 -rw-r--r-- 2 user group 0 Jul 1 10:00 source.txt ← 同一个 inode!
# 67890 lrwxrwxrwx 1 user group 10 Jul 1 10:00 soft_link.txt -> source.txt
# ↑ 类型是 l(link),inode 不同,大小是"目标路径名长度"6.2 详细对比#
| 特性 | 硬链接 (hard link) | 软链接 (symlink / symbolic link) |
|---|---|---|
| 本质 | 同一个 inode 的多个文件名 | 一个独立的"指针文件",存目标路径字符串 |
| inode | 相同(与源文件共享 inode) | 不同(自己有一个新 inode) |
| 跨文件系统 | 不允许(不同文件系统 inode 独立) | 允许(存的是路径字符串) |
| 跨分区 | 不允许 | 允许 |
| 链接目录 | 不允许(有些系统/特权除外) | 允许 |
| 删除源文件后 | 链接仍可用(只要 link count > 0,数据还在) | 链接断掉(指向不存在的路径) |
| 修改链接 | 源文件同步变(同一个 inode) | 源文件同步变(写操作经过路径解析) |
| 显示 | ls -l 看不出,"感觉"就是普通文件 | ls -l 第一列为 l,文件名后有 -> |
| 用途 | 防止误删重要数据 | 快捷方式、路径别名、跨分区引用 |
6.3 典型用法#
# 软链接:给长路径起个短名
ln -s /opt/application-v3.2.1-current /opt/app
cd /opt/app # 实际进到 /opt/application-v3.2.1-current
# 软链接:版本切换(滚动更新)
rm /opt/app
ln -s /opt/application-v3.2.2-current /opt/app # 一键切版本
# 硬链接:备份保护
ln important_file important_file.backup
# 即使有人 rm important_file,数据还在(link count > 0)
# 软链接:配置文件管理
ln -s /etc/nginx/sites-available/myapp.conf /etc/nginx/sites-enabled/
# Nginx 只加载 sites-enabled 下的配置6.4 注意事项#
# 软链接要注意相对路径陷阱
ln -s ../config/app.conf /etc/myapp/link_app.conf
# 软链接里存的是字面字符串;解析时相对「软链接自己所在的目录」(/etc/myapp/),
# 而不是你的当前目录——这里实际指向 /etc/config/app.conf。
# 坑在于:数 .. 要从软链接的位置数(容易数错),且一旦软链接被移动,目标就跟着错位
# 建议:创建软链接时目标用绝对路径
ln -s /opt/app/config/app.conf /etc/myapp/link_app.conf # 安全
# 查看软链接的真正目标
readlink /etc/myapp/link_app.conf # 显示目标路径
readlink -f /etc/myapp/link_app.conf # 递归解析到最终绝对路径7. 实战:排查"幽灵空间"#
场景#
监控告警:/data 分区使用率达到 98%。运维登录排查。
步骤#
# Step 1:确认告警
df -h /data
# /dev/sdb1 100G 98G 2G 98% /data
# Step 2:定位大户
du -sh /data/* 2>/dev/null | sort -rh | head -5
# 52G /data/mysql
# 18G /data/logs
# 5.2G /data/backup
# ...加起来才 75G,98G - 75G = 23G 去哪了?
# Step 3:查是不是"已删文件句柄没释放"
lsof +L1 /data 2>/dev/null | awk '{print $1, $2, $7}' | sort -nrk3 | head
# java 12345 15032385536 ← 一个 14G 的已删除文件!
# syslog-ng 8876 8589934592 ← 一个 8G 的已删除日志!
# Step 4:确认是哪个进程
ps aux | grep 12345
# appuser 12345 5.2 ... java -jar /data/app.jar
# Step 5:通知开发/业务方,重启对应的进程
systemctl restart myapp
# Step 6:确认释放
df -h /data
# /dev/sdb1 100G 75G 25G 75% /data ← 恢复8. 小结#
| 知识点 | 一句话记忆 |
|---|---|
| FHS | 配置去 /etc,日志去 /var/log,临时文件去 /tmp |
| 绝对/相对路径 | 写脚本用绝对,日常用相对 |
| inode | 存元数据,文件名只是指向 inode 的标签 |
rm 不释放空间 | 进程持有文件句柄时,空间不会释放——lsof | grep deleted |
| 软链接 | 快捷方式,删源文件就断;可跨分区、可链目录 |
| 硬链接 | 同一 inode 的别名,删源不影响;不可跨分区、不可链目录 |