Cron i Linux: historia, användning och funktion

Cron i Linux: historia, användning och funktion

Klassik skrev att lyckliga timmar inte övervakas. På den tiden fanns det varken programmerare eller Unix, men idag vet programmerare bestämt: istället för dem passar cron på tiden.

Kommandoradsverktyg är både en svaghet och en rutin för mig. sed, awk, wc, cut och andra gamla program körs varje dag genom skript på våra servrar. Många av dem är som uppgifter för cron, en schemaläggare med ursprung från 70-talet.

Jag använde länge cron ytligt, utan att gå in på detaljer, men en gång, när jag stötte på ett fel vid körning av ett skript, bestämde jag mig för att förstå det grundligt. Så här föddes denna artikel, där jag bekantade mig med POSIX crontab, de viktigaste versionerna av cron i populära Linux-distributioner och arkitekturen för några av dem.

Använder du Linux och kör uppgifter i cron? Är du intresserad av arkitekturen för systemapplikationer i Unix? Då är vi på samma spår!

Innehåll

Arternas ursprung

Att köra användar- eller systemprogram periodiskt är en uppenbar nödvändighet i alla operativsystem. Därför insåg programmerare tidigt behovet av tjänster som gör det möjligt att centralt schemalägga och utföra uppgifter.

Unix-liknande operativsystem härstammar från Version 7 Unix, som utvecklades på 70-talet i Bell Labs, bland annat av den berömda Ken Thompson. Tillsammans med Version 7 Unix levererades cron, en tjänst för regelbunden körning av uppgifter av superanvändare.

En typisk modern cron är ett enkelt program, men algoritmen för den ursprungliga versionen var ännu enklare: tjänsten vaknade en gång per minut, läste uppgiftstabellen från en enda fil (/etc/lib/crontab) och utförde de uppgifter som skulle utföras av superanvändaren under den aktuella minuten.

Senare förbättrade versioner av denna enkla och användbara tjänst levererades med alla Unix-liknande operativsystem.

Allmänna beskrivningar av formatet för crontab och de grundläggande principer för arbetet med verktyget inkluderades 1992 i huvudstandarden för Unix-liknande operativsystem — POSIX — och på så sätt blev cron från de facto-standarden en de jure-standard.

År 1987 utfärdade Paul Vixie, efter att ha frågat Unix-användare om deras önskemål för cron, en ny version av demon som åtgärdade vissa problem med de traditionella cron och utvidgade syntaxen för tabellfiler.

I den tredje versionen av Vixie cron uppfyllde den kraven enligt POSIX, dessutom hade programmet en liberal licens, eller snarare ingen licens alls, om man inte räknar önskemålen i README: författaren ger inga garantier, författarens namn får inte tas bort, och programmet får endast säljas tillsammans med källkoden. Dessa krav visade sig vara förenliga med principerna som började vinna popularitet under den tiden för fri programvara, därför valde några av de viktiga Linux-distributionerna som kom i början av 90-talet Vixie cron som systemstandard och utvecklar den fortfarande.

Specifikt utvecklar Red Hat och SUSE en fork av Vixie cron – cronie, medan Debian och Ubuntu använder den ursprungliga versionen av Vixie cron med ett flertal patchar.

Låt oss först bekanta oss med det användartillämpliga verktyget crontab som beskrivs i POSIX, och sedan går vi igenom syntaxutvidgningarna som introduceras i Vixie cron, samt användningen av varianter av Vixie cron i populära Linux-distributioner. Och slutligen, grädden på moset – en analys av hur cron-demonen fungerar.

POSIX crontab

Om den ursprungliga cron alltid fungerade för superanvändare, har moderna schemaläggare oftare att göra med uppgifter för vanliga användare, vilket är mer säkert och bekvämt.

Cron levereras som ett paket bestående av två program: en ständigt aktiv demon (cron) och ett verktyg (crontab) tillgängligt för användare. Det sistnämnda tillåter att redigera uppgifts-tabeller specifika för varje användare i systemet, medan demon kör uppgifter från användar- och systemtabeller.

I POSIX-standarden beskriver inte beteendet hos demon och formaliserar endast användarprogrammet crontab. Existensen av mekanismer för att starta användaruppgifter underförstås naturligtvis, men beskrivs inte närmare.

