Linuxカーネルから見た「コマンド名」
はじめに
Linuxを使っているみなさんは普段からLinux上で様々なコマンドを実行していると思います。それらを識別するときに「コマンド名」という単語を使っていると思いますが、文脈によってこの単語が意味するものは異なります。本記事ではLinuxカーネルがいうところのコマンド名がどういうものかについて書きます。
一番最初に短い結論、その次に具体的な説明、そして最後にこれについて調べようとしたきっかけ、およびその後の調査プロセスについて書きます。
結論
- Linuxカーネルから見たコマンド名は実行ファイル名のbasename(ファイル名からディレクトリ部分を除いたもの)の先頭15バイト
- カーネルのメモリ内のプロセス(正確にはカーネルレベルのスレッド)ごとに存在する
task_struct
という名前の構造体の中のcomm
という16バイトのフィールドにNULL終端文字列として格納されている - カーネルの中から低コストに、かつ、pidより高い可読性でプロセスを識別できるようになっている
- このコマンド名はカーネルログの中、psやpgrepなど、procpsパッケージなどから使われている。長いコマンド名だと途中で切れているのは上記15バイト制限のため
調査プロセス
調査に使ったソフトウェアのバージョン
- linux kernel: v5.15
- procps: 3.3.17
きっかけ
「結論」の節で述べたことを調べようとしたきっかけは、自作プログラムの中で使用していたpgrepコマンドがうまく動作しなかったことです。pgrepコマンドは引数で指定した文字列を正規表現として受け取り、これにマッチする動作中のプロセスのpid一覧を取得するというものです。たとえば以下は"foo.sh"という無限にsleepするスクリプトを実行したあとにpgrepでこのプログラムのpidを表示しています。
$ cat foo.sh
#!/bin/bash
sleep infinity
$ ./foo.sh &
[2] 1086408
$ pgrep "foo\.sh"
1086408
ところが"foo.sh"とまったく同じことをする"foo-bar-baz-hoge-huga.sh"というスクリプトに対して同じことをするとgrepは何も表示してくれませんでした。
$ cat foo-bar-baz-hoge-huga.sh
#!/bin/bash
sleep infinity
$ ./foo-bar-baz-hoge-huga.sh &
[2] 1086868
$ pgrep "foo-bar-baz-hoge-huga\.sh"
$
おかしいなと思ってman pgrep
を見ると、以下のような記述がありました。
NOTES
The process name used for matching is limited to the 15 characters present in the output of /proc/pid/stat.
実際にfoo-bar-baz-hoge-huga.sh
について/proc/pid/stat
ファイルを見ると、以下のような文字列が得られました。
$ cat /proc/601235/stat
601235 (foo-bar-baz-hog) S 593786 601235 593786 34817 601419 4194304 224 0 0 0 0 0 0 0 20 0 1 0 5735606 8617984 900 18446744073709551615 94266299658240 94266300571405 140732967030208 0 0 0 65536 4 65538 1 0 0 17 1 0 0 0 0 0 94266300816048 94266300864080 94266304847872 140732967036675 140732967036712 140732967036712 140732967038941 0
コマンド名を表示している第二フィールドのカッコの中に表示されている文字列はスクリプトの名前全体でなく、たしかに最初の15文字だけに一致していました。
仕様自体は理解して私のpgrepの使い方が誤っていることがわかったのですが、この15文字という制限が一体どこから来るものなのかを確かめることにしました。
procfsのマニュアルを見る
/proc/
ディレクトリ以下のファイルはprocfsというファイルシステムが提供します。procfsはext4やXFSのようにディスク上のデータを管理するファイルシステムではなく、ファイルを介してユーザがカーネルの情報を得たり、カーネルの状態を変更したりするためにあります。ここではprocfsの詳細には踏み込みませんが、気になるかたはこの動画を参考にしてください。
まずは/proc/pid/stat
ファイルの仕様を確認します。procfs以下のファイルの仕様はman procfs
に書いています。以下、該当部分を抜粋します。
/proc/[pid]/stat
Status information about the process. This is used by ps(1). It is defined in the kernel source file fs/proc/array.c.
...
(2) comm %s
The filename of the executable, in parentheses. Strings longer than TASK_COMM_LEN (16) characters (including the terminating null byte) are silently truncated. This is visible
whether or not the executable is swapped out.
/proc/pid/stat
ファイルの第二フィールドはカッコの中に実行ファイルの名前が書いていること、および、NULL終端文字列でNULL文字を含む16バイトを超える部分は無視されることがわかりました。16バイトからNULL文字の1バイトを引くと15バイトということで、pgrepのマニュアルに書いてある情報と一致します。
/pric/pid/stat
ファイルのハンドラを特定する
続いてカーネルソースを見て、実際にこの文字列を出力している箇所、および、どういう場所にデータが格納されているのかを確認しました。procfsのマニュアルには/proc/pid/stat
ファイルはカーネルソース内のfs/proc/array.c
ファイルで定義されているということなので、このファイルをまずは見てみました。
該当するコードはdo_task_stat()
関数の中の以下の部分のようでした。
seq_puts()
関数を呼び出すと、引数で指定した文字列をファイルに出力します。上記のコードでは562行目と564行目で"("と")"を出力しており、かつ、恐らく563行目のproc_task_name()
関数によってコマンド名をファイルに出力しているであろうことがわかります。
proc_task_name()
の中身を見る前に、まずは/proc/pid/stat
ファイルを読み出すと本当にdo_task_stat()
関数が呼ばれるのかを追うことにしました。do_task_stat()
関数の呼び出し元をたどるとproc_tid_stat()
とproc_tgid_stat()`という2つの関数から呼び出し順序になっていました。
カーネルの中でいうtidとはスレッドのIDで、tgidとはプロセス名を指すので、おそらくproc_tgid_stat()
関数が呼び出し元だと推測できます。procfs以下には/proc/pid/task
ディレクトリ以下にスレッドの状態を表示する関数があるので、proc_tid_stat()
関数はおそらく/proc/pid/task/tid
ファイルのハンドラなのでしょう。
さらにこれらの関数の呼び出し元をたどると、ユーザからprocfs内の各ファイルを読み書きしたときに呼ばれるハンドラを登録するproc/pid/base.c
ファイルの中に、/proc/tgid/stat
ファイル、別のいいかたをすると/proc/pid/stat
ファイルへのアクセス時にproc_tgid_stat()
関数を呼び出すよう登録していることが確認できました。
まとめると、以下のようになることがわかりました。
- ユーザが
/proc/pid/stat
ファイルを読み出す -
proc_tgid_stat()
関数が呼ばれる -
do_task_stat()
関数が呼ばれる -
proc_task_name()
関数が呼ばれてコマンド名をファイルの出力とする
コマンド名の情報源を特定する
proc_task_name()
関数の実装を見ると、以下のようになっていました。
詳細は省略しますが、pidで示されるプロセスが普通のプログラムの場合は103行目のif文の評価結果はfalseになります。この評価結果がtrueになるのは、カーネルの中で作る特別なプロセスの場合だけです。
さらにproc_task_name()
関数のescape
引数はproc_tgid_stat()
関数経由の呼び出しの場合はtrueになるため、108行目のif文の評価結果はtrueとなります。このため、proc_task_name()
関数内では__get_task_comm()
関数によって得たデータ(おそらくNULL終端文字列)を109行目で/proc/pid/stat
ファイルの出力として使っていることがわかりました。109行目のseq_escape_str()
関数では特殊文字やスペースをエスケープしているのですが、ここではそれは重要ではないので、詳しい説明はしません。
では今度は__get_task_comm()
関数の中身を見ます。
tsk->comm
の値が、もっというとtask_struct
という名前の構造体のcomm
フィールドの値がコマンド名の情報源だとわかります。task_struct
構造体はスレッドごとに存在します。ではtask_struct
構造体の定義を見てみましょう。
comm
フィールドは長さ16のcharの配列だとわかります。procfsのマニュアルにもTASK_COMM_LEN
の長さは16バイトと書いていましたね。
どこでtask_struct->commの値を設定しているのかを確かめる。
task_struct->comm
の設定をしているのは以下の__set_task_struct()
関数です。
__set_task_struct()
関数の呼び出し元はbegin_new_exec()
関数です。
この関数は新規プロセスを生成するexecve()
システムコールを呼び出したときに呼ばれる関数で、bprm->filename
にはプロセスに対応する実行ファイルの名前がNULL終端文字列として入っています。ここで実行ファイル名をkbasename()
という関数で加工した上でtask->comm
に保存していることがわかります。kbasename()
関数は標準Cライブラリに入っているbasename()関数と同様、ファイル名のディレクトリ部分を削除した文字列を返します。つまり実行ファイル名が"./foo.sh"ならばtask_struct->comm
には"foo.sh"が、"./foo-bar-baz-hoge-huga.sh"ならば"foo-bar-baz-hog"が入ることがわかります。これでついに/proc/pid/stat
ファイル、いいかたを変えるとLinuxカーネルがいうところの「コマンド名」の定義がわかりました。
procpsのソースを見る
最後にprocpsのソースを読んだところ、pgrepで出力する文字列はmanに書いている通り/proc/pid/stat
ファイルの第二フィールドから"("と")"を外した最長15文字を出力しているとわかりました。
とくに面白いことはしていないのでprocpsのソースの説明は省きます。
コラム: コマンド名の定義を考える
Linuxカーネルがいうところのコマンド名が実行ファイル名のbasenameの先頭15バイトということはわかりましたが、なぜbasenameで加工しているのでしょうか、なぜ最長15バイトに切り詰めているのでしょうか。その理由は恐らく次のようなものだと思います。
カーネルログなどを介してプロセスを特定するために、pidとは別にお手軽に文字列として見られる情報があると便利です。それには実行ファイル名が利用できます。ただしtask_struct
構造体の中に実行ファイル名をそのまま格納すると、カーネルのメモリを大量に使いますし、悪意のあるユーザが異常に長いファイル名のプログラムを実行するセキュリティ脆弱性になる恐れもあります。このためファイル名のすべてを格納するのは無理があります。
プロセスのメモリ内には実行ファイル名が入っているのでその値を見ればいいか…というとそうでもありません。プロセスのメモリにカーネルからアクセスする場合、該当するメモリがスワップアウトされている場合はスワップインさせてから読むという面倒なことをしなければいけません。面倒な上に、このようなことは、たとえばシステムのメモリが足りないことをカーネルログに出力するような用途では使えません。メモリが足りないときにメモリ使用量を増やすわけにはいかないのです。
"./foo.sh"のような実行時に指定したファイル名やフルパスではなく"foo.sh"のようなbasenameを使っている理由は、basenameでも十分視認性は高いという判断ではないかと推測できます。
おわりに
本記事ではLinuxカーネルがいうところのコマンド名の仕様がなぜこのようになっているのかについて書きました。また、コンピュータを使っているうちに生まれたちょっとした疑問の答えをソースコードを読んで調べていくという流れも書いて、ソースコードリーディングの追体験をしてもらいました。どちらも即座に役立つ知識ではありませんが、豆知識として役立てていただければ幸いです。
このソースコードリーディングの様子をYouTubeで公開していますので、よろしければそちらもごらんください。
余談ですが、こういうときにさくっとソースを読めるOSSというのはいいものだなあと再確認しました。おわり。
Discussion