Cron în Linux: istoric, utilizare și dispozitiv

Cron în Linux: istoric, utilizare și dispozitiv

Clasicul a scris că orele fericite nu privesc. În acele vremuri sălbatice nu existau nici programatori, nici Unix, dar astăzi programatorii știu sigur: cron va ține evidența timpului în locul lor.

Utilitarele liniei de comandă sunt atât o slăbiciune, cât și o corvoadă pentru mine. sed, awk, wc, cut și alte programe vechi sunt rulate de scripturi pe serverele noastre în fiecare zi. Multe dintre ele sunt concepute ca sarcini pentru cron, un planificator originar din anii 70.

Mult timp am folosit cron superficial, fără să intru în detalii, dar într-o zi, când am întâmpinat o eroare la rularea unui script, m-am hotărât să mă uit la el în detaliu. Așa a apărut acest articol, în timp ce îl scriam m-am familiarizat cu POSIX crontab, principalele opțiuni cron din distribuțiile populare Linux și structura unora dintre ele.

Folosiți Linux și rulați sarcini cron? Sunteți interesat de arhitectura aplicațiilor de sistem în Unix? Atunci suntem pe drum!

Conținut

Originea speciilor

Execuția periodică a programelor de utilizator sau de sistem este o necesitate evidentă în toate sistemele de operare. Prin urmare, programatorii și-au dat seama de necesitatea unor servicii care să le permită să planifice și să execute sarcinile centralizat cu mult timp în urmă.

Sistemele de operare asemănătoare Unix își au originea în Versiunea 7 Unix, dezvoltată în anii 70 ai secolului trecut la Bell Labs, inclusiv de celebrul Ken Thompson. Versiunea 7 Unix a inclus și cron, un serviciu pentru rularea regulată a sarcinilor superutilizatorului.

Un cron modern tipic este un program simplu, dar algoritmul de operare al versiunii originale a fost și mai simplu: serviciul s-a trezit o dată pe minut, a citit un tabel cu sarcini dintr-un singur fișier (/etc/lib/crontab) și a fost efectuat pentru superutilizator acele sarcini care ar fi trebuit îndeplinite în momentul actual .

Ulterior, versiuni îmbunătățite ale serviciului simplu și util au fost furnizate cu toate sistemele de operare asemănătoare Unix.

Descrierile generalizate ale formatului crontab și principiile de bază ale funcționării utilitarului au fost incluse în standardul principal al sistemelor de operare asemănătoare Unix - POSIX - în 1992 și, astfel, cron dintr-un standard de facto a devenit un standard de jure.

În 1987, Paul Vixie, după ce a chestionat utilizatorii Unix cu privire la dorințele lor pentru cron, a lansat o altă versiune a demonului care a corectat unele dintre problemele cron-ului tradițional și a extins sintaxa fișierelor de tabel.

Până la a treia versiune a Vixie cron a început să îndeplinească cerințele POSIX, în plus, programul avea o licență liberală, sau mai degrabă nu exista deloc licență, cu excepția dorințelor din README: autorul nu oferă garanții, numele autorului nu poate fi șters, iar programul poate fi vândut doar împreună cu codul sursă. Aceste cerințe s-au dovedit a fi compatibile cu principiile software-ului liber care câștiga popularitate în acei ani, așa că unele dintre distribuțiile cheie Linux care au apărut la începutul anilor 90 au luat Vixie cron drept sistem și îl dezvoltă și astăzi.

În special, Red Hat și SUSE dezvoltă un fork al Vixie cron - cronie, iar Debian și Ubuntu folosesc ediția originală a Vixie cron cu multe patch-uri.

Să ne familiarizăm mai întâi cu utilitarul crontab pentru utilizator descris în POSIX, după care ne vom uita la extensiile de sintaxă furnizate în Vixie cron și la utilizarea variațiilor Vixie cron în distribuțiile Linux populare. Și, în cele din urmă, cireasa de pe tort este analiza dispozitivului cron daemon.

POSIX crontab

