Cron v Linuxu: historie, použití a struktura

Cron v Linuxu: historie, použití a struktura

Klasik říkal, že šťastní lidé si čas nevšímají. V té divoké době ještě neexistovali programátoři ani Unix, ale v dnešní době programátoři hrdě vědí: místo nich si čas pohlídá cron.

Nástroje příkazového řádku jsou pro mě zároveň slabostí a rutinou. sed, awk, wc, cut a další staré programy se spouští skripty na našich serverech každý den. Mnohé z nich jsou naplánovány jako úkoly pro cron, plánovač ze 70. let.

Dlouho jsem používal cron povrchně, aniž bych se ponořil do detailů, ale jednou, když jsem se setkal s chybou při spuštění skriptu, rozhodl jsem se to důkladně prozkoumat. Tak vznikl tento článek, při jehož psaní jsem se seznámil s POSIX crontab, hlavními variantami cronu v populárních distribucích Linuxu a funkcemi některých z nich.

Používáte Linux a spouštíte úkoly v cron? Zajímá vás architektura systémových aplikací v Unixu? Tak jsme na stejné vlně!

Obsah

Původ druhů

Pravidelné spouštění uživatelských nebo systémových programů je očividnou nezbytností ve všech operačních systémech. Proto si programátoři tuto potřebu uvědomili už dávno.

Unixové operační systémy vychází z Version 7 Unix, vyvinutého v 70. letech minulého století v Bell Labs, mimo jiné slavným Kenem Thompsonem. Spolu s Version 7 Unix byl dodán i cron, služba pro pravidelné spouštění úkolů superuživatele.

Typický moderní cron je jednoduchý program, ale algoritmus původní verze byl ještě jednodušší: služba se probouzela každou minutu, četla tabulku úkolů z jediného souboru (/etc/lib/crontab) a prováděla pro superuživatele ty úkoly, které měly být provedeny v aktuální minutě.

V následujících letech byly vylepšené varianty této jednoduché a užitečné služby dodávány se všemi unixovými operačními systémy.

Obecné popisy formátu crontab a základních principů práce nástroje byly v roce 1992 zahrnuty do hlavního standardu unixových operačních systémů — POSIX — a tím se cron de facto stal de jure standardem.

V roce 1987 Paul Vixie, který se dotazoval uživatelů Unixu na přání k cron, vydal další verzi démona, která opravila některé problémy tradičního cronu a rozšířila syntaxi tabulkových souborů.

Ve třetí verzi se Vixie cron stal kompatibilním s požadavky POSIX, a navíc program neměl liberální licenci, spíše žádnou licenci, pokud nebereme v úvahu přání v README: autor negarantuje, jméno autora nelze odstranit a program lze prodávat pouze spolu se zdrojovým kódem. Tato omezení byla v souladu s principy rostoucí popularity svobodného softwaru, a proto některé klíčové distribuce Linuxu z počátku 90. let přijaly Vixie cron jako systémový a vyvíjejí ho dodnes.

Konkrétně Red Hat a SUSE vyvíjejí fork Vixie cronu — cronie, zatímco Debian a Ubuntu používají originální vydání Vixie cronu s mnoha opravami.

Nejprve se seznámíme s uživatelskou utilitou crontab popsanou v POSIX, poté se podíváme na rozšíření syntaxe, která byla představena ve Vixie cronu, a využití variant Vixie cronu v populárních distribucích Linuxu. A nakonec to nejlepší — analýzu architektury démona cron.

POSIX crontab

Zatímco původní cron vždy fungoval pro superuživatele, moderní plánovače se častěji zabývají úlohami běžných uživatelů, což je bezpečnější a pohodlnější.

Crony se dodávají jako součást dvou programů: neustále běžícího démona cron a uživatelské utility crontab. Ta umožňuje editaci úloh specifických pro každého uživatele v systému, zatímco démon spouští úlohy z uživatelských a systémových tabulek.

