Linux 进程、线程与调度面试题
18 道题- 分类
- Linux
- 题目数
- 18 道
1 fork 与 exec 的区别是什么?两者组合使用时的进程行为是怎样的?
答案:
fork() 基于父进程复制(Copy-on-Write)创建子进程,子进程获得与父进程相同的地址空间、文件描述符和信号处理;返回两次,父进程返回子进程 PID,子进程返回 0。exec() 族函数(execl、execv、execve 等)将当前进程的地址空间替换为新程序镜像,PID 保持不变。
组合使用的典型模式:
- Shell 执行命令:父 Shell 调用
fork()产生子进程,子进程调用execve()加载新程序,父进程调用waitpid()等待子进程结束。 - 守护进程(daemon):通过
fork()+setsid()+ 二次fork()实现脱离终端、会话独立。 - Copy-on-Write 机制:
fork()后父子进程共享物理页,只有任一方写入时才触发缺页中断复制页框,显著降低创建开销。
进程状态转换:
R (Running) → S (Sleeping) → D (Uninterruptible) → Z (Zombie) → X (Dead)
2 Linux CFS 调度器的核心原理是什么?vruntime 与红黑树如何配合?
答案:
CFS(Completely Fair Scheduler,2.6.23 起替代 O(1) 调度器)以"无限小粒度、多任务公平"为目标,基于虚拟运行时间 vruntime 与红黑树实现 O(log n) 的任务选择。
核心机制:
- vruntime 累计:
vruntime += delta_exec × NICE_0_LOAD / weight(其中NICE_0_LOAD = 1024为常量,weight 来自sched_prio_to_weight[]表,nice 0 进程 weight=1024 故 vruntime 累加 = delta_exec),权重由进程 nice 值决定。 - 红黑树以 vruntime 为键:每次调度选择最左节点(最小 vruntime)运行,保证最"饥饿"的进程优先获得 CPU。
- 调度延迟(sched_latency_ns):默认 6ms 内所有可运行任务都应至少运行一次;超过 nr_running 8 时切换为
sysctl_sched_min_granularity_ns(默认 1ms)控制单次运行时间。 - CFS 调度类:
sched_normal、sched_batch、sched_idle同属 CFS;实时任务(SCHED_FIFO、SCHED_RR)由rt_sched_class单独管理,优先级高于 CFS。 - 多核负载均衡:周期性
load_balance()、新唤醒任务wake_affine()亲和性判断、idle load balancing。
# 实时观测调度延迟
cat /proc/sys/kernel/sched_latency_ns
cat /proc/sys/kernel/sched_min_granularity_ns
3 协程(Coroutine)与线程的本质区别是什么?Linux 下常见的协程实现方式有哪些?
答案:
线程是 OS 调度的抢占式执行单元,上下文切换需陷入内核、保存/恢复寄存器与页表,开销约 1-10μs。协程是协作式用户态轻量线程,由程序自身控制切换点,单线程内可承载数十万协程,单次切换约 100-200ns。
对比维度:
| 特性 | 线程 | 协程 |
|---|---|---|
| 调度主体 | 内核 | 用户态运行时(runtime) |
| 切换开销 | 1-10μs | 100-200ns |
| 抢占支持 | 强(时间片) | 弱(依赖 yield) |
| 并行能力 | 真正并行 | 单线程内并发,多线程模型下并行 |
| 阻塞代价 | 阻塞 OS 调度 | 必须确保 IO 非阻塞 |
Linux 协程实现:
- ucontext / getcontext / makecontext / swapcontext:POSIX 异步上下文接口,glibc 提供。
- Boost.Coroutine / Boost.Asio stackless coroutine:基于 C++20
co_await/co_yield标准化。 - Goroutine(Go):M:N 调度模型,G-M-P 三级队列。
- io_uring + 协程:异步 IO 与协程配合,单线程处理数万并发连接。
4 Linux 进程有哪些状态?`/proc/<pid>/stat` 字段如何解读?
答案:
Linux 进程状态在 <sys/wait.h> 与内核 task_struct->state 中定义,常用状态:
| 状态 | 字符 | 内核宏 | 含义 |
|---|---|---|---|
| Running | R | TASK_RUNNING | 就绪或正在运行(在 runqueue 中) |
| Sleeping | S | TASK_INTERRUPTIBLE | 可中断睡眠,等待事件 |
| Disk Sleep | D | TASK_UNINTERRUPTIBLE | 不可中断睡眠(IO 阻塞,ps 显示 D) |
| Stopped | T | TASK_STOPPED | 收到 SIGSTOP/SIGTSTP |
| Tracing | t | TASK_TRACED | 被 ptrace 跟踪 |
| Zombie | Z | EXIT_ZOMBIE | 终止未 wait,PID 仍占 |
| Dead | X | EXIT_DEAD | 临时态,wait 回收中 |
| Idle | I | TASK_IDLE | 内核 idle 线程 |
/proc/<pid>/stat 关键字段(空格分隔,第 3 列为状态字符):
1 (cat) R 1234 1234 ... 0 0 0 0 ...
├ PID ├├ 名称(括号) ├状态
完整 52 个字段中关键指标:
- minflt / majflt(10/12):次缺页/主缺页次数
- utime / stime(14/15):用户态/内核态 CPU 时间(jiffies)
- starttime(22):进程启动时间(jiffies,自 boot)
- vsize / rss(23/24):虚拟内存 / 常驻物理页数
实战工具:
# 批量看进程状态
ps -eo pid,ppid,stat,comm,%cpu,%mem
# 看进程 CPU 占用(time + %cpu)
top -b -n 1 -p <pid>
# 长期采样到文件
pidstat -u -r 1 -p <pid> 5
# 看进程启动命令行
cat /proc/<pid>/cmdline | tr '\0' ' '
典型场景:
- 进程一直
D态 → 多为 IO 阻塞(慢盘、nfs 卡死、设备 hung)。D态不响应任何信号,只能重启或等待 IO 恢复。 - 大量
Z态 → 父进程未wait(),需修父进程或临时用wait4工具清理。
5 nice / renice 与 PRI 是什么关系?进程优先级如何调整?
答案:
Linux 进程优先级有两层抽象:用户态 nice 值(-20 ~ +19,root 可负)和内核态优先级(priority,0-139,real-time 0-99 + normal 100-139)。
映射关系:
- 普通进程(CFS):
PRI = 100 + nice + 20(默认 nice=0 → PRI=120;nice=-20 → PRI=100;nice=+19 → PRI=139) - 实时进程:
sched_priority1-99 由sched_setattr()直接设置(数字越小优先级越高),ps/top 工具按PRI = 1 + (-rt_priority)公式显示
nice 调整:
# 启动时设置 nice
nice -n -20 /usr/bin/important-job # 最高优先级(需 root)
nice -n 19 /usr/bin/batch-job # 最低优先级
# 运行时调整(仅 root 可降 nice)
renice -n -5 -p 1234
# cgroup v2 资源加权(按权重比例)
echo 200 > /sys/fs/cgroup/system.slice/cpu.weight
CPU 亲和性(绑定到特定核心):
# taskset 设置亲和性
taskset -c 0,2,4 /usr/bin/nginx
taskset -p 0x3 1234 # 二进制掩码(CPU 0,1)
# 看进程 CPU 分布
taskset -cp <pid>
实时调度策略:
SCHED_FIFO:先入先出,无时间片,运行直到阻塞或被抢占SCHED_RR:时间片轮转的 FIFOSCHED_DEADLINE:基于 EDF(最早截止优先),用于周期性实时任务
chrt -f -p 50 1234 # 设置为 SCHED_FIFO,优先级 50
chrt -r -p 80 1234 # SCHED_RR,优先级 80
chrt -d --runtime 1000000 --deadline 10000000 --period 10000000 1234 # DEADLINE
生产实践:
- 数据库(MySQL/PostgreSQL)建议
nice=-20+ 绑核 + 关闭 NUMA balancing - 实时音视频用
SCHED_RR,避免 CFS 调度抖动 - cgroup v2
cpu.weight适合多租户场景(比例分配)
6 systemd 的 unit 类型与启动流程是什么?如何排查启动慢问题?
答案:
systemd 是现代 Linux 系统的init + 服务管理器,替代传统 SysVinit。所有受管对象都是 unit(单元),类型如下:
| Unit 类型 | 后缀 | 用途 |
|---|---|---|
| service | .service | 守护进程(最常见) |
| target | .target | 一组 unit 的集合(类 runlevel) |
| socket | .socket | socket 激活(按需启动) |
| timer | .timer | 替代 cron(支持 monotonic time) |
| mount / automount | .mount | 文件系统挂载 |
| path | .path | 监控文件变化触发动作 |
| swap | .swap | swap 设备 |
| slice / scope | .slice/.scope | cgroup 资源切片 |
启动流程(以 sysemd 为 PID 1):
BIOS/UEFI → GRUB → 内核 → initramfs → systemd (PID 1)
↓
default.target (graphical/multi-user)
↓
并行启动依赖 unit(解决循环依赖)
↓
getty.target → login.service
Unit 依赖类型:
Requires=foo.service:硬依赖,foo 失败则本 unit 也不启动Wants=foo.service:软依赖,foo 失败不影响本 unit(推荐)After=/Before=:启动顺序(不强制依赖)BindsTo=foo.service:双向绑定,foo 停止本 unit 也停
关键命令:
# 查看启动耗时(定位慢 unit)
systemd-analyze
systemd-analyze blame
systemd-analyze critical-chain
# 看 unit 状态
systemctl status nginx.service
systemctl list-units --type=service --state=running
# 启用/禁用
systemctl enable --now nginx # 启用自启 + 立即启动
systemctl disable nginx # 禁用自启
# 重载配置(不重启)
systemctl reload nginx
启动慢排查:
# 1. 找耗时最长 unit
systemd-analyze blame | head -10
# 2. 看关键路径
systemd-analyze critical-chain network.target
# 3. 关闭不需要的 unit
systemctl disable bluetooth.service
# 4. 重写 unit 加速(去掉不必要的 After=/Requires=)
systemctl edit nginx.service # 增量覆盖
# 5. 开机不等待 IO(systemd 默认有 30s 等待)
# /etc/fstab 加 nofail,x-systemd.device-timeout=5s
实战: 在云原生场景,K8s 节点上 systemd 还负责管理 kubelet/containerd 进程,systemctl status kubelet 是排障第一命令。
7 守护进程(daemon)的标准编写规范是什么?setsid 与 nohup 区别?
答案:
守护进程是脱离终端、会话独立、在后台长期运行的进程。编写规范(来自 daemon(7) man page):
标准编写步骤:
fork()+ 父进程exit():让 init 收养子进程,避免僵尸进程setsid():创建新会话、脱离控制终端- 二次
fork():确保进程不是会话首进程,无法再开 tty chdir("/"):避免占用挂载点导致 umount 失败umask(0):重设文件权限掩码- 关闭/重定向 fd 0/1/2 →
/dev/null或日志文件 - 处理 SIGTERM/SIGINT/SIGHUP:优雅退出
典型 C 代码骨架:
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
void daemonize() {
pid_t pid = fork();
if (pid < 0) exit(1);
if (pid > 0) exit(0); // 父进程退出
setsid(); // 新会话
pid = fork();
if (pid < 0) exit(1);
if (pid > 0) exit(0); // 二次 fork
chdir("/");
umask(0);
// 关闭 fd
close(0); close(1); close(2);
open("/dev/null", O_RDONLY);
open("/dev/null", O_WRONLY);
open("/dev/null", O_WRONLY);
}
nohup vs setsid vs disown 对比:
| 工具 | 作用 | 原理 |
|---|---|---|
| nohup | 忽略 SIGHUP(终端关闭信号) | 重定向 stdout/stderr 到 nohup.out |
| setsid | 创建新会话,脱离控制终端 | setsid(cmd) |
| disown | 从 shell 作业表移除(jobs 不再显示) | bash 内建(仅影响作业控制列表,HUP 发送由 shell 退出方式决定) |
| & + exit | 后台运行 + 父进程退出 | 由 init 收养 |
实战用法:
# 1. 经典 nohup(POSIX 通用)
nohup ./long-job > /var/log/job.log 2>&1 &
# 2. setsid(GNU coreutils)
setsid -f ./long-job > /var/log/job.log 2>&1 < /dev/null
# 3. screen / tmux(推荐,保留交互)
tmux new -d -s myjob './long-job | tee /var/log/job.log'
# 4. systemd-run(生产推荐,自动 cgroup 隔离)
systemd-run --unit=myjob --slice=batch.slice ./long-job
生产建议:
- 长期服务用 systemd unit 文件(自动重启、cgroup、journald 集成)
- 一次性后台任务用 systemd-run(结束后自动清理 cgroup)
- 老脚本用 nohup + & 兜底
8 SIGTERM / SIGKILL / SIGINT / SIGHUP 四个信号有什么区别?如何优雅退出?
答案:
信号(signal)是 Linux 进程间异步通知机制,每个信号有编号、默认行为、是否可捕获:
| 信号 | 编号 | 默认行为 | 可捕获 | 触发场景 |
|---|---|---|---|---|
| SIGTERM | 15 | 终止 | ✅ | kill/killall 默认;systemd stop |
| SIGKILL | 9 | 终止(不可阻止) | ❌ | kill -9 |
| SIGINT | 2 | 终止 | ✅ | Ctrl-C |
| SIGHUP | 1 | 终止 | ✅ | 终端关闭;守护进程常被捕获为 reload 配置 |
| SIGQUIT | 3 | core dump | ✅ | Ctrl-\ |
| SIGUSR1/2 | 10/12 | 终止(默认)/ 自定义 | ✅ | 自定义(如 Nginx 重开日志 / worker_processes 热重载) |
| SIGSTOP/SIGCONT | 19/18 | 暂停/继续 | ❌/✅ | Ctrl-Z / fg |
优雅退出(Graceful Shutdown)三阶段:
- 接收 SIGTERM → 进程捕获信号
- 停止接收新请求 → 摘除负载均衡(如 K8s readiness probe 失败)
- 等待现有请求完成 → 设置宽限期(默认 30s)
- 关闭资源(DB 连接、文件句柄)→ 退出
典型代码(Go):
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGTERM, syscall.SIGINT)
defer stop()
<-ctx.Done()
log.Println("收到 SIGTERM,开始优雅退出")
// 设置 30s 宽限期
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Println("强制退出:", err)
}
K8s 中的钩子:
- preStop hook:在容器收到 SIGTERM 前执行(如 sleep 5 等待 service endpoint 摘除)
- terminationGracePeriodSeconds:宽限期(默认 30s)
- 宽限期满 → kubelet 发送 SIGKILL
信号陷阱:
kill -9直接杀进程,未刷盘的 buffer 会丢(如 MySQL redo log)- 容器 init 系统(tini/dumb-init)作用:转发信号 + 收割僵尸进程。Docker
--init即添加 tini - 进程组(process group)信号:
kill -- -<pgid>发给整组
reload 配置模式:
# Nginx:SIGHUP 重载配置(不中断服务)
nginx -s reload
# 实际发送:kill -HUP <nginx-pid>
# Systemd:reload vs restart 区别
systemctl reload nginx # 不重启进程,仅重读配置
systemctl restart nginx # stop + start
9 cgroup v1 vs v2 区别是什么?为什么推荐 v2?
答案:
cgroup(Control Group)是 Linux 内核资源限制、统计、隔离机制,cgroup v1(2007 年起)与 v2(2013 年起,4.5 内核合并)并存多年,主流发行版从 RHEL 9 / Ubuntu 22.04 起默认 v2。
核心差异:
| 维度 | cgroup v1 | cgroup v2 |
|---|---|---|
| 控制器(subsystem) | 多层级、各自挂载(v1 时代 ~12 个分散) | 统一层级,14+ 个控制器(cpu memory io pids cpuset devices hugetlb freezer rdma misc …) |
| 文件系统 | 多 mount(/sys/fs/cgroup/cpu/、/sys/fs/cgroup/memory/) | 单一 mount(/sys/fs/cgroup/) |
| 进程归属 | 一个进程可在不同子系统层级 | 一棵层级树 |
| 资源控制粒度 | 各 controller 独立 | 跨 controller 统一(memory.high 触发回收时也受 cpu 节流) |
| cgroup v2 控制器数 | ~12 个分散 | 14+ 个统一(同一棵树,无 cpu/cpuacct 分离) |
| 容器友好度 | 弱(多层级难配置) | 强(容器直接挂载 v2 路径) |
cgroup v2 关键文件:
# /sys/fs/cgroup/<cgroup-path>/
cgroup.procs # 进程列表
cgroup.controllers # 启用的控制器
cgroup.subtree_control
cpu.max # "quota period"(如 "200000 100000" = 2 核)
cpu.weight # 100-10000,权重分配(默认 100)
memory.max # 硬限制(OOM kill)
memory.high # 软限制(触发回收)
memory.current # 当前使用
memory.events # low/high/max/oom 事件计数
io.max # "major:minor rbps=10485760 wbps=10485760"
io.stat # IO 统计
pids.max # 进程数限制
cgroup v2 实战:
# 1. 创建 cgroup
mkdir /sys/fs/cgroup/mycgroup
# 2. 启用控制器
echo "+cpu +memory +io +pids" > /sys/fs/cgroup/mycgroup/cgroup.subtree_control
# 3. 设置限制
echo "100000 100000" > /sys/fs/cgroup/mycgroup/cpu.max # 1 核
echo "536870912" > /sys/fs/cgroup/mycgroup/memory.max # 512MB
echo "10485760" > /sys/fs/cgroup/mycgroup/memory.high # 10MB 软限制
echo "8:0 rbps=104857600 wbps=104857600" > /sys/fs/cgroup/mycgroup/io.max # 100MB/s
# 4. 把进程加进去
echo $PID > /sys/fs/cgroup/mycgroup/cgroup.procs
# 5. systemd-run 简化版
systemd-run --scope --slice=mytest \
--property=CPUQuota=100% \
--property=MemoryMax=512M \
--property=IOReadBandwidthMax=/dev/sda 100M \
-- bash
K8s 与 cgroup v2:
- K8s 1.25+ 默认推荐 cgroup v2
kubelet --cgroup-driver=systemd是生产标准- Pod QoS 类映射到 cgroup v2 路径:
/sys/fs/cgroup/kubepods/burstable/pod<uid>/
为什么推荐 v2:
- 统一层级 → 容器引擎(containerd/CRI-O)配置简单
- 跨控制器一致性(如 memory.high 触发回收时正确节流 CPU)
- 新功能(PSI pressure stall information、eBPF 集成)仅 v2 支持
10 用户态(User Space)与内核态(Kernel Space)的区别是什么?系统调用如何完成一次状态切换?
答案:
CPU 通过特权级(Ring) 隔离用户态与内核态:x86_64 架构有 Ring 0-3 四级,Linux 仅使用 Ring 0(内核态)和 Ring 3(用户态)。用户态代码无法直接访问内核内存、硬件寄存器或执行特权指令,必须通过系统调用(syscall) 陷入内核态。
两态核心差异:
| 维度 | 用户态 | 内核态 |
|---|---|---|
| 特权级 | Ring 3 | Ring 0 |
| 可访问内存 | 仅自身进程虚拟地址空间 | 全部物理内存 + 内核地址空间 |
| 可执行指令 | 普通指令 | 含特权指令(关中断、MMIO、IO) |
| 上下文 | 进程独立栈 | 共享内核栈(per-CPU 或 per-thread) |
| 切换触发 | 系统调用/中断/异常 | 主动 sysret / iret 返回 |
| 崩溃影响 | 进程被 SIGSEGV 杀掉 | kernel panic,整机不可用 |
系统调用执行流程(以 read() 为例):
- 用户态 glibc
read()封装触发syscall指令,传入 syscall number(如__NR_read = 0)和参数。 - CPU 自动从 Ring 3 切换到 Ring 0,跳转到内核
entry_SYSCALL_64(5.17+ 改为entry_SYSCALL_64_handler)。 - 内核根据 syscall number 查
sys_call_table找到sys_read。 - 内核在 VFS 层找到 fd 对应的
file_operations,调用底层驱动read()。 - 数据从内核缓冲区拷贝到用户态缓冲区(注意:不是地址映射,是实际内存复制)。
- 内核执行
sysret/iret切回 Ring 3,glibc 包装层返回用户态。
性能开销:
- 单次 syscall 约 100-200ns(vDSO 优化路径 < 50ns,例如
clock_gettime) - 频繁 syscall 的程序应使用批处理 IO(
readv/writev、sendmsg/recvmmsg、io_uring) strace -c可统计进程 syscall 次数与耗时占比
典型场景:
- 高并发服务 syscall 占比 > 30% → 考虑用户态协议栈(DPDK、eBPF、XDP)绕过内核协议栈
- 文件 IO 频繁 → 用
mmap+writeback控制替代反复read/write,或换io_uring
11 进程与线程的本质区别是什么?内核如何表示和调度它们?
答案:
进程是资源分配的最小单位,线程是 CPU 调度的最小单位。Linux 用 task_struct 同时表示两者,线程被实现为"共享部分资源的 task_struct"。
核心资源归属:
| 资源 | 进程(process) | 线程(thread) |
|---|---|---|
task_struct | 独立 | 独立(但共享 mm_struct 等) |
| 进程地址空间(mm_struct) | 独立 | 共享(同 process 内所有 thread) |
| 文件描述符表(files_struct) | 独立 | 共享 |
| 信号处理(sighand_struct) | 独立 | 共享 |
| PID | 全局唯一 | 线程组共享 TGID,PID 是线程级 |
| 寄存器上下文 | 独立 | 独立(每线程独立 kernel stack) |
Linux 线程实现:
- 内核没有专门的 thread 结构,
thread= 与同进程其他 task 共享mm_struct/files_struct/sighand_struct的task_struct - 创建:
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD, ...),glibcpthread_create封装此调用 - 进程内首线程 PID = TGID,ps 中
LWP字段显示每个线程的轻量级进程号
进程 vs 线程对比:
| 维度 | 进程 | 线程 |
|---|---|---|
| 创建开销 | 1-10ms(含 COW 页表) | 10-100μs(共享页表) |
| 切换开销 | 1-10μs(切换页表 + TLB 刷新) | 1-5μs(同进程内不切页表) |
| 通信方式 | pipe/socket/shm/signal | 共享变量(注意同步) |
| 隔离性 | 强(一个崩溃不影响其他) | 弱(共享地址空间,一个线程 SEGV 整进程挂) |
| 适用 | CPU 密集 + 强隔离 | IO 密集 + 共享数据 |
实践建议:
- 默认用进程隔离(K8s pod 一个容器一个进程),故障域更小
- 必须用线程的场景:高并发 IO 共享大量只读状态(如 HTTP 框架 worker 池)、实时音视频 pipeline
- Go goroutine、Java ThreadPool 内部均映射为 OS 线程,但用户态有 M:N 调度层
12 什么是僵尸进程(Zombie)?如何产生、定位与清理?
答案:
僵尸进程是已终止但未被父进程 wait() 回收的进程,内核 task_struct 仍保留(PID、退出状态、统计信息),仅占用几 KB 内核内存。在 ps 中状态列为 Z,/proc/<pid> 仍可读。
产生原因:
- 父进程未调用
wait()/waitpid()/waitid()回收子进程退出信息 - 父进程先于子进程退出,子进程被 init(PID 1)收养但 init 还在等
- 父进程被 ptrace 跟踪或
fork后立即exec屏蔽了 SIGCHLD 处理
/proc/<pid>/stat 关键字段(定位 Z 态来源):
第 3 列 = Z # 进程状态
第 4 列 = PPID # 父进程 PID
第 17 列 = pgrp # 进程组
# 列出所有僵尸进程
ps -eo pid,ppid,stat,etime,comm | awk '$3 ~ /Z/'
# 找僵尸进程的父进程(真正未 wait 的元凶)
ps -eo pid,ppid,stat,comm | awk '$3 ~ /Z/ {print $2}' | sort -u
# 看父进程是否在 wait
cat /proc/<ppid>/status | grep -E "^(State|Threads)"
# State: S (sleeping) 长期 S + 大量 Z 子进程 = 未处理 SIGCHLD
清理方法:
# 1. 修父进程:注册 SIGCHLD handler,在 handler 内 waitpid(-1, NULL, WNOHANG) 循环
# 2. 临时方案:给父进程发 SIGCHLD(部分程序会触发回收)
kill -CHLD <ppid>
# 3. 终极方案:杀父进程,子进程被 init 接管并自动 wait
kill -9 <ppid>
# 4. 内核参数兜底(Linux 4.6+,需开启 CONFIG_CHECKPOINT_RESTORE)
echo 1 > /proc/sys/kernel/ns_last_pid # 已废弃
# 现代方案:systemd 管理的服务,cgroup 内 zombie 会被自动清理
典型场景:
- 一次性脚本
script.sh启动了 N 个后台 worker,未用wait→ 全部变 Z。修法:trap "wait" EXIT或显式wait $pid - 守护进程
fork()后子进程退出,父进程主循环没waitpid→ Z 累积。修法:注册 SIGCHLD handler
13 什么是孤儿进程(Orphan)?与僵尸进程有何区别?
答案:
孤儿进程是父进程已退出而自身仍在运行的进程,被内核自动过继给最近的有能力收养者(自身父进程链上仍活着的最近祖先,通常最终落到 PID 1 即 init/systemd)。
与僵尸进程的本质区别:
| 维度 | 孤儿进程 | 僵尸进程 |
|---|---|---|
| 是否在运行 | ✅ 仍在运行 | ❌ 已死亡,仅留 task_struct |
| 状态字符 | R/S/D 等 | Z |
| 资源占用 | 正常运行时占用 | 仅占几 KB 内核结构 |
| 处理方式 | 内核自动 reparent 到 init | 需父进程显式 wait() 回收 |
| 风险 | 多数情况无影响 | 长期累积会撑爆 PID pool |
产生与处理:
# 模拟孤儿进程
( sleep 60 ) & # 子 shell 启动 sleep
kill -9 $$ # 当前 shell 退出,sleep 被 init 接管
ps -o pid,ppid,comm -p $(pgrep sleep)
# PID PPID COMM
# 1234 1 sleep ← PPID 变为 1(init/systemd)
# 找系统中的孤儿进程
ps -eo pid,ppid,comm | awk '$2 == 1 && $1 != 1 {print}'
生产实践:
- 守护进程(daemon)标准做法是"二次 fork":第一次 fork 后父进程退出,子进程被 init 接管;第二次 fork 让孙子进程脱离会话,再
setsid()创建新会话。这样即使中间出错,也不会留下 PPID 异常的进程。 - K8s 中容器 PID 1 通常是应用进程本身,若应用未处理孤儿进程再收养,可能导致 PID 1 收到 SIGCHLD 而应用未处理(容器 SIGCHLD 转发行为由 runtime 决定)。建议容器内使用
tini/dumb-init作为 PID 1。
典型场景:
- 父进程是
make/xargs -P等并行调度工具,子进程执行慢,父进程先退,子进程成为孤儿,由 init 接管。这是预期行为,不是 bug。 - 守护进程未做二次 fork + setsid → 残留终端、stdin/stdout 连接到已退出的终端 → 行为异常。
14 什么是上下文切换(Context Switch)?它是如何发生的?
答案:
上下文切换是 CPU 从运行一个任务(进程/线程/中断)切换到另一个任务时,保存旧任务寄存器/页表等状态、加载新任务状态的过程。在 Linux 中分为进程/线程上下文切换与中断上下文切换。
进程/线程上下文切换流程:
- 触发:时间片耗尽(
scheduler_tick)、高优先级任务唤醒、自愿sched_yield()、阻塞 IO。 - 内核在旧 task 的
task_struct->thread(内核栈)保存寄存器上下文(switch_to宏)。 - 切换页表:
switch_mm()切换mm_struct(线程切换时同进程内不切),并触发 TLB flush 或 ASID 复用。 - 从新 task 的
thread恢复寄存器,跳回schedule()之后的执行点。
中断上下文切换:
- 硬件中断/软中断触发时,CPU 切换到中断上下文(独立栈,无
task_struct关联),不能被调度、不能睡眠。 - 中断返回时不一定切回原任务(
reschedule_softirq触发后才切),但有"中断-进程上下文切换"两层消耗。
关键指标:
# 系统全局上下文切换次数
vmstat 1
# cs 列 = context switch per second
# in 列 = interrupts per second
# 按进程统计
pidstat -w 1
# cswch/s : 自愿切换(阻塞 IO、sleep)
# nvcswch/s: 非自愿切换(被调度器抢占)
# 上下文切换源头
cat /proc/<pid>/status | grep -E "^(voluntary_ctxt_switches|nonvoluntary_ctxt_switches)"
性能影响:
- 一次上下文切换约 1-10μs(含 cache miss),主要由直接成本 + 间接 L1/L2 cache miss 构成
- 切换率 > 50k/s 的服务器通常意味着:线程池过大、锁竞争激烈、不当 IO 模型
- “自愿切换"高 = 大量阻塞(IO 等待);“非自愿切换"高 = CPU 争抢激烈
调优方向:
- 减少线程数(协程、IO 多路复用替代多线程)
- 减少锁粒度(无锁队列、RCU、读写锁替代互斥锁)
- CPU 亲和性
taskset减少跨核切换 - 中断合并 / NAPI / RSS 多队列降低中断切换
典型场景:
- 数据库服务器
cs/s> 100k → 锁竞争严重,先查perf top看热点 - Web 服务器
cs/s< 5k 但cs/in比值高 → 软中断频繁,调大net.core.netdev_max_backlog - “系统负载升高但 CPU 利用率低” → 99% 是上下文切换过载
15 系统调用(System Call)的完整流程是什么?为什么 vDSO 能加速部分系统调用?
答案:
系统调用是用户态请求内核态服务的唯一合法入口。Linux x86_64 通过 syscall 指令触发(32 位用 int 0x80),内核根据 rax 寄存器的 syscall number 派发到具体实现。
完整调用链(以 getpid() 为例):
- 用户态 glibc 包装函数
getpid()写入rax = __NR_getpid (39),执行syscall指令。 - CPU 硬件行为:保存
rcx(下一条指令地址)到rcx、保存r11(rflags)到r11、跳转到 MSRIA32_LSTAR指向的内核入口(entry_SYSCALL_64)。 - 内核入口用
swapgs切换 GS 寄存器(用户 GS ↔ 内核 GS),加载内核栈,从rax读 syscall number。 - 查
sys_call_table[39]找到sys_getpid(返回current->pid),执行并将返回值写入rax。 - 内核
sysretq恢复rip = rcx、rflags = r11、swapgs切回用户 GS,返回用户态。 - glibc 检查
rax(< 0 且 ≥ -4095 表示 errno),包装层返回。
vDSO(virtual Dynamic Shared Object):
部分系统调用无副作用或低开销,可由内核在用户态映射一段共享代码(vDSO),避免真实陷入内核:
# vDSO 映射位置(替代 libc 中的 syscall 包装)
cat /proc/self/maps | grep vdso
# 7fff12345000-7fff12346000 r-xp ... [vdso]
- 典型 vDSO 系统调用:
clock_gettime、gettimeofday、time、getcpu - 优势:< 50ns 完成(仅读 vDSO 数据或 TSC),避免 syscall 开销
ldd /bin/ls输出中可见linux-vdso.so.1 => (0x00007fff...)
与库函数的关系:
| 调用 | 是否进入内核 | 说明 |
|---|---|---|
getpid() vDSO | 否 | vDSO 直接读 task_struct 缓存 |
getpid() 普通 | 是 | syscall 走 sys_getpid |
malloc | 否 | glibc 用户态分配(ptmalloc2) |
open() | 是 | 必须内核(涉及 VFS) |
printf | 否 | glibc 缓冲,最后一次性 write() |
性能优化要点:
- 高频短小调用优先用 vDSO 路径(确认程序未禁用 vDSO:
AT_SYSINFO_EHDRauxv) - 批处理 syscall:
readv/writev、recvmmsg/sendmmsg、io_uring - eBPF /
bpf()syscall 一次性加载,多次触发无需再 syscall
典型场景:
- 性能分析发现
syscall占比 > 20% → 用strace -c -p <pid>看具体是哪些 syscall,再针对性优化 - 时间敏感代码禁用 vDSO(
setarch x86_64 -R /bin/bash)后性能骤降 → 说明时间获取是热点
16 CPU Load Average 与 CPU Usage 的区别是什么?如何正确解读?
答案:
CPU Usage 是瞬时利用率(1 个采样周期内 CPU 忙的比例),Load Average 是运行队列长度的可运行/不可中断任务的指数移动平均值,反映系统供需关系。
核心差异:
| 维度 | CPU Usage | Load Average |
|---|---|---|
| 含义 | CPU 时间被使用的比例 | 等待 CPU + 等待 IO 的任务数 |
| 范围 | 0-100%(单核),0-800%(8 核满载) | 0 到正无穷,无固定上限 |
| 采样 | 瞬时(1s、5s) | 1min / 5min / 15min 指数衰减均值 |
| 包含 D 态 | ❌ 不含 | ✅ 含 D 态(uninterruptible) |
| 评估 | 单核 80%+ 需关注 | 超过核数即过载 |
Load Average 计算(指数移动平均):
load_1 = load_1 * exp(-5/60) + nr_running * (1 - exp(-5/60))
load_5 = load_5 * exp(-5/300) + nr_running * (1 - exp(-5/300))
load_15 = load_15 * exp(-5/900) + nr_running * (1 - exp(-5/900))
1 分钟变化最快,15 分钟最平滑。三个值的关系反映趋势:
1min > 5min > 15min:负载上升中1min < 5min < 15min:负载下降中- 三者相近:稳态
- 数值大于 CPU 核数:有任务在等待
实战排查:
# 1. 看当前 load(注意要看核数,nproc)
uptime
# 14:30:01 up 10 days, load average: 12.34, 8.90, 6.78
# 2. 区分 R 态 vs D 态负载
cat /proc/loadavg
# 12.34 8.90 6.78 1/1234 56789
# ^^^^^^^^^ ^^^^^
# running/total 最近 PID
# 3. 用 pidstat 找贡献者
pidstat -u 1 5
# UID PID %usr %system %guest %CPU Command
# 1000 1234 95.0 3.0 0.0 98.0 stress
# 4. 用 top/htop 看各核分布
top # 按 1 显示各核利用率
htop # 彩色显示各核 R 态线程数
典型场景:
- Load 高 + CPU Usage 高:CPU 瓶颈(计算密集)
- Load 高 + CPU Usage 低:IO 瓶颈(D 态任务在等磁盘/网络)
- Load 高 + iowait 高:磁盘 IO 慢(机械盘、nfs 卡死、IO 调度器不匹配)
- Load 高 + 单核 100% + 其他核空闲:单线程应用,未利用多核
- 容器内
nproc看到的是宿主核数,需用cat /sys/fs/cgroup/cpu.max看实际配额
重要误区:
- “Load 1 < 核数就 OK” 是错的。如果业务对延迟敏感,Load > 0.7 × 核数就可能排队
- Load Average 不会因 CPU 频率调节(turbo/pstate)变化,是任务数而非 CPU 时间
17 Load Average 升高该如何系统化排查?
答案:
Load Average 升高的根因可归纳为三类:CPU 争抢、IO 阻塞、进程/线程数过多。排查要按"先看是 R 还是 D、再定位来源、最后看代码"的三段法推进。
排查工具链:
# Step 1: 区分 R/D 态来源
vmstat 1 5
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
# r b swpd free buff cache si so bi bo in cs us sy id wa st
# 8 2 0 102400 8192 524288 0 0 0 12 1234 5678 45 10 30 15 0
# ^ ^
# R D (8 个在运行队列,2 个 D 态阻塞)
# Step 2: 看是哪个进程贡献
pidstat -u -d 1 5 # CPU + IO 一起看
# 找出 %CPU 高 或 kB_rd/s + kB_wr/s 高的进程
# Step 3: 进程内部热点
top -H -p <pid> # 看线程级 CPU 分布
perf top -p <pid> # 看热点函数(on-CPU 火焰图)
perf record -g -p <pid> # 30s 后 perf report 看调用链
火焰图定位:
# on-CPU 火焰图(CPU 瓶颈)
perf record -F 99 -a -g -- sleep 30
perf script | stackcollapse-perf.pl | flamegraph.pl > cpu.svg
# off-CPU 火焰图(D 态/IO 瓶颈)
perf record -e sched:sched_switch -a -g -- sleep 30
# 或者用 bcc/bpftrace 工具
场景化排查清单:
| 现象 | 优先怀疑 | 验证手段 |
|---|---|---|
| Load 高 + us 高 | 应用层热点 | perf top、火焰图 |
| Load 高 + sy 高 | 内核/系统调用 | strace -c -p <pid> |
| Load 高 + wa 高 | 磁盘 IO | iostat -xz 1、iotop |
| Load 高 + st 高 | 虚拟化偷取 | 宿主机过载、绑核 |
| Load 高 + id 高 + 单核 100% | 单线程热点 | taskset 绑核 / 多线程改造 |
| Load 高 + D 态多 | IO hung | dmesg、cat /proc/<pid>/stack |
突发 vs 渐进:
- 突发(分钟级):临时任务(如 cron、备份、批处理)→ 用
atop看历史回放 - 渐进(小时/天级):内存泄漏、连接泄漏、缓存未释放 → 用监控长期趋势 +
pidstat -r
生产实战命令清单:
# 30 秒快速排查脚本
{
echo "=== uptime ==="; uptime
echo "=== nproc ==="; nproc
echo "=== top 进程 ==="; ps -eo pid,ppid,stat,pcpu,pmem,comm --sort=-pcpu | head
echo "=== vmstat ==="; vmstat 1 3
echo "=== D 态进程 ==="; ps -eo pid,ppid,stat,etime,comm | awk '$3 ~ /D/'
} | tee /tmp/load-investigate-$(date +%s).log
18 Java 进程占用 CPU/内存异常该如何排查?
答案:
Java 进程异常占用资源时,需穿透 JVM 抽象层定位到线程级、代码级根因。top 看到的 PID 是 JVM 进程,线程需通过 /proc/<pid>/task/<tid> 或 jstack 映射。
Step 1: 区分 CPU 高 / 内存高 / GC 频繁
# 看 Java 进程概览
jps -lvm # 列出所有 JVM 进程
jstat -gcutil <pid> 1s 5 # GC 统计,看 YGC/OGC 频率
jstat -gccause <pid> # 上次 GC 原因
jstat -class <pid> # 类加载/卸载统计(class leak)
# CPU 高 → 看热点线程
top -H -p <pid> # 找出 CPU 最高的 tid
# 把 tid 转 16 进制(jstack 用 16 进制)
printf '%x\n' <tid>
# 内存高 → 看堆/元空间/直接内存
jmap -heap <pid> # 堆配置 + 使用率
jcmd <pid> VM.native_memory summary # JDK 8u40+ 看 native/直接内存
jcmd <pid> GC.heap_dump /tmp/heap.hprof # 完整堆 dump(生产慎用,影响 STW)
Step 2: 定位热点线程
# jstack 取线程栈
jstack <pid> > /tmp/jstack.log
grep -A 30 "nid=0x<hex_tid>" /tmp/jstack.log
# 异步采样(async-profiler,低开销)
./profiler.sh -d 30 -e cpu -f /tmp/cpu-flame.html <pid>
./profiler.sh -d 30 -e alloc -f /tmp/alloc-flame.html <pid> # 分配热点
Step 3: 内存问题细分
| 现象 | 工具 | 根因定位 |
|---|---|---|
| 堆持续增长 | jmap -heap、jvisualvm | 内存泄漏(持有引用未释放) |
| Metaspace 满 | jcmd <pid> VM.metaspace | 类加载器泄漏(热部署、反射) |
| Code Cache 满 | jcmd <pid> VM.code_cache | JIT 编译热点爆炸 |
| DirectBuffer OOM | jcmd <pid> VM.native_memory | Netty/堆外内存泄漏 |
| 线程数暴涨 | jstack 统计线程状态 | 线程泄漏(线程池未 shutdown) |
| 频繁 Full GC | jstat -gccause + GC 日志 | 老年代碎片、大对象、内存泄漏 |
Step 4: GC 日志分析(生产标配)
# JDK 8: -Xloggc:/tmp/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps
# JDK 11+: -Xlog:gc*:file=/tmp/gc.log:time,uptime,level,tags:filecount=5,filesize=100M
# 用 gceasy.io 在线分析 或 GCViewer 离线分析
JVM 调优速查:
- 堆:
-Xms=-Xmx(避免运行时扩容抖动),MaxRAMPercentage=75.0适配容器 - G1(推荐):
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:InitiatingHeapOccupancyPercent=45 - 容器环境:必须加
-XX:+UseContainerSupport(JDK 8u191+ 默认开),否则 JVM 看不到 cgroup 限制 - 内存溢出自动 dump:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.hprof
典型场景:
- 凌晨 3 点 CPU 飙高 → 定时任务,导出 jstack 看 RUNNABLE 线程
- 启动后内存持续增长不释放 → 堆 dump 用 MAT 分析
Leak Suspects报告 - 应用响应慢但 CPU 不高 → 多为 GC STW 长,看
jstat -gcutil的 GCT 列 - K8s 容器被 OOM Killed 但 JVM 自身未 OOM → 容器
memory.limit小于 JVM-Xmx,未预留 metaspace/线程栈等开销