Cron en Linux: historia, uso y dispositivo

Cron en Linux: historia, uso y dispositivo

El clásico escribió que las horas felices no se miran. En aquellos tiempos salvajes no había ni programadores ni Unix, pero hoy los programadores lo saben con seguridad: cron llevará la cuenta del tiempo en lugar de ellos.

Las utilidades de línea de comandos son a la vez una debilidad y una tarea ardua para mí. sed, awk, wc, cut y otros programas antiguos se ejecutan mediante scripts en nuestros servidores todos los días. Muchas de ellas están diseñadas como tareas para cron, un programador originario de los años 70.

Durante mucho tiempo usé cron de manera superficial, sin entrar en detalles, pero un día, cuando encontré un error al ejecutar un script, decidí investigarlo a fondo. Así apareció este artículo, mientras lo escribía me familiaricé con el crontab POSIX, las principales opciones de cron en distribuciones populares de Linux y la estructura de algunas de ellas.

¿Estás usando Linux y ejecutas tareas cron? ¿Está interesado en la arquitectura de aplicaciones del sistema en Unix? ¡Entonces estamos en camino!

contenido

Origen de las especies

La ejecución periódica de programas de usuario o del sistema es una necesidad obvia en todos los sistemas operativos. Por lo tanto, los programadores se dieron cuenta hace mucho tiempo de la necesidad de servicios que les permitieran planificar y ejecutar tareas de forma centralizada.

Los sistemas operativos tipo Unix tienen su origen en la versión 7 de Unix, desarrollada en los años 70 del siglo pasado en los laboratorios Bell, incluido el famoso Ken Thompson. La versión 7 de Unix también incluía cron, un servicio para ejecutar tareas de superusuario con regularidad.

Un cron moderno típico es un programa simple, pero el algoritmo operativo de la versión original era aún más simple: el servicio se activaba una vez por minuto, leía una tabla con tareas de un solo archivo (/etc/lib/crontab) y ejecutaba para el superusuario aquellas tareas que deberían haberse realizado en el momento actual.

Posteriormente, se suministraron versiones mejoradas de este sencillo y útil servicio con todos los sistemas operativos tipo Unix.

Las descripciones generalizadas del formato crontab y los principios básicos del funcionamiento de la utilidad se incluyeron en el estándar principal de los sistemas operativos tipo Unix, POSIX, en 1992 y, por lo tanto, cron de un estándar de facto se convirtió en un estándar de jure.

En 1987, Paul Vixie, después de haber encuestado a los usuarios de Unix sobre sus deseos de cron, lanzó otra versión del demonio que corrigió algunos de los problemas del cron tradicional y amplió la sintaxis de los archivos de tabla.

A partir de la tercera versión de Vixie, cron comenzó a cumplir con los requisitos POSIX, además, el programa tenía una licencia liberal, o más bien no había ninguna licencia, excepto los deseos en el README: el autor no da garantías, el nombre del autor no se puede eliminar y el programa solo se puede vender junto con el código fuente. Estos requisitos resultaron ser compatibles con los principios del software libre que estaba ganando popularidad en esos años, por lo que algunas de las distribuciones clave de Linux que aparecieron a principios de los 90 tomaron Vixie cron como sistema y todavía lo están desarrollando hoy.

En particular, Red Hat y SUSE desarrollan una bifurcación de Vixie cron - cronie, y Debian y Ubuntu usan la edición original de Vixie cron con muchos parches.

Primero, familiaricémonos con la utilidad de usuario crontab descrita en POSIX, después de lo cual veremos las extensiones de sintaxis proporcionadas en Vixie cron y el uso de variaciones de Vixie cron en distribuciones populares de Linux. Y finalmente, la guinda del pastel es el análisis del dispositivo cron daemon.

crontab POSIX

Si el cron original siempre funcionó para el superusuario, los programadores modernos a menudo se ocupan de tareas de usuarios comunes, lo cual es más seguro y conveniente.

Los crons se suministran como un conjunto de dos programas: el demonio cron que se ejecuta constantemente y la utilidad crontab disponible para los usuarios. Este último le permite editar tablas de tareas específicas para cada usuario en el sistema, mientras que el demonio lanza tareas desde tablas de usuarios y del sistema.

В estándar POSIX el comportamiento del demonio no se describe de ninguna manera y solo se formaliza el programa de usuario crontab. Por supuesto, la existencia de mecanismos para iniciar tareas de usuario está implícita, pero no se describe en detalle.

Al llamar a la utilidad crontab, puede hacer cuatro cosas: editar la tabla de tareas del usuario en el editor, cargar la tabla desde un archivo, mostrar la tabla de tareas actual y borrar la tabla de tareas. Ejemplos de cómo funciona la utilidad crontab:

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

cuando se llama crontab -e Se utilizará el editor especificado en la variable de entorno estándar. EDITOR.

Las tareas en sí se describen en el siguiente 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

Los primeros cinco campos de los registros: minutos [1..60], horas [0..23], días del mes [1..31], meses [1..12], días de la semana [0. .6], donde 0 es domingo. El último, sexto campo, es una línea que será ejecutada por el intérprete de comandos estándar.

En los primeros cinco campos, los valores se pueden enumerar separados por comas:

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

O con un guión:

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

El acceso de los usuarios a la programación de tareas está regulado en POSIX por los archivos cron.allow y cron.deny, que enumeran los usuarios con acceso a crontab y los usuarios sin acceso al programa, respectivamente. La norma no regula de ninguna manera la ubicación de estos archivos.

Según el estándar, se deben pasar al menos cuatro variables de entorno a los programas iniciados:

  1. INICIO: directorio de inicio del usuario.
  2. LOGNAME: inicio de sesión del usuario.
  3. PATH es la ruta donde puede encontrar las utilidades estándar del sistema.
  4. SHELL: ruta al intérprete de comandos utilizado.

En particular, POSIX no dice nada sobre de dónde provienen los valores de estas variables.

Mejor vendido: Vixie cron 3.0pl1

El ancestro común de las variantes de cron populares es Vixie cron 3.0pl1, introducido en la lista de correo comp.sources.unix en 1992. Consideraremos las características principales de esta versión con más detalle.

Vixie cron viene en dos programas (cron y crontab). Como es habitual, el demonio es responsable de leer y ejecutar tareas desde la tabla de tareas del sistema y las tablas de tareas de usuarios individuales, y la utilidad crontab es responsable de editar las tablas de usuarios.

Tabla de tareas y archivos de configuración.

La tabla de tareas del superusuario se encuentra en /etc/crontab. La sintaxis de la tabla del sistema corresponde a la sintaxis de Vixie cron, con la excepción de que la sexta columna indica el nombre del usuario en cuyo nombre se inicia la tarea:

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

Las tablas de tareas de usuario habituales se encuentran en /var/cron/tabs/username y utilizan la misma sintaxis. Cuando ejecuta la utilidad crontab como usuario, estos son los archivos que se editan.

Las listas de usuarios con acceso a crontab se administran en los archivos /var/cron/allow y /var/cron/deny, donde solo necesita ingresar el nombre de usuario en una línea separada.

Sintaxis extendida

En comparación con crontab POSIX, la solución de Paul Vixey contiene varias modificaciones muy útiles en la sintaxis de las tablas de tareas de la utilidad.

Está disponible una nueva sintaxis de tabla: por ejemplo, puede especificar los días de la semana o los meses por nombre (lunes, martes, etc.):

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

Puede especificar el paso a través del cual se lanzan las tareas:

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

Los pasos e intervalos se pueden combinar:

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

Se admiten alternativas intuitivas a la sintaxis habitual (reiniciar, anual, anual, mensual, semanal, diaria, medianoche, cada hora):

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

Entorno de ejecución de tareas

Vixie cron le permite cambiar el entorno de las aplicaciones en ejecución.

Las variables de entorno USER, LOGNAME y HOME no las proporciona simplemente el demonio, sino que se toman de un archivo. passwd. La variable PATH está configurada en "/usr/bin:/bin" y la variable SHELL está configurada en "/bin/sh". Los valores de todas las variables excepto LOGNAME se pueden cambiar en las tablas de usuario.

El propio cron utiliza algunas variables de entorno (sobre todo SHELL y HOME) para ejecutar la tarea. Así es como se vería el uso de bash en lugar del sh estándar para ejecutar tareas personalizadas:

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

En última instancia, todas las variables de entorno definidas en la tabla (utilizadas por cron o necesarias para el proceso) se pasarán a la tarea en ejecución.

Para editar archivos, crontab utiliza el editor especificado en la variable de entorno VISUAL o EDITOR. Si el entorno donde se ejecutó crontab no tiene estas variables definidas, entonces se usa "/usr/ucb/vi" (ucb es probablemente la Universidad de California, Berkeley).

cron en Debian y Ubuntu

Los desarrolladores de Debian y distribuciones derivadas han lanzado versión muy modificada Vixie cron versión 3.0pl1. No hay diferencias en la sintaxis de los archivos de tabla, para los usuarios es el mismo cron de Vixie. Característica nueva más importante: soporte syslog, SELinux и PAM.

Los cambios menos notables, pero tangibles, incluyen la ubicación de los archivos de configuración y las tablas de tareas.

Las tablas de usuario en Debian están ubicadas en el directorio /var/spool/cron/crontabs, la tabla del sistema todavía está allí, en /etc/crontab. Las tablas de tareas específicas del paquete Debian se colocan en /etc/cron.d, desde donde el demonio cron las lee automáticamente. El control de acceso de usuarios está controlado por los archivos /etc/cron.allow y /etc/cron.deny.

El shell predeterminado sigue siendo /bin/sh, que en Debian es un pequeño shell compatible con POSIX. guión, lanzado sin leer ninguna configuración (en modo no interactivo).

El propio cron en las últimas versiones de Debian se inicia a través de systemd, y la configuración de inicio se puede ver en /lib/systemd/system/cron.service. No hay nada especial en la configuración del servicio, cualquier gestión de tareas más sutil se puede realizar a través de variables de entorno declaradas directamente en el crontab de cada usuario.

compinche en RedHat, Fedora y CentOS

cronie — bifurcación de Vixie cron versión 4.1. Al igual que en Debian, la sintaxis no ha cambiado, pero se ha agregado soporte para PAM y SELinux, trabajo en clúster, seguimiento de archivos mediante inotify y otras funciones.

La configuración predeterminada está en los lugares habituales: la tabla del sistema está en /etc/crontab, los paquetes colocan sus tablas en /etc/cron.d, las tablas de usuario van en /var/spool/cron/crontabs.

El demonio se ejecuta bajo el control de systemd, la configuración del servicio es /lib/systemd/system/crond.service.

En distribuciones tipo Red Hat, /bin/sh se usa de forma predeterminada al inicio, que es el bash estándar. Cabe señalar que cuando se ejecutan trabajos cron a través de /bin/sh, el shell bash se inicia en modo compatible con POSIX y no lee ninguna configuración adicional, ejecutándose en modo no interactivo.

compinche en SLES y openSUSE

La distribución alemana SLES y su derivado openSUSE utilizan el mismo compinche. El demonio aquí también se inicia en systemd, la configuración del servicio se encuentra en /usr/lib/systemd/system/cron.service. Configuración: /etc/crontab, /etc/cron.d, /var/spool/cron/tabs. /bin/sh es el mismo bash que se ejecuta en modo no interactivo compatible con POSIX.

Dispositivo cron Vixie

Los descendientes modernos de cron no han cambiado radicalmente en comparación con Vixie cron, pero aún así adquirieron nuevas características que no son necesarias para comprender los principios del programa. Muchas de estas extensiones están mal diseñadas y confunden el código. Es un placer leer el código fuente original de cron de Paul Vixey.

Por lo tanto, decidí analizar el dispositivo cron usando el ejemplo de un programa cron común a ambas ramas del desarrollo: Vixie cron 3.0pl1. Simplificaré los ejemplos eliminando ifdefs que complican la lectura y omitiendo detalles menores.

El trabajo del demonio se puede dividir en varias etapas:

  1. Inicialización del programa.
  2. Recopilar y actualizar la lista de tareas a ejecutar.
  3. Bucle cron principal en ejecución.
  4. Iniciar una tarea.

Veámoslos en orden.

Inicialización

Cuando se inicia, después de verificar los argumentos del proceso, cron instala los manejadores de señales SIGCHLD y SIGHUP. El primero hace una entrada de registro sobre la terminación del proceso hijo, el segundo cierra el descriptor del archivo de registro:

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

El demonio cron siempre se ejecuta solo en el sistema, sólo como superusuario y desde el directorio cron principal. Las siguientes llamadas crean un archivo de bloqueo con el PID del proceso del demonio, asegúrese de que el usuario sea correcto y cambie el directorio actual al principal:

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

Se establece la ruta predeterminada, que se utilizará al iniciar procesos:

setenv("PATH", _PATH_DEFPATH, 1);

Luego, el proceso se "daemoniza": crea una copia secundaria del proceso llamando a fork y una nueva sesión en el proceso hijo (llamando a setsid). El proceso principal ya no es necesario y sale:

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

La terminación del proceso principal libera el bloqueo en el archivo de bloqueo. Además, es necesario actualizar el PID en el archivo del niño. Después de esto, se completa la base de datos de tareas:

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

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

Luego cron pasa al ciclo de trabajo principal. Pero antes de eso, vale la pena echar un vistazo a cómo cargar la lista de tareas.

Recopilar y actualizar la lista de tareas.

La función load_database es responsable de cargar la lista de tareas. Comprueba el crontab principal del sistema y el directorio con archivos de usuario. Si los archivos y el directorio no han cambiado, la lista de tareas no se vuelve a leer. De lo contrario, comienza a formarse una nueva lista de tareas.

Cargando un archivo de sistema con nombres de archivos y tablas especiales:

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

Cargando tablas de usuarios en un bucle:

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

Después de lo cual la base de datos antigua se reemplaza por una nueva.

En los ejemplos anteriores, la llamada a la función Process_crontab verifica que exista un usuario que coincida con el nombre del archivo de la tabla (a menos que sea un superusuario) y luego llama a load_user. Este último ya lee el archivo línea por línea:

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

Aquí, se establece la variable de entorno (líneas del formato VAR=valor) usando las funciones load_env / env_set, o se lee la descripción de la tarea (* * * * * /path/to/exec) usando la función load_entry.

La entidad de entrada que devuelve load_entry es nuestra tarea, que se coloca en la lista general de tareas. La función en sí realiza un análisis detallado del formato de hora, pero estamos más interesados ​​en la formación de variables de entorno y parámetros de inicio de tareas:

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

El bucle principal funciona con la lista actual de tareas.

Bucle principal

El cron original de la versión 7 de Unix funcionaba de forma bastante sencilla: releía la configuración en un bucle, iniciaba las tareas del minuto actual como superusuario y dormía hasta el inicio del minuto siguiente. Este sencillo enfoque en máquinas más antiguas requería demasiados recursos.

Se propuso una versión alternativa en SysV, en la que el demonio se quedaba dormido hasta el minuto más cercano para el cual se definió la tarea, o durante 30 minutos. Se consumieron menos recursos para volver a leer la configuración y verificar las tareas en este modo, pero actualizar rápidamente la lista de tareas se volvió inconveniente.

Vixie cron volvió a comprobar las listas de tareas una vez por minuto; afortunadamente, a finales de los años 80 había muchos más recursos en las máquinas Unix estándar:

/* первичная загрузка задач */
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 función cron_sleep participa directamente en la ejecución de tareas, llamando a las funciones job_runqueue (enumerar y ejecutar tareas) y do_command (ejecutar cada tarea individual). Vale la pena examinar con más detalle la última función.

Ejecutando una tarea

La función do_command se ejecuta en buen estilo Unix, es decir, realiza una bifurcación para realizar la tarea de forma asincrónica. El proceso padre continúa lanzando tareas, el proceso hijo prepara el proceso de tarea:

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

Hay bastante lógica en child_process: toma la salida estándar y los flujos de error sobre sí mismo para luego enviarlos al correo (si la variable de entorno MAILTO está especificada en la tabla de tareas) y, finalmente, espera el mensaje principal. proceso de la tarea a completar.

El proceso de la tarea está formado por otra bifurcación:

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

Eso es básicamente todo lo que es cron. Omití algunos detalles interesantes, por ejemplo, tener en cuenta a los usuarios remotos, pero describí lo principal.

Epílogo

Cron es un programa sorprendentemente simple y útil, creado según las mejores tradiciones del mundo Unix. No hace nada extra, pero lleva varias décadas haciendo su trabajo maravillosamente. Leer el código de la versión que viene con Ubuntu no me llevó más de una hora y ¡me divertí mucho! Espero haber podido compartirlo contigo.

No sé ustedes, pero a mí me entristece un poco darme cuenta de que la programación moderna, con su tendencia a ser demasiado complicada y demasiado abstracta, no ha propiciado tal simplicidad durante mucho tiempo.

Existen muchas alternativas modernas a cron: systemd-timers le permite organizar sistemas complejos con dependencias, fcron le permite regular de manera más flexible el consumo de recursos por tareas. Pero personalmente, el crontab más simple siempre fue suficiente para mí.

En resumen, ama Unix, usa programas simples y ¡no olvides leer el maná de tu plataforma!

Fuente: habr.com

Añadir un comentario