ARTICLE DETAIL

资讯详情

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

Linux进程管理与计划任务:从原理到实战排查指南

Linux进程管理与计划任务:从原理到实战排查指南 Linux 进程与计划任务这俩词放在一起几乎就是 Linux 运维日常工作的半壁江山。无论是刚入行的新人还是写了几年脚本的老手每天打交道最多的无非就是这进程怎么又挂了、那个任务怎么到点没跑、后台服务怎么总被误杀。我最早接触 Linux 的时候第一个折腾明白的概念就是进程因为不理解进程你连“为什么 kill 掉一个程序这么费劲”“为什么 ps 里能看到一堆同名的东西”都说不清楚。而计划任务说白了就是让系统替你做那些重复性的脏活累活。这篇文章我不打算讲什么高深内核源码也不罗列所有命令参数而是把“进程管理和计划任务”这两个主题串成一条线先看进程是什么、怎么查、怎么管再看计划任务怎么写、怎么排障最后用我实际遇到过的问题来收尾。适合刚入门想系统理解 Linux 的人也适合那些会用命令但说不清原理、遇到问题全靠试的运维伙伴。读完你会发现很多“玄学”问题其实都是我们自己没搞懂底层逻辑。1. 先把进程这东西看明白1.1 进程和线程别再傻傻分不清很多人一上来就纠结进程和线程的区别面试题里几乎必考。但我更愿意先用一句话点破进程是操作系统分配资源内存、文件描述符、CPU 时间片的基本单位线程则是 CPU 调度执行的基本单位。翻译成人话就是——进程像一个工厂线程是工厂里的工人。工厂有自己独立的厂房独立的内存空间工人共享这个厂房和里面的机器共享进程内存但每个工人各干各的活各自有栈和寄存器状态。从这个类比能推出几个关键结论进程之间天然隔离一个进程崩了不会直接拖垮另一个而同一进程内的线程是兄弟一个线程 segfault 往往整个进程崩掉。进程切换开销大因为要切换内存地址空间、刷新各种缓存线程切换只在进程内部进行开销小得多。进程间通信IPC需要额外机制比如管道、信号、共享内存线程之间直接读写共享变量即可但要注意同步问题。但注意Linux 的线程实现有点特殊它通过 clone 系统调用创建很多资源是共享的所以线程在 ps 里看起来可能像进程只是对内核来说都叫“任务”。这就是为什么你有时候 ps -ef 看到很多同名 java 进程其实它们是同一个 JVM 进程里的线程只是被展示出来了而已。理解这一点排查“为什么 java 进程 CPU 占用从 100% 变成 300%”这类问题时就有方向了——别急着 kill先看是不是多线程在跑。1.2 进程的一生从 fork 到 wait进程是怎么诞生的绝大多数不是凭空出现的而是由一个已有进程通过 fork() 系统调用复制出来的。复制出来的那个叫子进程原来的叫父进程。fork 有意思的地方在于它会让子进程拥有一份父进程内存的拷贝但之后两者就各过各的了。然后通常子进程会调用 exec 系列函数去加载新的程序把自己替换成别的程序。这就是我们常说的“fork exec”。那进程怎么结束正常退出时调用 exit()把退出码返回给父进程不正常退出则收到信号比如段错误会收到 SIGSEGV。这里有个关键点子进程退出后并不会立刻消失它会变成一个“僵尸进程”直到父进程调用 wait() 或 waitpid() 来回收它的退出状态。如果父进程一直不收尸僵尸进程就留在进程表里虽然不占 CPU 和内存但它占着一个进程槽位积累多了系统就创建不了新进程了。我之前遇到过一个数据库容器跑了几个月后突然报 “fork: Cannot allocate memory”但 free 一看内存还有一大半。最后就是检查进程表发现有几千个 僵尸进程。根因是父进程脚本写得不严谨没有 wait 子进程的退出状态。所以看到这里你就明白热搜里那个“进程等待 wait”其实就是这个含义——父进程必须处理子进程的退出否则系统会被尸体填满。1.3 常用命令三板斧ps、top、htop排查进程最常用的就是 ps 和 top虽然它们很基础但很多人的用法其实是错的。先看 ps# 查看所有进程显示完整命令 ps -ef # 查看所有进程显示进程树关系 ps -ef --forest # 只查看某用户的进程 ps -u username # 过滤某个程序名 ps -ef | grep nginxps -ef 的字段从左到右分别是UID哪个用户的进程、PID进程号、PPID父进程号、CCPU占用、STIME开始时间、TTY所在终端、TIME累计占用CPU时间、CMD完整命令。这里有个细节PID 和 PPID 一定要养成看的习惯。排查“某个服务怎么起不来”时先确认是不是有一个父进程在循环拉起子进程导致你 kill 完又活了。top 是动态的适合看实时状况。进去后按 P 按 CPU 排序按 M 按内存排序按 H 切换线程模式。这几个快捷键必须记住比记 20 个参数有用得多。htop 是增强版彩色支持鼠标但很多生产环境默认没装所以我的建议是ps 和 top 必须熟练到条件反射htop 当作锦上添花。2. 进程管理的进阶操作2.1 守护进程与会话让程序在后台稳住“守护进程”这个词在热搜词里很显眼因为它是后台服务的核心形态。普通进程和终端绑定你关掉终端进程收到 SIGHUP 信号就会退出。守护进程则脱离终端独立运行生命周期从系统启动持续到关机或服务停止。守护进程是怎么做到的其实有三步调用 setsid() 创建新的会话脱离控制终端。忽略 SIGHUP 等与终端相关的信号。把工作目录切换到根目录或其他固定目录避免占用挂载点导致卸载失败。把标准输入、输出、错误重定向到 /dev/null 或日志文件。不过实际工作中我们很少手写这么底层的逻辑。systemd 时代通常就是写一个 service 单元文件systemd 帮你把守护化过程处理好了。但你得知道原理因为排查问题时需要判断我的程序是不是被当作守护进程了它当前属于哪个会话可以用 pidstat 或者直接看 /proc/PID/stat 里的会话信息也可以用 ps -o pid,ppid,sid,tty,cmd 来看。我记得有次帮同事排查一个 Java 应用他用 nohup 把进程放后台结果过了两天进程莫名其妙没了。原因就是 nohup 只忽略 SIGHUP但如果终端关闭导致进程变成孤儿后某些情况下还是会因为资源限制被杀。真正想让服务 7x24 稳定跑应该用 systemd 或 supervisor 这类进程守护工具由它们负责拉起来、监控、重启。2.2 给进程改名修改进程名的正确姿势热搜词里有一条很具体“linux 修改进程名称 大于15个字符”。这确实是个常见需求——默认情况下通过 exec 启动的程序进程名就是程序名在 /proc/PID/comm 里而这个字段内核限制为 15 个字符。想改的话有几种思路最简单的做法是启动时用 bash 的 exec -a 参数exec -a my_custom_name /usr/bin/my_program这在 Shell 层面设置了 argv[0]ps 看到的是自定义的名字。但注意这种方式不影响 /proc/PID/comm 里的内核线程名且长度仍受 argv 传递限制。真正稳妥的办法是程序内部调用 prctl(PR_SET_NAME, newname) 或者 pthread 里用 pthread_setname_np()这能修改内核线程名。比如一个 Python 脚本可以用 ctypes 调用 libc 的 prctlimport ctypes libc ctypes.CDLL(libc.so.6) libc.prctl(15, bmy_py_worker, 0, 0, 0)那怎么让进程名超过 15 个字符内核里 comm 字段就是 15 字节这是硬限制改不了。想显示更长的名称只能在 /proc/PID/cmdline 上做文章也就是 argv。比如你用/usr/bin/python3 /opt/myapp/worker.py --config /etc/worker.yml这种方式启动ps 看到的其实是命令行而命令行是没有长度限制的。所以实战中如果是脚本类程序靠完整命令行就能区分不同实例编译型程序才需要在代码层修改 comm 字段。2.3 进程间通信 IPC多条管道怎么连热搜词里“进程通信ipc”是高频也是面试和实际开发中都绕不开的。进程间通信的武器库里有管道pipe、命名管道FIFO、消息队列、共享内存、信号量、信号、套接字。你说系统为什么需要 IPC因为进程之间是隔离的不能直接访问对方内存但现实场景又需要协作比如 Web 服务器让 N 个 worker 进程共享一个监听端口比如监控进程去读取目标进程的状态。让我挑几个常用的说说匿名管道cmd1 | cmd2那种管道连接的是父子进程或兄弟进程是单向的临时存在于内存。它的本质是内核里的一段缓冲区写端写、读端读如果读端没准备好写端会阻塞。这个特性经常在 shell 脚本里产生“假死”现象——进程明明在跑却卡住不动很可能就是管道缓冲区满了。命名管道 FIFOmkfifo /tmp/myfifo创建不相关的进程也能通过同一个文件路径通信适合简单的单向对接。需要注意读端打开时会阻塞直到有写端打开反之亦然。共享内存性能最好适合大数据量场景。但要注意“可见性”和同步问题通常会搭配信号量使用否则可能出现读写竞争。信号这里不是指 signal 库的“信号量”而是 PID 级别的异步通知机制。最常见的用法是 kill -TERM、kill -HUP。进程可以捕获这些信号做自定义处理比如 Nginx 收到 HUP 就重载配置不中断服务。我实际使用中跨语言或跨进程的轻量通信更倾向于用 Unix socket 或 TCP socket因为它们稳定、语义清晰调试工具也多ss、nc、socat。共享内存我会谨慎用因为一旦进程非正常退出锁没释放排查起来挺痛苦的。给你一个避坑心得能用文件锁解决的别用共享内存能用 socket 的别碰信号量原则就是——简单可靠优先极端性能再说。2.4 进程池与并发一次启动的学问热搜词里还有“进程池”这更多是开发层的概念。比如 Python 的 multiprocessing.Pool、Golang 的 goroutine 配合 GOMAXPROCS、Java 的线程池。但运维视角下的“进程池”往往指的是预创建一类 worker 进程比如 PHP-FPM 的进程池、Nginx 的 worker_processes、Celery 的 worker 数量。为什么要池化因为进程创建和销毁的开销大提前准备好 N 个进程或线程请求来了直接分配能显著降低响应延迟也便于控制资源上限。配置进程池数量是我见过最容易踩坑的地方。不是越多越好因为每个进程都占一定的内存哪怕空闲上下文切换要消耗 CPU磁盘 IO 密集任务还可能互相争抢锁。以 PHP-FPM 为例我曾经把进程数从 50 调到 200本意是提升并发能力结果内存直接爆掉Swap 被疯狂占用响应反而更慢了。后来根据 PM dynamicpm.max_children (总内存 - 预留内存) / 单进程平均内存占用 来算16G 的机器只给了 80 左右问题迎刃而解。所以见到“进程池”关键词不要只想到代码更要想到系统资源预算。3. 计划任务让系统自己干活3.1 cron 与 crontab定时任务的老祖宗计划任务的重头戏肯定是 cron。cron 是系统里的一个守护进程cronie 或 vixie-cron它每分钟醒来一次检查是否有要执行的任务有则通过 sh 执行。配置方式有两种用户级 crontab 和系统级 crontab。用户级用crontab -e编辑格式是五个时间字段加一条命令分 时 日 月 周 命令例如每天凌晨 3 点备份数据库0 3 * * * /usr/local/bin/backup_db.sh /var/log/backup_db.log 21字段从左到右依次是分钟0-59、小时0-23、日1-31、月1-12、周0-7其中 0 和 7 都表示周日。*表示任意*/15表示每 15 分钟1,15表示第 1 和第 15 分钟2-6表示 2 到 6 的范围。系统级 cron 任务放在 /etc/crontab 文件里或者在 /etc/cron.d/ 目录下文件格式里比用户级别多了一个“用户名”字段0 3 * * * root /usr/local/bin/cleanup.sh我强烈建议有日志有标准输出的脚本任务不要直接写裸命令一定要写脚本文件。因为 crontab 环境变量非常精简不加载 /etc/profile 和 ~/.bashrc你平时能跑的命令放到 cron 里可能就找不到环境变量、找不到 PATH 路径。所以脚本第一行建议写#!/bin/bash第二行加载必要环境source /etc/profile然后所有路径用绝对路径。3.2 一次性任务 at 与 loop临时需求怎么办cron 适合周期性任务但“今天下午 4 点执行一次”“5 分钟后跑一个脚本”这种临时需求用 at 更合适。我用一个真实例子有一次线上数据库需要做表结构变更DBA 评估后说凌晨 1 点做影响最小但又不想改 crontab于是我用了echo mysql -u root -e ALTER TABLE ... | at 01:00这样到了时间会自动执行而且 at 会把任务挂到 atd 守护进程上即使终端退出也不影响。查看任务列表用atq删除任务用atrm 编号。与之类似地在较新的系统里也有 systemd-run 可以生成临时的一次性 timer但 at 轻量、简单仍然是我的首选。有一点要注意at 默认是否被允许取决于 /etc/at.allow 或 /etc/at.deny 文件有些最小化安装可能没启用 atd 服务。先systemctl status atd确认一下再使用。3.3 systemd timer新时代的定时器如果你的系统是 CentOS 7 或 Ubuntu 16那么 systemd timer 是比 cron 更贴近现代系统的计划任务方案。它和 cron 的核心区别是cron 只负责“到点了就执行命令”而 systemd timer 可以绑定到某个 unit还支持开机后延迟执行、错峰执行、依赖管理等。写一个简单的 timer 需要两个文件。首先是服务单元/etc/systemd/system/cleanup-task.service[Unit] DescriptionCleanup temp files [Service] Typeoneshot ExecStart/usr/local/bin/cleanup.sh然后是定时器单元/etc/systemd/system/cleanup-task.timer[Unit] DescriptionRun cleanup every day at 2am [Timer] OnCalendardaily Persistenttrue [Install] WantedBytimers.target启用systemctl daemon-reload systemctl enable cleanup-task.timer --now systemctl list-timersOnCalendar 的语法比 crontab 更灵活比如OnCalendarMon..Fri *-*-* 09:00:00表示每周一到周五早上 9 点。Persistenttrue的意思是如果系统在计划时间点处于关机状态下次开机后会补执行。这个特性是 cron 不具备的很多备份任务因此强烈建议用 systemd timer 而不是 cron。对了关于热搜词里出现的“做快手星火计划任务”那明显是和广告分发平台相关的任务系统跟 Linux 系统计划任务不是一个东西这里就不展开了只是提醒大家搜索时要分辨语境。3.4 任务被莫名禁用backup scan 这类任务的排查思路热搜词里有条 “backup scan计划任务怎么禁用”这很典型——很多应用装的监控/备份组件会往 cron 里写一些定时任务比如每天扫描、每天上报用户觉得烦或觉得占资源想禁掉。这种“系统里多出来的 cron 任务”要怎么找、怎么禁我给出一个标准排查流程先把你自己的用户任务列出来crontab -l。再看系统级任务cat /etc/crontab、ls /etc/cron.d/。还有几个隐藏目录别漏/etc/cron.hourly/、/etc/cron.daily/、/etc/cron.weekly/、/etc/cron.monthly/。某些安装包会往里面丢脚本。检查所有用户的任务grep -r /var/spool/cron/具体路径因发行版而异有的是 /var/spool/cron/有的是 /var/spool/cron/crontabs/。找到目标后确认它是否真的来自 backup scan 产品方法很简单看脚本内容或者看脚本头部注释。如果是某个 rpm/deb 包带进来的直接卸载那个包或者编辑/删除对应的 cron 文件即可。但我要提醒不少“禁用方法”其实是把 cron 脚本里命令注释掉但包升级时脚本会被还原。更彻底的办法是找到生成 cron 的配置文件把相关开关关掉或者干脆卸载不需要的组件。负责地建议如果 backup scan 是某个商业备份软件的组件最好在官网文档里查“disable scheduled scan”之类的关键词不要粗暴删除脚本。我见过有人删了 cron 文件后备份软件的控制台一直报错连手动备份都起不来。正确路径通常是停服务 - 禁用定时器 - 卸载组件而不是只删 cron 条目。4. 实战排障与避坑指南4.1 cron 任务就是不执行先查这几个地方这是我被问得最多的问题cron 明明配置了但任务没跑。按经验按顺序排查cron 服务是否在运行。systemctl status crondCentOS或systemctl status cronUbuntu。我见过有人改了 crontab 但服务没起来任务当然不跑。时间字段写错。最容易错的是“周与日”同时指定时的 OR 逻辑。cron 里日月和周是“或”关系比如0 0 1 * 0表示每月 1 日或每周日执行而不是 1 日且周日执行。这条规则颠覆直觉坑过无数人。环境变量问题。cron 的 PATH 通常只有/usr/bin:/bin。脚本里用了 /usr/local/bin 下的程序在交互 shell 没问题但 cron 里找不到表现为“任务日志不报错脚本没跑”。解决脚本开头 export PATH。日志没输出。很多人的 cron 脚本没有重定向输出导致执行失败时看不见任何错误。最直接的做法是在命令后面加 /var/log/mytask.log 21然后记录执行日志。权限问题。如果脚本没有执行权限没 chmod xcron 会静默失败检查 /var/log/cron 可以看到 “Permission denied”。任务执行时间太短导致没日志。如果脚本只执行了 1 秒根本来不及落盘。可以用CONTINUE_ON_IO_ERROR或者给 sleep 缓冲不过一般不会这么极端——先看 cron 日志最直接。cron 日志位置因发行版而异CentOS/RHEL 是 /var/log/cronDebian/Ubuntu 大多看 /var/log/syslog。用journalctl -u crond -f也能实时观察任务触发过程。4.2 java 进程 OOM 排查实录热搜词里有一条特别具体“idea 编译时,进程堆大小调整为8000,还是报错 java.lang.OutOfMemoryError”。这其实是两个层面的问题如果是对 Java 应用服务器生产环境调堆那是 -Xmx 参数的问题如果是 IDEA 编译时的进程那它可能是前台的编译守护进程崩了也可能是 Gradle/Maven 的 worker 进程内存不够。先说生产环境。Java 进程 OOM 有很多种表现堆内存溢出Java heap space、栈溢出StackOverflowError、Metaspace 溢出、或者干脆是无法分配本地内存。热搜里 “进程堆大小调整为 8000” 就是把 -Xmx 调成 8G但仍然报错。遇到这种情况我建议先别急着加大堆因为如果物理内存或容器限额不够你调大 -Xmx 反而可能导致系统 OOM Killer 直接杀掉进程而不是 JVM 正常抛出 OOM。正确的排查顺序# 1. 确认当前 JVM 参数 jps -l jinfo -flag MaxHeapSize PID # 2. 观察进程实际内存占用 top -p PID ps -o pid,rss,vsz,cmd -p PID # 3. 看 GC 日志和堆直方图 jstat -gcutil PID 1000 jmap -histo PID | head -30如果是编译环境的报错那就是另一个套路了。IDEA 的编译进程默认配置在 Help - Change Memory Settings 里或者通过自定义 VM options 调整。但注意编译进程启动时 -Xmx 如果已经给了 8G仍然报错很可能是Metaspace或**直接内存Direct Memory**不足而不是堆不够。编译大量代码时注解处理器和编译器会加载大量 Class 元数据到 Metaspace默认-XX:MaxMetaspaceSize可能偏小。排查时可以同时调大-Xmx8192m -XX:MaxMetaspaceSize2048m还要检查是不是同时启动了多个 Gradle/IDEA 编译守护进程每个都占一堆内存。jps能列出现有 Java 进程把多余守护进程停掉再编译往往就正常了。总之Java OOM 不能无脑加堆得先分清是“堆不够”还是“整体内存不够”还是“元空间不够”。4.3 僵尸进程与孤儿进程处理不当的后果前面讲 wait 的时候提过僵尸进程这里展开说。僵尸进程就是已经退出但还没被父进程 wait 回收的进程在 ps 里显示为defunct。之所以叫“僵尸”是因为它已经死了但进程表里的僵尸条目还在。它不消耗 CPU 和内存但消耗 PID 资源量大了会阻塞操作系统创建新进程。和僵尸容易混淆的是“孤儿进程”父进程先退出了子进程就成了孤儿被 PID 1systemd收养。这其实不是坏事很多守护化操作就是依赖这一点让子进程脱离原父进程的。但如果父进程反复 fork 而不 wait子进程退出便堆积僵尸这才是问题。处理僵尸进程的标准姿势# 找到僵尸进程 ps -eo pid,ppid,stat,cmd | grep defunct # 看它的父进程是谁 ps -o pid,ppid,cmd -p 父进程PID # 杀掉父进程让僵尸被 systemd 回收 kill 父进程PID杀父进程要谨慎尤其当它承载了关键服务时。更好的做法是在代码层面让父进程正确 wait 所有子进程的退出码脚本场景可以用wait命令后台任务可以用 trap wait EXIT 之类的技巧。如果父进程是 Java/Python 写的非托管程序且已经无法修改代码那就只能临时重启该父进程来清理僵尸池。4.4 一套顺手好用的巡检脚本最后把我日常高频使用的进程计划任务巡检命令整合成一个脚本虽然不算复杂但能让你在面试或汇报时拿得出手也能实实在在节省时间#!/bin/bash echo 系统运行时间与负载 uptime echo 内存使用 TOP10 ps -eo pmem,pid,comm --sort-pmem | head -11 echo CPU 使用 TOP10 ps -eo pcpu,pid,comm --sort-pcpu | head -11 echo 僵尸进程数量 ps -eo stat | grep -c defunct echo 当前所有用户的 crontab for user in $(cut -f1 -d: /etc/passwd); do crontab -l -u $user 2/dev/null echo --- $user --- done echo 系统级 cron 任务 ls -la /etc/cron.d/ /etc/cron.hourly/ /etc/cron.daily/ 2/dev/null echo systemd timers systemctl list-timers --all --no-pager这段脚本看起来平平无奇但实际能一句话看出系统是否有隐蔽的异常。有一次我就是通过“僵尸进程数量”发现某个老旧 PHP 长驻进程在持续泄漏任务进而在巡检工具中连一条异常告警都没触发的情况下提前暴露问题。建议你把它放进 cron 里每天跑一次输出重定向到日志再配合简单的 grep 告警规则基本就是一个小型的健康检查系统了。关于计划任务还有一个值得养成的习惯给任务加 LOCK 防止重入。比如一个脚本可能要跑 10 分钟但 cron 每 5 分钟触发一次就会造成两个实例同时运行轻则资源竞争、重则数据错乱。用 flock 能优雅解决#!/bin/bash exec 9/var/run/mytask.lock flock -n 9 || exit 1 # 业务代码...另外cron 脚本的执行结果建议推送到集中告警通道比如写入数据库或者调用 webhook。否则任务失败了日志躺在服务器角落只有事后翻才看到就等于没有计划任务。我个人在实际操作中的体会是进程管理和计划任务看着都是命令行的基本功但它们才是 Linux 运维的“地基”。地基松了上面无论是 Docker、Kubernetes 还是大数据框架都会在你最意想不到的时候给你上一课。最后再分享一个小技巧排查任何进程或任务问题第一步永远是date看时间、uptime看负载、df -h看磁盘因为这三样过不去后面全白折腾。记住这个顺序能少熬很多夜。
返回列表