
搞Linux运维或者日常用服务器干活的人早晚都会碰到at和cron这两个命令。很多新手一开始容易搞混觉得它们都是“定时执行任务”用起来应该差不多。实际上这俩东西的定位完全不同at是“一次性预约”cron是“周期性循环”。这篇文章我就从实际运维的角度把这两个命令的用法、背后的机制、常见的坑一次性讲透。先说你什么时候会需要它们。比如你半夜要跑一个数据迁移脚本但你不想熬夜守着或者服务器每天凌晨要清理日志、每周要备份数据库再或者你只是想让系统在某个特定时间点提醒你做一件事。这些都是at和cron的典型场景。理解了它们各自适合干什么你在实际工作中就能选得更准少走很多弯路。1. at和cron到底有什么区别什么场景选谁1.1 用生活里的例子理解两个命令我用一个生活化的类比来解释。at就像你打车时预约的单次行程——提前约好时间车到点来接你这次行程结束就完事。而cron就像你手机里的闹钟——你可以设置成每天早上7点响也可以设置成每个工作日早上响它是按规律循环的。从命令的设计初衷来看at命令生来就是为了处理“仅执行一次”的任务。比如你下午2点要开个会想提前1分钟让屏幕弹个提醒或者你晚上10点要执行一个数据库全量备份但只做这一次。而cron全称是clock daemon适合处理那些需要长期、固定周期执行的维护任务比如每天凌晨三点清理临时文件、每周日凌晨做一次系统增量备份、每月的第一天生成上月的报表。它们的底层机制也不一样。at会把你的任务写到一个队列文件里然后由atd守护进程每分钟扫描一次队列到时间了就执行。cron则是由crond守护进程常驻内存每分钟醒来一次检查当前时间是否匹配用户设定的时间规则匹配了就执行对应命令。所以at更轻量、更临时而cron更规整、更持久。1.2 怎么快速判断该用哪个在实际交流中我习惯用一句话来判断“这个任务是只跑一次还是反复跑”如果答案是只跑一次就用at如果答案是每隔一段时间就要跑或者固定在每天、每周、每月的某个时间点跑那就用cron。当然也有例外。比如你临时改需求需要在5分钟后跑一个脚本这时候用at就很顺手因为它的语法支持相对时间比如at now 5 minutes非常直接。而如果你非要用cron去模拟单次任务虽然技术上可行执行完把定时配置删掉但完全是多此一举而且容易因为忘了删导致任务重复执行给自己埋坑。还有一点要注意at适合“非交互式、无规律、偶发”的任务cron适合“有规律、固定周期”的任务。这里我把它们的区别整理成一个表格方便你对照对比项at命令cron命令任务性质一次性、偶发周期性、循环时间设定绝对时间或相对时间五段式时间表达式守护进程atdcrond配置文件队列目录下的文件crontab文件适用场景临时备份、提醒、一次性执行日志清理、周期备份、定时同步管理命令atq、atrmcrontab -l、crontab -e1.3 两个命令都依赖守护进程无论是at还是cron都不是内核直接提供的功能而是由独立的守护进程负责调度。atd对应at命令crond对应cron命令。如果你发现任务到了时间不执行第一步就去看守护进程的状态。在CentOS、Rocky、AlmaLinux这些基于RHEL的发行版上at和cron通常是预装的但atd可能默认没启动。Debian、Ubuntu系列则一般默认装好并且启动了。我后面会讲到具体的检查命令。另外有些精简版系统或者容器镜像里可能连命令都没装那你需要先安装这个也要注意。2. at命令完全实操一次性的预约任务2.1 先确认安装与守护进程状态在我的服务器上我习惯先跑一个检查命令确认at能不能用which at如果没有任何输出说明系统里没装。基于Debian的系统用sudo apt update sudo apt install at -y基于RHEL的系统用sudo yum install at -y装完之后记得启动atd守护进程并设置开机自启sudo systemctl enable --now atd检查状态systemctl status atd这里我多说一句。很多新手在容器环境里折腾at发现任务不执行就一脸懵。容器里的init进程往往不是systemd你需要直接用atd命令把守护进程跑起来比如atd 或者用supervisor这类进程管理器来托管效果一样。2.2 at命令的三种用法总有一种适合你at命令最常用的就是交互模式。你直接输入at加一个时间回车后进入交互提示符输入要执行的命令最后按CtrlD结束。我举个例子at 14:30 warning: commands will be executed using /bin/sh at echo meeting reminder /tmp/meeting.log at EOT job 5 at Thu Sep 14 14:30:00 2024这样就把“往/tmp/meeting.log写入一行提醒文字”这个任务预约到了当天下午2点30分。任务编号是5这个编号后面可以用来查任务状态或者删除任务。交互模式适合临时在终端里敲命令但更多时候我需要的是在脚本里写定时任务这时候用管道输入更优雅echo tar -czf /backup/www_$(date \%Y\%m\%d).tar.gz /var/www | at 02:00注意date命令里的%在at环境里要转义写成\%否则会被解释成别的含义。如果你要执行多条命令可以用;分隔或者直接写一个脚本路径at 23:30 bash /opt/scripts/db_backup.sh还有一种方式是从文件读取命令at 03:00 -f /opt/scripts/cleanup_tmp.sh2.3 at支持的时间格式at的时间格式非常灵活这也是它“轻量”的原因之一。除了标准的HH:MM小时:分钟它还支持相对时间now 5 minutes、now 2 hours、now 3 days、now 1 week关键字时间noon中午12点、midnight凌晨0点、teatime下午4点这个词很英国、today、tomorrow完整时间2024-12-31 18:30AM/PM表达3:30pm我在生产环境里最常用的是now N minutes这个格式因为很多时候就是突发需求比如“5分钟后重启一下服务试试看”这时候一条命令就搞定了echo systemctl restart nginx | at now 5 minutes还有一个小细节at默认会把任务的输出结果用邮件发给当前用户。如果服务器上没配邮件服务输出就丢了。我一般会在命令里重定向输出到日志文件比如at 02:00 tar -czf /backup/site.tar.gz /var/www /var/log/backup.log 21这样任务执行的结果就有了记录排查问题也方便。2.4 用atq和atrm管理预约任务预约的任务不是只能干等着我们也可以查、可以删。查看当前用户的所有预约任务用atq相当于at listatq输出类似这样3 Thu Sep 14 14:30:00 2024 a root 5 Thu Sep 14 23:30:00 2024 a root每行最前面的数字就是任务编号看到它我就能精确操作。如果想看某个任务的详细内容可以用at -c 任务号at -c 3它会显示这个任务将要执行的完整shell脚本包括环境变量、当前目录、需要执行的命令。不过大多数时候我们不会细细看这个只会在排查“任务为什么没执行”时才用。如果要删除某个任务用atrm加任务编号atrm 32.5 at的权限控制谁能用谁不能用在多人共用的服务器上at命令需要限制使用权限。它通过/etc/at.allow和/etc/at.deny这两个文件来控制。规则是这样的如果存在at.allow只有文件里列出的用户可以使用at其他用户一律禁止如果不存在at.allow但存在at.deny则文件里列出的用户禁止使用at其他用户允许如果两个文件都不存在只有root能使用at我来举个例子。你想把at的执行权限只开放给你的运维账号ops就创建/etc/at.allow里面写一行用户名sudo echo ops /etc/at.allow这比在sudoers里各种配置简单多了。日常我管理服务器如果团队里有实习新人我会在at.deny里把他们的账号加进去防止“误操作”或者“恶作剧式”的定时任务占用服务器资源。3. crontab和cron表达式从入门到实战拆解3.1 crontab命令你的周期任务管理器cron的配置文件叫crontab每个用户都有自己的一份。操作它的命令就是crontab。查看当前用户的周期任务crontab -l编辑当前用户的周期任务我特别强调一下实际工作中请务必用crontab -e来改而不是直接修改/var/spool/cron/下的文件crontab -ecrontab -e会自动做语法检查有错会提示而且它会帮你锁定文件防止多人同时编辑导致互相覆盖。我第一次带团队时有个新人就是直接去/var/spool/cron/root里改配置结果改完不生效排查了半天其实是那个路径在不同发行版上不一样有的叫/var/spool/cron/crontabs/有的叫/var/spool/cron/。用crontab -e就不会有这种问题。删除当前用户的所有周期任务这个命令要谨慎用crontab -r3.2 五段式时间表达式的字段含义cron表达式的结构是五个字段命令五个字段分别是分钟、小时、日、月、星期几。它的顺序非常固定我每次写的时候都会默念一遍“分、时、日、月、周”。我把每个字段的取值范围和含义整理成表字段含义取值范围说明第1列分0-59每一分钟第2列时0-23注意没有24点第3列日1-31这个月的第几天第4列月1-12几月份第5列周0-70和7都表示星期天1-6表示周一到周六这五个字段用空格分隔最后面跟要执行的命令。每个字段里可以用的特殊符号有*表示任意值比如分钟的*表示每一分钟都执行,表示枚举多个值比如1,15表示第1和第15-表示范围比如9-18表示9点到18点/表示步长比如*/5表示每5个单位前面这三个符号*、,、-都比较直观/稍微要理解一下。*/10在分钟字段表示“每隔10分钟执行一次”等价于0,10,20,30,40,50。0-30/5表示“0到30分钟之间每5分钟执行一次”。3.3 从简单到复杂的表达式实战我整理了几个实际工作最常用的cron表达式你照着用就行# 每天凌晨2点整执行备份脚本 0 2 * * * /opt/scripts/backup.sh # 每小时的5分、35分各执行一次 5,35 * * * * /opt/scripts/check_health.sh # 每天早上9点到下午6点每20分钟执行一次 */20 9-18 * * * /opt/scripts/collect_data.py # 每周一到周五的晚上11点执行每日汇总 0 23 * * 1-5 /opt/scripts/daily_summary.sh # 每月1号和15号的凌晨3点清理临时文件 0 3 1,15 * * /opt/scripts/tmp_cleanup.sh这里要特别提醒的是日和周这两个字段同时设置时会有点反直觉。在cron的实现里如果同日和周都设置了值任务会在“满足任一条件”时执行。也就是说0 2 15 * 1这样写会同时有在“每月15号”和“每周一”凌晨2点执行而不是“必须是15号同时又是周一”才执行。这个细节以前也坑过很多人。如果你确实想要“某月15号且是周一”那你需要在脚本里去判断cron本身做不到直接表达。我一般建议在cron表达式里日和周不要同时限制否则执行时机可能超出你的预期。3.4 环境变量是关键搞懂它才能真正掌控croncron执行任务时的环境变量,和你手动在终端里执行命令时完全不一样。这是一个大坑也是排查cron问题时的重点方向。具体来说cron执行脚本时使用的是一个极简的shell环境PATH通常只有/usr/bin:/bin不会有JAVA_HOME、MYSQL_HOME这类软件自定义的环境变量也不会有你当前shell里设定的各种别名和函数所以你在crontab里写java -version大概率会报“command not found”因为java的二进制路径在/usr/local/java/bin/java而cron的PATH里根本没有这个目录。我的解决方案是“三板斧”第一在crontab文件顶部显式声明环境变量。crontab -e进入编辑界面后在文件开头加上SHELL/bin/bash PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/java/bin JAVA_HOME/usr/local/jdk-17第二写脚本时使用绝对路径或者通过环境变量动态获取路径。比如/usr/local/java/bin/java -jar /opt/app/myapp.jar第三在脚本内部再source一遍环境变量文件。比如脚本开头加上source /etc/profile source ~/.bash_profile这样做的好处是脚本里的命令都会继承这些环境变量执行结果和你手动登录时保持一致。3.5 用户级和系统级的cron配置cron的配置方案不止crontab -e这一种。系统级的cron配置在/etc/crontab文件里这个文件的格式比用户级的多了一个“用户名”字段。我拿一份典型的/etc/crontab来举例SHELL/bin/bash PATH/sbin:/bin:/usr/sbin:/usr/bin MAILTOroot # 每小时的10分执行一次系统任务 10 * * * * root /usr/libexec/atrun # 每天凌晨2点执行日志轮转 0 2 * * * root /usr/sbin/logrotate /etc/logrotate.conf注意看用户级的crontab里命令前面的第五个字段直接就是命令而系统级/etc/crontab里第五个字段是用户名第六个字段才是命令。这个区别写错的话任务是不会执行的。还有一种常见的配置方式是放在/etc/cron.d/目录下。这个目录下的文件格式和/etc/crontab一样也需要用户名。不同的是cron.d目录允许你按软件包粒度去拆分配置。比如你装了一个应用它会在/etc/cron.d/下放一个自己的任务文件这样卸载应用时直接删文件就行不用去动全局crontab。3.6 cron.daily、cron.hourly这些目录的运作逻辑Linux发行版里通常还有/etc/cron.hourly/、/etc/cron.daily/、/etc/cron.weekly/、/etc/cron.monthly/这几个目录。你把可执行的脚本丢进这些目录系统就会按时去执行它们。真正实现这些目录调度的配置其实是放在/etc/crontab里的01 * * * * root run-parts /etc/cron.hourly 02 4 * * * root run-parts /etc/cron.daily 22 4 * * 0 root run-parts /etc/cron.weekly 42 4 1 * * root run-parts /etc/cron.monthlyrun-parts是一个命令作用是执行指定目录下所有的可执行脚本。所以你在/etc/cron.daily/下放一个脚本系统每天凌晨4点02分会自动执行。我管这些动作叫“自动化运维的基建”很多软件包比如logrotate、man-db默认都是通过这些目录来执行定期任务的。如果你在笔记本上或者是个人开发机上用cron会发现有个叫anacron的服务在补跑这些任务。anacron的设计初衷是如果电脑在你设定的时间点是关机的anacron会在下次开机后补跑错过的任务。这解决了cron“到点必须开机”的缺陷。但注意anacron只处理/etc/cron.hourly、daily、weekly、monthly这些目录级任务不会管你在crontab -e里写的那些表达式。4. 日志、排查、和那些让你头疼的cron坑4.1 cron的日志怎么看任务不执行的时候第一件事就是看日志。但不同的Linux发行版cron日志位置不一样。在Debian、Ubuntu上cron的日志写在/var/log/syslog里。你可以这样过滤grep CRON /var/log/syslog在CentOS、Rocky等RHEL系发行版上日志在/var/log/crontail -f /var/log/cron日志里通常会有这样的记录Sep 14 02:00:01 server CROND[2345]: (root) CMD (/opt/scripts/backup.sh)这说明cron已经尝试执行命令了。如果连这条记录都没有说明cron表达式本身就没被cron解析到或者crond没跑起来。如果日志显示CMD那一行但脚本没生效那问题多半出在脚本本身或者环境变量上。这时候我惯用的手法是在crontab命令后面加重定向把输出和报错都写进日志文件0 2 * * * /opt/scripts/backup.sh /var/log/backup_cron.log 21表示追加21标准错误也重定向到同一个文件。这样执行完你可以直接去看这个文件的内容很快就能定位到是脚本内部哪个命令报错了。4.2 常见问题速查表对照排查效率翻倍我在日常排障中总结了一套cron/at的问题速查表分享给你现象可能原因排查方法任务完全不执行crond守护进程没运行systemctl status crond或ps aux | grep crond任务到点没反应时区不一致检查date输出看系统时区是否与预期一致脚本手动能跑cron里不跑环境变量PTH缺失添加SHELL和PATH声明或脚本内绝对路径任务执行了但没产生预期结果命令输出被cron的邮件机制丢弃在crontab命令上加重定向输出到日志文件脚本报“command not found”cron的PATH过于精简命令用绝对路径或在文件头声明PATH每天应该执行一次却执行了多次日和周同时有值算“或”不是“且”调整cron表达式避免同时限制日和星期at任务创建了却消失atd守护进程未启动systemctl status atd然后重新创建at任务cron任务执行了两次同时配置了用户crontab和/etc/cron.d/查找所有cron配置crontab -lls /etc/cron.d/grep -r相关关键字时间比北京时间晚8小时系统时区是UTC修改/etc/localtime或使用timedatectl set-timezone Asia/Shanghai4.3 cron表达式时间计算的一个特殊案例说一个我实际踩过的坑。有一次有个任务需要“每隔30秒执行一次”我当时也没多想直接写* * * * * /opt/scripts/do_sth.sh这实际上会每分钟执行一次脚本而不是每30秒。因为cron的最小粒度是分钟它没法直接表达“每30秒”。要想做到每30秒执行思路是在脚本内部做sleep或者用循环。比如0 * * * * 具体任务配合脚本内部的循环for i in 1 2; do /opt/scripts/do_sth.sh sleep 30 done这个技巧在需要高频轮询的场景里很实用。但提醒一句生产环境里每30秒跑一次的任务要格外注意脚本的执行时间别让任务重叠——也就是上次还没跑完下次又开始了。解决重叠的办法是加文件锁用flock命令0 * * * * /usr/bin/flock -xn /tmp/do_sth.lock -c /opt/scripts/do_sth.sh-xn表示“如果锁已被占用就立即失败退出”。这样即使任务重叠了也不会两个实例同时跑。这条经验在处理数据库备份、数据同步任务时特别有用可以帮你避免很多脏数据和锁竞争问题。4.4 密码过期提醒和定时任务联动热词里有“linux密码过期提醒通知”我在这里顺手讲一下。这个需求经常是通过cron来实现的。系统里有个chage命令可以查看用户密码状态chage -l username要定时检查所有用户的密码是否快要过期你可以写一个脚本让它每天执行一次检测到密码剩余天数小于警戒值就发通知。cron表达式可以这样0 9 * * * /opt/scripts/check_passwd_expiry.sh脚本内部可以调用chage -l获取到期时间然后跟当前日期做差判断是否在7天以内。这种运维小脚本放在cron.daily目录里也非常合适。4.5 容器环境里cron的特殊注意事项现在很多人用Docker跑应用容器里用cron会遇到几个比较特殊的问题。第一很多官方镜像默认没有安装cron。你需要在Dockerfile里显式安装比如Debian镜像RUN apt-get update apt-get install -y cron然后要在启动命令里同时启动主程序和crond通常配合一个entrypoint脚本来做#!/bin/bash service cron start exec $第二容器里cron的执行环境非常“贫瘠”经常没有/usr/bin之外的东西也没有/etc/profile可供source。所以在容器里写cron任务我强烈建议所有命令都用绝对路径并且把环境变量全部写进crontab文件。第三容器重启后crontab文件经常会丢失所以我在制作镜像时会把crontab文件通过COPY指令放进去。在Dockerfile里可以通过ADD一个crontab文件再用一个启动脚本把它注册到当前用户ADD ./crontab /etc/cron.d/my-cron RUN chmod 0644 /etc/cron.d/my-cron其实只要文件在/etc/cron.d/目录下格式正确、权限是0644crond启动时就会自动加载。5. 进阶技巧用at和cron组合出更灵活的运维方案5.1 at和cron的组合使用场景乍一看at和cron是互相独立的但在实际工作里我会把它们组合起来用实现更灵活的任务编排。举个例子你在cron里设定了每天凌晨2点执行某个程序但这个程序在运行之前需要等一个外部数据文件到达而数据到达的时间不确定。这时候可以这样设计先用cron每分钟检查数据文件是否到位一旦到位就通过at去启动那个程序。cron扮演“哨兵”的角色at扮演“一次性执行器”的角色两者配合既避免了cron反复去启动同一个程序又不会漏掉数据文件到达后的执行窗口。这种“哨兵执行器”的组合思路我在很多自动化流水线里都用到过。你不需要为这种“数据驱动”场景引入复杂的调度框架比如Airflow轻量的cronat就能搞定。比如这样*/1 * * * * /opt/scripts/wait_for_file_and_trigger.sh脚本内部在检测到文件存在后用at去启动后续任务#!/bin/bash if [ -f /data/input/daily_data.csv ]; then echo bash /opt/scripts/process_daily_data.sh | at now 1 minute # 防止重复触发可以先移走文件 mv /data/input/daily_data.csv /data/input/daily_data.csv.bak fi整个过程不会重复执行也不需要额外安装任务调度软件。5.2 用cron定时控制服务自启动热词里有“linux设置tomcat自启动”。这个需求用systemd可以做得更标准但是如果你手里是一个没有systemd的旧系统或者不方便改服务配置cron也是一个应急的办法。比如我负责维护一个老平台Tomcat是解压版启动方式没有注册成systemd服务。我让它每天凌晨4点重启一次来释放内存就可以这样写0 4 * * * /usr/local/tomcat/bin/shutdown.sh sleep 10 /usr/local/tomcat/bin/startup.sh但注意这种“cron拉起服务”的方式有个风险如果服务还在正常运行你又跑到凌晨去执行重启脚本可能会启动失败或者重复启动。稳妥的办法是脚本里先判断进程是否存在#!/bin/bash if pgrep -f tomcat /dev/null; then /usr/local/tomcat/bin/shutdown.sh sleep 10 fi /usr/local/tomcat/bin/startup.sh这样即使cron多跑了一次也不会出现“重复启动”的问题。5.3 监控cron任务本身是否在运行有句话叫“运维的尽头是监控”。你配置了cron任务但怎么确认cron任务每天真的跑了如果你无人值守任务失败你可能要好几天后才发现。我的做法是在任务里埋“心跳”。“心跳”是一个文件或一条数据库记录任务每次执行都会更新它的时间戳。然后用一个独立的监控比如Zabbix、Prometheus或者简单的外层cron去检查这个时间戳是否是最新的。比如在我的备份脚本里每次执行完就更新心跳文件0 2 * * * /opt/scripts/backup.sh touch /var/log/backup_heartbeat然后用一个外层cron每10分钟检查心跳文件是否超过24小时没有更新*/10 * * * * /opt/scripts/check_heartbeat.shcheck_heartbeat.sh里可以用find命令判断文件mtimeif find /var/log/backup_heartbeat -mmin 1440 | grep -q .; then echo backup miss | mail -s ALERT: backup did not run opsexample.com fi这样做的好处是一旦cron任务因为某种原因没跑成你很快就能收到报警不用等到数据丢失才发现。这套思路不仅适用于cron也适用于at如果你用at安排了一个重要的一次性任务可以同样给它设置一个心跳等到任务预期时间后再看心跳有没有被更新。5.4 日志切割、清理这类老生常谈用cron高效搞定系统里有个组件叫logrotate它本身负责日志的轮转、压缩和清理但它什么时候跑很多时候就是靠cron来触发的。虽然现在systemd里有systemd-timer可以做同样的事但cron在多数服务器上依然是“最稳的老伙计”。你可以在crontab里写15 3 * * * /usr/sbin/logrotate /etc/logrotate.conf这个表达式的含义是每天凌晨3点15分执行logrotate。如果你希望多个对象的日志在不同时间点轮转可以在/etc/logrotate.d/下面建多个配置文件或者用多个cron条目错峰执行。和logrotate类似的还有tmpwatch或tmpreaper用来清理/tmp目录下过期的临时文件。在CentOS系上它可能放在/etc/cron.daily/tmpwatch里。如果你在一个非常吃磁盘的服务器上工作可以自己加一个cron任务定期清理磁盘上的大文件或者过期文件。5.5 用crontab -e提交任务时建议的文件头模板最后分享一个我习惯用的crontab文件头模板每个新服务器我都会复制一份可以减少很多不必要的问题# 推荐写到crontab开头的模板 SHELL/bin/bash PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin MAILTO LANGen_US.UTF-8 LC_ALLen_US.UTF-8 # 示例任务 # 0 2 * * * /opt/scripts/backup.sh /var/log/backup_cron.log 21MAILTO的意思是取消cron默认发邮件的行为。如果服务器没配邮件服务这可以避免cron往root邮箱里堆积大量无用的邮件也为系统减少开销。LANG和LC_ALL的作用是统一字符集避免脚本里涉及中文文件名或编码处理时出现乱码。这个模板你只需要写一次后面每次新配置cron任务时直接复制过来改时间表达式和命令就行。我个人这么多年用下来最大感受是at和cron看起来简单但真正用好需要理解它们的运行机制和局限而不是死记硬背命令参数。at适合“临时起意”的一次性任务cron适合“雷打不动”的周期任务。配合环境变量设置、日志重定向、心跳监控、文件锁这几个习惯你的任务执行靠谱度会上一个台阶。遇到任务不执行先看守护进程再看日志再看脚本环境基本都能快速定位。希望大家在实际运维中能够少踩几个坑把时间花在真正需要处理的事情上。