(2025/9/20追記: ipc namespaceがらみの話が弱かったため多めに加筆)
(2025/9/26追記: 書き漏れがあったのでさらに加筆)
● お題
mq_open()がerrno==EMFILEで失敗した
● マニュアル
man 3 mq_openによれば、
https://man7.org/linux/man-pages/man3/mq_open.3.html
- プロセスあたりのfile descriptor数上限に引っかかった -> RLIMIT_NOFILE(ulimit -n) を確認する
だが、該当しているように見えない。
ENFILEの可能性も考え、/proc/sys/fs/file-max と /proc/sys/fs/file-nr を確認するも、こちらも問題なし。
● mqueueのsysctlパラメータ(1)
/proc/sys/fs/mqueue/ にこんなのがある
- msg_max --- 1つのキューに入れられるメッセージの最大個を変更するときの上限、kernel内での変数名は(mq_msg_max)または(mq_maxmsg)、以下カッコ内のものは同じ意味
- msg_default --- mq_open()の引数で特に指定しなかったときの新規作成された1つのキューに入れられるメッセージの最大個 (mq_msg_default)
- msgsize_max ----- 1メッセージの最大サイズを変更するときの上限 (mq_msgsize_max)
- msgsize_default ---- mq_open()の引数で特に指定しなかったときの新規作成されるキューへ入れる1メッセージの最大サイズ (mq_msgsize_default)
- queues_max ----- システム全体で作れるqueueの個数 (mq_queues_max)
いずれも特権(CAP_SYS_RESOURCE)があれば制限を受けない。
mq_open()のエラーとなると、queues_maxが怪しいが、/dev/mqueue を見る限りキューの個数の上限に達したようには見えない。
● mqueueのsysctlパラメータ(2)
/proc/sys/kernel/ にこんなのがある
- msgmax 1つのメッセージの最大バイト数を制限する (msg_ctlmax)
- msgmnb システム全体で、msg_qbytesの初期化のパラメータ (msg_ctlmnb)
- msgmni システム全体で、mqの名前の個数を制限する (msg_ctlmni)
- auto_msgmni 今は意味をなさない。過去はメモリサイズが msgmni に影響していたため、これに1を書くことでmsgmniの再計算が走るようになっていた
- msg_next_id 次に作成するmqのIDを指示する、がデバッグ用なので通常は使わない?
こちらも同様、引っかかっているようには見えない。
● limitパラメータ
man 7 mq_overview をきっちり読むまでこれに気づかなかった。
https://man7.org/linux/man-pages/man7/mq_overview.7.html
RLIMIT_MSGQUEUE (ulimit -q) というのがある。1ユーザ(uid)あたりで使えるmqueueのメッセージの合計サイズの条件になる。
管理情報も含むため、メッセージの中身のバイト数の合計よりもちょっと厳しく働く。
サイズを予約するときにチェックする?という方向のため、mq_open()のタイミングで失敗する。
たとえ CAP_SYS_RESOURCE を持っていても制限を回避できない
CAP_SYS_RESOURCE を持ってるとlimitを変更する(ulimit -q unlimited)ことができ、制限を回避できる。
結局原因はこれだった。limitを変更すると動作するようになった。
● mqueueについてのその他の補足
- /dev/mqueueは、存在するqueueの名前を確認できる程度で、ここを直接catしたりduしたりはできない
- /dev/mqueue で rm すると、誰も開いていないqueueを名前指定で削除できる
- mqueueのnamespaceは ipc_namespaces になる。mqueueとsysvipcとが対象にある
- /proc/sys/ や /proc/sys/kernel/ のパラメータはnamespaceごとの制限となる。「システム全体」という古い記載がドキュメントに残ったままなことがあるので注意必要
- プロセスがどのnamespaceに属しているかは file /proc/self/ns/ipc で確認できる
- queueのリストやメッセージの状況は ipcs -q もしくは /proc/sysvipc/msg で確認できる
- /dev/mqueueには見えないのにqueueが作られたという状況になることがある模様?その場合 ipcrm -q でID指定すれば削除できる
● mq_open()のソースコードの確認
linux/ipc/mqueue.c の mqueue_get_inode()あたりでmq_open()を呼んだときのパラメータチェックがなされる。
- バックトレースはこうなる mq_open() -> do_mq_open() -> prepare_open() -> mqueue_create_attr() -> mqueue_get_inode()
- CAP_SYS_RESOURCEがあれば、HARD_MSGMAX HARD_MSGSIZEMAX でチェックし、ダメなら -EINVAL を返す
- CAP_SYS_RESOURCEがなければ、mq_msg_max と mq_msgsize_max でチェックし、ダメなら -EINVAL を返す
- mq_maxmsg * mq_msgsize を計算し、unsigned intをオーバフローしたら、-EOVERFLOWを返す
- mq_maxmsg * mq_msgsizeとオーバヘッド(sizeof(struct posix_msg_tree_node)かける個数)を ulimit -q に計上し、rlimitに引っかかったら -EMFILEを返す (★ CAP_SYS_RESOURCEを見ない)
● limitについての補足
下記のrlimitはuid namespaceごとで制限チェックされる(==同じUIDのプロセスの合計値もしくは共通で効く)、ただしややこしいことに制限値はプロセスごとに設定できる。
- RLIMIT_NPROC (UCOUNT_RLIMIT_NPROC)
- RLIMIT_MSGQUEUE (UCOUNT_RLIMIT_MSGQUEUE)
- RLIMIT_SIGPENDING (UCOUNT_RLIMIT_SIGPENDING)
- RLIMIT_MEMLOCK (UCOUNT_RLIMIT_MEMLOCK)
逆に下記はそうではない(==自プロセスのみに閉じた制限チェックになる))
- RLIMIT_CPU
- RLIMIT_FSIZE
- RLIMIT_DATA
- RLIMIT_STACK
- RLIMIT_CORE
- RLIMIT_RSS
- RLIMIT_NOFILE
- RLIMIT_AS
- RLIMIT_LOCKS
- RLIMIT_NICE
- RLIMIT_RTPRIO
- RLIMIT_RTTIME
● capabilitiesについての補足
- ランチャープログラムの場合は Ambient Capabilities で付与するのがよい(子に引き継げるため)、ただしUID/GIDの動的変更をしないfile capabilitiesによる付与を行わないという前提で。
- そうでない場合はマジメに capabilities のexec時の継承ルールを理解して利用するのがよい。
- man capabilities(7) にcapabilitiesを継承するルールが記載されているがこの記号式は何度見ても理解できない。下記に抜粋しておく。
P'(ambient) = (file is privileged) ? 0 : P(ambient)
P'(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & P(bounding)) | P'(ambient)
P'(effective) = F(effective) ? P'(permitted) : P'(ambient)
P'(inheritable) = P(inheritable) [i.e., unchanged]
P'(bounding) = P(bounding) [i.e., unchanged]
● 個人の感想
- rlimit(rlim)の値は、kernelのtask_structのメンバsignalの中にある。なんか直感に対して違和感があった
- msgctlというsysctlがあることを初めて知った
- getpcapsなにもわからない。capabilitiesを読みやすいテキストに表示してくれるという触れ込みなのに、表示されるテキストが記号的で逆に分かりづらい
- ゼロが多い巨大ファイルを強制的に Filesystem sparse (hole) に変換したい場合は cp --sparse=always で別ファイルを作るのがよい