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

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

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

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

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

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

** epoch と timezone [#abe51c4f]

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

まず,epoch とは「1970 年 1 月 1 日 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
         +<--- /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 であるが,どこで定義されているのか探してみると,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 の値を取得することができるようになっている.

 HDD
       yy/mm/dd hh:mm:zz
         #
         #
 --------#----------
 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 0
と,引数を付けた場合は settimeofday(2) で timezone を設定する.

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


トップ   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS