📖

Linuxカーネルから見た「コマンド名」

2022/09/03に公開

はじめに

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()関数の中の以下の部分のようでした。

https://github.com/torvalds/linux/blob/v5.15/fs/proc/array.c#L562-L564

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つの関数から呼び出し順序になっていました。

https://github.com/torvalds/linux/blob/v5.15/fs/proc/array.c#L646-L656

カーネルの中でいう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()関数を呼び出すよう登録していることが確認できました。

https://github.com/torvalds/linux/blob/v5.15/fs/proc/base.c#L3168-L3202

まとめると、以下のようになることがわかりました。

  1. ユーザが/proc/pid/statファイルを読み出す
  2. proc_tgid_stat()関数が呼ばれる
  3. do_task_stat()関数が呼ばれる
  4. proc_task_name()関数が呼ばれてコマンド名をファイルの出力とする

コマンド名の情報源を特定する

proc_task_name()関数の実装を見ると、以下のようになっていました。

https://github.com/torvalds/linux/blob/v5.15/fs/proc/array.c#L99-L112

詳細は省略しますが、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()関数の中身を見ます。

https://github.com/torvalds/linux/blob/v5.15/fs/exec.c#L1209-L1215

tsk->commの値が、もっというとtask_structという名前の構造体のcommフィールドの値がコマンド名の情報源だとわかります。task_struct構造体はスレッドごとに存在します。ではtask_struct構造体の定義を見てみましょう。

https://github.com/torvalds/linux/blob/master/include/linux/sched.h#L727-L1063

https://github.com/torvalds/linux/blob/master/include/linux/sched.h#L276-L282

commフィールドは長さ16のcharの配列だとわかります。procfsのマニュアルにもTASK_COMM_LENの長さは16バイトと書いていましたね。

どこでtask_struct->commの値を設定しているのかを確かめる。

task_struct->commの設定をしているのは以下の__set_task_struct()関数です。

https://github.com/torvalds/linux/blob/v5.15/fs/exec.c#L1223-L1230

__set_task_struct()関数の呼び出し元はbegin_new_exec()関数です。

https://github.com/torvalds/linux/blob/v5.15/fs/exec.c#L1238-L1357

この関数は新規プロセスを生成する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で公開していますので、よろしければそちらもごらんください。

https://youtu.be/n34LCB7Iwig

余談ですが、こういうときにさくっとソースを読めるOSSというのはいいものだなあと再確認しました。おわり。

Discussion