尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Linux进程全解析:从fork到僵尸进程,一次搞懂生命周期与排查实战

Linux进程全解析:从fork到僵尸进程,一次搞懂生命周期与排查实战 你是不是也遇到过这种情况Windows 上打开任务管理器发现微信开了十几个进程第一反应是这软件是不是中毒了。而在 Linux 上你执行一条ps aux屏幕上哗啦啦滚出几十行进程列表新手上路直接看懵。其实进程这个概念就是一个正在运行中的程序实例Windows 上那些多出来的进程和 Linux 里的进程列表本质上是同一个东西。我这篇文章就是来帮你把 Linux 进程的概念彻底理顺的。我不打算按教科书那样从操作系统的祖师爷讲起而是从一个实际干活的视角出发把进程到底是什么、进程怎么活又怎么死、进程之间怎么聊天、守护进程是个什么角色以及日常排查最常用到的命令和坑一次讲明白。内容既适合刚接触 Linux 的初学者也适合那些用了很久 Linux 但总觉得概念有点虚、有点糊的同学。看完你再去跑ps、top心里会踏实很多。1. 进程到底是什么不是程序是程序的运行现场1.1 程序和进程的区别用一个比喻说透很多人把程序和进程混为一谈这其实是第一个必须掰开的概念。程序是静态的它就是一个躺在磁盘上的文件比如你apt install nginx装完以后/usr/sbin/nginx这个文件就是程序。它既不占内存也不占 CPU它只是一堆指令和数据的集合放在那里也没人管。进程就不一样了。进程是程序的一次执行过程是程序在被操作系统加载到内存之后从代码变成了活物的那个状态。我用一个比较接地气的比方程序好比是菜谱进程好比是按照菜谱正在炒的那锅菜。菜谱放在书架上一百年也不会自己变成菜而当你把菜谱拿进厨房、架上锅、点上火开始操作时这锅菜才真正在执行。进程就是那道正在炒的菜包括了食材数据、火候CPU 时间、锅碗瓢盆系统资源。这就能解释很多现象了同一个程序可以产生多个进程。你打开两个终端各跑一个vim磁盘上还是同一个vim程序文件但内存里有两个完全独立的vim进程各自有各自的编辑状态互不影响。这就是程序是静态的、进程是动态的最直观的体现。1.2 进程在内核眼里长什么样PCB 是它的身份证从操作系统的角度来看进程不是一团模糊的内存数据而是内核里一个结构体这个结构体叫进程控制块PCBProcess Control Block。在 Linux 源码里这个结构体就是task_struct它藏在include/linux/sched.h头文件里是整个进程概念的物理载体。你可以把task_struct想象成每个人的户籍档案里面记着 PID进程号、PPID父进程号、进程状态、优先级、打开的文件描述符列表、内存地址空间、信号处理信息、计时器、各种统计信息…… 可以说内核只要能查到这档案就能完整还原这个进程的运行现场。ps aux命令你看到的每一个字段几乎都能在task_struct里找到对应项。这里有个底层细节值得理解进程在用户态和内核态是有分工的。你写的 C 程序调用printf、read、fork这些函数时本质上是通过系统调用进入内核态让内核去操作那个进程对应的task_struct而不是用户程序自己在瞎折腾。这也是 Linux 一切皆文件之外的另一个核心设计进程是资源分配的基本单位线程是调度的基本单位。这个说法你先记住后面讲线程时还会用到。1.3 PID、PPID 与进程树每个进程都有爹Linux 的进程不是凭空冒出来的几乎所有的进程都是由另一个进程通过fork()系统调用复制出来的。复制出来的这个进程叫子进程原来的进程叫父进程。于是整个系统就形成了一棵以 PID1 的进程为根的进程树。这里说的 PID1 就是init进程在当代 Ubuntu、CentOS 系发行版里 PID1 通常是systemd。它的职责很重其中一个关键功能是收养那些爹死了没人管的孤儿进程。我们可以在终端里快速验证这个树形结构。执行ps -ef你会看到第二列是 PID第三列是 PPID。再执行pstree就能直接看到层级关系systemd─┬─sshd─┬─sshd───bash───vim这种链式结构非常直观。平时排查问题看到一个进程的 PPID 异常基本就是被哪个程序拉起来的关键线索。比如你发现一个可疑的挖矿进程顺着 PPID 往上找往往能挖出它是被哪个服务搞出来的。所以在 Linux 世界里不存在进程从石头缝里蹦出来这回事。每条进程都有来路哪怕你是手动在终端里输入一条命令那命令进程的父进程也是当前这个 shell比如 bash。这就是进程树思想的第一课。2. 进程的生命周期从出生到死亡中间那几个状态全都见过吗2.1 fork 与 exec进程诞生的经典流程一个进程怎么出生教科书上讲得很复杂实际拆开就两大步先fork()再exec()。第一步fork()是系统调用作用是把当前的进程复制一份生成一个几乎一模一样的子进程。注意是几乎因为子进程有自己的 PID、自己的父进程 PPID、自己的计数器等私有信息不会随复制带过来。fork()执行成功后父子两个进程同时从fork()这个调用的返回点继续往下执行区别只在返回值父进程得到的是子进程的 PID子进程得到的是 0。这就是我们在代码里常写的if (pid 0) { 子进程逻辑 } else { 父进程逻辑 }的由来。但光有fork还不够子进程经常需要换一个身体去执行一个全新的程序。这时候就要调用exec系列函数execve、execlp等它干的活儿是把当前进程的代码段、数据段、堆栈全部替换成新程序的内容但 PID 保持不变。你可以把这理解成借壳上市进程的身份证还是那张但里面的人整个换掉了。bash执行外部命令时就是先fork出一个子进程然后子进程立即exec换成要运行的命令程序。这套流程你接手任何一门语言的服务进程排查时都躲不开。这里还埋着一个经典的 C 语言面试陷阱父进程和子进程的全局变量在fork之后是否共享答案是各有一份属于写时复制Copy-on-Write。子进程没改这些变量之前父子共享同一块物理内存一旦某一方要写内核才真正复制一份出来。这个机制极大减少了fork的额外开销也是 Linux 创建进程很快的原因之一。2.2 五种状态运行、就绪、阻塞、僵尸、停止用一张表看懂进程创建之后并不是一条道走到黑它会在若干状态之间反复横跳。Linux 进程状态在ps aux输出里用单字母表示分别是 R、S、D、T、Z 等。这里我整理一个速查表你在排查时能直接对照着用状态字段含义典型场景R (running/runnable)正在运行或在运行队列中等待调度CPU 密集任务如while(true)空转S (sleeping)可中断睡眠等待某个事件或资源等用户输入、等网络数据包D (disk sleep)不可中断睡眠通常在等磁盘 I/O 完成重度读写磁盘的进程如数据库落盘T (stopped)被暂停通常是被信号 SIGSTOP 或CtrlZ挂起调试时的断点暂停Z (zombie)僵尸进程已退出但父进程还没收割父进程没调用wait()子进程残留尸体I (idle)内核线程的空闲态较新内核里会出现内核 CRYPTO 线程等这几种状态虽然看似多但核心逻辑就是一句话CPU 是稀缺资源一个时刻核心里只在跑少数进程其余进程要么在等下个 CPU 时间片就绪、要么在等外部事件阻塞、要么已经死了但没埋僵尸。状态机不是玄学它就是内核对你到底能不能被调度做的标记。操作系统里还有一套经典的三态模型运行、就绪、阻塞。运行态和就绪态的区别在于有没有真正占用 CPU。就绪是排队等号运行是已经在窗口办业务。阻塞则是这个进程压根不抢 CPU它在等鼠标键盘输入、等网络响应、等磁盘数据等到了才会回到就绪队列重新排队。你看top命令里的%CPU为 0 但进程状态是 S 的那些进程基本都是阻塞态。2.3 僵尸进程这个坑为什么会阴魂不散僵尸进程Zombie是 Linux 新手最容易遇到、又最不容易理解的状态。它指的是子进程已经执行完退出了但它还在内核的进程表里占着一个位置因为它的退出状态exit code还没被父进程取走。你看到ps输出里一堆[vim] defunct的时候不用怀疑那就是僵尸。为什么会有这种状态因为父进程需要知道子进程干完活儿的结果比如pipeline里下游命令要知道上游命令的退出码。所以子进程想死也不能死得干干净净得留下尸体等父母来认领。父进程调wait()或waitpid()系统调用就能把子进程的退出状态取走这时内核才会彻底释放那个task_struct。可如果父进程一直不调用wait()僵尸就一直赖着。僵尸已经不算一个真正运行的进程了不占 CPU 也不占内存但它占着进程号PID和一部分内核资源。僵尸积累多了PID 号码就会被耗光新进程创建不出来这是生产环境的重大事故。我在实际工作中见过一台机器 PID 上限是 32768结果被上万只僵尸撑爆新起的服务全都Resource temporarily unavailable。这种时候你没法kill -9一只僵尸——它已经死了信号对它无效。唯一正确的解法是干掉它的父进程让 PID 为 1 的systemd接手之后自动wait()掉这些遗骸。排查僵尸的命令很简单ps aux | grep defunct或者top命令右上角的 zombie 计数器。这招在线上应急的时候值一顿饭钱。2.4 孤儿进程与守护进程另一种无父的常态和僵尸相对的是孤儿进程父进程先比子进程死掉子进程还活着。这时候子进程并不会变成没爹的孩子四处流浪而是会被 PID1 的initsystemd立即收养PPID 变成 1。这个设计保证了整个世界不会出现无人管理的进程。你要注意孤儿进程是正常的、活着的进程完全没有危害。比如你用nohup把一个任务丢到后台然后直接关闭终端父进程 bash 退出这个任务的进程就被收养成了孤儿但它依然活蹦乱跳地跑着。讲到这里就顺理成章地引出守护进程daemon的概念守护进程就是一种特殊的长命百岁的后台进程它刻意脱离了终端控制不会再收到CtrlC这种终端信号通常以d结尾命名——sshd、httpd、mysqld、nginx的 master 进程都是。标准写法里会fork一次或两次然后调用setsid()创建新会话彻底摆脱终端。这也是为什么很多人写服务部署脚本时老强调要用 systemd 服务而不是手动 nohup 启动因为 systemd 本身就是专业的守护进程管理器重定向日志、开机自启、崩溃拉起都给你管好了。3. 线程、进程池与调度进程从来不是一个人在战斗3.1 线程和进程的区别到底差在哪这可能是被问最多、也最容易讲糊的一个问题。我先说一个能落地的结论进程是资源分配的单位线程是 CPU 调度的单位。同一进程内的多个线程共享进程的内存空间、文件描述符、信号处理等资源但各自有独立的栈、寄存器上下文和线程 ID。用个比喻进程是一家饭店线程是饭店里的服务员。饭店有营业执照、有门面、有后厨设备、有营业执照里的法人PCB这些都是整个饭店共享的资源。服务员只管端菜、上菜、结账他们是实际干活的执行流。你想多接客人可以多招服务员线程但一个服务员多能干、有多快取决于饭店地基进程资源和排班表CPU 调度。这个区别带来一个巨大的现实后果线程切换比进程切换便宜得多。进程切换要切换地址空间、缓存、页表等开销较大而同进程内的线程切换共享地址空间开销小很多。这也是高性能服务普遍采用多线程而不是多进程的原因。但线程也有麻烦共享内存就意味着要加锁不加锁抢同一份变量轻则数据错乱重则直接崩。在 Linux 的实现里线程其实是从clone()系统调用来的clone可以视为更灵活的fork由参数控制父子之间共享哪些资源。你在ps -eLf里能直接看到这种线程关系每个线程其实对应着内核的一个task_struct。所以说在 Linux 上线程是轻量级进程不无道理。3.2 进程池和线程池为什么不能无限创建进程知道进程能fork新手容易犯的第一个错误就是能创建就使劲创建。实际上创建进程是有代价的系统调用forkexec的开销、新进程地址空间的建立、PCB 的分配全都要花时间和资源。你写一个服务每个请求都现场fork一个子进程去处理QPS 一高光是进程切换和创建销毁就能把 CPU 吃干。所以池化思想来了进程池Process Pool或线程池Thread Pool就是在进程启动时预先创建一批工作进程或线程放到一个池子里待命。来一个任务从池子里捞一个空闲的去处理处理完不是销毁而是归还池子继续接下一个任务。这就像打车平台搞的出租车池司机提前就位客人来了就派单而不是客人来了才去驾校现学一个司机出来。很多热词里提到的进程池其实在生产里很常见。Nginx 的 worker 进程就是固定几个不会按请求量疯狂 forkPython 的multiprocessing.Pool也是预设进程数然后往复利用。进程数不是越大越好经验法则CPU 密集型服务进程/线程数约等于 CPU 核数I/O 密集型服务可以适度增多但多到一定程度后上下文切换开销反而让吞吐下降。这就是为什么你压测 Web 服务时会发现 worker 数调到 8 个和调到 80 个性能差别不大甚至 80 个更差。3.3 进程调度谁多分 CPU谁去排队等进程调度是一个非常值得认真理解的概念因为它决定了谁先跑、谁多跑。Linux 的默认调度器是 CFS完全公平调度器Completely Fair Scheduler核心目标是让每个进程都能公平地获得 CPU 时间。CFS 的实现思路不是每个进程轮流跑固定时间片而是维护一个红黑树按虚拟运行时间排序每次都选虚拟运行时间最少的进程来运行。所谓虚拟运行时间会按进程优先级加权优先级高nice 值小比如-20的进程它的虚拟时间流逝得慢于是被选中的机会更多。你看top里NI列就是 nice 值PR是优先级两者有换算关系一般不用手动动它但你要知道这个逻辑。除了 CFSLinux 还区分了实时调度策略SCHED_FIFO和SCHED_RR用于对延迟极其敏感的场景比如音视频处理、工业控制。实时进程优先级高于普通进程内核在有实时进程就绪时会优先调度它们。这个机制也让新手明白一件事你的普通 Java 进程调优别想着靠调 nice 值翻盘老老实实优化代码才是正道。调度还有一个隐藏角色时间片。CFS 里有一个sched_latency目标大体是系统保证每个进程在某个周期内都有机会运行。如果运行进程太多每个进程分到的时间就短系统就会显得很卡。这就是为什么一台服务器上铺太多繁忙服务时top看每个进程的 CPU 使用率都不高但整体响应慢半拍其实就是上下文切换过频。4. 进程通信IPC进程之间怎么聊天4.1 为什么非要 IPC进程之间默认是隔离的进程是资源隔离的单位这一点是它最大的优点也是最大的麻烦。两个进程的内存地址空间互相看不见A 进程里变量等于 1B 进程完全不知道。可实际业务里进程之间总要传数据、发通知、共享状态比如 Nginx master 进程要通知 worker 平滑重启一个 web 服务器要跟旁边的缓存进程交换数据。没有通信机制进程就成了一个个信息孤岛操作系统也就失去了组合意义。于是进程间通信Inter-Process CommunicationIPC就是解决这个孤岛问题的整套方案。Linux 下 IPC 方式非常多从最古老的管道、到 System V 的共享内存和消息队列、再到信号和 socket各有各的适用场景。这里我必须强调一点没有银弹。别指望着一种 IPC 通吃所有需求选错了性能差十倍都是轻的。我把常见 IPC 做一个速查表告诉你它们最典型的用途IPC 方式特点典型场景管道pipe单向字节流简单命令行ls | grep命名管道FIFO可以在无亲缘关系的进程间用简单生产者消费者信号signal传递事件通知数据量极小kill -HUP让 nginx 重载配置System V 消息队列有格式的消息内核维护老系统模块间通信共享内存速度最快需同步高频交易、大流量数据交换信号量semaphore同步原语不传数据配合共享内存做互斥套接字socket跨主机通信Web 服务、数据库远程访问4.2 管道最朴素也好用的 IPC管道是最简单、也最符合Linux 哲学的通信方式。你在命令行里写cat access.log | grep 404这跟管道就出现了cat进程的输出接到grep进程的输入。管道在操作系统层面就是内核里的一段缓冲区一端写、一端读单向流动。管道分两类匿名管道pipe和命名管道FIFO。匿名管道只能在有亲缘关系的进程间使用一般就是fork出来的父子进程或 shell 里管道连接的兄弟进程之间用。命名管道则可以通过一个文件路径来标识两个不相关的进程只要约好都打开这个 FIFO 文件就能通信。经常看到有人问mkfifo是干嘛的其实就是创建命名管道用的。用管道有个容易踩的坑管道缓冲区有限。当写端写入的速度快于读端消费的速度时写端会阻塞在write系统调用上等着读端把数据读走腾出空间。反过来如果用管道时读端不读写端就会被卡死。Shell 管道可能会因为某个命令读得太快把上一条命令的输出带偏但更多时候你遇到的命令卡住不动就是管道两端处理速度不匹配导致的。排查时pv命令能帮你看到管道里面的数据吞吐。管道还自带一个特性读端关闭后再往里写内核会给写进程发 SIGPIPE 信号默认动作是终止写进程。这意味着如果你写一个管道程序读方退出后你再写你的进程可能莫名其妙就死了。排查此类诡异问题时别忘了看一眼信号处理。4.3 共享内存最快但需要同步双刃剑本性暴露如果说管道是搬运工共享内存就是直接在黑板上写字。共享内存的做法是内核把一块物理内存映射到多个进程的虚拟地址空间中让它们都能直接读写同一块区域。因为数据不需要在内核和用户空间之间来回拷贝所以它是所有 IPC 方式里性能最高的。代价是多个进程同时改同一块内存不加控制就产生竞态条件。比如进程 A 读到一个变量等于 100还没写回去进程 B 已经把它改成了 200A 再写就把 B 的修改覆盖掉了。这跟多线程并发改共享变量的混乱一模一样只不过是把范围从线程扩大到了进程。于是共享内存基本要和信号量、或futex、或原子操作配合使用先获取锁再读数据、写完释放锁。实际工程里很多语言层面的中间件都对共享内存做了封装比如 Python 的multiprocessing.shared_memoryC 里直接用shmgetshmat也行。但我要提醒一句共享内存虽然快编程难度高而且出错后极难排查。你的数据在多个进程间混杂没有日志、没有边界检查值被改错你只能靠肉眼盯着内存 hex dump 一份一份看。生产环境中如果不是性能真的吃紧了我建议先考虑相对安全的消息队列别一上来就上共享内存。4.4 信号、消息队列、socket各归其位信号signal是一种非常特殊的 IPC它传的不是数据而是事件通知。你可以把它理解成手机震动它不告诉你具体内容只告诉你有新消息快来看。给我发一个SIGTERM让我安全退出给我发一个SIGHUP让我重新读配置这是服务端程序员耳熟能详的场景。信号最大的限制是携带信息量太小你没法通过一个信号传 100 字节数据所以它通常只用于控制、通知类场景。消息队列Message QueueMQ则是内核维护的一个队列数据结构支持多个进程往队列里投递消息其他进程按顺序取走。好处是消息有边界、有格式比管道更结构化坏处是性能比共享内存差且在系统层面有限制。现在很多现代应用已经改用更上层的消息中间件如 Redis、Kafka、RabbitMQ而不是直接用 System V 消息队列但概念上是一样的模型投递-存储-消费。最后是 socket套接字。严格说 socket 不只是 IPC它可以跨主机是网络通信的基础。但在同一台机器上你也可以用 Unix Domain SocketUDS来做进程间通信。你会发现 Nginx 和 PHP-FPM 通信时很多人喜欢配置unix:/var/run/php-fpm.sock而不是 TCP 端口就是因为 Unix Domain Socket 少了 TCP/IP 协议栈和端口处理的额外开销在同一台机器上更快更可靠。Electron 主进程和渲染进程之间用 IPC 通信本质上也基于类似的消息机制只是包装好了ipcMain和ipcRenderer接口。很多前端同学问Electron 的 IPC 和 Vue 有关系吗答案是没关系Electron 是主进程和渲染进程之间的消息通信Vue 是渲染进程内部页面层的数据驱动框架两者不冲突只是经常会一起用。5. 进程实践查看、管理、排查一条龙5.1 看进程ps、top、htop 的使用与区别进程概念学得再好不会看实际状态也白搭。日常排查我使用频率最高的命令是ps。这里强烈建议养成ps -ef和ps aux两个组合的记忆习惯。ps -ef输出全格式PID、PPID、C、STIME、TTY、TIME、CMD适合看进程父子关系ps aux输出多出 CPU、内存占用百分比适合看资源占用情况。两者不完全等价-e表示所有进程a表示所有终端进程加其他用户的进程u表示以用户为主的格式x表示包括无终端的进程。常用组合可以这样用ps -ef | grep nginx ps aux --sort-%mem | head -n 10 ps -eo pid,ppid,stat,comm --sort-pcpu | head第一条是查某个服务进程是否存在第二条按内存占用排序看看是哪个进程在吃内存第三条按 CPU 排序适合找出谁在偷跑 CPU。如果要动态监控那就是top和htop的天下。top自带系统每次刷新全屏显示 CPU、内存和进程列表按P按 CPU 排序按M按内存排序按k可以输入 PID 杀进程。htop是增强版支持鼠标操作、树形视图、颜色高亮用起来更顺手建议新同学直接装一个htop。但生产环境不一定有 htop所以top的基础玩法必须练熟。5.2 查端口与进程8080 端口到底被谁占了查询 8080 端口进程这种需求你迟早会碰上。场景一般是你启动自己的服务报错 Address already in use或者你怀疑有未知服务在外网端口监听。解决思路三步走第一步用ss或netstat找出端口对应的进程 PIDss -tlnp | grep 8080 netstat -tunlp | grep 8080-t是 TCP-u是 UDP-l是监听状态-n不反解域名-p显示进程信息。ss是netstat的现代替代品输出更快建议以ss为主。第二步拿到 PID 之后用ps反查这个 PID 对应的进程名和启动命令ps -fp 23456 # 或者看 /proc 目录 ls -l /proc/23456/exe cat /proc/23456/cmdline/proc是 Linux 的一个虚拟文件系统每个运行中的进程都有一个/proc/PID目录exe软链接指向可执行文件cmdline存着完整的命令行参数用tr \0 把 NUL 分隔符转成空格就能看到完整的启动命令。这种排查比ps -ef更能看到隐藏的启动参数。第三步确定无误后用kill结束进程或kill -9强制杀了。这里有个现实提醒很多未知进程是他人部署的服务胡乱kill可能影响线上业务结束前先确认进程到底是什么、是不是自己负责的。5.3 杀进程为什么有的进程杀不掉、提示拒绝访问Windows 上资源监视进程显示已暂停且无法结束提示拒绝访问的热门问题在 Linux 里也有对应的杀不掉困境。Linux 下有两个常见原因让我拆开讲。第一进程处于不可中断睡眠 D 状态。它正在等内核态 I/O 完成此时任何信号包括 SIGKILL也就是kill -9都无法送达。你执行kill -9 12345会看到没有任何反应进程状态还是 D。这种情况最常见的元凶是 NFS 网络文件系统卡住、磁盘硬件故障或内核驱动 bug。没办法你只能等 I/O 恢复或者重启机器。生产环境中遇到 D 状态拖住进程第一选择是检查磁盘和网络存储的健康状况。第二进程权限高于你。如果你不是 root你只能杀你的进程杀掉 root 用户的进程会报Operation not permitted。这是内核在做权限校验时拒绝了信号传递。正确做法是sudo kill或者切换到对应用户去操作。很多新同学在普通用户下执行kill -9 进程发现杀不动第一反应是系统坏了其实只是权限问题。第三顺手提一个常见的坑kill -9并非永远优先选择。kill默认发 SIGTERM是让进程自己清理后退出kill -9是 SIGKILL内核强制终止进程没有任何机会保存状态或释放资源。线上服务能用 SIGTERM 就优先 SIGTERM实在不行再 SIGKILL。你可以理解为先给员工发辞退通知让他收拾东西走人而不是保安直接架出去。5.4 守护进程与 init 进程那些以 d 结尾的进程到底在干嘛看ps -ef时你会发现大量xxxd结尾的进程sshd、crond、rsyslogd、dockerd。它们就是常驻后台的守护进程。前文说过守护进程脱离了终端会话所以你在终端里敲命令时不会误发给它们信号它们的输出也不再写到终端上而是写到日志文件或系统日志journald。这其中init进程现在普遍是systemd是整个进程树的根PID 恒等于 1。它本身也是一个守护进程但它的地位是所有进程的终极父亲。维护系统时你看到 PID1 的进程命令是/sbin/init或/lib/systemd/systemd都很正常。如果你在容器里PID1 就变成了容器里的第一个进程比如你docker run nginx容器内 PID1 就是 nginx 的 master 进程。这带来一个经典问题容器里 PID1 讲收养孤儿、处理僵尸的职责做得不好时容器内就会堆满僵尸进程。我再多说一句 systemd 的理念。跟老的SysV init比起来systemd 不只是启动第一个进程它还能启动、停止、重启各种服务unit所以现代 Linux 上我们部署应用时几乎都推荐写一个 systemd service 文件[Unit] DescriptionMy Demo Service Afternetwork.target [Service] Typesimple ExecStart/usr/local/bin/demo_app Restarton-failure Userwww-data [Install] WantedBymulti-user.target把它放到/etc/systemd/system/mydemo.service然后systemctl daemon-reload、systemctl enable --now mydemo你的程序就跑成受 systemd 托管的守护进程了。它自动支持日志收集journalctl -u mydemo、开机自启、崩溃自动重启。这比你写一个nohup ... 然后担心关机丢进程要强悍太多。6. 真实排查日记僵尸进程、端口冲突和一个诡异的 Puppeteer 问题6.1 一个 Java 服务端口被占用的完整排查记录有一次部署一个 Java 应用启动时报错Web server failed to start. Port 8080 was already in use.当时服务器上有 N 个团队在共用大家用了同一条部署手册冲突是必然的。我当时的排查过程可以复现给你第一步定位监听该端口的进程ss -tlnp | grep 8080输出类似LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* users:((java,pid4321,fd83))。这里拿到了 PID 4321程序是 java。第二步进一步确认进程的启动命令。光知道是 java 还不够得知道它是哪个 jar 包、哪个项目ps -fp 4321 readlink -f /proc/4321/cwd cat /proc/4321/cmdline | tr \0 cwd软链接指向进程工作目录cmdline里一般能看到-jar xxx.jar之类的关键参数。确认下来是另一个团队的历史遗留服务已经没人维护了。第三步与负责人确认后使用优雅停止kill 4321然后等几秒再ss -tlnp | grep 8080确认端口已释放。这里强调一下不要一上来就kill -9Java 应用可能在停机钩子里做资源释放直接 SIGKILL 容易留下半写状态的配置文件或没刷新的 buffer。等个 5-10 秒确实没退再用kill -9。另外附送一个经验如果是反复开工单、谁也说不清楚 8080 该归谁的场景最好的出路是改端口。在 Spring Boot 里加一行server.port8180或者直接给每个服务分配独立端口段治标又治本。进程概念的底层问题——端口是全局资源——光靠抢回来是解决不了协作混乱的。6.2 僵尸进程排查线上报Resource temporarily unavailable还有一次压测环境出了问题应用日志里频繁报错java.io.IOException: Cannot run program sh: createProcess error2, Resource temporarily unavailable。一看就是进程创建失败但是为什么创建不了我第一反应是查进程数和 PID 上限ps -eLf | wc -l cat /proc/sys/kernel/pid_max进程数确实极其夸张有几万个。再用ps -eo stat | grep Z | wc -l统计僵尸进程数量好家伙上万只。当时情况是压测工具写了一个定时 fork 子进程但忘了 wait的 bug压测一跑僵尸盆满钵满。根因清楚了但产能还得救PID 已经耗干必须清理。清理思路有两条。先试的是直接对父进程下手找到那个压测工具的 java 进程kill掉它的主进程所有僵尸就被 systemd 收养systemd 会自动调用wait()清理掉PID 号陆续释放。等到/proc/sys/kernel/pid_max附近那些假性耗尽消失系统恢复正常。整个过程没有重启机器大概 1 分钟后就看到top里 zombie 数量稳定下降。这件事的教训是僵尸进程不是自动消失的必须父进程 wait而父进程要是不 wait任何外部手段都无法直接消灭僵尸。所以提前在代码里写好waitpid或使用concurrent.futures、multiprocessing.Pool这类帮你管理子进程生命周期的框架比事后抢救省太多事。6.3 Puppeteer 进程杀不干净的玄学其实是进程树没掌握有个做自动化测试的朋友遇到过每次跑完 Puppeteer 脚本机器上总会残留一堆 chrome 进程怎么kill都杀不完。他用pkill chrome杀一遍过一会儿又冒出来几个。这个问题的本质就是没把进程树概念用起来。Puppeteer 启动的是一个 headless Chrome 浏览器进程这个 Chrome 启动后又自己 fork 了渲染进程、GPU 进程、网络服务进程等一大窝。pkill chrome虽然杀掉了其中一部分但杀掉父进程后它的子进程变成了孤儿被 systemd 收养而不是跟着父进程陪葬。那些又冒出来的 chrome其实是原来就在的渲染进程只是你之前没杀到而已。正确做法是杀整个进程树而不是单个进程。用kill向进程组或会话里的所有进程发信号或者直接找到根父进程 PID然后pkill -TERM -P 父进程PID-P表示只匹配父 PID 为指定值的进程先杀子、再杀父一层层往下清。更省事的方案是在 Puppeteer 里配置args: [--no-zygote, --single-process]减少多进程结构或在脚本结束时手动browser.close()并launch时指定dumpio来观察子进程状态。说到底还是进程树关系没理顺导致的 杀不完 幻觉。6.4 常见问题速查表现象可能原因常用排查命令解决思路端口被占服务起不来已有进程监听该端口ss -tlnp | grep port确认进程归属kill 或改端口进程杀不掉kill -9 无效D 状态等待 I/O或权限不足ps -o stat,pid,cmd -p pid检查磁盘/NFS用 sudo kill系统新进程创建失败PID 号或内存耗尽ps -eLf | wc -l、free -m杀僵尸、找父进程 wait、检查内存top 里出现很多 Z 状态父进程没 wait 子进程ps -eo stat,pid,ppid,cmd | grep Z重启或杀掉父进程让 init 接管后台任务关终端后被杀进程没脱离子会话nohup或 systemd 服务使用 setsid 或写 service 文件进程占用 CPU 100%死循环或调度异常top按 P 排序perf top定位线程gdb 或 dump 分析6.5 一些从实战里长出来的心得体会说到最后分享几个我个人的经验。第一不要靠背命令来学进程。命令只是工具的皮真正值钱的是你知不知道这个进程从哪来、在等什么、要往哪去。你只要把进程树、状态机、通信方式这三大骨架搭起来任何命令都只是一本随时可以查的工具书。第二排查进程问题永远从这个进程是谁拉起来的开始。不管是 CPU 飚高还是端口被占先看ps -ef的 PPID 和/proc/PID里的信息父进程是谁顺藤摸瓜很少会查不到根因。很多安全事件、神秘挖矿进程都是靠这个思路一层层剥出来的。第三杀进程别急。除非矿机进程和恶意程序否则先用 SIGTERM 给进程一个善后的机会。数据库、消息队列这类有状态进程断电式强杀轻则丢数据重则需要长时间恢复。把kill当沟通手段而不是暴力工具你在生产环境的麻烦会少一半。第四进程数量的感觉一定要建立起来。一台 4 核机器合理进程数和暴涨进程数top一眼就该有数。如果ps aux一行都看半天说明你还没把进程概念变成直觉那就去找一台测试机开十个压力工具反复观察top、ps、ss的输出把各种状态都见过一遍直觉自然就来了。Linux 进程概念是一张网把进程树、状态、调度、IPC 串起来之后你会发现之前很多玄学问题本质都是这张网上某个结点出了故障。与其背一箩筐命令不如先把网搭起来。
返回列表