時刻・時間のしくみ

nmoshのSRFI-19実装を自前のものに変更しようとしているところ。つまり、OSというかlibcのネイティブ手続きを使用した実装に切り替えたい。
時間を意識しない日は無いとも言えるが、時刻システムは通常の人間には理解しづらい。

TAI、UT1、UTCうるう秒UNIX時間

まず、相対性理論はここでは無視する。現実問題としては異なる地点での時刻を厳密に比較することはできないが、地表のどこにいても、1年で1秒以上の違いを生むことはない。
こんにちの世界で使用されている時刻のソースはTAI(国際原子時)となっている。これは、世界各国に設置された原子時計の平均で構成され、(1977年以降)一定の歩度で進む。歩度が一定である とは、平たく言えば "時刻単位の長さが常に一定である" ということを指す。UT1やUTCではこれは成立しないため、経過時間を単純な引き算で求めることはできない。

↑の図を見ると:

  • UT(ふとい点線)はクネクネとしたグラフになっている。UTは地球の回転の観測を元に決定される時刻であるため、地球の回転速度が一定でないことにより時刻の進み方も一定ではなくなってしまう。下方向のグラフとなっているのは、TAIに比べてUTは少しづつ常に遅れていることを表わす。
  • UTCはTAIにうるう秒のオフセットを加えたものとして定義される。このため、階段状ではあるもののTAIとは基本的に平行となっている。UTCはUT1に近くなるようにTAIに整数オフセットを加えたものなので、一秒の長さは基本的にTAIと一致する。
  • TAIの原点は図では58年に設定されている。これは正確ではなく、実際にはTAIの歩度は1970年代に微妙に修正されている。

TAIとUTの差は例えばアメリカ海軍天文台のサイト( http://www.usno.navy.mil/USNO/earth-orientation/eo-products/long-term )でファイル( http://maia.usno.navy.mil/ser7/deltat.data )として公開されている。ここでは、地球時(TT)との差であるため、TAIとは静的なオフセット 32.184 秒の差が有る。
で、UNIX時間は一般に、UNIXエポックである1970年1月1日深夜(UTC)からの経過秒数と表現される。これは真の経過秒数でないUNIX時間はUTCで表現した時刻同士の引き算で定義されるため、うるう秒が入ると小数点以下は巻き戻ることになる。このため、2つのUNIX時刻の比較は注意深く行う必要がある。
この仕様について、POSIXのrationaleでは:

(That is, the majority of applications were judged to assume a single length-as measured in seconds since the Epoch-for all days. Thus, leap seconds are not applied to seconds since the Epoch.)

つまり時刻比較を犠牲にしつつ、POSIXは実装の単純さを取っている。NTPはUNIX時刻を元にしたプロトコルであるため、供給した時刻がうるう秒における1度目なのか2度目なのかを示すインジケータを持つ。
本当に時刻を比較する必要があれば、一旦TAIに戻すことで正確に比較することができる。この要求のために、時刻の記録にTAIを使用するシステムも有る。TAI利用で有名なのはDJBの一連のプログラムで、DJBはいくつかのページを割いてTAIやそのライブラリについて解説している http://cr.yp.to/proto/utctai.html

ローカルタイム、サマータイム

TAIからうるう秒をオフセットしたものがUTCで、そのUTCから午後0時が真昼になるように固有のオフセットを追加したものがローカルタイムとなる。更に、国によっては"サマータイム"によってオフセットの巾を変更することが有るため、UTCとローカルタイムの対応は難しい問題となる。
(日本では現状サマータイムは実施されていないが、アメリカやヨーロッパ各国では実施されているため、サマータイムを想定しないコードを書くのは避けた方が良い。)
現在、このオフセットのデータベースはIANAが管理しているものが一般に利用されている。

Windows以外のデスクトップOS、つまり、MacLinux等はこのtzデータベースを使用して、ライブラリ(= libc)レベルでローカルタイムとUTCの変換を実装している。ただし、一部のファイルシステム(FATやISO9660)はローカルタイムをファイルシステム時刻に採用してしまっているため、カーネル内でも一部ローカルタイムを扱う必要がある。
ローカルタイムにおける重要な問題としてサマータイムが有る。サマータイムは各地域によって定義が異なり、しかも法令等で毎年変更の可能性がある。このため、IANAのデータベースが重要になる。
基本的に、ローカルタイムはユーザとの境界にのみ使用する。データベースに記録したりファイルシステムに採用したりするのはあまり好ましくない。毎年ほぼ確実に入るサマータイムにより、時刻のオフセットはほぼ不正確となる。ファイルシステムやデータベースにUTCとTAIのどちらを採用すべきかは微妙なポイントと言える。

POSIXの時刻

(あとで書く - OS毎にかなり実装が異なるため)
良い考察としてはPythonのPEP-418が有る : https://www.python.org/dev/peps/pep-0418/
PEP-418はOSの時刻源毎にパフォーマンスや精度を考察している。
端的に言えば、良い時刻カウンタは比較的新しい概念と言える。例えば、CLOCK_MONOTONIC_RAWはLinux 2.6.28で導入された。なので古いシステムをサポートする場合はそれなりのfallbackを用意する必要がある。
POSIX時刻には伝統的に2100年問題が有った。これはPOSIXではPOSIX時刻と日付をコンポーネント毎に格納するstruct tmの対応を定義してしまっていることに因る。

tm_sec + tm_min*60 + tm_hour*3600 + tm_yday*86400 +
(tm_year-70)*31536000 + ((tm_year-69)/4)*86400

この定義に従うと、2100年がうるう年になってしまうため、この定義は2099年までしか正しくないことになる。式を見ると解るように、この式にはうるう年の定義のうち"4年に1度"の項しか含まれていない。(2000年は400年に一度の"100の倍数だけどうるう年"であるため、この式の定義域である1970年以降では4年に1度ルールからは特別扱いする必要がない)
これは近年の規格では修正されている。

tm_sec + tm_min*60 + tm_hour*3600 + tm_yday*86400 +
(tm_year-70)*31536000 + ((tm_year-69)/4)*86400 -
((tm_year-1)/100)*86400 + ((tm_year+299)/400)*86400

このため、2100年の扱いが実装によって異なる可能性が有る。もっとも、2100年よりも先にいわゆる2038年問題が来るため、そのような古いシステムは自動的に駆逐されるだろう。。

Windowsの時刻

Windowsは歴史と伝統のためにカーネル内でもローカルタイムを扱っている。特にWindowsはPC本体に記録するCMOS時刻もローカルタイムとしているため、UTCを使用する他のOSとの互換性の問題が有る。

MSDNのページに有るように、Windowsの時刻にはいくつかの種類がある:

CLOCK_MONOTONIC_RAWに相当する機能性はQueryUnbiasedInterruptTime API( http://msdn.microsoft.com/en-us/library/windows/desktop/ee662307%28v=vs.85%29.aspx )によって提供されているが、このAPIはXPやVistaには存在しない。XP等の古いWindowsでは、QueryPerformanceCounterで代替することになる。PEP 418にあるように、XPではQueryPerformanceCounterはRDTSC、つまりCPUのタイムスタンプカウンタを使用して実装されている。
FILETIMEはNTFSにおけるタイムスタンプでもある。このため、NTFSタイムスタンプは1601年から表現することができ、100ナノ秒精度を持つ。
記憶が正しければ、FILETIMEとWindowsは、正しいグレゴリオ暦のうるう年を処理し、うるう秒をサポートしない。これはテストしておいた方が良い気がしてきた。