はじめに

今回はpsを読みます。何故psかというと、

  • 伝統的なUNIXのコマンド
  • /procから情報を得るプログラムが実際どうやっているのかを見てみたい

という理由からです。それでは、psを使う際に多分最も多く使われるであろうオプション

ps aux

とした場合に何が行われるかを眺めます。

なお今回対象としたバージョンはprocps 3.2.7に含まれるpsです。

処理の流れ

psディレクトリにあるHACKINGを見るとどのファイルに何が書いてあるか書かれています。それによるとmain関数はdisplay.cに書かれているようです。

main関数を眺めると以下の処理が行われています。

  1. 初期化
  2. オプションの解析
  3. 出力の下準備
  4. プロセス情報の表示

それぞれ見ていくことにしましょう。

初期化

初期化はreset_global関数(global.c)に書かれています。

reset_global関数でまず呼ばれているreset_selection_list関数ですがちょっと洒落ているので載せておきます。

if(selection_list == (selection_node *)0xdeadbeef){
  selection_list = NULL;
  return;
}

無効なアドレスを指すのにdeadbeefですか:-)。deadbeafがなんで無効なアドレスかというとLinuxではc0000000以降はカーネルモードじゃないとアクセスできないからです。

次にset_personality関数を見てみます。bsearch関数使われてるの初めて見たかも。で、見つけた名前に対応するラベルにgotoしてます。コメントにも書いてありますがgoto先のラベルを変数に入れてるのはgcc拡張です。特に環境変数は設定してないので、personalityの値は0になります。

オプションの解析

では次にオプションの解析です。オプションの解析はarg_parse関数(parser.c)で行われています。

まずparse_all_options関数です。下請けのarg_type関数でハイフンのありなしからSysVのオプションだとかBSDのオプションだとか分岐しています。ここら辺、分岐したり取り込まれたりの歴史を感じますね:-)。auxはBSDのオプションなのでparse_bsd_option関数へ。以下のように変数が設定されます。

simple_select
SS_B_a | SS_B_x
format_flag
FF_Bu

次のthread_option_check関数ではthread_flagsは0初期化されたままなので以下のように設定されます。

thread_flags
TF_show_proc

次にprocess_sf_options関数(sortformat.c)です。format_flagがFF_Buなのでbsd_u_format("OL_u")のspecでdo_one_spec関数が呼ばれます。search_format_array関数の返り値はNULLで、search_macro_array関数は"OL_u"に対応するエントリが返ってきます。次に、マクロで定義されている各spec(表示する項目)に対してdo_one_spec関数を呼び出すことにより、format_listが設定されるようです。pid系の項目はget_pid_digits関数(proc/sysinfo.c)を呼ぶことで幅を取得しているようです。/proc/sys/kernel/pid_maxにpidの最大値が書かれてるんですね。 なお、search_format_array関数やsearch_macro_array関数および各エントリはoutput.cで定義されています。

"OL_u"の場合、以下の表示項目になります。

/* Justification control for flags field. */
#define USER      CF_USER   // left if text, right if numeric
#define LEFT      CF_LEFT
#define RIGHT     CF_RIGHT
#define UNLIMITED CF_UNLIMITED
#define PIDMAX    CF_PIDMAX
#define TO        CF_PRINT_THREAD_ONLY
#define PO        CF_PRINT_PROCESS_ONLY
#define ET        CF_PRINT_EVERY_TIME

/* short names to save space */
#define ARG PROC_FILLARG     /* read cmdline (cleared if c option) */
#define USR PROC_FILLUSR     /* uid_t -> user names */