Med kommandot för att kalla på verktyget crontab kan man göra fyra saker: redigera användarens uppgiftstabell i en textredigerare, ladda en tabell från en fil, visa den aktuella uppgiftstabellen och rensa uppgiftstabellen. Exempel på hur kommandot crontab används:

crontab -e # redigera uppgiftstabell
crontab -l # visa uppgiftstabell
crontab -r # ta bort uppgiftstabell
crontab path/to/file.crontab # ladda uppgiftstabell från fil

Vid anrop av crontab -e kommer att använda redigeraren som anges i standardmiljör variabeln EDITOR.

Själva uppgifterna beskrivs i följande 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.log

De första fem fälten i posterna: minuter [1..60], timmar [0..23], dagar i månaden [1..31], månader [1..12], veckodagar [0..6], där 0 är söndag. Det sista, sjätte fältet, är en sträng som kommer att utföras av den standardkommandotolk.

I de första fem fälten kan värden anges med komma:

# задача, выполняемая в первую и десятую минуты каждого часа
1,10 * * * * /path/to/exec -a -b -c

Eller med bindestreck:

# задача, выполняемая в каждую из первых десяти минут каждого часа
0-9 * * * * /path/to/exec -a -b -c

Användartillgång till schemaläggning av uppgifter regleras i POSIX-filerna cron.allow och cron.deny där, respektive, användarna med tillgång till crontab och användarna utan tillgång till programmet anges. Platsen för dessa filer regleras inte av standarden.

De program som körs, enligt standard, ska få minst fyra miljövariabler:

  1. HOME — användarens hemkatalog.
  2. LOGNAME — användarens inloggning.
  3. PATH — sökvägen för att hitta de standardverktyg i systemet.
  4. SHELL — sökvägen till den använda kommandotolken.

Det är anmärkningsvärt att POSIX inte säger något om var värdena för dessa variabler kommer ifrån.

Bestseller — Vixie cron 3.0pl1

Den gemensamma föregångaren till de populära varianterna av cron är Vixie cron 3.0pl1, som presenterades i åtgärden comp.sources.unix 1992. Vi kommer att titta närmare på de viktigaste funktionerna i denna version.

Vixie cron levereras i två program (cron och crontab). Som vanligt ansvarar demon för att läsa och köra uppgifter från systemtabellen och tabellerna för enskilda användare, medan verktyget crontab ansvarar för redigering av användartabeller.

Uppgiftstabell och konfigurationsfiler

Superanvändarens uppgiftstabell finns i /etc/crontab. Syntaxen för systemtabellen överensstämmer med syntaxen för Vixie cron, med undantag för att den sjätte kolumnen anger användarnamnet som uppgiften körs under:

# Запускается ежеминутно от пользователя vlad
* * * * * vlad /path/to/exec

Uppgiftstabeller för vanliga användare finns i /var/cron/tabs/username och använder en gemensam syntax. När verktyget crontab körs under en användare redigeras just dessa filer.

Hantera listorna över användare som har tillgång till crontab, sker i filerna /var/cron/allow och /var/cron/deny, där det är tillräckligt att ange användarnamnet på en separat rad.

Utvidgad syntax

Jämfört med POSIX crontab innehåller Paul Vixies lösning flera mycket användbara modifieringar av syntaxen för uppgiftstabellerna i verktyget.

En ny syntax för tabeller har blivit tillgänglig: till exempel kan veckodagar eller månader anges med namn (Mon, Tue och så vidare):

# Запускается ежеминутно по понедельникам и вторникам в январе
* * * Jan Mon,Tue /path/to/exec

Det går att ange steg mellan körningar av uppgifter:

# Запускается с шагом в две минуты
*/2 * * * Mon,Tue /path/to/exec

Steg och intervaller kan blandas:

# Запускается с шагом в две минуты в первых десять минут каждого часа
0-10/2 * * * * /path/to/exec

Intuitiva alternativ till den vanliga syntaxen stöds (reboot, yearly, annually, monthly, weekly, daily, midnight, hourly):

# Запускается после перезагрузки системы
@reboot /exec/on/reboot
# Запускается раз в день
@daily /exec/daily
# Запускается раз в час
@hourly /exec/daily

Körningsmiljö för uppgifter

Vixie cron tillåter att ändra miljön för körda applikationer.

Miljövariablerna USER, LOGNAME och HOME tillhandahålls inte bara av demon utan hämtas från filen passwd. Variabeln PATH får värdet "/usr/bin:/bin", medan SHELL blir "/bin/sh". Värdena för alla variabler, utom LOGNAME, kan ändras i användartabellerna.

Vissa miljövariabler (främst SHELL och HOME) används av cron själv för att köra uppgifter. Så här kan det se ut när bash används istället för standard sh för att köra användartasker:

SHELL=/bin/bash
HOME=/tmp/
# exec kommer att köras av bash i /tmp/
* * * * * /path/to/exec

I slutändan kommer alla definierade miljövariabler (som används av cron eller som krävs av processen) att överföras till den körda uppgiften.

För att redigera filer med crontab-verktyget används textredigeraren som anges i miljövariabeln VISUAL eller EDITOR. Om dessa variabler inte är definierade i miljön där crontab kördes, används "/usr/ucb/vi" (ucb står antagligen för University of California, Berkeley).

cron i Debian och Ubuntu

Utvecklarna av Debian och dess nedärvda distributioner har släppt en kraftigt modifierad version av Vixie cron 3.0pl1. Det finns inga skillnader i syntaxen för tabellfiler; för användarna är det samma Vixie cron. De största nya funktionerna är stödet för syslog, SELinux och PAM.

Av mindre uppenbara men betydelsefulla ändringar är placeringen av konfigurationsfiler och uppgiftstabeller.

Användartabeller i Debian finns i katalogen /var/spool/cron/crontabs, och systemtabellen finns fortfarande i /etc/crontab. Paket-specifika uppgiftstabeller i Debian placeras i /etc/cron.d, som demon cron automatiskt läser in. Åtkomstkontroll för användare regleras av filerna /etc/cron.allow och /etc/cron.deny.

Som standard används fortfarande /bin/sh som kommandotolk, där Debian använder en liten POSIX-kompatibel shell dash, som körs utan att läsa någon konfiguration (i icke-interaktivt läge).

Cron i de senaste versionerna av Debian körs via systemd, och konfigurationen kan ses i /lib/systemd/system/cron.service. Det finns inget särskilt med tjänstekonfigurationen, mer avancerad hantering av uppgifter kan göras genom miljövariabler som deklareras direkt i crontab för varje användare.

cronie i RedHat, Fedora och CentOS

cronie — en fork av Vixie cron version 4.1. Precis som i Debian har syntaxen inte förändrats, men stöd för PAM och SELinux, klusterarbete, filövervakning med inotify och andra funktioner har lagts till.

Standardkonfigurationen finns på vanliga ställen: systemtabellen ligger i /etc/crontab, paket placerar sina tabeller i /etc/cron.d, användartabeller hamnar i /var/spool/cron/crontabs.

Demon körs under systemds ledning, tjänstekonfigurationen är /lib/systemd/system/crond.service.

I Red Hat-liknande distributioner används som standard /bin/sh vid uppstart, där bash fungerar som standard. Det är värt att notera att när cron-uppgifter körs via /bin/sh startar bash-skalet i POSIX-kompatibelt läge och läser ingen ytterligare konfiguration, utan fungerar i icke-interaktivt läge.

cronie i SLES och openSUSE

Den tyska distributionen SLES och dess derivat openSUSE använder fortfarande cronie. Demonen här körs också under systemd, tjänstekonfigurationen finns i /usr/lib/systemd/system/cron.service. Konfiguration: /etc/crontab, /etc/cron.d, /var/spool/cron/tabs. Som /bin/sh fungerar fortfarande samma bash, startad i POSIX-kompatibelt icke-interaktivt läge.

Vixie cron-arkitektur

Moderna efterföljare till cron har inte radikalt förändrats jämfört med Vixie cron, men de har ändå fått nya funktioner som inte behövs för att förstå programmets arbetsprinciper. Många av dessa utvidgningar är slarvigt utformade och förvirrar koden. Den ursprungliga koden för cron i Paul Vixies utförande är en ren njutning att läsa.

Därför bestämde jag mig för att göra en genomgång av cron-strukturen med exempel på den gemensamma utvecklingsgrenen för cron-programmet — Vixie cron 3.0pl1. Jag kommer att förenkla exemplen genom att ta bort de komplicerade ifdef:erna och utelämna sekundära detaljer.

Arbetet hos demonen kan delas in i flera steg:

  1. Programinitiering.
  2. Insamling och uppdatering av listan över uppgifter som ska köras.
  3. Körning av cronhuvudloop.
  4. Körning av uppgiften.

