ps コマンドの %CPU がどのように計算されるかソースを追う

ps auxf を実行すると %CPU というカラムに CPU使用率 が表示される

f:id:hiboma:20170829173329p:plain

見慣れた数値ではあるが、そもそも この数値はどういうロジックで計算されているんだったかな … と疑問が湧いた

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

更に %CPUgrep すると それっぽい 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/grepGNU 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 のを引用しよう

https://access.redhat.com/documentation/ja-JP/Red_Hat_Enterprise_Linux/6/html/Deployment_Guide/s2-proc-uptime.html

このファイルには、前回の再起動から経過した時間に関する詳細情報が記載されています。/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 は頻繁に使うツールだけど、ソースを覗いてどのように計算されているかを見ることはなかったなぁと
  • 知っているつもりの数値であっても、いざ実装まで調べてみると あれやこれやと深追いしなければならないものだ。
  • ソースコードを読み進める順を書いてみても、経験則・暗黙的な知識に頼って進めている節があって、第三者に明瞭に伝えるのが難しい