V standartní POSIX se nijak nezmiňuje o chování démona a pouze formalizuje uživatelský program crontab. Existence mechanismů pro spuštění uživatelských úloh je samozřejmě předpokládána, ale není podrobně popsána.

Voláním utility crontab lze provést čtyři věci: upravit uživatelskou tabulku úloh v editoru, načíst tabulku ze souboru, zobrazit aktuální tabulku úloh a vymazat tabulku úloh. Příklady použití utility crontab:

crontab -e # upravit tabulku úloh
crontab -l # zobrazit tabulku úloh
crontab -r # smazat tabulku úloh
crontab path/to/file.crontab # načíst tabulku úloh ze souboru

Při volání crontab -e bude použita editor uvedený ve standardní proměnné prostředí EDITOR.

Úkoly jsou popsány v následujícím formátu:

# строки-комментарии игнорируются
#
# задача, выполняемая ежеминутно
* * * * * /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

Prvních pět polí záznamů: minuty [1..60], hodiny [0..23], dny v měsíci [1..31], měsíce [1..12], dny v týdnu [0..6], kde 0 znamená neděli. Poslední, šesté pole, je řetězec, který bude proveden standardním interpretrem příkazů.

V prvních pěti polích lze hodnoty uvádět oddělené čárkou:

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

Nebo pomocí pomlčky:

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

Přístup uživatelů k plánování úloh se řídí soubory POSIX cron.allow a cron.deny, ve kterých jsou uvedeni uživatelé s přístupem k crontabu a uživatelé bez přístupu k programu. Umístění těchto souborů standard nijak nereguluje.

Spouštěným programům by podle standardu měly být předány alespoň čtyři proměnné prostředí:

  1. HOME — domácí adresář uživatele.
  2. LOGNAME — přihlašovací jméno uživatele.
  3. PATH — cesta, po které lze nalézt standardní nástroje systému.
  4. SHELL — cesta k použitému příkazovému interpretru.

Je pozoruhodné, že POSIX nic neříká o tom, odkud hodnoty pro tyto proměnné pocházejí.

Best seller — Vixie cron 3.0pl1

Společným předkem populárních variant cron je Vixie cron 3.0pl1, představený v mailing listu comp.sources.unix v roce 1992. Hlavní funkce této verze budeme podrobněji zkoumat.

Vixie cron se dodává ve dvou programech (cron a crontab). Jak je obvyklé, démon odpovídá za čtení a spouštění úloh ze systémové tabulky úloh a tabulek úloh jednotlivých uživatelů, zatímco utilita crontab slouží k úpravě uživatelských tabulek.

Tabulka úloh a konfigurační soubory

Tabulka úloh superuživatele se nachází v /etc/crontab. Syntaxe systémové tabulky odpovídá syntaxi Vixie cron s tím, že šestý sloupec uvádí jméno uživatele, z jehož účtu je úloha spouštěna:

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

Tabulky úloh běžných uživatelů se nachází v /var/cron/tabs/username a používají společnou syntaxi. Při spuštění utility crontab jménem uživatele se upravují právě tyto soubory.

Správa seznamů uživatelů, kteří mají přístup k crontabu, se provádí v souborech /var/cron/allow a /var/cron/deny, do kterých stačí přidat jméno uživatele jednotlivým řádkem.

Rozšířená syntaxe

Ve srovnání s POSIX crontab obsahuje řešení Paula Vixie několik velmi užitečných modifikací v syntaxi tabulek úloh utility.

Byl zaveden nový syntax tabulek: například je možné uvádět dny v týdnu nebo měsíce jménem (Po, Út atd.):

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

Je možné uvádět krok, kterým se úkoly spouštějí:

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

Kroky a intervaly lze míchat:

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

Podporovány jsou intuitivní alternativy běžného syntaxe (restart, ročně, každoročně, měsíčně, týdně, denně, o půlnoci, hodinově):

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