/* code        header     print()      sort()       width need vendor flags  */
{"user",       "USER",    pr_euser,    sr_euser,      8, USR,    U98, ET|USER}, /* BSD n forces this to UID */
{"pid",        "PID",     pr_pid,      sr_tgid,       5,   0,    U98, PO|PIDMAX|RIGHT},
{"pcpu",       "%CPU",    pr_pcpu,     sr_pcpu,       4,   0,    U98, ET|RIGHT}, /*%cpu*/
{"pmem",       "%MEM",    pr_pmem,     sr_nop,        4,   0,    XXX, PO|RIGHT}, /*%mem*/
{"vsz",        "VSZ",     pr_vsz,      sr_vm_size,    6,   0,    U98, PO|RIGHT}, /*vsize*/
{"rsz",        "RSZ",     pr_rss,      sr_vm_rss,     5,   0,    BSD, PO|RIGHT}, /*rssize*/
{"tname",      "TTY",     pr_tty8,     sr_tty,        8,   0,    DEC, PO|LEFT},
{"stat",       "STAT",    pr_stat,     sr_state,      4,   0,    BSD, TO|LEFT}, /*state,s*/
{"start_time", "START",   pr_stime,    sr_start_time, 5,   0,    LNx, ET|RIGHT},
{"bsdtime",    "TIME",    pr_bsdtime,  sr_nop,        6,   0,    LNX, ET|RIGHT},
{"args",       "COMMAND", pr_args,     sr_cmd,       27, ARG,    U98, PO|UNLIMITED}, /*command*/

arg_parse関数に戻って次に呼ばれているのはselect_bits_setup関数(select.c)です。personalityが0でSS_U_aもSS_U_dも設定されていないのでSS_B_uが追加されます。そしてSS_B_a | SS_B_x | SS_B_gなのでall_processesが1になります。

最後にchoose_dimensions関数、wの数に応じて表示幅を設定しているようです。

出力の下準備

init_output()

出力の下準備としてまずinit_output関数(output.c)を呼んでいます。init_output関数では初めに出力バッファの確保を行っています。ん〜と、mmap使ってるのは効率がいいからかな。

次にmeminfo関数(proc/sysinfo.c)を呼び出して/proc/meminfoからメモリ情報を取得しています。ラベルと変数のアドレスのテーブルを用意し、bsearchを使って見つかったラベルに対応する変数に値を入れています。やろうとは思うものの知らない人に説明するのがめんどくさいってタイプのコードですね:-)

最後にcheck_header_width関数を呼び出してactive_cols変数を設定しています。

lists_and_needs()

次にlists_and_needs関数が呼び出されています。今まで設定された値を考えて読んでいくと以下の値が設定されるようです。

proc_format_list
format_list
proc_format_needs
PROC_FILLCOM | PROC_FILLUSR

プロセス情報の表示

ようやくプロセス情報の表示です。forest_typeが0でsort_listもNULLなのでsimple_spew関数が呼ばれます。simple_spew関数では以下の手順でプロセス情報を表示しています。

  1. プロセステーブルのオープン
  2. 次のプロセス情報の読み込み
  3. 表示対象かの判断
  4. プロセス情報の表示

それではひとつずつ見ていくことにしましょう。

openproc()

プロセステーブルのオープンを行うopenproc関数はproc/readproc.cに書かれています。simple_spew関数からopenproc関数に渡されるフラグは

needs_for_format | needs_for_sort | needs_for_select | needs_for_threads

なので展開すると以下になります。

PROC_FILLUSR | PROC_FILLCOM | PROC_FILLSTAT | PROC_FILLSTATUS

というわけでPROC_PIDは立っていないことを考えると、

PT->reader
simple_readproc
PT->procfs
opendir("/proc")
PT->finder
simple_nextpid

が設定されます。

readproc()

次にプロセス情報の読み込みを行っているreadproc関数です。実際の処理は先ほどopenproc関数で設定したsimple_nextpid関数とsimple_readproc関数で行われます。各関数に移る前に2点ほど興味深い所があるのでご紹介。

proc_t* readproc(PROCTAB *restrict const PT, proc_t *restrict p);

restrictキーワードはC99で追加されたものでポインタが同じものをさしていないということをコンパイラに教えるためのキーワードらしいです。ここら辺参照

次に、以下の部分

if (unlikely(! PT->finder(PT,p) )) goto out;

unlikelyはproc/procps.hで以下のように定義されています。

#if __GNUC__ > 2 || __GNUC_MINOR__ >= 96
#define likely(x)       __builtin_expect(!!(x),1)
#define unlikely(x)     __builtin_expect(!!(x),0)
#else
#define likely(x)       (x)
#define unlikely(x)     (x)

__buildin_expectはgcc拡張で大体この値になるから分岐を最適化してくれという要請らしいです。ここら辺参照。使用例ではPT->finderの返り値として大体1が返ってくるので!で0、!!でやっぱり0、というからくりらしいです。

