RHEL6/RHEL7ごとのanacronの比較をしてみた

Linuxの設計にあたり、こんな記事を見つけてしまった。
[細かすぎて伝わらない anacron の挙動](http://qiita.com/ohtsuka1317/items/b89ea66f3d227c172477)

内容をかいつまんで説明すると、
>CentOS 6 に付属の cronie-anacron-1.4.4　の動きが特徴的だということと、RHELの公式リファレンスに書かれた「最短の遅延時間は、デフォルトで 6 分に」がソースにのってないじゃないか！

という話

## 環境

今回自分が構築する環境は以下の2つ

|OS|rpmバージョン|
|:--|:---|
|RHEL6.7|cronie-anacron-1.4.4-15.el6.x86_64|
|RHEL7.2|cronie-anacron-1.4.11-14.el7.x86_64|

それぞれ見て行きたいと思う。

## ソースの取得方法

### ダウンロード

探すのに、苦労した。
最終的には以下のサイトで取得できた。

- [rpmfind](https://rpmfind.net/linux/RPM/index.html)
- [RPM phone.net](http://rpm.pbone.net/)
 
今回はソースコードを取得するため、**RPM**ではなく**SRPM**を取得する。
※SRPM=RPMを作成可能なソースコード　xxxx.src.rpm という名前

- [cronie-1.4.4-15.el6.src.rpm](http://vault.centos.org/6.7/os/Source/SPackages/cronie-1.4.4-15.el6.src.rpm)
- [cronie-1.4.11-14.el7.src.rpm](ftp://bo.mirror.garr.it/2/scientific/7x/SRPMS/vendor/cronie-1.4.11-14.el7.src.rpm)

※anacronのSRPMはcronのものと一緒くたになっているらしい


### ソースゲット！

ここを参考にした。
[第10回「ソースコード閲覧ノススメ」](http://www.intellilink.co.jp/article/column/oss10.html)

実際の手順はこちら

```
[root@rhel6 ~]# wget http://vault.centos.org/6.7/os/Source/SPackages/cronie-1.4.4-15.el6.src.rpm
[root@rhel6 ~]# yum -y install yum-utils rpm-build
[root@rhel6 ~]# yum -y libselinux-devel pam-devel audit-libs-devel
[root@rhel6 ~]# yum-builddep -y --nogpgcheck cronie-1.4.4-15.el6.src.rpm
[root@rhel6 ~]# rpm -ivh cronie-1.4.4-15.el6.src.rpm
[root@rhel6 ~]# rpmbuild -bp ~/rpmbuild/SPECS/cronie.spec
[root@rhel6 ~]# cd /root/rpmbuild/BUILD/cronie-1.4.4/anacron
[root@rhel6 anacron]# ls
Makefile.am  global.h  gregor.h  log.c   main.c.624043  matchrx.c.null-deref  readtab.c             runjob.c
Makefile.in  gregor.c  lock.c    main.c  matchrx.c      matchrx.h             readtab.c.null-deref  runjob.c.commoncriteria
```

## 動作の確認

### 最短の遅延時間は、デフォルトで6分？

以下が正しいか確認する。

>RANDOM_DELAY — ジョブごとに指定されている delay in minutes 変数に追加される最大の時間分数。最短の遅延時間は、デフォルトで 6 分に設定されています。
引用元：[RedHatEnterpriseLinux6 公式リファレンス](https://access.redhat.com/documentation/ja-JP/Red_Hat_Enterprise_Linux/6/html/Deployment_Guide/ch-Automating_System_Tasks.html)

### 結論

RHEL6/7いずれとも、anacronの遅延時間のアルゴリズムは**/etc/anacrontab**の`RANDOM_DELAY`と各ジョブの`delay in minutes`に準拠しており、最短6分のコードは見つからなかった。

### 調査

#### RHEL6のケース

value(RANDOM_DELAY)をunbiased_rand関数に渡してランダム値を生成し、random_numberに格納しています。

```:readtab.c
        else if (strncmp(env_var, "RANDOM_DELAY", 12) == 0) {
            r = match_rx("^([[:digit:]]+)$", value, 0);
            if (r == -1) goto reg_err;
            if (r == 0) goto reg_invalid;

            random_number = (int)unbiased_rand(atoi(value));
            Debug(("Randomized delay set: %d", random_number));

```

ランダム値を生成するunbiased_rand関数を見ても特に**6分**の記載はありません。

```:readtab.c
static long int
unbiased_rand(long int max)
{
    long int rn;
    long int divisor;

    divisor = RAND_MAX / (max + 1);

    do {
        rn = random() / divisor;
    } while (rn > max);

    return rn;
}
```

 delay(anacrontabのjobで定義されたdelay in minutes)にrandom_numberを追加しています。

```:readtab.c
    if (period < 0 || delay < 0)
    {
        complain("%s: number out of range on line %d, skipping",
                 anacrontab, line_num);
        return;
    }
    jr = obstack_alloc(&tab_o, sizeof(job_rec));
    jr->period = period;
    jr->named_period = 0;
    delay += random_number;


```

そしてmain.cではdelayを使用してジョブの実行タイミングが取得されるため、+6分されていません。

#### RHEL7のケース

value(RANDOM_DELAY)とRAND_MAXを使用してランダム値を生成し、random_numberに入れています。RHEL6との違いは関数をローカルにベタ書きしたということと、double型を使用していることぐらいでしょうか。
6分に関する記載はありません。

```:readtab.c
        if (strncmp(env_var, "RANDOM_DELAY", 12) == 0) {
            r = match_rx("^([[:digit:]]+)$", value, 0);
            if (r == -1) goto reg_err;
            if (r == 0) goto reg_invalid;
            if (r != -1) {
                int i = random();
                double x = 0;
                x = (double) i / (double) RAND_MAX * (double) (atoi(value));
                random_number = (int)x;
                Debug(("Randomized delay set: %d", random_number));
            }
        }
```

delayにrandom_numberを追加している処理は、RHEL6とソースが同一のためこちらも6分に関する記載はありません。
つまりRHEL6/7いずれともソースコードを見る限りは**最短6分**は無いと考えられます。

## おまけ
### anacron -s について

```
[root@docker ~]# more /etc/cron.hourly/0anacron
#!/bin/sh
# Check whether 0anacron was run today already
if test -r /var/spool/anacron/cron.daily; then
    day=`cat /var/spool/anacron/cron.daily`
fi
if [ `date +%Y%m%d` = "$day" ]; then
    exit 0;
fi

# Do not run jobs when on battery power
if test -x /usr/bin/on_ac_power; then
    /usr/bin/on_ac_power >/dev/null 2>&1
    if test $? -eq 1; then
    exit 0
    fi
fi
/usr/sbin/anacron -s
```

**anacro -s**で呼び出されているため、anacronは「ジョブの実行をシリアル化し、前の処理が終了する前に、 anacronのは、新しいジョブを開始しません。

```
    for(j = 0; j < njobs; ++j)
    {
        xsleep(time_till(job_array[j]));
        if (serialize) wait_jobs(); ←
        launch_job(job_array[j]);
    }
```


### RHEL 6と7の挙動の違い

#### (job起動時間＋遅延時間) が現在時刻より1日以上先の場合
---
|OS|挙動|
|:--|:--|
|RHEL6|jobのsleepの待機値が (job起動時間＋遅延時間) - 現在時刻分|
|RHEL7|jobのsleepの待機値が0|

下記の記載はRHEL6にはありません。

```:RHEL7のmain.c
    if (now) return 0;
    tn = time(NULL);
    tj = start_sec + jr->delay * 60;
    if (tj < tn) return 0;

#ここからRHEL7にしかない

    if (tj - tn > 3600*24)
    {
	explain("System time manipulation detected, job `%s' will run immediately",
	    jr->ident);
	return 0;
    }
```

**(Job起動時間＋遅延時間) - 現在時刻** に1日以上(3600秒×24時間)差がある場合には遅延時間なし（returnが0）になっています。

start_secはanacronを起動したときにtime(tnと同じ）が割り当てられますので、ほとんど無いとは思うのですが、start_secにtimeを割り当てた時とtnに割り当てた時で差が大きすぎる場合（NTPがおかしくなった場合、時間が巻き戻った場合）や、遅延時間(delay)が大きすぎて1日以上間が開く場合の対応なんでしょうか。

#### ファイルロックの違い
---

>しかし、anacron の動作として
>
>1. /var/spool/anacron/ 以下にあるジョブ名と同じ名前のファイルを読み、その中に書かれている前回実行日付から、ジョブを実行対象とするかどうかを判定する。たとえば週次ジョブなら、 /var/spool/anacron/cron.weekly ファイルに記載の前回実行日から7日経過しているかを判定する。
> <br>

>2. 所定の日数が経過していて実行対象と判定されたジョブについて、この前回実行日ファイルをロックする。
> <br>
>3. この後で、前述の START_HOURS_RANGE のチェックを行う。実行対象外と判定されたジョブについても、anacron 自体の完了まで前回実行日ファイルはアンロックされない。
><br>という順序になっているため、前述のように 2:01 の回の anacron が 3:01 以降まで cron.weekly の起動待ちになっている場合、実際には起動されない cron.daily の前回実行日ファイルも一緒にロックされたままになっている。
>引用：[細かすぎて伝わらない anacron の挙動](http://qiita.com/ohtsuka1317/items/b89ea66f3d227c172477)

RHEL6だと上記の通りで、`weekly`のせいで`daily`のジョブが実行されません。

```bash:messages
anacron -s を実行

Jan 20 09:50:19 rhel6 anacron[2627]: Anacron started on 2016-01-20
Jan 20 09:50:19 rhel6 anacron[2627]: Will run job `cron.weekly' in 65 min.
Jan 20 09:50:19 rhel6 anacron[2627]: Jobs will be executed sequentially


anacron -s を実行

Jan 20 09:50:36 rhel6 anacron[2630]: Anacron started on 2016-01-20
Jan 20 09:50:36 rhel6 anacron[2630]: Job `cron.weekly' locked by another anacron - skipping
Jan 20 09:50:36 rhel6 anacron[2630]: Normal exit (0 jobs run)
```

RHEL7だと、lockの制御が修正されており`weekly`がlockしていても`daily`には影響が出ない作りになっています。

```bash:messages
anacron -s を実行

Jan 20 18:42:24 master anacron[374]: Anacron started on 2016-01-20
Jan 20 18:42:24 master anacron[374]: Will run job `cron.daily' in 0 min.
Jan 20 18:42:24 master anacron[374]: Will run job `cron.weekly' in 65 min.
Jan 20 18:42:24 master anacron[374]: Jobs will be executed sequentially
Jan 20 18:42:24 master anacron[374]: Job `cron.daily' started
Jan 20 18:42:24 master run-parts(/etc/cron.daily)[375]: starting 0yum-daily.cron
Jan 20 18:42:24 master run-parts(/etc/cron.daily)[382]: finished 0yum-daily.cron
Jan 20 18:42:24 master run-parts(/etc/cron.daily)[375]: starting logrotate
Jan 20 18:42:24 master run-parts(/etc/cron.daily)[389]: finished logrotate
Jan 20 18:42:24 master run-parts(/etc/cron.daily)[375]: starting man-db.cron
Jan 20 18:42:26 master run-parts(/etc/cron.daily)[400]: finished man-db.cron
Jan 20 18:42:26 master run-parts(/etc/cron.daily)[375]: starting messages
Jan 20 18:42:26 master run-parts(/etc/cron.daily)[407]: finished messages
Jan 20 18:42:26 master run-parts(/etc/cron.daily)[375]: starting mlocate
Jan 20 18:42:26 master run-parts(/etc/cron.daily)[420]: finished mlocate
Jan 20 18:42:26 master anacron[374]: Job `cron.daily' terminated
※weeklyは終了していないのでNormal exitは出力されない

anacron -s を実行

Jan 20 18:42:41 master anacron[422]: Anacron started on 2016-01-20
Jan 20 18:42:41 master anacron[422]: Job `cron.weekly' locked by another anacron - skipping
Jan 20 18:42:41 master anacron[422]: Will run job `cron.daily' in 0 min.
Jan 20 18:42:41 master anacron[422]: Jobs will be executed sequentially
Jan 20 18:42:41 master anacron[422]: Job `cron.daily' started
Jan 20 18:42:41 master run-parts(/etc/cron.daily)[423]: starting 0yum-daily.cron
Jan 20 18:42:41 master run-parts(/etc/cron.daily)[432]: finished 0yum-daily.cron
Jan 20 18:42:41 master run-parts(/etc/cron.daily)[423]: starting logrotate
Jan 20 18:42:41 master run-parts(/etc/cron.daily)[439]: finished logrotate
Jan 20 18:42:41 master run-parts(/etc/cron.daily)[423]: starting man-db.cron
Jan 20 18:42:43 master run-parts(/etc/cron.daily)[460]: finished man-db.cron
Jan 20 18:42:43 master run-parts(/etc/cron.daily)[423]: starting messages
Jan 20 18:42:43 master run-parts(/etc/cron.daily)[468]: finished messages
Jan 20 18:42:43 master run-parts(/etc/cron.daily)[423]: starting mlocate
Jan 20 18:42:43 master run-parts(/etc/cron.daily)[479]: finished mlocate
Jan 20 18:42:43 master anacron[422]: Job `cron.daily' terminated
Jan 20 18:42:43 master anacron[422]: Normal exit (1 job run)
※weeklyは実行されず、dailyは終了しているのでNormal exitが出力される

```


#### PREFERRED_HOUR
---

RHEL7に追加されてました。/etc/anacrontabのオプションのようです。
manには載っていない模様。検索しても全然でてこない…。

```:RHEL7のreadtab.c
        if (strncmp(env_var, "PREFERRED_HOUR", 14) == 0) {
            r = match_rx("^([[:digit:]]+)$", value, 1, &pref_hour);
            if ((r != -1) || (pref_hour != NULL)) {
                preferred_hour = atoi(pref_hour);
                if ((preferred_hour < 0) || (preferred_hour > 24)) {
                    preferred_hour = -1;
                    goto reg_invalid;
                }
            }
        }
```