🕰️

ファイルのタイムスタンプが変更されるタイミング

2023/05/10に公開

実際の変化確認

macOS (APFS)

action modify change access birth now
新規作成 0 0 0 0 0
ls 0 0 0 0 2
上書き作成 4 4 0 0 4
touch 6 6 6 0 6
touch (Ruby) 8 8 8 0 8
追記 10 10 8 0 10
chmod 10 12 8 0 12
chown 10 14 8 0 14
新規作成 16 16 16 16 16
cat 16 16 18 16 18
touch 20 20 20 16 20
cat 20 20 20 16 22
追記 24 24 20 16 24
cat 24 24 25 16 26
cat 24 24 25 16 28
cat 24 24 25 16 30

Linux (ext4)

action modify change access now
新規作成 0 0 0 0
ls 0 0 0 2
上書き作成 4 4 0 4
touch 6 6 6 6
touch (Ruby) 7 7 7 8
追記 10 10 7 10
chmod 10 12 7 12
chown 10 14 7 14
新規作成 16 16 16 16
cat 16 16 17 18
touch 19 19 19 20
cat 19 19 21 22
追記 24 24 21 24
cat 24 24 25 26
cat 24 24 25 28
cat 24 24 25 30
  • 数字は対応する時間の秒を表わす
  • now は現在日時を表す
  • 1秒間隔にすると、より不可解な結果になるため2秒間隔にしている
  • 2秒間隔だと奇数になるはずがないのに実際は奇数秒になったりする(謎)
  • ext4 に birth は無い

参照するだけでは何も変化しない

action modify change access birth now
新規作成 0 0 0 0 0
find 0 0 0 0 1
ls 0 0 0 0 2
exist? 0 0 0 0 3

参照するということはアクセスするということなので当然 access は更新されるものと考えていたが実際は find, ls, ファイル存在確認程度では何も変化しなかった。

上書き作成と新規作成は異なる

action modify change access birth now
新規作成 0 0 0 0 0
上書作成 1 1 0 0 1
新規作成 2 2 2 2 2
更新 3 3 2 2 3
  • 同名のファイルをいったん削除してから作るのと上書きしてファイルを作るのとではファイルの情報に差が出る
  • 「上書き作成」は「更新」と同じで modify, change だけが更新されているのがわかる

chmod や chown で change だけが更新される

action modify change access birth now
新規作成 0 0 0 0 0
chmod 0 1 0 0 1
chown 0 2 0 0 2

これはわかる

modify だけ更新するつもりが change まで更新される

action modify change access birth now
新規作成 0 0 0 0 0
追記 1 1 0 0 1

どういうことだ……?

modify だけを変更するのは難しい

action modify change access birth now
新規作成 0 0 0 0 0
touch -m 2 2 0 0 2
touch -m (Ruby) 4 4 0 0 4

難しいというかできなかった。

touch --help-m change only the modification time と出てくるから touch -m としても change まで更新されてしまう。

File.utime を使って File.utime(File.atime("x"), Time.now, "x") としても change まで更新されてしまう。

ctime は作成日時ではなかった

action modify change access birth now
新規作成 0 0 0 0 0
追記 2 2 0 0 2
追記 4 4 3 0 4
  • ctime の c は change のこと
  • この change はコンテンツを更新すると modify と一緒に更新されているのがわかる
  • つまり「作成日時」ではなく「更新日時」に近い
  • それだと modify と同じになってしまって意味がないように感じる
  • 2回目の追記でいきなり access が更新されたのは謎
  • ctime の命名がよくない

touch は modify, change, access が変化する

action modify change access birth now
新規作成 0 0 0 0 0
touch 1 1 1 0 1
touch (Ruby) 2 2 2 0 2
  • さすがに birth は変化しない (それでいい)
  • 上書き作成のあとで access が変化しないのが気になるなら touch しておく
  • Ruby の FileUtils.touch の説明には modify と access が変化すると書いてあるが実際は change も変化した。このあたりはファイルシステムによって違うのかもしれない

birth は本当に必要だったもの

action modify change access birth now
新規作成 0 0 0 0 0
追記 1 1 0 0 1
chmod 1 2 0 0 2
新規作成 3 3 3 3 3
  • 作成日時を表す
  • 何をされても変化しない
  • Ruby だと File.birthtime で取れる
  • ext4 にはこの属性がない

access の変化タイミングはよくわからない

action modify change access birth now
新規作成 0 0 0 0 0
cat 0 0 1 0 1 0 > 0 は偽なのに更新された(謎)
touch 2 2 2 0 2
cat 2 2 2 0 3 2 > 2 は偽なので更新されない
追記 4 4 2 0 4
cat 4 4 5 0 5 4 > 2 は真なので更新された
cat 4 4 5 0 6 4 > 5 は偽なので更新されない
action modify change access birth now
追記 14 14 12 10 14
cat 14 14 15 10 15
cat 14 14 16 10 16 14 > 15 は偽なのにさらに更新された(謎)
cat 14 14 16 10 17

こちらによると「modify > access または change > access なら更新」というロジックになっているそうで、これが APFS にも合てはまるのかはわからないけど実際は上のようになっていてどういうロジックなのかよくわからなかった。

また noatime でマウントする と atime が更新されなくなるらしい。

ファイルシステムの調べ方

$ df -T /
Filesystem     Type 1K-blocks      Used Available Use% Mounted on
/dev/disk3s1s1 apfs 971350180 452586272 518763908  47% /

$ df --output=fstype / | tail -1
apfs

検証用コード

検証用コード
require "fileutils"
require "table_format"

F = FileUtils

class Context
  attr_reader :records

  def call(...)
    @records = []
    instance_eval(...)
    tp @records.collect(&:to_h)
    exit
  end

  def test(action)
    time_next
    yield
    @records << Record.new(self, action, @records.last)
  end

  def time_next
    i = Time.now.to_i
    nil until Time.now.to_i >= (i + step)
  end

  def step
    2
  end

  class Record
    attr_reader :context
    attr_reader :before
    attr_reader :times
    attr_reader :now

    def initialize(context, action, before)
      @context = context
      @action = action
      @before = before
      @now = Time.now

      s = File.stat("x")
      @times = {
        :modify => TimeOne.new(self, :modify, s.mtime),
        :change => TimeOne.new(self, :change, s.ctime),
        :access => TimeOne.new(self, :access, s.atime),
      }
      begin
        @times[:birth] = TimeOne.new(self, :birth, s.birthtime)
      rescue NotImplementedError
      end
    end

    def to_h
      {
        "action" => @action,
        **@times.inject({}) {|a, (_, e)| a.merge(e.to_h) },
        "now" => @now.to_i - @context.records.first.now.to_i,
      }
    end

    class TimeOne
      def initialize(record, name, time)
        @record = record
        @name = name
        @time = time
      end

      def to_i
        @time.to_i - @record.context.records.first.now.to_i
      end

      def to_s
        if @record.before && @record.before.times[@name].to_i != to_i
          "**#{to_i}**"
        else
          to_i.to_s
        end
      end

      def to_h
        { @name => to_s }
      end
    end
  end
end

Context.new.call do
  test("新規作成")     { `rm -f x; echo > x` }
  test("ls")           { `ls x`              }
  test("上書き作成")   { `echo > x`          }
  test("touch")        { `touch x`           }
  test("touch (Ruby)") { F.touch("x")        }
  test("追記")         { `echo >> x`         }
  test("chmod")        { `chmod a+w x`       }
  test("chown")        { `chown :wheel x`    }
  test("新規作成")     { `rm -f x; echo > x` }
  test("cat")          { `cat x`             }
  test("touch")        { `touch x`           }
  test("cat")          { `cat x`             }
  test("追記")         { `echo >> x`         }
  test("cat")          { `cat x`             }
  test("cat")          { `cat x`             }
  test("cat")          { `cat x`             }
end

Context.new.call do
  test("新規作成") { `rm -f x; echo > x`                    }
  test("追記")     { File.open("x", "a") { |e| e.puts "x" } }
  test("追記")     { File.open("x", "a") { |e| e.puts "x" } }
end

Context.new.call do
  test("新規作成")        { `rm -f x; echo > x`                        }
  test("touch -m")        { `touch -m x`                               }
  test("touch -m (Ruby)") { File.utime(File.atime("x"), Time.now, "x") }
end

Context.new.call do
  test("新規作成")         { `rm -f x; echo > x`          }
  test("touch -m")        { `touch -m x`                  }
  test("FileUtils.touch") { F.touch("x", mtime: Time.now) }
end

Context.new.call do
  test("新規作成") { `rm -f x; echo > x` }
  test("追記")     { `echo >> x`         }
end

Context.new.call do
  test("新規作成") { `rm -f x; echo > x` }
  test("chmod")    { `chmod a+w x`       }
  test("chown")    { `chown :wheel x`    }
end

Context.new.call do
  test("新規作成") { `rm -f x; echo > x` }
  test("追記")     { `echo >> x`         }
  test("chmod")    { `chmod a+w x`       }
  test("新規作成") { `rm -f x; echo > x` }
end

Context.new.call do
  test("新規作成") { `rm -f x; echo > x` }
  test("追記")     { `echo >> x`         }
  test("追記")     { `echo >> x`         }
end

Context.new.call do
  test("新規作成") { `rm -f x; echo > x` }
  test("cat")      { `cat x`             }
  test("touch")    { `touch x`           }
  test("cat")      { `cat x`             }
  test("追記")     { `echo >> x`         }
  test("cat")      { `cat x`             }
  test("cat")      { `cat x`             }
  test("cat")      { `cat x`             }
end

Context.new.call do
  test("新規作成")     { `rm -f x; echo > x` }
  test("touch")        { `touch x`           }
  test("touch (Ruby)") { F.touch("x")        }
end

Context.new.call do
  test("新規作成") { `rm -f x; echo > x` }
  test("上書作成") { `echo > x`          }
  test("新規作成") { `rm -f x; echo > x` }
  test("追記")     { `echo >> x`         }
end
# >> |--------------|--------|--------|--------|--------|-----|
# >> | action       | modify | change | access | birth  | now |
# >> |--------------|--------|--------|--------|--------|-----|
# >> | 新規作成     |      0 |      0 |      0 |      0 |   0 |
# >> | ls           |      0 |      0 |      0 |      0 |   2 |
# >> | 上書き作成   | **4**  | **4**  |      0 |      0 |   4 |
# >> | touch        | **6**  | **6**  | **6**  |      0 |   6 |
# >> | touch (Ruby) | **8**  | **8**  | **8**  |      0 |   8 |
# >> | 追記         | **10** | **10** |      8 |      0 |  10 |
# >> | chmod        |     10 | **12** |      8 |      0 |  12 |
# >> | chown        |     10 | **14** |      8 |      0 |  14 |
# >> | 新規作成     | **16** | **16** | **16** | **16** |  16 |
# >> | cat          |     16 |     16 | **18** |     16 |  18 |
# >> | touch        | **20** | **20** | **20** |     16 |  20 |
# >> | cat          |     20 |     20 |     20 |     16 |  22 |
# >> | 追記         | **24** | **24** |     20 |     16 |  24 |
# >> | cat          |     24 |     24 | **25** |     16 |  26 |
# >> | cat          |     24 |     24 |     25 |     16 |  28 |
# >> | cat          |     24 |     24 |     25 |     16 |  30 |
# >> |--------------|--------|--------|--------|--------|-----|

まとめ

  • ctime の c は change の c
  • atime の a は access だけど中身を読み出したときに変化するので言葉のイメージとしては access というより read の方があっている
  • birth は ext4 では使えない
  • ls したぐらいじゃ何も更新されない
  • 追記すると modify だけが更新されるらしいが実際は change も更新される
  • 追記では access は更新されないが、たまに更新される
  • chmod や chown で change のみ更新される
  • 上書きは中身を更新したのと同じ
  • cat するとmodify > access または change > access のとき access が更新されるのは確かだけど、条件無視で更新されることもあった
  • access の更新タイミングは予想がつかない
  • 反映される時間が現在の時間と1秒ずれていることがある
  • 調べたことで余計にわからないことが増えた

Discussion