Prostředí úloh

Vixie cron umožňuje měnit prostředí spouštěných aplikací.

Proměnné prostředí USER, LOGNAME a HOME nejsou jen poskytovány démonem, ale berou se z souboru passwd. Proměnná PATH získává hodnotu "/usr/bin:/bin", a SHELL – "/bin/sh". Hodnoty všech proměnných, kromě LOGNAME, lze měnit v tabulkách uživatelů.

Některé proměnné prostředí (zejména SHELL a HOME) používá samotný cron pro spouštění úlohy. Takto může vypadat použití bash místo standardního sh pro spouštění uživatelských úloh:

SHELL=/bin/bash
HOME=/tmp/
# exec bude spuštěn bash-em v /tmp/
* * * * * /path/to/exec

Nakonec budou všechny proměnné prostředí definované v tabulce (používané cronem nebo potřebné procesu) předány spuštěné úloze.

Pro editaci souborů pomocí nástroje crontab se používá editor uvedený v proměnné prostředí VISUAL nebo EDITOR. Pokud v prostředí, kde byl spuštěn crontab, tyto proměnné nejsou definovány, používá se "/usr/ucb/vi" (ucb je pravděpodobně University of California, Berkeley).

cron v Debianu a Ubuntu

Vývojáři Debianu a odvozených distribucí vydali silně modifikovanou verzi verze Vixie cron 3.0pl1. Rozdíly v syntaxi souborů-tabulek nejsou, pro uživatele je to stejný Vixie cron. Největší novinky: podpora Syslog, SELinux a PAM.

Mezi méně viditelnými, ale znatelnými změnami je umístění konfiguračních souborů a tabulek úloh.

Uživatelské tabulky v Debianu jsou umístěny v adresáři /var/spool/cron/crontabs, systémová tabulka je stále tam – v /etc/crontab. Specifické tabulky úloh pro balíčky Debianu se umisťují do /etc/cron.d, odkud je démon cron automaticky načítá. Řízení přístupu uživatelů se upravuje pomocí souborů /etc/cron.allow a /etc/cron.deny.

Jako výchozí příkazová оболíčka je stále používán /bin/sh, jehož roli v Debianu hraje malý POSIX-kompatibilní shell dash, spuštěný bez čtení jakékoliv konfigurace (v neinteraktivním režimu).

Sam cron v posledních verzích Debianu je spuštěn přes systemd a konfiguraci spuštění lze nalézt v /lib/systemd/system/cron.service. V konfiguraci služby není nic zvláštního, jakékoli detailnější řízení úloh je možné provádět prostřednictvím proměnných prostředí, které jsou vyhlášeny přímo v crontab každého uživatele.

cronie v RedHat, Fedora a CentOS

cronie — fork Vixie cron verze 4.1. Stejně jako v Debianu, syntaxe se nezměnila, ale byla přidána podpora PAM a SELinux, práce v clusteru, sledování souborů pomocí inotify a dalších možností.

Konfigurace podle defaultu se nachází na obvyklých místech: systémová tabulka — v /etc/crontab, balíčky ukládají své tabulky do /etc/cron.d, uživatelské tabulky končí v /var/spool/cron/crontabs.

Démon je spuštěn pod správou systemd, konfigurace služby — /lib/systemd/system/crond.service.

V distribucích podobných Red Hat používá při spuštění defaultně /bin/sh, jehož rolí je standardní bash. Je třeba poznamenat, že při spuštění úloh cron přes /bin/sh se shell bash spouští v režimu kompatibilním s POSIX a nečte žádnou další konfiguraci, pracuje v neinteraktivním režimu.

cronie v SLES a openSUSE

Německá distribuce SLES a její derivát openSUSE používají opět cronie. Démon zde také běží pod systemd, konfigurace služby leží v /usr/lib/systemd/system/cron.service. Konfigurace: /etc/crontab, /etc/cron.d, /var/spool/cron/tabs. Jako /bin/sh je použit tentýž bash, spuštěný v neinteraktivním režimu kompatibilním s POSIX.

