På denne siden

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: