Cron în Linux: istorie, utilizare și structură

Cron în Linux: istorie, utilizare și structură

Cineva a spus că oamenii fericiți nu își pierd timpul. În acele vremuri sălbatice nu existau nici programatori, nici Unix, dar în zilele noastre programatorii știu cu siguranță că în locul lor cron se va ocupa de timp.

Utilitarele din linia de comandă sunt deopotrivă slăbiciunea și rutina mea. sed, awk, wc, cut și alte programe vechi sunt rul trase de scripturi pe serverele noastre zilnic. Multe dintre ele sunt configurate ca sarcini pentru cron, un planificator ce provine din anii '70.

Am folosit cron superficial o lungă perioadă, neînțelegând detaliile, dar într-o zi, întâmpinând o eroare la rularea unui script, am decis să mă aprofundez. Așa a apărut acest articol, scriind cu informații din POSIX crontab, principalele variante cron în distribuțiile populare de Linux și structura unora dintre ele.

Folosești Linux și rulezi sarcini în cron? Ești interesat de arhitectura aplicațiilor de sistem în Unix? Atunci suntem pe aceeași lungime de undă!

Cuprins

Originea speciilor

Executarea periodică a programelor utilizatorilor sau sistemelor este o necesitate evidentă în toate sistemele de operare. De aceea, programatorii au realizat de mult nevoie de servicii care să permită planificarea și executarea centralizată a sarcinilor.

Sistemele de operare de tip Unix își au originile în Version 7 Unix, dezvoltată în anii '70 în Bell Labs, de către celebrul Ken Thompson. Împreună cu Version 7 Unix, a fost inclus și cron, un serviciu pentru executarea regulată a sarcinilor utilizatorului supervizor.

Un cron modern tipic este un program simplu, dar algoritmul funcționării versiunii originale era și mai simplu: serviciul se trezea la fiecare minut, citea tabelul cu sarcini dintr-un singur fișier (\/etc\/lib\/crontab) și executa pentru utilizatorul supervizor sarcinile care trebuiau îndeplinite în acel minut.

În timp, variantele îmbunătățite ale acestui serviciu simplu și util au fost livrate cu toate sistemele de operare de tip Unix.

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

În 1987, Paul Vixie, după ce a intervievat utilizatorii Unix despre dorințele lor legate de cron, a lansat o nouă versiune a demonului, care corectează unele probleme ale cron-ului tradițional și extinde sintaxa fișierelor-tabele.

A treia versiune a Vixie cron a devenit conformă cu cerințele POSIX, și programul avea o licență liberală, mai precis, nu avea deloc o licență, dacă nu luăm în considerare cerințele din README: autorul nu oferă garanții, numele autorului nu poate fi eliminat, 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, astfel că unele dintre principalele distribuții Linux apărute la începutul anilor '90 au adoptat Vixie cron ca sistem și continuă să-l dezvolte și astăzi.

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

Hai să ne familiarizăm mai întâi cu utilitarul crontab descris în POSIX, după care vom explora extensiile sintaxei prezentate în Vixie cron și utilizarea variantelor Vixie cron în distribuitorii populare de Linux. În cele din urmă, cireșa de pe tort — analiza funcției demonului cron.

POSIX crontab

Dacă cron-ul original a fost întotdeauna destinat superutilizatorului, planner-ele moderne se ocupă adesea de sarcinile utilizatorilor obișnuiți, ceea ce este mai sigur și mai convenabil.

Cron-urile vin ca un pachet de două programe: un demon cron care rulează continuu și un utilitar crontab accesibil utilizatorilor. Ultimul permite editarea tabelelor de sarcini, specifice fiecărui utilizator din sistem, în timp ce demonul rulează sarcinile din tabelele utilizatorului și din cele de sistem.

În standardul POSIX nu descrie comportamentul demonului și formalizează doar programul utilizatorului crontab. Existența mecanismelor pentru lansarea sarcinilor utilizatorilor este evidentă, dar nu este detaliată.

Prin invocarea utilitarului crontab pot fi efectuate patru acțiuni: editarea tabelei de sarcini a utilizatorului în editor, încărcarea tabelului dintr-un fișier, afișarea tabelei curente de sarcini și curățarea tabelei de sarcini. Exemple de utilizare a utilitarului crontab:

