Linux & 操作系统面试八股文 30 道|深度详解版(Java 后端必备)
本文是 Linux & 操作系统面试八股文深度详解版,共 30 题,覆盖进程/线程、内存管理、IO 模型、文件系统、调度算法等核心考点。每道题都配有原理分析 + 实战命令 + Java 视角,适合 Java 后端开发者面试前系统复习。
Q1. 进程 vs 线程的本质区别(从 Linux 内核 task_struct 角度解释)
核心结论
在 Linux 内核里,进程和线程都是 task_struct 结构体的实例,本质上都是调度单元。区别在于资源的共享程度。
task_struct 关键字段
1 | |
判断是进程还是线程:看 pid == tgid 是否成立。
- 主线程:
pid == tgid,是进程领头羊 - 子线程:
pid != tgid,属于同一线程组
关键区别对比
| 维度 | 进程 | 线程 |
|---|---|---|
| 资源 | 独立的虚拟地址空间、文件描述符 | 共享进程的内存、fd、堆 |
| 创建开销 | 大(fork() 需拷贝页表) |
小(clone() 共享内存) |
| 崩溃影响 | 独立,不影响其他进程 | 一个线程崩溃导致整个进程崩溃 |
| 通信 | IPC 机制(复杂) | 共享内存(简单但需同步) |
| Linux 系统调用 | fork() / exec() |
clone(CLONE_VM | CLONE_FILES) |
Java 视角
Java 的 Thread 底层就是通过 pthread_create 创建内核线程,对应 Linux 的 clone() 系统调用。每个 Java 线程都有独立的栈(默认 512KB~1MB),但共享堆内存。
1 | |
Q2. 进程间通信(IPC)6种方式
总览
| 方式 | 速度 | 适用场景 |
|---|---|---|
| 管道 | 中 | 父子进程、Shell 命令链 |
| 消息队列 | 中 | 异步解耦、带格式的消息 |
| 共享内存 | 最快 | 大量数据传输,需配合信号量 |
| 信号 | 快(无数据) | 异常通知、进程控制 |
| 信号量 | 快 | 互斥/同步控制 |
| Socket | 慢(最灵活) | 跨机器、跨语言通信 |
1. 管道(Pipe)
匿名管道:只能在父子进程间使用,单向,内核缓冲区通常 64KB。
1 | |
命名管道(FIFO):通过文件系统路径访问,无亲缘关系的进程也能用。
1 | |
2. 消息队列
内核维护的链表,按消息类型过滤,支持优先级。比管道多了消息边界和类型过滤。
1 | |
3. 共享内存(最快的 IPC)
直接映射同一块物理内存到不同进程的虚拟地址空间,零拷贝,但需要同步机制。
1 | |
4. 信号(Signal)
异步通知机制,只能携带信号编号,不能携带数据。常见信号:
SIGTERM(15):优雅停止SIGKILL(9):强制杀死(不可捕获)SIGUSR1/2:用户自定义
5. 信号量(Semaphore)
本质是一个计数器,用于进程间互斥/同步,不传数据。
6. Socket
最通用,支持同机(Unix Domain Socket,比 TCP 快)和跨机(TCP/UDP)。
1 | |
Java 视角
Java 微服务间通信(RPC/HTTP)本质是 Socket。JVM 进程内可以用 Queue,但跨 JVM 就需要 MQ(Kafka/RabbitMQ)。
Q3. 线程间同步的4种方式
为什么需要同步
多线程共享内存,并发访问导致竞态条件(Race Condition),需要同步保证操作的原子性和可见性。
1. Mutex(互斥锁)
同一时刻只有一个线程能持有锁,其他线程阻塞等待。
1 | |
Java 对应:synchronized、ReentrantLock
注意:mutex 是不可重入的(pthread_mutex_t),Java 的 synchronized 是可重入的。
2. 条件变量(Condition Variable)
配合 mutex 使用,让线程在满足特定条件前主动让出锁并挂起,条件满足时被唤醒。
1 | |
Java 对应:Object.wait()/notify()/notifyAll(),Condition.await()/signal()
经典用法:生产者-消费者模式
3. 信号量(Semaphore)
计数信号量,允许 N 个线程同时访问资源(N=1 时退化为 mutex)。
1 | |
Java 对应:java.util.concurrent.Semaphore
1 | |
4. 读写锁(ReadWriteLock)
允许多个读线程并发,写线程独占。适合读多写少的场景。
1 | |
Java 对应:ReentrantReadWriteLock,StampedLock(支持乐观读)
选择建议
- 简单互斥 → mutex / synchronized
- 等待条件 → 条件变量 / wait-notify
- 限流并发 → 信号量
- 读多写少 → 读写锁
Q4. 用户态线程 vs 内核态线程 vs 协程
三种模型
内核态线程(KLT - Kernel Level Thread)
由操作系统内核直接管理,每次线程切换都需要系统调用,有用户态↔内核态切换开销。
- Linux 的
pthread就是内核线程(1:1 模型) - Java 传统线程(JDK 19 之前)= 内核线程
- 调度由内核控制,可以利用多核
- 每个线程需要独立内核栈(通常 4~8MB),大量线程时内存消耗大
用户态线程(ULT - User Level Thread)
由用户空间的运行时(Runtime)管理,内核看到的只有少数内核线程(N:M 模型)。
- 切换不需要系统调用,速度极快(纳秒级)
- 无法利用多核(除非绑定到多个内核线程)
- Go 的 goroutine 早期用此模型,现在是 M:N
协程(Coroutine)
用户态的协作式调度,由程序员显式让出控制权(yield/await),而非抢占式。
1 | |
Java 的演进
| 时期 | 模型 | 说明 |
|---|---|---|
| JDK 1~18 | 1:1(平台线程) | Java线程 = 内核线程,重量级 |
| JDK 19~20 | 预览:虚拟线程 | M:N,由JVM调度 |
| JDK 21+ | 正式:虚拟线程 | Project Loom,轻量级协程 |
1 | |
虚拟线程阻塞时不会阻塞内核线程,JVM 运行时自动挂起并在 IO 完成后恢复,实现了协程语义。
Q5. 进程状态机(5种状态及转换)
五大状态
1 | |
Linux 实际进程状态
ps aux 的 STAT 列:
| 字符 | 含义 |
|---|---|
R |
Running 或 Runnable(就绪队列中) |
S |
Interruptible Sleep(可中断睡眠,等IO) |
D |
Uninterruptible Sleep(不可中断,正在IO,不能被信号打断) |
T |
Stopped(被 SIGSTOP 暂停) |
Z |
Zombie(僵尸进程) |
I |
Idle(内核线程空闲) |
D 状态很危险:进程卡在内核 IO 路径,kill -9 也杀不死,只能等 IO 完成或重启。
1 | |
状态转换触发条件
- 就绪 → 运行:调度器选中(
schedule()函数) - 运行 → 就绪:时间片耗尽、被高优先级进程抢占
- 运行 → 阻塞:
read()等待 IO、sleep()、wait() - 阻塞 → 就绪:IO 完成(硬件中断)、
notify()、定时器触发
Q6. 僵尸进程 vs 孤儿进程
僵尸进程(Zombie Process)
定义:子进程已经退出(exit),但父进程还没有调用 wait()/waitpid() 回收其 task_struct,导致进程表项残留。
特征:
- 进程状态为
Z - 不占用 CPU 和内存,但占用 PID 和进程表槽位
kill -9无法杀死(已经死了)
产生原因:父进程没有正确处理 SIGCHLD 信号。
1 | |
如何消灭:
- 杀死父进程(僵尸子进程会被 init 收养并回收)
- 父进程调用
wait() - 父进程注册
SIGCHLD处理函数
孤儿进程(Orphan Process)
定义:父进程先于子进程退出,子进程成为孤儿,被 init 进程(PID=1,或 systemd)收养。
特征:正常运行,不会产生危害,init 会负责回收。
1 | |
Java 视角
1 | |
实际排查:ps aux | grep Z | grep -v grep,若僵尸进程持续增多,通常是 Java 应用频繁 Runtime.exec() 但未调用 waitFor()。
Q7. fork() 的 Copy-On-Write(COW)机制
没有 COW 时的问题
fork() 创建子进程需要复制父进程所有内存。一个 4GB 堆的 JVM 进程 fork 一次需要复制 4GB 内存,耗时数秒,完全不可接受。
COW 的原理
fork() 后,父子进程共享相同的物理内存页,页表项都标记为只读。
当任意一方尝试写入某个页面时:
- CPU 触发写保护异常(Page Fault,写保护类型)
- 内核处理函数
do_wp_page()被调用 - 内核分配一个新的物理页,复制原页内容
- 将触发写操作的进程的页表项指向新页,标记可写
- 恢复执行写操作
1 | |
fork() + exec() 的优化
exec() 会替换进程的整个地址空间,所以 fork() + 立即 exec() 根本不会触发大量 COW,只复制极少量页(如栈、页表本身)。
Linux 还有 vfork():子进程完全共享父进程地址空间,父进程挂起直到子进程 exec() 或 exit(),专为 fork-exec 场景优化。
Java 视角 与 Redis 的 COW
Redis 的 BGSAVE 命令就是 fork() 子进程进行持久化,利用 COW 保证数据一致性。
Java 需要注意:JVM 进程 fork 后,父进程继续写入堆(GC 活动),会大量触发 COW,导致内存使用量翻倍。这就是为什么建议在 JVM 进程中尽量不 fork,而是用线程或进程间通信。
1 | |
Q8. 虚拟内存的作用与实现(页表、TLB、多级页表)
虚拟内存解决了什么问题
- 地址空间隔离:每个进程以为自己独占所有内存,互不干扰
- 内存超量使用:物理内存不足时,部分数据放到磁盘(swap)
- 内存共享:多个进程可以映射同一物理页(共享库 libc 只需一份)
- 内存保护:进程无法访问其他进程的内存
页表(Page Table)
地址翻译过程:
1 | |
x86-64 使用4级页表(Linux 5.x 支持5级):
1 | |
TLB(Translation Lookaside Buffer)
页表存在内存中,每次地址翻译都要走4次内存访问,太慢!TLB 是页表的硬件缓存(通常64~1024个条目),命中时直接给出物理地址。
TLB 失效(TLB miss):
- 访问新地址 → 硬件 Page Table Walk
- 进程切换 → 整个 TLB 失效(除非支持 ASID 标记)
上下文切换慢的主要原因之一就是 TLB 失效。
多级页表的意义
32位进程:虚拟地址空间 4GB,若 4KB 一页,需要 100万个页表项,每项 4B = 4MB 页表(每个进程都要!)。
4级页表:只需为实际用到的虚拟地址范围分配页表,节省大量内存。
1 | |
Q9. 缺页中断(Page Fault)的处理流程
什么是缺页中断
CPU 访问虚拟地址时,页表项中对应的物理页不在内存中(页表项 Present 位为0),触发 Page Fault 异常(中断号14)。
三种 Page Fault 类型
| 类型 | 原因 | 处理 |
|---|---|---|
| Minor Fault | 页在内存,但页表未建立(如 mmap 后首次访问) | 建立映射,无需IO |
| Major Fault | 页在 Swap 或文件,需要磁盘IO | 从磁盘读入,慢! |
| Invalid Fault | 非法访问(如空指针) | 发送 SIGSEGV 信号 |
完整处理流程
1 | |
Java 视角
1 | |
Java 启动慢的原因之一:JVM 加载大量类文件,触发大量 Minor Page Fault(mmap 映射 jar 包后,首次访问时才加载)。
JVM 的 -XX:+AlwaysPreTouch:JVM 启动时主动触碰所有堆内存,提前触发 Page Fault,避免运行时首次 GC 时卡顿。
Q10. 内存分配:malloc/free 的底层实现(brk vs mmap 阈值)
用户空间内存布局
1 | |
malloc 的两种分配策略
策略1:brk()——移动堆顶指针(小内存)
1 | |
策略2:mmap()——直接映射匿名页(大内存)
1 | |
glibc malloc 的内存池(bins)
glibc 使用 ptmalloc2 算法,维护多个大小的 free list(bins):
- fast bins:16~64字节,单链表,不合并相邻块
- small bins:< 512字节,双向链表
- large bins:≥ 512字节,按大小排序
- unsorted bin:刚释放的块先放这里
Java 视角
JVM 的内存分配走的是 JVM 自己的分配器(不直接调用 glibc malloc),但 JVM native 代码(JNI)分配的内存走 glibc。
1 | |
Q11. 内存泄漏的检测方法
内存泄漏的本质
已分配的内存,程序不再使用但无法释放(C:忘记 free;Java:对象有强引用但业务已不需要)。
Linux 层面检测工具
1. valgrind(最全面)
1 | |
输出示例:
1 | |
2. /proc/PID/smaps 监控
1 | |
3. /proc/PID/status 关键字段
1 | |
4. pmap 命令
1 | |
Java 内存泄漏排查
1 | |
Q12. 进程调度算法
FCFS(先来先服务)
按到达顺序调度,简单但护航效应严重(长进程阻塞后面的短进程)。非抢占式。
SJF(短作业优先)
选择预估执行时间最短的进程,平均等待时间最优,但需要预知执行时间(实际难以做到)。
SRTF(最短剩余时间)= 抢占式 SJF
轮转调度(Round Robin)
每个进程分配固定时间片(通常 10~100ms),时间片到则抢占,放到队尾。
- 时间片太大 → 退化为 FCFS
- 时间片太小 → 上下文切换开销过大
优先级调度
高优先级进程优先运行,可抢占低优先级进程。问题:低优先级进程饥饿(Starvation)。
解决:老化(Aging)——随时间推移提升等待进程的优先级。
CFS(完全公平调度)— Linux 默认
核心思想:让每个进程都获得公平的 CPU 时间,以虚拟运行时间(vruntime)衡量公平性。
1 | |
- **优先级(nice 值)**影响权重,而非直接影响调度顺序
- 使用红黑树存储所有可运行进程,
vruntime最小的进程最先运行(最左节点) - 时间复杂度 O(log n)
1 | |
Java 视角
Java 线程优先级(1-10)会被映射到 Linux nice 值,但映射关系因 JVM 实现而异,实际效果有限。更推荐通过业务架构(线程池隔离、任务队列)来控制优先级。
Q13. 上下文切换的开销
什么是上下文
进程/线程的”运行现场”:CPU 寄存器(通用寄存器、PC、SP、状态寄存器)+ 内核栈 + 内存映射信息。
上下文切换的步骤和开销
1. 保存当前线程上下文
1 | |
2. 切换内存空间(进程间切换才有)
1 | |
3. 恢复新线程上下文
1 | |
Cache 失效的间接开销
上下文切换后,新线程访问的数据不在 CPU Cache 中(L1/L2/L3 Cache Miss),触发大量内存访问,这个间接开销比直接开销大得多。
估算:
- 直接开销:
110 微秒(寄存器保存恢复) - 间接 Cache 冷启动:可高达 100+ 微秒
减少上下文切换的方法
1 | |
Java 视角
线程池大小 = CPU 核数(CPU 密集型)或更多(IO 密集型)。线程过多 → 大量上下文切换 → 性能下降。
Java 21 虚拟线程的优势:虚拟线程切换不需要系统调用,开销仅在 JVM 层面(纳秒级),大幅减少内核上下文切换。
Q14. 死锁的4个必要条件及 Banker 算法
四个必要条件(Coffman 条件)
- 互斥(Mutual Exclusion):资源一次只能被一个进程使用
- 持有并等待(Hold and Wait):进程持有资源的同时,等待其他进程持有的资源
- 不可剥夺(No Preemption):已分配的资源不能被强制剥夺
- 循环等待(Circular Wait):存在进程等待链形成环路
只要破坏其中一个,死锁就不会发生。
实际避免策略
| 策略 | 破坏条件 | 示例 |
|---|---|---|
| 资源一次性申请 | 持有并等待 | 事务开始前锁定所有资源 |
| 有序加锁 | 循环等待 | 约定锁的获取顺序 |
| 超时机制 | 不可剥夺 | tryLock(timeout) |
| 乐观锁/无锁 | 互斥 | CAS 操作 |
Banker 算法(银行家算法)
动态避免死锁,判断资源分配后系统是否仍处于安全状态(存在安全序列)。
1 | |
简化版算法:
1 | |
Java 死锁检测
1 | |
有序加锁示例(破坏循环等待):
1 | |
Q15. 文件系统:inode 是什么,软链接 vs 硬链接
inode 的本质
inode(Index Node)是文件系统中存储文件元数据的数据结构,每个文件/目录有唯一一个 inode。
inode 中存储什么:
- 文件类型(普通文件、目录、符号链接…)
- 权限(rwxrwxrwx)
- 硬链接计数
- 所有者 UID、GID
- 文件大小
- 时间戳(atime/mtime/ctime)
- 数据块指针(直接块 + 间接块)
inode 中不存储:文件名(文件名存在目录项中!)
1 | |
目录的本质
目录是一个特殊文件,内容是文件名 → inode 号的映射表:
1 | |
硬链接(Hard Link)
创建另一个目录项指向同一个 inode,inode 的链接计数 +1。
1 | |
特点:
- 删除原文件不影响硬链接(inode 引用计数 > 0)
- 不能跨文件系统(inode 号只在同一文件系统内唯一)
- 不能指向目录(防止循环)
软链接(Symbolic Link / Symlink)
创建一个新文件(新 inode),内容是目标路径字符串。
1 | |
特点:
- 可以跨文件系统
- 可以指向目录
- 原文件删除后,软链接变为悬空链接(dangling link)
- 访问软链接需要额外一次路径解析
对比总结
| 特性 | 硬链接 | 软链接 |
|---|---|---|
| inode | 相同 | 不同(新建) |
| 跨文件系统 | 不支持 | 支持 |
| 目标目录 | 不支持 | 支持 |
| 原文件删除 | 文件仍可访问 | 链接失效 |
| 本质 | 目录项引用 | 路径字符串文件 |
Q16. VFS(虚拟文件系统)的作用
问题背景
Linux 支持数十种文件系统:ext4、xfs、btrfs、tmpfs、procfs、nfs……用户程序难道要为每种文件系统写不同的代码?
VFS 的解决方案
VFS 是 Linux 内核中的文件系统抽象层,为上层提供统一的接口,屏蔽不同文件系统的实现差异。
1 | |
VFS 四大核心对象
| 对象 | 作用 |
|---|---|
super_block |
描述已挂载文件系统的整体信息 |
inode |
描述文件元数据(对应具体FS的inode) |
dentry |
目录项缓存,路径名 → inode 的映射 |
file |
进程打开文件的上下文(当前偏移量等) |
dentry 缓存(dcache)
路径查找是高频操作,VFS 用 dcache 缓存路径解析结果,避免每次都从磁盘读取目录项。
1 | |
Java 视角
Java 的 File、Path、FileChannel 等 API 最终都通过 JNI 调用 Linux 的 open()、read() 等系统调用,走 VFS 层。正因为有 VFS,Java 代码不需要关心底层是 ext4 还是 xfs。
/proc 和 /sys 也是 VFS 的文件系统,所以 Java 可以直接读取 /proc/self/status 获取 JVM 进程信息。
Q17. 磁盘调度算法
背景
传统机械硬盘(HDD)磁盘寻道时间(5~15ms)是主要瓶颈,调度算法目标是减少总寻道时间。(SSD 无寻道时间,这些算法意义较小,但 Linux 仍提供调度层抽象。)
FCFS(先来先服务)
按 IO 请求到达顺序处理,简单公平,但可能导致磁头来回大幅移动。
SSTF(最短寻道时间优先)
每次选择距当前磁头最近的请求,减少总寻道距离,但可能导致远端请求饥饿。
SCAN(电梯算法)
磁头像电梯一样,向一个方向移动,处理路径上的所有请求,到达端点后反向。
C-SCAN(循环 SCAN):只向一个方向扫描,到达端点后立即返回起点,再次向同一方向扫描。响应时间更均匀。
实际使用
1 | |
查看磁盘 IO 性能
1 | |
Q18. IO 的5种模型
以 recvfrom 系统调用为例,等待 UDP 数据包到来:
1. 阻塞 IO(Blocking IO)
1 | |
简单,但线程被阻塞,无法处理其他事情。
2. 非阻塞 IO(Non-blocking IO)
1 | |
不阻塞,但轮询浪费 CPU。
3. IO 多路复用(IO Multiplexing)
1 | |
单线程管理多个连接,是 Nginx、Redis 的基础。
4. 信号驱动 IO(Signal-driven IO)
1 | |
实际很少用(信号处理复杂,存在竞态)。
5. 异步 IO(Asynchronous IO)
1 | |
真正的异步,连数据拷贝也是内核做的。Linux 的 io_uring(5.1+)是目前最高效的实现。
区分同步异步、阻塞非阻塞
- 阻塞/非阻塞:描述调用者是否等待(发起调用到得到结果这段时间)
- 同步/异步:描述 IO 操作是否由调用者自己完成
- 同步:调用者自己调用 recvfrom 拷贝数据
- 异步:内核完成数据拷贝,通知调用者
前4种模型都是同步 IO(都需要调用者调用 recvfrom 等拷贝数据),只有第5种是异步 IO。
Q19. select / poll / epoll 的区别与触发模式
select
1 | |
缺点:
- fd 数量上限 1024(
FD_SETSIZE) - 每次调用都要把 fd 集合从用户态拷贝到内核态(O(n))
- 内核需要遍历所有 fd 检查就绪状态(O(n))
- 返回后用户还要遍历所有 fd 找到就绪的(O(n))
poll
1 | |
相对 select 的改进:去掉了 1024 的限制(用链表)。
仍然有:O(n) 拷贝、O(n) 遍历的问题。
epoll
1 | |
核心优势:
- 内核用红黑树存储所有监听 fd(O(log n) 增删)
- 内核用就绪链表存储就绪 fd
epoll_wait只返回就绪 fd,O(1) 处理- fd 信息存在内核,无需每次拷贝
水平触发(LT)vs 边缘触发(ET)
水平触发(Level Trigger,默认):
- 只要缓冲区有数据,每次
epoll_wait都会通知 - 没读完数据也没关系,下次还会通知
- 更安全,不容易丢事件
边缘触发(Edge Trigger):
- 只在状态变化时(从无数据→有数据)通知一次
- 必须一次性把数据读完(用非阻塞 IO + 循环读到 EAGAIN)
- 更高效,减少系统调用次数
1 | |
Java NIO / Netty 使用 epoll ET 模式(Linux 上),性能更高。
Q20. epoll 的内部实现(红黑树 + 就绪链表)
整体架构
1 | |
epoll_ctl 添加 fd 的过程
1 | |
数据就绪时的回调机制
1 | |
epoll_wait 的执行
1 | |
关键性能数据
| 操作 | select/poll | epoll |
|---|---|---|
| 注册 fd | O(1) | O(log n) |
| 检查就绪 | O(n) | O(1) |
| 拷贝 fd 集合 | O(n) | O(就绪数) |
| 最大 fd 数 | 1024 / 无限制 | 无限制(内存限制) |
Q21. mmap 的原理及与 read/write 的对比
传统 read/write 的数据路径
1 | |
每次 IO 涉及2次CPU拷贝 + 2次系统调用。
mmap 的原理
mmap() 将文件的某个区域直接映射到进程的虚拟地址空间,文件内容成为内存中的字节数组。
1 | |
数据路径:
1 | |
对比总结
| 方式 | 拷贝次数 | 系统调用 | 适用场景 |
|---|---|---|---|
| read/write | 2次 | 2次/IO | 小文件、流式处理 |
| mmap | 1次(Page Fault时) | 1次(mmap本身) | 大文件随机访问 |
Java 中的 mmap
1 | |
Kafka 为什么快:Kafka 的消费者读取消息用 sendfile(零拷贝),索引文件用 mmap(快速随机查找),是经典的高性能 IO 设计。
Q22. 零拷贝(Zero-Copy):sendfile vs mmap,Java NIO 的 transferTo()
传统文件传输的4次拷贝
1 | |
sendfile(Linux 2.1+)
1 | |
数据路径:
1 | |
sendfile + DMA Gather(Linux 2.4+,真正零拷贝)
支持 scatter-gather DMA 的网卡:
1 | |
Java NIO 的 transferTo()
1 | |
Netty 的零拷贝还包括:
CompositeByteBuf:逻辑合并多个 ByteBuf,不做内存拷贝slice():切片,共享原 Buffer 内存
mmap vs sendfile
| 特性 | mmap | sendfile |
|---|---|---|
| 适用 | 需要在用户态处理内容 | 纯文件→网络传输 |
| 修改内容 | 支持 | 不支持 |
| CPU 拷贝 | 1次 | 0次(scatter-gather) |
| 使用场景 | Kafka索引、数据库 | Nginx静态文件 |
Q23. Linux 信号机制:SIGKILL vs SIGTERM vs SIGINT,Java 的 ShutdownHook
常用信号
| 信号 | 编号 | 默认行为 | 可捕获/忽略 |
|---|---|---|---|
| SIGTERM | 15 | 终止进程 | 可捕获 |
| SIGKILL | 9 | 强制杀死 | 不可捕获 |
| SIGINT | 2 | 终止(Ctrl+C) | 可捕获 |
| SIGQUIT | 3 | 终止+Core Dump | 可捕获 |
| SIGHUP | 1 | 终止(终端挂断) | 可捕获 |
| SIGSEGV | 11 | Core Dump | 可捕获(但建议不处理) |
| SIGUSR1/2 | 10/12 | 用户自定义 | 可捕获 |
关键区别
- SIGTERM(15):礼貌地请求程序退出,程序可以捕获并做清理工作(关闭连接、flush 缓冲区)
- SIGKILL(9):内核直接杀死进程,程序没有机会做任何清理,慎用
- SIGINT(2):Ctrl+C,类似 SIGTERM,程序通常会捕获并退出
最佳实践:先发 SIGTERM,等几秒,再发 SIGKILL(Docker stop 的默认行为就是这样)
1 | |
Java 的 ShutdownHook
JVM 在收到 SIGTERM 时会触发 ShutdownHook:
1 | |
注意:
SIGKILL和 JVM 崩溃不会触发 ShutdownHook- Spring Boot 的
server.shutdown=graceful在 ShutdownHook 中等待请求完成 - ShutdownHook 里的代码要控制超时,避免关闭卡住
1 | |
Q24. /proc 文件系统的常用路径
/proc 的本质
/proc 是一个内存文件系统,不占用磁盘空间,内容由内核动态生成,用于暴露内核和进程信息。
进程排查常用路径
1 | |
Java 进程排查实用命令
1 | |
Q25. top/htop 命令关键指标解读
top 输出解析
1 | |
Load Average 解读
load average: 1.23, 1.45, 1.67 — 1分钟、5分钟、15分钟的平均负载
负载 = 运行中 + 等待CPU + D状态(不可中断等待IO)的进程数
- 单核 CPU:负载 = 1.0 表示满负载
- 4核 CPU:负载 = 4.0 表示满负载
- 负载持续 > CPU核数就要关注了
1 | |
CPU 时间百分比
| 指标 | 含义 |
|---|---|
us |
用户态 CPU(应用代码) |
sy |
内核态 CPU(系统调用) |
ni |
nice 调整后的用户态进程 |
id |
空闲 |
wa |
IO 等待(高 wa → 磁盘/网络瓶颈) |
hi |
硬件中断 |
si |
软件中断(高 si → 网络包处理多) |
st |
被虚拟机 Steal 的时间(云服务器 CPU 抢占) |
进程内存字段
| 字段 | 含义 |
|---|---|
VIRT |
虚拟内存总量(包括未使用的堆、mmap区域) |
RES |
实际物理内存(RSS,真正占用的RAM) |
SHR |
共享内存(共享库等,多进程共用) |
%MEM |
RES / 总物理内存 |
Java 进程的 VIRT 通常很大(几个GB),这是正常的,JVM 预先保留虚拟地址空间。看 RES 才是实际物理内存使用。
1 | |
Q26. 系统调用的执行流程
用户态 → 内核态切换过程
1 | |
syscall 指令的作用
syscall(x86-64)是专用指令,执行时:
- CPU 特权级从 Ring 3 → Ring 0
- 跳转到 MSR(Model Specific Register)中预设的内核入口点
- 切换到内核栈
- 开始执行内核代码
上下文切换 vs 系统调用
| 操作 | 说明 | 开销 |
|---|---|---|
| 系统调用 | 同一线程,切换特权级 | ~100ns |
| 上下文切换 | 切换到不同线程/进程 |
减少系统调用的技术
批量 IO:writev() / readv() 一次系统调用完成多次 IO
vDSO(Virtual Dynamic Shared Object):
1 | |
glibc 的 gettimeofday() 在 vDSO 中实现,不需要真正的系统调用(内核映射到用户空间的只读页)。
io_uring(Linux 5.1+):
用户态和内核态共享环形队列,批量提交 IO,大幅减少系统调用次数。
1 | |
Q27. NUMA 架构对 Java 应用的影响
什么是 NUMA
NUMA(Non-Uniform Memory Access,非一致内存访问)是多 CPU 插槽服务器的内存架构:
1 | |
访问本地内存(Local Memory)延迟约 80ns,访问远端内存(Remote Memory,跨 NUMA 节点)延迟约 160ns,慢约 2 倍。
NUMA 对 Java 的影响
JVM 分配对象时,如果堆内存跨 NUMA 节点,GC 和对象访问时就会产生大量跨节点内存访问,降低性能。
诊断 NUMA 问题
1 | |
优化方案
1. JVM 启用 NUMA 感知
1 | |
2. 绑定进程到单一 NUMA 节点
1 | |
3. interleave(适合无状态服务)
1 | |
最佳实践:大内存(>= 64GB)多 CPU 插槽的服务器,NUMA 优化可带来 10~30% 的性能提升。
Q28. Linux OOM Killer
触发条件
当系统物理内存 + Swap 都耗尽,且无法回收足够内存时,内核的 OOM Killer(Out-Of-Memory Killer)介入,强制杀死一个进程以释放内存。
oom_score 算法
内核为每个进程计算 oom_score(0-1000),分数越高越容易被杀。
1 | |
oom_score 计算因素:
- 进程占用内存越多 → 分数越高
- 运行时间越短 → 分数越高
- 子进程越多 → 分数越高
- root 进程适当降分
保护 Java 进程
1 | |
OOM 发生后的排查
1 | |
预防措施
1 | |
Q29. ulimit 限制对 Java 服务的影响
ulimit 是什么
ulimit 是 Linux 对每个进程资源使用的上限控制,分为软限制(soft,可以提高到硬限制)和硬限制(hard,只有 root 能提高)。
1 | |
对 Java 服务的关键影响
1. open files(-n,默认 1024)
1 | |
2. max user processes(-u,默认 1024 或 4096)
1 | |
3. stack size(-s,默认 8192KB = 8MB)
1 | |
4. 虚拟内存(-v)
通常不建议限制(JVM 大量使用虚拟地址空间,-v 过小导致 JVM 无法启动)
systemd 服务配置
1 | |
Q30. 实战:Java 服务器 CPU 飙高完整排查流程
场景
生产环境 Java 服务 CPU 使用率突然飙到 90%+,如何快速定位根因?
第一步:top 看全局
1 | |
第二步:ps 找到耗 CPU 的线程
1 | |
第三步:转换线程 ID(LWP → 十六进制)
1 | |
第四步:jstack 获取线程快照
1 | |
典型输出:
1 | |
第五步:根据 jstack 内容分析
1 | |
完整排查脚本
1 | |
常见 CPU 飙高原因汇总
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| GC 线程 CPU 高 | 内存不足/内存泄漏 | 增大堆/优化对象创建 |
| 业务线程 Runnable 且栈不变 | 死循环 | 找代码里的无限循环 |
| 大量线程 BLOCKED | 锁竞争 | 减锁范围/换并发结构 |
| sy CPU 高 | 频繁系统调用 | strace 找高频 syscall |
| si CPU 高 | 网络包处理 | 检查网络流量/连接数 |
| 负载高但 us 不高 | D 状态进程多 | 检查磁盘/NFS IO |
总结:Linux & OS 面试八股文的核心在于理解底层机制,而不是死记硬背。建议在自己的 Linux 服务器上亲手运行本文中的每一条命令,理解每个输出代表的含义。Java 后端开发者懂 OS 原理,才能在遇到线上问题时快速定位根因,而不是靠撞运气。祝面试顺利!
本文首发于个人技术博客,欢迎转载,转载请注明出处。