
Un classico scriveva che le ore felici non vengono osservate. In quei tempi lontani non c'erano programmatori, né Unix, ma ai giorni nostri i programmatori sanno con certezza: al posto loro, cron si occuperà del tempo.
Gli strumenti da riga di comando sono per me sia una debolezza che una routine. sed, awk, wc, cut e altri programmi storici vengono eseguiti con script sui nostri server quotidianamente. Molti di essi sono organizzati come attività per cron, un pianificatore nato negli anni '70.
A lungo ho usato cron superficialmente, senza approfondire i dettagli, ma un giorno, incontrando un errore nell'esecuzione di uno script, ho deciso di approfondire. Così è nato questo articolo, durante il quale mi sono aggiornato sul POSIX crontab, le principali varianti di cron nelle distribuzioni Linux più popolari e la struttura di alcune di esse.
Usi Linux e esegui compiti in cron? Sei interessato all'architettura delle applicazioni di sistema in Unix? Allora siamo sulla stessa lunghezza d'onda!
Contenuto
Origine delle specie
L'esecuzione periodica di programmi utente o di sistema è un'esigenza evidente in tutti i sistemi operativi. Pertanto, la necessità di servizi che permettano di pianificare ed eseguire centralmente le attività è stata riconosciuta dai programmatori da molto tempo.
I sistemi operativi Unix-like traggono origine dal Version 7 Unix, sviluppato negli anni '70 presso Bell Labs, incluso il famoso Ken Thompson. Insieme al Version 7 Unix, veniva fornito anche cron, il servizio per l'esecuzione regolare delle attività dell'utente root.
Un cron moderno tipico è un programma semplice, ma l'algoritmo di funzionamento della versione originale era ancora più semplice: il servizio si risvegliava una volta al minuto, leggeva la tabella delle attività da un unico file (/etc/lib/crontab) e eseguiva per l'utente root le attività da eseguire nel minuto attuale.
Successivamente, le varianti migliorate di questo semplice e utile servizio sono state incluse in tutti i sistemi operativi Unix-like.
Le descrizioni generali del formato crontab e dei principi base di funzionamento dell'utilità sono state incluse nel principale standard dei sistemi operativi Unix-like — POSIX — nel 1992, rendendo cron da standard de facto a standard de jure.
Nel 1987 Paul Vixie, intervistando gli utenti di Unix riguardo ai desideri per cron, rilasciò un'altra versione del demone, che correggeva alcuni problemi dei cron tradizionali e ampliava la sintassi dei file di tabella.
Con la terza versione, Vixie cron soddisfaceva i requisiti POSIX; inoltre, il programma aveva una licenza liberale, o meglio non esisteva alcuna licenza, a parte i desideri 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 rivelati compatibili con i principi del software libero, che in quegli anni stava guadagnando popolarità, per cui alcune distribuzioni Linux chiave emerse all'inizio degli anni '90 presero Vixie cron come sistema e continuano a svilupparlo 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 una panoramica dello strumento utente crontab descritto in POSIX, dopo di che analizzeremo le estensioni di sintassi presenti in Vixie cron e l'uso delle varianti di Vixie cron nelle distribuzioni Linux più popolari. E, infine, la ciliegina sulla torta: l'analisi della struttura del demone cron.
POSIX crontab
Se l'originale cron funzionava sempre per il superutente, oggi i pianificatori moderni si occupano spesso di attività degli utenti normali, il che è più sicuro e conveniente.
I cron vengono forniti in un pacchetto di due programmi: un demone cron in esecuzione continua e uno strumento crontab accessibile agli utenti. Quest'ultimo consente di modificare le tabelle di compiti specifiche per ogni utente nel sistema, mentre il demone esegue i compiti dalle tabelle degli utenti e da quella di sistema.
In non descrive in alcun modo il comportamento del demone e formalizza solo il programma utente . L'esistenza di meccanismi per l'esecuzione delle attività utente è ovviamente presupposta, ma non è descritta in dettaglio.
Con l'invocazione dello strumento crontab è possibile fare quattro cose: modificare la tabella dei compiti utente nell'editor, caricare la tabella da un file, mostrare la tabella dei compiti attuale e cancellare la tabella dei compiti. Esempi di utilizzo dello strumento crontab:
crontab -e # modificare la tabella dei compiti
crontab -l # mostrare la tabella dei compiti
crontab -r # cancellare la tabella dei compiti
crontab path/to/file.crontab # caricare la tabella dei compiti da un filela richiesta con informazioni sui protocolli, vedremo quanto segue: crontab -e verrà utilizzato l'editor specificato nella variabile d'ambiente standard EDITOR.
Le stesse attività sono descritte nel formato seguente:
# строки-комментарии игнорируются
#
# задача, выполняемая ежеминутно
* * * * * /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.logI 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 rappresenta la domenica. L'ultimo, sesto campo, è una stringa che verrà eseguita dall'interprete comandi standard.
Nei primi cinque campi, i valori possono essere separati da virgole:
# задача, выполняемая в первую и десятую минуты каждого часа
1,10 * * * * /path/to/exec -a -b -cOppure tramite trattino:
# задача, выполняемая в каждую из первых десяти минут каждого часа
0-9 * * * * /path/to/exec -a -b -cL'accesso degli utenti alla pianificazione delle attività è regolato dai file POSIX cron.allow e cron.deny, che elencano, rispettivamente, gli utenti con accesso a crontab e gli utenti senza accesso al programma. La posizione di questi file non è regolamentata da alcuno standard.
I programmi in esecuzione, secondo lo standard, devono ricevere almeno quattro variabili d'ambiente:
- HOME — la home directory dell'utente.
- LOGNAME — il login dell'utente.
- PATH — il percorso dove si possono trovare le utility standard del sistema.
- SHELL — il percorso dell'interprete comandi utilizzato.
È interessante notare che POSIX non specifica da dove provengano i valori per queste variabili.
Best seller — Vixie cron 3.0pl1
L'antenato comune delle varianti cron più popolari è Vixie cron 3.0pl1, presentato nella mailing list comp.sources.unix nel 1992. Le principali funzionalità di questa versione verranno esaminate più nel dettaglio.
Vixie cron viene fornito 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à degli utenti, mentre l'utilità crontab è responsabile della modifica delle tabelle degli utenti.
Tabella delle attività e file di configurazione
La tabella delle attività dell'utente root si trova in /etc/crontab. La sintassi della tabella di sistema corrisponde alla sintassi di Vixie cron con l'unica modifica che nella sesta colonna viene indicato il nome dell'utente sotto cui viene eseguita l'attività:
# Запускается ежеминутно от пользователя vlad
* * * * * vlad /path/to/execLe tabelle delle attività degli utenti normali si trovano in /var/cron/tabs/username e utilizzano la sintassi comune. Durante l'esecuzione dell'utilità crontab a nome dell'utente, si modificano proprio questi file.
La gestione delle liste degli utenti con accesso a crontab avviene nei file /var/cron/allow e /var/cron/deny, in cui è sufficiente inserire il nome dell'utente in una riga separata.
Sintassi avanzata
Rispetto a POSIX crontab, la soluzione di Paul Vixie contiene diverse modifiche molto utili nella sintassi delle tabelle delle attività dell'utilità.
È disponibile una nuova sintassi per le tabelle: ad esempio, è possibile specificare i giorni della settimana o i mesi per nome (Lun, Mar e così via):
# Запускается ежеминутно по понедельникам и вторникам в январе
* * * Jan Mon,Tue /path/to/execÈ possibile indicare il passo attraverso il quale vengono avviati i compiti:
# Запускается с шагом в две минуты
*/2 * * * Mon,Tue /path/to/execPassi e intervalli possono essere combinati:
# Запускается с шагом в две минуты в первых десять минут каждого часа
0-10/2 * * * * /path/to/execSono supportate alternative intuitive alla sintassi standard (reboot, yearly, annually, monthly, weekly, daily, midnight, hourly):
# Запускается после перезагрузки системы
@reboot /exec/on/reboot
# Запускается раз в день
@daily /exec/daily
# Запускается раз в час
@hourly /exec/dailyAmbiente di esecuzione dei compiti
Vixie cron consente di modificare l'ambiente delle applicazioni in esecuzione.
Le variabili ambientali USER, LOGNAME e HOME non vengono semplicemente fornite dal demone, ma sono estratte da un file . La variabile PATH assume il valore "/usr/bin:/bin", mentre SHELL diventa "/bin/sh". I valori di tutte le variabili, tranne LOGNAME, possono essere modificati nelle tabelle degli utenti.
Alcune variabili environnementali (soprattutto SHELL e HOME) vengono usate dallo stesso cron per avviare i compiti. Ecco come può apparire l'uso di bash invece del normale sh per avviare i compiti degli utenti:
SHELL=/bin/bash
HOME=/tmp/
# l'esecuzione avverrà con bash in /tmp/
* * * * * /path/to/execAlla fine, tutte le variabili ambientali definite nella tabella (utilizzate da cron o necessarie al processo) saranno passate al compito avviato.
Per modificare i file, l'utilità crontab utilizza l'editor specificato nella variabile ambientale VISUAL o EDITOR. Se nell'ambiente in cui è stato avviato crontab queste variabili non sono definite, viene utilizzato "/usr/ucb/vi" (ucb sta probabilmente per University of California, Berkeley).
cron in Debian e Ubuntu
Gli sviluppatori di Debian e delle distribuzioni derivate hanno rilasciato della versione Vixie cron 3.0pl1. Non ci sono differenze nella sintassi dei file-tabelle, per gli utenti è lo stesso Vixie cron. Le principali nuove funzionalità: supporto , e .
Tra le modifiche meno visibili, ma tangibili, ci sono la posizione dei file di configurazione e delle tabelle dei compiti.
Le tabelle utente in Debian si trovano nella directory /var/spool/cron/crontabs, mentre la tabella di sistema rimane in /etc/crontab. Le tabelle dei compiti specifiche per i pacchetti Debian vengono collocate in /etc/cron.d, da cui il demone cron le legge automaticamente. La gestione dell'accesso degli utenti è regolata dai file /etc/cron.allow e /etc/cron.deny.
Come shell predefinita viene ancora utilizzato /bin/sh, che in Debian è rappresentato da una piccola shell compatibile POSIX , eseguita senza leggere alcuna configurazione (in modalità non interattiva).
Il cron nelle ultime versioni di Debian viene eseguito 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ù dettagliata dei task può essere effettuata tramite variabili d'ambiente definite direttamente nel crontab di ciascun utente.
cronie in RedHat, Fedora e CentOS
— un fork di Vixie cron versione 4.1. Come in Debian, la sintassi non è cambiata, ma è stata aggiunta la supporto per PAM e SELinux, il funzionamento in cluster, il monitoraggio dei file tramite inotify e altre funzionalità.
La configurazione predefinita si trova nei luoghi consueti: la tabella di sistema è in /etc/crontab, i pacchetti posizionano le loro tabelle in /etc/cron.d, le tabelle degli utenti finiscono in /var/spool/cron/crontabs.
Il demone viene eseguito sotto il controllo di systemd, la configurazione del servizio è in /lib/systemd/system/crond.service.
Nei sistemi operativi simili a Red Hat, per impostazione predefinita si utilizza /bin/sh, il quale è il normale bash. È importante notare che quando si eseguono i compiti cron tramite /bin/sh, la shell bash viene attivata in modalità compatibile con POSIX e non legge alcuna configurazione aggiuntiva, funzionando in modalità non interattiva.
cronie in SLES e openSUSE
La distribuzione tedesca SLES e il suo derivato openSUSE usano lo stesso cronie. Qui il demone è anche eseguito 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. Come /bin/sh viene utilizzato lo stesso bash, avviato in modalità non interattiva compatibile con POSIX.
Struttura di Vixie cron
I moderni discendenti di cron rispetto a Vixie cron non sono cambiati radicalmente, ma hanno acquisito nuove funzionalità che non sono necessarie per comprendere i principi di funzionamento del programma. Molti di questi miglioramenti sono maldocumentati e confondono il codice. Il codice sorgente originale di cron nella realizzazione di Paul Vixie è un piacere da leggere.
Pertanto, ho deciso di analizzare il funzionamento del cron usando come esempio la versione comune a entrambi i rami di sviluppo del programma cron — Vixie cron 3.0pl1. Semplificherò gli esempi, rimuovendo le complicazioni della lettura come gli ifdef e tralasciando i dettagli secondari.
Il funzionamento del demone può essere suddiviso in diverse fasi:
- Inizializzazione del programma.
- Raccolta e aggiornamento dell'elenco dei compiti da eseguire.
- Funzionamento del ciclo principale di cron.
- Esecuzione di un compito.
Analizziamoli in ordine.
Inizializzazione
All'avvio, dopo il controllo degli argomenti, il processo cron imposta i gestori dei segnali SIGCHLD e SIGHUP. Il primo registra nel log la chiusura del processo figlio, il secondo chiude il descrittore del file di log:
signal(SIGCHLD, sigchld_handler);
signal(SIGHUP, sighup_handler);Il demone cron nel sistema funziona sempre in solitudine, esclusivamente con i privilegi di superutente e dalla directory principale di cron. Le seguenti chiamate creano un file di lock con il PID del processo demone, verificano che l'utente sia corretto e cambiano la directory corrente nella principale:
acquire_daemonlock(0);
set_cron_uid();
set_cron_cwd();Viene impostato il percorso predefinito che sarà utilizzato all'avvio dei processi:
setenv("PATH", _PATH_DEFPATH, 1);Successivamente, il processo viene "demoniizzato": crea una copia figlia del processo attraverso la chiamata fork e una nuova sessione nel processo figlio (chiamata setsid). Non c'è più necessità nel processo genitore — e questo termina:
switch (fork()) {
case -1:
/* errore critico e terminazione *\
exit(0);
break;
case 0:
/* processo figlio *\
(void) setsid();
break;
default:
/* processo genitore termina *\
_exit(0);
}
La terminazione del processo genitore libera il lock sul file di lock. Inoltre, è necessario aggiornare il PID nel file a quello del processo figlio. Dopo di che, viene popolato il database dei compiti:
/* повторный захват лока */
acquire_daemonlock(0);
/* Заполнение БД */
database.head = NULL;
database.tail = NULL;
database.mtime = (time_t) 0;
load_database(&database);Successivamente, cron passa al ciclo principale di lavoro. Ma prima è opportuno dare un'occhiata al caricamento dell'elenco dei compiti.
Raccolta e aggiornamento dell'elenco dei compiti
La funzione load_database è responsabile del caricamento dell'elenco dei compiti. Essa controlla il crontab principale del sistema e la directory con i file degli utenti. Se i file e la directory non sono cambiati, l'elenco dei compiti non viene riletto. Altrimenti, inizia a formarsi un nuovo elenco di compiti.
Caricamento del file di sistema con nomi di file speciali e tabella:
/* если файл системной таблицы изменился, перечитываем */
if (syscron_stat.st_mtime) {
process_crontab("root", "*system*",
SYSCRONTAB, &syscron_stat,
&new_db, old_db);
}Caricamento delle tabelle degli utenti in un ciclo:
while (NULL != (dp = readdir(dir))) {
char fname[MAXNAMLEN+1],
tabname[MAXNAMLEN+1];
/* non è necessario leggere i 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 precedenti, 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), dopo di che chiama load_user. Quest'ultima legge il file stesso riga per riga:
mentre ((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 la variabile d'ambiente (stringhe del tipo VAR=value) dalle funzioni load_env / env_set, oppure viene letta la descrizione del compito (***** /path/to/exec) dalla funzione load_entry.
L'entità entry restituita da load_entry è il nostro compito, inserito nella lista generale dei compiti. Nella funzione viene effettuata un'analisi dettagliata del formato temporale, a noi interessa maggiormente la formazione 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);Con l'elenco attuale dei compiti lavora il ciclo principale.
Ciclo principale
Il cron originale della Version 7 Unix funzionava in modo molto semplice: in un ciclo rilevava nuovamente la configurazione, avviava i compiti del minuto corrente come superutente e poi si metteva a dormire fino all'inizio del minuto successivo. Questo approccio semplice richiedeva troppe risorse su macchine vecchie.
In SysV è stata proposta una versione alternativa in cui il demone si addormentava fino al minuto più vicino per il quale era definito un compito, oppure per 30 minuti. In questo modo erano necessarie meno risorse per rileggere la configurazione e controllare i compiti, ma aggiornare rapidamente l'elenco dei compiti divenne scomodo.
Vixie cron è tornato a controllare le liste dei compiti una volta al minuto, fortunatamente alla fine degli anni '80 le risorse sulle 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 effettiva dei compiti è gestita dalla funzione cron_sleep, che chiama le funzioni job_runqueue (scorrimento e avvio dei compiti) e do_command (avvio di ogni singolo compito). Quest'ultima funzione merita di essere analizzata più dettagliatamente.
Avvio del compito
La funzione do_command è realizzata in puro stile Unix, il che significa che per l'esecuzione asincrona del compito effettua un fork. Il processo padre continua ad avviare i compiti, quello figlio si occupa della preparazione del processo del compito:
switch (fork()) {
case -1:
/* non siamo riusciti a eseguire il fork */
break;
case 0:
/* processo figlio: per sicurezza proviamo ancora a ottenere il lock principale */
acquire_daemonlock(1);
/* passiamo alla preparazione del processo del compito */
child_process(e, u);
/* al termine, il processo figlio termina il lavoro */
_exit(OK_EXIT);
break;
default:
/* il processo padre continua a lavorare */
break;
}In child_process c'è molta logica: accetta gli stream di output e errori per poi inoltrarli via email (se nella tabella dei compiti è specificata la variabile d'ambiente MAILTO), e infine attende il completamento del processo principale del compito.
Il processo del compito si crea con un ulteriore fork:
switch (vfork()) {
case -1:
/* in caso di errore il lavoro termina immediatamente */
exit(ERROR_EXIT);
case 0:
/* il processo figlio crea una nuova sessione, terminale, ecc.
*/
(void) setsid();
/*
* di seguito c'è una configurazione dettagliata dell'output del processo, la tralasciamo per brevità
*/
/* cambiamento di directory, utente e gruppo,
* il che significa che il processo non è più eseguito come superutente
*/
setgid(e->gid);
setuid(e->uid);
chdir(env_get("HOME", e->envp));
/* esecuzione del comando stesso
*/
{
/* la variabile d'ambiente SHELL indica l'interprete da eseguire */
char *shell = env_get("SHELL", e->envp);
/* il processo è avviato senza passare l'ambiente del processo padre,
* quindi proprio come descritto nella tabella dei compiti 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 funzionare: attende il completamento del lavoro e dell'output */
break;
}Ecco, in poche parole, tutto il cron. Alcuni dettagli interessanti, come la gestione degli utenti remoti, li ho omessi, ma ho esposto l'essenziale.
Postfazione
Cron è sorprendentemente semplice e utile, realizzato secondo le migliori tradizioni del mondo Unix. Non fa nulla di superfluo, ma svolge il suo compito in modo eccellente da già diversi decenni. La familiarizzazione con il codice della versione fornita con Ubuntu ha richiesto non più di un'ora, e mi ha dato un grande piacere! Spero di essere riuscito a condividerlo con voi.
Non so voi, ma a me fa un po' triste constatare che la programmazione moderna, con la sua tendenza alla sovrastruttura e alla sovraastrazione, non offre già da tempo tale semplicità.
Esistono molte moderne alternative a cron: i systemd-timers permettono di organizzare sistemi complessi con dipendenze, in fcron è possibile regolare più facilmente il consumo di risorse per i compiti. Ma personalmente mi sono sempre bastati i semplici crontab.
In sintesi, amate Unix, usate programmi semplici e non dimenticate di leggere le man per la vostra piattaforma!
Fonte: habr.com
