* ファイルシステムとタイムスタンプ [#e41c69af]

** そもそもの疑問 [#y52839a4]

Linux で FAT フォーマットの SD カードを mount し,ls -l でディレクトリの中身を覗いていて

> 何で Linux 上で FAT filesystem のタイムスタンプが正常に表示できるんだろう

と,すごく気になりだしたのである.

というわけで追っかけてみた.

** epoch と timezone [#abe51c4f]

「何でそんなこと気にするんだ」という話のとっかかりとして,まずはここらへんから.

まず,epoch とは「1970 年 1 月 1 日 00:00:00 UTC からの経過秒数」のことである.
UTC とは協定世界時のことで,サマータイムが適用されていないグリニッジ標準時と同じ時刻である.
UNIX では,カーネルが持っている時計の現在時刻を time(2) システムコールで取得することができる.

次に timezone について.
timezone とは要するに「現地時間の UTC からの時差」である.
たとえば日本時間は,UTC に対して 9 時間進んだ時刻である.

** ls -l コマンドを実行すると… [#meee1fa6]

まず,ディレクトリ内のファイル名・ディレクトリ名の一覧を取得する.
そして,各ファイル・ディレクトリに対して stat(2) システムコールを発行する.

stat(2) が返す構造体の形式は以下のとおりである.

          struct stat {
              dev_t     st_dev;     /* ファイルがあるデバイスの ID */
              ino_t     st_ino;     /* inode 番号 */
              mode_t    st_mode;    /* アクセス保護 */
              nlink_t   st_nlink;   /* ハードリンクの数 */
              uid_t     st_uid;     /* 所有者のユーザ ID */
              gid_t     st_gid;     /* 所有者のグループ ID */
              dev_t     st_rdev;    /* デバイス ID (特殊ファイルの場合) */
              off_t     st_size;    /* 全体のサイズ (バイト単位) */
              blksize_t st_blksize; /* ファイルシステム I/O での
                                       ブロックサイズ */
              blkcnt_t  st_blocks;  /* 割り当てられたブロック数 */
              time_t    st_atime;   /* 最終アクセス時刻 */
              time_t    st_mtime;   /* 最終修正時刻 */
              time_t    st_ctime;   /* 最終状態変更時刻 */
          };

ファイルのタイムスタンプは time_t 型であり,つまり epoch 形式である.

この epoch 形式を現地時間の「年月日時分秒」に変換するのは libc の仕事である.
libc は /etc/localtime ファイルにある時差情報に基づいて epoch を現地時間に変換する.
かくして ls -l のタイムスタンプは現地時間で表示されるわけである.

あ,そうそう,ext3 などの UNIX / Linux native のファイルシステムでは,タイムスタンプは epoch 形式のまま HDD に保存されている.

ここらへんの仕組みをまとめると,下図のようになる.

 HDD
       epoch
         |
         |
 --------|----------
 kernel  |
         |
 --------|---------- stat(2)
 user    |
         +<--- /etc/localtime
         #
         #
         V
  yy/mm/dd hh:mm:ss JST


** FAT の場合 [#mea1bd8a]

FAT filesystem の場合,少々話が違ってくる.
「[http://www.geocities.co.jp/SiliconValley-PaloAlto/2038/fat.html FAT FS フォーマットの実装についての覚え書き]」あたりを見てみると,FAT では「年月日時分秒」形式で時刻が保存されているが,''timezone という概念は存在しない''.
つまり「どこの現地時間か UTC かは知らないけど,そういう時刻」ということでタイムスタンプが打たれているわけである.

しかし,strace をかけてみても ls -l ではやはり stat(2) でファイル情報を取得していており,ext3 上のディレクトリの処理と一緒である.
ということは,
> カーネル内部で「年月日時分秒」形式を現地時間として epoch 形式に変換している

ということになる.
ところが,これには timezone 情報が必要であるのだが,timezone 情報は /etc/localtime ファイルに保存されている.
/etc/* のファイルは,ユーザランドプログラムやライブラリが設定ファイルを慣習的に配置しているだけであり,カーネルがここのファイルを直接参照することは,普通はない.

ということで冒頭の疑問に戻るわけである.

** FAT のファイルシステムドライバ [#ndf9d9f2]

この疑問を解決するには,やはり
> Linux カーネルのソースを見る

のが一番だろう.
Linux カーネルはこのソースに従って動作しているのだから.

というわけで,FAT のファイルシステムドライバでタイムスタンプを扱っているところを探す.
ファイルシステム回りは linux-x.y.z.w/fs/ の下にまとまっている.

linux-2.6.17.1/fs/fat/misc.c にこんな記述を見つける.

 extern struct timezone sys_tz;
 
 …(略)…
 
 /* Convert a MS-DOS time/date pair to a UNIX date (seconds since 1 1 70). */
 int date_dos2unix(unsigned short time, unsigned short date)
 {
         int month, year, secs;
 
         /*
          * first subtract and mask after that... Otherwise, if
          * date == 0, bad things happen
          */
         month = ((date >> 5) - 1) & 15;
         year = date >> 9;
         secs = (time & 31)*2+60*((time >> 5) & 63)+(time >> 11)*3600+86400*
             ((date & 31)-1+day_n[month]+(year/4)+year*365-((year & 3) == 0 &&
             month < 2 ? 1 : 0)+3653);
                         /* days since 1.1.70 plus 80's leap day */
         secs += sys_tz.tz_minuteswest*60;
         return secs;
 }

カーネル内部に timezone 構造体の sys_tz が保持されいて,この値を元に時刻を epoch 形式に変換していることがわかる.

この sys_tz であるが,どこで定義されているのか探してみると,linux-2.6.17.1/kernel/time.c にたどりつく.

 struct timezone sys_tz;
 
 EXPORT_SYMBOL(sys_tz);
 
 …(略)…
 
 int do_sys_settimeofday(struct timespec *tv, struct timezone *tz)
 {
         static int firsttime = 1;
         int error = 0;
 
         if (tv && !timespec_valid(tv))
                 return -EINVAL;
 
         error = security_settime(tv, tz);
         if (error)
                 return error;
 
         if (tz) {
                 /* SMP safe, global irq locking makes it work. */
                 sys_tz = *tz;
                 if (firsttime) {
                         firsttime = 0;
                         if (!tv)
                                 warp_clock();
                 }
         }
         if (tv)
         {
                 /* SMP safe, again the code in arch/foo/time.c should
                  * globally block out interrupts when it runs.
                  */
                 return do_settimeofday(tv);
         }
         return 0;
 }

このソースで sys_tz を定義し,カーネル全体のグローバル変数として公開している.
で,do_sys_settimeofday() という関数で sys_tz へ値を代入している.
この関数,名前を見て気づいた人も多いと思うが,システムコールの settimeofday(2) の処理を行っている関数である.

このソースファイルでは settimeofday(2) と兄弟の gettimeofday(2) の処理も定義されている.
もちろん,gettimeofday(2) ではカーネルの sys_tz の値を取得することができるようになっている.

FAT でのタイムスタンプを読み取る仕組みをまとめると,下図のようになる.

 HDD
       yy/mm/dd hh:mm:ss
         #
         #
 --------#----------
 kernel  # (assume localtime)
         #
         +<------------------------------- sys_tz (timezone)
         | (epoch)                          ^  |
         |                                  |  |
 --------|---------- stat(2)            ----|--|--- gettimeofday(2) / settimeofday(2)
 user    |                                  |  |
         +<---- /etc/localtime              |  V
         #
         #
         V
  yy/mm/dd hh:mm:ss JST

** じっけん [#b3b271e1]

以下のようなプログラムを書く.

 #include <sys/time.h>
 #include <stdio.h>
 #include <stdlib.h>
 
 int main ( int argc, char *argv[] )
 {
 	struct timezone tz;
 	struct timeval tv;
 	int t, s, h, m;
 
 	if ( argc > 1 ) {
 		gettimeofday ( NULL, &tz );
 		tz.tz_minuteswest = atoi ( argv[1] );
 		settimeofday ( NULL, &tz );
 	}
 
 	gettimeofday ( NULL, &tz );
 
 	printf ( "minuteswest = %d, dsttime = %d\n",
 		 tz.tz_minuteswest, tz.tz_dsttime );
 
 	return 0;
 }

コンパイルし,timezone という実行プログラムを作る.
 gcc timezone.c -o timezone

このプログラムは
 # ./timezone
と,引数なしで呼び出されたときは単に
 minuteswest = -540, dsttime = 0
と,timezone 構造体の中はを出力し,
と,timezone 構造体の中身を出力し,
 # ./timezone 0
と,引数を付けた場合は settimeofday(2) で timezone を設定する.

次に FAT フォーマットの SD カードを用意する.
windows でフォーマットし,適当なファイルを入れておく.

 J:\>dir
  ドライブ J のボリューム ラベルがありません。
  ボリューム シリアル番号は 98D0-E398 です
 
  J:\ のディレクトリ
 
 2007/06/23  22:10                    0 textfile.txt
                1 個のファイル                   0 バイト
                0 個のディレクトリ      30,854,656 バイトの空き領域

この SD カードを Linux 上で mount し,中を見ると
  # ls -l
 合計 0
 -rwxr-xr-x 1 root root 0 2007-06-23 22:10 textfile.txt
と,見える.

このとき,カーネルが保持している timezone は
 # ~/timezone 
 minuteswest = -540, dsttime = 0
であり,JST である.
ここで,minuteswest の値がマイナスとなっているので「おや」と思った人もいるだろう.
minuteswest は,その名前のとおりグリニッジから西方向ををプラスとした時差であり,単位は分である.
日本はグリニッジから東側に9時間なので,-540 分となるのである.
単に「UTC を基準とした時差」でないところが妙なところでもあるが,これだとアメリカの時差がマイナスになってしまうからなのだろう.
まぁ,ここらへんは歴史的なしがらみだと思うので「こんなもんだ」ということでスルーする.

この場合,FAT filesystem 上でのタイムスタンプは下図のように扱われて出力されている.

 HDD
       2007/06/23 22:10
         #
         #
 --------#----------
 kernel  # (assume localtime)
         #
         +<------------------------------- sys_tz (timezone = JST)
         | (epoch = 2007/06/23 22:10 JST)   ^  |
         |                                  |  |
 --------|---------- stat(2)            ----|--|--- gettimeofday(2) / settimeofday(2)
 user    |                                  |  |
         +<---- /etc/localtime = JST        |  V
         #
         #
         V
 2007/06/23 22:10 JST

ここで,
 # ~/timezone 0
 minuteswest = 0, dsttime = 0
とし,カーネルが保持している timezone を 0 に設定する.
そして,SD カードを umount し,mount しなおすと…

 # ls -l
 合計 0
 -rwxr-xr-x 1 root root 0 2007-06-24 07:10 textfile.txt

予想通り,表示される時刻が狂う.
が,タイムスタンプ取扱いモデルと照らしあわせると,このような日時が出力されるのは当然である.

 HDD
       2007/06/23 22:10
         #
         #
 --------#----------
 kernel  # (assume localtime)
         #
         +<------------------------------- sys_tz (timezone = UTC)
         | (epoch = 2007/06/23 22:10 UTC)   ^  |
         |                                  |  |
 --------|---------- stat(2)            ----|--|--- gettimeofday(2) / settimeofday(2)
 user    |                                  |  |
         +<---- /etc/localtime = JST        |  V
         #
         #
         V
 2007/06/24 07:10 JST

** まとめ [#k0011530]

というわけで,まとめてみると

- FAT filesystem でのタイムスタンプは,Linux カーネルが保持している timezone でのタイムスタンプとして epoch に変換され,stat(2) でユーザプロセスへと渡される
- ユーザプロセスでは /etc/localtime の timezone に従って現地時間に変換され,出力される

というあたりかな.

** gettimeofday(2) / settimeofday(2) の man [#x53d42fa]

蛇足として.

[http://www.linux.or.jp/JM/html/LDP_man-pages/man2/gettimeofday.2.html gettimeofday(2) / settimeofday(2) の man (オンラインマニュアル)]であるが,説明がいまひとつのような気がする.

> timezone 構造体を使うのは時代遅れ (obsolete) である: tz 引き数は通常は NULL に指定すべきである。

とあり,tz が後方互換性だけのための引数のように書かれているが,どっこい tz は現役の引数である.
FAT filesystem (や,timezone の概念の無い他の filesystem)がある限り,tz が引退する日はきっと来ないだろう,と思う.

トップ   編集 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS