系列 · Linux · 第 7 篇

Linux(七):进程与资源管理 —— 从 top 到 cgroups

进程是怎么诞生和死亡的:fork/exec 模型、ps 和 top 在 STAT 列里打印的状态机、真正约束一台服务器的四个资源轴(CPU/内存/磁盘 I/O/网络)、那几个一定要记住的信号,以及把这一切组合成容器的 cgroup 与 namespace 原语。

Linux 运维的核心能力在于快速定位问题的根源,而不是记住更多命令。例如,当遇到网站变慢、接口超时或服务器无响应等模糊现象时,关键是迅速找到资源瓶颈:CPU 是否被占满?内存是被缓存占用(正常现象)还是被某个失控的进程消耗(需重点关注)?磁盘队列是否严重积压?某个 socket 是否出现泄漏?一旦定位到问题根源,选用合适的工具就顺理成章。

Linux(七):进程与资源管理:从 top 到 cgroups — 章节概览图

本文将按照这个思路,带你完整梳理一遍 Linux 系统中进程与资源管理的全貌。首先,我们会从头讲起:一个进程是如何诞生的(fork() + exec()),内核如何驱动它在不同状态间切换,以及限制它行为的四种核心资源维度。接着,我们会把常用的工具链——top / htop / ps / pstree / lsof / ss / iostat——重新梳理,避免简单罗列命令,而是以一种分层的方式,帮助你从不同角度观察同一个系统。最后,我们会探讨你能对进程做哪些操作:发送信号、放到后台运行、调整优先级(nice/renice),以及支撑现代容器技术的底层内核机制——cgroup 和 namespace。这些内容不仅实用,还能让你更深刻地理解 Linux 系统的本质。


进程、程序与线程——为什么区分它们很重要#

进程状态

在日常对话中,这三个术语经常被混用,但在内核世界里,它们有着截然不同的含义。

术语含义标识符
程序磁盘上的静态文件(例如 /usr/bin/vim),包含指令和数据布局inode + 路径
进程程序的一次运行实例,拥有独立的地址空间、文件描述符和 PIDPID 和父进程 PPID
线程进程内部的一个执行流,与其他线程共享地址空间TID (内核中的 task_struct

进程是资源分配的基本单位——内存页、文件描述符、打开的套接字等都归属于进程。而线程则是调度的基本单位,它与同一进程内的其他线程共享这些资源。在 Linux 中,这种区别非常微妙:无论是进程还是线程,在内核看来都是 task_struct,唯一的不同在于创建时共享了哪些资源(由 clone() 的标志位决定)。这就是为什么 ps -eLf 会显示相同 PID 但不同 LWP 的线程列表,也是为什么一个多线程的 JVM 在系统中表现为一个进程,背后却有数百个 task_struct

以下是几个关键点:

  • 每个进程都有一个父进程。 PPID 为 0 是内核保留的特殊值, PPID 为 1 则是 systemd(或 init),这是内核启动的第一个用户态进程。无论从任一进程向上追溯父进程链,最终都会抵达 PPID 为 1 的进程。
  • 进程之间默认是隔离的:它们的地址空间互不重叠,进程 A 打开文件不会影响进程 B。若需共享数据,必须通过显式的 IPC 机制(如管道、套接字、共享内存或信号)。
  • 进程是动态变化的:它们不断被创建、调度、阻塞、唤醒和销毁。一台健康的服务器每分钟可能会产生数千个短生命周期的进程,这完全正常。

进程怎么诞生: fork() 和 exec()#

fork/exec 模型

在 Linux 中,创建新进程只有一种方式:forkfork()(或者它的现代版本 clone())会复制调用进程,生成一个几乎完全相同的副本。调用完成后,父进程和子进程会从同一行代码继续执行,唯一的区别在于返回值——子进程返回 0,而父进程返回子进程的 PID。它们共享打开的文件描述符、相同的代码、堆内容,甚至最初连内存页都是共享的,这得益于写时复制(Copy-on-Write)机制。

不过,仅仅靠 fork() 只能复制出一个一模一样的程序。真正的关键在于接下来的一步:exec。在刚刚 fork 出来的子进程中,调用 execve("/bin/ls", argv, envp) 会让内核用另一个二进制程序替换当前进程的映像,同时保留原来的 PID、 PPID,以及默认情况下打开的文件描述符。所以,当你在终端输入 ls 时,背后实际发生的事情是这样的:

  1. shell 调用 fork(),这时系统中出现了两个 shell。
  2. 子进程调用 execve("/bin/ls", ...),将自身的程序映像替换为 ls
  3. 父进程(也就是原来的 shell)调用 waitpid() 阻塞自己,等待子进程退出,并将退出状态码存入 $?

这种“先创建进程,再加载程序”的分离设计,正是 shell 管道、重定向、环境变量修改以及 setuid 权限降级等功能得以实现的基础。所有这些操作都发生在 fork 之后、 exec 之前的子进程中,此时新程序尚未启动。

这种设计带来了两个常见的后果,经常会在实际生产环境中遇到:

  • 文件描述符的继承问题: 子进程会继承父进程中所有未标记为 O_CLOEXEC 的文件描述符。这就是为什么 cmd > log.txt 能正常工作: shell 在子进程中、 exec 之前就已经将目标文件以文件描述符 1 打开。这也是为什么某些泄漏的守护进程会导致已被删除的文件仍然被占用(稍后会详细讨论)。
  • PID 1 的特殊性: 如果一个进程退出时仍有子进程存在,这些子进程会被重新挂载到 PID 1 下,由它负责调用 wait() 回收资源。容器中的 PID 1 也承担同样的职责——如果忘记处理,就会出现那个经典的“Docker 容器里僵尸进程堆积”的问题。

进程的状态机#

pstop 命令会为每个进程显示一个单字母的 STAT 列,这个字母表示内核当前将任务置于的状态:

编码状态含义
R运行中当前正在某个 CPU 上执行,或者在运行队列中等待 CPU 资源
S可中断睡眠等待某个事件(如从 socket 读取数据或定时器触发),可以被信号唤醒
D不可中断睡眠等待内核处理某些无法中断的操作,通常是磁盘 I/O——即使 kill -9 也无能为力
T暂停SIGSTOPSIGTSTP 信号暂停(比如你按下了 Ctrl+Z)
Z僵尸进程进程已退出,但父进程尚未调用 wait() 回收其资源
X死亡进程销毁过程中的短暂状态,几乎不会看到

这些状态不仅仅是冷知识。如果一台服务器上有 200 个进程处于 D 状态,那问题不是“CPU 被打满了”,而是它们卡在了存储 I/O 上——无论你怎么优化 CPU 都无济于事。如果 Z 状态的进程数量不断增加,说明父进程存在 bug,忘记调用 wait() 来回收子进程。而如果 T 状态的进程一直存在,则可能是某个脚本或用户发送了 SIGSTOP 信号后,没有及时发送 SIGCONT 来恢复进程。

top 的顶部信息,例如 Tasks: 150 total, 2 running, 148 sleeping, 0 stopped, 0 zombie,是你的第一道健康检查线。如果 stoppedzombie 的值不是 0,那就需要进一步排查问题所在。

四个核心资源维度#

在陷入各种工具之前,先理清思路,建立正确的心智模型。生产环境中的问题,几乎都可以归结到以下四个关键维度之一:

  1. CPU——每秒能够完成多少计算任务;
  2. 内存——进程能容纳多大的工作集而不触发磁盘交换;
  3. 磁盘 I/O——数据在持久化存储中读写的快慢;
  4. 网络——数据进出服务器的速度。

任何一个维度出现瓶颈,都会拖累依赖它的所有环节。关键在于找出具体是哪个维度出了问题——而这正是监控工具的核心价值所在。

CPU#

通常你会关心两个问题:

  • 这台机器到底有多少核?nproclscpu 可以告诉你答案。
  • 当前负载如何?uptime 会显示三个负载值: 1 分钟、 5 分钟和 15 分钟的平均负载。

需要注意的是,负载平均值并不等于 CPU 使用率。它统计的是处于 运行中(running)或不可中断状态(uninterruptible) 的进程数(R + D)。例如,一台 4 核机器,如果负载平均值是 4.0,但 CPU 仍然有 95% 的空闲时间,那大概率是磁盘 I/O 出了问题:任务卡在 D 状态等待 I/O,而不是争抢 CPU 资源。通过对比负载值与核心数可以快速判断:

  • < 核心数 表示轻松;
  • ≈ 核心数 表示满载;
  • >> 核心数 则意味着过载。
1
2
3
4
uptime
# 06:56:12 up 12 days,  3:45,  3 users,  load average: 0.22, 0.45, 0.56
nproc
# 4

内存#

查看内存使用情况的标准命令是 free -h。但千万别用 Windows 任务管理器的思维去解读它:

1
2
3
              total        used        free      shared  buff/cache   available
Mem:           15Gi       2.5Gi       1.0Gi       100Mi       11.5Gi        12Gi
Swap:         2.0Gi          0B       2.0Gi

新手看到 free: 1.0Gi 往往会紧张。别急: Linux 会主动利用闲置内存作为 buffer/cache 来加速 I/O 操作——这里的 11.5 GiB 是随时可回收的。真正值得关注的是 available:它反映了内核估算的可用内存,即当进程需要时,系统能够腾出多少。

缓存分为两种类型:

  • Buffer 是写操作的暂存区。当你写入文件时,数据会先进入页缓存,随后批量刷入磁盘——这就是为什么小规模写入速度很快。
  • Cache 是读操作的缓存区:从磁盘读取的页会被内核保留下来,以便下次再读时直接命中——而这种情况非常常见。

只有当以下三种情况同时发生时,才能判定内存真的不足:

  • available 接近于零;
  • swap used 持续增长,而不仅仅是非零;
  • 内核开始记录 OOM (Out of Memory)杀进程的日志(dmesg | grep -i 'killed process')。

磁盘与网络#

磁盘容量可以通过 df -h 查看(按文件系统)或者 du -sh *(按目录)。实时 I/O 监控则依赖 iostat -x 1iotop;其中最关键的两个指标是:

  • %util:设备的繁忙程度;
  • await:请求在队列中等待的时间。

如果某个设备的 %util 达到 100%,且 await 高达几十毫秒,那它就是真正的性能瓶颈。

至于网络:

  • 使用 ss -tulnp 查看谁在监听端口;
  • 使用 ip -s link 查看接口的流量统计和丢包情况;
  • 使用 iftopnload 实时监控带宽。

如果想知道“谁占用了 80 端口”,最快的方法是运行 lsof -i :80 或者 ss -tulnp | grep :80

监控工具链#

top——首先要用的工具#

top 输出解析

top 是系统资源的全景视图,每个区域都有特定的信息:

  • 顶部信息:运行时间、登录用户数、平均负载。记得把负载和 nproc 对比来看。
  • 任务统计:总任务数 / 运行中 / 睡眠中 / 停止 / 僵尸进程。最后两项应该为 0。
  • %Cpu(s):分为用户态(us)、系统态(sy)、低优先级(ni)、空闲(id)、 I/O 等待(wa)、硬中断/软中断(hi/si)以及被偷走的时间(st)。如果 wa 高,说明磁盘性能可能有问题;在虚拟机中,st 高意味着宿主机把你的 CPU 时间分配给了其他租户——这种情况在云环境中很常见,尤其是邻居噪音问题。
  • 内存与交换分区:关注 avail Mem,而不是 free。如果 Swap used 持续增长,说明系统正面临真正的内存压力。
  • 进程列表:支持快捷键排序。P 按 CPU 使用率排序,M 按内存使用率排序,T 按累计运行时间排序。按 1 可以切换到按 CPU 核心显示——当某个核心被占满而其他核心闲置时特别有用。按 k 输入 PID 发送信号,按 q 退出。

htop——更友好的 top#

htoptop 的增强版,支持彩色显示、鼠标操作、树状视图(F5)以及内置的信号发送功能(F9)。如果你需要在一个机器上停留超过一分钟,建议安装它:

1
2
sudo apt install htop          # Debian/Ubuntu
sudo dnf install htop          # RHEL/Rocky/Fedora

ps——静态快照工具#

top 是动态刷新的,而 ps 则是某一时刻的静态快照,非常适合用于脚本编写和筛选。

1
2
3
ps -ef                  # System V 风格:所有进程,完整字段
ps aux                  # BSD 风格:所有进程,包括没有 TTY 的
ps -eo pid,ppid,user,stat,%cpu,%mem,cmd --sort=-%cpu | head -10

ps aux 中值得关注的字段:

  • VSZ——虚拟内存大小(进程申请的内存,单独看意义不大)。
  • RSS——常驻内存大小,实际占用的物理内存。这是衡量内存使用的真实指标。
  • STAT——状态码,可能带额外的后缀(< 表示高优先级,N 表示低优先级,s 表示会话首领,+ 表示前台进程组)。
  • TIME——累计的 CPU 时间,不是墙上时间。

pstree——进程父子关系#

1
2
pstree -ap            # 显示命令行和 PID
pstree -ap <PID>      # 显示某个 PID 下的子树

如果某个进程反复重启,顺着它的父进程往上查,总会找到谁在背后拉它起来。

lsof——打开的“文件”一览#

lsof 列出所有打开的“文件”,这里的文件是 Unix 意义上的广义文件——包括普通文件、 socket、管道、设备节点,甚至内核句柄。

1
2
3
4
5
6
7
lsof -p <PID>          # 查看某个 PID 打开了哪些文件
lsof -c nginx          # 查看所有 nginx 进程打开了哪些文件
lsof -u alice          # 查看用户 alice 的进程打开了哪些文件
lsof -i :80            # 查看谁占用了 80 端口
lsof -i tcp            # 查看所有 TCP socket
lsof +D /var/log       # 查看 /var/log 下被打开的所有文件
lsof +L1               # 查看链接数小于 1 的文件——已删除但仍然被占用

FD 列是一套小型语法:

  • cwd——当前工作目录
  • txt——可执行程序本身
  • mem——内存映射文件(如共享库)
  • 0r / 1w / 2w——标准输入 / 标准输出 / 标准错误
  • Nuu / r / w)——普通文件描述符及其模式

ss——现代版的 netstat#

1
2
3
ss -tulnp            # 显示 TCP 和 UDP 的监听端口、数字格式、带 PID
ss -s                # 显示所有 socket 状态的简要汇总
ss -tan state established '( dport = :443 )'

iostatiotop——磁盘监控利器#

1
2
iostat -x 1          # 每秒刷新一次,显示每个设备的详细统计信息
sudo iotop -o        # 仅显示当前正在进行 I/O 的进程

重点关注 %utilr/sw/sawait。如果设备的 %util 达到 100%,但 await 是个位数,说明设备忙但健康;如果 %util 达到 100%,同时 await 高达几百毫秒,则说明设备排队严重,性能堪忧

控制进程#

信号#

信号速查

kill 这个命令的名字其实有点误导人。它并不是直接“杀死”进程,而是发送一个信号,而大多数信号的默认行为恰好是终止进程。日常工作中真正需要关注的信号如下:

信号编号作用
SIGTERM15礼貌地请求进程终止,进程有机会进行清理工作。永远优先尝试这个信号。
SIGINT2按下 Ctrl+C 时发送的信号,语义上表示“用户中断了你的操作”。
SIGHUP1原本表示“终端挂断”,但现在通常被守护进程(如 nginx、 sshd、 syslog)用来重新加载配置
SIGKILL9强制终止进程的终极手段,内核直接干掉进程,无法被捕获或忽略,不留任何清理机会
SIGSTOP19暂停进程,无法被捕获或忽略,需要用 SIGCONT 恢复运行。
SIGTSTP20可被捕获的暂停信号,按下 Ctrl+Z 时发送的就是这个信号。
SIGCONT18恢复被暂停的进程继续运行。
SIGUSR1 / SIGUSR210 / 12用户自定义信号,很多守护进程用 SIGUSR1 来触发日志轮转。
SIGCHLD17子进程状态发生变化时发送给父进程,父进程应该调用 wait() 收集子进程的状态。
SIGPIPE13当你向一个没有读端的管道写入数据时触发(例如 `yes

正确的处理顺序应该是:先发送 SIGTERM,给进程几秒钟时间自行退出;如果无效,再考虑使用 SIGKILLkill -9 是最后的手段,因为它会跳过析构函数、不释放锁、留下临时文件,甚至可能导致数据库损坏。

1
2
3
4
5
6
kill <PID>             # 默认发送 SIGTERM
kill -HUP <PID>        # 重载配置
kill -9 <PID>          # 发送 SIGKILL,作为最后手段
kill -l                # 列出当前内核支持的所有信号及其编号
pkill -HUP nginx       # 按进程名或模式发送信号
killall -USR1 nginx    # 按精确程序名发送信号

后台任务与会话分离#

如果你需要让某个任务在 SSH 会话结束后继续运行,可以按以下方法选择,从简单到更可靠逐步升级:

1
2
3
4
./long_task.sh &                                  # 在当前 shell 中后台运行;
                                                  # SSH 断开后会收到 SIGHUP 而终止
nohup ./long_task.sh >/dev/null 2>&1 &           # 忽略 SIGHUP,丢弃输出
tmux new -s work     # 在 tmux 会话中运行,Ctrl-B d 分离会话,稍后可重新连接

对于交互式任务或长时间运行的任务,tmux(或者更老的 screen)是最优解。但如果是服务类任务,这些方法都不够专业——应该编写一个 systemd 配置单元,交由 init 系统管理(具体见服务管理篇)。

在单个 shell 内切换前台和后台任务:

1
2
3
4
5
6
./task.sh         # 前台运行
^Z                # 按下 Ctrl+Z,发送 SIGTSTP,暂停任务并返回 shell
bg %1             # 将任务放到后台继续运行
fg %1             # 将任务拉回前台运行
jobs              # 列出当前 shell 中的所有任务
disown %1         # 将任务与当前 shell 解绑,即使登出也不会终止

优先级:nicerenice#

调度器在决定下一个运行哪个进程时,会参考进程的 nice 值。这是一个从 -20(最高优先级,调度器偏爱)到 19(最低优先级,对其他进程友好)的整数,默认值为 0。将 nice 值调低到 0 以下需要 root 权限。

1
2
3
4
nice -n 19  ./backup.sh                # 以低优先级运行后台备份任务
nice -n -10 ./important.sh             # 提高优先级运行重要任务(需 root)
renice -n 10 -p <PID>                  # 修改正在运行的进程的优先级
renice -n 5 -u alice                   # 修改用户 alice 的所有进程的优先级

nice 只是一个软性提示,它会影响调度权重,但在系统空闲时,低优先级进程仍然可以获得 CPU 时间。如果需要设置硬性限制(例如“这个批处理任务最多只能使用 2 个核心和 4 GiB 内存”),应该使用 cgroup,而不是 nice

孤儿进程与僵尸进程#

两种容易让新手困惑的状态,都与父子进程的关系有关:

  • 孤儿进程(Orphan):父进程比子进程先退出时,子进程会被内核重新挂到 PID 1 (通常是 systemdinit)名下,由其接管 wait() 的职责。正常情况下无害
  • 僵尸进程(Zombie,标记为 Z:子进程已经退出,但父进程尚未调用 wait() 收集其退出状态。此时,进程表中仍保留该进程的 task_struct,占用一个 PID 和退出码。僵尸进程不消耗 CPU 或内存,但会占用进程表槽位——积累过多会导致 fork() 失败。

解决僵尸进程的关键在于修复父进程。如果父进程在你的控制范围内,确保它调用 wait()(或者注册 SIGCHLD 处理函数,或者将 SIGCHLD 设置为 SIG_IGN,让内核自动回收子进程)。如果父进程是第三方程序且无法修复,可以尝试杀死父进程,僵尸进程会被重新挂到 systemd 名下并立即回收。重启系统是最后的无奈之举。

快速列出当前的僵尸进程:

1
ps -eo pid,ppid,stat,cmd | awk '$3 ~ /^Z/'

cgroups + namespaces——支撑容器的两大基石#

cgroups 和 namespaces

搞清楚进程的概念后,内核中的两个特性就能帮你彻底理解容器的运行原理。即使你从来不碰 Docker,这两个特性依然非常有用。

Namespaces 的作用是隔离进程的视野,让每个进程只能看到自己的一片“小天地”。每种 namespace 都负责虚拟化一类系统资源:

  • pid——独立的进程 ID 空间;容器内的 PID 1 和外部的 PID 1 没有任何关系。
  • net——独立的网络设备、路由表和 socket。
  • mnt——独立的挂载点视图;容器里的根文件系统可以和宿主机完全不同。
  • uts——独立的主机名和域名。
  • ipc——独立的 SysV/POSIX 进程间通信对象。
  • user——独立的用户和组 ID 范围;容器内的 root 用户映射到宿主机上是一个普通用户。
  • cgroup——独立的 cgroup 层级结构视图。

cgroups (控制组, v2) 的职责是限制进程的资源使用量。一个 cgroup 是 /sys/fs/cgroup/ 下的一个目录,里面存放了属于该组的进程 ID 列表,以及一组用来配置资源限制的文件:

1
2
3
4
5
# 创建一个 cgroup,限制为半核 CPU 和 512 MiB 内存,并将当前 shell 放入其中
sudo mkdir /sys/fs/cgroup/demo
echo "50000 100000" | sudo tee /sys/fs/cgroup/demo/cpu.max     # 每 100ms 只能用 50ms
echo $((512*1024*1024)) | sudo tee /sys/fs/cgroup/demo/memory.max
echo $$ | sudo tee /sys/fs/cgroup/demo/cgroup.procs            # 将当前 shell 加入

需要记住的几个关键控制器:

  • cpu.max——CPU 使用带宽上限(quota / period)。
  • cpu.weight——在资源竞争时分配的相对权重。
  • memory.max——内存硬限制;超出限制会触发该 cgroup 内部的 OOM killer,而不会影响整个宿主机。
  • io.max——按设备设置的 IOPS 和 BPS 上限。
  • pids.max——防止 fork 炸弹,限制组内进程数量。
  • cpuset.cpus——绑定到特定的 CPU 或 NUMA 节点。

像 Docker、 Podman 或 containerd 这样的容器引擎,本质上就是一个小程序,完成了以下三件事:

  1. 创建一组新的 namespace (通过带正确标志的 unshare(2)clone(2) 系统调用)。
  2. 切换到从镜像中解压出来的新根文件系统(pivot_root)。
  3. 把生成的进程树放入一个带有指定资源限制的 cgroup (比如 docker run --cpus 0.5 --memory 512m)。

明白了这些底层机制后,调试容器问题就不再那么神秘了。用 nsenter -t <PID> -a 可以进入一个正在运行的容器的 namespace;查看 /proc/<PID>/cgroup 能知道它属于哪个 cgroup;而 systemd-cgtop 则是专为 cgroup 设计的 top 工具。

演练:机器变慢了,接下来怎么办?#

当遇到“机器变慢”的问题时,可以按照以下步骤一步步排查。这种方法不仅系统化,还能帮助你避免误判。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 1. 先看看有没有明显的异常?
uptime                          # 负载值是否过高?对比 CPU 核心数 (nproc)
dmesg -T | tail -50             # 检查是否有 OOM 杀进程、硬件错误或段错误 (segfault)
df -h                           # 文件系统是否快满了?

# 2. 哪个资源瓶颈最严重?
top                             # 按下 1 查看每个 CPU 核心的使用情况;重点关注 us/sy/wa/st
free -h                         # 内存还够用吗?swap 是否在增长?
iostat -x 1 5                   # 每个设备的 %util 和 await 是否异常?
ss -s                           # socket 连接数是否正常?TIME-WAIT 是否过多?

# 3. 找出罪魁祸首
ps aux --sort=-%cpu | head      # 占用 CPU 最多的进程有哪些?
ps aux --sort=-%mem | head      # 占用内存最多的进程有哪些?
sudo iotop -o                   # 哪些进程在频繁读写磁盘?
sudo lsof -i -nP | head         # 哪些进程在使用网络连接?

# 4. 针对某个具体进程深入分析
cat /proc/<PID>/status          # 查看进程状态、线程数和内存使用情况
ls -l /proc/<PID>/fd/           # 进程打开了哪些文件?
cat /proc/<PID>/limits          # 进程的实际 ulimit 限制是什么?

# 5. 做出决策并采取行动
renice -n 10 -p <PID>           # 如果是失控的批处理任务,降低它的优先级
kill <PID>                      # 先尝试优雅地终止进程
kill -9 <PID>                   # 只有在 SIGTERM 无效时才强制杀死

千万不要跳过第 2 步!很多“服务器变慢”的问题之所以被误诊,就是因为有人直接打开 top,看到某个进程占用了 100% 的 CPU,就急着用 kill -9 干掉它。殊不知,真正的问题可能是磁盘 I/O 饱和,而那个进程只是因为等待 I/O 而卡在 D 状态(不可中断的睡眠状态)。

实战:恢复被误删的日志文件#

经典场景:有人在 nginx 正常写日志的时候,手一抖执行了 rm -rf /var/log/nginx/access.log。等反应过来时,文件已经被删了,但事情还没完:

  • ls /var/log/nginx/ 查看目录,发现文件已经不见了(目录项被移除了)。
  • 再用 df -h 查看磁盘使用情况,发现空间占用居然没变(inode 和数据块依然存在)。
  • 更神奇的是, nginx 依然在往原来的文件描述符(FD)写入数据,仿佛什么都没发生——实际上确实如此。

这是 Unix 文件系统语义的经典体现:虽然文件名从目录中被删除了,但只要还有进程持有该文件的打开 FD, inode 就不会被释放,数据依然存在。内核会通过 /proc/<pid>/fd/<fd> 暴露这个 FD,这意味着我们有机会把文件内容抢救回来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
PID=$(pidof nginx | awk '{print $1}')

# 1. 确认被删除的文件是否仍处于打开状态
sudo lsof -p "$PID" | grep access.log
# nginx 1234 root  6w  REG  8,1  123456789  4711  /var/log/nginx/access.log (deleted)

# 2. 从 /proc 中恢复文件
sudo cp /proc/"$PID"/fd/6 /var/log/nginx/access.log

# 3. 通知 nginx 重新打开日志文件
sudo nginx -s reload    # 或者:sudo kill -USR1 "$PID"

这种方法适用于任何“被删除但仍有进程打开”的文件。不过需要注意的是,一旦最后一个 FD 被关闭,内核就会彻底回收 inode 和数据块——所以务必立即行动,赶在相关进程重启之前完成恢复操作。

一页带走#

  • 进程是正在运行的程序,拥有独立的地址空间;而线程共享这个地址空间。在调度器眼里,只有 task_struct 才是真实存在的。
  • 新进程总是通过 fork() + exec() 创建,从来不会凭空生成。 PID 1 (也就是 systemdinit)是所有进程树的起点。
  • 状态机R/S/D/T/Z)就是 pstop 命令输出的状态列,每种状态背后都对应着不同的排查思路。
  • 资源瓶颈通常出现在四个维度——CPU、内存、磁盘 I/O 和网络——市面上大多数监控工具不过是这些维度的不同视角罢了。
  • Linux 的内存管理看起来比实际复杂:真正值得关注的是 available,而不是 free
  • kill 命令用来发送信号;先尝试 SIGTERM,实在不行再用 SIGKILL。守护进程收到 SIGHUP 时会重新加载配置。
  • nicerenice 是软性的调度建议;而cgroups 则是硬性限制,它和 namespaces 共同构成了容器技术的核心基础。

推荐阅读#

  • Brendan Gregg,《Linux 性能优化》——http://www.brendangregg.com/linuxperf.html
  • man proc(5) —— /proc 文件系统的详尽参考
  • man 7 signal —— 每个信号的默认行为和捕获规则
  • man 7 cgroups —— cgroup v2 的设计与控制器列表
  • man 7 namespaces —— 每种命名空间虚拟化的具体内容

本系列下一篇预告#

  • Linux 磁盘管理 —— 分区、文件系统、 LVM 和挂载栈
  • Linux 用户管理 —— 用户、组、 sudo、 PAM 和最小权限原则

到目前为止,你应该已经具备了快速定位问题的能力:面对一台性能不佳的机器,能在一分钟内指出哪个资源维度出现了瓶颈,并且有目的地选择合适的工具,而不是盲目地运行 top。这才是从“跑命令”到“运维系统”的关键转变。

本系列

Linux 9 篇

  1. 01 Linux(一):使用基础
  2. 02 Linux(二):文件权限 —— rwx、chmod、chown 与超越它们的机制
  3. 03 Linux(三):磁盘管理
  4. 04 Linux(四):软件包管理
  5. 05 Linux(五):用户管理
  6. 06 Linux(六):系统服务管理
  7. 07 Linux(七):进程与资源管理 —— 从 top 到 cgroups 当前
  8. 08 Linux(八):文件操作深入解析
  9. 09 Linux(九):Vim 编辑器精要

读有所得?

GitHub 关注我 → 新文周更

GitHub