cron har planlagt jobber på Linux i tiår og er fortsatt et helt utmerket verktøy. Men hvert systemd-system kjører allerede en andre planlegger, én som logger hver kjøring til journalen og kan kjøre jobber maskinen sov gjennom på nytt. Denne guiden handler om den.
Alt her er sjekket på Ubuntu 26.04 (systemd 259.5), Fedora 44 (systemd 259.8) og Arch (261.2). Timere oppfører seg identisk på alle tre på tvers av de systemd-versjonene.
Hvorfor en timer, når cron finnes
En systemd-timer er en unit-fil som sier når. Den reiser alltid sammen
med en tjeneste-unit som sier hva: foo.timer utløser foo.service,
tjenesten kjører skriptet ditt, og journalen tar vare på det hele.
Kapittelet Automatisering dekker
cron, og for en personlig skrivebordsjobb er cron ofte den kortere veien.
Timere betaler for seg når du vil ha kjøringen logget i journalctl som
alle andre systemd-operasjoner, når maskinen kan være av ved
utløsningstidspunktet, eller når jobben ikke skal kjøre før systemklokken
er satt. Ingenting hindrer deg i å bruke begge.
Anatomien til et timer-par
To filer i /etc/systemd/system/, samme grunnnavn:
# /etc/systemd/system/foo.service
[Unit]
Description=Do the thing
[Service]
Type=oneshot
ExecStart=/usr/local/bin/foo.sh
# /etc/systemd/system/foo.timer
[Unit]
Description=Do the thing on a schedule
[Timer]
OnCalendar=daily
[Install]
WantedBy=timers.target
Type=oneshot betyr at tjenesten kjører én gang og avslutter, passelig for
et skript. Paret passer sammen automatisk fordi de deler grunnnavn: systemd
leter etter foo.service ved siden av foo.timer uten en Unit=-linje.
Koblingen er streng. En bar.timer uten bar.service nekter å starte
med Refusing to start, unit bar.service to trigger not loaded.
Ser du den meldingen, er fiksen som regel en skrivefeil i ett av navnene.
OnCalendar-formatet
OnCalendar=daily er en kortform. Den lange formen skriver ut syv felt:
day-of-week, year-month-day, hour:minute:second. * betyr alle verdier,
.. spenner over et område, og et ukedagnavn kommer foran. Timeren leser
lokal systemtid, som på servere ofte er UTC.
Fordi syntaksen er tett, bør du gjøre systemd-analyze calendar til en
vane: den tolker en spesifikasjon uten å kjøre noe og viser når neste
utløsning lander:
$ systemd-analyze calendar daily
Original form: daily
Normalized form: *-*-* 00:00:00
Next elapse: Wed 2026-09-02 00:00:00 UTC
From now: 16h left
Datoene dine blir annerledes, selvsagt. Den normaliserte formen er den interessante delen: slik forsto systemd deg. Et knippe spesifikasjoner verdt å kunne:
| Det du skriver | Normalisert form | Utløses |
|---|---|---|
daily |
*-*-* 00:00:00 |
hver dag ved midnatt |
weekly |
Mon *-*-* 00:00:00 |
hver mandag ved midnatt |
Mon..Fri *-*-* 06:30:00 |
(den samme) | hverdager kl. 06:30 |
*-*-01 04:00:00 |
(den samme) | den første i måneden kl. 04:00 |
Sat *-*-* 04:00:00 |
(den samme) | hver lørdag kl. 04:00 |
En feilformet spesifikasjon fanges opp, med exit status 1, før den blir en jobb som i stillhet aldri kjører:
$ systemd-analyze calendar nonsense
Failed to parse calendar specification 'nonsense': Invalid argument
Monotone timere: å telle i stedet for å lese klokka
Alt overfor var en klokkebasert timer: den leser kalenderen. En monoton
timer teller fra en hendelse, ikke fra klokka. De vanlige alternativene
er OnActiveSec (så langt etter at timeren starter) og OnUnitActiveSec
(så langt etter at tjenesten sist kjørte).
Et hjerteslag er den klassiske bruken. Ti sekunder etter at timeren starter, og hvert tiende sekund etter hver kjøring:
# /etc/systemd/system/heartbeat.timer, plus the same
# [Unit] and [Install] sections as foo.timer above
[Timer]
OnActiveSec=10s
OnUnitActiveSec=10s
AccuracySec=1s
Den AccuracySec=1s-linjen betyr mer enn den ser ut som. Som standard
tillater systemd seg ett minutt med slakk for å pakke timerens oppvåkning
sammen med annet ventende arbeid, så en timer på ti sekunder utløses hvor
som helst fra ti til omtrent seksti sekunder for sent, og du kommer til å
bruke en hel kveld på å være overbevist om at den er ødelagt. Med accuracy
satt til ett sekund landet utløsningene omtrent elleve sekunder fra
hverandre i testingen. For daglige jobber er standarden fin.
Aktivere og starte
Legg begge filene i /etc/systemd/system/ som root. En unit systemd
aldri har sett, plukkes opp umiddelbart, ingen reload trengs. Redigerer
du en fil den alt kjenner, er det annerledes: systemd beholder den gamle
versjonen i minnet til du kjører sudo systemctl daemon-reload, og
advarer deg hvis du glemmer.
Så får systemd håndtere begge halvdelene av jobben:
$ sudo systemctl enable --now foo.timer
Created symlink '/etc/systemd/system/timers.target.wants/foo.timer' → '/etc/systemd/system/foo.timer'.
enable sørger for at timeren starter ved oppstart ved å droppe en symlenke
i timers.target.wants, og --now starter den i samme slengen. Uten
--now har du klargjort tidsplanen for fremtidige oppstarter, men latt
den ligge i dvale akkurat nå, en klassisk «hvorfor skjer det ingenting»-felle.
Symlenken kan du se i /etc/systemd/system/timers.target.wants.
To sjekker med ett ord hver bekrefter tilstanden. systemctl is-enabled foo.timer svarer enabled, og systemctl is-active foo.timer svarer
active. Angre-knappene er sudo systemctl stop foo.timer (i dvale til
neste oppstart, hvis den fortsatt er aktivert) og
sudo systemctl disable foo.timer, som svarer
Removed '/etc/systemd/system/timers.target.wants/foo.timer'.
Etter stopp rapporterer systemctl list-timers 0 timers listed., med
hintet Pass --all to see loaded but inactive timers, too.
Alt her gjelder hele systemet, som root. Timere per bruker
(systemctl --user) fortjener sin egen gjennomgang.
Se en timer utløses
systemctl list-timers er cockpitvisningen: én rad per timer, med når
den sist kjørte, når den utløses neste gang, og hvilken tjeneste den
utløser. Neste utløsningstidspunkt er det du skal kikke ekstra på når en
jobb virker sen. For én timer i detalj viser
systemctl status foo.timer neste utløsningstidspunkt.
Én ting som ser alarmerende ut, men ikke er det: etter en vellykket
kjøring viser den tilhørende foo.service seg som static i
Loaded-linjen sin og inactive mellom kjøringene. En oneshot-tjeneste
er bare «aktiv» mens skriptet kjører, noe som vil si noen sekunder.
Den egentlige kvitteringen ligger i journalen, filtrert etter unit:
$ journalctl -u foo.service
Sep 01 07:26:18 linuz-ubuntu foo.sh[3406]: foo timer fired at 07:26:18
Alt skriptet ditt skriver ut, havner i journalen med tidsstempel og PID, og
det er hele grunnen til at loggargumentet for timere holder. Bekymre deg
ikke hvis «Starting»- og «Finished»-rammelinjene skiller seg fra en venns
system. Ordlyden deres har endret seg mellom utgivelser. Kapittelet
Tjenester og logger dekker journalctl.
Opphentingskjøringer og jitter
Persistent=true i [Timer]-seksjonen tar seg av tilfellet der
maskinen var av. Uten den blir en utløsning som treffer mens maskinen
sover eller er slått av, rett og slett hoppet over. Med den noterer
systemd siste kjøring i en stempelfil under /var/lib/systemd/timers/
og kjører tjenesten så snart den kan etter en savnet utløsning. I
testingen utløste en timer med en gammel stempelfil seg innen omtrent
et sekund etter at den ble startet. Ett blindpunkt: en helt ny timer
hvis OnCalendar allerede ligger i fortiden, fyller ikke bakover ved
første aktivering. Det finnes ingen stempelfil ennå, så systemd har
ingen savnet kjøring å opphente. Opphenting trenger historikk.
RandomizedDelaySec=30 gjør det motsatte: den forsinker hver utløsning
med en tilfeldig ekstra 0 til 30 sekunder, trukket på nytt hver gang.
Én maskin vinner ingenting her, men hundre servere bedt om å kjøre
weekly ville truffet samme pakkespeil i samme sekund. Noen tilfeldige
sekunder sprer den belastningen.
Engangstimere med systemd-run
systemd-run bygger en transient timer og tjeneste for en jobb som
kjører én gang, på et valgt tidspunkt, uten unit-filer å skrive:
$ sudo systemd-run --on-active=30min --unit=oneshot-demo -- /usr/local/bin/disk-report.sh
Running timer as unit: oneshot-demo.timer
Will run service as unit: oneshot-demo.service
---skilletegnet betyr noe: alt etter det er kommandoen som skal
kjøres, argumenter inkludert, med ingenting tolket som
systemd-alternativer. Om en halvtime kjører skriptet én gang, og begge
unitene fordamper. En transient timer henger igjen i list-timers en
kort stund etter utløsning, borte godt innen ett minutt.
Avslutningsprosjekt: ukentlig rydding i scratch-mappen
Si at /tmp/scratch samler .log-filer fra diverse verktøy, og du vil
ha alt eldre enn en uke borte hver lørdag kl. 04:00, lokal tid.
Først, bevis filutvalget før noe sletter én eneste byte. find med
-mtime +7 matcher filer endret for mer enn syv dager siden (kapittelet
Filnavigering dekker find i dybden):
$ find /tmp/scratch -name '*.log' -mtime +7
/tmp/scratch/old.log
find skriver ut treff som standard, så den utdataen er tørrkjøringen.
Når listen ser riktig ut, legger du til -delete, som fjerner nøyaktig de
filene. Begge formene ble testet på samme mappe og valgte samme eneste
fil, ingenting annet rørt. Tørrkjøring først, alltid.
Nå skriptet, lagret som /usr/local/bin/scratch-cleanup.sh:
#!/bin/bash
find /tmp/scratch -name '*.log' -mtime +7 -delete
echo "scratch-cleanup ran at $(date +%T), removing logs older than 7 days"
Ekkolinjen finnes slik at journalen viser bevis for hver kjøring. Vokser et skript ut over noen linjer, tar guiden Bash — skript som holder mål over.
Tjenestehalvdelen av paret havner i /etc/systemd/system/:
# /etc/systemd/system/scratch-cleanup.service
[Unit]
Description=Clean old logs out of /tmp/scratch
[Service]
Type=oneshot
ExecStart=/usr/local/bin/scratch-cleanup.sh
En bevisst feil verdt å lære av: kjør tjenesten før du gjør skriptet kjørbart.
$ sudo systemctl start scratch-cleanup.service
Job for scratch-cleanup.service failed because the control process exited with error code.
See "systemctl status scratch-cleanup.service" and "journalctl -xeu scratch-cleanup.service" for details.
systemctl status sier bare at tjenesten feilet med resultatet
exit-code. Journalen peker på den egentlige forbrytelsen:
Failed at step EXEC spawning /usr/local/bin/scratch-cleanup.sh: Permission denied
Skriptet kjørte aldri i det hele tatt: exit code 203 med en EXEC-feil betyr at systemd ikke fikk startet filen, og «Permission denied» betyr nesten alltid en manglende execute-bit. Fiksen, og så start på nytt:
$ sudo chmod +x /usr/local/bin/scratch-cleanup.sh
$ sudo systemctl start scratch-cleanup.service
Journalen viser nå kjøringen:
Sep 01 07:36:52 linuz-ubuntu scratch-cleanup.sh[3725]: scratch-cleanup ran at 07:36:52, removing logs older than 7 days
Timerhalvdelen havner ved siden av:
# /etc/systemd/system/scratch-cleanup.timer
[Unit]
Description=Weekly cleanup of /tmp/scratch
[Timer]
OnCalendar=Sat *-*-* 04:00:00
Persistent=true
[Install]
WantedBy=timers.target
OnCalendar=Sat *-*-* 04:00:00 tolkes til neste lørdag kl. 04:00 lokal
tid, og Persistent=true låner opphentingstriksene, så en laptop som
sover på lørdagen fortsatt blir ryddet senere. Første aktivering har ikke
noe stempel ennå, så første kjøring skjer på den første levende lørdagen.
Sett den i beredskap, og test den samme dagen i stedet for å vente en uke:
$ sudo systemctl enable --now scratch-cleanup.timer
$ sudo systemctl start scratch-cleanup.service
Å starte tjenesten for hånd kjører skriptet umiddelbart og lar tidsplanen være urørt. Aktiver for fremtiden, start for et bevis.
Hvor du står nå
Hele loopen: valider en spesifikasjon med systemd-analyze calendar, skriv
et .service/.timer-par, aktiver det med --now, følg det i list-timers
og journalctl, og feilsøk en feilet start gjennom journalen. Monotone
timere, opphentingskjøringer og transiente engangstimer dekker de klossete
tilfellene cron ellers lar deg fake. Når en timer oppfører seg dårlig,
svarer validatoren og journalen til sammen på nesten hvert spørsmål.
For den komplette alternativreferansen er de offisielle sidene de du skal ha liggende åpne:
- systemd.timer(5) — alle timer-alternativene, inkludert accuracy og persistence
- systemd.time(7) — den fullstendige kalender- og tidsromsyntaksen
- ArchWiki: systemd/Timers — ferdige eksempler og vanlige mønstre