Linux の Cron: 歴史、使用法、デバイス

Linux の Cron: 歴史、使用法、デバイス

古典は、幸せな時間は見ていないと書きました。 あの乱暴な時代にはプログラマーも Unix も存在しませんでしたが、今日のプログラマーは、プログラマーの代わりに cron が時間を追跡することを確信しています。

コマンドラインユーティリティは私にとって弱点でもあり面倒でもあります。 sed、awk、wc、cut、その他の古いプログラムは、サーバー上のスクリプトによって毎日実行されます。 それらの多くは、もともと 70 年代のスケジューラーである cron のタスクとして設計されています。

長い間、cron を詳しく説明することなく表面的に使用していましたが、ある日、スクリプトの実行中にエラーが発生したため、徹底的に調べてみることにしました。 この記事はこのように書かれていますが、この記事を書いているうちに、一般的な Linux ディストリビューションの主要な cron オプションである POSIX crontab とその一部の構造について詳しくなりました。

Linux を使用していて cron タスクを実行していますか? Unix のシステム アプリケーション アーキテクチャに興味がありますか? それでは出発です!

ページ内容

種の起源

ユーザー プログラムまたはシステム プログラムの定期的な実行は、すべてのオペレーティング システムで明らかに必要です。 したがって、プログラマーは、ずっと前からタスクを一元的に計画して実行できるサービスの必要性を認識していました。

Unix に似たオペレーティング システムの起源は、前世紀の 7 年代に有名な Ken Thompson を含むベル研究所で開発されたバージョン 70 Unix に遡ります。 バージョン 7 Unix には、スーパーユーザー タスクを定期的に実行するためのサービスである cron も含まれていました。

典型的な現代の cron は単純なプログラムですが、元のバージョンの動作アルゴリズムはさらに単純でした。サービスは XNUMX 分に XNUMX 回起動し、単一のファイル (/etc/lib/crontab) からタスクを含むテーブルを読み取り、スーパーユーザー 現時点で実行されるべきタスク。

その後、このシンプルで便利なサービスの改良版が、すべての Unix 系オペレーティング システムに提供されました。

crontab 形式の一般的な説明とユーティリティの動作の基本原理は、1992 年に Unix 系オペレーティング システムの主要標準である POSIX に組み込まれ、事実上の標準である cron が法定の標準になりました。

1987 年、Paul Vixie は Unix ユーザーに cron への要望を調査し、従来の cron の問題の一部を修正し、テーブル ファイルの構文を拡張した別のバージョンのデーモンをリリースしました。

Vixie cron の 90 番目のバージョンでは、POSIX 要件を満たし始めました。さらに、このプログラムには自由なライセンスが与えられました。あるいは、README に記載されている希望を除いて、ライセンスはまったくありませんでした。作者は保証を提供しません。作者の名前は保証されません。削除することはできず、プログラムはソース コードと一緒にのみ販売できます。 これらの要件は、当時人気を博していたフリー ソフトウェアの原理と互換性があることが判明したため、XNUMX 年代初頭に登場した主要な Linux ディストリビューションのいくつかは Vixie cron をシステム ディストリビューションとして採用し、現在も開発を続けています。

特に、Red Hat と SUSE は Vixie cron のフォーク (cronie) を開発し、Debian と Ubuntu は多くのパッチが適用された Vixie cron のオリジナル エディションを使用します。

まず、POSIX で説明されているユーザー ユーティリティ crontab について理解しましょう。その後、Vixie cron で提供される構文拡張機能と、一般的な Linux ディストリビューションでの Vixie cron のバリエーションの使用について見ていきます。 そして最後の目玉は、cron デーモン デバイスの分析です。

POSIX crontab

元の cron が常にスーパーユーザーのために機能する場合、最新のスケジューラーは通常のユーザーのタスクを処理することが多く、これはより安全で便利です。

