SVX日記

2004|04|05|06|07|08|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|12|
2007|01|02|03|04|05|06|07|08|09|10|11|12|
2008|01|02|03|04|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|

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秒足りともズレることなく正しく時が刻まれるのであった。

  この余計な1秒を刻むコードは、linuxカーネル2.4.26のソースコード、kernel/timer.cの以下の部分である。

    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;

  このcaseに引っかかるためには、time_stateという変数がTIME_INSになる必要があり、それをTIME_INSに設定しているのは、その直前の以下の部分である。

    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;

  つまりtime_statusのSTA_INSビットが立っていればいい。ちなみにTIME_INS及びSTA_INSはinclude/linux/timex.hで以下のように定義されいる。

    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パッケージを作成して、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

  件のstrftimeで、時刻を表示するプログラム「strftime.rb」を作る。Cで作るのも悪くないが、ココはrubyでラクをする。

      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"

  ……出ましたッ!! 60秒ッ!!

  今日の日記は、うるう秒の話題に目を輝かせていた、かわいいあの娘に捧げさせていただく。ではまた。