Cron w Linux: historia, zastosowanie i konstrukcja

Cron w Linux: historia, zastosowanie i konstrukcja

Klasyk pisał, że szczęśliwi godzin nie liczą. W tamtych dzikich czasach nie było jeszcze ani programistów, ani Unix, ale w dzisiejszych czasach programiści dobrze wiedzą: zamiast nich czas obserwuje cron.

Narzędzia wiersza poleceń są dla mnie jednocześnie słabością i rutyną. sed, awk, wc, cut i inne stare programy są uruchamiane przez skrypty na naszych serwerach codziennie. Wiele z nich jest ustalonych jako zadania dla cron, planownika z lat 70.

Długo używałem cron powierzchownie, nie wnikając w szczegóły, ale pewnego dnia, napotykając błąd podczas uruchamiania skryptu, postanowiłem zrozumieć to dokładnie. Tak powstał ten artykuł, podczas pisania którego zapoznałem się z POSIX crontab, podstawowymi wariantami cron w popularnych dystrybucjach Linuxa i działaniem niektórych z nich.

Używasz Linuxa i uruchamiasz zadania w cron? Interesuje cię architektura aplikacji systemowych w Unix? W takim razie jesteśmy w tym razem!

Spis treści

O pochodzeniu gatunków

Okresowe wykonywanie programów użytkownika lub systemowych — to oczywista potrzeba we wszystkich systemach operacyjnych. Dlatego też potrzeba usług, które pozwalają centralnie planować i wykonywać zadania, została dostrzegnięta przez programistów już dawno temu.

Systemy operacyjne podobne do Unix prowadzą swoją genealogię od Version 7 Unix, opracowanego w latach 70. XX wieku w Bell Labs, w tym słynnego Kena Thompsona. Razem z Version 7 Unix dostarczano również cron, usługę do regularnego wykonywania zadań superużytkownika.

Typowy nowoczesny cron to nieskomplikowany program, ale algorytm działania oryginalnej wersji był jeszcze prostszy: usługa budziła się co minutę, czytała tabelę z zadaniami z jednego pliku (/etc/lib/crontab) i wykonywała dla superużytkownika te zadania, które należało wykonać w bieżącej minucie.

W późniejszym czasie ulepszone wersje prostus i użytecznego serwisu były dostarczane ze wszystkimi systemami operacyjnymi podobnymi do Unix.

Uogólnione opisy formatu crontab i podstawowych zasad działania narzędzia w 1992 roku zostały włączone do głównego standardu systemów operacyjnych podobnych do Unix — POSIX — i w ten sposób cron z de facto stał się de jure standardem.

W 1987 roku Paul Vixie, po ankietowaniu użytkowników systemu Unix w sprawie ich oczekiwań dotyczących crona, wydał kolejną wersję demona, która rozwiązywała niektóre problemy tradycyjnego crona i rozszerzała składnię plików tabel.

W trzeciej wersji Vixie cron spełniał wymagania POSIX, a program miał liberalną licencję, właściwie nie było żadnej licencji, jeśli nie liczyć wskazówek w README: autor nie daje żadnych gwarancji, imienia autora nie można usunąć, a program można sprzedawać tylko razem z kodem źródłowym. Te wymagania okazały się zgodne z zasadami rosnącej wówczas popularności wolnego oprogramowania, więc niektóre istotne dystrybucje Linuksa, które pojawiły się na początku lat 90., przyjęły Vixie cron jako systemowy i rozwijają go do dziś.

W szczególności Red Hat i SUSE rozwijają forka Vixie cron — cronie, a Debian i Ubuntu używają oryginalnej edycji Vixie cron z wieloma poprawkami.

Zacznijmy od poznania opisanego w POSIX narzędzia użytkownika crontab, a potem omówimy rozszerzenia składni zaprezentowane w Vixie cron oraz wykorzystanie wariacji Vixie cron w popularnych dystrybucjach Linuksa. Na koniec wisienka na torcie — omówienie działania demona cron.

POSIX crontab

Jeśli oryginalny cron zawsze działał dla superużytkownika, to nowoczesne harmonogramy częściej zajmują się zadaniami zwykłych użytkowników, co jest znacznie bezpieczniejsze i wygodniejsze.

Crony dostarczane są w zestawie dwóch programów: stale działającego demona cron i dostępnego dla użytkowników narzędzia crontab. To ostatnie pozwala edytować tabele zadań specyficzne dla każdego użytkownika w systemie, natomiast demon uruchamia zadania z tabel użytkowników i systemowej.

W standardzie POSIX nie opisuje zachowania demona, formalizując jedynie program użytkownika crontab. Istnienie mechanizmów uruchamiania zadań użytkowników jest oczywiście domniemane, ale nie opisane szczegółowo.

Za pomocą wywołania narzędzia crontab można wykonać cztery czynności: edytować użytkownikową tabelę zadań w edytorze, załadować tabelę z pliku, wyświetlić bieżącą tabelę zadań oraz wyczyścić tabelę zadań. Przykłady użycia narzędzia crontab:

crontab -e # edytować tabelę zadań
crontab -l # wyświetlić tabelę zadań
crontab -r # usunąć tabelę zadań
crontab path/to/file.crontab # załadować tabelę zadań z pliku

Przy wywołaniu crontab -e Będzie używany edytor wskazany w standardowej zmiennej środowiskowej EDYTOR.

Same zadania są opisane w następującym formacie:

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

Pierwsze pięć pól wpisów: minuty [1..60], godziny [0..23], dni miesiąca [1..31], miesiące [1..12], dni tygodnia [0..6], gdzie 0 to niedziela. Ostatnie, szóste pole, to ciąg, który zostanie wykonany przez standardowy interpreter poleceń.

W pierwszych pięciu polach wartości można wymieniać oddzielając je przecinkiem:

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

Lub myślnikiem:

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

Dostęp użytkowników do planowania zadań jest regulowany w plikach POSIX cron.allow i cron.deny, w których wyliczeni są odpowiednio użytkownicy z dostępem do crontab i użytkownicy bez dostępu do programu. Lokalizacja tych plików nie jest regulowana przez standard.

Uruchamianym programom, zgodnie ze standardem, muszą być przekazywane co najmniej cztery zmienne środowiskowe:

  1. HOME — domowy katalog użytkownika.
  2. LOGNAME — login użytkownika.
  3. PATH — ścieżka, w której można znaleźć standardowe narzędzia systemowe.
  4. SHELL — ścieżka do używanego interpretera poleceń.

Warto zauważyć, że POSIX nic nie mówi o tym, skąd pochodzą wartości dla tych zmiennych.

Besteller — Vixie cron 3.0pl1

Wspólnym przodkiem popularnych wariantów cron jest Vixie cron 3.0pl1, przedstawiony w wiadomościach comp.sources.unix w 1992 roku. Podstawowe możliwości tej wersji omówimy bardziej szczegółowo.

Vixie cron dostarczany jest w dwóch programach (cron i crontab). Jak zwykle, demon odpowiada za odczyt i uruchamianie zadań z systemowej tabeli zadań oraz tabel zadań poszczególnych użytkowników, a narzędzie crontab — za edytowanie tabel użytkowników.

Tabela zadań i pliki konfiguracyjne

Tabela zadań superużytkownika znajduje się w /etc/crontab. Składnia systemowej tabeli odpowiada składni Vixie cron z poprawką na to, że w szóstej kolumnie podawana jest nazwa użytkownika, w imieniu którego uruchamiane jest zadanie:

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

Tabele zadań zwykłych użytkowników znajdują się w /var/cron/tabs/nazwa_użytkownika i używają wspólnej składni. Podczas uruchamiania narzędzia crontab w imieniu użytkownika edytowane są właśnie te pliki.

Zarządzanie listami użytkowników mających dostęp do crontab odbywa się w plikach /var/cron/allow i /var/cron/deny, gdzie wystarczy wpisać nazwę użytkownika oddzielnie w nowej linii.

Rozszerzona składnia

W porównaniu z POSIX crontab, rozwiązanie Paula Vixie zawiera kilka bardzo przydatnych modyfikacji w składni tabel zadań narzędzia.

Now a new table syntax is available: for example, you can specify the days of the week or months by name (Mon, Tue, etc.):

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

You can specify the step interval between task executions:

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

You can mix steps and intervals:

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

Intuitive alternatives to the standard syntax are supported (reboot, yearly, annually, monthly, weekly, daily, midnight, hourly):

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

Task execution environment

Vixie cron allows you to change the environment of the executed applications.

The environment variables USER, LOGNAME, and HOME are not just provided by the daemon but are taken from the file passwd. The PATH variable is set to "/usr/bin:/bin", and SHELL to "/bin/sh". Values for all variables except LOGNAME can be changed in the user tables.

Some environment variables (primarily SHELL and HOME) are used by cron itself to run tasks. Here's an example of using bash instead of the standard sh to run user tasks:

SHELL=/bin/bash
HOME=/tmp/
# exec will be run by bash in /tmp/
* * * * * /path/to/exec

Ultimately, all environment variables defined in the table (used by cron or required by the process) will be passed to the launched task.

To edit files with the crontab utility, the editor specified in the VISUAL or EDITOR environment variable is used. If these variables are not defined in the environment where crontab was started, "/usr/ucb/vi" is used (ucb likely stands for University of California, Berkeley).

cron w Debian i Ubuntu

Debian developers and derivative distributions have released a heavily modified version of Vixie cron 3.0pl1. There are no differences in the table file syntax; for users, it's the same Vixie cron. The biggest new features include support for syslog, SELinux i PAM.

Among less noticeable but tangible changes are the locations of configuration files and task tables.

User tables in Debian are located in the directory /var/spool/cron/crontabs, the system table is still in /etc/crontab. Package-specific task tables for Debian are placed in /etc/cron.d, from where the cron daemon automatically reads them. User access management is regulated by the files /etc/cron.allow and /etc/cron.deny.

The default shell is still /bin/sh, which in Debian is a small POSIX-compatible shell dash, run without reading any configuration (in non-interactive mode).

W najnowszych wersjach Debiana cron jest uruchamiany przez systemd, a konfigurację uruchamiania można znaleźć w /lib/systemd/system/cron.service. W konfiguracji usługi nie ma nic szczególnego, wszelkie bardziej zaawansowane zarządzanie zadaniami można zrealizować za pomocą zmiennych środowiskowych zadeklarowanych bezpośrednio w crontabie każdego z użytkowników.

cronie w RedHat, Fedora i CentOS

cronie — fork Vixie cron w wersji 4.1. Podobnie jak w Debiana, składnia nie została zmieniona, ale dodano wsparcie dla PAM i SELinux, obsługę w klastrze, monitorowanie plików za pomocą inotify i inne możliwości.

Domyślna konfiguracja znajduje się w standardowych lokalizacjach: systemowa tabela — w /etc/crontab, pakiety umieszczają swoje tabele w /etc/cron.d, a tabele użytkowników trafiają do /var/spool/cron/crontabs.

Demon jest uruchamiany pod kontrolą systemd, a konfiguracja usługi znajduje się w /lib/systemd/system/crond.service.

W dystrybucjach podobnych do Red Hat domyślnie używana jest /bin/sh, której rolę pełni standardowy bash. Należy zauważyć, że przy uruchamianiu zadań cron przez /bin/sh powłoka bash jest uruchamiana w trybie zgodnym z POSIX i nie czyta żadnej dodatkowej konfiguracji, działając w trybie nieinteraktywnym.

cronie w SLES i openSUSE

Niemiecka dystrybucja SLES oraz jej pochodna openSUSE korzystają z tego samego cronie. Demon tutaj również jest uruchamiany przez systemd, a konfiguracja usługi znajduje się w /usr/lib/systemd/system/cron.service. Konfiguracja: /etc/crontab, /etc/cron.d, /var/spool/cron/tabs. W roli /bin/sh występuje ten sam bash, uruchomiony w trybie nieinteraktywnym zgodnym z POSIX.

Działanie Vixie cron

Współczesne potomki crona w porównaniu do Vixie crona nie zmieniły się radykalnie, jednak zyskały nowe możliwości, które nie są wymagane do zrozumienia zasad działania programu. Wiele z tych rozszerzeń jest niechlujnie zrealizowanych i wprowadza zamieszanie w kodzie. Oryginalny kod źródłowy crona w wykonaniu Paula Vixiego jest przyjemnością do czytania.

Dlatego postanowiłem przeanalizować strukturę crona na przykładzie wspólnej dla obu gałęzi rozwoju programu crona — Vixie cron 3.0pl1. Przykłady uproszczę, usuwając utrudniające czytanie ifdef-y i pomijając drugorzędne szczegóły.

Pracę demona można podzielić na kilka etapów:

  1. Inicjalizacja programu.
  2. Zbieranie i aktualizacja listy zadań do uruchomienia.
  3. Praca głównej pętli crona.
  4. Uruchomienie zadania.

Przeanalizujmy je kolejno.

Inicjalizacja

Podczas uruchamiania po sprawdzeniu argumentów process cron ustawia obsługiwacze sygnałów SIGCHLD i SIGHUP. Pierwszy zapisuje informacje o zakończeniu pracy procesu potomnego w dzienniku, drugi — zamyka deskryptor pliku dziennika:

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

Demon cron w systemie zawsze działa jako jeden, tylko w roli superużytkownika i z głównego katalogu crona. Następujące wywołania tworzą plik blokady z PID-em procesu demona, upewniają się, że użytkownik jest poprawny i zmieniają bieżący katalog na główny:

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

Ustala się domyślną ścieżkę, która będzie używana podczas uruchamiania procesów:

setenv("PATH", _PATH_DEFPATH, 1);

Następnie proces "demonizuje się": tworzy kopię potomną procesu przez wywołanie fork i nową sesję w procesie potomnym (wywołanie setsid). W procesie rodzicielskim nie ma już potrzeby — i on kończy działanie:

switch (fork()) {
case -1:
    \/\* błąd krytyczny i zakończenie pracy *\/ 
    exit(0);
break;
case 0:
    \/\* proces potomny *\/ 
    (void) setsid();
break;
default:
    \/\* proces rodzicielski kończy pracę *\/ 
    _exit(0);
}

Zakończenie procesu rodzicielskiego zwalnia blokadę na pliku blokady. Ponadto wymagane jest zaktualizowanie PID w pliku na PID potomka. Następnie wypełnia się bazę zadań:

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

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

Następnie cron przechodzi do głównej pętli pracy. Ale przed tym warto przyjrzeć się ładowaniu listy zadań.

Zbieranie i aktualizacja listy zadań

Za ładowanie listy zadań odpowiada funkcja load_database. Sprawdza ona główny crontab systemowy i katalog z plikami użytkowników. Jeśli pliki i katalog się nie zmieniały, lista zadań nie jest ponownie wczytywana. W przeciwnym razie zaczyna się formować nowa lista zadań.

Ładowanie pliku systemowego z specjalnymi nazwami plików i tabel:

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

Ładowanie tabel użytkowników w pętli:

while (NULL != (dp = readdir(dir))) {
    char    fname[MAXNAMLEN+1],
            tabname[MAXNAMLEN+1];
    \/\* nie trzeba czytać plików z kropką *\/ 
    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);
}

Po czym stara baza danych zostaje zastąpiona nową.

W powyższych przykładach wywołanie funkcji process_crontab upewnia się w istnieniu użytkownika odpowiadającego nazwie pliku tabeli (o ile nie jest to superużytkownik), po czym wywołuje load_user. Ta ostatnia już odczytuje sam plik linia po linii:

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

Tutaj albo ustawia się zmienną środowiskową (linie w formacie VAR=value) przy pomocy funkcji load_env / env_set, albo wczytuje się opis zadania (** * * * /path/to/exec) przy pomocy funkcji load_entry.