Funkce Vixie cronu

Moderní potomci cron ve srovnání s Vixie cron se nezměnili radikálně, ale přece jen obdrželi nové možnosti, které nejsou nutné pro pochopení principů fungování programu. Mnoho z těchto rozšíření je zpracováno neobratně a mate kód. Originální zdrojový kód cron v podání Paula Vixieho je radost číst.

Proto jsem se rozhodl provést rozbor zařízení cron na příkladu společného pro obě větve vývoje programu cron — Vixie cron 3.0pl1. Příklady zjednoduším, odstraním zbytečné ifdefy a vynechám druhotné detaily.

Práci démona lze rozdělit na několik etap:

  1. Inicializace programu.
  2. Sběr a aktualizace seznamu úloh k vykonání.
  3. Práce hlavního cyklu cron.
  4. Spuštění úlohy.

Probereme je postupně.

Inicializace

Při spuštění po kontrole argumentů proces cron nastavuje obslužné rutiny signálů SIGCHLD a SIGHUP. První zapisuje do logu záznam o ukončení podřízeného procesu, druhý zavírá popisovač souboru s logem:

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

Démon cron v systému vždy běží jeden, jen v roli superuživatele a z hlavního adresáře cronu. Následující volání vytvářejí lockfile s PID procesu démona, zajišťují, že uživatel je správný, a mění aktuální adresář na hlavní:

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

Nastavuje se výchozí cesta, která bude použita při spuštění procesů:

setenv("PATH", _PATH_DEFPATH, 1);

Dále se proces "démonizuje": vytváří podřízenou kopii procesu voláním fork a novou relaci v podřízeném procesu (volání setsid). V rodičovském procesu už není potřeba — a ten končí:

switch (fork()) {
case -1:
    
    /* kritická chyba a ukončení */
    exit(0);
break;
case 0:
    
    /* podřízený proces */
    (void) setsid();
break;
default:
    
    /* rodičovský proces ukončuje */
    _exit(0);
}

Ukončení rodičovského procesu uvolňuje lock na lockfile. Navíc je potřeba aktualizovat PID v souboru na podřízený. Poté se naplní databáze úloh:

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

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

Dále cron přechází do hlavního pracovního cyklu. Ale před tím se podíváme na načítání seznamu úloh.

Sbírání a aktualizace seznamu úloh

Za načítání seznamu úloh odpovídá funkce load_database. Ta kontroluje hlavní systémový crontab a adresář s uživatelskými soubory. Pokud se soubory a adresář neměnily, seznam úloh se nepřečte. V opačném případě se začíná vytvářet nový seznam úloh.

Načítání systémového souboru se speciálními názvy souborů a tabulkou:

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

Načítání uživatelských tabulek cyklem:

while (NULL != (dp = readdir(dir))) {
    char    fname[MAXNAMLEN+1],
            tabname[MAXNAMLEN+1];
    /* číst soubory s tečkou není třeba */
    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);
}

Poté se stará databáze nahrazuje novou.

V uvedených příkladech funkce process_crontab zajišťuje existenci uživatele odpovídajícího názvu souboru tabulky (pokud nejde o superuživatele), poté volá load_user. Ta poté čte samotný soubor po řádcích:

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

Zde buď nastavujeme proměnnou prostředí (řetězce ve formátu VAR=value) funkcemi load_env / env_set, nebo čteme popis úlohy (* * * * * /path/to/exec) pomocí funkce load_entry.

Entita entry, kterou vrací load_entry, je naše úloha, umístěná do společného seznamu úloh. Funkce provádí podrobnou analýzu formátu času, nás ale více zajímá vytváření proměnných prostředí a parametrů spuštění úlohy:

