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 от де-факто стандарт стана стандарт де-юре.

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

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

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

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

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

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

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