Cron в Linux: история, использование и устройство

@

Классик писал, что счастливые часов не наблюдают. В те дикие времена ещё не было ни программистов, ни Unix, но в наши дни программисты знают твёрдо: вместо них за временем проследит cron.


Утилиты командной строки для меня одновременно слабость и рутина. sed, awk, wc, cut и другие старые программы запускаются скриптами на наших серверах ежедневно. Многие из них оформлены в виде задач для cron, планировщика родом из 70-х.


Я долго пользовался cron поверхностно, не вникая в детали, но однажды, столкнувшись с ошибкой при запуске скрипта, решил разобраться основательно. Так появилась эта статья, при написании которой я ознакомился с POSIX crontab, основными вариантами cron в популярных дистрибутивах Linux и устройством некоторых из них.


Используете Linux и запускаете задачи в cron? Вам интересна архитектура системных приложений в Unix? Тогда нам по пути!


Содержание



Происхождение видов


Периодическое выполнение пользовательских или системных программ — очевидная необходимость во всех операционных системах. Поэтому потребность в сервисах, позволяющих централизованно планировать и выполнять задачи, программисты осознали очень давно.


Unix-подобные операционные системы ведут свою родословную от Version 7 Unix, разработанной в 70-х годах прошлого века в Bell Labs в том числе и знаменитым Кеном Томпсоном (англ. Ken Thompson). Вместе c Version 7 Unix поставлялся и cron, сервис для регулярного выполнения задач суперпользователя.


Типичный современный cron — несложная программа, но алгоритм работы оригинального варианта был ещё проще: сервис просыпался раз в минуту, читал табличку с задачами из единственного файл (/etc/lib/crontab) и выполнял для суперпользователя те задачи, которые следовало выполнить в текущую минуту.


Впоследствии усовершенствованные варианты простого и полезного сервиса поставлялись со всеми Unix-подобными операционными системами.


Обобщённые описания формата crontab и базовых принципов работы утилиты в 1992 году были включены в главный стандарт Unix-подобных операционных систем — POSIX — и таким образом cron из стандарта де-факто стал стандартом де-юре.


В 1987 году Пол Викси (англ. Paul Vixie), опросив пользователей Unix на предмет пожеланий к cron, выпустил ещё одну версию демона, исправляющую некоторые проблемы традиционных cron и расширяющую синтаксис файлов-таблиц.


К третьей версии Vixie cron стал отвечать требованиям POSIX, к тому же у программы была либеральная лицензия, вернее не было вообще никакой лицензии, если не считать пожеланий в README: гарантий автор не даёт, имя автора удалять нельзя, а продавать программу можно только вместе с исходным кодом. Эти требования оказались совместимы с принципами набиравшего в те годы популярность свободного ПО, поэтому некоторые ключевые из появившихся в начале 90-х дистрибутивов Linux взяли Vixie cron в качестве системного и развивают его до сих пор.


В частности, Red Hat и SUSE развивают форк Vixie cron — cronie, а Debian и Ubuntu используют оригинальное издание Vixie cron со множеством патчей.


Давайте для начала познакомимся с описанной в POSIX пользовательской утилитой crontab, после чего разберём расширения синтаксиса, представленные в Vixie cron, и использование вариаций Vixie cron в популярных дистрибутивах Linux. И, наконец, вишенка на торте — разбор устройства демона cron.


POSIX crontab


Если оригинальный cron всегда работал для суперпользователя, то современные планировщики чаще имеют дело с задачами обычных пользователей, что более безопасно и удобно.


Сron-ы поставляются комплектом из двух программ: постоянно работающего демона cron и доступной пользователям утилиты crontab. Последняя позволяет редактировать таблицы задач, специфичные для каждого пользователя в системе, демон же запускает задачи из пользовательских и системной таблиц.


В стандарте POSIX никак не описывается поведение демона и формализована только пользовательская программа crontab. Существование механизмов запуска пользовательских задач, конечно, подразумевается, но не описано подробно.


Вызовом утилиты crontab можно сделать четыре вещи: отредактировать пользовательскую таблицу задач в редакторе, загрузить таблицу из файла, показать текущую таблицу задач и очистить таблицу задач. Примеры работы утилиты crontab:


crontab -e # редактировать таблицу задач crontab -l # показать таблицу задач crontab -r # удалить таблицу задач crontab path/to/file.crontab # загрузить таблицу задач из файла

При вызове crontab -e будет использоваться редактор, указанный в стандартной переменной окружения 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, представленный в рассылке comp.sources.unix в 1992 году. Основные возможности этой версии мы и рассмотрим подробнее.


Vixie cron поставляется в двух программах (cron и crontab). Как обычно, демон отвечает за чтение и запуск задач из системной таблицы задач и таблиц задач отдельных пользователей, а утилита crontab — за редактирование пользовательских таблиц.


Таблица задач и файлы конфигурации


Таблица задач суперпользователя расположена в /etc/crontab. Синтаксис системной таблицы соответствует синтаксису Vixie cron с поправкой на то, что в ней шестой колонкой указывается имя пользователя, от лица которого запускается задача:


# Запускается ежеминутно от пользователя vlad * * * * * vlad /path/to/exec

Таблицы задач обычных пользователей располагаются в /var/cron/tabs/username и используют общий синтаксис. При запуске утилиты crontab от имени пользователя редактируются именно эти файлы.


Управление списками пользователей, имеющих доступ к crontab, происходит в файлах /var/cron/allow и /var/cron/deny, куда достаточно внести имя пользователя отдельной строкой.


Расширенный синтаксис


По сравнению с POSIX crontab решение Пола Викси содержит несколько очень полезных модификаций в синтаксисе таблиц задач утилиты.


Стал доступен новый синтаксис таблиц: например, можно указывать дни недели или месяцы поимённо (Mon, Tue и так далее):


# Запускается ежеминутно по понедельникам и вторникам в январе * * * Jan Mon,Tue /path/to/exec

Можно указывать шаг, через который запускаются задачи:


# Запускается с шагом в две минуты */2 * * * Mon,Tue /path/to/exec

Шаги и интервалы можно смешивать:


# Запускается с шагом в две минуты в первых десять минут каждого часа 0-10/2 * * * * /path/to/exec

Поддерживаются интуитивные альтернативы обычному синтаксису (reboot, yearly, annually, monthly, weekly, daily, midnight, hourly):


# Запускается после перезагрузки системы @reboot /exec/on/reboot # Запускается раз в день @daily /exec/daily # Запускается раз в час @hourly /exec/daily

Среда выполнения задач


Vixie cron позволяет менять окружение запускаемых приложений.


Переменные окружения USER, LOGNAME и HOME не просто предоставляются демоном, а берутся из файла passwd. Переменная PATH получает значение "/usr/bin:/bin", а SHELL — "/bin/sh". Значения всех переменных, кроме LOGNAME, можно изменить в таблицах пользователей.


Некоторые переменные окружения (прежде всего SHELL и HOME) используются самим cron для запуска задачи. Вот как может выглядеть использование bash вместо стандартного sh для запуска пользовательских задач:


SHELL=/bin/bash HOME=/tmp/ # exec будет запущен bash-ем в /tmp/ * * * * * /path/to/exec

В конечном итоге все определённые в таблице переменные окружения (используемые cron или необходимые процессу) будут переданы запущенной задаче.


Для редактирования файлов утилитой crontab используется редактор, указанный в переменной окружения VISUAL или EDITOR. Если в среде, где был запущен crontab, эти переменные не определены, то используется "/usr/ucb/vi" (ucb — это, вероятно, University of California, Berkeley).


cron в Debian и Ubuntu


Разработчики 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.


В качестве командной оболочки по умолчанию по-прежнему используется /bin/sh, в роли которого в Debian выступает небольшой POSIX-совместимый шелл dash, запущенный без чтения какой-либо конфигурации (в неинтерактивном режиме).


Сам cron в последних версиях Debian запускается через systemd, а конфигурацию запуска можно посмотреть в /lib/systemd/system/cron.service. Ничего особенного в конфигурации сервиса нет, любое более тонкое управление задачами возможно осуществить через переменные окружения, объявленные прямо в crontab каждого из пользователей.


cronie в RedHat, Fedora и CentOS


cronie — форк Vixie cron версии 4.1. Как и в Debian, синтаксис не менялся, но добавлена поддержка PAM и SELinux, работы в кластере, слежения за файлами при помощи inotify и других возможностей.


Конфигурация по умолчанию находится в обычных местах: системная таблица — в /etc/crontab, пакеты помещают свои таблицы в /etc/cron.d, пользовательские таблицы попадают в /var/spool/cron/crontabs.


Демон запускается под управлением systemd, конфигурация сервиса — /lib/systemd/system/crond.service.


В Red Hat-подобных дистрибутивах при запуске по умолчанию используется /bin/sh, в роли которого выступает стандартный bash. Надо заметить, что при запуске задач cron через /bin/sh командная оболочка bash запускается в POSIX-совместимом режиме и не читает никакой дополнительной конфигурации, работая в неинтерактивном режиме.


cronie в SLES и openSUSE


Немецкий дистрибутив SLES и его дериватив openSUSE используют всё тот же cronie. Демон здесь тоже запускается под systemd, конфигурация сервиса лежит в /usr/lib/systemd/system/cron.service. Конфигурация: /etc/crontab, /etc/cron.d, /var/spool/cron/tabs. В качестве /bin/sh выступает тот же самый bash, запущенный в POSIX-совместимом неинтерактивном режиме.


Устройство Vixie cron


Современные потомки cron по сравнению с Vixie cron не изменились радикально, но всё же обзавелись новыми возможностями, не требующимися для понимания принципов работы программы. Многие из этих расширений оформлены неаккуратно и путают код. Оригинальный же исходный код cron в исполнении Пола Викси читать одно удовольствие.


Поэтому разбор устройства cron я решил провести на примере общей для обеих ветвей развития cron программы — Vixie cron 3.0pl1. Примеры я упрощу, убрав усложняющие чтение ifdef-ы и опустив второстепенные детали.


Работу демона можно разделить на несколько этапов:


  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/to/exec) функцией load_entry.


Сущность entry, которую возвращает load_entry, — это и есть наша задача, помещаемая в общий список задач. В самой функции проводится многословный разбор формата времени, нас же больше интересует формирование переменных окружения и параметров запуска задачи:


  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); }   sprintf(envstr, "%s=%s", "LOGNAME", pw->pw_name); e->envp = env_set(e->envp, envstr);

С актуальным списком задач и работает главный цикл.


Главный цикл


Оригинальный cron из Version 7 Unix работал совсем просто: в цикле перечитывал конфигурацию, запускал суперпользователем задачи текущей минуты и спал до начала следующей минуты. Этот простой подход на старых машинах требовал слишком много ресурсов.


В 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; } 

Непосредственно выполнением задач занимается функция cron_sleep, вызывающая функции job_runqueue (перебор и запуск задач) и do_command (запуск каждой отдельной задачи). Последнюю функцию стоит разобрать подробнее.


Запуск задачи


Функция do_command исполнена в хорошем Unix-стиле, то есть для асинхронного выполнения задачи она делает fork. Родительский процесс продолжает запуск задач, дочерний — занимается подготовкой процесса задачи:


switch (fork()) { case -1:   break; case 0:   acquire_daemonlock(1);   child_process(e, u);   _exit(OK_EXIT); break; default:   break; }

В child_process довольно много логики: она принимает стандартные потоки вывода и ошибок на себя, чтобы потом переслать на почту (если в таблице задач указана переменная окружения MAILTO), и, наконец, ждёт завершения работы основного процесса задачи.


Процесс задачи формируется еще одним fork:


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. Какие-то интересные детали, например учёт удалённых пользователей, я опустил, но главное изложил.


Послесловие


Сron — на удивление простая и полезная программа, выполненная в лучших традициях мира Unix. Она не делает ничего лишнего, но свою работу выполняет замечательно на протяжении уже нескольких десятилетий. Ознакомление с кодом той версии, что поставляется с Ubuntu, заняло не больше часа, а удовольствия я получил массу! Надеюсь, я смог поделиться им с вами.


Не знаю, как вам, но мне немного грустно осознавать, что современное программирование с его склонностью к переусложнению и переабстрагированию уже давно не располагает к подобной простоте.


Существует множество современных альтернатив cron: systemd-timers позволяют организовать сложные системы с зависимостями, в fcron можно гибче регулировать потребление ресурсов задачами. Но лично мне всегда хватало простейших crontab.


Словом, любите Unix, используйте простые программы и не забывайте читать маны для вашей платформы!

Анализ
×
Томпсон Кен
UC
Сфера деятельности:Образование и наука