ARTICLE DETAIL

资讯详情

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

Linux进程管理精讲:从ps、kill到systemd实战

Linux进程管理精讲:从ps、kill到systemd实战 1. 先搞明白进程到底是个什么东西玩Linux的人迟早都要跟“进程”打交道。我见过不少新手学了几个命令就以为自己会了——ps aux看一眼kill -9梭哈一把结果该学的没学会不该杀的全杀了。一问为什么这么干回答是“网上都这么写”。这不叫懂进程管理这叫背口诀。先回到最基础的问题进程是什么一句话版本——进程就是一个正在运行的程序实例。注意“正在运行”和“实例”这两个词。程序本身只是磁盘上的静态文件比如/usr/bin/nginx那一堆二进制当你把它跑起来系统内核就为它分配内存、加载代码、创建执行上下文这个“跑起来的东西”才是进程。同一份程序可以同时起多个进程比如你开了三个终端窗口跑bash那就是三个独立的 bash 进程互相不干扰。再进一步进程和线程的区别也经常有人搞混。通常情况下进程是资源分配的最小单位线程是 CPU 调度的最小单位。一个进程可以有多个线程这些线程共享进程的内存和文件描述符但各自有独立的栈和执行流。Linux 内核眼里并没有特别区分“线程”和“进程”它管线程用的数据结构叫task_struct线程其实就是“轻量级进程”LWPLight Weight Process。所以你在ps输出里看到的一行“进程”可能只是某个多线程进程里的一个线程TOP 命令里的 PID 和 TID 就是干这个用的。还有一对概念也需要掰扯清楚前台进程和后台进程。进程在启动时可以带终端也可以不带。和某个终端TTY关联、能接收你键盘输入的进程叫前台进程脱离终端或者不接收终端输入、只闷头干活的叫后台进程。Linux 里有一套完整的前后台切换机制后面我专门讲。理解进程之前还有一件事得知道在 Linux 里一切进程都逃不出 init 或 systemd 的“家族树”。进程之间有父子关系子进程是父进程 fork 出来的。你可以在终端里敲pstree -p会看到一棵从systemdPID 为 1开始的进程树系统里所有用户进程都是这棵树的叶子。为什么会有这种树状结构因为进程创建的经典方式就是fork()——内核把父进程的数据结构几乎原样复制一份子进程从 fork 返回的地方接着往下跑新进程不是凭空冒出来的而是“爹生儿子”。正因如此进程的父进程 PIDPPID对排障非常有用——你能通过 PPID 找到“谁把这家伙拉起来的”。提示看到进程树或者 PPID 时先别慌绝大多数“莫名其妙的进程”都能通过找爸爸来定位来源。这比盲猜进程名字靠谱得多。理解了这些基础概念后面的一切命令和操作就都有了根。很多人觉得进程管理难不是难在命令而是难在对“进程是什么”没有体感遇到问题只能靠一顿乱敲来碰运气。2. 把进程“看穿”从 ps 到 top再到 /proc 文件系统2.1 ps 是进程查看的基石但你要会读输出ps是进程查看的第一把钥匙。最常用的两种写法是ps -ef和ps aux很多人纠结用哪个其实都一样只是输出格式的侧重不同。$ ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 16:20 ? 00:00:12 /sbin/init root 2 0 0 16:20 ? 00:00:00 [kthreadd] root 38 2 0 16:20 ? 00:00:00 [ksoftirqd/0] root 1234 1228 0 16:22 pts/0 00:00:00 -bash www-data 4567 1234 0 16:25 pts/0 00:00:15 nginx: worker process字段从左到右UID运行者、PID进程ID、PPID父进程ID、CCPU占用百分比、STIME启动时间、TTY关联终端?表示无终端、TIME累计CPU时间、CMD命令。ps aux则会多出%CPU、%MEM、RSS常驻物理内存这些资源占用列格式是 BSD 风格。两种都可以我个人更习惯ps aux因为带内存和 CPU 很容易一眼扫出“哪个不健康”。但是只看一秒钟的快照是不够的。ps是静态视图它只看某一瞬间。要发现 CPU 占用反复波动、内存持续上涨这类动态问题必须用top。2.2 top 交互界面不止是按一下 q 退出top的本质是每隔几秒刷新一次的动态视图。进入界面后按P按 CPU 排序、按M按内存排序、按T按累计CPU时间排序这是三种最常用的排序方式。排查“谁在偷吃 CPU”时先按P然后看最上面几行基本就有答案了。top上半部分的统计区也很关键——load average三个值分别代表过去1分钟、5分钟、15分钟的平均负载。这里的“负载”不是单纯的 CPU 使用率而是一段时间内处于可运行状态和不可中断睡眠状态的进程平均数量。一个 8 核的机器负载 8 代表差不多把每个核都排满了负载超过 8 说明开始排队了。很多人看一眼%Cpu(s)里的 us 和 sy 比例就以为懂了全部——其实 us 高代表用户态程序在算sy 高代表系统调用频繁比如 IO、锁竞争。如果你发现 sy 比用户态还高那大概率是系统层出问题了比如磁盘 IO 太慢、网络中断风暴。top里还有一个被低估的能力按c切换完整命令行能看到进程的真实完整参数按f还能自定义显示列。排查时如果进程名看起来很正经但完整命令里藏着奇怪的脚本路径就按c看看老底。注意top 虽然直观但它的刷新有延迟而且交互式操作不好用脚本去抓。真正要抓瞬间快照或者做持续采样我后面讲pidstat和glances。2.3 精准定位pgrep 和 pidof 的实用组合ps aux | grep xxx是找进程的通用笨办法但 grep 自己也会出现在结果里当年ps aux | grep grep的效果大家都懂。现在更推荐用pgrep$ pgrep -a nginx 4567 nginx: worker process-a会顺便把命令行打印出来新版 pgrep 可能没有-a不同发行版情况不一样要求稳的话可以配合-l用。只想精确匹配进程名可以用pgrep -x nginx它要求进程名完全等于 nginx不会把nginx-master之类的一并捞出来。pidof也是类似的定位工具但它输出的是“纯 PID 列表”比如$ pidof nginx 4567 4566这种纯数字输出在脚本里特别好用。配合ps -p可以快速验证$ ps -p 4567 -o pid,cmd2.4 进程的一切都在 /proc 里ps和top再花哨底层数据都来自内核暴露的/proc虚拟文件系统。每个运行中的进程在/proc下都有一个以 PID 命名的目录。比如 PID 是 4567就有/proc/4567/目录。这里面值得研究的东西太多了/proc/4567/cmdline完整的命令行参数多个参数之间用\0分隔。用tr \0 /proc/4567/cmdline能把它转成可读格式。/proc/4567/environ进程启动时的环境变量同样\0分隔。/proc/4567/fd/进程打开的文件描述符符号链接指向实际的文件、socket、管道。这个目录在排查“端口被占”“文件被锁”时是终极武器。/proc/4567/status进程状态、内存、线程数等信息的汇总比如Threads字段直接告诉你这个进程有多少线程。当你怀疑某个程序“卡死”时先看/proc/PID/status里的State字段R是运行中S是睡眠D是不可中断睡眠通常正在等 IOZ是僵尸进程T是停止。这个字段比ps输出里的STAT更原始也更可靠。实操体会真正到了排障现场我很少一遍遍刷ps而是直接进/proc/PID去看这个目录下的细节。所有监控工具做的都是“帮你读 /proc”你亲手读一遍很多东西就豁然开朗了。3. 进程的“生老病死”启动、停止、退出的全链路3.1 进程怎么来的fork、exec 和 systemd 启动进程启动不是“操作系统凭空造一个出来”这么简单。经典流程是fork execfork父进程调用fork()内核复制一份父进程的数据结构子进程获得独立的 PID并且和父进程共享部分内存页写时复制COW。exec子进程调用exec()系列函数把自己替换成一个全新的程序。比如bash调用fork生成子 shell子 shell 再exec /usr/bin/ls把 ls 的代码载入内存。所以在 Linux 里所有用户进程往上追溯都是systemdPID 1通过某种方式拉起来的。systemd通过 service 单元定义怎样启动、怎样守护、怎样重启这套机制比传统的init.d脚本健壮得多。“进程管理”在 systemd 语境下就是systemctl start/stop/restart某个 unit——不过 unit 是“服务”最终落实到系统里仍然是一个或一组进程。如果你用命令行直接启动一个程序比如./my_server这个进程的父进程就是当前 shell。如果你闲得没事儿在终端里跑了前台程序又直接关掉窗口终端会向这个前台进程组发送SIGHUP挂断信号进程通常就死了。这就是为什么很多服务要用nohup或者注册成 systemd 服务来跑——目的就是脱离和控制这种“终端挂断就死”的命运。3.2 kill 不是“杀掉”是“发信号”这是被误解最深的命令。kill真正的功能是向进程发送信号至于收到信号之后干什么取决于进程自己。最经典的信号表如下信号数值默认动作常见用途SIGHUP1终止进程终端挂断、重新加载配置很多守护进程约定收到它就 reloadSIGINT2终止进程CtrlC 产生的信号让前台进程中断SIGQUIT3终止进程并产生 core dump调试用类 Ctrl\SIGKILL9强制终止不可被捕获杀不掉时的最后手段SIGTERM15终止进程kill 默认信号请求进程自行退出SIGSTOP19暂停进程相当于冻结进程无法拦截SIGCONT18继续运行配合上面的 SIGSTOP 使用所以kill -9不是什么“更强的杀法”而是“直接让内核把进程宰了不给它辩解的机会”。kill -15也就是不带参数默认是“请进程自己收拾完摊子再走”。我见过一个很蠢的用法kill -9一把梭去停服务结果数据库把表弄坏了。正常的停服务姿势应该是先用systemctl stop或者给进程发SIGTERM让它优雅退出做检查点、刷缓存、关连接。等 10 秒发现它还不肯退再考虑SIGKILL。把kill -9当习惯迟早出事。3.3 僵尸进程为什么有“杀都杀不掉的进程”有一种进程的状态是Zzombie僵尸特别吓人——ps里看着它“还在”但kill -9怎么都杀不死CPU 和内存占用却是 0。僵尸进程的本质是什么子进程已经执行完退出了但它的父进程还没有调用wait()认领子进程的退出状态。在“退出生效”之前内核留着这个进程的退出码、CPU 时间统计等最小信息等父进程来拿。这个“只剩一层壳”的进程就是僵尸。kill -9杀不掉僵尸因为它已经死了——它根本不是活着的进程。要清理僵尸正确思路是处理它的父亲如果父进程是正常程序它迟早会 wait 到子进程的状态然后回收僵尸。如果父进程死掉了僵尸会被 init/systemd 收养不定时回收。如果父进程是个有问题的程序始终不 wait僵尸就会堆在那里。这时你应该去解决父进程的问题或者重启父进程本身。系统里偶尔一两个僵尸不算大事但如果僵尸堆了成千上百说明父进程有 bug。工作中我见过某个 Java 应用疯狂 fork 子任务但从不 wait最后把PID号耗完整个系统起不了新进程——这是典型的“僵尸堆”事故。3.4 前后台切换与 ctrlz 的真相在命令行里运行一个命令它是前台进程。按CtrlZ会发送SIGTSTP给前台进程组进程被暂停状态 T然后你会回到 shell。jobs列出当前 shell 的后台任务bg %1让 1 号任务在后台继续跑fg %1把它调回前台。这里的关键是CtrlZ 不是“放到后台”是“暂停”。暂停和后台运行是两回事——background表示进程在跑只是不占用终端的输入CtrlZ是进程完全挂起。很多人在 vim 里CtrlZ切出来干点别的事忘了fg回去vim 就一直挂着以为自己写的东西丢了其实进度还在。不过这套机制有其局限性一旦终端关闭前后台进程都会收到挂断信号很可能死掉。这就是nohup、setsid存在的理由。$ nohup ./my_server server.log 21 nohup让进程忽略SIGHUP放进后台 server.log 21把输出重定向到文件。比nohup更彻底的是setsid它让进程完全脱离当前会话成为一个新的会话领导连终端都没有——就算 shell 退出它也不受影响。不过话说回来如果是生产环境要长期运行的服务我强烈建议你别用 nohup直接写 systemd unit让 systemd 来管启动、日志、崩溃自动重启比裸奔的 nohup 进程可控得多。4. 资源限制与优先级让重要进程跑得快让野马进程跑不掉4.1 nice 值不是“品德分”是 CPU 排队插队资格每个进程还有一个叫nice value的字段从 -20 到 19默认 0。nice 值越小进程被 CPU 调度的优先级越高反过来 nice 越大越“谦让”你 nice别人就先挑 CPU。普通用户只能把 nice 调大变得更谦让只有 root 能调成负数抢资源。查看 nice 值用ps -o pid,nice,cmd或者 top 里的NI列。启动时想直接指定 nice 值$ nice -n -5 ./my_server对已经在跑的进程调整$ renice -n -5 -p 4567调整 nice 值就是调整 CPU 调度策略里的静态优先级。内核里对普通进程用的调度算法叫CFS完全公平调度器它并不看“谁 nice 值小先跑谁”这么简单而是通过虚拟运行时间来决定。你只需要理解关系紧张的机器上给核心服务 nice 调低一点root 权限给备份任务 nice 调高一点能让核心服务的延迟明显下降。但在单核机器上CPU 本身是瓶颈nice 再调也不会把资源变多出来——该排队还得排队。4.2 ulimit进程能碰多少资源是父进程传下来的进程的资源上限不是随心所欲涨的ulimit控制着这个“天花板”。常见的$ ulimit -n 1024这是文件描述符上限。很多高并发服务Nginx、Redis默认 1024 不够用所以要调。注意ulimit 是 shell 内置的东西子进程会继承父进程的 ulimit所以改ulimit的姿势是在启动服务的 shell 里先ulimit -n 65535或者直接写进 systemd unit。$ ulimit -u这是最大用户进程数限制。如果系统报“fork: Cannot allocate memory”而内存明明还有剩余多半是ulimit -u太低或者 cgroup 的 pids 控制器限制了该 cgroup 内的进程数量。我第一次遇到这个报错也懵了好久排查到最后才发现是容器里 PID 数被打满了。其他常用的还有ulimit -ccore dump 文件大小、ulimit -v虚拟内存上限。想永久生效在/etc/security/limits.conf里写。4.3 systemd 的资源控制如果你在用 systemd 管理服务那么ulimit不再是唯一的限制手段。systemd unit 里有几个重要的资源限制字段比如[Service] LimitNOFILE65535 LimitNPROC4096 MemoryMax2G CPUQuota80%MemoryMax是硬性内存 cgroup 限制进程超出会被 OOM killer 干掉CPUQuota80%表示最多用一个核的 80%。这些字段的效果远比ulimit精细因为它走的是内核的 cgroup v2 机制可以对整棵进程树做限额。心得我在生产机器上给每个服务都配了MemoryMax和LimitNOFILE。宁可早期让服务 OOM 重启也不要让它内存涨到拖垮整个物理机。做好资源边框是进程管理最容易被忽略但最救命的一环。4.4 进程停止和退出的优雅姿势聊完限制顺手说说停止一个进程时的优先级顺序这也是老手的肌肉记忆如果服务由 systemd 管理systemctl stop xxx它会给进程发SIGTERM。如果裸进程先kill -TERM PID等几秒。如果它不理会再kill -INT PIDCtrlC 对应的信号。实在不行kill -KILL PID。但这里有一个陷阱一个进程可能有多条线程、多个子进程你在前台看到 PID 只是主进程。你想让整个服务全退只杀主进程往往不够子进程可能仍然活着。这时候kill -- -PGID负号 进程组ID可以整组发送信号。进程组的概念就是终端任务分组的基本单位CtrlC也是发给整个前台进程组的。5. 进程排障实战CPU 狂飙、僵死、端口被占的三连现场5.1 CPU 100% 的排查思路从 top 到线程栈CPU 被某个进程占满这是最常见的故障。完整链路是top按 P 键排序锁定高 CPU 的 PID。如果涉及 Java、Python 这类多线程程序要看线程级别的占用top -H -p PID。拿到线程 IDTID后转成十六进制printf %x\n 线程TID。对 Java 程序可以用jstack PID | grep -A 30 线程ID看线程栈对 C/C 程序可以用gdb attach PID或者看/proc/PID/stack。如果怀疑是 IO 导致的 cpu 高比如sy高用pidstat -d PID看它的块设备 IO 速率。有一次我们一个服务突然 CPU 100%按这个链路查到是某个 worker 线程在无限循环拉取远程配置因为配置服务挂了它不断重试。从线程栈里能直接看到重试的调用栈——这种问题不看线程栈根本无从下手。pidstat是 sysstat 包里的工具比 top 更适合持续采样$ pidstat -p 4567 -u 2 5每 2 秒取样一次持续 5 次看该进程的 CPU 占用变化。5.2 进程“不可中断睡眠”D 状态时怎么办进程长时间处于 D不可中断睡眠状态通常意味着它在等某个内核 IO 操作而这个 IO 卡死或极慢。可能是 NFS 挂载的目录失联了也可能是底层磁盘在慢速重试。D 状态的进程不能被 CtrlC、kill -15 打断因为它在内核态深度睡眠最狠的只有 kill -9 碰碰运气但有时连 kill -9 都排不上队。这里有个很经典的教训NFS 服务器挂了客户端上所有涉及 NFS 的进程全部变 D 状态连ls卡住kill -9都没反应。这时候能做的有限——尽快恢复底层存储或者等内核的 IO 超时机制自己兜底。因此在生产环境里能用本地盘就用本地盘NFS 存储依赖越多出事儿就越被动。排障 D 状态进程可以看/proc/PID/wchan进程在内核里正在等待的内核函数。比如$ cat /proc/4567/wchan rpc_wait_bit_killable这个字段点名了它在等 NFS 的 RPC 调用。有了这个线索就能判断问题出在哪个内核子系统上。5.3 端口被占用三步找出“占坑”进程端口冲突是最频繁的小问题。老手从不重启服务器来解决端口占用——三步定位ss -lntp | grep :8080优先用ss而不是netstatss 更现代、更快。输出里有 PID 就拿到 PID 了如果ss没显示 PID权限不足加sudo。拿到 PID 后ps -fp PID看是什么程序再决定是杀还是让它自己走。如果只想看某个进程打开了哪些端口从/proc/PID/net/tcp里也能翻但远不如ss直观。这个排查思路可以扩展到文件锁、Unix socket 占用等情况凡是“资源被占用”类问题无非是从进程找资源、从资源找进程两条路而/proc/PID/fd和ss就是这两条路的入口。5.4 进程留下的孤儿find 唯一被忽略的孤儿进程还有一种特殊情况父进程退出后子进程没有跟着退出变成孤儿进程被 init/systemd 收养。这种进程通常无害但如果是父进程意外挂掉留下的长任务你可能会在ps里看到它的 PPID 变成 1。排查思路“找孤儿”在ps -eo pid,ppid,cmd | awk $21 $1!1一把抓出来再根据命令行判断要不要处理。因为没有了“家长”这类进程的行为比较难预测——所以能用 systemd 管服务都尽量交给它出问题至少能查到是从哪个 unit 拉起来的。6. 进程管理的一点点“道”从命令到思维这个很快。拿我自己的体会来说Linux 进程管理最难的不是命令本身而是建立“一切都有迹可循”的思维。看到任何一个进程问自己三个问题是谁把它拉起来的PPID它在干什么/proc/PID 下的 cmdline、fd、stack它占了多少资源top、pidstat、cgroup答案都能从系统里找到。日常工作中我习惯把进程管理系统化重要服务全部用 systemd 管理并配好资源限制和自动重启排查问题时固定用ss ps /proc的组合拳定期用ps -eo pid,ppid,state扫一眼有没有大量僵尸给每个服务的启动脚本里显式设置ulimit。这些习惯叠加起来会让“进程管理”从一堆零碎命令变成一个可依赖的防护网。最后再分享一个实用的小技巧当你怀疑某台机器有异常进程时先不要急着一顿kill可以写一个小脚本把当前所有进程的快照PID、PPID、状态、命令行记录下来间隔几秒再记录一次对比两次快照新出现的进程往往就是问题的起点。这个方法我救过好几次急比你盯着top刷新有效得多。
返回列表