系统(三):文件系统、IO 与硬件基础
系统(三):文件系统、IO 与硬件基础
导语:这一篇把「数据从磁盘到应用要经过什么」讲透。从 inode 与文件读写流程、硬链接与软链接、fd 与"一切皆文件"讲起,说清顺序 IO 与随机 IO 的性能差异,再补齐 CPU 缓存与伪共享、中断与 DMA、软中断与中断下半部(网络性能的关键),最后落到进程调度算法,共 8 题。
一、文件系统
1. 什么是 inode?一次文件读取经历了哪些步骤?
答: inode(索引节点)是文件系统描述"一个文件"的元数据结构——它回答「文件的数据块在哪、有多大、属于谁、有什么权限」。
| inode 中的关键字段 | 作用 |
|---|---|
| 权限(rwx)与属主/属组 | 访问控制 |
| 大小、块数 | 文件长度 |
| 三个时间戳 | atime(访问)、mtime(内容修改)、ctime(inode 变更,改名/改权限也会变) |
| 链接计数(link count) | 硬链接数量;减到 0 才真正释放 inode 与数据块 |
| 数据块指针表 | 直接 + 一级/二级/三级间接指针(支持超大文件) |
一次 open + read 的完整流程:
四个高频追问:
| 问题 | 答案 |
|---|---|
「为什么 ls -l 慢、ls 快?」 | ls -l 要读每个文件的 inode(取权限/大小/时间),而 ls 只需读目录内容(文件名 + inode 号) |
「为什么文件名不能有 /?为什么重命名很快?」 | 文件名只存在目录项里;mv(同分区)只是改目录项的名字指向,不搬数据 |
「df 显示 inode 用满但空间还有?」 | 典型场景是大量小文件——数据块没用多少,但 inode 数量被耗尽(df -i 可查),此时建不了新文件 |
| 「删除正在被写入的大文件后空间没释放?」 | rm 只是减少链接计数;进程仍持有 fd 引用 → inode 与数据块不释放(`lsof |
2. 硬链接与软链接有什么区别?
答:
| 维度 | 硬链接(Hard Link) | 软链接(Symbolic Link,符号链接) |
|---|---|---|
| 本质 | 多个目录项指向同一个 inode | 一个独立文件,内容是"目标路径" |
| inode | 与源文件相同 | 自己的 inode(与源文件不同) |
| 跨文件系统 | ❌ 不支持(inode 号只在同一文件系统内唯一) | ✅ 支持 |
| 指向目录 | ❌ 一般不允许(会造成目录环) | ✅ 允许 |
| 源文件被删 | 文件仍可访问(链接计数 -1,数据不丢) | 链接失效(变成"断链 / dangling",ls 显示红色/闪烁) |
| 创建命令 | ln src dst | ln -s src dst(-s 是 symbolic) |
ls -l 显示 | 普通文件(链接计数 > 1) | l 开头 + dst -> src |
| 大小 | 与源文件相同(就是同一个 inode) | 很小(只存路径字符串的长度) |
三个高频追问:
- 「
ls -l里第二列的数字是什么?」 → 硬链接计数(link count)。目录的计数至少为 2(.与父目录里的自己); - 「为什么目录不能建硬链接?」 → 会造成目录树出现环(遍历/删除时死循环、
fsck无法处理); - 「软链接的典型用途?」 → ① 跨越文件系统/分区(
/usr/lib指向实际版本目录);② 版本切换(current -> v1.2.3,切换只改链接);③ 动态库版本管理(libfoo.so -> libfoo.so.1.2)。
3. 什么是"一切皆文件"?文件描述符与打开文件表是什么关系?
答:
"一切皆文件" 是 Unix 的设计哲学:把设备、管道、socket、目录都抽象成"文件",用统一的 open/read/write/close/ioctl 接口操作。
| 对象 | 在文件系统中的表现 |
|---|---|
| 普通文件 | 常规文件 |
| 目录 | 内容为"文件名 → inode 号"的文件 |
| 块设备 | /dev/sda(可 dd 直接读写) |
| 字符设备 | /dev/tty、/dev/null |
| 管道 / FIFO | 有文件名的管道 |
| Socket | 有 fd,可用 read/write(但 lseek 无效) |
/proc、/sys | 虚拟文件系统,读写即"查询/修改内核状态" |
统一的代价:socket 没有"文件偏移"、epoll 不能 lseek——所以"一切皆文件"是接口层面的统一,不是语义层面的等同。
三层结构(fd → 打开文件表 → inode):
由此解释的三个现象(面试常考):
| 现象 | 原因 |
|---|---|
fork 后父子进程的读写会互相影响 | 它们共享同一个"打开文件描述"的 offset(不是各自独立) |
dup(fd) 得到的两个 fd 互相影响 | 同上一行;而两次独立 open 同一个文件则各有独立 offset |
ulimit -n 限制的是"fd 数量" | 每个进程的 fd 表大小有限 → 高并发服务必须调大(见《网络(四)》) |
Linux 的 VFS(虚拟文件系统):在具体文件系统(ext4/XFS/NFS/overlayfs)之上提供统一抽象(inode、dentry、file、superblock 四个核心对象)。这带来的工程价值:
- 应用无需关心底层是 ext4 还是 NFS(统一
read/write); - 容器技术的基础:overlayfs 用"联合挂载"实现镜像分层(只读层 + 可写层);
/proc、/sys、cgroup都是 VFS 的伪文件系统(所以echo 1 > /proc/...能改内核参数)。
二、IO 性能与硬件
4. 顺序 IO 与随机 IO 有什么区别?机械盘和 SSD 差异在哪?
答:
| 指标 | 含义 | HDD | SSD |
|---|---|---|---|
| IOPS | 每秒 IO 次数 | 100~200 | 几万~几十万(随机) |
| 吞吐(MB/s) | 每秒传输字节 | 100~200 | 500~7000(NVMe) |
| 延迟 | 单次 IO 耗时 | ~10ms | ~0.1ms 甚至更低 |
| 顺序 vs 随机 | — | 顺序快 100 倍以上 | 差距小很多(但仍存在,因为预读与写放大) |
| 随机写代价 | — | 高(寻道) | 中高(写放大 + GC) |
| 寿命 | 擦写次数 | 几乎无限 | 有限(TBW) |
由此推出的架构原则(面试必答的三条):
| 原则 | 原因 |
|---|---|
| ① 日志类写入一律"顺序追加(Append-Only)" | Kafka 的 CommitLog、MySQL 的 redo log、RocketMQ 的 CommitLog 都是顺序写 —— 顺序写能接近磁盘顺序带宽,随机写会让 HDD 直接崩 |
| ② 用"顺序写 + 后台归并"代替"原地随机更新" | LSM-Tree(RocksDB、HBase、LevelDB、ClickHouse 的 MergeTree 思路):内存写 memtable → 顺序刷成 SST 文件 → 后台 compaction 合并。牺牲读放大换取写的高吞吐 |
| ③ 用 Page Cache 把随机访问"变"成内存访问 | 索引、热点数据常驻页缓存(Kafka 索引 mmap、MySQL Buffer Pool) |
B+ 树 vs LSM-Tree(经典对比,常被追问):
| 维度 | B+ 树(MySQL InnoDB) | LSM-Tree(RocksDB / HBase) |
|---|---|---|
| 写方式 | 原地更新(可能随机写) | 顺序追加 + 后台合并 |
| 写性能 | 中(随机写 + 页分裂) | 高(顺序写) |
| 读性能 | 好(树高固定,几次 IO) | 较差(可能查多层 + 需 compaction) |
| 空间放大 | 小 | 中(旧版本数据要等 compaction 才清理) |
| 适用 | 读多写少、事务型(OLTP) | 写多读少、日志/时序/大数据量 |
面试延伸:「为什么 Kafka 用磁盘还能做到百万级吞吐?」→ 顺序写(Append-Only)+ Page Cache + 零拷贝(sendfile)+ 批量 + 分区并行。"顺序写 + 页缓存"让磁盘的顺序写速度接近内存带宽,这才是关键(不是"磁盘比内存快")。
5. CPU 缓存与局部性原理是什么?什么是伪共享(False Sharing)?
答:
为什么需要 Cache:CPU 与内存速度差距巨大(CPU 纳秒级、内存百纳秒级,差 1~2 个数量级),直接访存会成为瓶颈。
| 层级 | 位置 | 特点 |
|---|---|---|
| L1 | 每核独享 | 最快最小,分指令 Cache(I-Cache)与数据 Cache(D-Cache) |
| L2 | 一般每核独享 | 较快,中等容量 |
| L3 | 多核共享 | 容量大、比内存快,是核间数据交换的"中转站" |
局部性原理(Cache 生效的前提):
| 原理 | 含义 | 如何被利用 |
|---|---|---|
| 时间局部性 | 刚访问过的数据很可能很快再次访问(循环变量、函数返回地址) | Cache 把它留着,再次访问即命中 |
| 空间局部性 | 访问某地址时,邻接地址很可能随后被访问(数组遍历、顺序取指令) | Cache 按 Cache Line(通常 64 字节)整块加载,一次载入一片 |
工程启示:数组顺序遍历远比链表随机跳访缓存友好;把热数据放在一起(AoS → SoA)能显著提升命中率。
伪共享(False Sharing)——CPU 缓存最经典的性能陷阱:
两个必须知道的实例(面试高频):
| 实例 | 说明 |
|---|---|
@Contended(Java 8+) | JDK 内部用 @sun.misc.Contended 给字段填充(padding),让它独占 Cache Line。典型用于 Thread、ForkJoinPool、ConcurrentHashMap 的计数单元、LongAdder 的 Cell——LongAdder 比 AtomicLong 快就是因为把计数分散到多个 Cell 并做了 padding |
LongAdder / AtomicLong 的对比 | AtomicLong 所有线程 CAS 同一个变量(真共享 + CAS 自旋);LongAdder 分段累加(Cell 数组),每个 Cell 独立 padding,大幅降低竞争(在读少写多的高并发计数场景快数倍) |
避免伪共享的手段:
// ① Java:用 @Contended(需 -XX:-RestrictContended 才对外部类生效)
@sun.misc.Contended
static final class PaddedLong { volatile long value; }
// ② 手工填充(经典做法):用无关字段把变量"撑"满一条 Cache Line(64B)
class PaddedCounter {
public volatile long p1, p2, p3, p4, p5, p6, p7; // 7 × 8B = 56B 填充
public volatile long value; // 独占一条 Line
public volatile long q1, q2, q3, q4, q5, q6, q7;
}面试怎么答:先讲「Cache Line 是缓存一致性的粒度(64B)」,再讲「不同变量落在同一 Line 上会产生"逻辑无关、物理竞争"的伪共享」,最后给出「padding /
@Contended/LongAdder分段」三个解法,并提一句「这也是并发性能调优里最容易被忽略的一环」。
三、中断与调度
6. 中断是什么?为什么需要 DMA?
答:
中断(Interrupt):硬件/软件事件打断 CPU 当前执行流,转去执行对应的中断处理程序。
| 类型 | 来源 | 例子 |
|---|---|---|
| 外部硬件中断 | 设备 | 网卡收包、磁盘 IO 完成、时钟 tick、键盘 |
| 内部异常(同步中断) | 当前指令 | 缺页异常、除零、非法指令、系统调用(int 0x80/syscall) |
| 软中断(软件中断) | 软件触发 | Linux 的 softirq(性能关键,见第 7 题) |
中断的意义:让 CPU 不必"轮询"设备——没有中断就只能不断问"数据到了吗?",浪费大量 CPU。中断让 CPU 在设备就绪时被动被通知。
从"中断驱动 IO"到"DMA"的演进(关键逻辑):
| 维度 | 中断 | DMA |
|---|---|---|
| 本质 | 事件通知机制 | 数据传输机制 |
| 谁搬数据 | CPU | DMA 控制器 |
| 中断频率 | 每字节/每字(PIO 模式) | 整块传输完成后一次 |
| 关系 | 两者配合:DMA 完成整块传输后用一次中断告知 CPU | 同左 |
与零拷贝的呼应(把知识串起来):
两个高频追问:
- 「DMA 会不会与 CPU 抢内存带宽?」 → 会。所以有 IOMMU(做地址翻译与隔离)与内存带宽瓶颈的问题;高吞吐场景(100Gbps 网卡)会受内存带宽限制;
- 「为什么高并发下网卡中断会成为瓶颈?」 → 每包一次中断在百万 PPS 下会让 CPU 全在处理中断(
si占比高)→ 解法见第 7 题(NAPI、RPS、多队列网卡)。
7. 什么是软中断与中断下半部?为什么它和网络性能相关?
答:
问题:中断处理程序执行期间会屏蔽(部分)中断,如果它做太多事(拷贝数据、协议栈处理),会长时间阻塞其他中断与调度,导致系统响应变差。
解法:把中断处理拆成"上半部 + 下半部":
为什么它决定网络性能(这是面试的考察点):
高并发下的瓶颈与四个优化手段(面试高频):
| 手段 | 原理 | 效果 |
|---|---|---|
| ① NAPI(New API) | 中断 + 轮询结合:第一个包触发中断,之后暂时关闭中断改为轮询批量收包,收完再开中断 | 避免"每包一中断",显著降低 si |
| ② RPS(Receive Packet Steering) | 软件层面把包按哈希分发给多个 CPU 核处理协议栈 | 单队列网卡也能多核分担(吃 CPU 但改善单核瓶颈) |
| ③ RFS / aRFS | 让处理该包的 CPU 恰是应用所在 CPU | 提升 CPU 缓存与 socket 局部性 |
④ 多队列网卡(RSS)硬件分流 + SO_REUSEPORT 多进程 | 网卡硬件按四元组哈希把包分到不同队列,每个队列绑定不同 CPU 与中断 | 从硬件层分流,最有效 |
⑤ busy_poll / XDP / DPDK | 轮询收包、内核旁路 | 极致低延迟(代价是 CPU 跑满、失去通用性) |
排查命令(实操加分):
# ① 看软中断在 CPU 时间里的占比(si 列)
top # 关注 si(软中断)与 hi(硬中断)两列
mpstat -P ALL 1 # 看是否只有某几个核的 si 很高(说明中断/软中断集中在少数核)
# ② 看软中断的细分统计(NET_RX 是网络收包)
cat /proc/softirqs # 各 CPU 上各类 softirq 的累计次数(NET_RX / NET_TX / TIMER / RCU...)
# ③ 看硬中断分布与网卡队列
cat /proc/interrupts | grep -E "eth|ens|nvme"
ethtool -l eth0 # 网卡队列数量(RSS)
ethtool -S eth0 | grep -i drop # 网卡丢包统计
# ④ 看软中断处理线程的 CPU 占用(ksoftirqd/<cpu>)
ps -eo pid,comm,pcpu | grep ksoftirqd面试收尾句:「网络高并发性能问题的排查,本质上要区分"是硬中断、软中断(si)、还是进程上下文(sys)在吃 CPU"。如果
si很高而应用 CPU 不高,就要往 NAPI/RPS/多队列网卡的方向优化,而不是继续加应用线程。」
8. 常见的进程调度算法有哪些?
答:
| 算法 | 思路 | 优点 | 缺点 |
|---|---|---|---|
| FCFS(先来先服务) | 按到达顺序执行 | 简单 | 长作业会拖死短作业(护航效应) |
| SJF(短作业优先) | 优先执行预计运行时间最短的 | 平均等待时间最优(理论) | 需要预知运行时间;长作业可能饥饿 |
| HRRN(高响应比优先) | 响应比 = (等待时间 + 服务时间) / 服务时间,取最大 | 兼顾等待与长度,避免饥饿 | 需计算,依然要预知服务时间 |
| 时间片轮转(RR) | 每个进程分配一个时间片,用完就排到队尾 | 公平、响应快 | 时间片太小 → 切换开销大;太大 → 退化为 FCFS |
| 优先级调度 | 按优先级执行(可抢占/非抢占) | 能满足实时性 | 低优先级饥饿(可用老化 aging 缓解) |
| 多级反馈队列(MLFQ) | 多个优先级队列 + 动态调整:新进程进高优先级队列(小时间片),用完就降到低优先级(大时间片);定期提升低优先级 | 综合最好:短作业响应快、长作业也能跑完、无需预知运行时间 | 实现复杂、参数多 |
| CFS(完全公平调度,Linux 默认) | 用虚拟运行时间 vruntime 衡量"谁欠得多",总选 vruntime 最小的(用红黑树组织) | 无需固定时间片、公平且自适应 | 需要维护红黑树;不适合硬实时 |
| 实时调度(SCHED_FIFO / SCHED_RR / EDF) | 按固定优先级/截止时间调度 | 满足确定性 | 配置不当会导致普通任务完全饿死 |
Linux 的现代调度类(面试加分):
三个高频追问:
- 「为什么时间片不能太小也不能太大?」 → 太小 → 上下文切换开销占比过高(切换可能比干活还贵);太大 → 交互式任务的响应延迟变大(等不到 CPU)→ 所以需要"多级反馈队列 / CFS"这类自适应方案;
- 「怎样让一个重要进程优先跑?」 → ①
nice -n -10 ./prog(普通用户只能调高,调低需 root);②chrt -f 50 ./prog(设为 FIFO 实时任务,谨慎,可能饿死系统进程);③taskset/cpuset绑定 CPU;④ 容器里用 cgroup cpu.shares / cpu.max 限流与保障; - 「CFS 里的
vruntime是什么?」 → 衡量"该进程已经消耗的加权 CPU 时间",权重来自 nice 值。调度器总是选 vruntime 最小的 → 让所有进程的加权运行时间趋于相等(完全公平)。
查看与调试(实操):
ps -eo pid,ni,pri,psr,pcpu,comm --sort=-pcpu | head # ni=nice, pri=优先级, psr=当前 CPU
chrt -p <pid> # 查看某进程的调度策略与优先级
taskset -cp <pid> # 查看/设置 CPU 亲和性
cat /proc/<pid>/sched # 单进程调度详情(vruntime、切换次数)
cat /proc/schedstat # 全局调度统计
perf sched record / report # 深入分析调度延迟(进阶)下一篇:《系统(四)》进入Linux 性能排查与调优——
load average的正确判读、CPU 使用率的组成(us/sy/wa/si)与高 CPU 排查、内存指标(free的available、OOM Killer)、磁盘 IO 瓶颈定位、网络排查(ss/tcpdump)、常用性能工具清单、关键内核参数总表,以及一套「线上变慢」的完整排查 SOP。