Dacă cron-ul original a funcționat întotdeauna pentru superutilizator, programatorii moderni se ocupă adesea de sarcinile utilizatorilor obișnuiți, ceea ce este mai sigur și mai convenabil.

Crons sunt furnizate ca un set de două programe: daemonul cron care rulează constant și utilitarul crontab disponibil utilizatorilor. Acesta din urmă vă permite să editați tabelele de sarcini specifice fiecărui utilizator din sistem, în timp ce demonul lansează sarcini din tabelele de utilizator și de sistem.

В Standardul POSIX comportamentul demonului nu este descris în niciun fel și doar programul utilizatorului este formalizat crontab. Existența mecanismelor de lansare a sarcinilor utilizatorului este, desigur, implicită, dar nu este descrisă în detaliu.

Apelând utilitarul crontab, puteți face patru lucruri: editați tabelul de sarcini al utilizatorului în editor, încărcați tabelul dintr-un fișier, afișați tabelul de activități curent și ștergeți tabelul de activități. Exemple de cum funcționează utilitarul crontab:

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

Când chemat crontab -e va fi folosit editorul specificat în variabila de mediu standard EDITOR.

Sarcinile în sine sunt descrise în următorul format:

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

Primele cinci câmpuri ale înregistrărilor: minute [1..60], ore [0..23], zile ale lunii [1..31], luni [1..12], zile ale săptămânii [0. .6], unde 0 este duminica. Ultimul, al șaselea, câmp este o linie care va fi executată de interpretul de comandă standard.

În primele cinci câmpuri, valorile pot fi enumerate separate prin virgule:

# задача, выполняемая в первую и десятую минуты каждого часа
1,10 * * * * /path/to/exec -a -b -c

Sau cu cratima:

# задача, выполняемая в каждую из первых десяти минут каждого часа
0-9 * * * * /path/to/exec -a -b -c

Accesul utilizatorilor la programarea sarcinilor este reglementat în POSIX de fișierele cron.allow și cron.deny, care listează utilizatorii cu acces la crontab și, respectiv, utilizatorii fără acces la program. Standardul nu reglementează în niciun fel locația acestor fișiere.

Conform standardului, cel puțin patru variabile de mediu trebuie să fie transmise programelor lansate:

  1. HOME - directorul principal al utilizatorului.
  2. LOGNAME — autentificare utilizator.
  3. PATH este calea unde puteți găsi utilitare standard de sistem.
  4. SHELL — calea către interpretul de comenzi utilizat.

În special, POSIX nu spune nimic despre de unde provin valorile acestor variabile.

Cel mai bine vândut - Vixie cron 3.0pl1

Strămoșul comun al variantelor cron populare este Vixie cron 3.0pl1, introdus în lista de corespondență comp.sources.unix în 1992. Vom analiza mai detaliat principalele caracteristici ale acestei versiuni.

Vixie cron vine în două programe (cron și crontab). Ca de obicei, demonul este responsabil pentru citirea și rularea sarcinilor din tabelul de sarcini de sistem și tabelele de sarcini individuale ale utilizatorului, iar utilitarul crontab este responsabil de editarea tabelelor de utilizator.

Tabelul de sarcini și fișierele de configurare

Tabelul de sarcini pentru superutilizator este localizat în /etc/crontab. Sintaxa tabelului de sistem corespunde sintaxei lui Vixie cron, cu excepția faptului că a șasea coloană din acesta indică numele utilizatorului în numele căruia este lansată sarcina:

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

Tabelele de sarcini de utilizator obișnuite sunt situate în /var/cron/tabs/username și folosesc aceeași sintaxă. Când rulați utilitarul crontab ca utilizator, acestea sunt fișierele care sunt editate.

Listele de utilizatori cu acces la crontab sunt gestionate în fișierele /var/cron/allow și /var/cron/deny, unde trebuie doar să introduceți numele de utilizator într-o linie separată.

Sintaxă extinsă

În comparație cu crontab POSIX, soluția lui Paul Vixey conține câteva modificări foarte utile la sintaxa tabelelor de sarcini ale utilitarului.

