Linux 中的 Cron:歷史、用途和設備

Linux 中的 Cron:歷史、用途和設備

經典寫道,歡樂時光不看。 在那些狂野的時代,既沒有程式設計師,也沒有 Unix,但今天程式設計師肯定知道:cron 將代替他們來記錄時間。

命令列實用程式對我來說既是一個弱點又是一件苦差事。 sed、awk、wc、cut 等舊程式每天都在我們的伺服器上透過腳本運行。 其中許多是為 cron(源自 70 年代的調度程序)設計的任務。

很長一段時間我對 cron 的使用都比較膚淺,沒有深究細節,但是有一天,當我運行腳本時遇到錯誤時,我決定徹底研究一下。 這篇文章就是這樣出現的,在寫這篇文章的過程中,我開始熟悉 POSIX crontab、流行 Linux 發行版中的主要 cron 選項以及其中一些選項的結構。

您使用 Linux 並運行 cron 任務嗎? 您對 Unix 中的系統應用程式架構感興趣嗎? 然後我們就上路了!

Содержание

物種起源

在所有作業系統中,週期性執行使用者或系統程式顯然是必要的。 因此,程式設計師很早就意識到需要能夠讓他們集中規劃和執行任務的服務。

類 Unix 作業系統的起源可以追溯到上世紀 7 年代貝爾實驗室開發的 Unix 版本 70,其中包括著名的 Ken Thompson。 Unix 7 版本還包含 cron,這是一項定期執行超級使用者任務的服務。

典型的現代 cron 是一個簡單的程序,但原始版本的操作演算法更簡單:服務每分鐘喚醒一次,從單個文件(/etc/lib/crontab)讀取包含任務的表並執行超級用戶那些當前應該執行的任務。

隨後,所有類別 Unix 作業系統都提供了這個簡單而有用的服務的改進版本。

1992 年,對 crontab 格式的概括描述以及該實用程式操作的基本原理被納入類 Unix 作業系統的主要標準 - POSIX - 中,因此 cron 從事實上的標準變成了法律上的標準。

1987年,Paul Vixie在調查了Unix使用者對cron的願望後,發布了守護程式的另一個版本,修正了傳統cron的一些問題並擴展了表格檔案的語法。

到了 Vixie cron 的第三個版本開始滿足 POSIX 要求,此外,該程序擁有自由許可證,或者更確切地說根本沒有許可證,除了自述文件中的願望:作者不提供保證,作者姓名無法刪除,程序只能與原始碼一起出售。 事實證明,這些要求與當時流行的自由軟體的原則是相容的,因此 90 年代初出現的一些主要 Linux 發行版都將 Vixie cron 作為其係統之一,並且至今仍在開發中。

特別是,Red Hat和SUSE開發了Vixie cron的一個分支-cronie,而Debian和Ubuntu則使用帶有許多補丁的Vixie cron的原始版本。

讓我們先熟悉 POSIX 中所述的使用者實用程式 crontab,然後我們將了解 Vixie cron 中提供的語法擴充以及 Vixie cron 變體在流行 Linux 發行版中的使用。 最後,錦上添花的是 cron 守護程式設備的分析。

POSIX 計時任務

如果說原來的 cron 總是為超級用戶工作,那麼現代的調度程序通常會處理普通用戶的任務,這更加安全和方便。

Cron 作為兩個程式的集合提供:持續運行的 cron 守護程式和使用者可用的 crontab 公用程式。 後者可讓您編輯特定於系統中每個使用者的任務表,而守護程式則從使用者和系統表啟動任務。

В POSIX標準 守護程序的行為沒有以任何方式描述,只有使用者程式被形式化 crontab中。 當然,啟動使用者任務的機制的存在是隱含的,但沒有詳細描述。

透過呼叫 crontab 實用程序,您可以執行四件事:在編輯器中編輯使用者的任務表、從檔案載入該表、顯示目前任務表以及清除任務表。 crontab 實用程式如何運作的範例:

crontab -e # редактировать таблицу задач
crontab -l # показать таблицу задач
crontab -r # удалить таблицу задач
crontab path/to/file.crontab # загрузить таблицу задач из файла

打電話時 crontab -e 將使用標準環境變數中指定的編輯器 EDITOR.

