(以下の本文の実行環境は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である限り) ない。