cron は、常時実行される cron デーモンとユーザーが使用できる crontab ユーティリティの XNUMX つのプログラムのセットとして提供されます。 後者では、システム内の各ユーザーに固有のタスク テーブルを編集できる一方、デーモンはユーザー テーブルとシステム テーブルからタスクを起動します。

В POSIX標準 デーモンの動作は一切記述されておらず、ユーザープログラムのみが形式化されています。 crontab。 もちろん、ユーザータスクを起動するためのメカニズムの存在は暗示されていますが、詳細は説明されていません。

crontab ユーティリティを呼び出すと、エディタでユーザーのタスク テーブルを編集する、ファイルからテーブルをロードする、現在のタスク テーブルを表示する、タスク テーブルをクリアするという XNUMX つのことを行うことができます。 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]、XNUMXは日曜日です。 最後の XNUMX 番目のフィールドは、標準のコマンド インタープリタによって実行される行です。

最初の XNUMX つのフィールドでは、値をカンマで区切ってリストできます。

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

またはハイフンを使用して:

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

タスク スケジュールへのユーザー アクセスは、POSIX では cron.allow および cron.deny ファイルによって規制されており、これらのファイルには、それぞれ crontab にアクセスできるユーザーとプログラムにアクセスできないユーザーがリストされています。 この規格では、これらのファイルの場所は一切規制されていません。

標準に従って、起動されたプログラムには少なくとも XNUMX つの環境変数を渡す必要があります。

  1. HOME - ユーザーのホームディレクトリ。
  2. LOGNAME — ユーザーのログイン。
  3. PATH は、標準システム ユーティリティを見つけることができるパスです。
  4. SHELL — 使用されるコマンド インタプリタへのパス。

注目すべきことに、POSIX はこれらの変数の値がどこから来たのかについて何も述べていません。

ベストセラー - Vixie cron 3.0pl1

一般的な cron 亜種の共通の祖先は、3.0 年に comp.sources.unix メーリング リストに導入された Vixie cron 1pl1992 です。 このバージョンの主な機能をさらに詳しく検討します。

Vixie cron には XNUMX つのプログラム (cron と crontab) があります。 通常どおり、デーモンはシステム タスク テーブルと個々のユーザー タスク テーブルからのタスクの読み取りと実行を担当し、crontab ユーティリティはユーザー テーブルの編集を担当します。

タスクテーブルと設定ファイル

スーパーユーザー タスク テーブルは /etc/crontab にあります。 システム テーブルの構文は、Vixie cron の構文に対応していますが、その XNUMX 番目の列が、タスクを起動するユーザーの名前を示している点が異なります。

# Запускается ежеминутно от пользователя 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

通常の構文に代わる直感的な代替手段がサポートされています (再起動、毎年、毎年、毎月、毎週、毎日、午前 XNUMX 時、毎時)。

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

タスク実行環境

Vixie cron を使用すると、実行中のアプリケーションの環境を変更できます。

環境変数 USER、LOGNAME、HOME はデーモンによって単に提供されるのではなく、ファイルから取得されます。 passwd。 PATH 変数は「/usr/bin:/bin」に設定され、SHELL 変数は「/bin/sh」に設定されます。 LOGNAMEを除くすべての変数の値はユーザーテーブルで変更できます。

一部の環境変数 (特に SHELL と HOME) は、タスクを実行するために cron 自体によって使用されます。 標準の sh の代わりに bash を使用してカスタム タスクを実行すると、次のようになります。

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 です。 最大の新機能: サポート syslog, SELinuxの и PAM.

あまり目立ちませんが、具体的な変更には、構成ファイルとタスク テーブルの場所が含まれます。

Debian のユーザー テーブルは /var/spool/cron/crontabs ディレクトリにあり、システム テーブルは /etc/crontab にまだ存在します。 Debian パッケージ固有のタスク テーブルは /etc/cron.d に配置され、cron デーモンがそこから自動的に読み取ります。 ユーザーのアクセス制御は、/etc/cron.allow および /etc/cron.deny ファイルによって制御されます。

デフォルトのシェルは依然として /bin/sh であり、Debian ではこれは小さな POSIX 準拠のシェルです。 ダッシュ、設定を読み取らずに起動されます (非対話モード)。

最新バージョンの Debian の Cron 自体は systemd 経由で起動され、起動設定は /lib/systemd/system/cron.service で確認できます。 サービスの設定には特別なことは何もありません。さらに微妙なタスク管理は、各ユーザーの crontab で直接宣言された環境変数を通じて行うことができます。

RedHat、Fedora、CentOS 上の 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 シェルは 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 デバイス

cron の現代の子孫は、Vixie cron と比べて根本的には変わっていませんが、プログラムの原理を理解する必要のない新しい機能を獲得しています。 これらの拡張機能の多くは設計が不十分で、コードを混乱させます。 Paul Vixey によるオリジナルの cron ソース コードは、読むのが楽しいです。

したがって、両方の開発ブランチに共通する cron プログラムの例、Vixie cron 3.0pl1 を使用して cron デバイスを分析することにしました。 読み取りを複雑にする ifdef を削除し、細かい詳細を省略することで、例を簡略化します。

悪魔の働きはいくつかの段階に分けられます。

  1. プログラムの初期化。
  2. 実行するタスクのリストを収集して更新します。
  3. メイン cron ループが実行中。
  4. タスクを開始します。

順番に見ていきましょう。

初期化

cron が開始されると、プロセス引数を確認した後、SIGCHLD および SIGHUP シグナル ハンドラーがインストールされます。 最初のものは子プロセスの終了に関するログ エントリを作成し、XNUMX つ目はログ ファイルのファイル記述子を閉じます。

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 を子に更新する必要があります。 この後、タスク データベースにデータが入力されます。

/* повторный захват лока */
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 を呼び出します。 後者はすでにファイル自体を XNUMX 行ずつ読み取ります。

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

メイン ループは、現在のタスクのリストを処理します。

メインループ

バージョン 7 Unix の元の cron は非常に単純に動作しました。ループ内で構成を再読み込みし、スーパーユーザーとして現在の分のタスクを起動し、次の分の開始までスリープしていました。 古いマシンでのこの単純なアプローチには、多すぎるリソースが必要でした。

SysV では代替バージョンが提案されており、デーモンはタスクが定義されている最も近い分まで、または 30 分間スリープ状態になります。 このモードでは、構成の再読み取りとタスクのチェックに消費されるリソースは少なくなりますが、タスクのリストをすぐに更新するのは不便になりました。

Vixie cron はタスク リストを 80 分に XNUMX 回チェックするように戻りました。幸いなことに XNUMX 年代の終わりまでに、標準的な 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 スタイルで実行されます。つまり、フォークを実行してタスクを非同期に実行します。 親プロセスはタスクの起動を続行し、子プロセスはタスク プロセスを準備します。

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

child_process には非常に多くのロジックがあります。標準出力とエラー ストリームを受け取り、それらをメールに送信し (タスク テーブルに MAILTO 環境変数が指定されている場合)、最後にタスクのメイン プロセスが送信するのを待ちます。完了。

タスク プロセスは別のフォークによって形成されます。

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 に付属のバージョンのコードを理解するのに XNUMX 時間もかかりませんでした。とても楽しかったです。 それを皆さんと共有できれば幸いです。

あなたはどうか知りませんが、現代のプログラミングは過度に複雑で過度に抽象化する傾向があり、長い間、そのような単純さを実現できていなかったことを知り、少し残念に思っています。

cron に代わる最新の手段は数多くあります。systemd-timers を使用すると、依存関係のある複雑なシステムを整理でき、fcron を使用すると、タスクによるリソース消費をより柔軟に制御できます。 しかし個人的には、最も単純な crontab で常に十分でした。

つまり、Unix を愛し、シンプルなプログラムを使用し、プラットフォームのマナを読むことを忘れないでください。

出所: habr.com

コメントを追加します