Cron sous Linux : histoire, utilisation et fonctionnement

Cron sous Linux : histoire, utilisation et fonctionnement

Le classique a écrit que les heures heureuses ne se regardent pas. À cette époque sauvage, il n'y avait ni programmeurs ni Unix, mais de nos jours, les programmeurs savent avec certitude : au lieu d'eux, cron s'occupera du temps.

Les utilitaires de ligne de commande sont à la fois ma faiblesse et ma routine. sed, awk, wc, cut et d'autres anciens programmes sont lancés par des scripts sur nos serveurs chaque jour. Beaucoup d'entre eux sont configurés en tant que tâches pour cron, un planificateur créé dans les années 70.

J'ai longtemps utilisé cron de manière superficielle, sans entrer dans les détails, mais un jour, confronté à une erreur lors de l'exécution d'un script, j'ai décidé de m'y plonger sérieusement. C'est ainsi qu'est né cet article, lors de la rédaction duquel j'ai consulté le crontab POSIX, les principales variantes de cron dans les distributions Linux populaires et le fonctionnement de certaines d'entre elles.

Vous utilisez Linux et exécutez des tâches dans cron ? Vous êtes intéressé par l'architecture des applications système sous Unix ? Alors faisons route ensemble !

Contenu

Origine des espèces

L'exécution périodique de programmes utilisateurs ou système est une nécessité évidente dans tous les systèmes d'exploitation. C'est pourquoi le besoin de services permettant de planifier et d'exécuter des tâches de manière centralisée a été reconnu par les programmeurs depuis longtemps.

Les systèmes d'exploitation de type Unix descendent de Version 7 Unix, développé dans les années 70 au Bell Labs, notamment par le célèbre Ken Thompson. Avec Version 7 Unix, cron était également inclus, un service pour l'exécution régulière des tâches de l'utilisateur super.

Un cron moderne typique est un programme simple, mais l'algorithme de fonctionnement de la version originale était encore plus simple : le service se réveillait une fois par minute, lisait une table de tâches dans un unique fichier (/etc/lib/crontab) et exécutait pour l'utilisateur super les tâches devant être réalisées à la minute en cours.

Par la suite, des versions améliorées de ce service simple et utile ont été fournies avec tous les systèmes d'exploitation de type Unix.

Les descriptions généralisées du format crontab et des principes de base du fonctionnement de l'utilitaire ont été incluses en 1992 dans la norme principale des systèmes d'exploitation de type Unix — POSIX — et ainsi cron est devenu un standard de jure depuis étant un standard de facto.

En 1987, Paul Vixie a interrogé les utilisateurs Unix sur leurs souhaits concernant cron et a publié une autre version du démon, corrigeant certains problèmes des cron traditionnels et étendant la syntaxe des fichiers de table.

Avec la troisième version, Vixie cron a répondu aux exigences de POSIX, et le programme disposait d'une licence libérale, à savoir qu'il n'y avait en fait aucune licence, sauf des souhaits dans le README : l'auteur ne donne aucune garantie, le nom de l'auteur ne peut pas être supprimé, et le programme ne peut être vendu qu'avec le code source. Ces exigences se sont avérées compatibles avec les principes du logiciel libre, qui commençait à gagner en popularité à cette époque, c'est pourquoi certaines distributions Linux clés qui sont apparues au début des années 90 ont adopté Vixie cron comme système et continuent de le développer.

En particulier, Red Hat et SUSE développent un fork de Vixie cron appelé cronie, tandis que Debian et Ubuntu utilisent l'édition originale de Vixie cron avec de nombreux patchs.

Tout d'abord, familiarisons-nous avec l'outil utilisateur crontab décrit dans POSIX, après quoi nous examinerons les extensions de syntaxe présentées dans Vixie cron, ainsi que l'utilisation des variations de Vixie cron dans les distributions Linux populaires. Enfin, la cerise sur le gâteau — l'étude de l'architecture du démon cron.

crontab POSIX

Alors que l'original cron fonctionnait toujours pour l'utilisateur root, les planificateurs modernes traitent souvent des tâches d'utilisateurs ordinaires, ce qui est plus sécurisé et pratique.

Les cron se composent de deux programmes : un démon cron en fonctionnement continu et l'utilitaire crontab accessible aux utilisateurs. Ce dernier permet d'éditer des tables de tâches spécifiques à chaque utilisateur dans le système, tandis que le démon lance des tâches à partir des tables des utilisateurs et de la table système.

Dans la norme POSIX ne décrit pas le comportement du démon et ne formalise que le programme utilisateur crontab. L'existence de mécanismes pour exécuter des tâches utilisateur est bien sûr sous-entendue, mais n'est pas décrite en détail.

Avec l'appel de l'utilitaire crontab, on peut faire quatre choses : éditer la table de tâches utilisateur dans un éditeur, charger la table depuis un fichier, afficher la table de tâches actuelle et vider la table de tâches. Exemples d'utilisation de l'utilitaire crontab :

crontab -e # éditer la table de tâches
crontab -l # afficher la table de tâches
crontab -r # supprimer la table de tâches
crontab path/to/file.crontab # charger la table de tâches depuis un fichier

Lors de l'appel crontab -e l'éditeur spécifié dans la variable d'environnement standard sera utilisé ÉDITEUR.

Les tâches elles-mêmes sont décrites dans le format suivant :

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

Les cinq premiers champs des enregistrements : minutes [1..60], heures [0..23], jours du mois [1..31], mois [1..12], jours de la semaine [0..6], où 0 est dimanche. Le dernier champ, le sixième, est une chaîne qui sera exécutée par l'interpréteur de commandes standard.

Dans les cinq premiers champs, les valeurs peuvent être énumérées séparément par des virgules :

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

Ou par un tiret :

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

L'accès des utilisateurs à la planification des tâches est régulé dans les fichiers POSIX cron.allow et cron.deny, qui énumèrent respectivement les utilisateurs ayant accès à crontab et ceux n'ayant pas accès au programme. La localisation de ces fichiers n'est pas réglementée par la norme.

Les programmes à exécuter doivent recevoir au moins quatre variables d'environnement, selon la norme :

  1. HOME — le répertoire personnel de l'utilisateur.
  2. LOGNAME — le nom d'utilisateur.
  3. PATH — le chemin où les utilitaires standard du système peuvent être trouvés.
  4. SHELL — le chemin vers l'interpréteur de commandes utilisé.

Il est intéressant de noter que POSIX ne mentionne pas d'où proviennent les valeurs pour ces variables.

Meilleure vente — Vixie cron 3.0pl1

L'ancêtre commun des versions populaires de cron est Vixie cron 3.0pl1, présenté dans la liste de diffusion comp.sources.unix en 1992. Nous examinerons plus en détail les principales fonctionnalités de cette version.

Vixie cron est fourni avec deux programmes (cron et crontab). Comme d'habitude, le démon est responsable de la lecture et de l'exécution des tâches à partir de la table des tâches système et des tables de tâches des utilisateurs, tandis que l'utilitaire crontab gère l'édition des tables utilisateur.

Table des tâches et fichiers de configuration

La table des tâches de l'utilisateur superutilisateur est située dans /etc/crontab. La syntaxe de la table système correspond à la syntaxe de Vixie cron, à l'exception du fait que la sixième colonne indique le nom d'utilisateur sous lequel la tâche s'exécute :

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

Les tables des tâches des utilisateurs ordinaires se trouvent dans /var/cron/tabs/nom_utilisateur et utilisent la syntaxe identique. Lorsqu'on exécute l'utilitaire crontab au nom d'un utilisateur, ce sont ces fichiers qui sont effectivement édités.

La gestion des listes d'utilisateurs ayant accès à crontab se fait dans les fichiers /var/cron/allow et /var/cron/deny, où il suffit d'ajouter le nom d'utilisateur sur une ligne distincte.

Syntaxe avancée

Comparé à POSIX crontab, la solution de Paul Vixie comporte plusieurs modifications très utiles dans la syntaxe des tables de tâches de l'utilitaire.

Une nouvelle syntaxe de tableau est désormais disponible : par exemple, il est possible de spécifier les jours de la semaine ou les mois par leur nom (lun, mar, etc.) :

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

Il est possible de spécifier un pas, à travers lequel les tâches sont lancées :

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

Les pas et les intervalles peuvent être mélangés :

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

Des alternatives intuitives à la syntaxe habituelle sont prises en charge (reboot, yearly, annually, monthly, weekly, daily, midnight, hourly) :

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

Environnement d'exécution des tâches

Vixie cron permet de modifier l'environnement des applications lancées.

Les variables d'environnement USER, LOGNAME et HOME ne sont pas simplement fournies par le démon, mais sont prises depuis le fichier passwd. La variable PATH prend la valeur « /usr/bin:/bin », et SHELL prend « /bin/sh ». Les valeurs de toutes les variables, sauf LOGNAME, peuvent être modifiées dans les tables d'utilisateurs.

Certaines variables d'environnement (en particulier SHELL et HOME) sont utilisées par cron lui-même pour lancer les tâches. Voici comment cela peut apparaître en utilisant bash au lieu de sh standard pour exécuter des tâches utilisateur :

SHELL=/bin/bash
HOME=/tmp/
# exec sera lancé par bash dans /tmp/
* * * * * /path/to/exec

En fin de compte, toutes les variables d'environnement définies dans la table (utilisées par cron ou nécessaires au processus) seront transmises à la tâche lancée.

Pour éditer des fichiers à l'aide de la commande crontab, l'éditeur spécifié dans la variable d'environnement VISUAL ou EDITOR est utilisé. Si ces variables ne sont pas définies dans l'environnement à partir duquel crontab a été lancé, « /usr/ucb/vi » est utilisé (ucb fait probablement référence à l'Université de Californie à Berkeley).

cron sous Debian et Ubuntu

Les développeurs de Debian et des distributions dérivées ont publié une version fortement modifiée de Vixie cron 3.0pl1. Il n'y a pas de différence dans la syntaxe des fichiers de table, pour les utilisateurs, c'est le même Vixie cron. Les principales nouvelles fonctionnalités : support de syslog, SELinux et PAM.

Parmi les changements moins visibles mais tangibles, on trouve l'emplacement des fichiers de configuration et des tables de tâches.

Les tables d'utilisateur dans Debian se trouvent dans le répertoire /var/spool/cron/crontabs, et la table système y est également - dans /etc/crontab. Les tables de tâches spécifiques aux paquets Debian sont placées dans /etc/cron.d, d'où le démon cron les lit automatiquement. La gestion de l'accès des utilisateurs est régulée par les fichiers /etc/cron.allow et /etc/cron.deny.

Le shell par défaut reste /bin/sh, dont le rôle est rempli dans Debian par un petit shell compatible POSIX dash, lancé sans lire aucune configuration (en mode non interactif).

Le Cron dans les dernières versions de Debian est lancé via systemd, et la configuration de lancement peut être consultée dans /lib/systemd/system/cron.service. Il n'y a rien de particulier dans la configuration du service, toute gestion plus fine des tâches peut être réalisée via les variables d'environnement déclarées directement dans le crontab de chaque utilisateur.

cronie dans RedHat, Fedora et CentOS

cronie — un fork de Vixie cron version 4.1. Comme dans Debian, la syntaxe n'a pas changé, mais un support pour PAM et SELinux a été ajouté, ainsi que des fonctionnalités pour travailler en cluster, surveiller des fichiers via inotify, et d'autres possibilités.

La configuration par défaut se trouve aux emplacements habituels : la table système — dans /etc/crontab, les paquets placent leurs tables dans /etc/cron.d, les tables utilisateur vont dans /var/spool/cron/crontabs.

Le démon est géré par systemd, la configuration du service est — /lib/systemd/system/crond.service.

Dans les distributions similaires à Red Hat, par défaut, /bin/sh est utilisé lors de l'exécution, qui est le bash standard. Il convient de noter que lors de l'exécution des tâches cron via /bin/sh, le shell bash est lancé en mode compatible POSIX et ne lit aucune configuration supplémentaire, fonctionnant en mode non interactif.

cronie sous SLES et openSUSE

La distribution allemande SLES et son dérivé openSUSE utilisent toujours cronie. Le démon ici est également lancé sous systemd, la configuration du service se trouve dans /usr/lib/systemd/system/cron.service. Configuration : /etc/crontab, /etc/cron.d, /var/spool/cron/tabs. En tant que /bin/sh, c'est le même bash qui est lancé en mode non interactif compatible POSIX.

Fonctionnement de Vixie cron

Les versions modernes de cron par rapport à Vixie cron n'ont pas radicalement changé, mais ont tout de même acquis de nouvelles fonctionnalités qui ne sont pas nécessaires pour comprendre les principes de fonctionnement du programme. Beaucoup de ces extensions sont mal conçues et compliquent le code. L'original du code source de cron de Paul Vixie est un vrai plaisir à lire.

Ainsi, j'ai décidé d'analyser le fonctionnement de cron en prenant comme exemple la version commune aux deux branches de développement du programme cron — Vixie cron 3.0pl1. Je simplifierai les exemples en enlevant les ifdef compliquant la lecture et en omettant les détails secondaires.

Le fonctionnement du démon peut être divisé en plusieurs étapes :

  1. Initialisation du programme.
  2. Collecte et mise à jour de la liste des tâches à exécuter.
  3. Fonctionnement de la boucle principale de cron.
  4. Lancement de la tâche.

Analysons-les dans l'ordre.

Initialisation

Lors du lancement, après vérification des arguments, le processus cron définit les gestionnaires de signaux SIGCHLD et SIGHUP. Le premier enregistre dans le journal la fin du processus fils, le second ferme le descripteur de fichier du journal :

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

Le démon cron fonctionne toujours en tant que superutilisateur et dans le répertoire principal de cron. Les appels suivants créent un fichier de verrouillage avec le PID du processus démon, vérifient que l'utilisateur est correct et changent le répertoire actuel sur le principal :

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

Un chemin par défaut est défini, qui sera utilisé lors du lancement des processus :

setenv("PATH", _PATH_DEFPATH, 1);

Ensuite, le processus est « démonisé » : il crée une copie fils du processus via un appel fork et une nouvelle session dans le processus fils (appel setsid). Il n'y a plus de nécessité pour le processus parent — et il se termine :

switch (fork()) {
case -1:
    /* erreur critique et fin du processus */
    exit(0);
break;
case 0:
    /* processus fils */
    (void) setsid();
break;
default:
    /* processus parent se termine */
    _exit(0);
}

La fin du processus parent libère le verrou du fichier de verrouillage. De plus, il est nécessaire de mettre à jour le PID dans le fichier avec celui du fils. Après cela, la base de tâches est remplie :

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

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

Ensuite, cron entre dans la boucle principale de travail. Mais avant cela, regardons la charge de la liste des tâches.

Collecte et mise à jour de la liste des tâches

La fonction load_database est responsable du chargement de la liste des tâches. Elle vérifie le crontab système principal et le répertoire des fichiers utilisateurs. Si les fichiers et le répertoire n'ont pas changé, la liste des tâches n'est pas relue. Sinon, une nouvelle liste de tâches commence à se former.

Chargement du fichier système avec des noms de fichiers spéciaux et de la table :

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

Chargement des tables utilisateur en boucle :

while (NULL != (dp = readdir(dir))) {
    char    fname[MAXNAMLEN+1],
            tabname[MAXNAMLEN+1];
    /* les fichiers commençant par un point ne doivent pas être lus */
    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);
}

Après cela, l'ancienne base de données est remplacée par la nouvelle.

Dans les exemples ci-dessus, l'appel de la fonction process_crontab vérifie l'existence de l'utilisateur correspondant au nom de fichier de la table (sauf s'il s'agit du superutilisateur), puis appelle load_user. Cette dernière lit le fichier ligne par ligne :

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

Ici, une variable d'environnement (chaînes du type VAR=value) est soit définie par les fonctions load_env / env_set, soit la description de la tâche (* * * * * /path/to/exec) est lue par la fonction load_entry.

L'entité entry, renvoyée par load_entry, représente notre tâche, insérée dans la liste des tâches générales. Dans la fonction elle-même, un long examen du format temporel est effectué, mais ce qui nous intéresse surtout, ce sont la formation des variables d'environnement et des paramètres de lancement de la tâche :

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

C'est avec la liste des tâches actuelle que fonctionne la boucle principale.

Boucle principale

Le cron original de Version 7 Unix fonctionnait de manière très simple : il relisait la configuration dans une boucle, lançait les tâches de la minute en cours sous l'utilisateur super et dormait jusqu'au début de la minute suivante. Cette méthode simple nécessitait trop de ressources sur les anciennes machines.

Dans SysV, une version alternative était proposée, où le démon s'endormait soit jusqu'à la minute la plus proche pour laquelle une tâche était définie, soit pendant 30 minutes. Les ressources nécessaires pour relire la configuration et vérifier les tâches dans ce mode étaient moindres, mais mettre à jour rapidement la liste des tâches était devenu peu pratique.

Le Vixie cron est revenu à la vérification des listes de tâches une fois par minute, heureusement, à la fin des années 80, les ressources sur les machines Unix standard étaient devenues considérablement plus nombreuses :

/* первичная загрузка задач */
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 fonction cron_sleep s'occupe directement de l'exécution des tâches, appelant les fonctions job_runqueue (itération et lancement des tâches) et do_command (lancement de chaque tâche individuelle). Cette dernière fonction mérite une attention particulière.

Lancement de la tâche

La fonction do_command est conçue dans le bon style Unix, c'est-à-dire qu'elle effectue un fork pour l'exécution asynchrone de la tâche. Le processus parent continue de lancer les tâches, tandis que le processus enfant s'occupe de la préparation du processus de la tâche :

switch (fork()) {
case -1:
    /* impossible d'effectuer le fork */
    break;
case 0:
    /* processus enfant : dans le doute, nous essayons encore une fois de prendre le verrou principal */
    acquire_daemonlock(1);
    /* passons à la formation du processus de tâche */
    child_process(e, u);
    /* à la fin, le processus enfant termine son travail */
    _exit(OK_EXIT);
    break;
default:
    /* le processus parent continue son travail */
    break;
}

Dans child_process, il y a assez de logique : elle prend en charge les flux de sortie standard et d'erreur pour ensuite les envoyer par e-mail (si la variable d'environnement MAILTO est spécifiée dans la table des tâches), et enfin, elle attend la fin du processus principal de la tâche.

Le processus de tâche est généré par un autre fork :

switch (vfork()) {
case -1:
    /* en cas d'erreur, le travail se termine immédiatement */
    exit(ERROR_EXIT);
case 0:
    /* le processus enfant crée une nouvelle session, un terminal, etc. */
    (void) setsid();

    /*
     * ensuite, une configuration verbeuse de la sortie du processus, que nous allons ignorer pour la brièveté
     * /

    /* changement de répertoire, d'utilisateur et de groupe d'utilisateur,
     * ce qui signifie que le processus n'est plus superutilisateur
     * /
    setgid(e->gid);
    setuid(e->uid);
    chdir(env_get("HOME", e->envp));

    /* lancement de la commande elle-même
     * /
    {
        /* la variable d'environnement SHELL indique l'interpréteur à utiliser pour le lancement */
        char    *shell = env_get("SHELL", e->envp);

        /* le processus se lance sans transmission de l'environnement du processus parent,
         * c'est-à-dire exactement comme décrit dans la table des tâches de l'utilisateur  */
        execle(shell, shell, "-c", e->cmd, (char *)0, e->envp);

        /* erreur — et le processus ne s'est pas lancé ? terminaison du travail */
        perror("execl");
        _exit(ERROR_EXIT);
    }
    break;
default:
    /* le processus lui-même continue de fonctionner : attend la fin et la sortie */
    break;
}

Et voilà, c'est tout le cron. J'ai omis quelques détails intéressants, comme la prise en compte des utilisateurs distants, mais l'essentiel est là.

Postface

Cron est une programme étonnamment simple et utile, réalisée dans les meilleures traditions du monde Unix. Il ne fait rien de superflu, mais accomplit son travail de manière remarquable depuis plusieurs décennies. La découverte du code de la version fournie avec Ubuntu n'a pas pris plus d'une heure, et j'en ai retiré un grand plaisir ! J'espère avoir pu vous transmettre cela.

Je ne sais pas pour vous, mais il m'est un peu triste de réaliser que la programmation moderne, avec sa tendance à la complexité excessive et à l'abstraction, n'offre plus cette simplicité depuis longtemps.

Il existe de nombreuses alternatives modernes au cron : les systemd-timers permettent d'organiser des systèmes complexes avec des dépendances, et fcron offre une flexibilité accrue dans la gestion de la consommation des ressources par les tâches. Mais personnellement, j'ai toujours été satisfait des crontab les plus simples.

En somme, aimez Unix, utilisez des programmes simples et n'oubliez pas de consulter les manuels de votre plateforme !

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster