跳转到内容

Linux 进程、线程与调度面试题

18 道题
分类
Linux
题目数
18 道
已阅读 0 / 18 题
1 fork 与 exec 的区别是什么?两者组合使用时的进程行为是怎样的?

答案:

fork() 基于父进程复制(Copy-on-Write)创建子进程,子进程获得与父进程相同的地址空间、文件描述符和信号处理;返回两次,父进程返回子进程 PID,子进程返回 0。exec() 族函数(execlexecvexecve 等)将当前进程的地址空间替换为新程序镜像,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_normalsched_batchsched_idle 同属 CFS;实时任务(SCHED_FIFOSCHED_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μs100-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 中定义,常用状态:

状态字符内核宏含义
RunningRTASK_RUNNING就绪或正在运行(在 runqueue 中)
SleepingSTASK_INTERRUPTIBLE可中断睡眠,等待事件
Disk SleepDTASK_UNINTERRUPTIBLE不可中断睡眠(IO 阻塞,ps 显示 D)
StoppedTTASK_STOPPED收到 SIGSTOP/SIGTSTP
TracingtTASK_TRACED被 ptrace 跟踪
ZombieZEXIT_ZOMBIE终止未 wait,PID 仍占
DeadXEXIT_DEAD临时态,wait 回收中
IdleITASK_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_priority 1-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:时间片轮转的 FIFO
  • SCHED_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.socketsocket 激活(按需启动)
timer.timer替代 cron(支持 monotonic time)
mount / automount.mount文件系统挂载
path.path监控文件变化触发动作
swap.swapswap 设备
slice / scope.slice/.scopecgroup 资源切片

启动流程(以 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):

标准编写步骤:

  1. fork() + 父进程 exit():让 init 收养子进程,避免僵尸进程
  2. setsid():创建新会话、脱离控制终端
  3. 二次 fork():确保进程不是会话首进程,无法再开 tty
  4. chdir("/"):避免占用挂载点导致 umount 失败
  5. umask(0):重设文件权限掩码
  6. 关闭/重定向 fd 0/1/2/dev/null 或日志文件
  7. 处理 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 进程间异步通知机制,每个信号有编号、默认行为、是否可捕获:

信号编号默认行为可捕获触发场景
SIGTERM15终止kill/killall 默认;systemd stop
SIGKILL9终止(不可阻止)kill -9
SIGINT2终止Ctrl-C
SIGHUP1终止终端关闭;守护进程常被捕获为 reload 配置
SIGQUIT3core dumpCtrl-\
SIGUSR1/210/12终止(默认)/ 自定义自定义(如 Nginx 重开日志 / worker_processes 热重载)
SIGSTOP/SIGCONT19/18暂停/继续❌/✅Ctrl-Z / fg

优雅退出(Graceful Shutdown)三阶段:

  1. 接收 SIGTERM → 进程捕获信号
  2. 停止接收新请求 → 摘除负载均衡(如 K8s readiness probe 失败)
  3. 等待现有请求完成 → 设置宽限期(默认 30s)
  4. 关闭资源(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 v1cgroup 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 3Ring 0
可访问内存仅自身进程虚拟地址空间全部物理内存 + 内核地址空间
可执行指令普通指令含特权指令(关中断、MMIO、IO)
上下文进程独立栈共享内核栈(per-CPU 或 per-thread)
切换触发系统调用/中断/异常主动 sysret / iret 返回
崩溃影响进程被 SIGSEGV 杀掉kernel panic,整机不可用

系统调用执行流程(以 read() 为例):

  1. 用户态 glibc read() 封装触发 syscall 指令,传入 syscall number(如 __NR_read = 0)和参数。
  2. CPU 自动从 Ring 3 切换到 Ring 0,跳转到内核 entry_SYSCALL_64(5.17+ 改为 entry_SYSCALL_64_handler)。
  3. 内核根据 syscall number 查 sys_call_table 找到 sys_read
  4. 内核在 VFS 层找到 fd 对应的 file_operations,调用底层驱动 read()
  5. 数据从内核缓冲区拷贝到用户态缓冲区(注意:不是地址映射,是实际内存复制)。
  6. 内核执行 sysret/ iret 切回 Ring 3,glibc 包装层返回用户态。

性能开销:

  • 单次 syscall 约 100-200ns(vDSO 优化路径 < 50ns,例如 clock_gettime
  • 频繁 syscall 的程序应使用批处理 IO(readv/writevsendmsg/recvmmsgio_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_structtask_struct
  • 创建:clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD, ...),glibc pthread_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 中分为进程/线程上下文切换中断上下文切换

进程/线程上下文切换流程:

  1. 触发:时间片耗尽(scheduler_tick)、高优先级任务唤醒、自愿 sched_yield()、阻塞 IO。
  2. 内核在旧 task 的 task_struct->thread(内核栈)保存寄存器上下文(switch_to 宏)。
  3. 切换页表:switch_mm() 切换 mm_struct(线程切换时同进程内不切),并触发 TLB flush 或 ASID 复用。
  4. 从新 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() 为例):

  1. 用户态 glibc 包装函数 getpid() 写入 rax = __NR_getpid (39),执行 syscall 指令。
  2. CPU 硬件行为:保存 rcx(下一条指令地址)到 rcx、保存 r11(rflags)到 r11、跳转到 MSR IA32_LSTAR 指向的内核入口(entry_SYSCALL_64)。
  3. 内核入口用 swapgs 切换 GS 寄存器(用户 GS ↔ 内核 GS),加载内核栈,从 rax 读 syscall number。
  4. sys_call_table[39] 找到 sys_getpid(返回 current->pid),执行并将返回值写入 rax
  5. 内核 sysretq 恢复 rip = rcxrflags = r11swapgs 切回用户 GS,返回用户态。
  6. 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_gettimegettimeofdaytimegetcpu
  • 优势:< 50ns 完成(仅读 vDSO 数据或 TSC),避免 syscall 开销
  • ldd /bin/ls 输出中可见 linux-vdso.so.1 => (0x00007fff...)

与库函数的关系:

调用是否进入内核说明
getpid() vDSOvDSO 直接读 task_struct 缓存
getpid() 普通syscall 走 sys_getpid
mallocglibc 用户态分配(ptmalloc2)
open()必须内核(涉及 VFS)
printfglibc 缓冲,最后一次性 write()

性能优化要点:

  • 高频短小调用优先用 vDSO 路径(确认程序未禁用 vDSO:AT_SYSINFO_EHDR auxv)
  • 批处理 syscall:readv/writevrecvmmsg/sendmmsgio_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 UsageLoad 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 高磁盘 IOiostat -xz 1iotop
Load 高 + st 高虚拟化偷取宿主机过载、绑核
Load 高 + id 高 + 单核 100%单线程热点taskset 绑核 / 多线程改造
Load 高 + D 态多IO hungdmesgcat /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 -heapjvisualvm内存泄漏(持有引用未释放)
Metaspace 满jcmd <pid> VM.metaspace类加载器泄漏(热部署、反射)
Code Cache 满jcmd <pid> VM.code_cacheJIT 编译热点爆炸
DirectBuffer OOMjcmd <pid> VM.native_memoryNetty/堆外内存泄漏
线程数暴涨jstack 统计线程状态线程泄漏(线程池未 shutdown)
频繁 Full GCjstat -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/线程栈等开销