simple_nextpid()

それではPT->finderの実体のsimple_nextpid関数です。openprocで開いた/procディレクトリからエントリを拾って名前が数字の場合に次のpidがあったということで1を返しています。エントリを最後まで読むと0を返しています。

simple_readproc()

次にPT->readerの実体のsimple_readproc関数です。

まずプロセスディレクトリを引数にstat関数を実行して所有者・グループからプロセスの実行ユーザ・グループを取得しています。

次に、PROC_FILLSTATフラグが立っているのでプロセスディレクトリ内のstatファイルを読みプロセス情報を設定しています。さらにPROC_FILLSTATUSフラグも立っているので同様にstatusファイルを読んでプロセス情報に設定しています。statusファイルをproc_t構造体に設定しているstatus2procはgperfで作ったハッシュ表を使っているようです。う〜ん、これから作りましたという元ファイルが含まれてないな。

その後、PROC_FILLUSRフラグが立っているのでuidからユーザ名を取得しプロセス情報に設定されています。user_from_uid関数はproc/pwcache.cに書かれています。

最後に、PROC_FILLCOMフラグが立っているのでプロセスディレクトリ内のcmdlineファイルを読みコマンドラインをプロセス情報に取得しています。file2strvec関数で返されるポインタは以下のような構造になっているようです。

引数1(NUL)引数2(NUL)引数3(NUL)[align]<引数1へのポインタ><引数2へのポインタ><引数3へのポインタ>
<--     ファイルの内容     -->       ↑
                                     返されるポインタ

want_this_proc()

表示対象かの判断を行うwant_this_proc関数はselect.cに書かれています。all_processesが1なので単純に全てOKとなります。

show_one_proc()

最後にプロセス情報の表示です。show_one_proc関数はoutput.cに書かれています。

初めて呼び出された場合、lines_to_next_headerは1なのでproc_tポインタがNULLで再帰呼び出しされています。NULLを渡されるとヘッダを表示するようです。

show_one_procではformat_nodeをたどり、スペースとかを考慮して出力を行っています。実際の出力はformat_nodeのprメンバーに設定されている関数が使われます。さらっと見た感じおもしろいことをやってそうな以下の関数を詳しく見てみましょう。

  • pr_pcpu()
  • pr_tty8()

pr_pcpu()

pr_pcpu関数ではプロセスが使用したCPU時間を計算して表示しています。

はまあ普通の処理なのですが設定された記憶のないHertz変数が使われています。grepしてみるとHertz変数はsysinfo.cのinit_libproc関数で設定されています。いつ呼ばれたんだ?と思ったらinit_libproc関数は__attribute__((constructor))マークが付いているので起動時に呼ばれているようです。init_libproc関数内で使用しているlinux_version_codeも同様にversion.cに書かれている__attribute__((consutuctor))付きのinit_Linux_version関数で設定されるようです。

Linuxのバージョンは2.4.0以上として、init_libproc関数は環境変数の後にあるらしいELF notesと呼ばれる領域からHetrzを取得しています。ググるとsysconf関数からも取れるようですが、まあ効率重視なのでしょう。

pr_tty8()

次にttyの表示を行うpr_tty8関数です。実際の処理はproc/devname.cのdev_to_tty関数に委譲されています。

dev_to_tty関数はdriver_name関数を呼ぶことでtty番号からtty名を解決しています。driver_name関数は/proc/tty/driversを読んでtty番号のmajor, minor番号(0-1048575のような範囲指定もあり)に対するtty名のテーブルを用意した後、渡されたtty番号に対応するデバイスファイルがあるかをチェックしています。driver_name関数から戻ったdev_to_tty関数は引数で指定された出力バッファにtty名を設定しています。

おわりに

今回はpsの挙動を見てきました。感想としては、

  • いろんなことを処理するのにテーブルベースの処理がよく使われている
  • gotoラベルの変数代入や__attribute__などいろいろなgcc拡張が使われている
  • 効率アップのために知らないと理解に苦しむという部分もちらほらある

といったところです。それではみなさんもよいコードリーディングを。


トップ   編集 凍結解除 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2007-10-13 (土) 12:52:50 (4437d)