4
1

More than 3 years have passed since last update.

Linuxで、ファイルのタイムスタンプが僅かな過去になる件。

Last updated at Posted at 2020-05-02

(以下の本文の実行環境はubuntu 18.04。ソースコードは、リンクを張るためubuntu 18.04のパッケージと同じ版のupstream版のgitを参照している)

きっかけ

ファイルを更新した後、タイムスタンプを見ると、わずかに過去であることがある。

$ date +%H:%M:%S.%N ; touch file ; date +%H:%M:%S.%N ; stat --printf="%y\n" file
XX:46:44.546008239          ← 1
XX:46:44.550890822          ← 2
20XX-XX-XX XX:46:44.543994035 +0900 ← 3

1と2の間でファイルを触っているので、タイムスタンプ3は1と2の間の時刻にならないといけないが、よく見ると3は1より2msほど前の時刻である。

date(1)とtouch(1)を調べる

dateコマンドは、straceをかけても時刻を得るシステムコールは出さない。これはvsyscallという仕組みでカーネルを呼ばずに時刻を求めているからで、実際にはclock_gettime(2)というシステムコールを使っていると考えて良い。dateコマンドのソースコードより、

              /* Prepare to print the current date/time.  */
              gettime (&when);

gettime()はgnulib (GNU Portability Library) で定義されている

void
gettime (struct timespec *ts)
{
#if HAVE_NANOTIME
  nanotime (ts);
#else

# if defined CLOCK_REALTIME && HAVE_CLOCK_GETTIME
  if (clock_gettime (CLOCK_REALTIME, ts) == 0)
    return;
# endif

  {
    struct timeval tv;
    gettimeofday (&tv, NULL);
    ts->tv_sec = tv.tv_sec;
    ts->tv_nsec = tv.tv_usec * 1000;
  }

#endif
}

#ifで、中程のclock_gettimeの行が採用される。

一方touchの方は、普通にstraceが取れる。

$ strace touch file
...
openat(AT_FDCWD, "file", O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK, 0666) = 3
dup2(3, 0)                              = 0
close(3)                                = 0
utimensat(0, NULL, NULL, 0)             = 0
close(0)                                = 0
...

utimensat(2)というシステムコールを使っている。

カーネルを調べる

utimensat(2)は、fs/utimes.cというファイルで定義されている

SYSCALL_DEFINE4(utimensat, int, dfd, const char __user *, filename,
        struct timespec __user *, utimes, int, flags)
{
略
}

この先、do_utimes()utimes_common()から、fs/attr.cにあるnotify_change()呼ばれる

現在時刻に設定する場合、attr->ia_validは、ATTR_CTIME | ATTR_MTIME | ATTR_ATIME | ATTR_TOUCHになっている。notify_change()は、ファイルの各種属性の変更をする関数だが、属性変更時はctime、mtime、atimeを現在時刻に変更する普通であるので、そのような処理になっている。

その現在時刻は、current_time()という関数で得ている

current_time()は、fs/inode.cで定義されていて、current_kernel_time()の結果を、ファイルシステム依存のタイムスタンプ精度に丸める、ということをやっている。なおext4ではタイムスタンプはns単位で保持できるので、ここで丸められることはない。

current_kernel_time()は、いくつかのwrapper関数を経て、最終的には以下の関数に至る。

struct timespec64 current_kernel_time64(void)
{
    struct timekeeper *tk = &tk_core.timekeeper;
    struct timespec64 now;
    unsigned long seq;

    do {
        seq = read_seqcount_begin(&tk_core.seq);

        now = tk_xtime(tk);
    } while (read_seqcount_retry(&tk_core.seq, seq));

    return now;
}

inline関数tk_xtime()を展開すると、

struct timespec64 current_kernel_time64(void)
{
    struct timekeeper *tk = &tk_core.timekeeper;
    struct timespec64 now;
    unsigned long seq;

    do {
        seq = read_seqcount_begin(&tk_core.seq);

        now.tv_sec = tk->xtime_sec;
        now.tv_nsec = (long)(tk->tkr_mono.xtime_nsec >> tk->tkr_mono.shift);
    } while (read_seqcount_retry(&tk_core.seq, seq));

    return now;
}

read_seqcount_begin()read_seqcount_retry()のループは一種の決まり文句で、更新途中の中途半端なデータを掴まないようにするためのもの。

次に、clock_gettime()の方は、kernel/time/posix-stubs.cにある。こちらは最終的に
以下の__getnstimeofday64に来る。

int __getnstimeofday64(struct timespec64 *ts)
{
    struct timekeeper *tk = &tk_core.timekeeper;
    unsigned long seq;
    u64 nsecs;

    do {
        seq = read_seqcount_begin(&tk_core.seq);

        ts->tv_sec = tk->xtime_sec;
        nsecs = timekeeping_get_ns(&tk->tkr_mono);

    } while (read_seqcount_retry(&tk_core.seq, seq));

    ts->tv_nsec = 0;
    timespec64_add_ns(ts, nsecs);

    /*
     * Do not bail out early, in case there were callers still using
     * the value, even in the face of the WARN_ON.
     */
    if (unlikely(timekeeping_suspended))
        return -EAGAIN;
    return 0;
}

両者を比べると、tv_nsec部分に違いがあることがわかる。touchコマンドのutimensat()ではtk->tkr_mono.xtime_nsecという変数を加工しているのに対し、dateコマンドのclock_gettime(CLOCK_REALTIME)は、timekeeping_get_ns()の結果を用い、オーバーフローした場合はtv_secを修正するようなことをしている。timekeeping_get_ns()をおいかけると、clocksourceを呼び出している。

本質的な違いは

タイムスタンプは変数のみ。時刻を得るのはclocksource (ハードウェア) 呼び出し。これが本質的な違い。

タイムスタンプを変えるときに参照されている変数は、tick、すなわち (原則として) HZ分の1秒ごとの定時処理で更新される。したがって、タイムスタンプはtickの単位で、ubuntu 18.04のgenericではHZ=250なので1/250秒=4ミリ秒まで過去の時刻を表す。ハードウェアにアクセスしないので、あらゆるアーキテクチャで高速に時刻が取得できる。

dateコマンドで現在時刻を得るときには、この変数の他にclocksourceのハードウェア (現在のx86では通常CPUのTSC: time stamp counter) を参照する。

TSCはたまたま高速にアクセス可能であるが、他のアーキテクチャのclocksourceが高速アクセス可能であるとは限らないため、dateコマンドなどで現在時刻を得るより頻繁に実行されると考えられるファイル更新のたびにclocksourceにアクセスするのは合理的ではない。

CLOCK_REALTIME_COARSE

実は、タイムスタンプを変えるときに使われているこの時刻、clock_gettime(2)でも取得することできる。このときには、第1引数にCLOCK_REALTIME_COARSEを指定する。

CLOCK_REALTIMEとCLOCK_REALTIME_COARSEを交互に呼ぶ、つまり、

    clock_gettime(CLOCK_REALTIME, &ts1);
    clock_gettime(CLOCK_REALTIME_COARSE, &ts2);
    clock_gettime(CLOCK_REALTIME, &ts3);
    clock_gettime(CLOCK_REALTIME_COARSE, &ts4);

のようにしたとき、ts2とts4は割と高い確率で同じ値になるが、ts1とts2、ts3が同じ値になることは、(少なくともclocksourceがtscである限り) ない。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1