Posted at

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

More than 3 years have 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です。

色々な要素が含まれるのでややこしいのですが、一つ一つ理解すると上記のように具体的な意味が理解できました。