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

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

Класикът пише, че щастливи часове не се спазват. В онези диви времена все още не е имало програмисти или Unix, но в наши дни програмистите знаят със сигурност: вместо тях, cron ще следи времето.

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

Дълго време използвах cron повърхностно, без да се задълбочавам в подробностите, но един ден, изправен пред грешка при изпълнение на скрипта, реших да го разгледам задълбочено. Така се роди тази статия, по време на която се запознах с POSIX crontab, основните cron опции в популярните Linux дистрибуции и дизайна на някои от тях.

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

Съдържание

Произход на видовете

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

Unix-подобните операционни системи проследяват произхода си до Версия 7 Unix, разработена през 70-те години на миналия век в Bell Labs, включително известния Кен Томпсън. Версия 7 Unix също се доставя с cron, услуга за редовно изпълнявани задачи на суперпотребител.

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

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

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

През 1987 г. Пол Викси, след анкетиране на потребители на 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 винаги се изпълняваше за суперпотребителя, съвременните планировчици е по-вероятно да се справят с обикновени потребителски задачи, което е по-безопасно и по-удобно.

Crons идват с набор от две програми: постоянно работещ 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, решението на Paul Wixey съдържа някои много полезни модификации на синтаксиса на таблиците със задачи на помощната програма.

Стана достъпен нов синтаксис на таблицата: например можете да посочите дните от седмицата или месеците по име (понеделник, вторник и т.н.):

# Запускается ежеминутно по понедельникам и вторникам в январе
* * * 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 не се предоставят само от демона, но се вземат от файла ако съществува. Променливата 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 вероятно е Университетът на Калифорния, Бъркли).

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 тире, стартира без четене на конфигурация (в неинтерактивен режим).

Самият 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 използват един и същи приятел. Демонът тук също работи под 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 от Paul Wixie е удоволствие да се чете.

Затова реших да анализирам устройството cron, като използвам примера на програма, обща за двата клона на разработката на cron - Vixie cron 3.0pl1. Ще опростя примерите, като премахна 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/to/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);

С действителния списък със задачи основният цикъл работи.

Главен контур

Оригиналният cron от версия 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, тоест прави разклонение, за да изпълни задача асинхронно. Родителският процес продължава да изпълнява задачи, дъщерният процес подготвя процеса на задача:

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, използвайте прости програми и не забравяйте да прочетете мана за вашата платформа!

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

Добавяне на нов коментар