
Clásico escribió que los felices no prestan atención al tiempo. En aquellos tiempos salvajes no había ni programadores ni Unix, pero en nuestros días los programadores saben con certeza: en lugar de ellos, cron se encargará del tiempo.
Las utilidades de línea de comandos son al mismo tiempo una debilidad y una rutina para mí. sed, awk, wc, cut y otros programas antiguos se ejecutan a través de scripts en nuestros servidores a diario. Muchos de ellos están configurados como trabajos para cron, un planificador que data de los años 70.
Durante mucho tiempo usé cron superficialmente, sin profundizar en los detalles, pero un día, al enfrentarme a un error al ejecutar un script, decidí investigar a fondo. Así nació este artículo, en el que me familiaricé con el crontab POSIX, las variantes principales de cron en distribuciones populares de Linux y el funcionamiento de algunas de ellas.
¿Usas Linux y ejecutas tareas en cron? ¿Te interesa la arquitectura de aplicaciones del sistema en Unix? Entonces somos compatibles.
Contenido
El origen de las especies
La ejecución periódica de programas de usuario o del sistema es una necesidad evidente en todos los sistemas operativos. Por lo tanto, los programadores han reconocido hace mucho tiempo la necesidad de servicios que permitan programar y ejecutar tareas de manera centralizada.
Los sistemas operativos similares a Unix descienden de Version 7 Unix, que fue desarrollada en los años 70 en Bell Labs, incluido el famoso Ken Thompson. Junto con Version 7 Unix, también se entregó cron, un servicio para la ejecución regular de tareas del superusuario.
Un cron moderno típico es un programa sencillo, pero el algoritmo de funcionamiento de la versión original era aún más simple: el servicio se despertaba una vez por minuto, leía la tabla de tareas de un único archivo (/etc/lib/crontab) y ejecutaba para el superusuario las tareas que debían ejecutarse en el minuto actual.
Posteriormente, versiones mejoradas de este servicio simple y útil se incluyeron con todos los sistemas operativos similares a Unix.
Las descripciones generales del formato crontab y de los principios básicos de funcionamiento de la utilidad fueron incluidas en 1992 en el estándar principal de sistemas operativos similares a Unix — POSIX — y así cron pasó de ser un estándar de facto a un estándar de jure.
En 1987, Paul Vixie, al encuestar a los usuarios de Unix sobre sus deseos para cron, lanzó otra versión del demonio que corregía algunos problemas del cron tradicional y ampliaba la sintaxis de los archivos-tabla.
Con la tercera versión, Vixie cron cumplió con los requisitos de POSIX, además de tener una licencia liberal, o más bien, no tener ninguna licencia, salvo los deseos en el README: el autor no ofrece garantías, no se puede eliminar el nombre del autor 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 empezaba a ganar popularidad en esos años, por lo que algunas de las principales distribuciones de Linux que surgieron a principios de los años 90 adoptaron Vixie cron como su sistema y lo desarrollan hasta hoy.
En particular, Red Hat y SUSE desarrollan un fork de Vixie cron llamado cronie, mientras que Debian y Ubuntu utilizan la edición original de Vixie cron con una multitud de parches.
Comencemos por conocer la utilidad para el usuario crontab descrita en POSIX, después revisaremos las extensiones de la sintaxis presentadas en Vixie cron y el uso de variaciones de Vixie cron en las distribuciones populares de Linux. Y, finalmente, la cereza del pastel: el análisis del funcionamiento del demonio cron.
crontab POSIX
Si el cron original siempre funcionaba para el superusuario, los programadores modernos a menudo manejan tareas de usuarios normales, lo cual es más seguro y conveniente.
Los crones se suministran como un conjunto de dos programas: un demonio cron que se ejecuta continuamente y la utilidad crontab accesible a los usuarios. Esta última permite editar tablas de tareas específicas para cada usuario en el sistema, mientras que el demonio ejecuta tareas de las tablas de usuarios y del sistema.
En no describe el comportamiento del demonio y solo formaliza el programa del usuario . La existencia de mecanismos para ejecutar tareas de usuario, por supuesto, se implica, pero no se detalla.
Con la utilidad crontab se pueden hacer cuatro cosas: editar la tabla de tareas del usuario en un editor, cargar la tabla desde un archivo, mostrar la tabla de tareas actual y limpiar la tabla de tareas. Ejemplos de uso de la utilidad crontab:
crontab -e # editar la tabla de tareas
crontab -l # mostrar la tabla de tareas
crontab -r # eliminar la tabla de tareas
crontab path/to/file.crontab # cargar la tabla de tareas desde un archivoAl invocar 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.logLos primeros cinco campos de las entradas: 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 cadena que será ejecutada por el intérprete de comandos estándar.
En los primeros cinco campos, los valores pueden separarse por comas:
# задача, выполняемая в первую и десятую минуты каждого часа
1,10 * * * * /path/to/exec -a -b -cO por guiones:
# задача, выполняемая в каждую из первых десяти минут каждого часа
0-9 * * * * /path/to/exec -a -b -cEl acceso de los usuarios a la programación de tareas se regula en los archivos POSIX cron.allow y cron.deny, en los que se enumeran, respectivamente, los usuarios con acceso a crontab y los usuarios sin acceso al programa. La ubicación de estos archivos no está estandarizada.
A los programas que se ejecutan, según el estándar, se deben pasar al menos cuatro variables de entorno:
- HOME — el directorio personal del usuario.
- LOGNAME — el nombre de usuario.
- PATH — la ruta donde se pueden encontrar las utilidades estándar del sistema.
- SHELL — la ruta al intérprete de comandos utilizado.
Es notable que POSIX no especifica de dónde provienen los valores para estas variables.
Mejor vendedor — Vixie cron 3.0pl1
El ancestro común de las variantes populares de cron es Vixie cron 3.0pl1, presentado en la lista de correo comp.sources.unix en 1992. Las características principales de esta versión las examinaremos en detalle.
Vixie cron se presenta en dos programas (cron y crontab). Como de costumbre, el demonio se encarga de leer y ejecutar las tareas de la tabla de tareas del sistema y de las tablas de tareas de los usuarios individuales, mientras que la utilidad crontab se encarga de editar las tablas de los 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 salvedad de que en la sexta columna se indica el nombre del usuario bajo cuyo contexto se ejecuta la tarea:
# Запускается ежеминутно от пользователя vlad
* * * * * vlad /path/to/execLas tablas de tareas de los usuarios normales se encuentran en /var/cron/tabs/username y utilizan una sintaxis común. Al ejecutar la utilidad crontab en nombre de un usuario, se editan precisamente estos archivos.
La gestión de las listas de usuarios que tienen acceso a crontab se realiza en los archivos /var/cron/allow y /var/cron/deny, donde basta con añadir el nombre de usuario en una línea separada.
Sintaxis ampliada
En comparación con POSIX crontab, la solución de Paul Vixie contiene varias modificaciones muy útiles en la sintaxis de las tablas de tareas de la utilidad.
Se ha hecho disponible una nueva sintaxis de tablas: por ejemplo, se pueden especificar los días de la semana o los meses por su nombre (Lun, Mar, etc.):
# Запускается ежеминутно по понедельникам и вторникам в январе
* * * Jan Mon,Tue /path/to/execSe puede especificar el intervalo en el que se ejecutan las tareas:
# Запускается с шагом в две минуты
*/2 * * * Mon,Tue /path/to/execSe pueden mezclar intervalos y pasos:
# Запускается с шагом в две минуты в первых десять минут каждого часа
0-10/2 * * * * /path/to/execSe admiten alternativas intuitivas a la sintaxis habitual (reboot, yearly, annually, monthly, weekly, daily, midnight, hourly):
# Запускается после перезагрузки системы
@reboot /exec/on/reboot
# Запускается раз в день
@daily /exec/daily
# Запускается раз в час
@hourly /exec/dailyEntorno de ejecución de tareas
Vixie cron permite cambiar el entorno de las aplicaciones que se ejecutan.
Las variables de entorno USER, LOGNAME y HOME no solo son proporcionadas por el demonio, sino que se obtienen de un archivo . La variable PATH se establece en «/usr/bin:/bin», y SHELL en «/bin/sh». Los valores de todas las variables, excepto LOGNAME, se pueden modificar en las tablas de usuarios.
Algunas variables de entorno (sobre todo SHELL y HOME) son utilizadas por cron para ejecutar la tarea. Así es como podría verse el uso de bash en lugar del estándar sh para ejecutar tareas de usuario:
SHELL=/bin/bash
HOME=/tmp/
# se ejecutará bash en /tmp/
* * * * * /path/to/execAl final, todas las variables de entorno definidas en la tabla (utilizadas por cron o necesarias para el proceso) serán pasadas a la tarea ejecutada.
Para editar archivos, la utilidad crontab utiliza el editor especificado en la variable de entorno VISUAL o EDITOR. Si en el entorno donde se ejecutó crontab no están definidas estas variables, se utiliza «/usr/ucb/vi» (ucb probablemente se refiere a la Universidad de California, Berkeley).
cron en Debian y Ubuntu
Los desarrolladores de Debian y distribuciones derivadas han lanzado de Vixie cron 3.0pl1. No hay diferencias en la sintaxis de los archivos de tablas, para los usuarios es el mismo Vixie cron. Las mayores novedades: soporte de , y .
Entre los cambios menos notorios, pero palpables, está la ubicación de los archivos de configuración y las tablas de tareas.
Las tablas de usuario en Debian se encuentran en el directorio /var/spool/cron/crontabs, la tabla del sistema sigue estando en /etc/crontab. Las tablas de tareas específicas para paquetes de Debian se colocan en /etc/cron.d, desde donde el demonio cron las lee automáticamente. El control de accesos de usuarios está regulado por los archivos /etc/cron.allow y /etc/cron.deny.
Como intérprete de comandos por defecto, aún se utiliza /bin/sh, el cual en Debian es un pequeño shell compatible con POSIX , ejecutado sin leer ninguna configuración (en modo no interactivo).
El cron en las últimas versiones de Debian se ejecuta 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 más precisa de las tareas se puede realizar a través de las variables de entorno, que se declaran directamente en el crontab de cada uno de los usuarios.
cronie en RedHat, Fedora y CentOS
es un fork de Vixie cron versión 4.1. Al igual que en Debian, la sintaxis no ha cambiado, pero se ha añadido soporte para PAM y SELinux, trabajo en clúster, seguimiento de archivos mediante inotify y otras funcionalidades.
La configuración predeterminada se encuentra en lugares habituales: la tabla del sistema está en /etc/crontab, los paquetes colocan sus tablas en /etc/cron.d, las tablas de usuario van a /var/spool/cron/crontabs.
El demonio se ejecuta bajo el control de systemd, la configuración del servicio está en /lib/systemd/system/crond.service.
En distribuciones similares a Red Hat, al iniciar se usa por defecto /bin/sh, cuyo rol es asumido por el bash estándar. Es importante notar que al ejecutar tareas de cron a través de /bin/sh, la shell bash se inicia en modo compatible con POSIX y no lee ninguna configuración adicional, trabajando en modo no interactivo.
cronie en SLES y openSUSE
La distribución alemana SLES y su derivado openSUSE utilizan el mismo cronie. Aquí, el demonio también se ejecuta bajo systemd, la configuración del servicio está en /usr/lib/systemd/system/cron.service. Configuración: /etc/crontab, /etc/cron.d, /var/spool/cron/tabs. Como /bin/sh se utiliza el mismo bash, iniciado en modo no interactivo compatible con POSIX.
Funcionamiento de Vixie cron
Los descendientes modernos de cron en comparación con Vixie cron no han cambiado radicalmente, pero aún así han adquirido nuevas funcionalidades, que no son necesarias para comprender los principios de funcionamiento del programa. Muchas de estas expansiones están mal organizadas y confunden el código. El código fuente original de cron en la implementación de Paul Vixie es un placer de leer.
Por lo tanto, decidí llevar a cabo un análisis del funcionamiento de cron utilizando como ejemplo la versión común a ambas ramas de desarrollo del programa — Vixie cron 3.0pl1. Simplificaré los ejemplos, eliminando ifdef complicados de leer y omitiendo detalles secundarios.
El trabajo del demonio se puede dividir en varias etapas:
- Inicialización del programa.
- Recolección y actualización de la lista de tareas para ejecutar.
- Ejercicio del ciclo principal de cron.
- Ejecución de la tarea.
Vamos a desglosarlos en orden.
Inicialización
Al iniciarse, después de verificar los argumentos, el proceso cron establece los controladores de señales SIGCHLD y SIGHUP. El primero registra en el log la finalización del proceso hijo, y el segundo cierra el descriptor de archivo del archivo de log:
signal(SIGCHLD, sigchld_handler);
signal(SIGHUP, sighup_handler);El demonio cron en el sistema siempre se ejecuta solo, únicamente como superusuario y desde el directorio principal de cron. Las siguientes llamadas crean un archivo de bloqueo con el PID del proceso demonio, aseguran que el usuario sea el correcto y cambian el directorio actual al principal:
acquire_daemonlock(0);
set_cron_uid();
set_cron_cwd();Se establece una ruta predeterminada que se utilizará al iniciar procesos:
setenv("PATH", _PATH_DEFPATH, 1);Luego, el proceso se "demoniza": crea una copia hija del proceso mediante la llamada fork y una nueva sesión en el proceso hijo (llamada setsid). En el proceso padre ya no hay necesidad — y este se termina:
switch (fork()) {
case -1:
/* error crítico y finalización */
exit(0);
break;
case 0:
/* proceso hijo */
(void) setsid();
break;
default:
/* proceso padre finaliza */
_exit(0);
}
La finalización del proceso padre libera el bloqueo en el archivo de bloqueo. Además, es necesario actualizar el PID en el archivo al del hijo. Después de esto, se llena la base de tareas:
/* повторный захват лока */
acquire_daemonlock(0);
/* Заполнение БД */
database.head = NULL;
database.tail = NULL;
database.mtime = (time_t) 0;
load_database(&database);A continuación, cron pasa al ciclo principal de trabajo. Pero antes, es conveniente revisar la carga de la lista de tareas.
Recolección y actualización de la lista de tareas
La función load_database es responsable de la carga de la lista de tareas. Verifica el crontab del sistema principal y el directorio con los archivos de los usuarios. Si los archivos y el directorio no han cambiado, entonces la lista de tareas no se vuelve a leer. De lo contrario, se comienza a formar una nueva lista de tareas.
Carga del archivo del sistema con nombres de archivo especiales y la tabla:
/* если файл системной таблицы изменился, перечитываем */
if (syscron_stat.st_mtime) {
process_crontab("root", "*system*",
SYSCRONTAB, &syscron_stat,
&new_db, old_db);
}Carga de las tablas de usuarios en un ciclo:
while (NULL != (dp = readdir(dir))) {
char fname[MAXNAMLEN+1],
tabname[MAXNAMLEN+1];
/* no es necesario leer archivos que comiencen con 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);
}
Después de esto, la antigua base de datos es reemplazada por la nueva.
En los ejemplos anteriores, la llamada a la función process_crontab verifica la existencia del usuario correspondiente al nombre del archivo de la tabla (a menos que sea superusuario), después de lo cual se llama a load_user. Esta última lee el archivo línea por línea:
mientras ((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 (cadenas del tipo VAR=value) mediante las funciones load_env / env_set, o se lee la descripción de la tarea (***** /path/to/exec) mediante la función load_entry.
La entidad entry, que devuelve load_entry, es nuestra tarea, colocada en la lista común de tareas. En la propia función se realiza un análisis detallado del formato de tiempo, y a nosotros nos interesa más la formación de las variables de entorno y los parámetros de inicio de la tarea:
/* пользователь и группа для запуска задачи берутся из 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 ciclo principal trabaja con la lista actual de tareas.
Ciclo principal
El cron original de Version 7 Unix funcionaba de manera muy sencilla: leía la configuración en un ciclo, ejecutaba las tareas del usuario root para el minuto actual y dormía hasta el inicio del siguiente minuto. Este enfoque simple requería muchos recursos en máquinas antiguas.
En SysV se propuso una versión alternativa, donde el demonio dormía hasta el minuto más cercano para el cual se definía una tarea, o durante 30 minutos. En este modo se consumían menos recursos para releer la configuración y verificar las tareas, pero se volvió incómodo actualizar rápidamente la lista de tareas.
Vixie cron volvió a verificar las listas de tareas cada minuto, ya que a finales de los 80 los recursos en las máquinas Unix estándar habían aumentado significativamente:
/* первичная загрузка задач */
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 ejecución de las tareas está a cargo de la función cron_sleep, que llama a las funciones job_runqueue (iterar y ejecutar tareas) y do_command (ejecutar cada tarea individual). Vale la pena analizar esta última función con más detalle.
Ejecución de la tarea
La función do_command está escrita en buen estilo Unix, lo que significa que para la ejecución asíncrona de la tarea hace un fork. El proceso padre continúa ejecutando tareas, mientras que el hijo se ocupa de preparar el proceso de la tarea:
switch (fork()) {
case -1:
/* no se pudo realizar el fork */
break;
case 0:
/* proceso hijo: por si acaso, intentamos captar el lock principal nuevamente */
acquire_daemonlock(1);
/* procedemos a formar el proceso de la tarea */
child_process(e, u);
/* al finalizar, el proceso hijo termina su trabajo */
_exit(OK_EXIT);
break;
default:
/* proceso padre continúa el trabajo */
break;
}En child_process hay bastante lógica: acepta flujos de salida y errores estándar, que luego reenvía por correo (si en la tabla de tareas se especifica la variable de entorno MAILTO), y, finalmente, espera a que termine el proceso principal de la tarea.
El proceso de la tarea se forma con otro fork:
switch (vfork()) {
case -1:
/* si hay error, finaliza inmediatamente */
exit(ERROR_EXIT);
case 0:
/* el proceso hijo crea una nueva sesión, terminal, etc. */
(void) setsid();
/*
* aquí se lleva a cabo una configuración verbose del output del proceso, omitiremos por brevedad
*
/* cambio de directorio, usuario y grupo, es decir, el proceso ya no es superusuario
*
setgid(e->gid);
setuid(e->uid);
chdir(env_get("HOME", e->envp));
/* ejecución del comando */
{
/* la variable de entorno SHELL indica el intérprete a utilizar */
char *shell = env_get("SHELL", e->envp);
/* el proceso se inicia sin pasar el entorno del proceso padre,
* es decir, exactamente como se describe en la tabla de tareas del usuario */
execle(shell, shell, "-c", e->cmd, (char *)0, e->envp);
/* error — ¿y el proceso no se inició? finaliza la ejecución */
perror("execl");
_exit(ERROR_EXIT);
}
break;
default:
/* el proceso continúa: espera a que termine y a la salida */
break;
}Y así es como funciona cron. He omitido algunos detalles interesantes, como el manejo de usuarios remotos, pero he expuesto lo principal.
Póscrito
Cron es una sorprendentemente simple y útil aplicación, realizada en la mejor tradición del mundo Unix. No hace nada innecesario, pero su trabajo lo realiza de manera excepcional durante ya varias décadas. Familiarizarse con el código de la versión que se suministra con Ubuntu no tomó más de una hora, ¡y disfruté mucho! Espero haber podido compartir eso con ustedes.
No sé cómo a ustedes, pero me entristece un poco darme cuenta de que la programación moderna, con su tendencia a la sobrecomplicación y sobreabstracción, ya no se presta a esta simplicidad.
Existen muchas alternativas modernas a cron: los systemd-timers permiten organizar sistemas complejos con dependencias, y fcron permite regular el consumo de recursos de las tareas de manera más flexible. Pero, personalmente, siempre me ha bastado con un simple crontab.
En resumen, amen a Unix, utilicen programas simples y no olviden leer las man pages de su plataforma.
Fuente: habr.com
