プロセスリソースに制限があろうとも僕は Process.#setrlimit で故意に落ちる

皆さん、恋してますか？僕は故意に恋い焦がれて故意に泣きたい心境です。

# きっかけ

[Fukuoka.rb #109](https://fukuokarb.connpass.com/event/103883/) でもくもくと[なるほどUnixプロセス ― Rubyで学ぶUnixの基礎](https://tatsu-zine.com/books/naruhounix)を読み進めていたのだが、 `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](https://docs.ruby-lang.org/ja/latest/method/Process/m/setrlimit.html)を見てみると何故かメソッドが 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](https://twitter.com/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|

なんじゃないかな、と思った。（ツッコミ待ち）