O nouă sintaxă de tabel a devenit disponibilă: de exemplu, puteți specifica zilele săptămânii sau lunile după nume (luni, marți și așa mai departe):

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

Puteți specifica pasul prin care sunt lansate sarcinile:

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

Pașii și intervalele pot fi amestecate:

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

Sunt acceptate alternative intuitive la sintaxa obișnuită (repornire, anual, anual, lunar, săptămânal, zilnic, la miezul nopții, la oră):

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

Mediul de execuție a sarcinilor

Vixie cron vă permite să schimbați mediul de rulare a aplicațiilor.

Variabilele de mediu USER, LOGNAME și HOME nu sunt pur și simplu furnizate de demon, ci sunt preluate dintr-un fișier passwd. Variabila PATH este setată la „/usr/bin:/bin”, iar variabila SHELL este setată la „/bin/sh”. Valorile tuturor variabilelor, cu excepția LOGNAME, pot fi modificate în tabelele de utilizator.

Unele variabile de mediu (în special SHELL și HOME) sunt folosite de cron însuși pentru a rula sarcina. Iată cum ar putea arăta utilizarea bash în loc de sh standard pentru a rula sarcini personalizate:

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

În cele din urmă, toate variabilele de mediu definite în tabel (utilizate de cron sau necesare procesului) vor fi transmise sarcinii care rulează.

Pentru a edita fișiere, crontab folosește editorul specificat în variabila de mediu VISUAL sau EDITOR. Dacă mediul în care a fost rulat crontab nu are aceste variabile definite, atunci se folosește „/usr/ucb/vi” (ucb este probabil Universitatea din California, Berkeley).

cron pe Debian și Ubuntu

Dezvoltatorii Debian și distribuțiile derivate au lansat versiune foarte modificată Vixie cron versiunea 3.0pl1. Nu există diferențe în sintaxa fișierelor de tabel; pentru utilizatori este același cron Vixie. Cea mai mare caracteristică nouă: Asistență syslog, SELinux и PAM.

Modificările mai puțin vizibile, dar tangibile includ locația fișierelor de configurare și a tabelelor de activități.

Tabelele utilizatorilor din Debian sunt situate în directorul /var/spool/cron/crontabs, tabelul de sistem este încă acolo - în /etc/crontab. Tabelele de sarcini specifice pachetului Debian sunt plasate în /etc/cron.d, de unde demonul cron le citește automat. Controlul accesului utilizatorului este controlat de fișierele /etc/cron.allow și /etc/cron.deny.

Shell-ul implicit este încă /bin/sh, care în Debian este un mic shell compatibil cu POSIX liniuţă, lansat fără a citi nicio configurație (în modul non-interactiv).

Cron însuși în cele mai recente versiuni de Debian este lansat prin systemd, iar configurația de lansare poate fi vizualizată în /lib/systemd/system/cron.service. Nu există nimic special în configurația serviciului; orice gestionare mai subtilă a sarcinilor se poate face prin variabilele de mediu declarate direct în crontab-ul fiecărui utilizator.

cronie pe RedHat, Fedora și CentOS

amice — furcă a Vixie cron versiunea 4.1. Ca și în Debian, sintaxa nu s-a schimbat, dar a fost adăugat suport pentru PAM și SELinux, lucrul într-un cluster, urmărirea fișierelor folosind inotify și alte caracteristici.

Configurația implicită este în locurile obișnuite: tabelul de sistem este în /etc/crontab, pachetele își pun tabelele în /etc/cron.d, tabelele de utilizator merg în /var/spool/cron/crontabs.

Daemonul rulează sub control systemd, configurația serviciului este /lib/systemd/system/crond.service.

Pe distribuțiile asemănătoare Red Hat, /bin/sh este folosit implicit la pornire, care este bash-ul standard. Trebuie remarcat faptul că atunci când rulează joburi cron prin /bin/sh, shell-ul bash pornește în modul compatibil POSIX și nu citește nicio configurație suplimentară, rulând în modul non-interactiv.

