ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux进程管理全解:从原理、状态到排障实战

Linux进程管理全解:从原理、状态到排障实战 我第一次真正被Linux进程管理“教训”到是刚接手一台线上服务器不到两周的时候。那台机器跑着Nginx和两个Java服务本来很稳突然某天下午load average飙到30多CPU排满了。我第一反应是重启大法但同事拉住了我让我先看看进程列表。那之后我才意识到以前学的“Linux常用命令”和真正的操作系统进程管理之间差着好几个彻夜排查的经历。这篇文章我想把Linux操作系统中关于进程管理的那套东西从原理到命令再到实战排障一次性讲透。适合刚入门Linux的小白也给已经用了一段时间、但总觉得进程这块隔着一层纱的朋友一个梳理。1. 先搞清楚进程在Linux里到底是什么样的存在很多教程一上来就让你敲ps、top但进程到底是什么、从哪里来、到哪里去这些背景不清楚的话命令敲得再顺也只是皮毛。我见过不少同事会用ps查PID但遇到进程杀不掉、状态卡住、系统变慢时就完全没了方向。问题就出在对进程这个概念本身理解得不够。1.1 进程、程序、线程别再把它们混为一谈程序是一个静态的文件比如/usr/bin/nginx。它躺在磁盘上不占CPU也不吃内存本质上就是一堆二进制指令和数据。进程是程序被加载到内存后开始运行的动态实体它有PID、有独立的内存空间、有打开的文件描述符、有自己的状态。进程每一次切换、每一次系统调用都会留下痕迹。线程则是在进程内部的更小执行单元。同一个进程里的多个线程共享内存空间、文件描述符等资源但各自有独立的栈和寄存器上下文Linux底层用LWP轻量级进程来描述它们。你可以简单理解为程序是剧本进程是把剧本搬上舞台的整场演出线程则是台上的一个个演员。这个区别在排障时非常重要。比如你用ps看到Tomcat的PID是12345但Tomcat内部可能跑着几十个线程。当CPU高时ps显示的是进程整体占用而你真正需要定位的是哪个线程在捣乱。这时候要用top的H键或者ps -Lf把线程拆出来看。很多人卡在“进程状态正常但CPU高”这个困惑里就是因为没意识到线程这一层。1.2 PID、PPID和进程树Linux怎么为几十万个进程“排座次”每个进程都有唯一编号PID还有父进程编号PPID。Linux系统从内核启动后第一个用户态进程通常是systemd早期是init它的PID一般是1。所有其他进程都直接或间接由它fork出来最终形成一棵进程树。这棵树的形态不是摆设。它决定了进程间的依赖关系、信号传递的路径也决定了进程退出后“孤儿”会被谁接管。比如你启动一个shell脚本脚本里再启动一个Java进程那么Java进程的PPID就是脚本进程。如果脚本退出Java进程不会自动消失它会被挂到systemd或init名下变成由PID 1收养的“孤儿进程”。我遇到过一个问题明明用kill杀掉了某个服务的进程但端口还占着。后来一查才发现真正持有端口的是这个服务的子进程。父进程被杀后子进程成了孤儿继续存活资源一点没释放。所以排查端口占用时不能只看单个PID要用pstree -ap看完整的进程树才能搞清楚谁是谁的爹、谁是谁的儿。1.3 “一切皆文件”在进程上也成立Linux有一句经典的话叫“一切皆文件”进程也不例外。每个进程在/proc目录下都有一个以PID命名的子目录比如/proc/12345/。进去看看里面有cmdline启动命令、environ环境变量、fd打开的文件描述符、status状态还有cwd当前工作目录等。很多常用的进程管理命令本质上只是把/proc里的内容做了格式化输出而已。比如你想知道一个进程的工作目录ps里不直接显示但可以读/proc/12345/cwd这个软链接。想知道它当前打开了哪些文件可以看/proc/12345/fd/。这个能力在进程进入异常状态时特别有用。后面讲D状态和Z状态时我们还会用到/proc里的status文件。2. ps与top把进程信息看出门道的两条指令2.1 ps aux 和 ps -ef到底选哪个这是Linux学习群里被问烂的问题。ps aux来自BSD风格ps -ef来自System V风格。两者默认显示的信息有细微差异但绝大多数场景下都能满足需求。我个人的习惯是要看完整命令带参数用ps aux要快速看父子关系用ps -ef因为它有PPID列。一个容易被忽略的要点是ps aux里的PID下面是%CPU和%MEM这里的CPU占用率是进程自启动以来的平均占用不是瞬时值。所以当你说“CPU高”时用ps判断其实不够准确因为它反映的是历史累积。真正的瞬时负载得靠top、htop或者pidstat来看。我平时最常用的命令是ps aux --sort-%cpu | head -20这条命令的好处是把占用CPU最高的20个进程排在最前面一眼就能看出谁在“吃”资源。如果还想看某个进程下所有线程可以加-L参数比如ps -Lf -p 123452.2 top的交互操作和关键指标top是Linux管理员绕不开的工具。启动后上半部分是系统概览下半部分是进程列表。最上面几行信息有很有讲究load average后面的三个数字分别代表1分钟、5分钟、15分钟的平均负载。很多人以为这个数字高就是CPU忙其实它统计的是处于可运行状态和不可中断状态的进程数量。如果只有两个CPU核心负载到了4.0意味着平均每个核上有两个进程在排队。进入top后有几个按键我强烈建议记住P按CPU使用率排序M按内存使用率排序H切换是否显示线程k杀掉指定PID的进程r调整进程优先级renice我最常用的场景是先用P排序找到异常进程再按H看它内部的线程找到具体的线程PID。然后用top -Hp 单独监控这个进程的线程情况。这个过程在定位Java或Python后端服务CPU飙高时几乎是标准动作。2.3 关键字段别只看个热闹很多人看top的进程列表只知道看%CPU和%MEM但其他字段关键时刻很有用。这里整理一张表字段含义我的使用场景PID进程ID定位、killUSER启动进程的用户判断是不是被入侵的临时用户启动PR内核调度优先级查看进程优先级NInice值越低越优先调整优先级时使用VIRT虚拟内存总量判断程序是否申请了过多虚存RES常驻物理内存判断真实内存占用SHR共享内存大小分析是否过度共享S进程状态判断R、D、S、Z等状态TIME累计CPU时间长时间占用CPU的进程这个值会很大COMMAND命令名结合完整参数分析比如VIRT这个字段经常吓到新人。Java程序一启动VIRT可能几十GB但RES可能只有1GB。这是因为Java虚拟机预占了大量虚拟地址空间不代表真用了那么多物理内存。判断内存是否紧张更该看RES和系统的free / available。2.4 实操案例用top找到CPU异常进程一次我负责的测试服务器突然响应缓慢。登录后先运行top看到有个进程名叫“php-fpm”的CPU占用超过200%多核情况下可以超过100%。我按H展开线程发现是php-fpm的某个worker线程异常。再用strace -p 跟踪这个线程的系统调用发现它卡在了一个网络请求的读取操作上不断重试。当时我没有急着重启而是先看它的启动时间和父进程确认是不是前天上线的一个定时任务脚本残留。顺着进程树找到父进程发现是cron任务启动后没有正确退出。最后kill掉异常worker再修复了脚本里的超时配置问题才根治。如果一开始无脑重启php-fpm可能当时恢复了但下一个周期又会复发。3. R/S/D/Z读懂进程状态字母背后的含义3.1 状态字母总览ps和top里进程状态那一列往往是一两个字母。我见过不少新手只认识R和S看到D和Z就懵了。其实每一个字母都代表进程在某个“等待”中的位置直接反映了系统面临的瓶颈。状态含义常见场景Rrunning或runnable正在运行或等待CPU调度CPU密集型计算、大量进程竞争CPUSsleeping可中断睡眠等待某个事件完成大多数服务进程处于此状态Ddisk sleep不可中断睡眠不可被信号中断正在等待磁盘IO返回常见于NFS卡死Zzombie僵尸进程已退出但未被父进程回收父进程没有正确调用waitTstopped被暂停通常是被SIGSTOP或CtrlZ调试场景、任务挂起ttracing stop被调试器暂停gdb调试时很常见R和S的区别经常被误解。R不代表进程一定在占用CPU它只是说明进程在运行队列里等待调度。如果系统上同时有几百个R状态进程它们都会排队但单个进程的CPU%可能不高。S才是大多数服务进程的常态它们在等待网络包、等待磁盘、等待计时器一旦事件到达内核会唤醒它们。3.2 Z状态是怎么产生的为什么kill不掉僵尸进程是Linux进程管理里最经典的坑。当一个子进程退出后它会向父进程发送SIGCHLD信号父进程需要调用wait/waitpid系统调用去读取子进程的退出状态。如果父进程没有处理子进程的task_struct结构就无法从内核中彻底释放变成Zombie状态。僵尸进程既不占CPU也不占内存但它会在进程表中保留一个条目。如果数量不多影响不大但如果成千上万的僵尸进程堆积PID会被耗尽新的进程无法创建系统就会慢慢“失去响应”。最让人头疼的是僵尸进程根本杀不死因为严格来说它已经“死”了kill任何信号都无效。唯一正确的处理方式是处理它的父进程。如果父进程还活着让父进程结束或重启即可如果父进程已经退出僵尸进程会被init/systemd接管由系统自动清理。所以排查僵尸进程时第一步永远是看PPID是谁然后决定怎么处理。我用一个实际案例说明某次测试环境里跑着一个遗留的Python脚本它不断用subprocess调用子进程但不回收。几天后ps命令里出现了几百个Z状态的“python”进程。我用ps -o pid,ppid,stat,cmd查了一下确认父进程是那个Python脚本。重启脚本后所有僵尸进程被systemd接管并清理系统恢复正常。3.3 D状态比Z更让人头疼的“沉睡”D状态的全称是Uninterruptible Sleep不可中断睡眠。进程在等待一个不能被信号打断的IO操作完成最常见的就是磁盘读写和NFS网络文件系统。这时你向它发SIGKILL信号会被内核挂起进程就是不退出。D状态出现一两个是正常的系统负载高时偶尔会有闪烁。但如果大量进程长时间处于D状态基本可以判定IO出了问题。我遇到过一次真实的D状态风暴服务器mount了一个NFS共享目录因为网络中断所有访问该目录的进程全部进入D状态整个系统负载飙到几十连登录都卡。这种时候kill是没有用的因为信号无法被处理。正确流程是先恢复底层IO问题比如修复网络、恢复存储。如果NFS已经永久不可用强制卸载umount -f后D状态的进程才会收到IO错误返回然后被唤醒。最后再清理个别残留进程。需要注意的是强制umount也可能卡死有时候要重启机器才能彻底恢复。所以生产环境用NFS一定要设计好挂载超时和重试策略别让一个网络抖动把整机拖垮。3.4 用/proc/PID/status深入查看进程状态ps和top显示的STAT字母其实是通过/proc/ /status里的State字段得到的。但后者信息更细还有一些额外字段可以帮助判断问题。cat /proc/12345/status | grep -E State|PPid|Voluntary|NonvoluntaryState那一行会显示类似“State: R (running)”。还可以看Voluntary_ctxt_switches和Nonvoluntary_ctxt_switches即进程主动和被动切换上下文的次数。如果Nonvoluntary很多说明进程经常被抢占CPU资源竞争激烈如果Voluntary很多说明进程频繁进入阻塞等待可能在大量IO操作。这些信息在性能调优时很有参考价值日常排障不一定用得上但知道到哪里看底层数据会让你比那些只会敲几条命令的“脚本玩家”更深一层。4. 进程的一生从fork到exec再到退出与回收4.1 fork、exec、wait三兄弟的角色Linux创建进程的方式很有意思几乎所有的进程都是通过fork系统调用复制父进程得到的。fork会创建一个几乎完全相同的子进程但子进程有自己的PID并且fork在父进程和子进程中返回不同的值——父进程得到的是子进程PID子进程得到的是0。fork之后如果子进程想要运行一个完全不同的程序比如从shell里启动ls它会调用exec系统调用用新的程序镜像替换掉当前进程的内存映像。这就是“forkexec”组合先复制再替换。而shell要等命令执行完毕就需要调用wait系统调用阻塞等待子进程退出并获取其退出码。理解这个过程你就会明白为什么Linux进程管理里“父进程”如此重要。父进程如果不wait子进程退出后就会变成僵尸进程。而父进程如果先退出子进程就成了孤儿被根进程收养。这些都是同一条生命周期线上不同节点的结果。4.2 孤儿进程与守护进程脱离终端后怎么活守护进程daemon是一类后台运行、不依赖终端、生命周期通常贯穿系统整个运行时间的进程。比如sshd、nginx、crond都是守护进程。它们的创建方式与我们手动执行的普通进程有本质区别。一个普通进程如果绑定了当前终端的控制终端当终端关闭时系统会向进程发送SIGHUP信号很多进程默认会因此退出。这也就是为什么ssh登录服务器后直接跑一个服务退出登录后服务就挂掉的原因。要让进程脱离终端存活常用方法是nohup、setsid或systemd方式启动。nohup的作用是忽略SIGHUP信号而setsid是让进程创建一个新的会话彻底脱离控制终端。实际工作中我更喜欢用systemd来管理长期运行的服务因为它能提供开机自启、崩溃重启、日志收集等一整套能力后面会细讲。4.3 systemd与孤儿进程的收养逻辑现代Linux发行版几乎都使用systemd作为init系统。systemd的PID是1是所有进程的“终极父进程”。当一个进程的父进程退出后这个孤儿进程会被重新挂到systemd名下由systemd负责回收。这个机制也有一个副作用。如果你在某个服务里启动了一个孙进程而服务本身退出后没有清理孙进程它就会被systemd收养变成看似“来路不明”的进程。查的时候PPID是1根本看不出来是谁启动的。我在排查一台机器时就发现有大量PHP进程的PPID是1一开始以为是被入侵了后来发现是之前跑的旧版队列脚本用完没有回收。所以建议大家写脚本启动后台任务时一定要先写好进程清理逻辑排查进程时也不要只盯着PPID为1就觉得“不是自己人”。4.4 进程间通讯管道、信号与共享内存的最小认知进程之间免不了要通信。最简单的是管道比如ps aux | grep nginx前一个进程的标准输出作为后一个进程的标准输入。这种“接力”式通信在命令行里非常常见。信号则是操作系统通知进程“发生了某个事件”的一种机制。我们常用的kill命令本质就是向进程发送信号。比如kill -TERM 请求进程正常退出kill -KILL 强制杀死进程kill -STOP 暂停进程kill -CONT 继续运行暂停的进程在运维中我最常踩的坑是kill -9用得太多太随意。其实应该先给进程一个优雅退出的机会让它保存状态、清理临时文件。对Java服务来说直接kill -9可能导致数据不一致。现在我会默认先kill -TERM等十几秒没退出再考虑kill -9。除此之外Linux还有共享内存、消息队列、信号量等System V IPC机制以及socket通信。它们不在本篇文章展开但当你看到ipcs命令输出时至少要知道那是系统级进程间通信的状态而不是什么神秘数据。5. 让进程按预期运行前后台、优先级与资源限制5.1 前台、后台与nohup的边界在终端里直接执行一个脚本时命令会占用当前shell直到运行结束。这就是前台进程。如果命令运行时间很长你可以按CtrlZ把它暂停然后输入bg让它转后台继续跑也可以直接启动时加符号让它一上来就在后台运行。但是这种“后台”只是相对于当前shell而言的它仍然绑定了终端会话。一旦你退出sshshell就会向它发送SIGHUP进程可能随之退出。想要真正脱离终端可以用nohup long-running-command nohup会忽略SIGHUP信号并且默认把输出写入nohup.out。更稳妥的写法是同时指定输出重定向nohup ./start.sh /tmp/app.log 21 注意别把nohup的输出留在当前目录否则nohup.out会越攒越大占满磁盘。我见过好几次磁盘满的故障最后查出来都是历史遗留的nohup.out。5.2 nice与renice调度优先级的实际用法Linux内核调度器给每个进程分配一个nice值范围是-20到19。nice值越低表示“越友好”的优先级越高能获得更多CPU时间。普通用户只能把nice值调高降低优先级root用户才能调低提升优先级。启动时指定优先级用nicenice -n -5 ./heavy-task.sh对运行中的进程调整优先级用renicerenice -n 10 -p 12345我通常只在两种场景下用renice一是“不希望某个CPU密集任务把线上服务拖死”给它一个较高nice值让它礼让别人二是“某个关键批处理任务必须优先跑完”在root权限下把nice值调低。日常开发中不建议随便调弄反了反而会让系统响应恶化。5.3 ulimit与systemd资源限制进程除了受CPU调度影响还会受到资源限制。bash内置命令ulimit -a可以查看当前会话的资源限制比如open files进程能打开的最大文件数max user processes同一用户能创建的最大进程数core file size核心转储文件大小address space虚拟内存上限Java服务经常遇到的Too many open files问题就是ulimit -n设置太小。临时调高可以用ulimit -n 65535但更规范的做法是在systemd服务文件里配置[Service] LimitNOFILE65535 LimitNPROC4096配置后执行systemctl daemon-reload然后重启服务。这样即使服务被systemd拉起也能保证资源限制生效。很多生产事故都源于“手动启动时正常systemd启动时崩溃”就是因为忘了在service文件里同步设置limits。5.4 实际场景部署Java进程时如何管理以部署一个Spring Boot应用为例我现在的标准流程是写一个systemd service文件放在/etc/systemd/system/app.service。在ExecStart里用java -Xms512m -Xmx1024m -jar /opt/app/app.jar。设置LimitNOFILE保证并发连接数。使用systemctl enable --now app实现开机自启和立即启动。查看日志用journalctl -u app -f。这样做比nohup java -jar明显多几个好处进程崩溃后systemd能自动重启开机自动拉起日志归一到journal还能用systemctl status app随时查看状态。对于正式环境我强烈建议放弃nohup方案。6. 进程管理排障实战一次CPU飙升的排查全过程6.1 问题现象与初始判断某天下午监控告警说一台4核8G的云主机CPU使用率持续超过90%。登录机器后我第一件事不是照搬网上的“top三连”而是先确认这是CPU密集问题还是负载均衡问题。运行uptime看到load average是5.80, 4.30, 3.20说明1分钟负载明显高于15分钟这是刚刚开始恶化的趋势。然后运行top发现一个名为“worker-bin”的进程占用了约350%的CPU。这个进程名太泛了像是某个框架生成的临时进程名。我没有直接kill因为要先搞清它是什么、谁启动的否则可能杀了业务进程。6.2 完整排查链路top到pidstat到strace我的排查链路通常是这样的先看进程详情ps -ef | grep worker-bin ls -l /proc/12345/exe第一条命令能看到启动参数和PPID第二条命令能定位可执行文件的真实路径。当时我看到这个进程的PPID是1启动命令里带了一个诡异的数据目录参数执行文件路径在/tmp下。到这一步我已经高度怀疑它不是正常业务程序。接着用pidstat看它在CPU时间片上的分布pidstat -p 12345 1 5确认它每个采样周期都在消耗CPU。然后用strace跟踪系统调用strace -p 12345 -e tracenetwork,file,process很快看到它反复连接一个外部IP地址的某个高端口并且在尝试读写临时目录下的可疑脚本。到这一步基本可以定性这台机器被植入了攻击性挖矿进程而且伪装成了worker-bin。6.3 处理步骤与复盘处理这类问题第一步是断网止损把云主机的安全组改为只允许运维IP访问避免攻击者继续下发指令。然后用kill -9结束可疑进程。查看crontab -l清理掉定时任务里下载脚本的条目。检查/tmp目录下的脚本文件并删除。用lsof -i检查是否有其他隐藏连接。修改服务器密码和相关密钥。事后复盘时我发现入侵入口是某台测试机上暴露的弱口令Redis服务。所以进程管理排障不只是处理一个进程更要顺着进程链路找到入口把后门清干净。从那以后我的服务器默认开启防火墙所有对外服务都做了访问控制。6.4 防患未然监控与巡检等故障处理完我才意识到之前的监控只看了CPU和内存根本没有进程级别的监控。后来我给关键服务器加了如下内容定期巡检写一个脚本每天检查/tmp目录下的可执行文件、crontab里新增的任务。进程异常监控对关键服务进程做单独的systemctl status检查失败自动告警。资源限制对所有面向外部的进程设置systemd资源限制防止单进程耗尽CPU。日志审计记录系统登录、sudo操作、crontab变化等关键事件。这套体系跑下来再没出过类似的“半夜CPU飙升”事件。7. 虚拟机与国产系统上进程管理的几个微妙差异7.1 VMware里安装Linux后的init进程差异如果你是在VMware、VirtualBox这类虚拟机里装Linux进程管理的绝大多数原理都一样但有个细节值得注意虚拟机里看到的CPU核心数是由宿主机分配的。top显示的逻辑核数如果比物理机少不代表进程管理有问题而是虚拟机的CPU配额限制。另外某些发行版在虚拟机环境下可能没有加载systemd而是使用SysV init尤其是一些精简版系统。区别主要在于PID 1的名字不是systemd而是init服务启停的命令是service sshd start而不是systemctl start sshd。如果你在写教程或脚本先跑一下ps -p 1 -o comm确认init类型能省不少弯腰。在VMware里安装Linux时如果出现“卡在启动logo”或“CPU被禁用”之类的报错多数情况下是虚拟机CPU设置不匹配比如启用了嵌套虚拟化但客户机不支持。这时候可以尝试关闭虚拟机的“硬件虚拟化”选项或者换用与客户机兼容性更好的CPU模型。7.2 麒麟等国产系统上的systemd使用注意近几年国产Linux系统用得越来越多比如麒麟、统信UOS它们底层大多数还是基于主流Linux内核systemd也是标配。我实际接触过的银河麒麟服务器版基本命令和CentOS很接近systemctl、ps、top都能正常使用。但有一个坑部分国产系统自带的软件源里某些服务的管理脚本可能对systemd的版本有要求。比如我在麒麟上部署Docker时就遇到过systemctl start docker失败但手动dockerd能跑起来的情况。排查后发现是systemd服务文件里写了一个较新的参数换个写法就好了。处理这一类问题时先看systemctl status和journalctl -xe的输出别急着重装。另外国产系统上sudo权限管理、用户权限隔离的机制和标准Linux完全一致日常进程管理思路可以平移。7.3 容器与虚拟机之下的PID namespace概念最后补充一个越来越常见的场景容器。Docker容器复用宿主机的内核但通过PID namespace让容器内的进程拥有独立PID空间。也就是说容器里看到的PID 1可能只是宿主机上的某个几百号进程。同一时间系统上可能存在很多PID都为1的容器进程它们分布在不同的namespace里。这种隔离给进程管理带来一个新维度在宿主机上看到一个PID你需要知道它属于哪个容器进入容器后你需要知道它在容器里可能映射了不同的PID。排查网络或文件问题时用nsenter -t 宿主机PID -n进入对应namespace是常规操作。我刚开始用容器时在宿主机上kill一个容器内PID结果杀错了进程就是因为没搞清楚PID namespace映射。现在我的经验是能通过docker top 容器名看容器内进程的宿主机PID再从宿主机上定位就不会乱了。如果项目里同时用虚机和容器建议维护一张“服务-虚拟机/容器-进程PID”的映射表或者统一通过编排工具管理。进程管理在传统Linux和容器世界里本质上是一套内核机制但工作方式已经发生了不小变化。最后再分享一个我自己的习惯无论多忙都要养成定期看进程树的习惯。不需要记住所有命令参数但至少要知道机器上“正在跑什么、正常应该有哪些进程”。很多时候系统出问题不是配置多难而是我们根本没发现自己机器上多了一个不该有的进程。从这篇文章里的命令开始练手比存一堆资料有用得多。
返回列表