很多刚接触Linux的朋友都有过这种经历:命令背了一堆,书也翻了不少,但真到了机器上,发现进程杀不掉、系统莫名卡顿、写好的脚本定时任务就是不执行。问题出在哪里?多半是没把进程管理和计划任务管理这两块基础功打扎实。
这篇文章不打算从UNIX历史讲起,也不搞那种“xxx命令详解”的字典式罗列。我直接按实际工作里最常用的场景来拆,把进程从生到死的过程讲透,把计划任务从at到cron再到systemd timer串起来,最后给出几个我在生产环境里真实踩过的坑和排查思路。无论你是刚入门的学生、准备面试的求职者,还是已经在做运维开发想补短板的工程师,这篇文章都能给你一些能直接用的东西。
1. 进程的本质:从一行命令到系统中的“活体”
1.1 程序和进程的区别,以及为什么PID这么重要
很多人会把“程序”和“进程”混为一谈。简单说,程序是磁盘上一个静态的文件,比如/usr/bin/nginx这个可执行文件,它躺在那里,什么也不干。而进程是程序被加载到内存后运行的实例,是系统进行资源分配和调度的基本单位。
这个区别在实操中有个很直接的体现:你可以同时运行同一个程序的多个进程。比如Nginx,主进程(master)负责管理,多个工作进程(worker)负责处理请求。它们运行的是同一份代码,但却是各自独立的进程,有各自的PID(Process ID,进程标识符)、独立的内存空间、独立的文件描述符。
PID就是进程的身份证号,系统中每个进程都有一个唯一的PID。之所以强调它,是因为我们后续所有操作——查看进程状态、发送信号、调整优先级、终止进程——几乎都要围绕PID来展开。你可以在终端用echo $$查看当前Shell的PID,用echo $!查看刚放到后台执行的进程PID,这两个是日常脚本里很常用的特殊变量。
还有一点必须知道:PID是会复用的。系统不会无限增长PID,当进程退出后,它的PID可能被后续新创建的进程复用。所以在写脚本时,如果要根据PID做判断,一定要先确认这个PID对应的进程是否还是你关心的那个,否则可能误杀无辜。
1.2 进程状态机:R、S、D、Z、T到底代表什么
ps aux输出里的STAT列,很多人只会看R和S,但实际排查问题时,D和Z才是真正让人头疼的。
- R(Running/runnable):进程正在运行或处于运行队列中,随时可能被调度到CPU上执行。
- S(Sleeping):可中断睡眠。进程在等待某个事件完成,比如等待I/O、等待网络数据,这种状态可以被信号唤醒。
- D(Uninterruptible sleep):不可中断睡眠。通常是进程在内核态等待I/O完成,比如等待磁盘读写。这种状态不能被信号打断,你用
kill -9也杀不掉,只能等I/O完成或者重启系统。排查时如果发现大量D状态进程,八九不离十是磁盘或存储出了问题。 - Z(Zombie):僵尸进程。子进程已经终止,但父进程没有调用
wait()系统调用去回收它的退出状态,导致这个进程的进程表项一直残留在系统里。后面会专门讲怎么处理。 - T(Stopped/traced):进程被暂停,比如按了
Ctrl+Z,或者被调试器(如gdb)挂起。
理解状态机是后续排查问题的前提。比如你用kill -9杀一个进程,发现怎么都杀不掉,ps一看是D状态,那就别再跟它较劲了,去查存储和I/O才是正道。
1.3 父子进程关系与孤儿进程
Linux的进程不是凭空产生的,除了0号进程(系统启动时创建)之外,所有进程都是由另一个进程通过fork()系统调用复制出来的,这个“另一个进程”就是父进程,新产生的就是子进程。ps -ef输出里的PPID就是父进程PID。
这种父子关系在实际运维中意义重大:
- 信号传递:很多终止操作会连带影响子进程。
- 资源回收:父进程负责回收子进程的资源,如果父进程挂掉了,子进程会变成孤儿进程(Orphan),被系统的1号进程(通常systemd)收养。
- 进程管理:用
pkill -P 父PID可以精准杀掉某个进程的所有子进程,这在清理服务进程树时非常好用。
写脚本时需要留意:如果你在Shell脚本里启动了一个后台进程,当脚本退出时,这个后台进程可能并不会跟着退出,它会变成孤儿进程被systemd收养继续运行。如果想要它跟着脚本生命周期走,需要做更精细的处理,比如用trap捕获退出信号进行清理。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 查看进程:ps、top与一切你需要的“眼睛”
2.1 ps命令的常用组合与字段含义
ps是process status的缩写,是查看进程静态快照的核心工具。语法上有BSD风格和UNIX风格,我平时最常用的是这两组:
bash复制# 查看当前终端相关的进程
ps
# 查看系统所有进程,以完整格式输出
ps -ef
# 查看系统所有进程,带用户、CPU、内存占用等(BSD风格)
ps aux
ps aux输出里的关键字段:
| 字段 | 含义 | 排查提示 |
|---|---|---|
| USER | 运行该进程的用户 | 属主不对可能意味着提权或配置错误 |
| PID | 进程ID | 定位进程的唯一标识 |
| %CPU | CPU使用率(累计平均值) | 瞬时值看top更准 |
| %MEM | 内存使用率(占物理内存百分比) | 结合VSZ/RSS看真实占用 |
| VSZ | 虚拟内存大小 | 包含未实际分配的地址空间 |
| RSS | 常驻内存大小 | 实际占用的物理内存,排查内存问题重点看这个 |
| STAT | 进程状态 | 见上文状态机 |
| START | 进程启动时间 | 排查异常进程时确认启动时间很有用 |
| COMMAND | 执行的命令 | 注意有些进程会用[...]括起来,表示内核线程 |
我排查进程时,第一步永远是:
bash复制ps -ef --sort=-%cpu | head -20 # 按CPU降序
ps aux --sort=-rss | head -20 # 按内存占用降序
这条命令能最快帮我找到系统里最“活跃”的进程是谁。
2.2 top和htop:动态监控的正确姿势
ps是静态的,而top可以实时刷新。刚接触Linux时我只会打开top然后盯着看,发现CPU飙高就按k输入PID把进程杀了。但用久了才明白,top里面有很多容易被忽略但非常关键的信息。
进入top后,有几个快捷键建议直接记下来:
P:按CPU使用率排序M:按内存使用率排序1:展开/收起每个CPU核心的使用情况c:显示完整命令行z:高亮当前运行的进程x:高亮当前排序的列e:切换内存显示单位(从KB到MB到GB),老版本可能用E切总内存单位
我习惯进入top后先按1看一眼每个核心的负载是否均匀,按c和z方便定位进程。如果某个进程CPU占用持续在100%以上(多核情况下可以超过100%),再按M看看是不是内存泄漏导致的频繁GC或swap。
htop是top的增强版,支持鼠标操作、树状视图(F5)、信号发送(F9),还支持按用户过滤。Ubuntu和CentOS上安装都很简单:
bash复制# Ubuntu/Debian
apt install htop
# CentOS/RHEL
yum install htop
说实话,生产环境很多机器没有htop,但top是标配,所以两样都得会,我个人习惯在排查问题时用top,日常巡检看整体情况用htop更直观。另外还有atop,它能记录历史负载数据,适合事后回溯,感兴趣的可以自己研究下。
2.3 按条件精确查找进程:pgrep、pidof与pgrep家族
服务器上进程那么多,靠ps的输出然后grep只是最基础的做法。更推荐的是用专门的查找工具:
bash复制pgrep -u nginx # 查找nginx用户的进程
pgrep -f 'python.*server.py' # 匹配完整命令行
pidof nginx # 精确匹配进程名,输出PID列表
pgrep -a sshd # -a 显示进程名和完整命令行
pgrep -f的-f参数很实用,它会匹配完整的命令行字符串,而不仅仅是进程名。写监控脚本时,比如要判断某个服务是否存活,我常常这么写:
bash复制#!/bin/bash
if pgrep -f '/usr/local/bin/app-server' > /dev/null; then
echo "App server is running"
else
echo "App server is down"
systemctl start app-server
fi
注意pgrep要写在if条件里时要配合> /dev/null,避免脚本输出多余的PID干扰日志。这一点看似小,但在生产环境的定时脚本里特别重要,输出太多会把日志文件塞满。
3. 进程控制实战:从前后台切换到底层信号
3.1 把进程放到后台:&、nohup、setsid的取舍
运行一个耗时任务,最忌讳的就是一直占着终端。这里的操作很有讲究。
场景一:任务只是临时跑一下,不需要保持终端关闭后继续
直接用&把任务放到后台:
bash复制./long_running_task &
这个进程的父进程还是当前Shell,如果终端退出,Shell会发送SIGHUP信号给它,进程可能随之终止。可以用jobs查看当前终端下的后台任务,用fg %1把编号为1的后台任务调回前台,bg %1把暂停的任务放到后台继续跑。
场景二:任务需要完全脱离终端,终端关了也要继续跑
生产环境最常用的方式:
bash复制nohup ./long_running_task > /var/log/task.log 2>&1 &
nohup的作用就是忽略SIGHUP信号,终端退出后进程不会被挂断。> /var/log/task.log 2>&1是把标准输出和错误输出都重定向到日志文件,如果不重定向,进程可能会因为写入已关闭的终端而报错。
场景三:任务需要完全脱离会话,并且之后在任意终端都能找到它
bash复制setsid ./long_running_task < /dev/null > /var/log/task.log 2>&1 &
setsid会创建一个新的会话(session),进程完全脱离当前终端和进程组,比nohup更彻底。
这三种方式的区别和选择是面试常考点,也是生产环境管理后台任务的必修课。我个人的经验是:临时调试用&,通用部署用nohup,真正需要守护进程级别的任务用setsid或systemd service,后者最规范。
3.2 终止进程:kill、killall、pkill与信号的本质
kill这个名字其实起得有点误导性,它并不是“杀死”进程,而是“向进程发送信号”。信号的种类有很多,最常用的几个:
| 信号 | 编号 | 默认行为 | 用途 |
|---|---|---|---|
| SIGTERM | 15 | 终止进程(可被捕获/忽略) | 优雅停止,程序可以清理资源后退出 |
| SIGKILL | 9 | 强制终止(不可捕获/忽略) | 进程无响应时兜底,但可能留下脏数据 |
| SIGHUP | 1 | 终止进程(可被捕获) | 终端断开时触发,很多守护进程用它重载配置 |
| SIGINT | 2 | 中断进程 | 等价于Ctrl+C |
| SIGSTOP | 19 | 暂停进程(不可捕获) | 等价于Ctrl+Z |
| SIGCONT | 18 | 继续运行暂停的进程 | 配合SIGSTOP使用 |
日常操作用法:
bash复制kill 1234 # 默认发送SIGTERM,优雅终止PID 1234
kill -9 1234 # 发送SIGKILL,强制终止
kill -HUP $(pidof nginx) # 让nginx重新加载配置
killall nginx # 按进程名终止所有nginx进程(注意会全部杀掉)
pkill -f 'java.*gateway' # 按完整命令行匹配杀进程
pkill -u testuser # 杀掉某个用户的所有进程
这里我再三强调:先试SIGTERM,再试SIGKILL,绝对不是所有情况都无脑kill -9。 数据库、消息队列这类有状态的程序,SIGKILL可能导致数据文件损坏或需要长时间恢复。正常的操作顺序是:先kill PID等几秒,不行再kill -9 PID。
3.3 调整优先级:nice和renice的实战用法
Linux内核用CFS(完全公平调度器)来调度进程,但不同进程的“权重”不同,这个权重通过nice值来体现。nice值范围是-20到19,数值越低,优先级越高。默认进程nice值是0。
创建进程时用nice指定优先级:
bash复制nice -n 10 ./heavy_task # 以nice值10运行,降低优先级
nice -n -5 ./urgent_task # 以nice值-5运行,提高优先级(通常需要root)
对已运行的进程调整优先级用renice:
bash复制renice -n 10 -p 1234 # 把PID 1234的nice值调为10
renice -n 5 -u testuser # 把test用户所有进程的nice值调为5
什么场景需要调优先级?比如一个服务器上同时跑着线上数据库和离线数据分析批处理任务。如果不做任何调整,批处理任务可能拖慢数据库响应。这时候对批处理任务nice -n 15,就能把CPU资源优先让给数据库。这比单纯用cpulimit限CPU更符合Linux的调度哲学。
4. 计划任务管理:从一次性at到周期性cron
4.1 一次性任务:at命令的实践与场景
cron适合周期性的任务,但如果你只需要在某个时间点执行一次命令,更合适的工具是at。
使用方式:
bash复制# 安装(大多数发行版默认没装)
# Ubuntu/Debian
apt install at
# CentOS/RHEL
yum install at
# 启动守护进程
systemctl enable --now atd
# 指定时间执行任务
echo "/usr/local/bin/backup.sh" | at 02:30
echo "systemctl restart nginx" | at now + 10 minutes
at 14:00 <<< "echo '提醒自己下午开会' | mail -s 'Meeting' user@example.com"
at的交互模式(直接输入at 02:30回车)适合临时输入多条命令,但脚本化使用用管道或<<<更规范。查看和删除任务:
bash复制atq # 查看当前用户的所有待执行任务
atrm 5 # 删除编号为5的任务
「一次性的、非周期性的、临时的」任务用at,周期性的、规律性的任务才用cron,这是工具选型的基本原则。比如“数据库今天凌晨要迁移,需要3点停应用、3点10分导出数据”这种多次执行但仅此一次的场景,写一个带sleep的at任务链最适合。
4.2 crontab语法、环境变量与常见坑
cron是Linux下最核心的计划任务工具,配置方式有两种:系统级的/etc/crontab和用户级的crontab -e。
cron表达式是5个字段:分钟(0-59)、小时(0-23)、日(1-31)、月(1-12)、星期(0-7,0和7都代表周日)。
bash复制# 用户级crontab,语法示例
* * * * * # 每分钟执行一次
*/5 * * * * # 每5分钟执行一次
0 2 * * * # 每天凌晨2点执行
30 4 * * 1-5 # 工作日(周一至周五)4:30执行
0 0 1,15 * * # 每月1号和15号0点执行
使用crontab -e编辑任务,crontab -l查看任务,crontab -r删除所有任务。
这里必须提最经典的坑:cron的环境变量极简。
cron执行时的PATH环境变量通常只有/usr/bin:/bin,不包含你系统里可能配置的/usr/local/bin、/opt/software/bin等路径。这就导致什么现象?你在终端手动执行好好的python3 /opt/scripts/run.py,放到crontab里就是不执行,或者报“command not found”。
解决方案有两种,我建议双管齐下:
bash复制# 方式一:脚本内使用绝对路径,并在crontab中定义PATH
PATH=/usr/local/bin:/usr/bin:/bin
0 2 * * * python3 /opt/scripts/run.py >> /var/log/run.log 2>&1
# 方式二:脚本开头重新定义环境
#!/bin/bash
export PATH=/usr/local/bin:/usr/bin:/bin
source /etc/profile
另一个经典坑:crontab中%是特殊字符,会被解释成换行符,导致命令被截断。如果你需要在命令里用到%,比如date +%Y%m%d,必须转义成\%:
bash复制0 2 * * * echo "backup_$(date +\%Y\%m\%d)" >> /tmp/test.log
第三个坑:脚本执行结果重定向。如果不重定向,cron默认会用系统邮箱把输出发给当前用户,但绝大多数服务器没有配置邮件服务,导致邮件积压,或者任务明明成功了却看不到输出。规范做法是每个任务都加>> /var/log/xxx.log 2>&1,这样出问题还有日志可查。
4.3 进阶选择:systemd timer是cron的现代替代
很多Linux发行版已经把systemd作为默认init系统,systemd自带timer机制可以完全取代cron的部分功能,而且更强大。它的优势在于:
- 可以设置依赖关系(比如网络就绪后才执行)
- 可以设置精确到微秒级别的重复间隔
- 任务状态、上次执行时间、下次执行时间一目了然
- 日志统一由journald管理
- 支持随机延迟(
RandomizedDelaySec),很适合防止大量机器同时执行任务造成负载峰值
一个最简单的timer,需要两个文件。
/etc/systemd/system/mytask.service:
ini复制[Unit]
Description=My backup task
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
/etc/systemd/system/mytask.timer:
ini复制[Unit]
Description=Run mytask every day at 2am
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.target
然后:
bash复制systemctl daemon-reload
systemctl enable --now mytask.timer
systemctl list-timers # 查看所有timer
Persistent=true的含义是:如果机器在计划执行时间点处于关机状态,下次开机后会补执行错过的任务。这个特性是cron没有的,在笔记本和按需启动的虚拟机上非常实用。
5. 实战排查:当系统变卡,如何一步步定位进程问题
5.1 CPU飙高:分清用户态、内核态与负载均值
排查CPU问题,我有一套固定的流程。第一步先看uptime,看1、5、15分钟负载均值:
bash复制uptime
# 输出示例:09:30:22 up 10 days, 2:13, 1 user, load average: 4.50, 2.10, 1.30
负载均值高于CPU核心数就说明系统过载了。但注意,负载高不一定等于CPU都跑满了,也可能是大量进程处于D状态(等待I/O),这时候CPU可能很闲但系统响应极慢。
第二步用top按1查看每个CPU核心的使用率,重点看us(用户态)、sy(内核态)、wa(等待I/O)占比:
us高:用户态程序在跑计算任务,查具体是哪个进程。sy高:系统调用频繁或内核态死循环,可能跟驱动、文件系统有关。wa高:磁盘I/O成为瓶颈,查iostat或iotop。
第三步定位进程:top按P排序,记住异常进程的PID,然后:
bash复制# 查看进程的启动时间,确认是不是历史遗留的
ps -p PID -o lstart,etime,cmd
# 查看进程的线程级CPU占用(多线程程序很多,需要定位到线程)
top -H -p PID
排查CPU飙高的一个真实案例:一次线上Java应用CPU持续100%,top看到PID后top -H -p PID发现一个线程占用超高,用printf '%x\n' 线程PID转成十六进制,然后在jstack PID | grep -A 20 十六进制线程号里定位到具体代码行,最后发现是正则表达式回溯导致的死循环。这是典型的用户态CPU问题排查链路。
5.2 内存不足:RSS、Swap与OOM Killer
内存问题常见的现象是:应用响应越来越慢,甚至被系统杀掉。排查思路:
先用free -h看整体内存和swap使用情况:
bash复制free -h
然后ps aux --sort=-rss | head -10查看RSS排序后的进程内存占用。
内存泄漏是长期运行服务最常见的隐性故障。怎么判断?如果进程RSS随时间线性增长、从不回落,基本可以断定有内存泄漏。配合日志检查是否频繁触发GC(Java)或malloc_trim(C/C++)来确认。
当系统内存耗尽时,内核会启动OOM Killer,挑一个进程杀掉释放内存。查看过去被杀的记录:
bash复制dmesg -T | grep -i 'killed process'
journalctl -k --since "1 hour ago" | grep -i 'oom'
一个很典型的教训:别在内存紧张的服务器上随意打开-Xmx设得过高的Java应用,OOM Killer的评分跟进程占用的内存量直接相关,越大越容易被杀。设置/proc/PID/oom_score_adj可以调整优先级,但更根本的办法是合理规划内存分配。
5.3 僵尸进程:成因、危害与清理方法
僵尸进程是初学者最容易懵的状态。它的成因是:子进程退出时,会向父进程发送SIGCHLD信号,父进程需要调用wait()或waitpid()系统调用来获取子进程的退出状态,从而回收资源。如果父进程没有调用(比如父进程是一个有bug的程序,或者在等待其他事情),内核不能直接丢弃子进程的任务结构体,于是子进程就处于Z状态。
注意:僵尸进程不占用CPU和内存(它的内存已经被释放了,只剩下内核中的进程表项),但会占用进程表空间。 进程表是有上限的,如果僵尸进程堆积到几千个,可能导致系统无法创建新进程。
常见处理方式优先级从高到低:
- 修复父进程,让它正确调用wait()。这是根本解法。
- 杀掉父进程,让僵尸进程变成孤儿,被systemd(PID 1)收养,systemd会自动回收它们。这是最快的临时方案。
- 如果父进程是init并且永不回收(旧系统上PID 1不是systemd时可能这样),只能重启系统。
查看僵尸进程:
bash复制ps aux | awk '$8=="Z" {print}'
ps -eo pid,ppid,stat,cmd | awk '$3=="Z" {print}'
发现大量僵尸进程后,pkill -9 父进程PID会连带把僵尸进程父进程杀掉,然后僵尸进程被系统接管清理。这个方法在遇到无法修改代码的第三方软件产生僵尸进程时非常有效。
5.4 进程消失与重启:谁杀死了我的进程
“进程跑着跑着不见了”是运维中最常见也最让人抓狂的问题之一。排查思路我认为应该按以下顺序:
第一步,查OOM:
bash复制dmesg -T | grep -i -E 'killed process|out of memory'
不到一秒就能确认是不是内存不足被内核杀了。
第二步,查systemd/服务的重启记录:
bash复制systemctl status myservice
journalctl -u myservice --since "1 hour ago"
如果服务被systemd主动重启,journalctl会记录原因。
第三步,查有没有人手动kill:
bash复制grep -i 'killed' /var/log/secure # CentOS/RHEL
grep -i 'killed' /var/log/auth.log # Ubuntu/Debian
如果所有日志都查不到痕迹,说明要么是进程自己崩溃退出(看应用自己的日志),要么是被kill -9且系统没有记录(一般用户手杀不记录)。到最后一步,就得靠监控系统(Zabbix、Prometheus)回溯历史指标了。
6. 计划任务与进程管理:生产环境常见坑与面试高频题
6.1 Crontab任务不执行,按这个顺序排查
“crontab加了,但就是不跑”,我每年能遇到十几个人来问。排查建议按这个顺序:
- 确认cron服务在运行:
systemctl status crond(CentOS)或systemctl status cron(Ubuntu),没有运行就systemctl enable --now crond。 - 确认任务语法正确:
crontab -l查看任务,人工检查时间是今天/明天对应的准确值。最容易错的是把“分 时 日 月 周”的顺序搞反。 - 确认脚本权限:crontab执行的脚本必须有执行权限:
chmod +x /path/to/script.sh。注意cron不会像终端那样继承用户的环境变量和PATH,脚本第一行务必备好#!/bin/bash。 - 查看日志:
bash复制# CentOS/RHEL
tail -f /var/log/cron
# Ubuntu/Debian
grep CRON /var/log/syslog
日志会明确显示任务是否被触发、执行的命令、返回码。
5. 手动执行一遍脚本:用脚本里写的绝对路径手动跑一遍,如果手动执行报错但cron里没写重定向,报错信息会丢失。这也是我之前反复强调要加重定向的原因。
6.2 面试常考:如何设计一个可靠的定时任务体系
问“你会怎么管理服务器的定时任务”时,如果只回答“用crontab”,大概率只能拿基础分。想拿加分,需要表现出体系化的思考:
- 脚本用绝对路径,不要依赖相对路径和用户环境。
- 每个任务必须有日志,单独文件,按日期切割:
>> /var/log/backup_$(date +\%Y-\%m-\%d).log 2>&1。 - 任务锁防重入:防止上一个任务没跑完、下一个任务又启动。用
flock是POSIX标准做法:
bash复制0 2 * * * /usr/bin/flock -xn /tmp/backup.lock -c "/usr/local/bin/backup.sh"
-x是排他锁,-n是非阻塞(拿不到锁直接失败不等待),这样如果上次备份超过24小时,下次任务不会并发执行。
- 确认任务状态:定期检查上次执行结果,而不是配置完就不管。cron任务失败是静默的,没有通知机制,最好在脚本里加上失败告警(如调用企业微信机器人Webhook或发邮件)。
- 考虑跨时区和夏令时:服务器统一使用UTC,然后在脚本里转换本地时间,避免定时任务的执行时间受到夏令时影响。
- 统一入口:把所有cron任务集中到一个文件(如
/etc/cron.d下建一个app-jobs文件),而不是散落在各个用户下,方便review和审计。
6.3 从“kill -9杀不掉”到“内存泄漏”:几道经典面试思路
面试技术岗,进程管理和计划任务几乎是必聊话题。总结几个高频问题和应对思路:
“僵尸进程是怎么产生的?如何避免?”
回答分两层:产生原因是父进程没有调用wait();避免的办法是“不让父进程成为那个不负责的进程”和“让子进程被专门的子reaper进程收养”。如果父进程是自己写的,必须正确调用wait/waitpid;如果是第三方进程,监控到大量Z的时候及时处理。
“为什么kill -9不一定能杀掉进程?”
因为SIGKILL不可被捕获和忽略,但它依然需要在进程被调度到的时刻才生效。如果进程处于D状态(不可中断睡眠,比如正在等待磁盘I/O),内核根本不会把信号递送给它,所以杀不掉。操作系统不是“立即”处理信号的,信号有递送时机,对R和S状态进程是即时的,对D状态进程则要等I/O返回。
“内存泄漏如何排查?”
先free看整体,再ps aux --sort=-rss定位进程,配合top观察RSS趋势。Java应用用jstat -gcutil看GC频率,C/C++用valgrind或/proc/PID/status里的VmRSS判断。容器环境还要注意cgroup限制导致的容器内进程被OOM Kill,但宿主机的dmesg却不一定能看到。
“一个任务需要在每月最后一个周五的凌晨2点执行,怎么写cron?”
cron的5位时间格式无法直接表达“最后一个周五”。大多数人的答案是用0 2 * * 5(每个周五都执行)然后脚本里判断“如果今天之后的下一个周五已经跨月,就不执行”。这个思路是考察对cron机制的理解和对复杂需求的拆解能力。当然,用systemd timer加OnCalendar=Fri *-*~1..7 02:00:00这样的写法更优雅,但生产环境不一定都支持。两条路都讲出来才显得全面。
7. 实践项目:一键实现“自动备份 + 进程守护 + 告警”的完整脚本
理论说再多,不如一个能直接用的脚本。下面是我在一个客户环境里实际使用的、支持多服务自动备份和进程守护的完整示例,建议直接抄到你的环境做适配。
项目需求:
- 每天凌晨2点备份MySQL数据库
- 每天凌晨3点打包Nginx日志,清理7天前的历史日志
- 每5分钟检查核心服务端口,若服务异常则尝试拉起并告警
/usr/local/bin/backup_db.sh:
bash复制#!/bin/bash
# 数据库自动备份脚本
BACKUP_DIR=/data/backup/mysql
DB_USER=backup
DB_PASS='YourStrongPasswordHere'
KEEP_DAYS=7
mkdir -p "${BACKUP_DIR}"
DATA_TIME=$(date +%Y-%m-%d_%H-%M-%S)
mysqldump -u"${DB_USER}" -p"${DB_PASS}" --all-databases --single-transaction --routines --triggers > "${BACKUP_DIR}/all_db_${DATA_TIME}.sql"
if [ $? -eq 0 ]; then
gzip "${BACKUP_DIR}/all_db_${DATA_TIME}.sql"
echo "$(date '+%F %T') Backup success: all_db_${DATA_TIME}.sql.gz" >> /var/log/backup_db.log
else
echo "$(date '+%F %T') Backup FAILED!" >> /var/log/backup_db.log
exit 1
fi
# 删除7天前的备份文件
find "${BACKUP_DIR}" -name "all_db_*.sql.gz" -type f -mtime +${KEEP_DAYS} -delete
/usr/local/bin/rotate_nginx_log.sh:
bash复制#!/bin/bash
# Nginx日志按天切割并清理7天前的日志
LOG_DIR=/var/log/nginx
YESTERDAY=$(date -d "yesterday" +%Y-%m-%d)
mv "${LOG_DIR}/access.log" "${LOG_DIR}/access_${YESTERDAY}.log"
mv "${LOG_DIR}/error.log" "${LOG_DIR}/error_${YESTERDAY}.log"
kill -USR1 $(cat /var/run/nginx.pid)
# 给nginx一点时间完成日志重写
sleep 1
find "${LOG_DIR}" -name "access_*.log" -type f -mtime +7 -delete
find "${LOG_DIR}" -name "error_*.log" -type f -mtime +7 -delete
echo "$(date '+%F %T') Nginx log rotation done" >> /var/log/nginx_rotate.log
/usr/local/bin/check_services.sh:
bash复制#!/bin/bash
# 核心服务守护脚本:发现异常自动拉起并告警
SERVICES=(
"nginx:80"
"mysqld:3306"
"redis-server:6379"
)
check_and_restart() {
local service_name=$1
local port=$2
# 方式一:检查端口是否监听
if ! ss -tlnp | grep -q ":${port} "; then
echo "$(date '+%F %T') ${service_name} is DOWN, trying restart..." >> /var/log/service_guard.log
systemctl restart "${service_name}"
sleep 5
if ss -tlnp | grep -q ":${port} "; then
echo "$(date '+%F %T') ${service_name} restarted successfully" >> /var/log/service_guard.log
send_alert "${service_name} was restarted"
else
echo "$(date '+%F %T') ${service_name} restart FAILED!" >> /var/log/service_guard.log
send_alert "${service_name} restart FAILED, consider manual intervention"
fi
fi
}
send_alert() {
local message=$1
# 这里以企业微信机器人Webhook为例,换成你自己的
local webhook_url="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY"
curl -s -H "Content-Type: application/json" -d "{\"msgtype\":\"text\",\"text\":{\"content\":\"[预警] ${message}\"}}" "${webhook_url}" > /dev/null
}
for item in "${SERVICES[@]}"; do
service_name="${item%%:*}"
port="${item##*:}"
check_and_restart "${service_name}" "${port}"
done
crontab配置:
bash复制# 每天凌晨2点备份数据库
0 2 * * * /usr/local/bin/backup_db.sh
# 每天凌晨3点切割日志
0 3 * * * /usr/local/bin/rotate_nginx_log.sh
# 每5分钟检查服务状态
*/5 * * * * /usr/local/bin/check_services.sh >> /var/log/service_guard.log 2>&1
这套方案的核心思路是:脚本都写绝对路径,日志全部单独落盘,进程守护用端口检测配合systemctl,告警直接推到手机。kill -USR1是Nginx优雅重开日志的标准方式,比直接restart影响小得多。
我实测这套脚本在一台2核4G的服务器上稳定运行了大半年,唯一一次告警是磁盘写满导致mysqldump失败。当时backup_db.sh的日志里清晰地记录了失败原因,我扩容磁盘后就恢复了。这也印证了前面说的:定时任务一定要有日志,否则出问题连原因都找不到。
8. 个人经验总结:基础命令背后的系统思维
写了这么多,最后聊几句我的体感。
进程管理和计划任务管理,表面上是一堆命令的组合:ps、kill、crontab、at、systemctl……但把这些命令串起来的是一套系统思维——你要知道你管理的对象是谁(进程),它的状态是什么(进程状态机),它在什么条件下会被执行(调度),它什么时候被安排执行(cron/at/timer),它挂了之后如何被接住(systemd/守护脚本)。
初学阶段,我建议把这篇文章里的命令全部亲手敲一遍,然后自己构造一些故障场景来练习:跑一个死循环进程然后杀掉它;写一个cron任务故意不重定向看日志会发生什么;用flock试一下任务锁的效果。这些东西只有自己亲手踩过,才真正属于你。
面试时,比起把命令背得多流利,面试官更看重的是你在实际场景中怎么决策。比如你可以主动提到“我习惯先用SIGTERM优雅退出,不行再SIGKILL”,提到“cron任务我会给每个脚本都加flock锁”,这些细节才是拉开差距的地方。希望这篇文章能帮你把基础打得更牢。
