Linux上的Cron:历史记录,用法和设备


经典著作写道,欢乐时光没有被观察到。 在那些荒芜的时代,既没有程序员也没有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的用户和无法访问程序的用户。 该标准不规范这些文件的位置。


根据标准,正在运行的程序必须至少传递四个环境变量:


  1. HOME是用户的主目录。
  2. LOGNAME-用户登录。
  3. PATH是查找标准系统实用程序的路径。
  4. 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。 最大的新功能: syslogSELinuxPAM支持。


较不明显但切实的更改-配置文件和任务表的位置。


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并省略次要细节来简化示例。


恶魔的工作可以分为几个阶段:


  1. 程序初始化。
  2. 收集并更新要运行的任务列表。
  3. 主cron循环操作。
  4. 任务启动。

让我们对它们进行排序。


初始化


启动后,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返回的入口实体是我们的任务,位于一般任务列表中。 在函数本身中,对时间格式进行了冗长的分析,但是我们对环境变量和任务启动参数的形成更感兴趣:


 /*         passwd*/ e->uid = pw->pw_uid; e->gid = pw->pw_gid; /*    (/bin/sh),      */ 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); } /*     passwd */ 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); /*  TargetTime    */ cron_sync(); while (TRUE) { /*  ,     TargetTime    ,    */ cron_sleep(); /*   */ load_database(&database); /*      */ cron_tick(&database); /*  TargetTime     */ TargetTime += 60; } 

调用函数job_runqueue(任务的枚举和开始)和do_command(每个任务的开始)的cron_sleep函数直接参与任务的执行。 应该更详细地考虑最后一个功能。


任务启动


do_command函数以良好的Unix风格执行,也就是说,它确实分叉用于异步任务执行。 父进程继续启动任务,子进程正在准备任务进程:


 switch (fork()) { case -1: /*   fork */ 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)); /*    */ { /*   SHELL      */ 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,使用简单的程序,不要忘记阅读平台的魔法!

Source: https://habr.com/ru/post/zh-CN468061/


All Articles