ps auxf
を実行すると %CPU
というカラムに CPU使用率 が表示される
見慣れた数値ではあるが、そもそも この数値はどういうロジックで計算されているんだったかな … と疑問が湧いた
man を調べる
man 1 ps では次のように説明されている
%cpu %CPU プロセスの cpu 使用率は "##.#" というフォーマットである。 現在のところ CPU 使用率は、プロセスの生存期間中に 実行に利用した時間のパーセンテージで表される。 これを全部足しても 100% になることは (よほど幸運でなければ) ない。 (別名 pcpu)。
man は便利だが、今回は実装を知りたいのでソースを追うことにする
ソースを追う
同じようなテーマを調べたブログは他にもあるかもしれないが、自分の中で手順を整理するためにも イチから書いてみよう。ソースの解説ではないので、ご了承を。
ディストリビューション
CentOS7.3 を対象として進める
パッケージ名を調べる
CentOS なので rpm -qf
でパッケージ名を調べる
$ rpm -qf $(which ps) procps-ng-3.3.10-10.el7.x86_64
ソースを手に入れる
SRPM をダウンロード、展開する。SRPM に付属するディストリビューションのパッチを当てた状態のソースが参照できる
$ sudo yum install -y yum-utils # 直接 vault.centos.org を探すこともある $ yumdownloader --source procps-ng $ rpm -ivh procps-ng-3.3.10-10.el7.src.rpm $ sudo yum-builddep -y ~/rpmbuild/SPECS/procps-ng.spec $ rpmbuild -bp ~/rpmbuild/SPECS/procps-ng.spec
rpmbuild -bp
で展開されるとこんな感じのファイルが並ぶ
$ cd ~/rpmbuild/BUILD/procps-ng-3.3.10/ $ ls -hal | head -20 合計 1.8M drwxr-xr-x. 14 vagrant vagrant 4.0K 8月 25 16:32 . drwxr-xr-x. 5 vagrant vagrant 71 8月 25 16:32 .. -rw-r--r--. 1 vagrant vagrant 7 9月 23 2014 .tarball-version -rw-r--r--. 1 vagrant vagrant 53K 9月 23 2014 ABOUT-NLS -rw-r--r--. 1 vagrant vagrant 1.4K 9月 23 2014 AUTHORS -rw-r--r--. 1 vagrant vagrant 18K 9月 23 2014 COPYING -rw-r--r--. 1 vagrant vagrant 25K 9月 23 2014 COPYING.LIB -rw-r--r--. 1 vagrant vagrant 65 9月 23 2014 ChangeLog drwxr-xr-x. 2 vagrant vagrant 60 9月 23 2014 Documentation -rw-r--r--. 1 vagrant vagrant 3.2K 9月 23 2014 Makefile.am -rw-r--r--. 1 vagrant vagrant 60K 9月 23 2014 Makefile.in -rw-r--r--. 1 vagrant vagrant 22K 9月 23 2014 NEWS -rw-r--r--. 1 vagrant vagrant 2.0K 9月 23 2014 README -rw-r--r--. 1 vagrant vagrant 47K 9月 23 2014 aclocal.m4 -rwxr-xr-x. 1 vagrant vagrant 2.2K 9月 23 2014 autogen.sh -rwxr-xr-x. 1 vagrant vagrant 7.2K 8月 5 2013 compile -rwxr-xr-x. 1 vagrant vagrant 44K 8月 5 2013 config.guess -rw-r--r--. 1 vagrant vagrant 11K 9月 23 2014 config.h.in -rwxr-xr-x. 1 vagrant vagrant 15K 9月 23 2014 config.rpath ... 略
ps の %CPU を計算するロジックを探す
ps のソースは ps/
ディレクトリ以下にまとまっているので、ここにアタリを付けて調べる。知っていればすぐに判別つくし、知らなければディレクトリをざっと見渡したり適当なワードで grep かけて推測・予想することになるだろう。
$ ls -hal ps/ 合計 356K drwxr-xr-x. 2 vagrant vagrant 255 8月 25 16:32 . drwxr-xr-x. 14 vagrant vagrant 4.0K 8月 25 16:32 .. -rw-r--r--. 1 vagrant vagrant 25K 9月 23 2014 COPYING -rw-r--r--. 1 vagrant vagrant 2.1K 9月 23 2014 HACKING -rw-r--r--. 1 vagrant vagrant 681 9月 23 2014 Makefile.am -rw-r--r--. 1 vagrant vagrant 26K 9月 23 2014 Makefile.in -rw-r--r--. 1 vagrant vagrant 12K 9月 23 2014 common.h -rw-r--r--. 1 vagrant vagrant 19K 9月 23 2014 display.c -rw-r--r--. 1 vagrant vagrant 16K 9月 23 2014 global.c -rw-r--r--. 1 vagrant vagrant 9.5K 9月 23 2014 help.c -rw-r--r--. 1 vagrant vagrant 87K 8月 25 16:32 output.c -rw-r--r--. 1 vagrant vagrant 40K 9月 23 2014 parser.c -rw-r--r--. 1 vagrant vagrant 45K 8月 25 16:32 ps.1 -rw-r--r--. 1 vagrant vagrant 757 9月 23 2014 regression -rw-r--r--. 1 vagrant vagrant 5.2K 9月 23 2014 select.c -rw-r--r--. 1 vagrant vagrant 29K 9月 23 2014 sortformat.c -rw-r--r--. 1 vagrant vagrant 4.2K 9月 23 2014 stacktrace.c
更に %CPU
で grep すると それっぽい ps/output.c
という名前のファイルが見つかるので ここを起点に掘っていく
$ grep -R %CPU ps ps/parser.c: trace("C use raw CPU time for %%CPU instead of decaying ave\n"); ps/display.c:/***** fill in %CPU; not in libproc because of include_dead_children */ ps/sortformat.c: fmt_delete("%CPU"); fmt_delete("CPU"); fmt_delete("CP"); fmt_delete("C"); ps/sortformat.c: fmt_add_after("%CPU", fn); ps/output.c:%CPU pcpu The % of cpu time used recently, with unspecified "recently". ps/output.c:/* normal %CPU in ##.# format. */ ps/output.c:// PRI PRI PRI PRI NI %CPU PID COMMAND ps/output.c: * user u up "USER PID %CPU %MEM SIZE RSS TTY STAT START TIME COMMAND ps/output.c:{"%cpu", "%CPU", pr_pcpu, sr_pcpu, 4, 0, BSD, ET|RIGHT}, /*pcpu*/ 🔥 ps/output.c:{"pcpu", "%CPU", pr_pcpu, sr_pcpu, 4, 0, U98, ET|RIGHT}, /*%cpu*/ 🔥 ps/output.c:{'C', "pcpu", "%CPU"}, ps/ps.1:.\" use raw CPU time for %CPU instead of decaying average ps/ps.1:%C pcpu %CPU ps/ps.1:%cpu %CPU T{ ps/ps.1:pcpu %CPU T{
色々見比べていくと pr_pcpu
がそれっぽい関数なのだとアタリがつく
pr_pcpu を読む
下記のようなロジックで %CPU
を計算している
/* normal %CPU in ##.# format. */ static int pr_pcpu(char *restrict const outbuf, const proc_t *restrict const pp){ unsigned long long total_time; /* jiffies used by this process */ unsigned pcpu = 0; /* scaled %cpu, 999 means 99.9% */ unsigned long long seconds; /* seconds of process life */ total_time = pp->utime + pp->stime; if(include_dead_children) total_time += (pp->cutime + pp->cstime); seconds = cook_etime(pp); if(seconds) pcpu = (total_time * 1000ULL / Hertz) / seconds; if (pcpu > 999U) return snprintf(outbuf, COLWID, "%u", pcpu/10U); return snprintf(outbuf, COLWID, "%u.%u", pcpu/10U, pcpu%10U); }
user 時間と system 時間を加算したり、子プロセスの時間を含めたり、jiffies
(tick?) から 秒
への単位変換などが行われている。 ここの肝は cook_etime
でマクロとなっている
#define cook_etime(P) (((unsigned long long)seconds_since_boot >= (P->start_time / Hertz)) ? ((unsigned long long)seconds_since_boot - (P->start_time / Hertz)) : 0)
seconds_since_boot
はブートしてからの秒数と言うのは明瞭な名前付けから察することができる。ブートしてからの秒数と、プロセスが開始してからの秒数との差でプロセスが実行されている実時間を出している
さて seconds_since_boot
の値はどこから取られているのだろう?
seconds_since_boot を探す
ag/grep や GNU global 等を使ってコードを追う。
time_t seconds_since_boot = -1;
seconds_since_boot
の初期化は下記でなされる
/************ Call this to reinitialize everything ***************/ void reset_global(void){ static proc_t p; reset_selection_list(); look_up_our_self(&p); set_screen_size(); set_personality(); all_processes = 0; bsd_c_option = 0; bsd_e_option = 0; cached_euid = geteuid(); cached_tty = p.tty; /* forest_prefix must be all zero because of POSIX */ forest_type = 0; format_flags = 0; /* -l -f l u s -j... */ format_list = NULL; /* digested formatting options */ format_modifiers = 0; /* -c -j -y -P -L... */ header_gap = -1; /* send lines_to_next_header to -infinity */ header_type = HEAD_SINGLE; include_dead_children = 0; lines_to_next_header = 1; negate_selection = 0; page_size = getpagesize(); running_only = 0; seconds_since_boot = uptime(0,0); 🔥 selection_list = NULL; simple_select = 0; sort_list = NULL; thread_flags = 0; unix_f_option = 0; user_is_number = 0; wchan_is_number = 0; /* Translation Note: . The following translatable word will be used to recognize the . user's request for help text. In other words, the translation . you provide will alter program behavior. . . It must be limited to 15 characters or less. */ the_word_help = _("help"); }
uptime
の実装を追う。uptiem の UPTIME_FILE
から何か読み取ってるのがわかる
/***********************************************************************/ int uptime(double *restrict uptime_secs, double *restrict idle_secs) { double up=0, idle=0; char *savelocale; FILE_TO_BUF(UPTIME_FILE,uptime_fd); 🔥 savelocale = strdup(setlocale(LC_NUMERIC, NULL)); setlocalen(LC_NUMERIC,"C"); if (sscanf(buf, "%lf %lf", &up, &idle) < 2) { setlocale(LC_NUMERIC,savelocale); free(savelocale); fputs("bad data in " UPTIME_FILE "\n", stderr); return 0; } setlocale(LC_NUMERIC,savelocale); free(savelocale); SET_IF_DESIRED(uptime_secs, up); SET_IF_DESIRED(idle_secs, idle); return up; /* assume never be zero seconds in practice */ }
UPTIME_FILE は /proc/uptime
のマクロ だった
proc/sysinfo.c 50:#define UPTIME_FILE "/proc/uptime"
/proc/uptime とは?
$ cat /proc/uptime 43192.39 85961.67
Google で検索をかけるといろいろな解説があるが、まずは RHEL のを引用しよう
このファイルには、前回の再起動から経過した時間に関する詳細情報が記載されています。/proc/uptime の出力は極めて少なくなります: 第 1 の値は、システムが起動してから経過した合計時間を示しています。 第 2 の値は、各コアがアイドル状態で経過した合計時間の秒数です。このため、マルチコア搭載のシステムでは、第 2 の値がシステムアップタイムの合計よりも大きくなる場合があります。
man 5 proc では下記の通りだ
/proc/uptime このファイルは システム起動時から経過した時間 (秒) と アイドル (idle) しているプロセスが消費した時間 (秒) の 2 つの数を含む。
プロセスが生起した時間
seconds_since_boot
との差分をとっている P->start_time
がどこから取られているか追っていく。 grep や gtags 等で調べていくと proc/readproc.c に stat2proc
という関数が見つかる。
実装をみると /proc/$pid/stat
から採取しているのがわかる ( ここで user 時間や system 時間も一緒にとっている )
// Reads /proc/*/stat files, being careful not to trip over processes with // names like ":-) 1 2 3 4 5 6". static void stat2proc(const char* S, proc_t *restrict P) { unsigned num; char* tmp; ENTER(0x160); /* fill in default values for older kernels */ P->processor = 0; P->rtprio = -1; P->sched = -1; P->nlwp = 0; S = strchr(S, '(') + 1; tmp = strrchr(S, ')'); num = tmp - S; if(unlikely(num >= sizeof P->cmd)) num = sizeof P->cmd - 1; memcpy(P->cmd, S, num); P->cmd[num] = '\0'; S = tmp + 2; // skip ") " num = sscanf(S, "%c " "%d %d %d %d %d " "%lu %lu %lu %lu %lu " "%Lu %Lu %Lu %Lu " /* utime stime cutime cstime */ "%ld %ld " "%d " "%ld " "%Lu " /* start_time */ "%lu " "%ld " "%lu %"KLF"u %"KLF"u %"KLF"u %"KLF"u %"KLF"u " "%*s %*s %*s %*s " /* discard, no RT signals & Linux 2.1 used hex */ "%"KLF"u %*u %*u " "%d %d " "%lu %lu", &P->state, &P->ppid, &P->pgrp, &P->session, &P->tty, &P->tpgid, &P->flags, &P->min_flt, &P->cmin_flt, &P->maj_flt, &P->cmaj_flt, &P->utime, &P->stime, &P->cutime, &P->cstime, &P->priority, &P->nice, &P->nlwp, &P->alarm, &P->start_time, 🔥 &P->vsize, &P->rss, &P->rss_rlim, &P->start_code, &P->end_code, &P->start_stack, &P->kstk_esp, &P->kstk_eip, /* P->signal, P->blocked, P->sigignore, P->sigcatch, */ /* can't use */ &P->wchan, /* &P->nswap, &P->cnswap, */ /* nswap and cnswap dead for 2.4.xx and up */ /* -- Linux 2.0.35 ends here -- */ &P->exit_signal, &P->processor, /* 2.2.1 ends with "exit_signal" */ /* -- Linux 2.2.8 to 2.5.17 end here -- */ &P->rtprio, &P->sched /* both added to 2.5.18 */ ); if(!P->nlwp){ P->nlwp = 1; } LEAVE(0x160); }
man 5 proc には下記の通りの説明がある
(22) starttime %llu プロセスの起動時刻。システムが起動した時刻が起点である。 Linux 2.6 より前のカーネルでは、 この値の単位は jiffies であった。 Linux 2.6 以降では、 値の単位はクロック tick である (sysconf(_SC_CLK_TCK) で割った値となる)。 このフィールドのフォーマットは Linux 2.6 より前では %lu であった。
ここまで呼んで全貌がだいたいわかった感じする
感想
ps
は頻繁に使うツールだけど、ソースを覗いてどのように計算されているかを見ることはなかったなぁと- 知っているつもりの数値であっても、いざ実装まで調べてみると あれやこれやと深追いしなければならないものだ。
- ソースコードを読み進める順を書いてみても、経験則・暗黙的な知識に頼って進めている節があって、第三者に明瞭に伝えるのが難しい