SVX日記
2005-12-08(Thu) うるう秒を先取る
久々にうるう秒がやってくるらしい。地球の自転のズレがどうとかで発生する、いわば「スキマな時間」であるワケだが、たった1秒であるから有効に活用することもできない。うるう年にある「うるう日」は、シッカリ24時間あるから活用できるが、暦の一部であるからフツーに平日である。ちぇ。
ちなみにこの「閏」という漢字。元は「うるう」という読みはなく「ジュン」と読むのが正しかったらしい。なんでも「潤」の漢字と読み間違ったところから「うるう年」という読みが生まれ「閏」の訓読みとして定着したそうな。マコトにダサいが、ママあるコトでもある。オーケー、OK、Oll-Kollect、All-Collect……こいつもそうだしな。
もうひとついうと、うるうシリーズには「うるう年」「うるう月」「うるう日」「うるう秒」がある。うるう月は太陰暦の概念であり、現在の暦には存在しない。一方で、うるう年はうるう日の存在する年を表す言葉であり、実体を持たない時間である。コトバというものの、誠にファジーであることよ。
それに引き換え、地球の自転の正確なこと。そりゃ、4年に一度、100年に一度、400年に一度の「うるう日」の補正は入るが、それ以外は数年に一度のうるう秒だけなんて、どうにかしているとしか思えない正確さ、偶然さである。月がいつもこっちを向いているのと同じくらい奇妙である。いつか悪いことがおきるような気がするぞ。知らないぞ。
しかしながら、いただけないのは、365日、7日/週、12月/年という現在の暦制度。プログラムでの日時の扱いの面倒だったらない。まぁ、365を因数分解したところで、5x73までしか分解できないから、画期的に美しい暦は提案できないだろうから、仕方ないトコロではあるが……あぁ、この世の基数が2でないことの、なんという醜さよ、かく言う私の指も片手に8本ないのが恨めしい……というギャグは置いておいて。
さて、今日のテーマはうるう秒である。たった1秒であるが、これが結構ややこしい。なにがややこしいかって、うるう秒がOSでどう扱われているかがややこしいのだ。で、オイラはLinuxの人なので、このあたりの知識をねっとりと勉強するハメになった。実はこの件に関しての質問が多いのよ。1秒くらいどうでもいいじゃんとは思うし、その1秒を重要視する必要のある状況は多くないだろうから、興味本位の質問だとは思うのだが。
結論から言うと、Linuxのカーネルにはうるう秒が清く正しくプログラムされている。このインターネット時代、NTPで時刻同期をするのはもはや常識ではあるが、このNTPプロトコルにもうるう秒を取り扱うための情報が含まれているのだ。その名もLI(LeapIndicater)ビット。このビットはうるう秒が存在する日は一日中オンになっていて、この情報は下位のNTPサーバに伝播していく。
で、問題は実際の補正がどのように行われるかである。NTPクライアントがLIビットを受け取ると、これはOSに渡される。Linuxの場合、カーネル内部のtime_stateという変数がTIME_INSに変化する。そして、時間が日本時間で8時59分59秒から60秒になった瞬間、60秒から1が引かれ、59秒に戻されるのである。かくして1秒間の余計な時間が刻まれることで、その瞬間とて、1秒足りともズレることなく正しく時が刻まれるのであった。
392 case TIME_INS:
393 if (xtime.tv_sec % 86400 == 0) {
394 xtime.tv_sec--;
395 time_state = TIME_OOP;
396 printk(KERN_NOTICE "Clock: inserting leap second 23:59:60 UTC\n");
397 }
398 break;
385 case TIME_OK:
386 if (time_status & STA_INS)
387 time_state = TIME_INS;
388 else if (time_status & STA_DEL)
389 time_state = TIME_DEL;
390 break;
240 #define TIME_INS 1 /* insert leap second */
221 #define STA_INS 0x0010 /* insert leap (rw) */
このビットを立ててやれば、なにも正月を待つことなく、明日の朝9時にでも、うるう秒を挿入することができるのである。このビット、通常はNTPから、おそらくadjtimex関数経由で立てられるビットであるが、面倒くさいので今回は直接カーネル内部の変数をイジってしまうことにする。カーネル内部の変数をイジるには、以前に作ったモジュール、intruder.cを使う。
1 //
2 // カーネルの内部変数を確認、変更する&内部関数を呼ぶモジュール
3 //
4 // コンパイル方法
5 // # cc -c -I /usr/src/linux-`uname -r`/include intruder.c
6 //
7 // 実行方法
8 // # insmod intruder.o; rmmod intruder; dmesg | tail
9 //
10
11 #define MODULE
12 #include <linux/module.h>
13
14 #include <linux/kernel.h> // for printk
15 #include <linux/time.h> // for struct timeval
16 #include <linux/timex.h> // for STA_INS
17
18 int init_module(void) {
19 printk(">>> Intruder module installed.\n");
20 {
21 int* time_status = (int*)0xc0XXXXXX; // /boot/System.map - time_status
22 printk("time_status = %x (before)\n", *time_status);
23 *time_status |= STA_INS;
24 printk("time_status = %x (after)\n", *time_status);
25 }
26 {
27 int* time_state = (int*)0xc0XXXXXX; // /boot/System.map - time_state
28 // *time_state = 1;
29 printk("time_state = %x\n", *time_state);
30 }
31 return 0;
32 }
33
34 void cleanup_module(void) {
35 printk("<<< Intruder module uninstalled.\n");
36 }
37
38 MODULE_AUTHOR("ITLine Inc. <furutanian@gmail.com>");
39 MODULE_LICENSE("GPL");
40 MODULE_DESCRIPTION("Kernel Intruding Module");
time_statusとtime_stateはカーネルのグローバル変数であるが、外部に公開されていないので、上記の「0xc0XXXXXX」部分に直接アドレスを書き込む必要がある。書き込むアドレスを知るには/boot/System.mapを参照し、現在稼動中のカーネルのシンボルを確認しなければならない。アドレスを間違えるとカーネルのどこかを適当に書き換えてしまうので、暴走する危険があるので注意だ。そもそもSystem.mapがなかったり、情報が不足している場合は、以下のコマンドでvmlinuxからSystem.mapを生成するとよい。
nm vmlinux | grep -v '\(compiled\)\|\(\.o$$\)\|\( [aUw] \)\|\(\.\.ng$$\)\|\(LASH[RL]DI\)' | sort > System.map
で、ちゃんとintruder.cの該当部にアドレスを書き込んでコンパイルしたら「# insmod ./intruder.o; rmmod intruder; dmesg | tail」を実行。time_state = 1が表示されたら「うるうビット」が立った証拠である。「date -s "mm/dd 8:59 2005"」を実行、時刻を8時59分にセットして、dateコマンドを連打。うるう秒が発生するのを待とう。
Www Mmm dd 08:59:60 JST 2005……出た? なに!? 出なかった? そう。実は「59秒が2秒間継続する」だけで、60秒という時間は出てこないのである。というのも、UNIXの標準を定めているPOSIX規格では、うるう秒の存在は「ないことになっている」のである。まぁ、確かに数年に一度だけ60秒なんて数字が出てこられたら、アプリケーションが問題を起こしてしまうよな……ん? 待てよ? すると、アレはなんなんだ!? 「man strftime」の中の「%S 秒 (10 進数表記) (00-61)」の記述は!? オイラが見たい未来は、こんな59秒が2秒間続くような未来じゃないんだッ!!
確かにPOSIX規格の中には60秒は存在しない。しかし、タイムゾーンの設定によっては、POSIX規格を敢えて外れ、うるう秒を正しく処理することもできるのである。ちなみに、タイムゾーンの設定はWBEL3(RHEL3)の場合「/usr/share/zoneinfo」の下にある。
# find /usr/share/zoneinfo -name Tokyo -exec ls -l {} \;
-rw-r--r-- 2 root root 125 12月 16 05:44 /usr/share/zoneinfo/Asia/Tokyo
-rw-r--r-- 2 root root 125 12月 16 05:44 /usr/share/zoneinfo/posix/Asia/Tokyo
-rw-r--r-- 2 root root 309 12月 16 05:44 /usr/share/zoneinfo/right/Asia/Tokyo
3つのファイルが見つかった。デフォルトはAsia/Tokyoで、実はAsia/Tokyoとposix/Asia/Tokyoは同じ物である。つまり、デフォルトではPOSIX準拠の動作が行われるために、60秒という時間を刻むワケがないのである。そこで、以下のコマンドを試してみる。
# date; TZ=right/Asia/Tokyo date
2005年 12月 12日 月曜日 09:23:36 JST
2005年 12月 12日 月曜日 09:23:14 JST
同じJSTのハズなのに、後者は22秒遅れている。実はこれこそが、今までに発生したうるう秒を考慮した「right」な時間なのである。このタイムゾーンファイルright/Asia/Tokyoには、今までのすべてのうるう秒が登録されている。しかしながらRHEL3の発売当初には、今年の正月にうるう秒が挿入されるコトなど判明しているワケがない。よって、うるう秒発生の場合にはタイムゾーンファイルを更新する必要があるわけだ。以下の場所から、最新のタイムゾーンファイルをダウンロードしよう。
ftp://ftp.redhat.com/pub/redhat/linux/updates/enterprise/3ES/en/os/SRPMS/tzdata-2005m-1.EL3.src.rpm
# rpm -ivh tzdata-2005m-1.EL3.src.rpm
# rpmbuild -ba /usr/src/redhat/SPECS/tzdata.spec
# rpm -ivh /usr/src/redhat/RPMS/noarch/tzdata-2005m-1.EL3.noarch.rpm
1 #!/usr/bin/ruby
2
3 p ENV['TZ'] = 'Asia/Tokyo'
4 p Time.now.strftime('%F %T')
5
6 p ENV['TZ'] = 'right/Asia/Tokyo'
7 p Time.now.strftime('%F %T')
後はタイムゾーンを「export TZ=right/Asia/Tokyo」で設定して、さっきの「insmod ./intruder.o; rmmod intruder; dmesg | tail」は「やらない」。そして今度は「date -s "01/01 8:59 2006"」を実行し、ホントにうるう秒が発生する時刻の直前にセット。チョイと小洒落て「while true; do ./strftime.rb; echo; sleep 1; done」なんて小細工しちゃったりして、うるう秒が発生するのを小粋に待とう……
# while true; do ./strftime.rb; echo; sleep 1; done
"Asia/Tokyo"
"2006-01-01 09:00:20"
"right/Asia/Tokyo"
"2006-01-01 08:59:58"
"Asia/Tokyo"
"2006-01-01 09:00:21"
"right/Asia/Tokyo"
"2006-01-01 08:59:59"
"Asia/Tokyo"
"2006-01-01 09:00:22"
"right/Asia/Tokyo"
"2006-01-01 08:59:60" ←60秒目が発生!!
"Asia/Tokyo"
"2006-01-01 09:00:23"
"right/Asia/Tokyo"
"2006-01-01 09:00:00"