Vi går igenom dem i ordning.

Initiering

Vid start efter att ha verifierat argumenten sätter cron-processen signalhanterare för SIGCHLD och SIGHUP. Den första registrerar en loggpost om avslutningen av barnprocessen, den andra stänger filbeskrivaren för loggfilen:

signal(SIGCHLD, sigchld_handler);
signal(SIGHUP, sighup_handler);

Cron-demonen i systemet körs alltid av en enda instans, endast som superanvändare och från cron:s huvudkatalog. Följande anrop skapar en lockfil med demonprocessens PID, säkerställer att användaren är korrekt och byter nuvarande katalog till huvudkatalogen:

acquire_daemonlock(0);
set_cron_uid();
set_cron_cwd();

En standardväg ställs in som kommer att användas vid start av processer:

setenv("PATH", _PATH_DEFPATH, 1);

Därefter "demoniseras" processen: en barnkopiering av processen skapas genom att anropa fork och en ny session i barnprocessen (anrop av setsid). Det finns ingen större användning för föräldraprocessen — och den avslutar jobbet:

switch (fork()) {
case -1:
    \/\* kritiskt fel och avslut av jobbet *\/\
    exit(0);
break;
case 0:
    \/\* barnprocess *\/\
    (void) setsid();
break;
default:
    \/\* föräldraprocessen avslutar jobbet *\/\
    _exit(0);
}

Avslutningen av föräldraprocessen frigör låset på lockfilen. Dessutom måste PID i filen uppdateras till barnets. Efter detta fylls uppgiftsdatabasen:

/* повторный захват лока */
acquire_daemonlock(0);

/* Заполнение БД  */
database.head = NULL;
database.tail = NULL;
database.mtime = (time_t) 0;
load_database(&database);

Därefter går cron in i huvudarbetscykeln. Men innan dess är det värt att ta en titt på laddningen av uppgiftslistan.

Insamling och uppdatering av uppgiftslista

Funktionen load_database ansvarar för laddningen av uppgiftslistan. Den kontrollerar den huvudsakliga system crontab och katalogen med användarfiler. Om filer och katalog inte har ändrats, läses inte uppgiftslistan om. I annat fall börjar en ny uppgiftslista att formas.

Laddning av systemfilen med speciella filnamn och tabellen:

/* если файл системной таблицы изменился, перечитываем */
if (syscron_stat.st_mtime) {
    process_crontab("root", "*system*",
    SYSCRONTAB, &syscron_stat,
    &new_db, old_db);
}

Laddning av användartabeller i en loop:

while (NULL != (dp = readdir(dir))) {
    char    fname[MAXNAMLEN+1],
            tabname[MAXNAMLEN+1];
    \/\* läsa filer med punkt behövs inte *\/\
    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);
}

Efter detta ersätts den gamla databasen med den nya.

I exemplen ovan kontrollerar anropet av funktionen process_crontab om det finns en användare som motsvarar namnet på tabellfilen (såvida det inte är en superanvändare), varefter load_user anropas. Den senare läser filen rad för rad:

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;
    }
}

Här antingen sätts miljövariabeln (strängar av typen VAR=value) av funktionerna load_env / env_set, eller så läses uppgifter in via funktionen load_entry.

Entiteten entry som returneras av load_entry är vår uppgift, som placeras i den gemensamma uppgiftslistan. I själva funktionen genomförs en detaljerad analys av tidsformatet; vi är mer intresserade av att skapa miljövariabler och körparameters för uppgiften:

/* пользователь и группа для запуска задачи берутся из 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 aktuella uppgiftslistan hanteras av huvudloopen.

Huvudloop

Den ursprungliga cron från Version 7 Unix fungerade på ett mycket enkelt sätt: den läste om konfigurationen i en loop, körde uppgifter för den aktuella minuten som superanvändare och sov till början av nästa minut. Denna enkla metod krävde för mycket resurser på gamla maskiner.

I SysV föreslogs en alternativ version där demon sov antingen till nästa minut som hade en definierad uppgift, eller i 30 minuter. Resurser för att läsa om konfigurationen och kontrollera uppgifter förbrukades mindre i detta läge, men det blev obekvämt att snabbt uppdatera uppgiftslistan.

Vixie cron återvände till att kontrollera uppgiftslistorna en gång per minut, tack och lov hade resurserna på standard Unix-maskiner ökat betydligt mot slutet av 80-talet:

/* первичная загрузка задач */
load_database(&database);
/* запустить задачи, поставленные к выполнению после перезагрузки системы */
run_reboot_jobs(&database);
/* сделать TargetTime началом ближайшей минуты */
cron_sync();
while (TRUE) {
    /* выполнить задачи, после чего спать до TargetTime с поправкой на время, потраченное на задачи */
    cron_sleep();

    /* перечитать конфигурацию */
    load_database(&database);

    /* собрать задачи для данной минуты */
    cron_tick(&database);

    /* перевести TargetTime на начало следующей минуты */
    TargetTime += 60;
}

Själva utförandet av uppgifter hanteras av funktionen cron_sleep, som anropar funktionerna job_runqueue (genomgång och körning av uppgifter) och do_command (körning av varje enskild uppgift). Den senare funktionen förtjänar en närmare granskning.

Körning av uppgift

Funktionen do_command är utförd i god Unix-stil, vilket innebär att för asynkron körning av uppgiften görs en fork. Föräldraprocessen fortsätter att köra uppgifter, medan den barnprocessen förbereder uppgiftsprocessen:

switch (fork()) {
case -1:
    /* kunde inte utföra fork */
    break;
case 0:
    /* barnprocess: vi försöker återigen ta huvudlåset */
    acquire_daemonlock(1);
    /* går vidare till att forma uppgiftsprocessen */
    child_process(e, u);
    /* när den är klar avslutar barnprocessen sitt arbete */
    _exit(OK_EXIT);
    break;
default:
    /* föräldraprocessen fortsätter sitt arbete */
    break;
}

I child_process finns det en hel del logik: den tar emot standardutdata och felströmmen för att sedan vidarebefordra dem via e-post (om miljövariabeln MAILTO anges i uppgiftslistan), och väntar tills huvudprocessen för uppgiften är klar.

Uppgiftsprocessen bildas av en annan fork:

switch (vfork()) {
case -1:
    
    /* om ett fel inträffar avslutas processen direkt */
    exit(ERROR_EXIT);
case 0:
    /* barnprocessen skapar en ny session, terminal osv.
     */
    (void) setsid();

    /*
     * här följer en omfattande konfiguration av processen, vi hoppar över det för korthetens skull
     */

    /* ändra katalog, användare och grupp, 
     * det vill säga processen är inte längre superanvändare */
    setgid(e->gid);
    setuid(e->uid);
    chdir(env_get("HOME", e->envp));

    /* kör kommandot */
    {
        /* miljövariabeln SHELL anger tolk för körning */
        char    *shell = env_get("SHELL", e->envp);

        /* processen startar utan att vidarebefordra moderprocessens miljö, 
         * det vill säga precis som det beskrivs i användarens uppgiftslista */
        execle(shell, shell, "-c", e->cmd, (char *)0, e->envp);

        /* fel - processen startades inte? avsluta */
        perror("execl");
        _exit(ERROR_EXIT);
    }
    break;
default:
    /* själva processen fortsätter att arbeta: väntar på avslut och utdata */
    break;
}

Så där har vi hela cron. Några intressanta detaljer, som hantering av fjärranvändare, har jag utelämnat, men det viktigaste har jag tagit upp.

Epilog

Cron är ett överraskande enkelt och användbart program, utfört i bästa Unix-tradition. Det gör inget överflödigt, men utför sitt arbete utmärkt sedan flera decennier tillbaka. Att bekanta sig med koden i den version som följer med Ubuntu tog inte mer än en timme, och jag fick stor glädje av det! Jag hoppas att jag kunnat dela detta med er.

Jag vet inte hur ni känner, men jag blir lite ledsen av att förstå att modern programmering, med sin tendens till överkomplicering och överabstraktion, länge har saknat denna enkelhet.

Det finns många moderna alternativ till cron: systemd-timers möjliggör komplicerade system med beroenden, i fcron kan man mer flexibelt reglera resurstilldelningen för uppgifter. Men personligen har jag alltid varit nöjd med de enklaste crontab.

Kort sagt, älska Unix, använd enkla program och glöm inte att läsa manualerna för er plattform!

Källa: habr.com

Köp pålitlig webbhotell med DDoS-skydd, VPS VDS-servrar 🔥 Köp pålitlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster