Cron in Linux: storia, utilizzo e struttura

Cron in Linux: storia, utilizzo e struttura

Un classico diceva che le ore felici non si osservano. In quei tempi selvaggi non c'erano né programmatori né Unix, ma oggigiorno i programmatori sanno bene: al posto loro, a tenere d'occhio il tempo ci penserà cron.

Le utilità da riga di comando sono per me sia una debolezza che una routine. sed, awk, wc, cut e altri vecchi programmi vengono eseguiti da script sui nostri server quotidianamente. Molti di essi sono organizzati come attività per cron, un pianificatore originario degli anni '70.

Per molto tempo ho usato cron superficialmente, senza approfondire i dettagli, ma un giorno, affrontando un errore durante l'esecuzione di uno script, ho deciso di occuparmene a fondo. Così è nato questo articolo, durante la stesura del quale ho esaminato il POSIX crontab, le principali varianti di cron nelle distribuzioni popolari di Linux e il funzionamento di alcune di esse.

Usi Linux ed esegui attività in cron? Sei interessato all'architettura delle applicazioni di sistema in Unix? Allora siamo sulla stessa lunghezza d'onda!

Contenuto

L'origine delle specie

L'esecuzione periodica di programmi utente o di sistema è una necessità evidente in tutti i sistemi operativi. Pertanto, la richiesta di servizi che consentano la pianificazione e l'esecuzione centralizzate delle attività è stata riconosciuta dai programmatori da tempo.

I sistemi operativi simili a Unix risalgono a Version 7 Unix, sviluppato negli anni '70 presso i Bell Labs, incluso il famoso Ken Thompson. Insieme a Version 7 Unix, venne fornito anche cron, un servizio per l'esecuzione regolare delle attività dell'utente root.

Il cron moderno è un programma semplice, ma l'algoritmo originale era ancora più semplice: il servizio si attivava ogni minuto, leggeva un elenco di attività da un unico file (/etc/lib/crontab) ed eseguiva le attività dell'utente root che dovevano essere completate in quel minuto.

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

Le descrizioni generali del formato crontab e dei principi di base dell'utilizzo risalgono al 1992, quando furono incluse nello standard principale dei sistemi operativi simili a Unix — POSIX — trasformando così cron dallo standard di fatto a uno standard di diritto.

Nel 1987, Paul Vixie, dopo aver consultato gli utenti di Unix riguardo le loro richieste su cron, pubblicò un'altra versione del demone, che risolveva alcuni problemi delle tradizionali implementazioni di cron e ampliava la sintassi dei file-tabella.

Con la terza versione, Vixie cron soddisfaceva i requisiti di POSIX. Inoltre, il programma aveva una licenza liberale, non esistendo praticamente alcuna licenza, eccetto le richieste nel README: l'autore non offre garanzie, il nome dell'autore non può essere rimosso, e il programma può essere venduto solo insieme al codice sorgente. Questi requisiti si sono dimostrati compatibili con i principi del software libero, che stava guadagnando popolarità in quegli anni, e quindi alcune delle principali distribuzioni di Linux emerse all'inizio degli anni '90 hanno adottato 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 numerosi patch.

Iniziamo con la utility crontab per utenti descritta in POSIX, per poi discutere le estensioni di sintassi presentate in Vixie cron e l'uso delle varianti di Vixie cron nelle distribuzioni Linux più popolari. E, infine, la ciliegina sulla torta: un'analisi del demone cron.

POSIX crontab

Mentre il cron originale funzionava sempre per l'utente root, i pianificatori moderni si occupano più spesso dei compiti degli utenti normali, il che è più sicuro e comodo.

I cron vengono forniti in un pacchetto composto da due programmi: il demone cron sempre in esecuzione e l'utilità crontab accessibile agli utenti. Quest'ultima consente di modificare le tabelle dei compiti specifiche per ciascun utente nel sistema, mentre il demone esegue compiti dalle tabelle degli utenti e di sistema.