Entria, którą zwraca load_entry, to nasze zadanie, umieszczone w wspólnej liście zadań. W samej funkcji przeprowadzany jest szczegółowy parser formatu czasu, nas bardziej interesuje tworzenie zmiennych środowiskowych oraz parametrów uruchomienia zadania:

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

Z aktualną listą zadań działa główny cykl.

Główna pętla

Oryginalny cron z Version 7 Unix działał bardzo prosto: w pętli odczytywał konfigurację, uruchamiał zadania bieżącej minuty jako superużytkownik i spał do początku następnej minuty. Takie proste podejście na starych maszynach wymagało zbyt wielu zasobów.

W SysV zaproponowano alternatywną wersję, w której demon zasypiał albo do najbliższej minuty, dla której zdefiniowano zadanie, albo na 30 minut. W takim trybie zużycie zasobów na odczytanie konfiguracji i sprawdzenie zadań było mniejsze, ale szybkie aktualizowanie listy zadań stało się niewygodne.

Vixie cron wrócił do sprawdzania list zadań co minutę, na szczęście pod koniec lat 80. zasobów na standardowych maszynach Unix było znacznie więcej:

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

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

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

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

Bezpośrednim wykonaniem zadań zajmuje się funkcja cron_sleep, wywołująca funkcje job_runqueue (przeglądanie i uruchamianie zadań) oraz do_command (uruchamianie każdego pojedynczego zadania). Ta ostatnia funkcja zasługuje na bardziej szczegółowe omówienie.

Uruchomienie zadania

Funkcja do_command jest napisana w dobrym stylu Unix, tzn. dla asynchronicznego wykonania zadania wykonuje fork. Proces macierzysty kontynuuje uruchamianie zadań, a podrzędny zajmuje się przygotowaniem procesu zadania:

switch (fork()) {
case -1:
    /* nie mogliśmy wykonać fork */
    break;
case 0:
    /* proces podrzędny: na wszelki wypadek jeszcze raz próbujemy zdobyć główny lock */
    acquire_daemonlock(1);
    /* przechodzimy do formowania procesu zadania */
    child_process(e, u);
    /* po zakończeniu proces podrzędny kończy pracę */
    _exit(OK_EXIT);
    break;
default:
    /* proces macierzysty kontynuuje pracę */
    break;
}

W child_process znajduje się sporo logiki: przejmuje standardowe strumienie wyjścia i błędów, aby następnie przesłać je na e-mail (jeśli w tabeli zadań podana jest zmienna środowiskowa MAILTO), a na koniec czeka na zakończenie głównego procesu zadania.

Proces zadania jest tworzony przez kolejny 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;
}

I to w zasadzie cały cron. Ominąłem kilka interesujących szczegółów, na przykład uwzględnienie zdalnych użytkowników, ale najważniejsze przedstawiłem.

Epilog

Cron to zaskakująco prosta i przydatna aplikacja, stworzona w najlepszych tradycjach świata Unix. Nie robi nic zbędnego, ale swoją pracę wykonuje znakomicie od kilku dziesięcioleci. Zaznajomienie się z kodem tej wersji, która jest dostarczana z Ubuntu, zajęło mi nie więcej niż godzinę, a sprawiło mi ogromną przyjemność! Mam nadzieję, że mogłem się nią z Wami podzielić.

Nie wiem, jak Wy, ale mnie nieco smuci świadomość, że nowoczesne programowanie, z jego skłonnością do nadmiernej komplikacji i abstrahowania, już dawno nie sprzyja takiej prostocie.

Istnieje wiele nowoczesnych alternatyw dla crona: systemd-timers pozwala na organizację skomplikowanych systemów z zależnościami, w fcron można elastyczniej regulować zużycie zasobów przez zadania. Ale osobiście zawsze wystarczał mi najprostszy crontab.

Krótko mówiąc, kochajcie Unix, korzystajcie z prostych programów i nie zapominajcie czytać manów dla swojej platformy!

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster