ファイルをオープン(open)したらクローズ(close)しないといけない理由について

僕がプログラミングを始めたばかりの頃、よく「ファイルをクローズし忘れてるよ」と指摘されていました。
当時は「ファイルはオープンしたらクローズするもの...」と反射的にファイルをクローズするようにしていました。しかし、今になって思うと「なぜオープンしたファイルをクローズしなければならないのか」を疑問に思うべきでした。
実を言うと、未だにファイルをオープンしたらクローズしなければならない理由をハッキリと理解していません。メモリの使用量が増えるから...という何となく...の理解をしています。今回はオープンしたファイルはなぜクローズしなければならないのか調べてみました。

ファイルをオープンした時に何が起きているのか

なぜファイルをクローズしなければならないかは、ファイルをオープンした時に何が起きているのかを理解するのが一番の近道です。 Rubyでファイルをオープンした時の挙動を追っていきましょう。
ただ、僕がよく業務で書いているRubyはブロックのスコープを抜けるとファイルを自動的にクローズしてくれるという優れた機構を持っています。

File.open("sample.txt", "r") do |f|
  puts f.read
end

ブロックを指定して呼び出した場合は、File オブジェクトを引数としてブロックを実行します。
ブロックの実行が終了すると、ファイルは自動的にクローズされます。ブロックの実行結果を返します。

File.new (Ruby 3.2 リファレンスマニュアル)

ブロックを指定しない場合、File.open(path)はFileオブジェクトを返します。

irb(main):001:0> f = File.open("sample.txt", "r")
=> #<File:sample.txt>

このFileオブジェクトが返るまでにファイルがオープンされて、OSによってファイルディスクリプタという値(一般的には数値)が割り当てられます。ファイルディスクリプタとはファイルなどに割り当てられるユニークな値で、ファイルにアクセスする際にファイルディスクリプタの値が使用されます。

  • 1つ目のファイルが開かれた時: ファイルディスクリプタ: 3が与えられる
  • 2つ目のファイルが開かれた時: ファイルディスクリプタ: 4が与えられる

ちなみに0, 1, 2はすでに用途が決まっており、ファイルに割り当てられることはありません。

  • 0: 標準入力(stdin)
  • 1: 標準出力(stdout)
  • 2: 標準エラー出力(stderr)

つまり、ファイルへの直接のアクセスは許されずファイルディスクリプタという情報を経由して、やり取りをしているわけです(OSには詳しくないので正しい表現や間違いがあれば、ご指摘ください)。

ファイルディスクリプタはどこに記録されるのか

ファイルに割り当てられたファイルディスクリプタは一体、どこに記録されるのでしょうか。
その答えはメモリでした。自分がファイルを大量にオープンしてクローズしないままにしておくとメモリを圧迫するという理解をしていたのは、ここに根本的な理由がありました。

ただし、ファイルディスクリプタは先ほど紹介したように一般的にはただの数値でありメモリを圧迫するということは考えにくいです。それでも数万、数百万...とファイルがオープン後に放置されれば、塵も積もれば山となってメモリを圧迫する可能性は考えられます。

ファイルディスクリプタの上限値

ただし、ファイルを数万、数百万...とオープンすることはOSによっては難しいようです。
少なくともLinux系OSではファイルディスクリプタに上限を儲けているため、どちらかというと上限値に引っかかるという問題が発生するのですね。

Linux系OSではファイルディスクリプタの上限値が標準で『1024』となっています。

OSのファイル数設定

手元のMacbookでファイルディスクリプタの上限値を確認したところ256でした。
なお、上限値は設定ファイルを更新すれば変更できるそうです。

$ ulimit -n
256

ファイルを257個、オープンしてみたところエラーが発生しました。

files = []
(1..257).each do
  f = File.open("sample.txt", "r")
  files.push(f)
end

puts files.size
$ ruby file_open.rb
hello world!
file_open.rb:7:in `initialize': Too many open files @ rb_sysopen - sample.txt (Errno::EMFILE)
  from file_open.rb:7:in `open'
  from file_open.rb:7:in `block in <main>'
    from file_open.rb:6:in `each'
  from file_open.rb:6:in `<main>'

整合性と排他制御の問題

ファイルはクローズされないと変更内容が書き込まれません。
そのため、クローズを忘れているとオープン後に追加した情報が書き込まれずに「あれ、ファイルの中身が何も変化していない...」という事象が発生してしまいます。変更の整合性を保つためにはファイルのクローズが重要になってきます。

また、ファイルがオープンされた間、ファイルのオープンを行ったプロセス以外からはアクセスができず待機状態になるそうです。同じファイルへのアクセスは排他制御されるため、後続の処理への負担を避けるためにも目的を達成したファイルは早めにクローズさせた方が良さそうです。

まとめ

オープンしたファイルをクローズしなければならないのは以下の4つの理由のためです。

  • 大量のファイルをオープンしまくるとメモリを圧迫する可能性があるため
  • ファイルディスクリプタの上限に達してしまう可能性があるため
  • ファイルへの変更はクローズしないと書き込まれないため、整合性を確保するため
  • 他プロセスからのファイルへのアクセスをスムーズにするため

ようやく、ファイルをクローズしなければならない理由が分かりました。
プログラムの都合というよりかはOSの挙動・仕様に依存しているんですね。自分のようにWEB開発から入ってOSの知識が乏しい人間は要注意ですね。幅広い知識を身につけていきたいです。

少しでも「ええな〜」と思ったらイイネ!・シェア!・はてなブックマークを頂けると励みになります。