Closed9

ファイルディスクリプタについて一回整理したい

tamaco489tamaco489

🔧 概要

ファイルディスクリプタとは?

  • OSがファイルやソケット、パイプなどの入出力リソースを管理・識別するために使う一意の整数値。
  • プログラムがファイルや通信を扱うとき、通常まずはOSからディスクリプタを取得し、それを通してデータの読み書きを行っている。

代表的なファイルディスクリプタ番号

OS起動時、プロセスには以下の3つの標準ディスクリプタが自動で割り当てられている

ディスクリプタ 名前 詳細
0 stdin 標準入力(キーボードなど)
1 stdout 標準出力(画面など)
2 stderr 標準エラー出力
tamaco489tamaco489

ファイルディスクリプタの役割と利点

  • OSカーネルがリソースを管理しやすくなる。
  • 抽象化により、ファイル・ソケット・パイプなどを同じ方法で扱える。
  • パフォーマンスが高く、低レベルの操作が可能になる。
tamaco489tamaco489

現在の設定値の確認方法について

プロセスごとの最大オープン可能なファイルディスクリプタ数を確認
$ 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
tamaco489tamaco489

ファイルディスクリプタが増える場合の条件について

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を閉じるような処理を実装する。
tamaco489tamaco489

ファイルディスクリプタが増える条件まとめ

条件 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() を実行
tamaco489tamaco489

FDが枯渇した場合

ファイルディスクリプタが 枯渇(上限に到達) した場合、 サーバ側では致命的なエラーやサービス不能状態(DoS的な挙動) が発生することがある。
※新規接続の拒否・処理不能・クラッシュといった障害に陥る。

tamaco489tamaco489

ファイルディスクリプタの上限値を引き上げる設定

  • ファイルディスクリプタの上限は 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-maxsysctl で変更
tamaco489tamaco489

ファイルディスクリプタの上限値を引き上げた際の影響について

ファイルディスクリプタを増やすことで、高負荷なシステムや多数の同時接続が発生するアプリケーションを正常に動作させやすくなるが、副作用やリスクも存在する。

  1. ファイルディスクリプタを増やす利点
項目 内容
同時接続数の向上 Webサーバ・ソケットアプリなどで多くのクライアントを同時に扱える
スケーラビリティの改善 接続制限によるボトルネックを解消し、負荷に強くなる
エラーの軽減 Too many open files のような致命的エラーを防止できる
長時間稼働時の安定性 ログ・ファイルなど多数のFDを開き続ける処理でも枯渇しにくくなる
  1. ファイルディスクリプタを増やした場合の影響・リスク
リスク・影響 内容
メモリ使用量の増加 各FDに対してカーネルリソース(メモリバッファなど)が割り当てられる
パフォーマンス劣化の可能性 大量のFDを扱うと select/poll 系APIの処理が遅くなる(epoll推奨)
FDリークによる影響拡大 FDを閉じ忘れると、増加した分だけ長時間気づきにくくなる
セキュリティ・リソース制御の難化 子プロセスへのFD引き継ぎや管理が煩雑になる
運用管理の複雑化 リソース監視・制限設定(ulimit・systemd・sysctlなど)の整合が必要
  1. 増やす際の注意点とベストプラクティス
項目 推奨事項
最小限必要な分だけ増やす 無闇に上限を上げず、実際の用途に応じて必要な範囲で設定
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 の設定がバラバラにならないように
このスクラップは5ヶ月前にクローズされました