/* пользователь и группа для запуска задачи берутся из 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);

S aktuálním seznamem úloh funguje hlavní cyklus.

Hlavní cyklus

Původní cron z Version 7 Unix fungoval velmi jednoduše: v cyklu znovu načítal konfiguraci, spouštěl úlohy aktuální minuty jako superuživatel a spal do začátku další minuty. Tento jednoduchý přístup vyžadoval na starých strojích příliš mnoho zdrojů.

V SysV byla navržena alternativní verze, kde démon usnul buď do nejbližší minuty, pro kterou je definována úloha, nebo na 30 minut. Zdroje na opětovné načítání konfigurace a kontrolu úloh v takovém režimu se spotřebovávaly méně, ale rychlý přístup k seznamu úloh se stal nepraktickým.

Vixie cron se vrátil k kontrole seznamů úloh každou minutu, na konci 80. let byla totiž na standardních Unixových strojích znatelně více zdrojů:

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

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

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

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

Přímo vykonáváním úloh se zabývá funkce cron_sleep, která volá funkce job_runqueue (iterace a spuštění úloh) a do_command (spuštění každé jednotlivé úlohy). Poslední funkci stojí za to podrobněji prozkoumat.

Spuštění úlohy

Funkce do_command je napsána ve stylu dobrého Unixu, to znamená, že pro asynchronní vykonávání úlohy vytváří fork. Rodičovský proces pokračuje ve spuštění úloh, zatímco dceřiný se zabývá přípravou procesu úlohy:

switch (fork()) {
case -1:
    /* neprovedli jsme fork */
    break;
case 0:
    /* dceřiný proces: pro jistotu ještě jednou zkoušíme získat hlavní zámek */
    acquire_daemonlock(1);
    /* přecházíme k přípravě procesu úlohy */
    child_process(e, u);
    /* po dokončení dceřiný proces končí práci */
    _exit(OK_EXIT);
    break;
default:
    /* rodičovský proces pokračuje v práci */
    break;
}

V child_process je poměrně hodně logiky: přijímá standardní výstupy a chybové toky a poté je přesměrovává na e-mail (pokud je v tabulce úloh uvedena proměnná prostředí MAILTO) a nakonec čeká na dokončení hlavního procesu úlohy.

Proces úlohy je vytvářen dalším fork:

switch (vfork()) {
case -1:
    
    exit(ERROR_EXIT);
case 0:
    
    (void) setsid();

    
    

    
    setgid(e->gid);
    setuid(e->uid);
    chdir(env_get("HOME", e->envp));

    
    {
        
        char    *shell = env_get("SHELL", e->envp);

        
        execle(shell, shell, "-c", e->cmd, (char *)0, e->envp);

        
        perror("execl");
        _exit(ERROR_EXIT);
    }
    break;
default:
    
    break;
}

To je v podstatě celý cron. Některé zajímavé detaily, jako je zohlednění vzdálených uživatelů, jsem opomněl, ale hlavní myšlenku jsem vystihl.

Poznámka na závěr

Cron je překvapivě jednoduchý a užitečný program, který se drží nejlepších tradic světa Unix. Nedělá nic nadbytečného, ale svou práci vykonává skvěle již několik desetiletí. Seznámení s kódem verze dodávané s Ubuntu mi zabralo nejvýše hodinu a užil jsem si to! Doufám, že jsem to mohl sdílet i s vámi.

Nevím, jak vám, ale mě trochu smutní, že moderní programování, se svou tendencí k nadměrnému zjednodušování a abstrakci, už dlouho nenabízí takovou jednoduchost.

Existuje mnoho moderních alternativ k cronu: systemd-timers umožňují organizovat složité systémy s závislostmi, ve fcron můžete flexibilněji řídit spotřebu zdrojů úlohami. Ale osobně mi vždy stačily nejjednodušší crontab.

Celkově, milujte Unix, používejte jednoduché programy a nezapomínejte číst manuály pro vaši platformu!

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster