
Klassikeren skrev, at tilfredse timer ikke observeres. I de vilde tider var der hverken programmører eller Unix, men i dag ved programmører med sikkerhed: I stedet vil cron holde øje med tiden for dem.
Kommandolinje-værktøjer er både en svaghed og en rutine for mig. sed, awk, wc, cut og andre gamle programmer køres af scripts på vores servere dagligt. Mange af dem er indstillet som opgaver for cron, planlæggeren fra 70'erne.
Jeg brugte længe cron overfladisk uden at dykke ned i detaljerne, men en dag, da jeg stødte på en fejl ved kørsel af et script, besluttede jeg mig for at forstå det grundigt. Sådan opstod denne artikel, hvor jeg satte mig ind i POSIX crontab, de vigtigste versioner af cron i populære Linux-distroer og opbygningen af nogle af dem.
Bruger du Linux og kører opgaver i cron? Er du interesseret i arkitekturen af systemapplikationer i Unix? Så er vi på samme kurs!
Indhold
Arternes oprindelse
Regelmæssig udførelse af bruger- eller systemprogrammer er en åbenlys nødvendighed i alle operativsystemer. Derfor har programmører længe været klar over behovet for tjenester, der muliggør centraliseret planlægning og udførelse af opgaver.
Unix-lignende operativsystemer stammer fra Version 7 Unix, der blev udviklet i 70'erne i Bell Labs, blandt andet af den berømte Ken Thompson. Sammen med Version 7 Unix blev cron også leveret, en tjeneste til regelmæssig udførelse af opgaver for superbrugeren.
En typisk moderne cron er et simpelt program, men algoritmen for den originale version var endnu enklere: tjenesten vågnede en gang i minuttet, læste en opgaveliste fra en enkelt fil (/etc/lib/crontab) og udførte de opgaver for superbrugeren, som skulle udføres i det aktuelle minut.
Senere blev forbedrede versioner af denne simple og nyttige tjeneste leveret med alle Unix-lignende operativsystemer.
Generelle beskrivelser af formatet crontab og de grundlæggende principper for driften af værktøjet blev i 1992 inkluderet i den primære standard for Unix-lignende operativsystemer — POSIX — og således blev cron fra de facto standard til de jure standard.
I 1987 udgav Paul Vixie, efter at han havde spurgt Unix-brugere om deres ønsker til cron, endnu en version af dæmonen, der løste nogle problemer med de traditionelle cron og udvidede syntaksen for tabel-filer.
Til den tredje version af Vixie cron overholdt den POSIX-kravene, og programmet havde en liberalt licens, eller rettere sagt, der var slet ingen licens, bortset fra ønskerne i README: forfatteren giver ingen garantier, forfatterens navn må ikke fjernes, og programmet må kun sælges sammen med kildekoden. Disse krav viste sig at være kompatible med principperne for den frit tilgængelige software, som i de år blev mere og mere populær, derfor tog nogle af de centrale Linux-distributioner, der dukkede op i begyndelsen af 90'erne, Vixie cron som systemprogram og har udviklet det lige siden.
Specifikt udvikler Red Hat og SUSE en fork af Vixie cron - cronie, mens Debian og Ubuntu bruger den originale udgave af Vixie cron med mange patches.
Lad os først se på den brugerværktøj, der er beskrevet i POSIX, crontab, hvorefter vi vil analysere syntaksudvidelserne præsenteret i Vixie cron og brugen af variationer af Vixie cron i populære Linux-distributioner. Og til sidst, kirsebær på toppen - vi går i dybden med strukturen af cron-dæmonen.
POSIX crontab
Mens den originale cron altid arbejdede for superbrugeren, har moderne planlæggere ofte med opgaver fra almindelige brugere at gøre, hvilket er mere sikkert og praktisk.
Cron'er leveres som et sæt af to programmer: en konstant kørende daemon cron og et brugbart værktøj crontab. Sidstnævnte gør det muligt at redigere opgavetabeller, der er specifikke for hver bruger i systemet, mens dæmonen kører opgaver fra både bruger- og systemtabeller.
I beskriver ikke dæmonens adfærd, og kun den brugerdefinerede program er formaliseret . Eksistensen af mekanismer til at starte brugeropgaver er selvfølgelig impliceret, men ikke beskrevet i detaljer.
Ved at kalde crontab-værktøjet kan man gøre fire ting: redigere den brugerdefinerede opgavetabel i en teksteditor, uploade tabellen fra en fil, vise den nuværende opgavetabel og rydde opgavetabellen. Eksempler på anvendelse af crontab-værktøjet:
crontab -e # redigere opgavetabel
crontab -l # vise opgavetabel
crontab -r # slette opgavetabel
crontab path/to/file.crontab # uploade opgavetabel fra filVed kald af crontab -e vil blive brugt en editor, der er angivet i den standard miljøvariabel EDITOR.
Opgaverne er beskrevet i følgende format:
# строки-комментарии игнорируются
#
# задача, выполняемая ежеминутно
* * * * * /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.logDe første fem felter i posterne: minutter [1..60], timer [0..23], dage i måneden [1..31], måneder [1..12], ugedage [0..6], hvor 0 er søndag. Det sjette felt er en streng, der vil blive udført af den standard kommandotolk.
I de første fem felter kan værdierne adskilles med komma:
# задача, выполняемая в первую и десятую минуты каждого часа
1,10 * * * * /path/to/exec -a -b -cEller med bindestreg:
# задача, выполняемая в каждую из первых десяти минут каждого часа
0-9 * * * * /path/to/exec -a -b -cBrugeres adgang til planlægning af opgaver reguleres i POSIX-filerne cron.allow og cron.deny, hvor henholdsvis de brugere med adgang til crontab og brugere uden adgang til programmet opregnes. Placeringen af disse filer er ikke standardiseret.
De programmer, der skal køres, skal ifølge standarden have mindst fire miljøvariabler:
- HOME — brugerens hjemmemappe.
- LOGNAME — brugerens login.
- PATH — stien, hvor standard systemværktøjer kan findes.
- SHELL — stien til den anvendte kommandotolk.
Det er bemærkelsesværdigt, at POSIX ikke angiver, hvor værdierne for disse variabler kommer fra.
Bedst sælgende — Vixie cron 3.0pl1
Den fælles forfader til de populære variationer af cron er Vixie cron 3.0pl1, som blev præsenteret i mailinglisten comp.sources.unix i 1992. Vi vil se nærmere på de vigtigste funktioner i denne version.
Vixie cron leveres i to programmer (cron og crontab). Som sædvanligt er dæmonen ansvarlig for at læse og køre opgaver fra systemets opgaveliste og individuelle brugeres opgavelister, mens værktøjet crontab er ansvarligt for at redigere brugerens lister.
Opgaveliste og konfigurationsfiler
Superbrugerens opgaveliste er placeret i /etc/crontab. Syntaxen for systemets tabel svarer til syntaxen for Vixie cron med den bemærkning, at den sjette kolonne angiver brugernavnet, under hvilket opgaven køres:
# Запускается ежеминутно от пользователя vlad
* * * * * vlad /path/to/execOpgavelisterne for almindelige brugere ligger i /var/cron/tabs/brugernavn og bruger en fælles syntax. Når værktøjet crontab køres som bruger, redigeres netop disse filer.
Håndtering af lister over brugere, der har adgang til crontab, sker i filerne /var/cron/allow og /var/cron/deny, hvor det er tilstrækkeligt at tilføje brugernavnet som en enkelt linje.
Udvidet syntax
Sammenlignet med POSIX crontab indeholder Paul Vixies løsning flere meget nyttige modifikationer af syntaxen i opgavelisterne for værktøjet.
Den nye syntaks for tabeller er blevet tilgængelig: for eksempel kan du angive ugedage eller måneder ved navn (Mon, Tue osv.):
# Запускается ежеминутно по понедельникам и вторникам в январе
* * * Jan Mon,Tue /path/to/execDet er muligt at angive et interval, hvormed opgaver kører:
# Запускается с шагом в две минуты
*/2 * * * Mon,Tue /path/to/execIntervaller og trin kan blandes:
# Запускается с шагом в две минуты в первых десять минут каждого часа
0-10/2 * * * * /path/to/execIntuitive alternativer til den normale syntaks understøttes (reboot, yearly, annually, monthly, weekly, daily, midnight, hourly):
# Запускается после перезагрузки системы
@reboot /exec/on/reboot
# Запускается раз в день
@daily /exec/daily
# Запускается раз в час
@hourly /exec/dailyOpgaveeksekveringsmiljø
Vixie cron tillader ændring af miljøet for de kørte applikationer.
Miljøvariablerne USER, LOGNAME og HOME leveres ikke blot af dæmonen, men tages fra filen . Variablen PATH får værdien "/usr/bin:/bin", og SHELL får "\/bin\/sh". Værdierne for alle variabler, undtagen LOGNAME, kan ændres i bruger-tabellerne.
Nogle miljøvariabler (først og fremmest SHELL og HOME) bruges af cron selv til at køre opgaver. Her er et eksempel på, hvordan man kan bruge bash i stedet for den standard sh til at køre brugeropgaver:
SHELL=\/bin\/bash
HOME=\/tmp\/
# exec vil blive kørt i bash i \/tmp\/
* * * * * \/path\/to\/execI sidste ende vil alle de miljøvariabler, der er defineret i tabellen (brugt af cron eller nødvendige for processen), blive videregivet til den kørte opgave.
For at redigere filer med crontab-kommandoen bruger man den editor, der er angivet i miljøvariablen VISUAL eller EDITOR. Hvis disse variabler ikke er defineret i det miljø, hvor crontab blev startet, bruges "/usr/ucb/vi" (ucb står for University of California, Berkeley).
cron i Debian og Ubuntu
Debian-udviklere og de afledte distributioner har udgivet version 3.0pl1 af Vixie cron. Der er ingen forskelle i syntaksen for tabel-filerne; for brugeren er det den samme Vixie cron. De største nye funktioner inkluderer support for , og .
Blandt de mindre mærkbare, men tydelige ændringer er placeringen af konfigurationsfilerne og opgavetabellerne.
Brugerdefinerede tabeller i Debian findes i mappen "/var/spool/cron/crontabs", mens systemtabellen stadig findes i "/etc/crontab". Pakke-specifikke opgavetabeller for Debian placeres i "/etc/cron.d", hvor cron-dæmonen automatisk læser dem. Brugeradgang reguleres af filerne "/etc/cron.allow" og "/etc/cron.deny".
Som standard shell anvendes stadig "/bin/sh", som i Debian erstattes af en lille POSIX-kompatibel shell , der kører uden at læse nogen konfiguration (i ikke-interaktiv tilstand).
Cron kører i de nyeste versioner af Debian via systemd, og konfigurationen kan ses i /lib/systemd/system/cron.service. Der er ikke noget særligt ved servicekonfigurationen; enhver mere detaljeret opgaveadministration kan udføres gennem miljøvariabler, der er erklæret direkte i crontab for hver bruger.
cronie i RedHat, Fedora og CentOS
— en fork af Vixie cron version 4.1. Ligesom i Debian er syntaksen ikke ændret, men der er tilføjet støtte til PAM og SELinux, klyngearbejde, overvågning af filer ved hjælp af inotify og andre funktioner.
Standardkonfigurationen findes på de sædvanlige steder: systemtabellen — i /etc/crontab, pakker placerer deres tabeller i /etc/cron.d, bruger tabeller lander i /var/spool/cron/crontabs.
Dæmonen kører under systemd, servicekonfigurationen er i /lib/systemd/system/crond.service.
I Red Hat-lignende distributioner bruges /bin/sh som standard ved opstart, og standardbashen fungerer som den. Det skal bemærkes, at når cron-opgaver startes via /bin/sh, køres bash-skal’en i POSIX-kompatibel tilstand og læser ikke nogen yderligere konfiguration, hvilket fungerer i ikke-interaktiv tilstand.
cronie i SLES og openSUSE
Det tyske distributionssystem SLES og dets afledninger openSUSE bruger stadig cronie. Dæmonen her kører også under systemd, og servicekonfigurationen ligger i /usr/lib/systemd/system/cron.service. Konfigurationen: /etc/crontab, /etc/cron.d, /var/spool/cron/tabs. Som /bin/sh bruges den samme bash, der kører i POSIX-kompatibel ikke-interaktiv tilstand.
Opbygning af Vixie cron
Moderne efterkommere af cron er ikke radikalt ændret sammenlignet med Vixie cron, men har dog fået nye funktioner, der ikke er nødvendige for at forstå programmets principper. Mange af disse udvidelser er usmooth og forvirrer koden. Den originale kildekode til cron i Paul Vixies udgave er en fryd at læse.
Derfor besluttede jeg at gennemgå strukturen af cron ved at bruge en version, der er fælles for begge udviklingsgrene af cron-programmet — Vixie cron 3.0pl1. Jeg vil forenkle eksemplerne ved at fjerne komplicerende ifdef'er og undlade sekundære detaljer.
Dæmonens arbejde kan opdeles i flere faser:
- Initialisering af programmet.
- Indsamling og opdatering af liste over opgaver til kørsel.
- Arbejdet med cron's hovedsløjfe.
- Kørsel af opgaven.
Lad os gennemgå dem én ad gangen.
Initialisering
Når der startes, efter at argumenterne er kontrolleret, sætter cron-processen signalhåndtererne SIGCHLD og SIGHUP. Den første logger, når et børneproces afsluttes, og den anden lukker filbeskriveren for logfilen:
signal(SIGCHLD, sigchld_handler);
signal(SIGHUP, sighup_handler);Cron-dæmonen kører altid som én instans i systemet, kun under superbrugerrettigheder og fra cron's hovedmappe. Følgende opkald skaber en lock-fil med demonens PID, sikrer, at brugeren er korrekt, og ændrer den nuværende mappe til hovedmappen:
acquire_daemonlock(0);
set_cron_uid();
set_cron_cwd();En standardsti er angivet, som vil blive brugt ved opstart af processer:
setenv("PATH", _PATH_DEFPATH, 1);Herefter "demoniseres" processen: der skabes en børneproces ved at kalde fork og en ny session i børneprocessen (kald til setsid). Der er ikke længere behov for forældreprocessen, som afslutter sig selv:
switch (fork()) {
case -1:
/* kritisk fejl og afslutning */
exit(0);
break;
case 0:
/* børneproces */
(void) setsid();
break;
default:
/* forældreproces afslutter sig */
_exit(0);
}
Afslutningen af forældreprocessen frigiver låsen på lock-filen. Desuden skal PID'en i filen opdateres til den nye børneproces. Derefter fyldes opgavedatabasen:
/* повторный захват лока */
acquire_daemonlock(0);
/* Заполнение БД */
database.head = NULL;
database.tail = NULL;
database.mtime = (time_t) 0;
load_database(&database);Herefter går cron ind i hovedarbejdsløkken. Men inden det skal vi se på lastning af opgavelisten.
Indsamling og opdatering af opgavelisten
Funktionen load_database er ansvarlig for indlæsning af opgavelisten. Den kontrollerer hovedsystemets crontab og mappen med brugerfiler. Hvis filerne og mappen ikke er ændret, genlæses opgavelisten ikke. Ellers begynder der at blive dannet en ny opgaveliste.
Indlæsning af systemfilen med specielle filnavne og tabellen:
/* если файл системной таблицы изменился, перечитываем */
if (syscron_stat.st_mtime) {
process_crontab("root", "*system*",
SYSCRONTAB, &syscron_stat,
&new_db, old_db);
}Indlæsning af brugertabeller i en løkke:
while (NULL != (dp = readdir(dir))) {
char fname[MAXNAMLEN+1],
tabname[MAXNAMLEN+1];
/* filer med punktum skal ikke læses */
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);
}
Herefter erstattes den gamle database med den nye.
I de tidligere eksempler sørger kaldet til funktionen process_crontab for, at der findes en bruger svarende til filnavnet for tabellen (medmindre det er superbruger), hvorefter det kalder load_user. Sidstnævnte læser filen linje for linje:
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;
}
}Her sættes enten miljøvariablen (strenge i formatet VAR=value) af funktionerne load_env / env_set, eller opgaven beskrevet (* * * * * /path/to/exec) læses af funktionen load_entry.
Entiteten entry, som returneres af load_entry, er vores opgave, der placeres i den fælles liste over opgaver. I selve funktionen foretages en langvarig parsing af tidsformatet; vi er derimod mere interesserede i at danne miljøvariabler og opgaveparametre:
/* пользователь и группа для запуска задачи берутся из 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);Den aktuelle liste over opgaver behandles i hovedloopet.
Hovedloop
Den originale cron fra Version 7 Unix fungerede meget simpelt: i en løkke læste den konfigurationen, kørte superbrugerens opgaver for det aktuelle minut og sov indtil begyndelsen af det næste minut. Denne enkle tilgang krævede for mange ressourcer på gamle maskiner.
I SysV blev der foreslået en alternativ version, hvor dæmonen sov indtil det nærmeste minut, for hvilket en opgave var defineret, eller i 30 minutter. Ressourcerne til at genlæse konfigurationen og tjekke opgaver blev brugt mindre i denne tilstand, men det blev besværligt at opdatere opgavelisten hurtigt.
Vixie cron vendte tilbage til at tjekke opgavelister hver minut, godt, at der i slutningen af 80'erne var blevet betydeligt flere ressourcer til rådighed på standard Unix-maskiner:
/* первичная загрузка задач */
load_database(&database);
/* запустить задачи, поставленные к выполнению после перезагрузки системы */
run_reboot_jobs(&database);
/* сделать TargetTime началом ближайшей минуты */
cron_sync();
while (TRUE) {
/* выполнить задачи, после чего спать до TargetTime с поправкой на время, потраченное на задачи */
cron_sleep();
/* перечитать конфигурацию */
load_database(&database);
/* собрать задачи для данной минуты */
cron_tick(&database);
/* перевести TargetTime на начало следующей минуты */
TargetTime += 60;
}
Det er funktionen cron_sleep, der direkte udfører opgaverne, som kalder funktionerne job_runqueue (gennemgang og udførelse af opgaver) og do_command (udførelse af hver enkelt opgave). Den sidste funktion bør gennemgås mere detaljeret.
Udførelse af opgave
Funktionen do_command er skrevet i en god Unix-stil, hvilket betyder, at den foretager en fork til asynkron udførelse af opgaven. Forælderprocessen fortsætter med at køre opgaverne, mens det barn, der genereres, tager sig af processen for opgaven:
switch (fork()) {
case -1:
/* kunne ikke udføre fork */
break;
case 0:
/* barnprocessen: prøver igen at tage hovedlåsen */
acquire_daemonlock(1);
/* går til dannelsen af opgaveprocessen */
child_process(e, u);
/* ved afslutningen stopper barnet processen */
_exit(OK_EXIT);
break;
default:
/* forælderprocessen fortsætter arbejdet */
break;
}I child_process er der en del logik: den tager standard output- og fejrstømmer på sig for derefter at videresende dem til mail (hvis miljøvariablen MAILTO er angivet i opgavelisten), og til sidst venter den på, at hovedprocessen afslutter sit arbejde.
Opgaveprocessen dannes via endnu et fork:
switch (vfork()) {
case -1:
/* ved fejl afsluttes arbejdet straks */
exit(ERROR_EXIT);
case 0:
/* barneprocessen opretter en ny session, terminal osv.
*/
(void) setsid();
/*
* herefter langvarig opsætning af procesoutput, lad os springe over for korthed
*/
/* ændring af bibliotek, bruger og brugergruppe,
* det vil sige at processen ikke længere er superbruger
*/
setgid(e->gid);
setuid(e->uid);
chdir(env_get("HOME", e->envp));
/* kørsel af selve kommandoen
*/
{
/* miljøvariablen SHELL angiver fortolker til kørsel */
char *shell = env_get("SHELL", e->envp);
/* processen startes uden at videregive miljøet fra overordnet proces,
* det vil sige præcis som beskrevet i brugerens opgaveliste */
execle(shell, shell, "-c", e->cmd, (char *)0, e->envp);
/* fejl – og processen er ikke startet? afslut arbejdet */
perror("execl");
_exit(ERROR_EXIT);
}
break;
default:
/* selve processen fortsætter arbejdet: venter på, at den afsluttes og output genereres */
break;
}Det er i grunden hele cron. Nogle interessante detaljer, som f.eks. hensyntagen til fjerntliggende brugere, har jeg udeladt, men det vigtigste er dækket.
Efterord
Cron er overraskende enkel og nyttig program, udført i de bedste traditioner fra Unix-verdenen. Den gør ikke noget overflødigt, men udfører sit arbejde fremragende i flere årtier. At gennemgå koden fra den version, der leveres med Ubuntu, tog ikke mere end en time, og jeg fik en masse fornøjelse! Jeg håber, jeg har været i stand til at dele den med jer.
Jeg ved ikke, hvordan det er med jer, men det gør mig en smule trist at erkende, at moderne programmering med sin tendens til overkomplicering og overabstraktion længe ikke strækker sig mod denne enkelhed.
Der findes mange moderne alternativer til cron: systemd-timere gør det muligt at organisere komplekse systemer med afhængigheder, i fcron kan man fleksibelt regulere ressourceforbruget for opgaverne. Men for mig har de mest fundamentale crontab altid været tilstrækkelige.
Kort sagt, elsk Unix, brug enkle programmer og glem ikke at læse manualerne til din platform!
Kilde: habr.com
