← Automatización con Scripts en Bash Bloque 8 · Tareas programadas · Tema 35 de 42

Tema 35

Temporizadores de systemd y at

Los dos temas anteriores mostraron lo que cron hace bien, que es mucho, y también sus límites: no recupera las ejecuciones perdidas, no registra cómo terminó una tarea, deja que dos ejecuciones se pisen y entrega la salida a un correo que casi nadie tiene configurado. En las distribuciones actuales, systemd ofrece otra forma de programar tareas que resuelve todo eso de fábrica: los temporizadores. Este tema los explica, junto con su pariente para las tareas que se ejecutan una sola vez, el veterano comando at.

1. Qué agregan los temporizadores

Un temporizador de systemd (timer) es una unidad de systemd que, al llegar un momento determinado, pone en marcha otra unidad, normalmente un servicio que ejecuta la tarea. Cumple el papel de una línea de crontab, pero la tarea pasa a ser un servicio más del sistema, con todo lo que systemd sabe hacer con ellos.

systemd es el programa que arranca y administra los servicios en casi todas las distribuciones de Linux actuales. Que las tareas programadas sean servicios trae, sin escribir una línea de código, varias cosas que con cron había que resolver a mano:

Problema con cronCon un temporizador
Si el equipo estaba apagado a la hora prevista, la tarea no se ejecuta (tema 33).Con Persistent=true, se ejecuta al arrancar.
La salida va por correo, o se pierde (tema 34).Va al registro del sistema, con fecha y hora, y se consulta con journalctl.
No se sabe cómo terminó la última ejecución.systemctl status muestra el código de salida, la duración y la hora.
Dos ejecuciones pueden superponerse; hace falta flock (tema 27).Si el servicio sigue en marcha, systemd no lanza otro.
Probar la tarea exactamente como la ejecutará cron es difícil (tema 34).systemctl start la ejecuta ya, en su entorno real.
Limitar el tiempo, la memoria o la prioridad exige herramientas aparte.Se declara en el servicio: TimeoutStartSec, MemoryMax, Nice.

El precio es la ceremonia: donde cron necesita una línea, un temporizador necesita dos archivos pequeños. Hacia el final del tema veremos cuándo compensa cada uno.

2. Dos unidades: servicio y temporizador

systemd administra unidades, descritas en archivos de texto con un formato parecido al de los archivos de configuración del tema 30: secciones entre corchetes y líneas Clave=valor. Una tarea programada usa dos unidades con el mismo nombre y distinta extensión:

El temporizador respaldo.timer, con OnCalendar y Persistent, decide cuándo; al vencer, pone en marcha el servicio respaldo.service, que con ExecStart y User decide qué y cómo; el servicio ejecuta el script /usr/local/bin/respaldar; la salida del script va al registro del sistema, que se consulta con journalctl respaldo.timer OnCalendar, Persistent cuándo respaldo.service ExecStart, User, límites qué y cómo /usr/local/bin/ respaldar el script de siempre el registro journalctl -u activa ejecuta salida
El temporizador decide cuándo; el servicio, qué se ejecuta y en qué condiciones. El script no cambia: es el mismo que se usaría con cron.

La separación tiene una ventaja práctica enorme: el servicio existe por sí mismo, y se puede ejecutar en cualquier momento sin esperar al temporizador. Y el script de la tarea no necesita saber nada de systemd: el mismo respaldar sirve con cron, con un temporizador o a mano.

Las unidades del sistema que escribe el administrador van en /etc/systemd/system/. Las que instalan los paquetes están en /usr/lib/systemd/system/, y no se tocan: si hace falta cambiar una, se copia o se complementa en /etc.

3. El servicio

/etc/systemd/system/respaldo.service
[Unit]
Description=Respaldo nocturno de /home

[Service]
Type=oneshot
User=backup
ExecStart=/usr/local/bin/respaldar -q --destino /mnt/copias
Nice=10
IOSchedulingClass=idle
TimeoutStartSec=2h

Type=oneshot es el tipo que corresponde a una tarea: un proceso que hace su trabajo y termina, a diferencia de un servidor web, que queda en marcha. Un servicio de tarea no lleva sección [Install], porque no se activa por sí mismo al arrancar el sistema, sino a través de su temporizador. Estas son las directivas más útiles:

DirectivaEfecto
ExecStart=El comando, con la ruta absoluta del programa. Puede haber varias líneas, que se ejecutan en orden.
User=, Group=La cuenta con que se ejecuta. Sin ellas, se ejecuta como root.
WorkingDirectory=El directorio de trabajo. Por omisión, /.
Environment=, EnvironmentFile=Variables para la tarea, escritas aquí o leídas de un archivo.
TimeoutStartSec=El tiempo máximo; si se supera, systemd detiene la tarea y la marca como fallida. En un oneshot, por omisión no hay límite.
Nice=, IOSchedulingClass=idleMenos prioridad de procesador y de disco, para no molestar a los demás.
MemoryMax=, CPUQuota=Límites de memoria y de procesador.
ExecStart no es una línea de shell

systemd separa el comando en argumentos con sus propias reglas de comillas y lo ejecuta directamente, sin pasar por ninguna shell. No hay redirecciones, tuberías, && ni comodines: ExecStart=/usr/bin/tar czf /tmp/x.tgz /home > /tmp/log le pasa a tar un argumento >. Además, % introduce un especificador, como %n, el nombre de la unidad, y $ una variable, de modo que un % o un $ literales se escriben %% y $$. Si hace falta lógica de shell, lo correcto es un script; como recurso, ExecStart=/bin/bash -c '…'.

Antes de crear el temporizador, el servicio ya se puede probar. Después de escribir o cambiar un archivo de unidad, systemctl daemon-reload hace que systemd lo vuelva a leer:

terminal · ejecutar la tarea ahora, como la ejecutará el temporizador
$ sudo systemctl daemon-reload
$ sudo systemctl start respaldo.service              # espera a que termine
$ systemctl status respaldo.service
○ respaldo.service - Respaldo nocturno de /home
     Loaded: loaded (/etc/systemd/system/respaldo.service; static)
     Active: inactive (dead) since Thu 2026-10-15 11:02:14 -03; 5s ago
    Process: 53120 ExecStart=/usr/local/bin/respaldar -q --destino /mnt/copias (code=exited, status=0/SUCCESS)
   Main PID: 53120 (code=exited, status=0/SUCCESS)
        CPU: 1.912s
$ journalctl -u respaldo.service -n 4
oct 15 11:02:01 servidor systemd[1]: Starting respaldo.service - Respaldo nocturno de /home...
oct 15 11:02:14 servidor respaldar[53120]: Respaldo completo: 1284 archivos, 412 MiB
oct 15 11:02:14 servidor systemd[1]: respaldo.service: Deactivated successfully.
oct 15 11:02:14 servidor systemd[1]: Finished respaldo.service - Respaldo nocturno de /home.

Esto es lo que en el tema 34 costaba tanto: la tarea corrió con su usuario, su entorno y sus límites reales, y su salida quedó registrada, con el nombre del programa y su número de proceso. static indica que la unidad no se habilita por sí misma, lo esperado en un servicio que activa un temporizador. El entorno de un servicio también es reducido, y no lee ~/.bashrc, pero su PATH incluye /usr/local/bin y los directorios sbin, y no hay sorpresas: lo que funciona con systemctl start funciona igual con el temporizador.

4. El temporizador

/etc/systemd/system/respaldo.timer
[Unit]
Description=Respaldo nocturno de /home, de lunes a viernes

[Timer]
OnCalendar=Mon..Fri 02:30
Persistent=true
RandomizedDelaySec=10min

[Install]
WantedBy=timers.target

Por omisión, un temporizador activa el servicio de su mismo nombre: respaldo.timer activa respaldo.service. La sección [Install] sí es necesaria aquí: WantedBy=timers.target hace que, una vez habilitado, el temporizador se ponga en marcha en cada arranque.

DirectivaEfecto
OnCalendar=Los momentos en que vence, con la sintaxis de la sección siguiente. Puede repetirse.
Persistent=trueRecuerda la última ejecución en el disco; si al arrancar se perdió alguna mientras el equipo estaba apagado, la ejecuta enseguida. Solo tiene efecto con OnCalendar.
RandomizedDelaySec=Agrega un retraso al azar, hasta el valor indicado, para que muchas máquinas no lancen la tarea en el mismo segundo.
AccuracySec=Cuánto puede retrasarse el vencimiento para agruparlo con otros y ahorrar energía. Por omisión, un minuto; 1s si importa la precisión.
Unit=Activar un servicio con otro nombre.

Para que el temporizador funcione hay que habilitarlo, para que arranque con el sistema, y ponerlo en marcha ahora. enable --now hace las dos cosas. Se habilita el temporizador, no el servicio:

terminal
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now respaldo.timer
Created symlink /etc/systemd/system/timers.target.wants/respaldo.timer → /etc/systemd/system/respaldo.timer.
$ systemctl list-timers respaldo.timer
NEXT                        LEFT LAST PASSED UNIT           ACTIVATES
Fri 2026-10-16 02:34:41 -03  15h -    -      respaldo.timer respaldo.service

1 timers listed.

La próxima ejecución es el viernes a las 2:34:41: las 2:30 más el retraso al azar de RandomizedDelaySec. Las columnas LAST y PASSED mostrarán la última ejecución cuando la haya.

5. Las expresiones de OnCalendar

La forma completa de una expresión es DíaDeLaSemana Año-Mes-Día Hora:Minuto:Segundo, y cada parte se puede omitir: sin día de la semana, vale cualquiera; sin fecha, todos los días; sin segundos, el segundo 0. Dentro de cada parte se usan * para cualquier valor, comas para las listas, .. para los rangos y / para los pasos.

OnCalendarcronSe ejecuta
*:0/5*/5 * * * *Cada 5 minutos.
hourly0 * * * *Al comienzo de cada hora.
02:3030 2 * * *Todos los días a las 2:30.
*-*-* 08,20:000 8,20 * * *A las 8:00 y a las 20:00.
Mon..Fri 02:3030 2 * * 1-5De lunes a viernes a las 2:30.
Sat,Sun 10:000 10 * * 6,0Sábados y domingos a las 10.
*-*-01 08:000 8 1 * *El día 1 de cada mes a las 8.
*-01/3-01 03:000 3 1 */3 *El 1 de enero, abril, julio y octubre.
Mon *-*-01..07 09:00—El primer lunes de cada mes.
*-*~01 23:00—El último día de cada mes.
daily, weekly, monthly@daily…A medianoche: cada día, cada lunes, cada día 1.

Hay dos diferencias con cron que conviene fijar. La primera resuelve el problema de la sección 6 del tema 33: en systemd, el día de la semana y la fecha se combinan con «y», como el resto, de modo que Mon *-*-01..07 es exactamente «un lunes entre el 1 y el 7», y Fri *-*-13, los viernes 13; la tilde cuenta los días desde el final del mes, de modo que ~01 es el último. La segunda es una trampa: weekly vence los lunes, mientras que @weekly de cron, los domingos.

No hace falta adivinar si una expresión dice lo que uno cree: systemd-analyze calendar la interpreta, la muestra en su forma completa y calcula los próximos vencimientos.

terminal · comprobar una expresión
$ systemd-analyze calendar 'Mon..Fri 02:30'
  Original form: Mon..Fri 02:30
Normalized form: Mon..Fri *-*-* 02:30:00
    Next elapse: Fri 2026-10-16 02:30:00 -03
       (in UTC): Fri 2026-10-16 05:30:00 UTC
       From now: 15h left
$ systemd-analyze calendar --iterations=3 'Mon *-*-01..07 09:00'
  Original form: Mon *-*-01..07 09:00
Normalized form: Mon *-*-01..07 09:00:00
    Next elapse: Mon 2026-11-02 09:00:00 -03
       …
   Iteration #2: Mon 2026-12-07 09:00:00 -03
       …
   Iteration #3: Mon 2027-01-04 09:00:00 -03
       …
$ systemd-analyze calendar '*/5 * * * *'
Failed to parse calendar specification '*/5 * * * *': Invalid argument

La última línea muestra el error más común al pasar de cron a systemd: copiar la expresión de cron, que systemd no entiende. Al final de una expresión puede ir además una zona horaria, como 02:30 UTC, que evita las sorpresas del horario de verano que vimos en el tema 33.

6. Temporizadores relativos