cronie în SLES și openSUSE

Distribuția germană SLES și derivatul său openSUSE folosesc același client. Demonul de aici este lansat și sub systemd, configurația serviciului se află în /usr/lib/systemd/system/cron.service. Configurație: /etc/crontab, /etc/cron.d, /var/spool/cron/tabs. /bin/sh este același bash care rulează în modul non-interactiv compatibil POSIX.

Dispozitivul Vixie cron

Descendenții moderni ai cron nu s-au schimbat radical în comparație cu Vixie cron, dar au dobândit totuși noi caracteristici care nu sunt necesare pentru a înțelege principiile programului. Multe dintre aceste extensii sunt prost proiectate și confundă codul. Codul sursă cron original de Paul Vixey este o plăcere de citit.

Prin urmare, am decis să analizez dispozitivul cron folosind exemplul unui program cron comun ambelor ramuri de dezvoltare - Vixie cron 3.0pl1. Voi simplifica exemplele eliminând ifdef-urile care complică citirea și omițând detalii minore.

Lucrarea demonului poate fi împărțită în mai multe etape:

  1. Inițializarea programului.
  2. Colectarea și actualizarea listei de sarcini de executat.
  3. Bucla cron principală rulează.
  4. Începeți o sarcină.

Să le privim în ordine.

Inițializare

La pornire, după verificarea argumentelor procesului, cron instalează manevrele de semnal SIGCHLD și SIGHUP. Primul face o intrare în jurnal despre terminarea procesului copil, al doilea închide descriptorul fișierului jurnal:

signal(SIGCHLD, sigchld_handler);
signal(SIGHUP, sighup_handler);

Daemonul cron rulează întotdeauna singur pe sistem, doar ca superutilizator și din directorul cron principal. Următoarele apeluri creează un fișier de blocare cu PID-ul procesului demon, asigurați-vă că utilizatorul este corect și schimbați directorul curent în cel principal:

acquire_daemonlock(0);
set_cron_uid();
set_cron_cwd();

Este setată calea implicită, care va fi folosită la pornirea proceselor:

setenv("PATH", _PATH_DEFPATH, 1);

Apoi procesul este „daemonizat”: creează o copie copil a procesului prin apelarea fork și o nouă sesiune în procesul copil (apel setsid). Procesul părinte nu mai este necesar și iese:

switch (fork()) {
case -1:
    /* критическая ошибка и завершение работы */
    exit(0);
break;
case 0:
    /* дочерний процесс */
    (void) setsid();
break;
default:
    /* родительский процесс завершает работу */
    _exit(0);
}

Încetarea procesului părinte eliberează blocarea fișierului de blocare. În plus, este necesară actualizarea PID-ului din fișier pentru copil. După aceasta, baza de date de sarcini este completată:

/* повторный захват лока */
acquire_daemonlock(0);

/* Заполнение БД  */
database.head = NULL;
database.tail = NULL;
database.mtime = (time_t) 0;
load_database(&database);

Apoi cron trece la ciclul de lucru principal. Dar înainte de asta, merită să aruncați o privire la încărcarea listei de sarcini.

Colectarea și actualizarea listei de sarcini

Funcția load_database este responsabilă pentru încărcarea listei de sarcini. Verifică sistemul principal crontab și directorul cu fișierele utilizator. Dacă fișierele și directorul nu s-au modificat, lista de activități nu este recitită. În caz contrar, începe să se formeze o nouă listă de sarcini.

Încărcarea unui fișier de sistem cu nume speciale de fișiere și tabele:

/* если файл системной таблицы изменился, перечитываем */
if (syscron_stat.st_mtime) {
    process_crontab("root", "*system*",
    SYSCRONTAB, &syscron_stat,
    &new_db, old_db);
}

Încărcarea tabelelor utilizatorilor într-o buclă:

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

După care vechea bază de date este înlocuită cu una nouă.

În exemplele de mai sus, apelul funcției process_crontab verifică dacă un utilizator care se potrivește cu numele fișierului tabelului există (cu excepția cazului în care este un superutilizator) și apoi apelează load_user. Acesta din urmă citește deja fișierul în sine linie cu linie:

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

Aici, fie variabila de mediu este setată (linii de forma VAR=valoare) folosind funcțiile load_env / env_set, fie descrierea sarcinii este citită (* * * * * /path/to/exec) folosind funcția load_entry.

Entitatea de intrare pe care load_entry o returnează este sarcina noastră, care este plasată în lista generală de sarcini. Funcția în sine efectuează o analiză verbosă a formatului de timp, dar suntem mai interesați de formarea variabilelor de mediu și a parametrilor de lansare a sarcinilor:

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

Bucla principală funcționează cu lista curentă de sarcini.

Bucla principală

Cron-ul original din Versiunea 7 Unix a funcționat destul de simplu: a recitit configurația într-o buclă, a lansat sarcinile din minutul curent ca superutilizator și a dormit până la începutul următorului minut. Această abordare simplă pe mașinile mai vechi necesita prea multe resurse.

În SysV a fost propusă o versiune alternativă, în care demonul a intrat în somn fie până la cel mai apropiat minut pentru care a fost definită sarcina, fie timp de 30 de minute. Au fost consumate mai puține resurse pentru recitirea configurației și verificarea sarcinilor în acest mod, dar actualizarea rapidă a listei de sarcini a devenit incomod.

Vixie Cron a revenit la verificarea listelor de sarcini o dată pe minut, din fericire până la sfârșitul anilor 80 existau mult mai multe resurse pe mașinile standard 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;
}

Funcția cron_sleep este direct implicată în executarea sarcinilor, apelând funcțiile job_runqueue (enumerați și rulați sarcini) și do_command (rulați fiecare sarcină individuală). Ultima funcție merită examinată mai detaliat.

Rularea unei sarcini

Funcția do_command este executată în stil Unix bun, adică face o furcă pentru a efectua sarcina în mod asincron. Procesul părinte continuă să lanseze sarcini, procesul copil pregătește procesul de sarcini:

switch (fork()) {
case -1:
    /*не смогли выполнить fork */
    break;
case 0:
    /* дочерний процесс: на всякий случай еще раз пробуем захватить главный лок */
    acquire_daemonlock(1);
    /* переходим к формированию процесса задачи */
    child_process(e, u);
    /* по завершению дочерний процесс заканчивает работу */
    _exit(OK_EXIT);
    break;
default:
    /* родительский процесс продолжает работу */
    break;
}

Există destul de multă logică în child_process: preia fluxuri standard de ieșire și de eroare pe sine, pentru a le trimite apoi la e-mail (dacă variabila de mediu MAILTO este specificată în tabelul de activități) și, în cele din urmă, așteaptă procesul sarcinii de finalizat.

Procesul de sarcină este format dintr-o altă furcă:

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

Asta este practic tot ce este cron. Am omis câteva detalii interesante, de exemplu, contabilizarea utilizatorilor la distanță, dar am subliniat principalul lucru.

postfață

Cron este un program surprinzător de simplu și util, realizat în cele mai bune tradiții ale lumii Unix. Ea nu face nimic în plus, dar își face treaba minunat de câteva decenii acum. Obținerea codului pentru versiunea care vine cu Ubuntu nu a durat mai mult de o oră și m-am distrat foarte mult! Sper că am reușit să vi-l împărtășesc.

Nu știu despre tine, dar sunt puțin trist să-mi dau seama că programarea modernă, cu tendința ei de a se complica și prea abstractă, nu a fost favorabilă unei asemenea simplități de mult timp.

Există multe alternative moderne la cron: systemd-timers vă permit să organizați sisteme complexe cu dependențe, fcron vă permite să reglați mai flexibil consumul de resurse pe sarcini. Dar personal, cel mai simplu crontab a fost întotdeauna suficient pentru mine.

Pe scurt, iubește Unix, folosește programe simple și nu uita să citești mana pentru platforma ta!

Sursa: www.habr.com

Adauga un comentariu