Cron sous Linux : historique, utilisation et appareil

Cron sous Linux : historique, utilisation et appareil

Le classique écrivait que les happy hours ne se regardent pas. En ces temps fous, il n'y avait ni programmeurs ni Unix, mais aujourd'hui, les programmeurs en sont sûrs : cron suivra le temps à leur place.

Les utilitaires de ligne de commande sont à la fois une faiblesse et une corvée pour moi. sed, awk, wc, cut et d'autres anciens programmes sont exécutés quotidiennement par des scripts sur nos serveurs. Beaucoup d’entre elles sont conçues comme des tâches pour cron, un planificateur originaire des années 70.

Pendant longtemps, j'ai utilisé cron superficiellement, sans entrer dans les détails, mais un jour, lorsque j'ai rencontré une erreur lors de l'exécution d'un script, j'ai décidé de l'examiner en profondeur. C'est ainsi que cet article est apparu, en l'écrivant, je me suis familiarisé avec la crontab POSIX, les principales options cron des distributions Linux populaires et la structure de certaines d'entre elles.

Utilisez-vous Linux et exécutez des tâches cron ? Êtes-vous intéressé par l’architecture des applications système sous Unix ? Alors c'est parti !

Teneur

L'origine des espèces

L'exécution périodique de programmes utilisateur ou système est une nécessité évidente dans tous les systèmes d'exploitation. Par conséquent, les programmeurs ont compris depuis longtemps le besoin de services leur permettant de planifier et d’exécuter des tâches de manière centralisée.

Les systèmes d'exploitation de type Unix trouvent leurs origines dans la version 7 d'Unix, développée dans les années 70 du siècle dernier aux Bell Labs, notamment par le célèbre Ken Thompson. La version 7 d'Unix incluait également cron, un service permettant d'exécuter régulièrement des tâches de superutilisateur.

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 avec les tâches d'un seul fichier (/etc/lib/crontab) et exécutait pour le superutilisateur les tâches qui auraient dû être effectuées à l'heure actuelle.

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.

Des descriptions généralisées du format crontab et des principes de base du fonctionnement de l'utilitaire ont été incluses dans le principal standard des systèmes d'exploitation de type Unix - POSIX - en 1992, et ainsi cron d'un standard de facto est devenu un standard de jure.

En 1987, Paul Vixie, après avoir interrogé les utilisateurs d'Unix sur leurs souhaits concernant cron, a publié une autre version du démon qui corrigeait certains des problèmes du cron traditionnel et élargissait la syntaxe des fichiers de table.

Dès la troisième version de Vixie cron a commencé à répondre aux exigences POSIX, de plus, le programme avait une licence libérale, ou plutôt il n'y avait pas de licence du tout, à l'exception des souhaits du 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 gagnait en popularité au cours de ces années, de sorte que certaines des distributions Linux clés apparues au début des années 90 ont pris Vixie cron comme système et le développent encore aujourd'hui.

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

Commençons par nous familiariser avec l'utilitaire utilisateur crontab décrit dans POSIX, après quoi nous examinerons les extensions de syntaxe fournies dans Vixie cron et l'utilisation de variantes de Vixie cron dans les distributions Linux populaires. Et enfin, la cerise sur le gâteau est l’analyse du dispositif démon cron.

crontab POSIX

Si le cron d'origine a toujours fonctionné pour le superutilisateur, les planificateurs modernes s'occupent souvent des tâches des utilisateurs ordinaires, ce qui est plus sûr et plus pratique.

Les crons sont fournis sous la forme d'un ensemble de deux programmes : le démon cron exécuté en permanence et l'utilitaire crontab disponible pour les utilisateurs. Ce dernier permet d'éditer des tables de tâches spécifiques à chaque utilisateur du système, tandis que le démon lance des tâches à partir des tables utilisateur et système.

В Norme POSIX le comportement du démon n'est en aucun cas décrit et seul le programme utilisateur est formalisé crontab. L'existence de mécanismes de lancement de tâches utilisateur est bien entendu implicite, mais n'est pas décrite en détail.