crontab -e # editează tabela de sarcini
crontab -l # afișează tabela de sarcini
crontab -r # șterge tabela de sarcini
crontab path/to/file.crontab # încarcă tabela de sarcini dintr-un fișier

Când apelăm crontab -e se va folosi editorul specificat în variabila de mediu standard EDITOR.

Sarcinile 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 un șir de caractere care va fi executat de interpretul de comenzi standard.

În primele cinci câmpuri, valorile pot fi separate prin virgulă:

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

Sau prin cratimă:

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

Accesul utilizatorilor la programarea sarcinilor este reglementat în fișierele POSIX cron.allow și cron.deny în care sunt enumerate, respectiv, utilizatorii cu acces la crontab și utilizatorii fără acces la program. Locația acestor fișiere nu este reglementată de standard.

Programele care trebuie să fie executate, conform standardului, trebuie să primească cel puțin patru variabile de mediu:

  1. HOME — directorul de acasă al utilizatorului.
  2. LOGNAME — numele de utilizator.
  3. PATH — calea unde se pot găsi utilitarele standard ale sistemului.
  4. SHELL — calea către interpretul de comenzi utilizat.

Este notabil că POSIX nu menționează nimic despre sursa valorilor pentru aceste variabile.

Cel mai vândut — Vixie cron 3.0pl1

Strămoșul comun al variantelor populare cron este Vixie cron 3.0pl1, lansat în mailing listul comp.sources.unix în 1992. Vom analiza în detaliu principalele funcționalități ale acestei versiuni.

Vixie cron este livrat în două programe (cron și crontab). Ca de obicei, demonul este responsabil pentru citirea și executarea sarcinilor din tabelul de sarcini de sistem și din tabelele de sarcini ale utilizatorilor individuali, iar utilitarul crontab — pentru editarea tabelelor utilizatorilor.

Tabela sarcinilor și fișierele de configurare

Tabela de sarcini a superutilizatorului se află în /etc/crontab. Sintaxa tabelei de sistem corespunde sintaxei Vixie cron, cu mențiunea că a șasea coloană indică numele utilizatorului sub care se execută sarcina:

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

Tabelele de sarcini ale utilizatorilor obișnuiți se află în /var/cron/tabs/username și utilizează o sintaxă comună. Atunci când se execută utilitarul crontab în numele utilizatorului, se editează exact aceste fișiere.

Gestionarea listelor de utilizatori care au acces la crontab se face în fișierele /var/cron/allow și /var/cron/deny, unde este suficient să adaugi numele utilizatorului pe o linie separată.

Sintaxă avansată

Comparativ cu POSIX crontab, soluția lui Paul Vixie conține câteva modificări foarte utile în sintaxa tabelelor de sarcini ale utilitarului.

A apărut o nouă sintaxă pentru tabele: de exemplu, se pot specifica zilele săptămânii sau lunile nominal (Lun, Mar și așa mai departe):

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

Se poate specifica pasul prin care se lansează sarcinile:

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

Pașii și intervalele pot fi combinate:

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

Sunt acceptate alternative intuitive la sintaxa obișnuită (reboot, yearly, annually, monthly, weekly, daily, midnight, hourly):

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

Mediul de execuție a sarcinilor

Vixie cron permite modificarea mediului aplicațiilor care sunt lansate.

Variabilele de mediu USER, LOGNAME și HOME nu sunt doar furnizate de daemon, ci sunt preluate din fișier passwd. Variabila PATH primește valoarea "/usr/bin:/bin", iar SHELL - "/bin/sh". Valorile tuturor variabilelor, cu excepția LOGNAME, pot fi modificate în tabelele utilizatorilor.

Unele variabile de mediu (în special SHELL și HOME) sunt utilizate de cron pentru a lansa sarcina. Iată cum poate arăta utilizarea bash în locul standardului sh pentru a lansa sarcini de utilizator:

SHELL=/bin/bash
HOME=/tmp/
# exec va fi lansat cu bash în /tmp/
* * * * * /path/to/exec

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

Pentru editarea fișierelor cu utilitarul crontab se folosește editorul specificat în variabila de mediu VISUAL sau EDITOR. Dacă în mediu, unde a fost lansat crontab, aceste variabile nu sunt definite, se folosește "/usr/ucb/vi" (ucb este probabil Universitatea din California, Berkeley).

cron în Debian și Ubuntu

Dezvoltatorii Debian și ai distribuțiilor derivate au lansat o versiune puternic modificată a versiunii Vixie cron 3.0pl1. Nu există diferențe în sintaxa fișierelor-tabele, pentru utilizatori este același Vixie cron. Cele mai mari noi caracteristici: suport pentru syslog, SELinux și PAM.

Printre modificările mai puțin vizibile, dar tangibile, se numără locațiile fișierelor de configurare și ale tabelelor de sarcini.

Tabelele utilizatorului în Debian se află în directorul /var/spool/cron/crontabs, tabela sistemului rămâne tot acolo - în /etc/crontab. Tabelele de sarcini specifice pachetelor Debian sunt plasate în /etc/cron.d, de unde daemonul cron le citește automat. Controlul accesului utilizatorilor este reglementat prin fișierele /etc/cron.allow și /etc/cron.deny.

Ca shell implicit se folosește în continuare /bin/sh, iar în rolul său, în Debian, se află un shell mic compatibil POSIX dash, lansat fără a citi vreo configurație (în modul neinteractiv).

Cronul în versiunile recente de Debian este gestionat prin systemd, iar configurarea acestuia poate fi consultată în /lib/systemd/system/cron.service. Nu există nimic special în configurația serviciului, orice gestionare mai detaliată a sarcinilor se poate realiza prin variabile de mediu, declarate direct în crontab-ul fiecărui utilizator.

cronie în RedHat, Fedora și CentOS

cronie — o ramificație a Vixie cron versiunea 4.1. La fel ca în Debian, sintaxa nu s-a schimbat, dar a fost adăugată suport pentru PAM și SELinux, funcționare în cluster, monitorizarea fișierelor prin inotify și alte funcționalități.

Configurarea implicită se află în locuri obișnuite: tabela de sistem — în /etc/crontab, pachetele își plasează tabelele în /etc/cron.d, iar tabelele utilizatorilor sunt plasate în /var/spool/cron/crontabs.

Demonul este gestionat de systemd, configurarea serviciului fiind în /lib/systemd/system/crond.service.

În distribuțiile de tip Red Hat, la pornire se folosește implicit /bin/sh, care este, de fapt, bash standard. Este important de menționat că, atunci când sarcinile cron sunt lansate prin /bin/sh, shell-ul bash pornește în modul compatibil POSIX și nu citește nicio configurație suplimentară, funcționând în modul non-interactiv.

cronie în SLES și openSUSE

Distribuția germană SLES și derivatul său openSUSE folosesc același cronie. Demonul este, de asemenea, gestionat de systemd, iar configurația serviciului se află în /usr/lib/systemd/system/cron.service. Configurarea include: /etc/crontab, /etc/cron.d, /var/spool/cron/tabs. Ca /bin/sh este utilizat același bash, pornit în modul non-interactiv compatibil POSIX.

Structura Vixie cron

În comparație cu Vixie cron, descendenții moderni ai cron nu s-au schimbat radical, dar au dobândit totuși noi funcționalități care nu sunt necesare pentru înțelegerea principiilor de funcționare ale programului. Multe dintre aceste extensii sunt implementate neglijent și complică codul. Codul sursă original al cron, scris de Paul Vixie, este o plăcere de citit.

De aceea, am decis să analizez structura cron folosind exemplul comun pentru ambele ramuri de dezvoltare a programului cron — Vixie cron 3.0pl1. Voi simplifica exemplele, eliminând directivele ifdef care complică lectura și lăsând deoparte detaliile secundare.

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

  1. Inițializarea programului.
  2. Colectarea și actualizarea listei de sarcini de lansat.
  3. Funcționarea ciclului principal cron.
  4. Lansarea sarcinii.

Să le analizăm pe rând.

Inițializare

Când este lansat, după verificarea argumentelor, procesul cron stabilește handlerii pentru semnalele SIGCHLD și SIGHUP. Primul înregistrează în jurnal închiderea procesului fiu, iar al doilea închide descriptorul de fișier al fișierului de jurnal:

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

Demonul cron din sistem rulează întotdeauna ca un singur proces, exclusiv ca superutilizator și din directorul principal cron. Următoarele apeluri creează un fișier de blocare cu PID-ul procesului demon, se asigură că utilizatorul este corect și schimbă directorul curent în cel principal:

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

Se setează calea implicită, care va fi utilizată la lansarea proceselor:

setenv("PATH", _PATH_DEFPATH, 1);

Apoi, procesul "demonizează": creează o copie fiică a procesului printr-un apel fork și o nouă sesiune în procesul fiu (apelul setsid). Nu mai este nevoie de procesul părinte — așa că acesta își termină execuția:

switch (fork()) {
case -1:
    \/\* eroare critică și terminarea execuției *\/ 
    exit(0);
break;
case 0:
    \/\* proces fiu *\/ 
    (void) setsid();
break;
default:
    \/\* proces părinte își termină execuția *\/ 
    _exit(0);
}

Finalizarea procesului părinte eliberează blocajul de pe fișierul de blocare. În plus, este necesară actualizarea PID-ului din fișier cu cel al procesului fiu. După aceasta, se umple baza de date a sarcinilor:

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

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

Apoi, cron trece la ciclul principal de lucru. Dar înainte de aceasta, este bine să verificăm încărcarea listei de sarcini.

Colectarea și actualizarea listei de sarcini

Funcția load_database este responsabilă de încărcarea listei de sarcini. Aceasta verifică crontabul principal al sistemului și directorul cu fișierele utilizatorilor. Dacă fișierele și directorul nu s-au schimbat, lista de sarcini nu este reîncărcată. În cazul contrar, începe formarea unei noi liste de sarcini.

Încărcarea fișierului sistemului cu nume specfice 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 în ciclu:

while (NULL != (dp = readdir(dir))) {
    char    fname[MAXNAMLEN+1],
            tabname[MAXNAMLEN+1];
    \/\* nu este nevoie să citim fișierele care începe cu punct *\/ 
    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ă aceea, vechea bază de date este înlocuită cu cea nouă.

În exemplele de mai sus, apelul funcției process_crontab se asigură că există un utilizator corespunzător numelui fișierului tabel (cu excepția cazului în care este superutilizator), după care apelează load_user. Ultima citește fișierul 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 se setează variabilă de mediu (stringuri de tip VAR=value) prin funcțiile load_env / env_set, fie se citește descrierea sarcinii (***** /path/to/exec) prin funcția load_entry.

Entitatea entry, pe care o returnează load_entry, este sarcina noastră, plasată în lista comună de sarcini. În această funcție se desfășoară o analiză cuprinzătoare a formatului de timp, dar pe noi ne interesează mai mult formarea variabilelor de mediu și parametrii de execuție ai sarcinii:

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

Ciclul principal funcționează cu lista actuală de sarcini.

Bucla principală

Cron-ul original din Version 7 Unix funcționa foarte simplu: în ciclu citea configurația, lansa sarcinile minutei curente ca superutilizator și dormea până la începutul minutei următoare. Această abordare simplă necesita prea multe resurse pe mașinile vechi.

În SysV a fost propusă o versiune alternativă, în care demonul adormea fie până la cea mai apropiată minută pentru care era definită o sarcină, fie pentru 30 de minute. Resursele necesare pentru citirea configurației și verificarea sarcinilor în acest mod erau mai reduse, dar actualizarea rapidă a listei de sarcini devenea incomodă.

Vixie cron a revenit la verificarea listelor de sarcini la fiecare minut, având în vedere că spre sfârșitul anilor '80 resursele pe mașinile Unix standard au crescut semnificativ:

/* первичная загрузка задач */
load_database(&database);
/* запустить задачи, поставленные к выполнению после перезагрузки системы */
run_reboot_jobs(&database);
/* сделать TargetTime началом ближайшей минуты */
cron_sync();
while (TRUE) {
    /* выполнить задачи, после чего спать до TargetTime с поправкой на время, потраченное на задачи */
    cron_sleep();

    /* перечитать конфигурацию */
    load_database(&database);

    /* собрать задачи для данной минуты */
    cron_tick(&database);

    /* перевести TargetTime на начало следующей минуты */
    TargetTime += 60;
}

Execuția sarcinilor este realizată de funcția cron_sleep, care apelează funcțiile job_runqueue (iterarea și lansarea sarcinilor) și do_command (execuția fiecărei sarcini individuale). Această din urmă funcție merită discutată mai în detaliu.

Lansarea sarcinii

Funcția do_command este implementată în stil Unix, adică pentru a executa sarcina în mod asincron, face un fork. Procesul părinte continuă să lanseze sarcini, în timp ce procesul fiu se ocupă de pregătirea procesului sarcinii:

switch (fork()) {
case -1:
    /* nu s-a putut efectua fork */
    break;
case 0:
    /* procesul fiu: pentru siguranță încercăm din nou să capturăm blocajul principal */
    acquire_daemonlock(1);
    /* trecem la formarea procesului sarcinii */
    child_process(e, u);
    /* la final, procesul fiu își încheie activitatea */
    _exit(OK_EXIT);
    break;
default:
    /* procesul părinte continuă să lucreze */
    break;
}

În child_process există o mulțime de logica: acceptă fluxurile standard de ieșire și erori pentru a le trimite apoi pe email (dacă în tabelul de sarcini este specificată variabila de mediu MAILTO), și, în cele din urmă, așteaptă finalizarea procesului principal al sarcinii.

Procesul sarcinii este generat printr-un alt fork:

switch (vfork()) {
case -1:
    /* în caz de eroare, procesul se încheie imediat */
    exit(ERROR_EXIT);
case 0:
    /* procesul copil generează o nouă sesiune, terminal etc.
     */
    (void) setsid();

    /*
     * urmează o configurare lungă a ieșirii procesului, o sărim pentru brevități
     */

    /* schimbarea directorului, utilizatorului și grupului utilizatorului,
     * adică procesul nu mai este super-utilizator
     */
    setgid(e->gid);
    setuid(e->uid);
    chdir(env_get("HOME", e->envp));

    /* lansarea efectivă a comenzii
     */
    {
        /* variabila de mediu SHELL indică interpretul pentru lansare */
        char    *shell = env_get("SHELL", e->envp);

        /* procesul este lansat fără a transmite mediul procesului părinte,
         * exact așa cum este descris în tabelul de sarcini al utilizatorului  */
        execle(shell, shell, "-c", e->cmd, (char *)0, e->envp);

        /* eroare — și procesul nu a fost lansat? încheierea procesului */
        perror("execl");
        _exit(ERROR_EXIT);
    }
    break;
default:
    /* procesul continuă să funcționeze: așteaptă finalizarea și ieșirea */
    break;
}

Ei bine, acesta este tot cron. Am omis unele detalii interesante, cum ar fi contabilizarea utilizatorilor îndepărtați, dar am prezentat esențialul.

Cuvânt înainte

Cron — o programă surprinzător de simplă și utilă, realizată în cele mai bune tradiții ale lumii Unix. Nu face nimic inutil, dar își face treaba minunat de câteva decenii. Familiarizarea cu codul versiunii care vine cu Ubuntu a durat nu mai mult de o oră, iar plăcerea pe care am simțit-o a fost imensă! Sper că am reușit să o împărtășesc cu voi.

Nu știu cum sunteți voi, dar este puțin trist să realizez că programarea modernă, cu tendința sa de a deveni complicată și abstractizată, nu mai oferă de multă vreme o asemenea simplitate.

Există multe alternative moderne la cron: systemd-timers permite organizarea unor sisteme complexe cu dependențe, iar fcron permite reglementarea mai flexibilă a consumului de resurse de către sarcini. Dar personal, mi-au fost întotdeauna suficiente cele mai simple crontab-uri.

Pe scurt, iubiți Unix, folosiți programe simple și nu uitați să citiți man-urile pentru platforma voastră!

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster