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

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). Заедно с Version 7 Unix беше предоставен и cron, услуга за редовно изпълнение на задачи на суперпотребителя.

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

Впоследствие подобрени варианти на простата и полезна услуга се предоставяха с всички Unix-подобни операционни системи.

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

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

Третата версия на Vixie cron отговаря на изискванията на POSIX, а програмата е с либерална лицензия, всъщност без никаква лицензия, освен пожеланията в README: авторът не дава гаранции, името му не може да бъде премахнато, а програмата може да се продава само заедно с изходния код. Тези изисквания се оказаха съвместими с принципите на нарастващата популярност на свободния софтуер, така че някои ключови дистрибуции на Linux, появили се в началото на 90-те години, избраха Vixie cron за системен и продължават да го развиват и до днес.

Конкретно, Red Hat и SUSE развиват форк на Vixie cron — cronie, а Debian и Ubuntu използват оригиналната версия на Vixie cron с множество пачове.

Нека първо се запознаем с потребителската утилита crontab, описана в POSIX, след което ще разгледаме разширенията на синтаксиса, представени в Vixie cron, и употребата на вариации на Vixie cron в популярните дистрибуции на Linux. И накрая, истинската изненада — разглеждане на устройството на демона 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.

Задачите са описани в следния формат:

# строки-комментарии игнорируются
#
# задача, выполняемая ежеминутно
* * * * * /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, решението на Пол Вики предоставя няколко много полезни модификации в синтаксиса на таблиците на задачите на утилитата.

Наличен нов синтаксис за таблици: например, можете да посочвате дните от седмицата или месеците поименно (Пн, Вт и така нататък):

# Запускается ежеминутно по понедельникам и вторникам в январе
* * * 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, от където demon 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, е нашата задача, поставена в общия списък от задачи. В самата функция се извършва многословен анализ на времевия формат, но ние сме по-загрижени за създаването на променливи на средата и параметри за стартиране на задачата:

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

Следсловие

Cron — за изненада, доста проста и полезна програма, изпълнена в най-добрите традиции на света на Unix. Тя не прави нищо излишно, но работи отлично вече десетилетия. Запознаването с кода на версията, която се предлага с Ubuntu, ми отне не повече от час, а удоволствието беше огромно! Надявам се, че успях да го споделя с вас.

Не знам за вас, но мен малко ме натъжава осъзнаването, че съвременното програмиране с тенденцията си към усложняване и преабстрахиране отдавна не носи подобна простота.

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

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

Източник: habr.com

Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри 🔥 Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри | ProHoster