In standard POSIX non descrive affatto il comportamento del demone e formalizza solo il programma utente crontab. L'esistenza di meccanismi per l'esecuzione di compiti utente è certamente sottintesa, ma non è descritta in dettaglio.

Con il comando crontab è possibile eseguire quattro operazioni: modificare la tabella delle attività dell'utente in un editor, caricare la tabella da un file, visualizzare la tabella delle attività corrente e cancellare la tabella delle attività. Esempi di utilizzo del comando crontab:

crontab -e # modifica la tabella delle attività
crontab -l # visualizza la tabella delle attività
crontab -r # elimina la tabella delle attività
crontab path/to/file.crontab # carica la tabella delle attività da un file

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, sesto campo è una stringa che verrà eseguita dal normale interprete dei comandi.

Nei primi cinque campi i valori possono essere elencati tramite virgola:

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

O tramite trattino:

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

L'accesso degli utenti alla pianificazione delle attività è regolato nei file POSIX cron.allow e cron.deny, che elencano rispettivamente gli utenti che hanno accesso a crontab e gli utenti privi di accesso al programma. La posizione di questi file non è standardizzata.

I programmi avviati devono ricevere almeno quattro variabili d'ambiente secondo lo standard:

  1. HOME — la directory principale dell'utente.
  2. LOGNAME — il login dell'utente.
  3. PATH — il percorso per trovare le utility standard del sistema.
  4. SHELL — il percorso del programma di interprete dei comandi utilizzato.

È interessante notare che POSIX non specifica da dove provengano i valori per queste variabili.

Il best seller — Vixie cron 3.0pl1

L'antenato comune delle varie versioni popolari di cron è Vixie cron 3.0pl1, introdotto nel 1992 nel newsgroup comp.sources.unix. Analizzeremo più a fondo le caratteristiche principali di questa versione.

Vixie cron è fornito in due programmi (cron e crontab). Come di consueto, il demone si occupa di leggere ed eseguire i compiti dalla tabella di sistema e dalle tabelle di compiti degli utenti, mentre l'utility crontab è responsabile della modifica delle tabelle degli utenti.

Tabella dei compiti e file di configurazione

La tabella dei compiti dell'utente root si trova in /etc/crontab. La sintassi della tabella di sistema segue la sintassi di Vixie cron, con l'aggiunta che nella sesta colonna è indicato il nome dell'utente sotto il quale viene eseguito il compito:

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

Le tabelle dei compiti degli utenti normali si trovano in /var/cron/tabs/username e utilizzano una sintassi comune. Quando si esegue l'utility crontab sotto il nome dell'utente, vengono modificati proprio questi file.

La gestione delle liste degli utenti che hanno accesso a crontab avviene nei file /var/cron/allow e /var/cron/deny, basta inserire il nome utente in una riga separata.

Sintassi avanzata

Rispetto al POSIX crontab, la soluzione di Paul Vixie contiene diverse modifiche molto utili nella sintassi delle tabelle dei compiti dell'utility.

È disponibile una nuova sintassi per le tabelle: ad esempio, è possibile indicare i giorni della settimana o i mesi per nome (Lun, Mar e così via):

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

È possibile specificare un passo, che determina con quale frequenza vengono eseguiti i compiti:

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

I passi e gli intervalli possono essere mescolati:

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

Sono supportate alternative intuitive alla sintassi normale (reboot, yearly, annually, monthly, weekly, daily, midnight, hourly):

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

Ambiente di esecuzione dei compiti

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

Le variabili d'ambiente USER, LOGNAME e HOME non vengono solo fornite dal demone, ma vengono prese dal file. passwd. La variabile PATH assume il valore «/usr/bin:/bin», mentre SHELL è «/bin/sh». I valori di tutte le variabili, tranne LOGNAME, possono essere modificati nelle tabelle degli utenti.

Alcune variabili d'ambiente (soprattutto SHELL e HOME) vengono utilizzate direttamente da cron per eseguire le attività. Ecco come si può utilizzare bash al posto del consueto sh per eseguire compiti degli utenti:

SHELL=/bin/bash
HOME=/tmp/
# exec verrà eseguito da bash in /tmp/
* * * * * /path/to/exec

Alla fine, tutte le variabili d'ambiente definite nelle tabelle (utilizzate da cron o necessarie al processo) verranno passate all'attività avviata.

Per modificare i file con l'utilità crontab si utilizza l'editor specificato nella variabile d'ambiente VISUAL o EDITOR. Se queste variabili non sono definite nell'ambiente in cui è stato avviato crontab, viene utilizzato «/usr/ucb/vi» (ucb sta probabilmente per University of California, Berkeley).

cron in Debian e Ubuntu

Gli sviluppatori di Debian e dei relativi sistemi operativi hanno rilasciato una versione fortemente modificata della versione Vixie cron 3.0pl1. Non ci sono differenze nella sintassi dei file-tabella, per gli utenti si tratta dello stesso Vixie cron. Le più grandi nuove funzionalità includono il supporto syslog, SELinux e PAM.

Tra i cambiamenti meno evidenti ma tangibili ci sono la posizione dei file di configurazione e delle tabelle delle attività.

Le tabelle delle attività personalizzate in Debian si trovano nella directory /var/spool/cron/crontabs, mentre la tabella di sistema si trova in /etc/crontab. Le tabelle delle attività specifiche per i pacchetti Debian vengono collocate in /etc/cron.d, da dove il demone cron le legge automaticamente. La gestione degli accessi degli utenti è regolata dai file /etc/cron.allow e /etc/cron.deny.

Come shell predefinita continua a essere utilizzato /bin/sh, che in Debian è rappresentato da una piccola shell compatibile con POSIX dash, avviata senza leggere alcuna configurazione (in modalità non interattiva).

Il cron stesso 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 particolare nella configurazione del servizio; qualsiasi gestione più fine delle attività può essere effettuata tramite variabili d'ambiente dichiarate direttamente nel crontab di ciascun utente.

cronie in RedHat, Fedora e CentOS

cronie — fork della versione 4.1 di Vixie cron. Come in Debian, la sintassi non è cambiata, ma è stata aggiunta la supporto per PAM e SELinux, operazioni in cluster, monitoraggio dei file tramite inotify e altre funzionalità.

La configurazione predefinita si trova nei percorsi abituali: la tabella di sistema in /etc/crontab, i pacchetti posizionano le loro tabelle in /etc/cron.d, le tabelle degli utenti sono in /var/spool/cron/crontabs.

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

Nei sistemi operativi basati su Red Hat viene utilizzato per impostazione predefinita /bin/sh, che è standardmente bash. È importante notare che quando i compiti cron vengono eseguiti tramite /bin/sh, la shell bash viene eseguita in modalità compatibile con POSIX e non legge alcuna configurazione aggiuntiva, operando in modalità non interattiva.

cronie in SLES e openSUSE

La distribuzione tedesca SLES e il suo derivato openSUSE utilizzano lo stesso cronie. Anche qui il demone viene avviato sotto systemd, la configurazione del servizio è in /usr/lib/systemd/system/cron.service. Configurazione: /etc/crontab, /etc/cron.d, /var/spool/cron/tabs. Come /bin/sh viene utilizzato lo stesso bash, avviato in modalità non interattiva compatibile con POSIX.

Funzionamento di Vixie cron

I moderni discendenti di cron, rispetto a Vixie cron, non hanno subito cambiamenti radicali, ma hanno acquisito nuove funzionalità che non sono necessarie per comprendere i principi di funzionamento del programma. Molte di queste estensioni sono scritte in modo disordinato e confondono il codice. Leggere il codice sorgente originale di cron, realizzato da Paul Vixie, è un vero piacere.

Pertanto, ho deciso di analizzare il funzionamento di cron con l'esempio della versione comune a entrambe le diramazioni dello sviluppo del programma — Vixie cron 3.0pl1. Gli esempi saranno semplificati, rimuovendo gli complicatori ifdef e tralasciando i dettagli secondari.

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

  1. Inizializzazione del programma.
  2. Raccolta e aggiornamento dell'elenco delle attività da eseguire.
  3. Funzionamento del ciclo principale di cron.
  4. Avvio del compito.

Analizziamo questi passaggi in ordine.

Inizializzazione

All'avvio, dopo aver verificato gli argomenti, cron imposta i gestori dei segnali SIGCHLD e SIGHUP. Il primo registra nel log la conclusione del processo figlio, il secondo chiude il file descriptor del file di log:

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

Il demone cron nel sistema funziona sempre come un'unica istanza, esclusivamente con privilegi di superutente e dalla directory principale di cron. Le seguenti chiamate creano un file di lock con il PID del processo demone, garantiscono che l'utente sia corretto e cambiano la directory corrente in quella principale:

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

Viene impostato un percorso predefinito che sarà utilizzato durante l'avvio dei processi:

setenv("PATH", _PATH_DEFPATH, 1);

Successivamente, il processo viene "demoniato": crea una copia figlia del processo tramite la chiamata fork e una nuova sessione nel processo figlio (chiamata setsid). Non c'è più bisogno del processo padre, quindi termina:

switch (fork()) {
case -1:
    /* errore critico e terminazione */
    exit(0);
break;
case 0:
    /* processo figlio */
    (void) setsid();
break;
default:
    /* il processo padre termina */
    _exit(0);
}

La terminazione del processo padre libera il lock sul file di lock. Inoltre, è necessario aggiornare il PID nel file a quello del processo figlio. Dopo ciò, viene popolato il database delle attività:

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

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

Successivamente, cron entra nel ciclo di lavoro principale. Ma prima di questo, è opportuno dare un'occhiata al caricamento dell'elenco delle attività.

Raccolta e aggiornamento dell'elenco delle attività

La funzione load_database è responsabile del caricamento della lista dei compiti. Essa controlla il crontab di sistema principale e la directory dei file utente. Se i file e la directory non sono stati modificati, la lista dei compiti non viene ricaricata. Altrimenti, inizia a formare una nuova lista di compiti.

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

/* если файл системной таблицы изменился, перечитываем */
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];
    /* Non è necessario leggere file che iniziano con un punto */
    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);
}

Dopo di che, il vecchio database viene sostituito dal nuovo.

Nei casi sopra, la chiamata alla funzione process_crontab verifica l'esistenza dell'utente corrispondente al nome del file della tabella (a meno che non si tratti di un superutente), dopodiché chiama load_user. Quest'ultima legge il file 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 viene impostata una variabile d'ambiente (stringhe del tipo VAR=value) usando le funzioni load_env / env_set, oppure viene letta la descrizione del compito (* * * * * /path/to/exec) attraverso la funzione load_entry.

L'entità entry restituita da load_entry è il nostro compito, che viene inserito in un elenco generale di task. Nella funzione avviene un'analisi dettagliata del formato del tempo, a noi interessa maggiormente la generazione delle variabili d'ambiente e dei parametri di avvio del compito:

/* пользователь и группа для запуска задачи берутся из 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 opera con l'elenco attuale dei compiti.

Ciclo principale

Il cron originale della Versione 7 di Unix funzionava in modo molto semplice: rilettura della configurazione in un ciclo, esecuzione dei compiti per il minuto corrente come superutente e attesa fino all'inizio del minuto successivo. Questo approccio semplice richiedeva troppe risorse su macchine più vecchie.

In SysV è stata proposta una versione alternativa, in cui il demone si metteva in sonno fino al prossimo minuto definito per un compito oppure per 30 minuti. In questo modo, si consumavano meno risorse per rileggere la configurazione e controllare i compiti, ma aggiornare rapidamente l'elenco dei compiti diventava scomodo.

Vixie cron è tornato a controllare le liste di lavoro ogni minuto, poiché alla fine degli anni '80 le risorse delle macchine Unix standard erano notevolmente aumentate.

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

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

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

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

L'esecuzione diretta dei compiti è gestita dalla funzione cron_sleep, che chiama le funzioni job_runqueue (iterazione e avvio dei compiti) e do_command (avvio di ciascun singolo compito). La seconda funzione merita un approfondimento.

Avvio del compito

La funzione do_command è scritta in puro stile Unix, cioè per eseguire un compito in modo asincrono crea un fork. Il processo padre continua a eseguire i compiti, mentre il figlio si occupa della preparazione del processo del compito:

switch (fork()) {
case -1:
    /* impossibile eseguire fork */
    break;
case 0:
    /* processo figlio: tentiamo nuovamente di acquisire il lock principale */
    acquire_daemonlock(1);
    /* passiamo alla formazione del processo del compito */
    child_process(e, u);
    /* al termine, il processo figlio termina */
    _exit(OK_EXIT);
    break;
default:
    /* il processo padre continua a lavorare */
    break;
}

Nel child_process c'è molta logica: gestisce i flussi standard di output e di errore, per poi inoltrarli via email (se nella tabella delle attività è specificata la variabile d'ambiente MAILTO) e infine attende il completamento del processo principale dell'attività.

Il processo dell'attività viene creato da un ulteriore fork:

switch (vfork()) {
case -1:
    /* in caso di errore, il lavoro si interrompe immediatamente */
    exit(ERROR_EXIT);
case 0:
    /* il processo figlio crea una nuova sessione, terminale ecc.
     */
    (void) setsid();

    /*
     * seguito da una configurazione verbosa dell'output del processo, tralasciamo per brevità
     */

    /* cambio della directory, dell'utente e del gruppo utente,
     * ovvero il processo non è più un superutente
     */
    setgid(e->gid);
    setuid(e->uid);
    chdir(env_get("HOME", e->envp));

    /* avvio del comando effettivo
     */
    {
        /* la variabile d'ambiente SHELL indica l'interprete da eseguire */
        char    *shell = env_get("SHELL", e->envp);

        /* il processo viene avviato senza passare l'ambiente del processo padre,
         * cioè proprio come descritto nella tabella delle attività dell'utente  */
        execle(shell, shell, "-c", e->cmd, (char *)0, e->envp);

        /* errore — e il processo non si è avviato? terminazione del lavoro */
        perror("execl");
        _exit(ERROR_EXIT);
    }
    break;
default:
    /* il processo continua a lavorare: attende la conclusione e l'output */
    break;
}

Ecco, in sostanza, tutto il cron. Alcuni dettagli interessanti, come il conteggio degli utenti remoti, li ho tralasciati, ma ho esposto le parti principali.

Epifania

Cron — sorprendentemente semplice e utile programma, realizzato secondo le migliori tradizioni del mondo Unix. Non fa nulla di superfluo, ma svolge magnificamente il suo lavoro da diversi decenni. Esaminare il codice della versione fornita con Ubuntu non ha preso più di un'ora e mi ha dato grande soddisfazione! Spero di essere riuscito a condividerlo con voi.

Non so voi, ma a me fa un po' triste pensare che la programmazione moderna, con la sua tendenza a complicare e astrare eccessivamente, non favorisca da tempo una tale semplicità.

Esistono molte alternative moderne a cron: i systemd-timers consentono di organizzare sistemi complessi con dipendenze, in fcron è possibile regolare l'uso delle risorse delle attività in modo più flessibile. Ma personalmente, mi è sempre bastato il semplice crontab.

In breve, amate Unix, usate programmi semplici e non dimenticate di leggere le man per la vostra piattaforma!

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster