Cron in Linux: storia, utilizzo e dispositivo

Cron in Linux: storia, utilizzo e dispositivo

Il classico scriveva che gli happy hour non si guardano. A quei tempi selvaggi non c'erano né programmatori né Unix, ma oggi i programmatori lo sanno per certo: cron terrà traccia del tempo al posto loro.

Le utilità della riga di comando sono sia un punto debole che un compito ingrato per me. sed, awk, wc, cut e altri vecchi programmi vengono eseguiti ogni giorno tramite script sui nostri server. Molti di essi sono progettati come attività per cron, uno scheduler originario degli anni '70.

Per molto tempo ho utilizzato cron in modo superficiale, senza entrare nei dettagli, ma un giorno, quando ho riscontrato un errore durante l'esecuzione di uno script, ho deciso di esaminarlo a fondo. Ecco come è apparso questo articolo, mentre lo scrivevo ho acquisito familiarità con POSIX crontab, le principali opzioni cron nelle popolari distribuzioni Linux e la struttura di alcune di esse.

Utilizzi Linux ed esegui attività cron? Sei interessato all'architettura delle applicazioni di sistema in Unix? Allora siamo sulla buona strada!

contenuto

Origine delle specie

L'esecuzione periodica dei programmi utente o di sistema è una necessità ovvia in tutti i sistemi operativi. Pertanto, i programmatori si sono resi conto già da tempo della necessità di servizi che consentano loro di pianificare ed eseguire le attività in modo centralizzato.

I sistemi operativi simili a Unix fanno risalire le loro origini alla versione 7 Unix, sviluppata negli anni '70 del secolo scorso presso i Bell Labs, anche dal famoso Ken Thompson. La versione 7 Unix includeva anche cron, un servizio per eseguire regolarmente attività di superutente.

Un tipico cron moderno è un programma semplice, ma l'algoritmo operativo della versione originale era ancora più semplice: il servizio si attivava una volta al minuto, leggeva una tabella con attività da un singolo file (/etc/lib/crontab) ed eseguiva per superutente quelle attività che avrebbero dovuto essere eseguite al momento attuale.

Successivamente sono state fornite versioni migliorate del servizio semplice e utile con tutti i sistemi operativi simili a Unix.

Le descrizioni generalizzate del formato crontab e i principi di base del funzionamento dell'utilità sono stati inclusi nello standard principale dei sistemi operativi simili a Unix - POSIX - nel 1992, e quindi cron da standard de facto è diventato uno standard de jure.

Nel 1987, Paul Vixie, dopo aver intervistato gli utenti Unix sui loro desideri per cron, rilasciò un'altra versione del demone che corresse alcuni dei problemi del cron tradizionale e ampliò la sintassi dei file tabellari.

Dalla terza versione di Vixie cron ha iniziato a soddisfare i requisiti POSIX, inoltre il programma aveva una licenza liberale, o meglio non c'era alcuna licenza, ad eccezione dei desideri nel README: l'autore non fornisce garanzie, il nome dell'autore non può essere eliminato e il programma può essere venduto solo insieme al codice sorgente. Questi requisiti si rivelarono compatibili con i principi del software libero che stava guadagnando popolarità in quegli anni, quindi alcune delle principali distribuzioni Linux apparse all'inizio degli anni '90 presero Vixie cron come sistema e lo stanno sviluppando ancora oggi.

In particolare, Red Hat e SUSE sviluppano un fork di Vixie cron - cronie, mentre Debian e Ubuntu utilizzano l'edizione originale di Vixie cron con molte patch.

Facciamo prima conoscenza con l'utilità utente crontab descritta in POSIX, dopodiché esamineremo le estensioni di sintassi fornite in Vixie cron e l'uso delle varianti di Vixie cron nelle popolari distribuzioni Linux. E infine, la ciliegina sulla torta è l'analisi del dispositivo demone cron.

Crontab POSIX

Se il cron originale funzionava sempre per il superutente, i moderni scheduler spesso si occupano delle attività degli utenti ordinari, il che è più sicuro e conveniente.

I cron vengono forniti come un insieme di due programmi: il demone cron costantemente in esecuzione e l'utilità crontab disponibile per gli utenti. Quest'ultimo consente di modificare le tabelle delle attività specifiche per ciascun utente nel sistema, mentre il demone avvia le attività dalle tabelle utente e di sistema.

В Norma POSIX il comportamento del demone non viene descritto in alcun modo e viene formalizzato solo il programma utente crontab. L'esistenza di meccanismi per l'avvio delle attività dell'utente è, ovviamente, implicita, ma non descritta in dettaglio.

Richiamando l'utilità crontab, puoi fare quattro cose: modificare la tabella delle attività dell'utente nell'editor, caricare la tabella da un file, mostrare la tabella delle attività corrente e cancellare la tabella delle attività. Esempi di come funziona l'utilità crontab:

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

Quando chiamato crontab -e verrà utilizzato l'editor specificato nella variabile d'ambiente standard EDITOR.

Le attività stesse sono descritte nel seguente formato:

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

I primi cinque campi delle registrazioni: minuti [1..60], ore [0..23], giorni del mese [1..31], mesi [1..12], giorni della settimana [0. .6], dove 0 è domenica. L'ultimo, il sesto, campo è una riga che verrà eseguita dall'interprete dei comandi standard.

Nei primi cinque campi i valori possono essere elencati separati da virgole:

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

Oppure con un trattino:

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

L'accesso degli utenti alla pianificazione delle attività è regolato in POSIX dai file cron.allow e cron.deny, che elencano rispettivamente gli utenti con accesso a crontab e gli utenti senza accesso al programma. Lo standard non regola in alcun modo la posizione di questi file.

Secondo lo standard, almeno quattro variabili d'ambiente devono essere passate ai programmi avviati:

  1. HOME: directory home dell'utente.
  2. LOGNAME: accesso utente.
  3. PATH è il percorso in cui è possibile trovare le utilità di sistema standard.
  4. SHELL: percorso dell'interprete dei comandi utilizzato.

In particolare, POSIX non dice nulla sulla provenienza dei valori di queste variabili.

Il più venduto: Vixie cron 3.0pl1

L'antenato comune delle popolari varianti di cron è Vixie cron 3.0pl1, introdotto nella mailing list comp.sources.unix nel 1992. Considereremo le caratteristiche principali di questa versione in modo più dettagliato.

Vixie cron è disponibile in due programmi (cron e crontab). Come al solito, il demone è responsabile della lettura e dell'esecuzione delle attività dalla tabella delle attività di sistema e dalle tabelle delle attività dei singoli utenti, mentre l'utilità crontab è responsabile della modifica delle tabelle utente.

Tabella delle attività e file di configurazione

La tabella delle attività del superutente si trova in /etc/crontab. La sintassi della tabella di sistema corrisponde alla sintassi di Vixie cron, con l'eccezione che la sesta colonna in essa indica il nome dell'utente per conto del quale viene avviata l'attività:

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

Le tabelle delle attività degli utenti regolari si trovano in /var/cron/tabs/username e utilizzano la stessa sintassi. Quando esegui l'utilità crontab come utente, questi sono i file che vengono modificati.

Gli elenchi degli utenti con accesso a crontab sono gestiti nei file /var/cron/allow e /var/cron/deny, dove è sufficiente inserire il nome utente in una riga separata.

Sintassi estesa

Rispetto al crontab POSIX, la soluzione di Paul Vixey contiene diverse modifiche molto utili alla sintassi delle tabelle delle attività dell'utilità.

È diventata disponibile una nuova sintassi della tabella: ad esempio, puoi specificare i giorni della settimana o i mesi per nome (lunedì, martedì e così via):

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

È possibile specificare il passaggio attraverso il quale vengono avviate le attività:

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

È possibile mescolare passi e intervalli:

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

Sono supportate alternative intuitive alla sintassi abituale (riavvio, annuale, annuale, mensile, settimanale, giornaliero, a mezzanotte, ogni ora):

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

Ambiente di esecuzione delle attività

Vixie cron ti consente di modificare l'ambiente delle applicazioni in esecuzione.

Le variabili d'ambiente USER, LOGNAME e HOME non sono semplicemente fornite dal demone, ma sono prese da un file passwd. La variabile PATH è impostata su "/usr/bin:/bin" e la variabile SHELL è impostata su "/bin/sh". I valori di tutte le variabili tranne LOGNAME possono essere modificati nelle tabelle utente.

Alcune variabili d'ambiente (in particolare SHELL e HOME) vengono utilizzate da cron stesso per eseguire l'attività. Ecco come potrebbe apparire l'utilizzo di bash invece di sh standard per eseguire attività personalizzate:

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

Alla fine, tutte le variabili di ambiente definite nella tabella (utilizzate da cron o necessarie al processo) verranno passate all'attività in esecuzione.

Per modificare i file, crontab utilizza l'editor specificato nella variabile d'ambiente VISUAL o EDITOR. Se l'ambiente in cui è stato eseguito crontab non ha queste variabili definite, viene utilizzato "/usr/ucb/vi" (ucb è probabilmente l'Università della California, Berkeley).

cron su Debian e Ubuntu

Gli sviluppatori di Debian e delle distribuzioni derivate hanno rilasciato versione altamente modificata Vixiecron versione 3.0pl1. Non ci sono differenze nella sintassi dei file tabella; per gli utenti è lo stesso Vixie cron. La novità più importante: il supporto syslog, SELinux и PAM.

Cambiamenti meno evidenti, ma tangibili, includono la posizione dei file di configurazione e delle tabelle delle attività.

Le tabelle utente in Debian si trovano nella directory /var/spool/cron/crontabs, la tabella di sistema è ancora lì - in /etc/crontab. Le tabelle delle attività specifiche dei pacchetti Debian sono collocate in /etc/cron.d, da dove il demone cron le legge automaticamente. Il controllo dell'accesso utente è controllato dai file /etc/cron.allow e /etc/cron.deny.

La shell predefinita è ancora /bin/sh, che in Debian è una piccola shell compatibile con POSIX trattino, lanciato senza leggere alcuna configurazione (in modalità non interattiva).

Lo stesso Cron nelle ultime versioni di Debian viene avviato tramite systemd e la configurazione di avvio può essere visualizzata in /lib/systemd/system/cron.service. Non c'è nulla di speciale nella configurazione del servizio; qualsiasi gestione più sottile delle attività può essere eseguita tramite variabili di ambiente dichiarate direttamente nel crontab di ciascun utente.

cronie su RedHat, Fedora e CentOS

amico — fork di Vixie cron versione 4.1. Come in Debian, la sintassi non è cambiata, ma è stato aggiunto il supporto per PAM e SELinux, il funzionamento in cluster, il tracciamento dei file utilizzando inotify e altre funzionalità.

La configurazione predefinita è nei soliti posti: la tabella di sistema è in /etc/crontab, i pacchetti inseriscono le loro tabelle in /etc/cron.d, le tabelle utente vanno in /var/spool/cron/crontabs.

Il demone viene eseguito sotto il controllo di systemd, la configurazione del servizio è /lib/systemd/system/crond.service.

Nelle distribuzioni simili a Red Hat, /bin/sh viene utilizzato per impostazione predefinita all'avvio, che è la bash standard. Va notato che quando si eseguono lavori cron tramite /bin/sh, la shell bash si avvia in modalità conforme a POSIX e non legge alcuna configurazione aggiuntiva, funzionando in modalità non interattiva.

cronie in SLES e openSUSE

La distribuzione tedesca SLES e la sua derivata openSUSE utilizzano lo stesso amico. Anche qui il demone viene lanciato sotto systemd, la configurazione del servizio si trova in /usr/lib/systemd/system/cron.service. Configurazione: /etc/crontab, /etc/cron.d, /var/spool/cron/tabs. /bin/sh è la stessa bash in esecuzione in modalità non interattiva conforme a POSIX.

Dispositivo Vixiecron

I moderni discendenti di cron non sono cambiati radicalmente rispetto a Vixie cron, ma hanno comunque acquisito nuove funzionalità che non sono necessarie per comprendere i principi del programma. Molte di queste estensioni sono mal progettate e confondono il codice. Il codice sorgente originale di cron di Paul Vixey è un piacere da leggere.

Pertanto, ho deciso di analizzare il dispositivo cron utilizzando l'esempio di un programma cron comune a entrambi i rami di sviluppo: Vixie cron 3.0pl1. Semplificherò gli esempi rimuovendo gli ifdef che complicano la lettura e omettendo dettagli minori.

Il lavoro del demone può essere suddiviso in diverse fasi:

  1. Inizializzazione del programma.
  2. Raccolta e aggiornamento dell'elenco delle attività da eseguire.
  3. Ciclo cron principale in esecuzione.
  4. Avvia un'attività.

Vediamoli in ordine.

inizializzazione

All'avvio, dopo aver controllato gli argomenti del processo, cron installa i gestori di segnale SIGCHLD e SIGHUP. Il primo crea una voce di log relativa alla terminazione del processo figlio, il secondo chiude il descrittore del file di log:

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

Il demone cron viene sempre eseguito da solo sul sistema, solo come superutente e dalla directory principale cron. Le seguenti chiamate creano un file di blocco con il PID del processo demone, assicurati che l'utente sia corretto e cambia la directory corrente in quella principale:

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

Viene impostato il percorso predefinito, che verrà utilizzato all'avvio dei processi:

setenv("PATH", _PATH_DEFPATH, 1);

Quindi il processo viene “demonizzato”: crea una copia figlio del processo chiamando fork e una nuova sessione nel processo figlio (chiamando setsid). Il processo genitore non è più necessario ed esce:

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

La terminazione del processo principale rilascia il blocco sul file di blocco. Inoltre, è necessario aggiornare il PID nel file del bambino. Successivamente viene compilato il database delle attività:

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

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

Quindi cron passa al ciclo di lavoro principale. Ma prima vale la pena dare un'occhiata al caricamento dell'elenco delle attività.

Raccolta e aggiornamento dell'elenco delle attività

La funzione load_database è responsabile del caricamento dell'elenco delle attività. Controlla il crontab del sistema principale e la directory con i file dell'utente. Se i file e la directory non sono cambiati, l'elenco delle attività non viene riletto. Altrimenti, inizia a formarsi un nuovo elenco di attività.

Caricamento di un file di sistema con nomi di file e tabelle speciali:

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

Caricamento delle tabelle utente in un ciclo:

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

Successivamente il vecchio database viene sostituito con uno nuovo.

Negli esempi precedenti, la chiamata alla funzione process_crontab verifica che esista un utente corrispondente al nome del file della tabella (a meno che non sia un superutente) e quindi chiama load_user. Quest'ultimo legge già il file stesso riga per riga:

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

Qui, la variabile di ambiente viene impostata (righe nella forma VAR=value) utilizzando le funzioni load_env / env_set oppure la descrizione dell'attività viene letta (* * * * * /path/to/exec) utilizzando la funzione load_entry.

L'entità di voce restituita da load_entry è la nostra attività, che viene inserita nell'elenco generale delle attività. La funzione stessa esegue un'analisi dettagliata del formato dell'ora, ma a noi interessa di più la formazione delle variabili di ambiente e dei parametri di avvio delle attività:

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

Il ciclo principale funziona con l'elenco corrente di attività.

Ciclo principale

Il cron originale della versione 7 di Unix funzionava in modo abbastanza semplice: rileggeva la configurazione in loop, avviava le attività del minuto corrente come superutente e dormiva fino all'inizio del minuto successivo. Questo semplice approccio sulle macchine più vecchie richiedeva troppe risorse.

In SysV è stata proposta una versione alternativa, in cui il demone andava in sospensione fino al minuto più vicino per il quale era stata definita l'attività, oppure per 30 minuti. Sono state consumate meno risorse per rileggere la configurazione e controllare le attività in questa modalità, ma l'aggiornamento rapido dell'elenco delle attività è diventato scomodo.

Vixie cron tornò a controllare gli elenchi delle attività una volta al minuto, fortunatamente alla fine degli anni '80 c'erano molte più risorse sulle macchine Unix standard:

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

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

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

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

La funzione cron_sleep è direttamente coinvolta nell'esecuzione delle attività, chiamando le funzioni job_runqueue (elencare ed eseguire attività) e do_command (eseguire ogni singola attività). Vale la pena esaminare più in dettaglio l'ultima funzione.

Esecuzione di un'attività

La funzione do_command viene eseguita in buon stile Unix, ovvero esegue un fork per eseguire l'attività in modo asincrono. Il processo genitore continua ad avviare le attività, il processo figlio prepara il processo delle attività:

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

C'è molta logica in child_process: prende su se stesso l'output standard e i flussi di errore, per poi inviarli alla posta (se la variabile d'ambiente MAILTO è specificata nella tabella delle attività) e, infine, attende il main processo del compito da completare.

Il processo del compito è formato da un altro 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;
}

Questo è fondamentalmente tutto ciò che è cron. Ho omesso alcuni dettagli interessanti, ad esempio la contabilità degli utenti remoti, ma ho delineato la cosa principale.

postfazione

Cron è un programma sorprendentemente semplice e utile, realizzato secondo le migliori tradizioni del mondo Unix. Non fa nulla in più, ma ormai da diversi decenni svolge meravigliosamente il suo lavoro. Completare il codice per la versione fornita con Ubuntu non ha richiesto più di un'ora e mi sono divertito moltissimo! Spero di essere riuscito a condividerlo con voi.

Non so voi, ma mi dispiace un po’ rendermi conto che la programmazione moderna, con la sua tendenza a essere eccessivamente complicata e eccessivamente astratta, non è stata favorevole a tale semplicità per molto tempo.

Esistono molte alternative moderne a cron: systemd-timer consente di organizzare sistemi complessi con dipendenze, fcron consente di regolare in modo più flessibile il consumo di risorse in base alle attività. Ma personalmente, il crontab più semplice mi è sempre bastato.

Insomma, ama Unix, usa programmi semplici e non dimenticare di leggere il mana per la tua piattaforma!

Fonte: habr.com

Aggiungi un commento