Además de las fechas del calendario, un temporizador puede vencer un tiempo después de un acontecimiento. Son los temporizadores monotónicos, que no dependen del reloj sino del tiempo transcurrido:

DirectivaVence
OnBootSec=Un tiempo después del arranque del sistema.
OnActiveSec=Un tiempo después de que se activó el propio temporizador.
OnUnitActiveSec=Un tiempo después de la última vez que empezó el servicio.
OnUnitInactiveSec=Un tiempo después de la última vez que terminó el servicio.
sincronizar.timer · cada 10 minutos, contados desde el final
[Timer]
OnBootSec=2min
OnUnitInactiveSec=10min

[Install]
WantedBy=timers.target

Esta combinación expresa algo que cron no puede: «dos minutos después de arrancar, y luego diez minutos después de que termine cada ejecución». Si una sincronización tarda doce minutos, la siguiente empieza diez minutos después de que termine, nunca encima. Los tiempos se escriben con unidades, como 30s, 10min, 2h o 1d, y se pueden combinar: 1h 30min.

7. Vigilar y avisar de los fallos

Una vez instalados, los temporizadores se vigilan con unos pocos comandos:

ComandoMuestra
systemctl list-timersTodos los temporizadores activos, con su próximo y su último vencimiento. --all incluye los inactivos.
systemctl status respaldo.serviceCómo terminó la última ejecución, con su código y su duración.
journalctl -u respaldo.serviceLa salida de todas las ejecuciones; --since yesterday, -n 50 o -f la acotan o la siguen en vivo.
systemctl --failedLas unidades cuya última ejecución falló.
sudo systemctl reset-failed respaldo.serviceBorra la marca de fallo, una vez resuelto el problema.

Si la tarea termina con un código distinto de cero, el servicio queda marcado como failed. Para enterarse sin tener que mirar, la directiva OnFailure= pone en marcha otra unidad cuando la tarea falla. Lo habitual es escribir una sola unidad de aviso, en forma de plantilla, que sirva para todas las tareas:

/etc/systemd/system/aviso-fallo@.service
[Unit]
Description=Aviso de fallo de %i

[Service]
Type=oneshot
ExecStart=/usr/local/bin/avisar "Falló %i en %H"
en respaldo.service, sección [Unit]
OnFailure=aviso-fallo@%n.service

El @ del nombre indica una plantilla: aviso-fallo@respaldo.service.service es una instancia de ella, en la que %i vale lo que va entre la arroba y la extensión. En respaldo.service, %n es su propio nombre, y en el aviso %H es el nombre del equipo. avisar es un script propio, que puede enviar un correo, un mensaje a un servicio de chat o lo que convenga, y que sirve a todas las tareas a la vez. Son los especificadores con % que obligaban a escribir %% en ExecStart.

8. Temporizadores de usuario

Como cada usuario tiene su crontab, cada usuario tiene también su propio systemd, que administra sus unidades sin necesitar root. Funcionan igual, con tres diferencias: los archivos van en ~/.config/systemd/user/, todos los comandos llevan --user, y se ejecutan siempre con la cuenta del usuario, de modo que no llevan User=.

terminal · un temporizador personal
$ ls ~/.config/systemd/user/
limpiar-descargas.service  limpiar-descargas.timer
$ systemctl --user daemon-reload
$ systemctl --user enable --now limpiar-descargas.timer
$ journalctl --user -u limpiar-descargas.service
$ sudo loginctl enable-linger ana                 # que funcione sin sesión abierta

La última línea resuelve una sorpresa: el systemd de un usuario se pone en marcha cuando inicia sesión y se detiene cuando cierra la última, y con él sus temporizadores. En un equipo personal, donde uno está conectado casi siempre, puede alcanzar; en un servidor, donde nadie tiene una sesión abierta a las 2:30, las tareas no correrían. loginctl enable-linger mantiene el systemd del usuario en marcha desde el arranque, con o sin sesión.

9. Temporizadores sin archivos: systemd-run

Para una tarea ocasional, escribir dos archivos es demasiado. systemd-run crea un servicio y un temporizador transitorios, que existen hasta que la tarea se ejecuta o hasta el próximo reinicio, y que tienen las mismas ventajas: registro, estado y límites.

terminal
$ systemd-run --user --on-active=2h /home/ana/bin/recordatorio "Llamar al banco"
Running timer as unit: run-r8c1f0b5e2d4a4f9b.timer
Will run service as unit: run-r8c1f0b5e2d4a4f9b.service
$ systemd-run --user --on-calendar='2026-10-20 09:00' /home/ana/bin/informe-mensual
$ systemctl --user list-timers 'run-*'
$ systemd-run --user --wait --pipe -p MemoryMax=1G /home/ana/bin/procesar-videos

Sin opciones de tiempo, systemd-run ejecuta el comando enseguida como un servicio, lo que también es útil: la última línea ejecuta un proceso con un límite de memoria, espera a que termine y muestra su salida en la terminal. Con root y sin --user, crea unidades del sistema.

10. cron o systemd

Ninguna de las dos herramientas reemplaza del todo a la otra, y en la práctica conviven:

AspectocronTemporizadores de systemd
Escribir una tareaUna línea.Dos archivos.
DisponibilidadCualquier Unix, contenedores, sistemas mínimos.Solo sistemas con systemd.
Expresiones de tiempoUniversales, pero con la regla del «o» y sin segundos.Más expresivas, y comprobables con systemd-analyze.
Ejecuciones perdidasNo se recuperan.Persistent=true.
Salida y estadoCorreo; el código se pierde.En el registro, con el código y la duración.
SuperposiciónHace falta flock.Evitada de fábrica.
Límites de recursosHerramientas aparte.Directivas del servicio.
Probar la tareaReproducir el entorno a mano.systemctl start.

Una regla razonable: para una tarea personal rápida o un sistema sin systemd, cron; para tareas de servidor que importan, como respaldos, sincronizaciones o limpiezas, de las que hay que saber si fallaron, un temporizador. Los scripts son los mismos en los dos casos, y pasar de uno a otro es cuestión de minutos.

11. Tareas de una sola vez: at

cron y los temporizadores repiten. Para ejecutar algo una sola vez en el futuro, como reiniciar un servicio esta noche, lanzar una compilación larga cuando termine la jornada o recordarse algo dentro de una hora, existe at. No siempre viene instalado: está en el paquete at, y necesita su servicio, atd, en marcha.

at recibe un momento como argumento, y los comandos por la entrada estándar, interactivamente o por una tubería:

terminal
$ at 23:00
warning: commands will be executed using /bin/sh
at> /home/ana/bin/compilar-todo > /tmp/compilacion.log 2>&1
at> <EOT>                                        # Ctrl+D para terminar
job 7 at Thu Oct 15 23:00:00 2026
$ echo '/home/ana/bin/recordatorio "Llamar al banco"' | at now + 1 hour
job 8 at Thu Oct 15 12:15:00 2026
$ at -f /home/ana/bin/reiniciar-app 03:00 tomorrow
job 9 at Fri Oct 16 03:00:00 2026
$ atq
7	Thu Oct 15 23:00:00 2026 a ana
8	Thu Oct 15 12:15:00 2026 a ana
9	Fri Oct 16 03:00:00 2026 a ana
$ atrm 8
FormaEjemplo
Una hora, hoy o mañana si ya pasóat 23:00, at 6pm
Palabrasat noon, at midnight, at now
Desde ahoraat now + 30 minutes, at now + 2 hours, at now + 3 days
Una fechaat 09:00 2026-10-20, at 09:00 tomorrow, at 10:00 next monday

Los comandos complementarios son atq, que lista las tareas pendientes, atrm, que borra una por su número, y at -c número, que muestra el script completo que se ejecutará. Ese script revela la diferencia más importante con cron: at guarda el entorno y el directorio de trabajo del momento en que se programó la tarea, y los restaura al ejecutarla. Lo que funciona en tu terminal tiene muchas más posibilidades de funcionar con at que con cron.

terminal · lo que guarda at
$ at -c 7
#!/bin/sh
# atrun uid=1000 gid=1000
…
PATH=/home/ana/bin:/usr/local/bin:/usr/bin:/bin; export PATH
LANG=es_AR.UTF-8; export LANG
…
cd /home/ana/proyectos/app || {
	 echo 'Execution directory inaccessible' >&2
	 exit 1
}
/home/ana/bin/compilar-todo > /tmp/compilacion.log 2>&1

Igual que cron, at usa /bin/sh y envía la salida por correo, que sin servicio de correo se pierde; por eso el primer ejemplo la redirige. Su pariente batch recibe los comandos de la misma forma, pero no los ejecuta a una hora sino cuando la carga del sistema baja, útil para trabajos pesados que pueden esperar. Y la alternativa con systemd es la de la sección 9: systemd-run --on-active=1h o --on-calendar, con la salida en el registro.

12. Un script completo

En el tema 33, cron-tarea agregaba y quitaba líneas del crontab sin duplicarlas. Este script hace lo mismo con temporizadores de usuario: escribe la pareja de unidades, las habilita y las quita, y deja todo como estaba si se ejecuta dos veces con los mismos datos. Escribe los dos archivos de cada tarea a partir de un nombre, un calendario y un comando, y resuelve los dos detalles más incómodos del formato: las comillas y los caracteres especiales de ExecStart.

timer-tarea
#!/usr/bin/env bash
#
# timer-tarea — crea, cambia y quita temporizadores de systemd del usuario:
# el equivalente de cron-tarea (tema 33) para systemd.
#
# Uso: timer-tarea poner NOMBRE 'CALENDARIO' PROGRAMA [ARGUMENTO...]
#      timer-tarea quitar NOMBRE
#      timer-tarea listar
#
set -euo pipefail

prog="${0##*/}"
dir="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"

uso() {
  echo "Uso: $prog poner NOMBRE 'CALENDARIO' PROGRAMA [ARGUMENTO...]" >&2
  echo "     $prog quitar NOMBRE" >&2
  echo "     $prog listar" >&2
  exit 2
}
error() { echo "$prog: $*" >&2; exit 1; }

escapar() {                           # un argumento, entre comillas y con los escapes de systemd
  local a="$1"
  a="${a//'\'/'\\'}"
  a="${a//'"'/'\"'}"
  a="${a//'%'/'%%'}"                  # % introduce un especificador
  a="${a//'$'/'$$'}"                  # $ introduce una variable
  printf '"%s"' "$a"
}

actualizar() {                        # escribe $2 en el archivo $1, solo si cambia
  local archivo="$1" contenido="$2"
  if [[ -f "$archivo" && "$(< "$archivo")" == "$contenido" ]]; then
    return 1                          # ya estaba así
  fi
  printf '%s\n' "$contenido" > "$archivo.tmp"
  mv -f -- "$archivo.tmp" "$archivo"
}

(( $# >= 1 )) || uso
accion="$1"; shift

case "$accion" in
  poner)
    (( $# >= 3 )) || uso
    nombre="$1" calendario="$2"; shift 2
    [[ "$nombre" =~ ^[a-z0-9][a-z0-9-]*$ ]] || error "nombre no válido: $nombre"
    [[ "$calendario" != *$'\n'* ]] && systemd-analyze calendar "$calendario" &> /dev/null ||
      error "calendario no válido: $calendario"
    programa=$(command -v -- "$1") && [[ "$programa" == /* ]] ||
      error "no se encuentra el programa: $1"
    shift

    linea=$(escapar "$programa")
    for argumento in "$@"; do
      [[ "$argumento" != *$'\n'* ]] || error "un argumento no puede tener saltos de línea"
      linea+=" $(escapar "$argumento")"
    done

    unidad="tarea-$nombre"
    mkdir -p "$dir"
    cambios=0
    actualizar "$dir/$unidad.service" "[Unit]
Description=Tarea $nombre (timer-tarea)

[Service]
Type=oneshot
ExecStart=$linea" && cambios=1
    actualizar "$dir/$unidad.timer" "[Unit]
Description=Temporizador de la tarea $nombre (timer-tarea)

[Timer]
OnCalendar=$calendario
Persistent=true

[Install]
WantedBy=timers.target" && cambios=1

    if (( cambios )); then
      systemctl --user daemon-reload
      systemctl --user enable --quiet "$unidad.timer"
      systemctl --user restart "$unidad.timer"
      echo "Tarea $nombre programada." >&2
    else
      systemctl --user enable --quiet --now "$unidad.timer"
      echo "La tarea $nombre ya estaba programada así; no hay cambios." >&2
    fi

    usuario="${USER:-$(id -un)}"
    if [[ "$(loginctl show-user "$usuario" --property=Linger --value 2> /dev/null)" != yes ]]; then
      echo "Aviso: la tarea solo correrá mientras tengas una sesión abierta;" >&2
      echo "       para evitarlo: sudo loginctl enable-linger $usuario" >&2
    fi
    systemctl --user list-timers --no-pager "$unidad.timer"
    ;;
  quitar)
    (( $# == 1 )) || uso
    unidad="tarea-$1"
    if [[ ! -e "$dir/$unidad.timer" && ! -e "$dir/$unidad.service" ]]; then
      echo "No había ninguna tarea llamada $1; no hay cambios." >&2
      exit 0
    fi
    systemctl --user disable --quiet --now "$unidad.timer" 2> /dev/null || true
    rm -f -- "$dir/$unidad.timer" "$dir/$unidad.service"
    systemctl --user daemon-reload
    echo "Tarea $1 quitada." >&2
    ;;
  listar)
    systemctl --user list-timers --all --no-pager 'tarea-*'
    ;;
  *) uso ;;
esac
terminal
$ timer-tarea poner respaldo 'Mon..Fri 02:30' respaldar -q --destino '/mnt/copias/casa de ana'
Tarea respaldo programada.
Aviso: la tarea solo correrá mientras tengas una sesión abierta;
       para evitarlo: sudo loginctl enable-linger ana
NEXT                        LEFT LAST PASSED UNIT                 ACTIVATES
Fri 2026-10-16 02:30:00 -03  15h -    -      tarea-respaldo.timer tarea-respaldo.service

1 timers listed.
$ sudo loginctl enable-linger ana
$ timer-tarea poner respaldo 'Mon..Fri 02:30' respaldar -q --destino '/mnt/copias/casa de ana'
La tarea respaldo ya estaba programada así; no hay cambios.
NEXT                        LEFT LAST PASSED UNIT                 ACTIVATES
Fri 2026-10-16 02:30:00 -03  15h -    -      tarea-respaldo.timer tarea-respaldo.service

1 timers listed.
$ cat ~/.config/systemd/user/tarea-respaldo.service
[Unit]
Description=Tarea respaldo (timer-tarea)

[Service]
Type=oneshot
ExecStart="/home/ana/bin/respaldar" "-q" "--destino" "/mnt/copias/casa de ana"
$ timer-tarea poner limpiar '*/5 * * * *' limpiar-temporales
timer-tarea: calendario no válido: */5 * * * *
$ systemctl --user start tarea-respaldo.service    # probarla ya
$ journalctl --user -u tarea-respaldo.service -n 1 -o cat
Finished tarea-respaldo.service - Tarea respaldo (timer-tarea).
$ timer-tarea quitar respaldo
Tarea respaldo quitada.

13. Errores frecuentes

EscribisteQué pasaCorrección
Cambiar un archivo de unidad y nada mássystemd sigue usando la versión anterior, y lo advierte en status.systemctl daemon-reload
systemctl enable respaldo.timerQueda habilitado para el próximo arranque, pero no corre hasta entonces.enable --now
systemctl enable respaldo.serviceSe intentó habilitar el servicio en lugar del temporizador: como no tiene [Install], systemd lo advierte y no habilita nada.Habilitar el .timer.
OnCalendar=*/5 * * * *Es sintaxis de cron: el temporizador no carga.OnCalendar=*:0/5, comprobado con systemd-analyze calendar.
OnCalendar=weekly esperando el domingoEn systemd, weekly es el lunes.OnCalendar=Sun 00:00
ExecStart=… > /tmp/log o con |No hay shell: > y | llegan como argumentos.Un script, o /bin/bash -c '…'.
date +%F en ExecStart%F es un especificador de systemd.%%F, o llevarlo a un script.
Un temporizador de usuario en un servidorSolo corre mientras el usuario tiene una sesión abierta.sudo loginctl enable-linger usuario
Persistent=true con OnBootSecNo tiene efecto: solo vale con OnCalendar.Usar OnCalendar si hay que recuperar ejecuciones.
at 23:00 sin atd en marchaCan't open /var/run/atd.pid to signal atd. No atd running?Instalar at y systemctl enable --now atd.
Una tarea de at sin redirigir la salidaSin correo configurado, la salida se pierde.> archivo 2>&1, o systemd-run --on-active.

14. Resumen

15. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Por qué una tarea programada con systemd usa dos unidades y no una?

Porque separan dos responsabilidades: el temporizador decide cuándo, y el servicio, qué se ejecuta y en qué condiciones. Así el servicio puede ejecutarse a mano con systemctl start, exactamente como lo hará el temporizador.

2. ¿Qué tipo de servicio corresponde a una tarea programada, y por qué no lleva sección [Install]?

Type=oneshot, porque la tarea hace su trabajo y termina. No lleva [Install] porque no se habilita por sí misma: la pone en marcha el temporizador, que es el que se habilita.

3. ¿Qué hace Persistent=true?

Hace que systemd recuerde en el disco la última ejecución. Si al arrancar descubre que se perdió alguna mientras el equipo estaba apagado, ejecuta la tarea enseguida. Solo funciona con OnCalendar.

4. ¿Por qué ExecStart=/usr/bin/echo hola > /tmp/saludo no crea el archivo?

Porque systemd ejecuta el comando directamente, sin una shell que interprete la redirección: echo recibe los argumentos hola, > y /tmp/saludo, y los escribe en el registro.

5. ¿Cómo se escribe en OnCalendar «el primer lunes de cada mes a las 9», y por qué en cron no es tan simple?

Mon *-*-01..07 09:00. En systemd, el día de la semana y la fecha se combinan con «y»; en cron, si los dos campos tienen valores, basta con que coincida uno, de modo que hay que agregar una condición en el comando.

6. ¿Cómo se comprueba una expresión de OnCalendar antes de usarla?

Con systemd-analyze calendar 'expresión', que la muestra en su forma completa y calcula el próximo vencimiento; con --iterations=N, los N próximos. Si la expresión no es válida, lo indica.

7. ¿Qué diferencia hay entre OnUnitActiveSec=10min y OnUnitInactiveSec=10min?

La primera cuenta los 10 minutos desde que empezó la última ejecución; la segunda, desde que terminó. Con la segunda, una ejecución larga nunca deja menos de 10 minutos de descanso hasta la siguiente.

8. Un temporizador de usuario funciona en tu portátil pero no en el servidor. ¿Cuál es la causa más probable?

Que el systemd del usuario solo está en marcha mientras tiene una sesión abierta, y en el servidor nadie está conectado a la hora de la tarea. Se resuelve con sudo loginctl enable-linger usuario.

9. ¿En qué se diferencia el entorno de una tarea de at del de una de cron?

at guarda las variables de entorno y el directorio de trabajo del momento en que se programó la tarea, y los restaura al ejecutarla. Cron arranca siempre con un entorno mínimo y en el directorio personal.

10. ¿Por qué timer-tarea escribe %% y $$ en ExecStart?

Porque en las unidades de systemd % introduce un especificador, como %n, y $ una variable. Duplicarlos hace que el programa reciba el carácter literal que el usuario escribió.

Para practicar

  1. Convierte a OnCalendar las expresiones de cron que escribiste en el tema 33, y comprueba cada una con systemd-analyze calendar --iterations=3.
  2. Crea un temporizador de usuario que cada 15 minutos escriba la fecha en un archivo. Ejecuta el servicio a mano con systemctl --user start, revisa journalctl --user -u y list-timers, y luego quítalo.
  3. Haz que la tarea anterior falle a propósito, con exit 1, y observa cómo aparece en systemctl --user status y en systemctl --user --failed.
  4. Escribe una plantilla aviso-fallo@.service que registre el fallo con logger, y conéctala a la tarea con OnFailure=.
  5. Programa con at un comando para dentro de dos minutos, mira el script que guardó con at -c, y compara el resultado con el mismo comando programado con systemd-run --user --on-active=2min.
  6. Agrega a timer-tarea una opción -r MINUTOS que escriba RandomizedDelaySec en el temporizador, y otra -t SEGUNDOS que escriba TimeoutStartSec en el servicio.