Cron в Linux: історія, використання та пристрій

Cron в Linux: історія, використання та пристрій

Класик писав, що щасливого годинника не спостерігає. У ті дикі часи ще не було ні програмістів, ні Unix, але сьогодні програмісти знають твердо: замість них за часом простежить cron.

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

Я довго користувався cron поверхнево, не вникаючи в деталі, але одного разу, зіткнувшись з помилкою під час запуску скрипта, вирішив ґрунтовно розібратися. Так з'явилася ця стаття, під час написання якої я ознайомився з POSIX crontab, основними варіантами cron у популярних дистрибутивах Linux та пристроєм деяких із них.

Ви використовуєте Linux і запускаєте завдання у cron? Вам цікава архітектура системних програм в Unix? Тоді нам на шляху!

Зміст

походження видів

p align="justify"> Періодичне виконання користувацьких або системних програм - очевидна необхідність у всіх операційних системах. Тому потреба у сервісах, що дозволяють централізовано планувати та виконувати завдання, програмісти усвідомили дуже давно.

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

Типовий сучасний cron - нескладна програма, але алгоритм роботи оригінального варіанту був ще простіше: сервіс прокидався раз на хвилину, читав табличку із завданнями з єдиного файлу (/etc/lib/crontab) і виконував для суперкористувача ті завдання, які слід виконати в поточну хвилину .

Згодом удосконалені варіанти простого та корисного сервісу поставлялися з усіма Unix-подібними операційними системами.

Узагальнені описи формату crontab та базових принципів роботи утиліти у 1992 році були включені до головного стандарту Unix-подібних операційних систем – POSIX – і таким чином cron зі стандарту де-факто став стандартом де-юре.

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

До третьої версії Vixie cron став відповідати вимогам POSIX, до того ж у програми була ліберальна ліцензія, вірніше не було взагалі жодної ліцензії, якщо не брати до уваги побажань у README: гарантій автор не дає, ім'я автора видаляти не можна, а продавати програму можна тільки разом з вихідним кодом. Ці вимоги виявилися сумісні з принципами популярного вільного ПЗ, що набирало в ті роки, тому деякі ключові з дистрибутивів Linux, що з'явилися на початку 90-х, взяли 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 -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. Найбільші нові можливості: підтримка системний журнал, SELinux и PAM.

З менш помітних, але відчутних змін - розташування файлів конфігурації і таблиць завдань.

Користувацькі таблиці в Debian розташовуються в директорії /var/spool/cron/crontabs, системна таблиця все там - у /etc/crontab. Специфічні для пакетів Debian таблиці завдань розміщуються в /etc/cron.d, де демон cron їх автоматично зчитує. Керування доступом користувачів регулюється файлами /etc/cron.allow та /etc/cron.deny.

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

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

cronie в RedHat, Fedora та CentOS

cronie - Форк Vixie cron версії 4.1. Як і в Debian, синтаксис не змінювався, але додана підтримка PAM і SELinux, роботи в кластері, стеження за файлами за допомогою inotify та інших можливостей.

Конфігурація за умовчанням знаходиться у звичайних місцях: системна таблиця - в /etc/crontabs, пакети поміщають свої таблиці в /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, - це і є наше завдання, яке міститься в загальний список завдань. У самій функції проводиться багатослівний розбір формату часу, нас більше цікавить формування змінних оточення і параметрів запуску завдання:

/* пользователь и группа для запуска задачи берутся из 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);

З актуальним списком завдань працює головний цикл.

Головний цикл

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

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

Безпосередньо виконанням завдань займається функція cron_sleep, що викликає функції job_runqueue (перебір та запуск задач) та do_command (запуск кожної окремої задачі). Останню функцію варто розібрати докладніше.

Запуск завдання

Функція do_command виконана у хорошому Unix-стилі, тобто для асинхронного виконання завдання вона робить fork. Батьківський процес продовжує запуск завдань, дочірній – займається підготовкою процесу завдання:

switch (fork()) {
case -1:
    /*не смогли выполнить fork */
    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));

    /* запуск самой команды
     */
    {
        /* переменная окружения 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. Якісь цікаві деталі, наприклад, облік віддалених користувачів, я опустив, але головне виклав.

Післямова

Сron - напрочуд проста і корисна програма, виконана в кращих традиціях світу Unix. Вона не робить нічого зайвого, але свою роботу виконує чудово вже протягом кількох десятиліть. Ознайомлення з кодом тієї версії, що поставляється з Ubuntu, зайняло не більше години, а насолоди я отримав масу! Сподіваюся, я зміг поділитися з вами.

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

Існує безліч сучасних альтернатив cron: systemd-timers дозволяють організувати складні системи із залежностями, у fcron можна гнучкіше регулювати споживання ресурсів завданнями. Але особисто мені завжди вистачало найпростіших crontab.

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

Джерело: habr.com

Додати коментар або відгук