ファイルディスクリプタについて一回整理したい
Reference
おなじみの
詳細
🔧 概要
ファイルディスクリプタとは?
- OSがファイルやソケット、パイプなどの入出力リソースを管理・識別するために使う一意の整数値。
- プログラムがファイルや通信を扱うとき、通常まずはOSからディスクリプタを取得し、それを通してデータの読み書きを行っている。
代表的なファイルディスクリプタ番号
OS起動時、プロセスには以下の3つの標準ディスクリプタが自動で割り当てられている
| ディスクリプタ | 名前 | 詳細 |
|---|---|---|
| 0 | stdin |
標準入力(キーボードなど) |
| 1 | stdout |
標準出力(画面など) |
| 2 | stderr |
標準エラー出力 |
ファイルディスクリプタの役割と利点
- OSカーネルがリソースを管理しやすくなる。
- 抽象化により、ファイル・ソケット・パイプなどを同じ方法で扱える。
- パフォーマンスが高く、低レベルの操作が可能になる。
現在の設定値の確認方法について
プロセスごとの最大オープン可能なファイルディスクリプタ数を確認
$ ulimit -n
1024
プロセスの現在のファイルディスクリプタ数を確認
# $$ は現在のシェルのプロセスIDを表す
$ ls /proc/$$/fd | sort -n
0
1
2
10
11
12
14
15
17
19
20
# プロセスの現在のファイルディスクリプタ数 ※FDは11個存在していることがわかる
$ ls /proc/$$/fd | wc -l
11
# `/proc/<pid>/fd` ディレクトリには現在開いているファイルディスクリプタの一覧が存在する
$ ls -lh /proc/$$/fd
total 0
lrwx------ 1 hoge hoge 64 May 1 05:53 0 -> /dev/pts/1
lrwx------ 1 hoge hoge 64 May 1 05:53 1 -> /dev/pts/1
lrwx------ 1 hoge hoge 64 May 1 05:53 10 -> /dev/pts/1
lr-x------ 1 hoge hoge 64 May 1 05:53 11 -> /usr/share/zsh/functions/Chpwd.zwc
lr-x------ 1 hoge hoge 64 May 1 05:53 12 -> /usr/share/zsh/functions/Completion.zwc
lr-x------ 1 hoge hoge 64 May 1 05:53 14 -> /usr/share/zsh/functions/Completion/Base.zwc
lr-x------ 1 hoge hoge 64 May 1 05:53 15 -> /usr/share/zsh/functions/Misc.zwc
lr-x------ 1 hoge hoge 64 May 1 05:53 17 -> /usr/share/zsh/functions/Zle.zwc
lr-x------ 1 hoge hoge 64 May 1 05:53 19 -> /usr/share/zsh/functions/Completion/Zsh.zwc
lrwx------ 1 hoge hoge 64 May 1 05:53 2 -> /dev/pts/1
lr-x------ 1 hoge hoge 64 May 1 05:53 20 -> /usr/share/zsh/functions/Completion/Unix.zwc
システム全体の上限値を確認
# システム全体の上限値を確認する
$ cat /proc/sys/fs/file-max
811372
ファイルディスクリプタが増える場合の条件について
1. ファイルを開いたとき
ファイルを開くと、OSはそれを識別するためにFDを割り当てる。
f, err := os.Open("/etc/hosts") // ファイルを開くとFDが1つ増える
if err != nil {
log.Fatal(err)
}
defer f.Close() // 閉じないとFDがリークする
備考:
- 同じファイルでも、複数回開くと複数のFDが割り当てられる。
-
os.Create()やos.OpenFile()も同様にFDを消費する。
2. ソケット通信を開始したとき
ソケットもFDとして管理される。
TCP/UDP通信などで net.Dial() や net.Listen() を使うとFDが増える。
ln, err := net.Listen("tcp", ":8080") // ポート待ち受けで1FD
conn, err := ln.Accept() // 接続ごとに1FD
備考:
- サーバー側は接続ごとに新たなFDを受け取る。
- クライアント側も Dial のたびにFDが増える。
3. パイプやプロセス間通信を使ったとき
パイプもFDを使ってデータを送受信する。
2つのFD(読み取り/書き込み)が生成される。
r, w := io.Pipe() // 2つのFD:rとw
または、
cmd := exec.Command("ls")
stdout, _ := cmd.StdoutPipe() // プロセス出力にパイプを接続 → FD増加
4. スレッドやプロセスでリソースを共有したとき
- fork() でプロセスを複製するとFDはコピーされて子プロセスにも存在。
- Goでは exec.Command() で別プロセスを起動するが、明示的にFDを渡すことも可能。
cmd := exec.Command("cat")
stdin, _ := cmd.StdinPipe() // FDが増える
5. 標準入出力のリダイレクト(シェル)
標準出力(1番)などをファイルにリダイレクトすると、ファイルがオープンされFDが割り当てられる。
f, _ := os.Create("output.txt")
cmd := exec.Command("echo", "hello")
cmd.Stdout = f // 標準出力をファイルにリダイレクト
cmd.Run()
f.Close()
6. ライブラリや言語処理系による内部利用
Goのランタイム、Zshなどのシェル、あるいはサードパーティライブラリは、内部で必要なファイルやソケットを開くことがある。
例:
- zsh の .zwc ファイル
- Goの net/http パッケージでの内部ソケット
- データベースライブラリが接続のために開くFD
備考:
- 開発者が意図していない場面でFDが増えるため、lsof や /proc/self/fd の確認が有効。
7. FDリーク(クローズし忘れ)
開いたFDを閉じ忘れると、FDはOSに残り続け、上限(通常1024など)に達して新規FDが取得できなくなる確率が上がる。
for i := 0; i < 1000; i++ {
f, _ := os.Open("/etc/hosts") // Close() 忘れ
}
備考:
- Goの defer f.Close() を使うことで、関数終了時に必ずFDを閉じるような処理を実装する。
ファイルディスクリプタが増える条件まとめ
| 条件 | FDが増える理由 | 対策・注意点 |
|---|---|---|
| ファイルを開く | OSがFDを割り当てる |
Close() 忘れずに |
| ソケット通信 | 接続ごとに1 FD |
conn.Close() を使う |
| パイプ通信 | 読み/書きの2 FD | 使い終えたら Close()
|
| プロセス共有 |
fork / exec で FD が引き継がれる |
不要なら CloseOnExec フラグを設定 |
| リダイレクト | ファイルを開く処理が発生(FDが追加で使われる) |
defer file.Close() 推奨 |
| 内部利用 | ランタイムやライブラリが内部で FD を使用 |
lsof, /proc/self/fd で可視化 |
| リーク | 開いた FD を閉じ忘れる |
defer で確実に Close() を実行 |
FDが枯渇した場合
ファイルディスクリプタが 枯渇(上限に到達) した場合、 サーバ側では致命的なエラーやサービス不能状態(DoS的な挙動) が発生することがある。
※新規接続の拒否・処理不能・クラッシュといった障害に陥る。
ファイルディスクリプタの上限値を引き上げる設定
- ファイルディスクリプタの上限は OS レベルで設定されており、必要に応じて増やすことが可能。
- ただし、無制限に増やすことは推奨されていない。
一時的な対応
# 現在のFDの上限値を確認
$ ulimit -n
1024
# FDの上限値を増やす
$ ulimit -n 65535
# FDの上限値が変わっていることを確認
$ ulimit -n
65535
※ プロセス単位での設定のため、shellやdaemon再起動で元に戻る。
永続的に設定(Ubuntu/Debian系)
/etc/security/limits.conf に追記する。
$ cat /etc/security/limits.conf
# /etc/security/limits.conf
#
#Each line describes a limit for a user in the form:
#
#<domain> <type> <item> <value>
... 省略
#* soft core 0
#root hard core 100000
#* hard rss 10000
#@student hard nproc 20
#@faculty soft nproc 20
#@faculty hard nproc 50
#ftp hard nproc 0
#ftp - chroot /ftp
#@student - maxlogins 4
# End of file
上記confに以下を追記
# ソフト/ハードともに上限値まで設定
<ユーザー名> soft nofile 65535
<ユーザー名> hard nofile 65535
common-session ファイルに以下の設定が含まれていることを確認
$ fgrep 'required' /etc/pam.d/common-session
session required pam_permit.so
session required pam_unix.so
補足:システム全体の上限確認と変更
現在利用しているLinuxマシン全体で最大何個のFDを開くことができるのかを確認
# 最大 811,372 個のファイルディスクリプタを開くことができる模様。
$ cat /proc/sys/fs/file-max
811372
永続化するには /etc/sysctl.conf に追記する必要がある
$ sudo cp -p /etc/sysctl.conf /etc/sysctl.conf_bk && ls -l $_
[sudo] password for hoge:
-rw-r--r-- 1 root root 2355 Feb 25 2022 /etc/sysctl.conf_bk
echo 'fs.file-max=2097152' >> /etc/sysctl.conf
📌 注意点
- この値を上げても、プロセスごとの
ulimit -nが低いと意味がないことがある。 - 値を極端に上げすぎると、メモリ不足やパフォーマンス劣化の原因になる。
- 必要であれば、現在使用中のFD数を確認して、実際にどれくらい余裕があるのかを把握しておくと尚良い。
$ cat /proc/sys/fs/file-nr
32000 0 811372
/proc/sys/fs/file-nr の出力項目の意味
| 項目 | 意味 |
|---|---|
| 現在使用中のFD数 | 例:32,000 個(開かれているFDの数) |
| 削除待ちのFD数 | 例:0(クローズ待ちのFD。通常は0) |
| システム上限 | 例:811,372 個(file-max の値) |
ファイルディスクリプタ上限を増やす方法まとめ
| 操作対象 | 方法 |
|---|---|
| セッション単位 | ulimit -n |
| 永続化(ユーザー単位) |
/etc/security/limits.conf に設定を追加 |
| サービス単位(systemd) |
.service ファイルに LimitNOFILE を追加 |
| システム全体 |
fs.file-max を sysctl で変更 |
ファイルディスクリプタの上限値を引き上げた際の影響について
ファイルディスクリプタを増やすことで、高負荷なシステムや多数の同時接続が発生するアプリケーションを正常に動作させやすくなるが、副作用やリスクも存在する。
- ファイルディスクリプタを増やす利点
| 項目 | 内容 |
|---|---|
| 同時接続数の向上 | Webサーバ・ソケットアプリなどで多くのクライアントを同時に扱える |
| スケーラビリティの改善 | 接続制限によるボトルネックを解消し、負荷に強くなる |
| エラーの軽減 |
Too many open files のような致命的エラーを防止できる |
| 長時間稼働時の安定性 | ログ・ファイルなど多数のFDを開き続ける処理でも枯渇しにくくなる |
- ファイルディスクリプタを増やした場合の影響・リスク
| リスク・影響 | 内容 |
|---|---|
| メモリ使用量の増加 | 各FDに対してカーネルリソース(メモリバッファなど)が割り当てられる |
| パフォーマンス劣化の可能性 | 大量のFDを扱うと select/poll 系APIの処理が遅くなる(epoll推奨) |
| FDリークによる影響拡大 | FDを閉じ忘れると、増加した分だけ長時間気づきにくくなる |
| セキュリティ・リソース制御の難化 | 子プロセスへのFD引き継ぎや管理が煩雑になる |
| 運用管理の複雑化 | リソース監視・制限設定(ulimit・systemd・sysctlなど)の整合が必要 |
- 増やす際の注意点とベストプラクティス
| 項目 | 推奨事項 |
|---|---|
| 最小限必要な分だけ増やす | 無闇に上限を上げず、実際の用途に応じて必要な範囲で設定 |
| FDの監視を導入する |
lsof, /proc/<pid>/fd, file-nr などで定期監視 |
| FDを確実に閉じる | Goなら defer file.Close()、Cなら close() を忘れずに使用 |
| epoll/selectの選択 | 大量のFDを扱うなら epoll 等のスケーラブルなI/O多重化を使用 |
| 設定を一貫して管理する |
ulimit, limits.conf, systemd, sysctl の設定がバラバラにならないように |