任務本身以以下格式描述:

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

記錄的前五個欄位:分鐘[1..60]、小時[0..23]、一月中的天數[1..31]、月數[1..12]、一週中的天數[0 ..6]. .0],其中 XNUMX 是星期日。 最後第六個欄位是將由標準命令解釋器執行的一行。

在前五個欄位中,可以用逗號分隔列出值:

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

或用連字符:

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

在 POSIX 中,使用者對任務排程的存取由 cron.allow 和 cron.deny 檔案控制,這兩個檔案分別列出了有權存取 crontab 的使用者和無權存取該程式的使用者。 該標準不以任何方式規範這些文件的位置。

根據標準,至少必須將四個環境變數傳遞給啟動的程序:

  1. HOME - 使用者的主目錄。
  2. LOGNAME — 使用者登入。
  3. PATH 是您可以找到標準系統實用程式的路徑。
  4. SHELL — 使用的命令解釋器的路徑。

值得注意的是,POSIX 沒有說明這些變數的值來自哪裡。

暢銷書 - Vixie cron 3.0pl1

流行的 cron 變體的共同祖先是 Vixie cron 3.0pl1,於 1992 年在 comp.sources.unix 郵件列表中引入。 我們將更詳細地考慮該版本的主要功能。

Vixie cron 有兩個程式(cron 和 crontab)。 像往常一樣,守護程序負責從系統任務表和個人使用者任務表中讀取和執行任務,而 crontab 公用程式則負責編輯使用者表。

任務表和設定檔

超級使用者任務表位於/etc/crontab 中。 系統表的語法與 Vixie cron 的語法相對應,但其中的第六列表示代表任務啟動的使用者名稱:

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

常規使用者任務表位於 /var/cron/tabs/username 中並使用相同的語法。 當您以使用者身分執行 crontab 公用程式時,這些是被編輯的檔案。

有權存取 crontab 的使用者清單在 /var/cron/allow 和 /var/cron/deny 檔案中進行管理,您只需在其中單獨的行中輸入使用者名稱即可。

擴展語法

與 POSIX crontab 相比,Paul Vixey 的解決方案包含對該實用程式任務表語法的一些非常有用的修改。

新的表格語法已可用:例如,您可以按名稱指定一周或幾個月的日期(週一、週二等):

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

您可以指定啟動任務的步驟:

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

步驟和間隔可以混合:

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

支援常用語法的直覺替代方案(重新啟動、每年、每年、每月、每週、每天、午夜、每小時):

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

任務執行環境

Vixie cron 允許您更改執行應用程式的環境。

環境變數 USER、LOGNAME 和 HOME 不是簡單地由守護程序提供,而是從檔案中取得 passwd文件。 PATH 變數設定為“/usr/bin:/bin”,SHELL 變數設定為“/bin/sh”。 除了LOGNAME之外的所有變數的值都可以在使用者表中變更。

cron 本身使用一些環境變數(最顯著的是 SHELL 和 HOME)來執行任務。 使用 bash 而不是標準 sh 來執行自訂任務可能如下所示:

SHELL=/bin/bash
HOME=/tmp/
# exec будет запущен bash-ем в /tmp/
* * * * * /path/to/exec

最終,表中定義的所有環境變數(由 cron 使用或進程需要)都會傳遞給正在執行的任務。

若要編輯文件,crontab 使用 VISUAL 或 EDITOR 環境變數中指定的編輯器。 如果運行 crontab 的環境沒有定義這些變量,則使用「/usr/ucb/vi」(ucb 可能是加州大學柏克萊分校)。

Debian 和 Ubuntu 上的 cron

Debian 及其衍生發行版的開發者已經發布了 高度修改版本 Vixie cron 版本 3.0pl1。 表文件的語法沒有差異;對於使用者來說,它是相同的 Vixie cron。 最大的新功能:支持 系統日誌, SELinux的 и PAM.

不太明顯但切實的變化包括設定檔和任務表的位置。

Debian 中的使用者表位於 /var/spool/cron/crontabs 目錄中,系統表仍然存在 - 在 /etc/crontab 中。 Debian 軟體包特定的任務表放置在 /etc/cron.d 中,cron 守護程式會自動從其中讀取它們。 使用者存取控制由 /etc/cron.allow 和 /etc/cron.deny 檔案控制。

預設 shell 仍然是 /bin/sh,在 Debian 中它是一個小型的 POSIX 相容 shell 短跑,在不讀取任何配置的情況下啟動(在非交互模式下)。

最新版本的 Debian 中的 Cron 本身是透過 systemd 啟動的,啟動配置可以在 /lib/systemd/system/cron.service 中查看。 服務配置沒有什麼特別的;任何更微妙的任務管理都可以透過直接在每個使用者的 crontab 中聲明的環境變數來完成。

RedHat、Fedora 和 CentOS 上的 cronie

cronie — Vixie cron 版本 4.1 的分支。 與 Debian 一樣,語法沒有改變,但添加了對 PAM 和 SELinux、在叢集中工作、使用 inotify 追蹤檔案和其他功能的支援。

預設配置位於通常的位置:系統表位於 /etc/crontab 中,軟體包將其表放在 /etc/cron.d 中,使用者表位於 /var/spool/cron/crontabs 中。

此守護程式在systemd控制下執行,服務配置為/lib/systemd/system/crond.service。

在類似 Red Hat 的發行版上,啟動時預設使用 /bin/sh,這是標準的 bash。 需要注意的是,當透過 /bin/sh 執行 cron 作業時,bash shell 以 POSIX 相容模式啟動,且不讀取任何附加配置,以非互動模式運作。

SLES 和 openSUSE 中的 cronie

德國發行版 SLES 及其衍生版本 openSUSE 使用相同的 cronie。 這裡的守護程式也是在systemd下啟動的,服務配置位於/usr/lib/systemd/system/cron.service。 配置:/etc/crontab、/etc/cron.d、/var/spool/cron/tabs。 /bin/sh 與在 POSIX 相容的非互動模式下運行的 bash 相同。

Vixie cron 設備

與 Vixie cron 相比,現代 cron 的後代並沒有根本性的變化,但仍然獲得了理解程序原理不需要的新功能。 其中許多擴充設計不佳,並且使程式碼混亂。 Paul Vixey 的原始 cron 原始碼讀起來很愉快。

因此,我決定使用兩個開發分支通用的 cron 程式範例來分析 cron 設備 - Vixie cron 3.0pl1。 我將透過刪除使閱讀變得複雜的 ifdef 並省略次要細節來簡化範例。

惡魔的工作可以分為幾個階段:

  1. 程序初始化。
  2. 收集並更新要執行的任務清單。
  3. 主 cron 循環正在運行。
  4. 開始一個任務。

讓我們按順序看一下它們。

初始化

啟動時,檢查進程參數後,cron 會安裝 SIGCHLD 和 SIGHUP 訊號處理程序。 第一個建立有關子程序終止的日誌條目,第二個關閉日誌檔案的檔案描述符:

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

cron 守護程式始終在系統上單獨運行,僅作為超級使用者並從主 cron 目錄運行。 以下呼叫使用守護程序的 PID 建立鎖定文件,確保使用者正確並將當前目錄變更為主目錄:

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

設定預設路徑,啟動進程時將使用該路徑:

setenv("PATH", _PATH_DEFPATH, 1);

然後該進程被「守護進程」:它透過呼叫 fork 來建立該進程的子副本並在子進程中建立一個新會話(呼叫setsid)。 不再需要父進程,它退出:

switch (fork()) {
case -1:
    /* критическая ошибка и завершение работы */
    exit(0);
break;
case 0:
    /* дочерний процесс */
    (void) setsid();
break;
default:
    /* родительский процесс завершает работу */
    _exit(0);
}

父進程的終止會釋放鎖定檔案上的鎖定。 另外,需要將文件中的PID更新為child。 之後,任務資料庫被填滿:

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

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

然後 cron 進入主工作週期。 但在此之前,值得先看一下載入任務清單。

收集並更新任務列表

load_database 函數負責載入任務清單。 它檢查主系統 crontab 和包含使用者檔案的目錄。 如果檔案和目錄沒有改變,則不會重新讀取任務清單。 否則,就會開始形成新的任務清單。

載入具有特殊檔案名稱和表名的系統檔案:

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

循環載入用戶表:

while (NULL != (dp = readdir(dir))) {
    char    fname[MAXNAMLEN+1],
            tabname[MAXNAMLEN+1];
    /* читать файлы с точкой не надо*/
    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);
}

之後舊資料庫將替換為新資料庫。

在上面的範例中,process_crontab 函數呼叫驗證是否存在與表格檔案名稱相符的使用者(除非它是超級使用者),然後呼叫 load_user。 後者已經逐行讀取文件本身:

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

在這裡,要么使用 load_env / env_set 函數設定環境變數(VAR=value 形式的行),要么使用 load_entry 函數讀取任務描述(* * * * * /path/to/exec)。

load_entry傳回的條目實體是我們的任務,它被放置在任務的通用清單中。 函數本身對時間格式進行了詳細的解析,但我們更感興趣的是環境變數和任務啟動參數的形成:

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

主循環處理目前任務清單。

主循環

Unix 版本 7 中的原始 cron 工作起來非常簡單:它循環重新讀取配置,以超級用戶身份啟動當前分鐘的任務,然後休眠直到下一分鐘開始。 這種在舊機器上的簡單方法需要太多資源。

SysV 中提出了一個替代版本,其中守護程序進入休眠狀態,直到定義任務的最近一分鐘,或 30 分鐘。 在這種模式下重新讀取配置和檢查任務消耗的資源較少,但快速更新任務清單變得不方便。

Vixie cron 恢復每分鐘檢查一次任務列表,幸運的是,到了 80 年代末,標準 Unix 機器上的資源顯著增加:

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

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

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

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

cron_sleep函數直接參與執行任務,呼叫job_runqueue(枚舉並執行任務)和do_command(執行每個單獨的任務)函數。 最後一個函數值得更詳細地研究。

運行任務

do_command 函數以良好的 Unix 風格執行,即它執行 fork 來非同步執行任務。 父行程繼續啟動任務,子行程準備任務流程:

switch (fork()) {
case -1:
    /*не смогли выполнить fork */
    break;
case 0:
    /* дочерний процесс: на всякий случай еще раз пробуем захватить главный лок */
    acquire_daemonlock(1);
    /* переходим к формированию процесса задачи */
    child_process(e, u);
    /* по завершению дочерний процесс заканчивает работу */
    _exit(OK_EXIT);
    break;
default:
    /* родительский процесс продолжает работу */
    break;
}

child_process 中有相當多的邏輯:它接收標準輸出和錯誤流到自身,然後將其發送到郵件(如果任務表中指定了 MAILTO 環境變數),最後等待 main任務完成的過程。

任務進程由另一個fork形成:

switch (vfork()) {
case -1:
    /* при ошибки сразу завершается работа */
    exit(ERROR_EXIT);
case 0:
    /* процесс-внук формирует новую сессию, терминал и т.д.
     */
    (void) setsid();

    /*
     * дальше многословная настройка вывода процесса, опустим для краткости
     */

    /* смена директории, пользователя и группы пользователя,
     * то есть процесс больше не суперпользовательский
     */
    setgid(e->gid);
    setuid(e->uid);
    chdir(env_get("HOME", e->envp));

    /* запуск самой команды
     */
    {
        /* переменная окружения SHELL указывает на интерпретатор для запуска */
        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;
}

這基本上就是 cron 的全部內容。 我省略了一些有趣的細節,例如,遠端使用者的統計,但我概述了主要內容。

後記

Cron 是一個非常簡單且有用的程序,它繼承了 Unix 世界的最佳傳統。 她沒有做任何額外的事情,但幾十年來她一直出色地完成自己的工作。 讀完Ubuntu自備版本的程式碼只花了不到一個小時,而且我玩得很開心! 我希望我能夠與你分享。

我不了解你的情況,但我有點難過地意識到,現代程式設計往往過於複雜和過於抽象,長期以來一直不利於這種簡單性。

cron 有許多現代替代方案:systemd-timers 可讓您組織具有依賴關係的複雜系統,fcron 允許您更靈活地調節任務的資源消耗。 但就我個人而言,最簡單的 crontab 對我來說總是足夠的。

簡而言之,熱愛 Unix,使用簡單的程序,並且不要忘記閱讀適用於您的平台的 mana!

來源: www.habr.com

添加評論