En appelant l'utilitaire crontab, vous pouvez faire quatre choses : modifier la table des tâches de l'utilisateur dans l'éditeur, charger la table à partir d'un fichier, afficher la table des tâches actuelle et effacer la table des tâches. Exemples de fonctionnement de l'utilitaire crontab :

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

Sur appel crontab -e l'éditeur spécifié dans la variable d'environnement standard sera utilisé EDITOR.

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, sixième champ, est une ligne qui sera exécutée par l'interpréteur de commandes standard.

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

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

Ou avec un trait d'union :

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

L'accès des utilisateurs à la planification des tâches est réglementé dans POSIX par les fichiers cron.allow et cron.deny, qui répertorient respectivement les utilisateurs ayant accès à crontab et les utilisateurs n'ayant pas accès au programme. La norme ne réglemente en aucun cas l'emplacement de ces fichiers.

Selon la norme, au moins quatre variables d'environnement doivent être transmises aux programmes lancés :

  1. HOME - répertoire personnel de l'utilisateur.
  2. LOGNAME — connexion de l'utilisateur.
  3. PATH est le chemin où vous pouvez trouver les utilitaires système standard.
  4. SHELL — chemin d'accès à l'interpréteur de commandes utilisé.

Notamment, POSIX ne dit rien sur l'origine des valeurs de ces variables.

Meilleure vente - Vixie cron 3.0pl1

L'ancêtre commun des variantes populaires de cron est Vixie cron 3.0pl1, introduit 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 se décline en deux programmes (cron et crontab). Comme d'habitude, le démon est responsable de la lecture et de l'exécution des tâches de la table des tâches système et des tables des tâches utilisateur individuelles, et l'utilitaire crontab est responsable de la modification des tables utilisateur.

Table des tâches et fichiers de configuration

La table des tâches du superutilisateur se trouve 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 de celle-ci indique le nom de l'utilisateur sous le nom duquel la tâche est lancée :

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

Les tables de tâches des utilisateurs réguliers se trouvent dans /var/cron/tabs/username et utilisent la même syntaxe. Lorsque vous exécutez l'utilitaire crontab en tant qu'utilisateur, ce sont les fichiers qui sont modifiés.

Les listes d'utilisateurs ayant accès à crontab sont gérées dans les fichiers /var/cron/allow et /var/cron/deny, où il suffit de saisir le nom de l'utilisateur sur une ligne séparée.

Syntaxe étendue

Par rapport à la crontab POSIX, la solution de Paul Vixey contient plusieurs modifications très utiles de la syntaxe des tables de tâches de l'utilitaire.

Une nouvelle syntaxe de tableau est devenue disponible : par exemple, vous pouvez spécifier les jours de la semaine ou les mois par leur nom (Lun, Mar, etc.) :

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

Vous pouvez spécifier l'étape par laquelle les tâches sont lancées :

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

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

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

Des alternatives intuitives à la syntaxe habituelle sont prises en charge (redémarrage, annuel, annuel, mensuel, hebdomadaire, quotidien, minuit, horaire) :

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

Environnement d'exécution des tâches

Vixie cron vous permet de modifier l'environnement des applications en cours d'exécution.

Les variables d'environnement USER, LOGNAME et HOME ne sont pas simplement fournies par le démon, mais sont extraites d'un fichier passwd. La variable PATH est définie sur "/usr/bin:/bin" et la variable SHELL est définie sur "/bin/sh". Les valeurs de toutes les variables à l'exception de LOGNAME peuvent être modifiées dans les tables utilisateur.

Certaines variables d'environnement (notamment SHELL et HOME) sont utilisées par cron lui-même pour exécuter la tâche. Voici à quoi pourrait ressembler l'utilisation de bash au lieu de sh standard pour exécuter des tâches personnalisées :

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

En fin de compte, toutes les variables d'environnement définies dans le tableau (utilisées par cron ou nécessaires au processus) seront transmises à la tâche en cours d'exécution.

Pour éditer des fichiers, crontab utilise l'éditeur spécifié dans la variable d'environnement VISUAL ou EDITOR. Si l'environnement dans lequel crontab a été exécuté ne dispose pas de ces variables définies, alors "/usr/ucb/vi" est utilisé (ucb est probablement l'Université de Californie, Berkeley).

cron sur Debian et Ubuntu

Les développeurs de Debian et des distributions dérivées ont publié version fortement modifiée Vixie cron version 3.0pl1. Il n'y a aucune différence dans la syntaxe des fichiers de table, pour les utilisateurs c'est le même Vixie cron. La plus grande nouveauté : le support syslog, SELinux и PAM et Bastion.

Les changements moins visibles, mais tangibles, incluent l'emplacement des fichiers de configuration et des tables de tâches.

Les tables utilisateur dans Debian se trouvent dans le répertoire /var/spool/cron/crontabs, la table système est toujours là - 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. Le contrôle d'accès des utilisateurs est contrôlé par les fichiers /etc/cron.allow et /etc/cron.deny.

Le shell par défaut est toujours /bin/sh, qui dans Debian est un petit shell compatible POSIX tiret, lancé sans lire aucune configuration (en mode non interactif).

Cron lui-même 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 spécial dans la configuration du service ; toute gestion de tâches plus subtile peut se faire via des variables d'environnement déclarées directement dans la crontab de chaque utilisateur.

copain sur RedHat, Fedora et CentOS

copain — fork de Vixie cron version 4.1. Comme dans Debian, la syntaxe n'a pas changé, mais la prise en charge de PAM et SELinux, le travail en cluster, le suivi des fichiers à l'aide d'inotify et d'autres fonctionnalités ont été ajoutées.

La configuration par défaut est aux endroits habituels : la table système est dans /etc/crontab, les packages mettent leurs tables dans /etc/cron.d, les tables utilisateur vont dans /var/spool/cron/crontabs.

Le démon s'exécute sous le contrôle de systemd, la configuration du service est /lib/systemd/system/crond.service.

Sur les distributions de type Red Hat, /bin/sh est utilisé par défaut au démarrage, ce qui est le bash standard. Il convient de noter que lors de l'exécution de tâches cron via /bin/sh, le shell bash démarre en mode compatible POSIX et ne lit aucune configuration supplémentaire, s'exécutant en mode non interactif.

copain dans SLES et openSUSE

La distribution allemande SLES et son dérivé openSUSE utilisent le même copain. 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. /bin/sh est le même bash exécuté en mode non interactif conforme à POSIX.

Appareil cron Vixie

Les descendants modernes de cron n'ont pas radicalement changé par rapport à Vixie cron, mais ont néanmoins acquis de nouvelles fonctionnalités qui ne sont pas nécessaires pour comprendre les principes du programme. Beaucoup de ces extensions sont mal conçues et confondent le code. Le code source cron original de Paul Vixey est un plaisir à lire.

Par conséquent, j'ai décidé d'analyser le périphérique cron en utilisant l'exemple d'un programme cron commun aux deux branches de développement - Vixie cron 3.0pl1. Je simplifierai les exemples en supprimant les ifdefs qui compliquent la lecture et en omettant des détails mineurs.

Le travail 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. Boucle cron principale en cours d'exécution.
  4. Commencez une tâche.

Regardons-les dans l'ordre.

Initialisation

Au démarrage, après avoir vérifié les arguments du processus, cron installe les gestionnaires de signaux SIGCHLD et SIGHUP. Le premier crée une entrée de journal concernant la fin du processus enfant, le second ferme le descripteur de fichier du fichier journal :

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

Le démon cron s'exécute toujours seul sur le système, uniquement en tant que superutilisateur et à partir du répertoire principal cron. Les appels suivants créent un fichier de verrouillage avec le PID du processus démon, assurez-vous que l'utilisateur a raison et remplacez le répertoire actuel par le répertoire principal :

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

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

setenv("PATH", _PATH_DEFPATH, 1);

Ensuite, le processus est « démonisé » : il crée une copie enfant du processus en appelant fork et une nouvelle session dans le processus enfant (en appelant setsid). Le processus parent n'est plus nécessaire et il se termine :

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

La fin du processus parent libère le verrou sur le fichier de verrouillage. De plus, il est nécessaire de mettre à jour le PID dans le fichier de l'enfant. Après cela, la base de données des tâches est remplie :

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

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

Ensuite, cron passe au cycle de travail principal. Mais avant cela, cela vaut la peine de jeter un œil au chargement de la liste des tâches.

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

La fonction load_database est chargée de charger la liste des tâches. Il vérifie la crontab principale du système et le répertoire contenant les fichiers utilisateur. 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 d'un fichier système avec des noms de fichiers et de tables spéciaux :

/* если файл системной таблицы изменился, перечитываем */
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];
    /* читать файлы с точкой не надо*/
    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 quoi l'ancienne base de données est remplacée par une nouvelle.

Dans les exemples ci-dessus, l'appel de fonction process_crontab vérifie qu'un utilisateur correspondant au nom du fichier de table existe (sauf s'il s'agit d'un superutilisateur), puis appelle load_user. Ce dernier lit déjà le fichier lui-même 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, soit la variable d'environnement est définie (lignes de la forme VAR=value) à l'aide des fonctions load_env / env_set, soit la description de la tâche est lue (* * * * * /path/to/exec) à l'aide de la fonction load_entry.

L'entité d'entrée renvoyée par load_entry est notre tâche, qui est placée dans la liste générale des tâches. La fonction elle-même effectue une analyse détaillée du format de l'heure, mais nous nous intéressons davantage à la formation des variables d'environnement et des paramètres de lancement des tâches :

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

La boucle principale fonctionne avec la liste actuelle des tâches.

Boucle principale

Le cron original de la version 7 d'Unix fonctionnait assez simplement : il relisait la configuration en boucle, lançait les tâches de la minute en cours en tant que superutilisateur, et dormait jusqu'au début de la minute suivante. Cette approche simple sur des machines plus anciennes nécessitait trop de ressources.

Une version alternative a été proposée dans SysV, dans laquelle le démon s'endormit soit jusqu'à la minute la plus proche pour laquelle la tâche a été définie, soit pendant 30 minutes. Moins de ressources étaient consommées pour relire la configuration et vérifier les tâches dans ce mode, mais la mise à jour rapide de la liste des tâches devenait peu pratique.

Vixie cron a recommencé à vérifier les listes de tâches une fois par minute. Heureusement, à la fin des années 80, il y avait beaucoup plus de ressources sur les machines 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 fonction cron_sleep est directement impliquée dans l'exécution des tâches, en appelant les fonctions job_runqueue (énumérer et exécuter les tâches) et do_command (exécuter chaque tâche individuelle). La dernière fonction mérite d’être examinée plus en détail.

Exécuter une tâche

La fonction do_command est exécutée dans le bon style Unix, c'est-à-dire qu'elle effectue un fork pour effectuer la tâche de manière asynchrone. Le processus parent continue de lancer des tâches, le processus enfant prépare le processus tâche :

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

Il y a beaucoup de logique dans child_process : il prend sur lui-même les flux de sortie et d'erreurs standard, afin de les envoyer ensuite au courrier (si la variable d'environnement MAILTO est spécifiée dans la table des tâches) et, enfin, attend le message principal. processus de la tâche à accomplir.

Le processus de tâche est formé par un autre 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;
}

C'est essentiellement tout ce qu'est Cron. J'ai omis certains détails intéressants, par exemple la comptabilisation des utilisateurs distants, mais j'ai souligné l'essentiel.

Postface

Cron est un programme étonnamment simple et utile, réalisé dans les meilleures traditions du monde Unix. Elle ne fait rien de plus, mais elle fait son travail à merveille depuis plusieurs décennies maintenant. Comprendre le code de la version fournie avec Ubuntu n'a pas pris plus d'une heure et je me suis beaucoup amusé ! J'espère avoir pu le partager avec vous.

Je ne sais pas pour vous, mais je suis un peu triste de constater que la programmation moderne, avec sa tendance à la complexité et à l’abstraction, n’a pas été propice à une telle simplicité depuis longtemps.

Il existe de nombreuses alternatives modernes à cron : systemd-timers vous permettent d'organiser des systèmes complexes avec des dépendances, fcron vous permet de réguler de manière plus flexible la consommation de ressources par tâches. Mais personnellement, la crontab la plus simple me suffisait toujours.

Bref, aimez Unix, utilisez des programmes simples et n'oubliez pas de lire le mana de votre plateforme !

Source: habr.com

Ajouter un commentaire