
经典著作写道,欢乐时光没有被观察到。 在那些荒芜的时代,既没有程序员也没有Unix,但是如今程序员已经非常了解:cron将代替时间,而不是他们。
对我而言,命令行实用程序既有弱点,又有常规性。 sed,awk,wc,cut和其他旧程序每天由我们服务器上的脚本运行。 他们中的许多人都是作为70年代的调度程序cron的任务而设计的。
很长时间以来,我只是简单地使用了cron,而没有涉及任何细节,但是有一次,在运行脚本时遇到错误,我决定彻底弄清楚。 因此,在撰写本文时,我熟悉了POSIX crontab,流行的Linux发行版中的主要cron变体以及其中的某些设备,从而出现了这篇文章。
使用Linux并在cron中运行任务? 对Unix系统应用程序架构感兴趣? 然后我们就在路上!
目录内容
物种起源
显然,所有操作系统都需要定期执行用户或系统程序。 因此,对于允许集中计划和执行任务的服务的需求,程序员已经实现了很长的时间。
类似Unix的操作系统的谱系来自1970年代由Bell Labs开发的Version 7 Unix,其中包括著名的Ken Thompson。 与版本7 Unix一起,提供了cron,该服务用于定期执行超级用户任务。
典型的现代cron是一个简单的程序,但是原始版本的算法甚至更简单:该服务每分钟唤醒一次,从单个文件(/ etc / lib / crontab)中读取任务板,并在当前时间执行应为超级用户执行的任务。
随后,所有类似Unix的操作系统都提供了用于简单有用的服务的高级选项。
类似于Unix的操作系统的主要标准POSIX中包含了1992年对crontab格式的通用描述和实用程序的基本原理,因此,事实上的cron成为了法律上的标准。
1987年,Paul Vixie在采访了Unix用户以获取有关cron的建议后,发布了该守护程序的另一个版本,该守护程序解决了传统cron的某些问题并扩展了表文件的语法。
在第三个版本中,Vixie cron开始满足POSIX的要求,此外,该程序具有自由许可证,或者什至根本没有许可证,除了README的愿望:作者不提供保证,您不能删除作者的名字,并且只能在以下情况下出售程序:源代码。 事实证明,这些要求与自由软件的原理兼容,自由软件的原理在那些年中变得越来越流行,因此90年代初出现的一些关键Linux发行版采用Vixie cron作为系统,并且仍在开发中。
特别是Red Hat和SUSE正在开发Vixie cron-cronie fork,而Debian和Ubuntu在使用带有许多补丁程序的原始Vixie cron。
首先,让我们熟悉POSIX中描述的用户定义的crontab实用程序,然后我们将研究Vixie cron中引入的语法扩展以及在流行的Linux发行版中使用Vixie cron变体。 最后,蛋糕上的樱桃是cron守护程序设备的解析。
Posix crontab
如果原始的cron始终为超级用户工作,那么现代的调度程序通常会处理普通用户的任务,这更安全,更方便。
Cron附带了两个程序集:不断运行的cron守护程序和可供用户使用的crontab实用程序。 后者允许您编辑特定于系统中每个用户的任务表,而守护程序则从用户和系统表启动任务。
POSIX标准未描述守护程序的行为,仅将crontab用户程序形式化。 当然,暗示了存在用于启动用户任务的机制,但是没有详细描述。
使用crontab实用程序可以执行四件事:在编辑器中编辑用户任务表,从文件中加载表,显示当前任务表,并清除任务表。 crontab实用程序的示例:
crontab -e # crontab -l # crontab -r # crontab path/to/file.crontab #
调用crontab -e
,将使用在标准EDITOR
环境EDITOR
指定的编辑器。
任务本身以以下格式描述:
# - # # , * * * * * /path/to/exec -a -b -c # , 10- 10 * * * * /path/to/exec -a -b -c # , 10- 10 2 * * * /path/to/exec -a -b -c > /tmp/cron-job-output.log
前五个记录字段:分钟[1..60],小时[0..23],每月[1..31],月份[1..12],一周[0..6],其中0-周日。 最后的第六个字段是将由标准命令解释器执行的字符串。
在前五个字段中,值可以用逗号列出:
# , 1,10 * * * * /path/to/exec -a -b -c
或通过连字符:
# , 0-9 * * * * /path/to/exec -a -b -c
POSIX文件cron.allow和cron.deny中分别规定了用户对任务计划的访问权限,该文件分别列出了有权访问crontab的用户和无法访问程序的用户。 该标准不规范这些文件的位置。
根据标准,正在运行的程序必须至少传递四个环境变量:
- HOME是用户的主目录。
- LOGNAME-用户登录。
- PATH是查找标准系统实用程序的路径。
- SHELL是所用外壳的路径。
值得注意的是,POSIX没有说明这些变量的值来自何处。
畅销书-Vixie cron 3.0pl1
流行的cron变体的共同祖先是Vixie cron 3.0pl1,出现在1992 comp.sources.unix邮件列表中。 我们将更详细地考虑此版本的主要功能。
Vixie cron包含两个程序(cron和crontab)。 通常,守护程序负责从系统任务表和单个用户的任务表中读取和启动任务,而crontab实用程序负责编辑用户表。
任务表和配置文件
超级用户任务表位于/ etc / crontab中。 系统表的语法与Vixie cron的语法相对应,已针对以下事实进行了调整:第六列指示启动任务的用户名称:
# vlad * * * * * vlad /path/to/exec
通用用户任务表位于/ var / cron / tabs / username中,并使用通用语法。 启动crontab实用程序后,这些文件将代表用户进行编辑。
可以在文件/ var / cron / allow和/ var / cron / deny中管理有权访问crontab的用户列表,将用户名添加为单独的一行就足够了。
扩展语法
与POSIX crontab相比,Paul Vixie的解决方案对实用程序任务表语法进行了一些非常有用的修改。
新的表语法已可用:例如,您可以按名称(星期一,星期二等)指定星期几或月份:
# * * * Jan Mon,Tue /path/to/exec
您可以指定启动任务的步骤:
# */2 * * * Mon,Tue /path/to/exec
步骤和间隔可以混合:
# 0-10/2 * * * * /path/to/exec
支持常规语法的直观替代(重新启动,每年,每年,每月,每月,每周,每天,午夜,每小时):
# @reboot /exec/on/reboot # @daily /exec/daily # @hourly /exec/daily
任务执行环境
Vixie cron允许您更改正在运行的应用程序的环境。
守护程序不仅提供USER,LOGNAME和HOME环境变量,而且还从passwd文件中获取 。 PATH变量获取值“ / usr / bin:/ bin”,而SHELL获取值“ / bin / sh”。 可以在用户表中更改除LOGNAME之外的所有变量的值。
cron本身使用一些环境变量(主要是SHELL和HOME)来运行任务。 这是使用bash而不是标准sh来运行自定义任务的样子:
SHELL=/bin/bash HOME=/tmp/ # exec bash- /tmp/ * * * * * /path/to/exec
最终,表中定义的所有环境变量(由cron使用或为流程所必需)都将传输到正在运行的任务。
crontab实用程序使用VISUAL或EDITOR环境变量中指定的编辑器来编辑文件。 如果在启动crontab的环境中未定义这些变量,则使用“ / usr / ucb / vi”(ucb可能是加州大学伯克利分校)。
在Debian和Ubuntu上使用cron
Debian和派生开发人员发布了高度修改的Vixie cron版本3.0pl1。 表文件的语法没有差异;对于用户而言,这是相同的Vixie cron。 最大的新功能: syslog , SELinux和PAM支持。
较不明显但切实的更改-配置文件和任务表的位置。
Debian中的用户表位于目录/ var / spool / cron / crontabs中,系统表仍位于/ etc / crontab中。 特定于Debian的任务表位于/etc/cron.d中,cron守护程序从中自动读取它们。 用户访问控制由/etc/cron.allow和/etc/cron.deny文件控制。
默认shell / bin / sh仍然用作默认shell Debian播放一个与POSIX兼容的小型dash shell,该dash shell运行时无需读取任何配置(在非交互模式下)。
最新版本的Debian中的Cron本身是通过systemd启动的,可以在/lib/systemd/system/cron.service中查看启动配置。 服务的配置没有什么特别的;任何更精细的任务管理都可以通过直接在每个用户的crontab中声明的环境变量来完成。
cronie在RedHat,Fedora和CentOS上
cronie -Vixie cron版本4.1的fork。 与在Debian中一样,语法没有改变,但是增加了对PAM和SELinux的支持,在集群中工作,使用inotify跟踪文件以及其他功能。
默认配置位于通常的位置:系统表位于/ etc / crontab中,程序包将其表置于/etc/cron.d中,用户表位于/ var / spool / cron / crontabs中。
该守护程序在systemd下运行,服务配置为/lib/systemd/system/crond.service。
在启动时,类似Red Hat的发行版默认使用/ bin / sh,其作用是标准bash。 应该注意的是,通过/ bin / sh运行cron任务时,bash shell在POSIX兼容模式下启动,并且在非交互模式下运行时不会读取任何其他配置。
SLES和openSUSE中的cronie
German SLES发行版及其openSUSE派生使用相同的cronie。 守护程序也在systemd下运行,服务配置位于/usr/lib/systemd/system/cron.service中。 配置:/etc/crontab、/etc/cron.d、/var/spool/cron/tabs。 由于/ bin / sh行为相同,因此以兼容POSIX的非交互模式启动。
Vixie Cron设备
与Vixie cron相比,cron的现代后代没有发生根本变化,但是尽管如此,他们已经获得了理解程序原理所不需要的新功能。 这些扩展中的许多都是凌乱的,使代码混乱。 Paul Vixie编写的原始cron源代码非常有趣。
因此,我决定使用针对cron开发的两个分支的通用程序示例Vixie cron 3.0pl1分析cron设备。 我将通过删除使阅读变得复杂的ifdefs并省略次要细节来简化示例。
恶魔的工作可以分为几个阶段:
- 程序初始化。
- 收集并更新要运行的任务列表。
- 主cron循环操作。
- 任务启动。
让我们对它们进行排序。
初始化
启动后,cron在检查过程参数之后,将安装SIGCHLD和SIGHUP信号处理程序。 第一个记录子进程的完成,第二个关闭日志文件的文件描述符:
signal(SIGCHLD, sigchld_handler); signal(SIGHUP, sighup_handler);
系统中的cron守护程序始终始终单独工作,仅作为超级用户和cron主目录运行。 以下调用使用守护进程的PID创建文件锁,确保用户正确,然后将当前目录更改为主目录:
acquire_daemonlock(0); set_cron_uid(); set_cron_cwd();
默认路径已设置,将在启动进程时使用:
setenv("PATH", _PATH_DEFPATH, 1);
然后,该进程被“守护”:通过调用fork和子进程中的新会话(调用setsid)来创建该进程的子副本。 父进程不再需要-它完成了工作:
switch (fork()) { case -1: exit(0); break; case 0: (void) setsid(); break; default: _exit(0); }
父进程的终止释放了锁文件上的锁。 另外,您需要将文件中的PID更新为子文件。 之后,将填充任务数据库:
acquire_daemonlock(0); database.head = NULL; database.tail = NULL; database.mtime = (time_t) 0; load_database(&database);
进一步的cron进入主要工作周期。 但在此之前,请看一下加载任务列表。
收集和更新任务列表
load_database函数负责加载任务列表。 它检查主系统crontab和带有用户文件的目录。 如果文件和目录未更改,则不会重新读取任务列表。 否则,将开始形成新的任务列表。
下载具有特殊文件名和表名的系统文件:
if (syscron_stat.st_mtime) { process_crontab("root", "*system*", SYSCRONTAB, &syscron_stat, &new_db, old_db); }
循环加载用户表:
while (NULL != (dp = readdir(dir))) { char fname[MAXNAMLEN+1], tabname[MAXNAMLEN+1]; if (dp->d_name[0] == '.') continue; (void) strcpy(fname, dp->d_name); sprintf(tabname, CRON_TAB(fname)); process_crontab(fname, fname, tabname, &statbuf, &new_db, old_db); }
然后,将旧数据库替换为新数据库。
在上面的示例中,调用process_crontab函数可确保存在与表文件名匹配的用户(除非它是超级用户),然后调用load_user。 后者已经逐行读取文件本身:
while ((status = load_env(envstr, file)) >= OK) { switch (status) { case ERR: free_user(u); u = NULL; goto done; case FALSE: e = load_entry(file, NULL, pw, envp); if (e) { e->next = u->crontab; u->crontab = e; } break; case TRUE: envp = env_set(envp, envstr); break; } }
在这里,环境变量(格式为VAR = value的行)由load_env / env_set函数设置,或者任务描述(* * * * * / path /到/ exec)由load_entry函数读取。
load_entry返回的入口实体是我们的任务,位于一般任务列表中。 在函数本身中,对时间格式进行了冗长的分析,但是我们对环境变量和任务启动参数的形成更感兴趣:
e->uid = pw->pw_uid; e->gid = pw->pw_gid; e->envp = env_copy(envp); if (!env_get("SHELL", e->envp)) { sprintf(envstr, "SHELL=%s", _PATH_BSHELL); e->envp = env_set(e->envp, envstr); } if (!env_get("HOME", e->envp)) { sprintf(envstr, "HOME=%s", pw->pw_dir); e->envp = env_set(e->envp, envstr); } if (!env_get("PATH", e->envp)) { sprintf(envstr, "PATH=%s", _PATH_DEFPATH); e->envp = env_set(e->envp, envstr); } sprintf(envstr, "%s=%s", "LOGNAME", pw->pw_name); e->envp = env_set(e->envp, envstr);
主循环还可以与当前任务列表一起使用。
主循环
来自第7版Unix的原始cron的工作非常简单:在一个周期中,我重新读取了配置,以超级用户身份运行了当前分钟的任务,并一直睡到下一分钟开始。 在旧计算机上使用这种简单方法需要太多资源。
SysV中提出了一个替代版本,在该版本中,守护程序要么进入睡眠状态,要么直到定义任务的第二分钟,要么进入30分钟。 在这种模式下,用于重新读取配置和检查任务的资源较少,但是快速更新任务列表变得不便。
Vixie cron每分钟返回一次检查任务列表,因为到80年代末,标准Unix计算机上的资源变得越来越大:
load_database(&database); run_reboot_jobs(&database); cron_sync(); while (TRUE) { cron_sleep(); load_database(&database); cron_tick(&database); TargetTime += 60; }
调用函数job_runqueue(任务的枚举和开始)和do_command(每个任务的开始)的cron_sleep函数直接参与任务的执行。 应该更详细地考虑最后一个功能。
任务启动
do_command函数以良好的Unix风格执行,也就是说,它确实分叉用于异步任务执行。 父进程继续启动任务,子进程正在准备任务进程:
switch (fork()) { case -1: break; case 0: acquire_daemonlock(1); child_process(e, u); _exit(OK_EXIT); break; default: break; }
child_process中有很多逻辑:它将标准输出和错误流传递到其自身上,以便随后可以将其发送到邮件(如果在任务表中指定了MAILTO环境变量),最后等待主任务过程完成。
任务过程由另一个分支形成:
switch (vfork()) { case -1: exit(ERROR_EXIT); case 0: (void) setsid(); setgid(e->gid); setuid(e->uid); chdir(env_get("HOME", e->envp)); { char *shell = env_get("SHELL", e->envp); execle(shell, shell, "-c", e->cmd, (char *)0, e->envp); perror("execl"); _exit(ERROR_EXIT); } break; default: break; }
在这里,一般来说,整个cron。 我省略了一些有趣的细节,例如,说明远程用户,但概述了主要内容。
后记
Cron是一个令人惊讶的简单实用的程序,它是Unix世界的最佳传统。 她没有做任何多余的事情,但是在过去的几十年中,她一直在出色地工作。 认识Ubuntu随附版本的代码只用了一个小时,我得到了很多乐趣! 希望我能和你分享。
我不了解您,但是让我感到难过的是,现代编程由于具有重新复杂化和抽象化的趋势,早已不再具有这种简单性。
cron有许多现代替代方案:systemd-timers允许您组织具有依赖性的复杂系统,在fcron中,您可以更灵活地控制任务对资源的消耗。 但就我个人而言,我一直拥有最简单的crontab。
简而言之,请热爱Unix,使用简单的程序,不要忘记阅读平台的魔法!