Ruby
Linux

ファイルディスクリプタについて理解する

More than 1 year has passed since last update.

ファイルディスクリプタについて分かっているような分かっていないような感じだったのRubyでコードを書きつつ、理解したときのメモ

参考

そもそもファイルディスクリプタとは何か

以下より引用

IT用語辞典

ファイルディスクリプタとは、プログラムがアクセスするファイルや標準入出力などをOSが識別するために用いる識別子。0から順番に整数の値が割り当てられる。OSによってはファイルディスクリプタにバッファ管理機能なども含めた「ファイルハンドル」と呼ばれる管理体系が存在する。

ファイルディスクリプタには、識別子とともにファイル名、ファイルサイズ、プログラムが操作中のファイル内の位置、ファイル作成、更新日時などの情報が含まれており、OSは識別子によってどのファイルを操作するかを判断する。

通常、0:標準入力(stdin)、1:標準出力(stdout)、2:標準エラー出力(stderr)の3つはOS(シェル)が最初に用意するため、プログラムがファイルをオープンすると「3」から順番にディスクリプタが割り当てられる。

分かったようなわからない部分もあるような。
実際にコードなどを書いて挙動を確認します。

RubyでIOクラスのメソッドではファイルディスクリプタの番号をfilenoメソッドで取得できるので、上記の最後の部分に書いてある事項について確認してみます。

descriptor.rb
puts $stdin.fileno # 0
puts $stdout.fileno # 1
puts $stderr.fileno # 2

確かに標準入力、標準出力、標準エラー出力にそれぞれファイルディスクリプタが上記数字の通り、割り当たっていることが分かりました。

今度は既存ファイルをread権限で読み込み、そのファイルディスクリプタの番号を取得してみます。

read.rb
file =  File.open('/etc/hosts', 'r')
puts file.fileno # 7
puts file.readline # 1行目を表示
puts file.readline # 2行目を表示
file.close

プロセス(今回はRubyプログラム)から直接ファイルへのIOは出来ないため、ファイルの読み込みなどは内部的にはカーネルを介して行っています。

上記プログラムの場合には以下をやっていることとなります。

  • 1行目->/etc/hostsのファイルを読み込みたい旨をカーネルに伝える(この部分はRubyがやってくれる)Rubyでは汎用的なFileオブジェクトを生成する
  • 2行目->Fileオブジェクトからファイルディスクリプタを確認。7番が利用されている
  • 3行目->ファイルディスクリプタ7番の1行目読みたい旨をカーネルに伝える
  • 4行目->ファイルディスクリプタ7番の次の行を読みたい旨をカーネルに伝える
  • 5行目->もうこのプロセス(Rubyプログラム)では使わないのでリソースを開放してくださいという旨をカーネルに伝える

コード4行目でファイルの2行目を読み込んでいますが、プロセス側では何行目まで読み込んだかは覚えてなくても、カーネルにファイルディスクリプタ番号と次の行を読みたいと旨を伝えれば

  • Rubyのプロセスでは
  • /etc/hostsファイルの
  • 2行目の内容(1行目はすでに読み込まれたので)を読みたい

という事を理解し、対象の内容のみを返却してくれます。
便利ですね。

上記例では一つのファイルのみを開きましたが、もし複数のファイルを開いた場合には数値がどんどん増加していき、それだけファイルディスクリプタが増えていくのが分かります。

read.rb
file =  File.open('/etc/hosts', 'r')
puts file.fileno # 7

file =  File.open('/etc/passwd', 'r')
puts file.fileno # 8

ただし、不要になったリソースをcloseする事でファイルディスクリプタが番号が再利用されます。

read.rb
file =  File.open('/etc/hosts', 'r')
puts file.fileno # 7
file.close
file =  File.open('/etc/passwd', 'r')
puts file.fileno # 7
file.close

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

ファイルディスクリプタとかulimitとか/proc/sys/fsとかめも

ファイルディスクリプタはリソース(ファイルなど)を利用する事で数字が増えていく事が分かりました。
ファイルディスクリプタは無限に使えるのかというとそんな事はなく、プロセスごとに使える制限があります。

Rubyでは以下のようにして制限数を確認できます。(irbを利用)

$irb
irb(main):001:0> p Process.getrlimit(:NOFILE)
[1024, 4096]
=> [1024, 4096]

配列の1番目の要素がファイルディスクリプタのソフトリミットで第2引数がファイルディスクリプタのハードリミットになります。一般ユーザではソフトリミットは変更が可能ですが、ハードリミット以上の値を設定することはできません。rootであればバードリミットの変更も可能です。

また、ulimitコマンドを使うことで同様にファイルディスクリプタの値が確認できます。

# ソフトリミット
$ulimit -Sn
1024

# ハードリミット
$ulimit -Hn
4096

プロセスではソフトリミットより多くのファイルディスクリプタを生成しようとするとエラーになります。

limit.rb
# ファイルディスクリプタを3にする
Process.setrlimit(:NOFILE, 3)
p Process.getrlimit(:NOFILE)

# ファイルを開く(ファイルディスクリプタが4になる)
File.open('/dev/null')
$ ruby limit.rb
[3, 3]
limit.rb:3:in `initialize': Too many open files @ rb_sysopen - /dev/null (Errno::EMFILE)
    from limit.rb:3:in `open'
    from limit.rb:3:in `<main>'

nginxやMySQLで同時接続数の設定をする場合などがありますが、その場合には併せてファイルディスクリプタを増やさないと上記のようにファイルが開けずエラーとなることがあります。

なお、上記はプロセスごとの制限数でOS全体で開くことができるファイル数は以下で確認できます。

$cat /proc/sys/fs/file-max
99848

また、/proc/[pid]/limits`ファイルを確認すると対象プロセスIDのファイルディスクリプタが確認できます。(Max open files)

$cat /proc/22892/limits
Limit                     Soft Limit           Hard Limit           Units
Max cpu time              unlimited            unlimited            seconds
Max file size             unlimited            unlimited            bytes
Max data size             unlimited            unlimited            bytes
Max stack size            8388608              unlimited            bytes
Max core file size        0                    unlimited            bytes
Max resident set          unlimited            unlimited            bytes
Max processes             3914                 3914                 processes
Max open files            3                    3                    files
Max locked memory         65536                65536                bytes
Max address space         unlimited            unlimited            bytes
Max file locks            unlimited            unlimited            locks
Max pending signals       3914                 3914                 signals
Max msgqueue size         819200               819200               bytes
Max nice priority         0                    0
Max realtime priority     0                    0
Max realtime timeout      unlimited            unlimited            us

ファイルディスクリプタの設定を変えるには

ulimitコマンドで設定する(一般ユーザー)

一番単純なのはulimitコマンドを使ってulimitを実行したシェル(bashやzshなど)とシェルから実行された子プロセスが対象です。

# ファイルディスクリプタのソフトリンクを3000に変更
$ulimit -Sn 3000

# 確認
$ulimit -Sn
3000

ただし、上記では一時的な設定の変更のため、一度ログアウトしてログインし直すと最初の値(1046)に戻ってしまうので注意が必要です。また、先ほど記載したようにハードリミット以上の値は設定出来ません。

$ulimit -Hn
4096

$ulimit -Sn 10000
-bash: ulimit: open files: limit を変更できません : 許可されていない操作です

/etc/security/limits.confで設定する

/etc/security/limits.confに設定を書くことでハードリンク、ソフトリンクを設定できます。

他のサイトにも色々書いてありますが、ここに書くとPAM認証(sudoやユーザーでログインした場合)がないと有効化されないので例えばOS再起動時に自動起動するプロセスでファイルディスクリプタを増やしたいような場合には不適切です。

なお、設定方法は以下のようです。(未検証)

/etc/security/limits.conf
 * soft nofile 2048
 * hard nofile 2048

ulimitで変更する(rootユーザー)

ulimitのハードリンクは一般ユーザーでは変更できませんが、rootユーザーであれば変更できます。

$sudo su

# ハードリンクを変更
$ulimit -Hn 10000
$ulimit -Hn
10000

# ソフトリンクも変更
$ulimit -Sn 10000
$ulimit -Sn
10000

rootユーザーでハードリンクを変更後、プロセス実行ユーザーを指定することでファイルディスクプリンタを変更できます。

# sleepするだけのプログラムをバックグラウンドで起動
sudo -u ec2-user ruby -e "sleep" &
[1] 26188

# プロセスを確認
$ps aux |grep ruby
root     26188  0.0  0.4 184076  4500 pts/1    S    12:55   0:00 sudo -u ec2-user ruby -e sleep
ec2-user 26189  0.0  0.8 135300  8644 pts/1    Sl   12:55   0:00 ruby -e sleep
root     26211  0.0  0.2 110472  2256 pts/1    S+   12:58   0:00 grep --color=auto ruby

# ファイルディスクリプタを確認。増加を確認
$cat /proc/26189/limits |grep -e "Max open files"
Max open files            10000                10000                files

上記より、デーモンの起動スクリプト(/etc/rc.d/init.d/など)でulimitコマンドを使ってファイルディスクリプタを設定するようになっていれば、デーモンプロセスでも設定を行うことができます。

>/dev/null 2>&1について

シェルスクリプトで標準出力、標準エラー出力を捨てる場合によく以下のように書きます。

command >/dev/null 2>&1

これですが、あまり意味が分かってませんでしたが、ファイルディスクリプタでは

  • 0は標準入力
  • 1は標準出力
  • 2は標準エラー出力

という事を思い出すとと理解しやすいです。

例えば以下のように書くと標準出力はhoge.txtに標準エラー出力はfuga.txtに書き込まれます。

$ruby -e "STDOUT.puts 'hoge';STDERR.puts 'fuga'" 1>hoge.txt 2>fuga.txt

1,2というのが何なのか今まで分かってなかったのでイマイチ覚えにくかったのですが、標準出力(1)と標準エラー出力(2)のファイルディスクリプタ番号を意味するというのが分かると上記の意味も良くわかります。

また、標準出力に関しては以下のように1を省略しても大丈夫であるという点を覚えておくと良いです。

$ruby -e "STDOUT.puts 'hoge';STDERR.puts 'fuga'" > hoge.txt 2>fuga.txt

あともう1点最後に覚えておくこととして&1と書くとファイル名ではなく、標準出力を表す数字である点もポイントです。
以下に例を示します。

# これだと1というファイルに標準エラー出力を書く
$ruby -e "STDOUT.puts 'hoge';STDERR.puts 'fuga'" 1>hoge.txt 2>1

# 標準出力と標準エラー出力をhoge.txtに出力
$ruby -e "STDOUT.puts 'hoge';STDERR.puts 'fuga'" 1>hoge.txt 2>&1

また、標準出力では1をは省略しても良い点を考えると以下のようにも書けます。

$ruby -e "STDOUT.puts 'hoge';STDERR.puts 'fuga'" >hoge.txt 2>&1

あとは最初の例のように出力先を/dev/nullにするなり、適当なファイル名にするでもOKです。
色々な要素が含まれるのでややこしいのですが、一つ一つ理解すると上記のように具体的な意味が理解できました。