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

资讯详情

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

Linux进程全解析:从fork到僵尸进程,一文掌握核心概念

Linux进程全解析:从fork到僵尸进程,一文掌握核心概念 在Linux上待久了你会发现所有东西都可以归纳为两类文件和进程。文件是静态的进程是活的。写代码调试问题、排查服务器故障、面Linux岗位的试最绕不开的一个概念就是进程。今天我不打算按教科书那个讲法从头铺开背定义我按自己的理解和实战经验把进程这个概念从头到尾捋一遍——它是什么、怎么来的、怎么没的、怎么管理、怎么和别的进程打交道。这篇文章既适合刚学Linux的新手也适合准备面试、想系统性捋一遍进程知识的人。1. 先搞明白程序是一堆代码进程是一台运转中的机器1.1 程序和进程差在哪里程序和进程的区别是老生常谈但绝大多数人背了定义还是没感觉。我举个例子程序像一张菜谱写在纸上的时候搁在抽屉里十年也不会自己做菜进程就是把菜谱拿来、开火、放油、下锅炒整个操作过程中锅碗瓢盆的状态才是进程。你写好的C代码、Python脚本二进制文件静静躺在磁盘上那就是程序你把它跑起来内核为它分配内存、加载代码、记录它的CPU状态这时候它才变成进程。从这个角度看进程是“进行中的程序”它是动态的、有时刻变化的——有诞生、有状态变化、有死亡。而程序是静态的同一份程序可以同时运行出多个进程。比如你开了三个终端窗口每个窗口都执行bash那机器上就同时存在三个bash进程但它们对应的程序文件是同一个/bin/bash。这块如果你理解了后面看PID进程ID就不会把它当成神秘的东西它只是内核给每个进程发的一张编号牌。1.2 进程在内存里长什么样虚拟地址空间我自己最早学进程的时候最有用的一个视角是看它的“内存地图”。一个进程跑起来之后它不会直接操作物理内存而是拿到一块虚拟地址空间。这块空间从低地址到高地址依次分为几个区域代码段Text存放程序指令只读多个相同进程可以共享这一块。数据段Data/BSS存放全局变量和静态变量。堆Heap程序运行时动态分配的内存区往高地址生长。内存映射区mmap共享库、动态加载的文件映射。栈Stack局部变量、函数调用帧往低地址生长。为什么要搞虚拟地址空间直接说结论隔离和连续。隔离是指一个进程在地址空间里乱写哪怕写坏了也只会影响到自己这段虚拟内存不会把别人的数据踩烂连续是指你可以看到一个从0到一个很大的数之间连续的地址范围但从物理内存视角看这些页可能东一块西一块靠页表翻译把虚拟地址映射到物理页。你可以在/proc/PID/maps里直接看到某个进程的详细内存布局比如 cat /proc/$$/maps 就能看到当前shell进程的虚拟内存映射里面每一行对应一段区域。这个文件对排查内存问题非常有用不过新手先知道有这个东西即可。等你以后测内存泄漏、看共享库加载情况时会频繁跟它打交道。1.3 内核怎么管理进程task_struct每个进程在内核里都有一个对应的数据结构叫task_struct放在一个双向链表里它记录了进程的全部“档案”PID、状态、父进程PID、打开的fd表、内存描述符、信号处理函数表、CPU上下文等。可以把它理解成一张病案本内核靠它来掌握每个进程的一举一动。进程调度器为什么能决定谁先跑、谁能跑因为调度器读取task_struct里的状态、优先级、运行时间等信息按调度算法进行取舍。实操提示你会看到很多资料里说“进程描述符PCBProcess Control Block”在Linux里具体实现就是task_struct。所以有人问你“进程的实体由什么组成”标准答案是三块程序代码、数据集合、进程控制块PCB。代码和数据是运行需要的资源PCB是内核管理用的档案。到这里进程的“静态画像”已经出来了虚拟地址空间加上task_struct。接下来看它怎么动起来。2. 进程的一生创建、换装、退出2.1 fork复制出来的新进程Linux里创建进程最核心的接口是fork()。这个名字很有意思分叉。调用fork之后内核会把当前进程复制一份原来的叫父进程复制出来的叫子进程。复制的是task_struct、页表等元数据然后通过写时复制COW技术让父子进程一开始共享同一批物理内存页谁先写这块页内核才现场复制一份出来这样能省下大量内存拷贝。fork()的诡异之处在于调用一次却返回两次。父进程中fork()返回子进程的PID子进程中fork()返回0。所以程序员靠判断返回值来区分自己在哪一边。我贴一个最简单但够用的demo#include stdio.h #include unistd.h int main(void) { pid_t pid fork(); if (pid 0) { perror(fork failed); return 1; } if (pid 0) { printf(子进程PID%d父进程PID%d\n, getpid(), getppid()); } else { printf(父进程PID%d子进程PID%d\n, getpid(), pid); } return 0; }编译运行后你会看到两行输出一行的PID是另一行的父PID。这里有个关键点父子进程谁先跑不确定取决于调度器。所以如果你指望fork之后父进程和子进程的输出顺序一定不要依赖它这是并发编程的第一个坑。在我的实际测试中很多时候子进程先打印但这不是约定换个内核版本、换台机器就可能反过来。2.2 exec从一个程序变成另一个程序fork生出来的子进程一开始和父进程几乎一模一样代码段都相同。但我们大部分时候要的不是一个“copy的bash”而是让它去跑ls、跑python、跑nginx。这就轮到exec族函数登场execve()以及它的各种封装execl、execv、execle等。exec的原理是把当前进程的代码段、数据段、堆、栈整体替换成新程序的映像但PID不变task_struct多数字段还保留。也就是说进程的“壳”没变里面“换芯”了。终端里你敲一个lsshell做的事情就是fork出一个子进程再在子进程里exec /usr/bin/ls这二者几乎总是配套出现所以行话叫“fork exec”。有一个特别直观的例子你执行 systemctl restart nginx旧nginx进程退出新nginx进程是全新的PID但你打开一个新的bash窗口能不断跑各种外部命令bash自己的PID从头到尾不变因为它只负责forkexec而自身一直运行着。理解这个你也就明白为什么很多守护进程的启动方式都是父进程forkexec出子进程然后父进程退出或者继续当接收信号的中转站。2.3 exit和wait进程的善终与僵尸进程进程正常结束会调用exit()或者在main函数里return。退出时内核会回收它的大部分资源内存页、打开的fd、锁等都被清理。但它不会立刻从进程表里消失——它给父进程留下一个“收尸”的任务父进程需要调用wait()或waitpid()来读取子进程的退出状态。如果父进程一直没有调用wait会发生什么子进程虽然已经“死”了task_struct还没被回收进程表里还挂着它状态显示为Zzombie僵尸。僵尸进程不占CPU也不占内存除了一点task_struct但它的PID一直占着名额大量堆积会导致系统无法创建新进程。这就像离职员工一直不收尾工牌和工位还占着一样。实操中我自己最常见的僵尸进程来源有两个一是写C/Go程序时fork之后忘了在父进程里wait二是父进程一直忙着不做wait子进程退出后立刻变僵尸。日志、中间件程序里偶尔看见几个僵尸处理方式很简单——找到僵尸进程的父进程让它收尸或者把它重启一遍。2.4 收养机制孤儿进程的去向还有一种比僵尸好点的“倒霉孩子”孤儿进程。当父进程先退出子进程还活着它就变成了孤儿。孤儿不会没人管内核会把它的父进程改成PID1的进程也就是init或systemd由它来收养并负责最终的收尸。所以孤儿进程最终一般都能正常结束不会被丢弃在进程表里。要注意的是孤儿进程和僵尸进程是两个完全不同的场景父子都正常的情况下父等子任务是wait父死了子系统改认1号进程当干爹干爹会定期wait所有被收养的子进程父活着却不管、子先死了才变成僵尸。反正记住一条子进程退出后的清理责任在父进程没人清理就变僵尸清理责任转移给1号进程后就能避免长期滞留。3. 进程状态详解与调度3.1 Linux下的进程状态查看进程状态最常用的命令是ps。例如 ps -l 显示STAT列top里的S列也是状态。Linux下常见的有Rrunning正在运行或在运行队列里等待调度。Ssleeping可中断睡眠等待某个事件比如等I/O、等键盘输入能被信号唤醒。Duninterruptible sleep不可中断睡眠通常在等待磁盘I/O等硬件层操作信号也打断不了。Tstopped停止状态进程收到SIGSTOP/SIGTSTP会进入CtrlZ就能把前台进程弄进去。Zzombie僵尸。Iidle空闲内核线程。很多人第一次看到D状态会蒙进程卡住了kill -9都杀不掉。这其实是它在等内核态的一块I/O完成内核认为此时不能强行打断否则数据会出问题。这种进程如果长时间卡在D状态通常意味着底层磁盘或者NFS挂了处理思路是排查存储而不是干瞪眼。有一个很经典的排障场景NFS挂载目录失联所有访问这个目录的进程全部进D状态怎么kill都没反应这时候只能先把NFS挂载点恢复或者强制卸载进程才会被释放。3.2 僵尸进程的清理实战判断系统有没有僵尸进程一行命令就行ps aux | awk /\[Z]/ {print $8, $2, $11}或者直接看top第一行的zombie计数。清理僵尸正确顺序是找到僵尸进程的父进程ps -o ppid -p 僵尸PID。看父进程是什么是正常业务进程就发SIGCHLD信号试试或者重启它。如果父进程本身就是1号进程且有大量僵尸那多半是某些服务起了很多子进程又不好好wait优先处理那个服务。我踩过的坑早期排查僵尸我直接用kill -9杀僵尸PID结果发现根本杀不掉。僵尸已经死了kill对它是无效的要处理的是它活着的父进程。所以以后看到一堆Z先别急着把僵尸当敌人找到父进程才是正解。3.3 进程调度与优先级进程拿到CPU的规则由调度器决定。Linux普通进程采用完全公平调度算法CFS核心思想是尽量让每个进程获得相对公平的CPU时间。每个进程有nice值范围-20到19默认0。nice值越小优先级越高但不绝对——因为CFS关注的是权重比例而不是简单按nice值排队。调整优先级nice -n -5 ./your_program # 以更低nice值启动程序 renice -n -5 -p 12345 # 调整已有进程12345的nice值在top界面里你可以按r键实时调整某个进程的nice值。日常排障时如果发现某个批处理任务把CPU吃满导致线上服务抖动可以renice提高它的nice值让调度器更偏向其他进程。这个方法我试过很多次比直接kill任务要安全得多。当然一般建议普通用户只调低优先级增大nice值调高优先级需要root权限。4. 进程管理实操命令行与工具4.1 查进程ps、pgrep、pidof、top命令行查进程ps是最常用的。我日常用这两条ps -ef | grep xxx # 查看进程及父进程、完整命令行 ps aux | grep xxx # 看CPU、内存、启动时间、状态如果你只想知道某个进程的PID用pidof最直接。pidof nginx pidof -x 脚本名再复杂点的按命令行关键字匹配用pgreppgrep -f python.*app.py它会返回匹配进程的PID配合pkill -f可以按完整命令行匹配杀进程。这个组合在清理一堆脚本进程时非常好使。比如程序起了多个worker进程你想全清掉又不想一个个数PID直接 pkill -f worker.py 就能按命令行匹配全部干掉。top则适合交互式观察进去后按P按CPU排序、按M按内存排序、按t看CPU时间汇总。还有一个htop交互体验更好能看到树形关系和每核负载生产环境没有htop的机器我就用ps加awk顶一下也够用。反正命令是工具能解决问题就行不追求花哨。4.2 杀进程的正确姿势信号机制kill这个命令本质不是“杀”它是给进程发信号。查看所有信号用 kill -l常用的几个SIGTERM15默认信号请求进程自己退出允许它做清理最温和。SIGKILL9强制杀死进程没机会清理不响应、不配合只能留给内核处理。SIGHUP1挂断信号终端关闭时发给前台进程组常被daemon用于重载配置。SIGSTOP19暂停进程。SIGCONT18继续运行。所以杀进程应该先用15等几秒再升级9。直接kill -9容易导致数据损坏或留下缺少清理的临时文件。我自己排障时99%的场景是先用kill PID不行再kill -9 PID。比如Java进程优雅停机时Spring Boot会注册SIGTERM处理器把线程池、数据库连接都关掉你直接9上去连接可能没来得及释放重启后一大堆TIME_WAIT。关于“进程杀不掉”的常见情况进程在D状态不是信号能解决的排查磁盘/存储。内核线程你是普通用户的话根本没有权限ps里那些加方括号的[kworker/0:1]就是内核线程通常不用杀。权限不够用sudo确认PID没弄错。4.3 排查资源占用服务器卡了最要紧的是找出谁在抢CPU和内存。我常用的三板斧top -b -n 1 | head -30 # 一次性输出当前占用排行 ps aux --sort-%cpu | head -10 # 按CPU排序 ps aux --sort-%mem | head -10 # 按内存排序如果怀疑某个进程的网络占用高比如有人问“centos怎么看进程的网络占用”Linux原生命令不好直接看“每个进程的网速”但有组合思路nethogs可以按进程显示流量ss -tnp能看出某个连接属于哪个进程iotop能看每个进程的磁盘I/O。这三件套覆盖了CPU、内存、网络、I/O四类资源。排查高CPU进程时很多运维老手会顺手执行 top -Hp PID 看线程级占用这在Java应用里特别有用能看到具体是哪个线程在疯狂转再配合jstack抓线程栈定位代码问题。5. 进程和线程到底什么关系5.1 线程的本质轻量级进程很多新手问进程和线程的区别教科书上会讲“进程是资源分配的最小单位线程是CPU调度的最小单位”但这句太抽象。放到Linux源码视角看线程其实是通过clone()创建出来的和fork的不同在于线程之间共享虚拟内存空间mm_struct和文件描述符表而进程之间是隔离的。所以可以认为Linux里并不存在一个独立的“线程”实体线程就是以一种特殊方式创建的进程叫轻量级进程LWP。正因为线程共享内存空间同一个进程内的多个线程天然可以访问同一份全局变量通信开销很小但也因此产生了竞争条件、锁、死锁这些东西。比如你用pthread创建四个线程它们各自有独立的栈、寄存器、线程局部存储TLS但堆和全局数据是公用的。这就好比你开了四间办公室办公桌各自独立但卫生间、水房都共用——高效是高效排队打架的事也免不了。5.2 多进程和多线程怎么选这不是非黑即白核心看你的需求需要高隔离性一个模块崩了不能拖垮全局选多进程比如浏览器的多进程架构、Nginx的worker进程。需要高并发和低通信开销模块间频繁共享数据选多线程比如Web应用的线程池。计算密集型且有大量内存共享需求多线程更省资源。稳定性第一某些任务允许开独立进程隔离多进程更踏实。我个人的经验是拿不准的时候先想“崩溃容忍度”。比如我自己写后台任务早期图方便全部上线程一个线程崩了整个进程都带崩。后来改成思路清晰的子进程隔离排障容易多了。当然写线程代码时小心锁和死锁也是一门硬功夫但如果概念没理顺多线程简直是灾难制造机。5.3 面试必问的对比放到一张表里更直观维度进程线程资源独立地址空间开销大共享地址空间开销小通信需要IPC机制管道、共享内存等直接读写共享变量稳定性一个进程崩溃基本不影响其他进程一个线程崩溃段错误可能导致整个进程退出创建开销较大较小调度单位进程本身的调度也按线程处理线程是调度器看到的单位适用场景强隔离、任务互不干扰高并发、共享数据频繁、低延迟注意第5行在Linux里调度器调度的其实是线程。也可以说进程是一组线程的容器。这样再去理解“Nginx每个worker是两个线程”这类描述就不会迷糊了。再多说一句面试时如果被问到“进程和线程谁更快”不要条件反射回答“线程快”要分场景线程创建和切换快但共享数据需要锁锁竞争反而会拖慢速度进程隔离性强数据复制或IPC开销可能更大。把权衡讲清楚比背结论更能打动面试官。6. 进程间通信IPC概念入门6.1 管道和信号进程之间地址空间是隔离的要用数据交换必须走IPC。最简单的是管道pipe终端里的竖线就是一个匿名管道比如 ps aux | grep nginx前一个进程的标准输出接到后一个进程的标准输入。整个过程在内核里有一块缓冲管道两个进程通过文件描述符读写它。因为简单高效日志过滤、文本处理里全是它的身影。信号则是异步通知机制。前面说的kill就是在发信号进程可以先注册信号处理函数收到SIGUSR1之类的信号时做自定义动作。很多系统里用SIGHUP让守护进程重新加载配置比如修改nginx.conf之后执行 kill -HUP $(cat /var/run/nginx.pid)进程不重启但配置热更新。信号的好处是轻量缺点是不适合传大量数据它更多用于“通知”而不是“传输”。6.2 共享内存、消息队列、套接字信号量、共享内存、消息队列这三大件是经典的System V IPC。共享内存是最快的IPC方式多个进程通过内核映射同一块物理内存数据直接读写零拷贝但你得自己处理同步问题。信号量Semaphore用来解决互斥与同步比如多个进程同时往共享内存写数据必须加信号量保护。消息队列则是以消息为单位传递数据不用像管道那样必须同步等待但要考虑消息大小限制。套接字socket不仅是网络编程的主要手段也可以用于本机进程通信Unix domain socket很多数据库、容器运行时都用它做本地通信性能比走TCP回环更高。我查资料时见过不少高性能中间件本地IPC都优先选Unix domain socket因为不走网络协议栈延迟更低。对于概念篇来说知道这些分类和各自适用场景就够了真到写代码时再针对每一种IPC深入。面试时被问“进程间有哪些通信方式”我一般按这个顺序答管道、消息队列、共享内存、信号量、信号、套接字并简单说出各自优缺点。7. 常见问题排查与面试考点7.1 终端与进程的迷之问题“终端进程启动失败本机异常无法启动conpty已移除winpty”这类问题在Windows下的WSL或VSCode终端里见过。它的本质是终端前端VSCode/Windows Terminal和后端shell之间的pty/console层出了问题conpty是Windows的伪终端实现winpty是老牌的第三方方案。常见解法重启VSCode/终端进程、清理缓存、更新WSL内核或者把默认集成终端改成外部终端先顶着。这类问题不是Linux本身的进程概念问题但排查“后台有进程、窗口打不开”的思路是相通的先看进程是否活着再看它依赖的终端通道是否正常。还有“chatgpt桌面端启动之后只有进程没有窗口”“codex点击没反应但后台有进程”这类GUI应用的问题基本都是图形环境变量DISPLAY/WAYLAND_DISPLAY、GPU、单实例锁冲突。处理时先看进程状态ps aux | grep chatgpt # 如果进程活着但没窗口多半是显示服务或显卡问题这类问题的通用排查思路是进程有没有真的起来、它的日志在哪个目录、有没有隔离实例锁这些信息能帮你快速定位。我曾经遇到一个electron应用双击没反应但ps能看到进程最后发现是$HOME/.config/AppName下面的SingletonLock文件残留删掉就正常了。7.2 dpkg锁和apt锁热搜里有个经典问题dpkg: 错误: 另外一个进程已经为 dpkg 前端锁 加锁。这通常是因为有另一个apt/dpkg进程还在跑或者上次被中断了锁文件没释放。排查和处理ps aux | grep -E apt|dpkg找到残留进程就kill掉然后删除锁文件不过删除锁文件前要确认没有其他安装进程在跑否则可能搞坏包管理。另外第一优先级推荐直接等待因为apt虽然慢最终会自己完成贸然删锁可能有风险。尤其是服务器上有其他人也在操作时你删锁打得火热对面可能正装了一半两败俱伤。先沟通、再处理是运维的基本素养。7.3 面试考点速查结合话题我整理了高频面试问题什么是进程进程和程序的区别是什么进程有哪些状态什么是僵尸进程怎么产生、怎么解决什么是孤儿进程会被谁收养fork返回什么为什么fork之后父子进程要区分进程和线程的区别各自优缺点Linux进程间通信方式有哪些如何查看某个进程占用的CPU内存如何查找僵尸进程回答这八道题这篇文章里基本都有素材。备考时自己讲一遍比背题更有效。特别提醒一下面试官追问“僵尸进程和孤儿进程的区别”时很多新手会答反记住“僵尸是死了没人收尸孤儿是活着但亲爹跑了”这两个场景完全不同但很多人一紧张就混。我自己从刚学Linux到现在回头看进程这个概念最大的体会是它不是一个背诵的知识点而是一套观察系统的视角。遇到任何奇怪的系统问题第一反应先ps看状态再看它和谁有关、依赖什么、处于什么状态很多问题就迎刃而解了。这篇文章的篇幅不算短但我希望你记住的核心就一句话程序是死的进程是活的进程有生、有状态、有通信、有死亡管理好它你才算真正开始操作Linux。
返回列表