皆さん、恋してますか?僕は故意に恋い焦がれて故意に泣きたい心境です。
きっかけ
Fukuoka.rb #109 でもくもくとなるほどUnixプロセス ― Rubyで学ぶUnixの基礎を読み進めていたのだが、 Process.#setrlimit
の項目で意図せず irb が落ちた。なぜ落ちたのか自分にはさっぱり分からなかった。
動作環境はこんな感じ。
- Debian 9 (VirtualBox)
- Ruby 2.5.1
- irb 0.9.6
事象
先述の書籍に以下のような実行例があった。
Process.setrlimit(:NOFILE, 3)
File.open('/dev/null')
これを実行すると Errno::EMFILE: Too many open files - /dev/null
が出力されると書いてあったので、素直に irb で試してみた。
$ irb
irb(main):001:0> Process.setrlimit(:NOFILE, 3)
=> nil
Traceback (most recent call last):
21: from /home/jinroq/.rbenv/versions/2.5.1/bin/irb:11:in `<main>'
20: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb:383:in `start'
19: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb:427:in `run'
18: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb:427:in `catch'
17: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb:428:in `block in run'
16: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb:487:in `eval_input'
15: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:231:in `each_top_level_statement'
14: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:231:in `catch'
13: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:232:in `block in each_top_level_statement'
12: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:232:in `loop'
11: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:236:in `block (2 levels) in each_top_level_statement'
10: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:266:in `lex'
9: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:292:in `token'
8: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/slex.rb:76:in `match'
7: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/slex.rb:206:in `match_io'
6: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:104:in `getc'
5: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:189:in `buf_input'
4: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb:470:in `block in eval_input'
3: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb:623:in `signal_status'
2: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb:471:in `block (2 levels) in eval_input'
1: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/input-method.rb:149:in `gets'
/home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/input-method.rb:149:in `input=': Invalid argument - dup (Errno::EINVAL)
$
Process.#setrlimit
を実行しただけで irb が落ちた。それだけならまだしもエラーメッセージが Invalid argument
と全く異なるものが出力された。
検証
Process.#setrlimitを見てみると何故かメソッドが 2 通り載っているが、これは
setrlimit(resource, cur_limit, max_limit = cur_limit)
ってことだろうか?ひとまず第二引数の値を色々いじって確認した。
$ irb
irb(main):001:0> Process.setrlimit(:NOFILE, 4)
=> nil
Traceback (most recent call last):
21: from /home/jinroq/.rbenv/versions/2.5.1/bin/irb:11:in `<main>'
20: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb:383:in `start'
19: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb:427:in `run'
18: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb:427:in `catch'
17: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb:428:in `block in run'
16: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb:487:in `eval_input'
15: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:231:in `each_top_level_statement'
14: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:231:in `catch'
13: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:232:in `block in each_top_level_statement'
12: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:232:in `loop'
11: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:236:in `block (2 levels) in each_top_level_statement'
10: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:266:in `lex'
9: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:292:in `token'
8: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/slex.rb:76:in `match'
7: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/slex.rb:206:in `match_io'
6: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:104:in `getc'
5: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:189:in `buf_input'
4: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb:470:in `block in eval_input'
3: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb:623:in `signal_status'
2: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb:471:in `block (2 levels) in eval_input'
1: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/input-method.rb:149:in `gets'
/home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/input-method.rb:149:in `input=': Too many open files - dup (Errno::EMFILE)
$
$ irb
irb(main):007:0> Process.setrlimit(:NOFILE, 8)
=> nil
Traceback (most recent call last):
21: from /home/jinroq/.rbenv/versions/2.5.1/bin/irb:11:in `<main>'
20: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb:383:in `start'
19: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb:427:in `run'
18: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb:427:in `catch'
17: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb:428:in `block in run'
16: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb:487:in `eval_input'
15: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:231:in `each_top_level_statement'
14: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:231:in `catch'
13: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:232:in `block in each_top_level_statement'
12: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:232:in `loop'
11: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:236:in `block (2 levels) in each_top_level_statement'
10: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:266:in `lex'
9: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:292:in `token'
8: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/slex.rb:76:in `match'
7: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/slex.rb:206:in `match_io'
6: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:104:in `getc'
5: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/ruby-lex.rb:189:in `buf_input'
4: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb:470:in `block in eval_input'
3: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb:623:in `signal_status'
2: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb:471:in `block (2 levels) in eval_input'
1: from /home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/input-method.rb:150:in `gets'
/home/jinroq/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb/input-method.rb:150:in `output=': Too many open files - dup (Errno::EMFILE)
$
$ irb
irb(main):001:0> Process.setrlimit(:NOFILE, 9)
=> nil
irb(main):002:0> p Process.getrlimit(:NOFILE)
[9, 9]
=> [9, 9]
irb(main):003:0>
まとめるとこんな感じ。
cur_limit の値 | 実行結果 |
---|---|
3 | Invalid argument - dup (Errno::EINVAL) |
4 以上 8 以下 | Too many open files - dup (Errno::EMFILE) |
9 以上 | 正常に制限値が設定される |
ただし cur_limit = 8
の場合だけ、エラー箇所が input-method.rb:150
となっていた。
分かりそうな人に聞いてみた
という訳で近くにいた nagachika さんに聞いてみた。非常に親切丁寧に解説していただいたのだが、正直お話がディープすぎてあまり理解できていない。記憶を探って書いてみる。
- 「irb が落ちている」のではなく、「irb が何かしらの理由で処理を停止させている」
- irb の
--noreadline
オプションを付けると停止しないはず - irb 起動中のファイルディスクリプターの最小値は 3 ではなく、実は他にも動いている
確かめてみた
$ irb --noreadline
irb(main):001:0> Process.setrlimit(:NOFILE, 3)
=> nil
irb(main):002:0> File.open('/dev/null')
Traceback (most recent call last):
4: from /home/jinroq/.rbenv/versions/2.5.1/bin/irb:11:in `<main>'
3: from (irb):2
2: from (irb):2:in `open'
1: from (irb):2:in `initialize'
Errno::EMFILE (Too many open files @ rb_sysopen - /dev/null)
irb(main):003:0>
おお!
$ irb
irb(main):001:0> require 'objspace'
=> true
irb(main):002:0> ObjectSpace.each_object(IO).each{ |i| p i unless i.closed? }
# <IO:<STDERR>>
# <IO:<STDOUT>>
# <IO:<STDIN>>
# <IO:fd 1>
# <IO:fd 0>
=> 6
irb(main):003:0>
おおー!
まとめ
- readline を使わなければ期待通りに動く
おまけ
$ irb
irb(main):001:0> passwd = File.open('/etc/passwd')
=> #<File:/etc/passwd>
irb(main):002:0> puts passwd.fileno
9
=> nil
irb(main):003:0>
どうも 9 が最小値っぽい。また本の訳注には「ruby 1.9.3-p392/2.0.0-p0 では最小値 5。3 と 4 は YARV に予約されていた」とあった。つまり、
irb 起動中のファイルディスクリプター番号 | 予約している人 |
---|---|
0, 1 | fd |
2 | STDIN |
3 | STDOUT |
4 | STDERR |
5, 6, 7 | YARV |
なんじゃないかな、と思った。(ツッコミ待ち)