1. 开篇:从"手动排队"到"自动调度"
如果你管过几台服务器,大概率经历过这样的场景:凌晨两点,业务低峰期,你算好了这会儿跑一个数据清理脚本最合适,但总不能真定个闹钟爬起来敲命令。又或者,某个临时任务需要在三小时后执行一次,但你正在开会、在路上、在睡觉,就是不在电脑前。
红帽134课程第2章"调度未来任务"解决的正是这类需求——让Linux系统在指定的时间点自动执行任务,把"人肉盯梢"变成"系统代办"。整章内容围绕两条技术主线展开:at/atq/atrm负责一次性任务的预约执行,cron/crontab负责周期性任务的长期自动运行。别小看这两个工具,它们几乎是所有Linux服务器运维工作的地基技能。无论是日志切割、应用重启、备份归档,还是报告生成,都依赖这套调度机制在背后默默跑着。
这篇文章我不会按课件顺序给你念概念,而是把这一章浓缩成一套能够直接落地的实操笔记:命令怎么用、配置怎么写、坑在哪里、权限怎么管、日志怎么查,全部按我实际维护服务器的经验来讲。不管你是备考认证,还是刚接手服务器的日常运维,读完都应该能独立搞定"让任务在指定时间自动执行"这件事。
2. 一次性任务:at命令族的完整用法
2.1 核心思路:把这个任务"存进"系统的时间表
先搞清楚一个底层逻辑:at命令做的事情并不是"定时器到了就去执行",而是把任务提交给atd守护进程,由它管理一个任务队列,到了时间就触发执行。
这个理解很关键,因为它解释了三个使用上的规范:
- at提交的任务是"一次性"的,执行完就出队,不会有二次触发。
- atd服务必须在运行状态,不然提交的任务不会被处理。
- 任务交给系统后,即使你退出终端甚至关机,只要系统在任务时间点处于运行状态,它就会执行。
从使用者的操作视角看,整个过程三步走:
- 用at命令提交任务,进入一个"输入模式"。
- 输入要执行的命令,用Ctrl+D结束输入。
- 用atq查看队列,用atrm取消任务。
这套流程非常简单,但要写出正确的命令,需要理解at的时间语法。
2.2 时间格式与实战示例
at命令最值得花时间掌握的就是它的时间表达方式。官方语法允许你使用相当灵活的组合,我第一次接触时也花了一点功夫才捋清楚。拆开来就三类:
- 绝对时间:
HH:MM(时分)、HH:MM AM/PM(上午下午) - 相对时间:
now + N minutes/hours/days/weeks - 特定时间词:
midnight(午夜)、noon(中午)、teatime(下午4点)
还有几种组合写法:
- 指定日期:
at 09:30 2025-06-15,注意日期格式是YYYY-MM-DD - 指定月和日:
at 09:30 Jun 15 - 今天的快捷写法:
at 09:30 today、at 09:30 tomorrow
为了避免搞混,我列个表:
| 写法 | 含义 | 适用场景 |
|---|---|---|
at now + 5 minutes | 5分钟后执行 | 测试atd服务是否正常 |
at 23:00 | 今天23:00执行,若已过则明天 | 当天晚间任务 |
at 23:00 tomorrow | 明天23:00执行 | 明确的次日任务 |
at 3:00 2025-07-01 | 指定日期3点执行 | 一次性计划任务 |
at now + 3 hours | 三小时后执行 | 配合超时等待类任务 |
at noon | 今天12点执行 | 简单快捷写法 |
提交任务的完整操作:
$ at now + 5 minutes warning: commands will be executed using /bin/sh at> /usr/local/bin/backup.sh at> echo "backup done" >> /var/log/backup.log at> <EOT> job 3 at Mon Jun 16 14:05:00 2025执行后会返回一个task ID(上面的job 3),记住这个编号,后面取消任务要用。
这里特别提醒一个最容易踩的坑:at默认的解释器是/bin/sh,不是bash。如果你提交的任务里用了bash特有的语法(比如[[ ]]判断、数组、source命令),就可能出现"手动执行没问题、at执行就报错"的现象。稳妥的做法是:所有复杂任务不要直接交裸命令,而是写成脚本文件,然后在at里只调用脚本。写成脚本还有个好处:脚本里的日志输出、错误处理都可以做得更完善,排查问题时思路更清晰。
2.3 atq与atrm:队列查看与任务取消
提交完任务,怎么确认它真的在队列里?怎么反悔?
查队列用atq,这个命令不需要参数,直接列出当前用户所有排队的任务:
$ atq 3 Mon Jun 16 14:05:00 2025 a root 5 Tue Jun 17 02:30:00 2025 a root输出字段分别是任务ID、执行时间、队列字母(a代表at队列)、用户。
如果要查看某个任务具体执行什么命令,用at -c 任务ID:
$ at -c 3取消任务用atrm,格式是atrm 任务ID,支持一次取消多个:
$ atrm 3 5取消后可以再跑atq验证任务确实没了。
从权限管理的角度看,at命令族受两个配置文件控制:/etc/at.allow和/etc/at.deny。
- 系统先检查at.allow,只要有这个文件,只有列在其中的用户能用at。
- 如果at.allow不存在,则检查at.deny,不在at.deny中的用户都能用。
- 两个文件都不存在,只有root能用at。
- 两个文件都存在,以at.allow为准。
实际生产环境中,我见过不少服务器直接限制只有root能用at,这个策略其实很合理。因为at执行的任务是root权限的话,一旦任务内容有误(比如写了一个死循环),影响面可能很大。能用at的人越少,出事的可能越小。
2.4 batch命令:让系统"有空再跑"
at命令族里还有个容易被人忽略的成员——batch。它的作用是:当系统平均负载降到0.8以下时,再执行指定的任务。
用法跟at基本一样:
$ batch at> /usr/local/bin/heavy_job.sh at> <EOT>这个命令的本意是让那些"比较重、但不急于执行"的任务等到系统空闲时再跑,避免跟高峰期业务抢资源。但说实话,生产环境里很少有人用batch,因为负载阈值的可控性太差,不如直接在脚本里做资源判断(比如用uptime读取负载值然后决定是否执行)来得精细。不过考试可能会考,知道它的存在和基本行为就够了。
3. cron周期性任务:配置与权限体系
3.1 系统级还是用户级:先理清两个调度入口
at解决的是"执行一次"的问题,cron解决的是"每隔一段时间反复执行"的问题。cron由crond守护进程驱动,只要系统在运行,它每分钟都会唤醒一次,检查是否有到点的任务要触发。
cron体系里有两类任务:
第一类是用户级任务,用crontab -e编辑,每个用户的配置文件存在/var/spool/cron/用户名里,格式受/etc/cron.allow和/etc/cron.deny控制。
第二类是系统级任务,放在/etc/crontab或者/etc/cron.d/目录下,格式比用户级多一个"用户名"字段,只有root能编辑。
这两类任务虽然都由crond管理,但配置文件的格式、存放位置、权限要求都不一样。初学者最容易混的就是这里——在/etc/crontab里写了五字段的配置,然后发现它不执行,就是因为少了一个用户名字段。
3.2 五字段格式:分钟到星期的排列规则
cron的调度表达式是五个时间字段,按顺序依次是:
分 时 日 月 星期 命令- 分:0-59
- 时:0-23
- 日:1-31
- 月:1-12
- 星期:0-7(0和7都代表周日)
每个字段支持四种写法:
- 固定值:比如
30表示第30分钟 - 通配符:
*表示任意值 - 列表:
,分隔多个值,比如0,30表示0分和30分 - 范围:
-表示区间,比如9-18表示9点到18点 - 步长:
/表示间隔,比如*/5表示每5个单位
只看定义比较抽象,我给几个真实场景的写法:
| 场景 | crontab写法 |
|---|---|
| 每天凌晨2点30分执行备份 | 30 2 * * * /opt/backup.sh |
| 每5分钟检查一次服务状态 | */5 * * * * /opt/check.sh |
| 工作日早9点到晚6点,每30分钟执行一次 | 0,30 9-18 * * 1-5 /opt/report.sh |
| 每月1日和15日执行对账脚本 | 0 3 1,15 * * /opt/reconcile.sh |
| 每周日凌晨3点做全量清理 | 0 3 * * 0 /opt/cleanup.sh |
这里有几个特别容易被绕进去的细节,我踩过的坑都写出来:
- 分钟和小时的对应关系别弄反。
*/5 * * * *才是每5分钟,如果你写* */5 * * *,含义是"每个能被5整除的小时里的每一分钟",也就是每小时跑60次,但只在0点、5点、10点等特殊小时才触发,实际效果跟你预想的完全不同。 - 日字段和星期字段是"或"的关系,不是"与"。比如
0 3 1 * 1,含义是"每月1号或每周一的凌晨3点",而不是"每月1号且是周一"。如果真想表达"某月的1号且恰好是周一",需要写在脚本里用date命令判断再退出。 - 系统crontab里的命令不执行,十有八九是路径问题。cron环境中PATH很精简,通常只有
/usr/bin:/bin,你手动执行没问题不代表cron环境也能找到命令。最省心的做法是脚本里全部使用绝对路径,或者脚本开头先export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin。
3.3 环境感知:为什么cron跑出来的结果和手动不一样
这个话题必须单独拎出来说,因为它在实际运维中制造的问题最多。
crond执行任务时,是从一个非常"干净"的环境里拉起的进程,跟你登录终端时的环境变量完全是两回事。具体差异体现在:
- HOME默认是执行用户的home目录,不是当前目录。
- PATH非常精简,系统安装的很多工具目录都不在默认PATH里。
- 不会加载你用户的
.bash_profile、.bashrc、.profile等初始化文件。
所以经常出现的情况是:你手动执行一个脚本一切正常,放到crontab里后,明明任务也被触发了,但脚本里的某个程序找不到了,或者读取相对路径的文件失败了,或者中文乱码了。
解决思路有三个,我按优先级给你排:
- 脚本顶部加上环境预设,把关键变量显式定义出来。
- 脚本内部全部使用绝对路径,包括命令路径和文件路径。
- 脚本运行日志重定向到指定文件,需要排查时可以直接看。
#!/bin/bash export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin export LANG=en_US.UTF-8 LOG_FILE=/var/log/my_task.log echo "$(date '+%Y-%m-%d %H:%M:%S') task started" >> "$LOG_FILE" /usr/local/bin/actual_job.py >> "$LOG_FILE" 2>&1 echo "$(date '+%Y-%m-%d %H:%M:%S') task finished" >> "$LOG_FILE"这种写法每次执行都有时间戳、有完成标记,日志内容一目了然。如果脚本异常,至少你能从日志里定位是哪一步挂了。
3.4 控制面板:允许和拒绝谁使用crontab
跟at一样,cron也有访问控制,而且逻辑基本一致:
- 检查
/etc/cron.allow,如果存在,只有文件里的用户能用crontab。 - 如果cron.allow不存在,检查
/etc/cron.deny,只要不在deny列表里就能用。 - 两个文件都不存在,默认只有root能用。
需要注意的一个细节是:即使某个用户不在allow/deny限制外,他配置的crontab任务如果恰好也被系统级的某个任务覆盖,可能产生重复执行的效果。比如系统级cron里有一个日志切割任务,某个用户的crontab里又配置了一个针对同一批日志的清理任务,两个任务同时触发就会出现竞争条件,日志文件可能被同时读写。这种问题虽然跟权限无关,但排查起来往往先从crontab列表入手,先确认是不是重复调度。
在管理策略上,我建议对cron权限做收紧处理。一般生产服务器的做法是设置/etc/cron.allow,只让固定的运维账号具备crontab权限,其他账号一律禁用。
4. 定时任务配置与占位目录:功能细节不要漏
4.1 crontab命令的增删改查
用户级crontab最常用的操作,我整理成一套"查-编-看-删-清"流程:
- 查看任务:
crontab -l - 编辑任务:
crontab -e - 临时生效:直接改文件后任务自动生效,不用重启crond
- 删除所有任务:
crontab -r - 恢复误删:如果你没有备份,就真的救不回来了。所以实际操作中,我强烈建议每次修改前先执行
crontab -l > ~/crontab_backup_$(date +%F).txt,做一次配置备份。
crontab -e默认使用vi编辑器打开配置文件,新装的系统可能默认是nano,用起来不习惯的人可以改成vim:
export EDITOR=vimEDITOR=vim crontab -e配置内容写好后,系统会自动检查语法。这个语法检查比较简单,只校验时间字段的合法性,不检查命令能不能执行。所以字段格式正确不代表命令一定有效,还是自己测一把才靠谱。
查一下crond服务是否在运行:
systemctl status crond如果没启动:
systemctl enable --now crond服务不在线这一条,比任何配置错误都更容易被忽略。新手排查crontab问题,第一步就应该是确认crond在跑。
4.2 /etc/cron.d/ 目录与系统占位目录
/etc/cron.d/目录存放系统级cron配置片段,里面每个文件的格式跟/etc/crontab一样,包含用户名字段。跟/etc/crontab不同的是,cron.d目录里的文件在软件包安装时可以被单独管理,修改某个软件包的调度不会影响到主配置文件,冲突概率更低。
此外还有一套"占位目录"机制:/etc/cron.hourly/、/etc/cron.daily/、/etc/cron.weekly/、/etc/cron.monthly/。这些目录的调度入口定义在/etc/anacrontab中,anacrontab是一个专门处理周期性占位任务的调度器,它跟crontab相互独立。
之所以需要一个额外的anacrontab来驱动这些占位目录,原因是:如果系统在某段时间内处于关机状态,原本应该执行的daily任务就错过了。crontab只管"到点就触发",不会去补跑;anacrontab的设计则会补执行错过的任务,而且还可以设定延迟时间和随机延迟,避免多台服务器同时跑任务产生负载尖峰。
如果你经常有"系统可能关机错过任务,但希望开机后补跑"的需求,把脚本放到这四个占位目录里就是一个不错的策略。反之,如果你希望任务严格按时间触发、错过也不补跑,就用/etc/crontab或crontab -e配置。
这里的规则差别值得记一下:
| 机制 | 主要特征 | 适合场景 |
|---|---|---|
| /etc/crontab | 精确时间触发,不补跑 | 对时间敏感的后台任务 |
| /etc/cron.d/ | 同crontab,文件可独立管理 | 软件包自带的调度配置 |
| /etc/cron.hourly/等 | anacrontab管理,错过可补跑 | 日志清理、备份等非精确时间任务 |
4.3 日志与排错:从log里看到任务是否真的跑了
cron任务排错时最核心的看日志命令是:
grep CRON /var/log/cron日志里每一行都记录了一条任务触发信息,包括执行时间、执行用户、执行的命令、进程PID。举例:
Jun 16 03:00:01 hostname CROND[12345]: (root) CMD (/opt/backup.sh)看到这行说明crond已经把/opt/backup.sh交给shell去执行了。如果连这行都没有,说明crontab配置本身就没被加载,要么是文件路径不对,要么是时间表达式写错了。
如果日志里有这行,但任务实际没执行,就要继续往下排查:
- 脚本是否有执行权限:
ls -l /opt/backup.sh - 脚本里的命令是否能找到:手动跑一遍
bash -x /opt/backup.sh - 日志重定向是否写到了有权限的目录
补充一个我自己的实践习惯:任务脚本不要图省事写/dev/null 2>&1,至少保留最近的日志或错误输出。有些运维图日志文件太大,直接丢弃输出,结果任务挂了几天都没发现。折中方案是:日志按天切分,保留最近7天。
4.4 向普通用户开放crontab时要当心的两个问题
如果服务器需要向普通用户开放crontab权限,有两个容易忽视的问题。
一是普通用户任务的执行环境更干净。普通用户登录时可能有某种环境注入机制(比如通过profile加载了某个特定配置),但cron拉起任务时不会加载这些。任务里如果依赖了这些环境变量,跑起来就会异常。
二是普通用户使用crontab执行任务时,任务的所有者就是该用户,权限当然受限。如果脚本里需要操作系统级路径(比如写/var/log),就得配合sudo或调整目录权限。一旦给普通用户sudo权限,他能读取其他用户甚至root的crontab吗?不一定——/var/spool/cron/目录默认只有root能读,普通用户最多只能看自己的crontab。这一点在生产环境上也要有个预期:普通用户crontab任务的排查往往需要root介入,因为相关日志文件普通用户看不了。
5. 实战排查:一次"任务没跑"的完整追查过程
讲几个实际遇到的案例,都是真实排障过程中积累的路径,按"症状-排查-结论"顺序写。
案例1:每分钟任务没执行
现象:crontab里写了*/1 * * * * /root/check.sh,但脚本好像完全没有执行。
排查过程:
- 先确认crond状态:
systemctl status crond,状态正常。 - 再看
/var/log/cron,发现日志里根本没这个任务的触发记录。 - 检查用户的crontab:
crontab -l,命令输出显示*/1 * * * * /root/check.sh确实存在。 - 最后把用户切换到root,查看
/var/spool/cron/root,发现文件内容跟预期一样,但文件末尾有不可见字符。
结论:crontab -e保存时可能带入了隐藏字符,导致整条记录格式异常。处理方式:删除该记录重新录入,保存后用crontab -l输出核对一遍。
案例2:任务触发了但脚本没执行
现象:日志里有CROND触发的记录,但脚本效果没出现。
排查过程:
- 单独执行脚本:
bash -x /root/check.sh,发现脚本本身能跑,但里面用了一个python3脚本,而这台机器python3在手写环境下能找到。 - 再查脚本文件权限:
ls -l /root/check.sh,权限是rwxr-xr--,所有者root,属组root。原因是root的crontab任务默认以为root用户来执行,按说权限没问题。 - 最终把脚本输出重定向到log,加上了绝对路径python3调用,问题解决。
结论:cron执行环境PATH不包含某些路径,你以为能找到的命令在cron环境里找不到。所有外部命令全用绝对路径,脚本开头把PATH写死。
案例3:服务器重启后任务不再执行
现象:服务器重启后,某个系统级任务消失了。
排查过程:
- 检查/etc/crontab,配置还在。
- 检查crond服务,状态正常。
- 查看日志,发现任务在重启前正常执行,重启后没有触发记录。
- 确认/etc/crontab文件里的时间表达式没有问题,然后发现这个任务实际上写在某个用户的crontab里,而该用户账户在重启后处于锁定状态。
结论:用户账户问题导致crontab无法被读取。恢复用户正常状态后,任务恢复执行。如果账户确实不需要登录,也要保留crontab的正常读取权限,或者把任务迁移到系统级配置。
这三个案例几乎可以覆盖大多数"定时任务不执行"的排查思路。按固定顺序从服务状态、触发日志、配置文件、执行权限递进排查,效率远高于逐个命令瞎试。
6. 额外补充:cron实战中的5个"少走弯路"的小技巧
最后分享几条我从日常工作中总结出来的经验,每条都踩过坑,按实用价值排序。
技巧1:先在命令行敲一遍,再往crontab里放
这是最重要的一条规则。任何待执行的脚本,先在终端里手动执行一遍,确认结果正确,再放进crontab。如果脚本在终端里都会失败,放cron里只会更诡异。
技巧2:执行频率过高的任务,小心并发叠加
每5分钟跑一次的任务,如果单次执行超过5分钟,就会产生任务叠加。可以用进程锁避免重复执行,比如在脚本开头加判断:
if [ -f /tmp/my_task.lock ]; then echo "Task already running." exit 0 fi touch /tmp/my_task.lock trap 'rm -f /tmp/my_task.lock' EXIT这种方式虽然简单,但在绝大多数场景下够用了。如果用flock,可参考:
flock -n /tmp/my_task.lock -c '/opt/my_task.sh'技巧3:任务运行结果一定要有记录
不要在crontab里写>/dev/null 2>&1。至少把输出写到日志文件。哪怕是最简单的追加方式:
30 2 * * * /opt/backup.sh >> /var/log/backup_task.log 2>&1这样下次谁值班都能一眼看到任务是否正常。
技巧4:修改完crontab后,先看一次日志
crontab -e保存退出后,等一个执行周期,马上用grep CRON /var/log/cron | tail确认新配置已生效。这个步骤只需要十秒钟,但能帮你减少大量凭空猜测的时间。
技巧5:定期检查crontab的完整性,防止"被清空"
有些恶意脚本或误操作可能清空用户crontab,导致业务任务静默消失。建议每周看一次/var/spool/cron/下各账号任务是否完整,有条件的话把crontab列表纳入备份范围。
7. 关于本章知识的一句话总结
调度未来任务,本质上就是让系统在"对的时间"做"对的事"。at命令处理"一次性提醒",cron处理"周期自动执行",两者搭配可以解决绝大多数运维自动化需求。真正掌握这一章的关键不在于记住命令,而在于理解cron的执行环境、权限控制、时间语法和日志排查套路。这套知识在认证考试中占的分量不多,但在实际运维中使用的频率极高,值得多花时间磨一磨。
以后你接手任何一台服务器,建议第一件事就是查一下它上面已有的定时任务——crontab -l和ls /etc/cron.d/,先搞清楚这台机器在自动做什么,然后再动它。少做一步,都可能遗漏一个隐藏的后台任务,而这正是很多线上故障的起点。