Linux でタイムゾーンを自作する

Linux でタイムゾーンを設定するときは `/etc/localtime` に `/usr/share/zoneinfo` にあるファイルへのシンボリックリンクを作ります。このファイルはテキストファイルではないため直接編集できず、専用のコマンドを利用します。

### タイムゾーンの情報 - tzfile(5)
`/etc/localtime` に置かれるファイルは [tzfile(5)](https://linuxjm.osdn.jp/html/LDP_man-pages/man5/tzfile.5.html) というタイムゾーンの情報専用のファイルフォーマットです。tzfile はヘッダは ascii でそのほかは独自のデータ構造になっています。

```text:/etc/localtime
$ od -Ax -tx1z /usr/share/zoneinfo/Asia/Tokyo
000000 54 5a 69 66 32 00 00 00 00 00 00 00 00 00 00 00  >TZif2...........<
000010 00 00 00 00 00 00 00 04 00 00 00 04 00 00 00 00  >................<
(略)
000120 90 00 08 4c 4d 54 00 4a 44 54 00 4a 53 54 00 00  >...LMT.JDT.JST..<
000130 00 00 01 00 00 00 01 0a 4a 53 54 2d 39 0a        >........JST-9.<
00013e
```

[zic(8)](https://linuxjm.osdn.jp/html/LDP_man-pages/man8/zic.8.html) コマンドは tzfile を作るコマンドで、テキストファイルに書いたタイムゾーンの情報をタイムゾーン毎に tzfile として作ることができます。テキストファイルには
`Zone  NAME            GMTOFF  RULES   FORMAT  [UNTIL]` でタイムゾーンを定義します。

| フィールド | 意味 | 例 |
|:----------|:-----|:---|
| Zone | "Zone" 固定. その行がタイムゾーンの定義であることを示す文字 | Zone|
| NAME | タイムゾーンの名前 | Australia/Adelaide |
| GMTOFF | GMT からの時差。 UTC ではないので要注意 | 9:30 |
| RULES | タイムゾーンに適用されるルールの名前 | Aus |
| FORMAT | タイムゾーンの短縮形。サマータイムで変わる場合は %s を使う | AC%sT |
| [UNTIL] | タイムゾーンが変更になる時点。このフィールドの値までそのタイムゾーンが有効になる | 1971 |

サマータイムはルールとして、`Rule  NAME  FROM  TO    TYPE  IN   ON       AT    SAVE  LETTER/S` の並びで定義します。

| フィールド | 意味 | 例 |
|:----------|:-----|:---|
| Rule |"Rule" 固定.その行がルールの定義であることを示す文字 | Rule |
| Name | このルールが適応されるタイムゾーン。 Zone 行の `RULES` に書いた文字と合わせる必要がある | Aus |
| FROM | このルールが有効になる年 | 2007 |
| TO | このルール無効になる年. only の場合は FROM と同じ値 | 2009 |
| TYPE | ルールが適応される期間。 - の場合は FROM から TO の間で適応される.  | - |
| IN | ルールが有効になる月 | Oct |
| ON | そのルールが効力を持つ日 | lastSun |
| AT | ルールが効力を持つ一日のうちの時刻 | 2:00s |
| SAVE | 適応されたときに時刻に追加される時間 | 1:00 |
| LETTER/S | タイムゾーン短縮型の %s に割り当てられる文字 | D |

現在使われているタイムゾーンをすべて書いたファイルは [tz Database](ftp://ftp.iana.org/tz/tz-link.html) として IETF が管理しています。ちなみに管理方法は "[RFC 6557 - Procedures for Maintaining the Time Zone Database](https://tools.ietf.org/html/rfc6557)" として公開されています。使い方も [tz-how-to](ftp://ftp.iana.org/tz/tz-how-to.html) に公開されています。Rule 行の TYPE に使える文字のチェックを行うスクリプトも `yearistype.sh` として用意されています。
ちなみに、tz Database にはタイムゾーンの定義だけではなく、その根拠となる情報もコメントアウトして書かれています。

tz Database の Asia/Tokyo の JST は次のように書かれています。

```text:asia(抜粋)
# Rule  NAME    FROM    TO      TYPE    IN      ON      AT      SAVE    LETTER/S
Rule    Japan   1948    only    -       May     Sat>=1  24:00   1:00    D
Rule    Japan   1948    1951    -       Sep     Sun>=9   0:00   0       S
Rule    Japan   1949    only    -       Apr     Sat>=1  24:00   1:00    D
Rule    Japan   1950    1951    -       May     Sat>=1  24:00   1:00    D

(略)
# Zone  NAME            GMTOFF  RULES   FORMAT  [UNTIL]
Zone    Asia/Tokyo      9:18:59 -       LMT     1887 Dec 31 15:00u
                        9:00    Japan   J%sT
```

これらの情報をもとに自作タイムゾーン (My/Zone) とサマータイムのルール (MyRule)を myzone に定義してみます。サマータイムのルールは、 2018/10/06 18:40 以降から 2018/10/13 0:00 までの間適応されるようにしています

```myzone
# Rule  NAME    FROM    TO      TYPE    IN      ON      AT      SAVE    LETTER/S
Rule    MyRule  2018    Only    -       Oct     Mon>=1   0:00   0       S
Rule    MyRule  2018    Only    -       Oct     Sat>=6   18:40   1:00    D
Rule    MyRule  2018    Only    -       Oct     Sat>=13   0:00   0       S
# Zone  NAME            GMTOFF  RULES   FORMAT  [UNTIL]
Zone    My/Zone         9:00    MyRule  MY%sT
```

出来上がったファイルは zic コマンドでコンパイルします。 zic コマンドは何も指定しなければ /usr/share/zoneinfo に出力するため root 以外実行できません。そのため `-d $PWD` を付けて実行します。
出来上がった tzfile は Zone の Name に指定したパスに出力されます。 

```
$ zic -d $PWD myzone
$ file My/Zone
My/Zone: timezone data, version 2, 2 gmt time flags, 2 std time flags, no leap seconds, 2 transition times, 2 abbreviation chars
$ od -Ax -tx1z My/Zone
000000 54 5a 69 66 32 00 00 00 00 00 00 00 00 00 00 00  >TZif2...........<
000010 00 00 00 00 00 00 00 02 00 00 00 02 00 00 00 00  >................<
000020 00 00 00 03 00 00 00 02 00 00 00 0a 5b b0 e4 f0  >............[...<
000030 5b b8 82 f0 5b c0 a8 e0 00 01 00 00 00 7e 90 00  >[...[........~..<
000040 00 00 00 8c a0 01 05 4d 59 53 54 00 4d 59 44 54  >.......MYST.MYDT<
000050 00 00 00 00 00 54 5a 69 66 32 00 00 00 00 00 00  >.....TZif2......<
000060 00 00 00 00 00 00 00 00 00 00 00 00 02 00 00 00  >................<
000070 02 00 00 00 00 00 00 00 03 00 00 00 02 00 00 00  >................<
000080 0a 00 00 00 00 5b b0 e4 f0 00 00 00 00 5b b8 82  >.....[.......[..<
000090 f0 00 00 00 00 5b c0 a8 e0 00 01 00 00 00 7e 90  >.....[........~.<
0000a0 00 00 00 00 8c a0 01 05 4d 59 53 54 00 4d 59 44  >........MYST.MYD<
0000b0 54 00 00 00 00 00 0a 4d 59 53 54 2d 39 0a        >T......MYST-9.<
0000be
$
```

中身を見ると `MYDT` と `MYST` が見えていて正しく作られてそうです。`TZif2` はヘッダです。
あとはこのファイルを通常通り `/etc/localtime` に置けば利用できます。ちなみに[localtime(5)](http://man7.org/linux/man-pages/man5/localtime.5.html)のパスは libc の再ビルド以外では変更できず、どのプロセスでも共通です。

## タイムゾーンを利用する - ctime(3)

`/etc/localtime` に置かれた情報は [tzset(3)](https://linuxjm.osdn.jp/html/LDP_man-pages/man3/tzset.3.html) でメモリ上に展開されます。localtime や strftime などの [ctime(3)](https://linuxjm.osdn.jp/html/LDP_man-pages/man3/ctime.3.html) の関数から内部的に呼ばれるため直接呼び出すことはあまりありません.

特に、glibc-2.27 の実装では localtime の処理は tzset と同じ個所で実装されています。

```c:localtime.c
struct tm *
localtime (const time_t *t)
{
  return __tz_convert (t, 1, &_tmbuf);
}
```

```c:tzset.c
struct tm *
__tz_convert (const time_t *timer, int use_localtime, struct tm *tp)
{
  long int leap_correction;
  int leap_extra_secs;

  if (timer == NULL)
    {
      __set_errno (EINVAL);
      return NULL;
    }

  __libc_lock_lock (tzset_lock);

  /* Update internal database according to current TZ setting.
     POSIX.1 8.3.7.2 says that localtime_r is not required to set tzname.
     This is a good idea since this allows at least a bit more parallelism.  */
  tzset_internal (tp == &_tmbuf && use_localtime);

(snip...)
```

tzset_internal が tzfile を読み込む関数本体で、引数には tzfile を常に読み込むかどうかを true/false で指定します。
コメントにも書かれている通り localtime は true になり、関数が呼ばれるたびに常に読み込みが行われます。
一方で localtime_r は `tp != &_tmbuf` になるため**初回の呼び出し時のみ**読み込まれます。このあたりの localtime/localtime_r はマニュアルにも書かれていないため使用時は注意が必要です。

余談ですが tzset は libc の実装次第で挙動が変わり、例えば、glibc は tzset が呼ばれるたびに `/etc/localtime` を読み込みますが、 BSD libc は tzset を直接呼び出したとしても基本的に初回の１度しか読み込まれません。

このあたりの実装の違いによって、 `/etc/localtime` を書き換えた後にプロセスの再起動が必要だったり、不要だったりします。

ここまでの内容をもとに検証してみます。
タイムゾーンを反映した時刻は date コマンドで取得できます。ただし date コマンドは tzset を１度しか呼び出さないため、複数呼び出した時の挙動を見ることはできません。そこで適当な検証用プログラムを動かして動作を見てみます。

```c:tztest.c

#include <sys/types.h>

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>
#include <err.h>

#define FMT     "%a, %d %b %Y %T (%Z)"

static volatile sig_atomic_t quit = 0;

static void
sighandler(int signo)
{

        switch (signo) {
        case SIGINT:
        case SIGTERM:
        case SIGQUIT:
        case SIGSTOP:
                quit = 1;
                break;
        default:
                break;
        }
}

int
main(int argc, char *argv[])
{
        struct tm               *tm_lt, tm_ltr;
        struct sigaction         sa;
        time_t                   now;
        char                     str_lt[BUFSIZ], str_ltr[BUFSIZ];

        sigemptyset(&sa.sa_mask);
        sa.sa_flags = SA_RESTART;
        sa.sa_handler = sighandler;
        sigaction(SIGINT, &sa, NULL);
        sigaction(SIGTERM, &sa, NULL);
        sigaction(SIGQUIT, &sa, NULL);
        sigaction(SIGSTOP, &sa, NULL);
        sigaction(SIGUSR1, &sa, NULL);

        printf("pid = %d\n", (int)getpid());

        while (quit == 0) {
                if (time(&now) == -1)
                        err(1, "time");

                if (localtime_r(&now, &tm_ltr) == NULL)
                        err(1, "localtime_r");

                if ((tm_lt = localtime(&now)) == NULL)
                        err(1, "localtime");


                strftime(str_lt, sizeof(str_lt), FMT, tm_lt);
                strftime(str_ltr, sizeof(str_ltr), FMT, &tm_ltr);

                printf("---------------------------------------------\n");
                printf("time                  : %ld\n", (int64_t)now);
                printf("ctime                 : %s\n", ctime(&now));
                printf("asctime(localtime)    : %s\n", asctime(tm_lt));
                printf("asctime(localtime_r)  : %s\n", asctime(&tm_ltr));
                printf("strftime(localtime)   : %s\n", str_lt);
                printf("strftime(localtime_r) : %s\n", str_ltr);

                sigsuspend(&sa.sa_mask);
        }

        return 0;
}
```

検証に使うプログラムはこんな感じで、[time(2)](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/time.2.html) を使って unix epoch  を取得した後その値を localtime_r, localtime で変換します。起動する際は、 '&' を付けてバックグラウンドで動かします。動かした後はプロセスIDが出力されるのでそのIDにシグナルを送信すると、その時点のタイムゾーンに従った時刻を出力します。

```
$ ./tztest &
[5] 10446
pid = 10446
---------------------------------------------
time                  : 1538818008
ctime                 : Sat Oct  6 18:26:48 2018

asctime(localtime)    : Sat Oct  6 18:26:48 2018

asctime(localtime_r)  : Sat Oct  6 18:26:48 2018

strftime(localtime)   : Sat, 06 Oct 2018 18:26:48 (JST)            <=== 起動時はJSTになっている
strftime(localtime_r) : Sat, 06 Oct 2018 18:26:48 (JST)

$ sudo cp My/Zone /etc/localtime                                   <=== JSTから自作タイムゾーンに変更
$ kill -USR1 10446                                                 <=== SIGUSR1 で時刻を再表示
---------------------------------------------
time                  : 1538818032
ctime                 : Sat Oct  6 18:27:12 2018

asctime(localtime)    : Sat Oct  6 18:27:12 2018

asctime(localtime_r)  : Sat Oct  6 18:27:12 2018

strftime(localtime)   : Sat, 06 Oct 2018 18:27:12 (MYST)           <=== 再読み込みで MYST
strftime(localtime_r) : Sat, 06 Oct 2018 18:27:12 (JST)            <=== localtime_r はそのまま JST
$ kill -USR1 10446                                                 <=== 18:40 になって夏時間になったのでもう一度 USR1
---------------------------------------------
time                  : 1538818817
ctime                 : Sat Oct  6 19:40:17 2018

asctime(localtime)    : Sat Oct  6 19:40:17 2018

asctime(localtime_r)  : Sat Oct  6 19:40:17 2018

strftime(localtime)   : Sat, 06 Oct 2018 19:40:17 (MYDT)          <=== 'D' が 'S' になって１時間進んだ
strftime(localtime_r) : Sat, 06 Oct 2018 19:40:17 (MYDT)          <=== 1 度目の localtime で MYST になった後夏時間になった
```

実行結果はこんな感じです。`/etc/localtime` に作った tzfile を置くことで自作タイムゾーンが利用できました。
特に、サマータイムが実施されれば tzfile(5) が書き換えられることになるので localtime を使っているプログラムは再起動しなくても反映されますが、localtime_r を使っているプログラムは再起動が必要になります。
どのプログラムが tzset 関数を呼び出しているかがわからないときは OS 再起動して全プロセスで tzfile を再読み込みする必要があります。

## タイムゾーンとサマータイムと時刻
- サマータイムはタイムゾーンとして扱われる
- PCの時刻がずれる ≠ サマータイムで時刻がずれる
    - PCの時刻がずれると time(3) で取得される時刻が変わる。サマータイムの時は変わらない。
    - サマータイムで時刻がずれると localtime などの関数の結果が変わる。time(3) はずれない。
    - サマータイムが有効になっても時刻がずれたわけではないので NTP は何もしない
- サマータイムを検証するためにタイムゾーンの自作が必要になると思ったらサマータイムが無くなった