あるプロセスが利用しているメモリサイズを procfs 経由で調べる

お題は「あるプロセスがどの程度の物理メモリを利用したかを知りたい」です。

手っとりばやく知りたいときは top や ps などで調べると良いでしょうか。例えば手元の coLinux で top して M キーでソートすると emacs のプロセスが最もメモリを使っているようです。

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
 1923 naoya     18   0 23120  19m 3096 S  0.0  2.0   0:55.40 emacs

メモリサイズは VIRT と RES がありますが、VIRT は Virtual の略で仮想メモリ領域のサイズ、RES が Resident の略で、実際に使用している物理メモリ領域のサイズ。19MB ほど使っているようです。この emacs のプロセスが利用するメモリ領域はざっくり 20MB 程度と考えることはできます。

しかし、ちょっと雑なのでもう少し詳しく見たい。こういう時は /proc//status を cat します。

% cat /proc/1923/status
Name:   emacs
State:  S (sleeping)
SleepAVG:       77%
Tgid:   1923
Pid:    1923
PPid:   1545
TracerPid:      0
Uid:    1000    1000    1000    1000
Gid:    1000    1000    1000    1000
FDSize: 32
Groups: 108 1000
VmPeak:    23124 kB
VmSize:    23120 kB
VmLck:         0 kB
VmHWM:     19968 kB
VmRSS:     19964 kB
VmData:    13800 kB
VmStk:       352 kB
VmExe:      1024 kB
VmLib:      4064 kB
VmPTE:        32 kB
Threads:        1

...以下略

Vm* の行がメモリに関するステータスです。VmSize が先ほどの top での VIRT 列、VmRSS が RES 列に相当します。他の行は何でしょうか。結論からいくと

行名 説明
VmPeak このプロセスがある時点で使っていた最大仮想メモリサイズ
VmLck ロックされている(スワップアウトされない)メモリのサイズ (mlock(2) などでロックできます)
VmHWM このプロセスがある時点で使っていた最大物理メモリサイズ
VmData このプロセスの動的仮想メモリ領域のサイズ
VmStk スタックサイズ
VmExe 実行ファイル (テキスト領域) のサイズ
VmLib ロードされたライブラリのサイズ
VmPTE ページテーブルのサイズ

になります。*1 VmHWM あたりが今回の場合特に気になる指標でしょう。

この emacs プロセスは VmHWM が 19968 kB なので、約 19 MB。VmRSS も 19964 kB で 19 MBほどでほとんど差はありません。ピーク時でも 19MB 程度なのが分かりました。

一方、これとは別のとあるデーモンプロセスの status を見てみます。このプロセスは大量のデータを用いた計算を行うデーモンプロセスで、動的に巨大なメモリの割り当てと開放を繰り返します。Vm* 行の出力は以下のようになりました。

VmPeak:  1323048 kB
VmSize:   884116 kB
VmLck:         0 kB
VmHWM:    861796 kB
VmRSS:    498316 kB
VmData:   858276 kB
VmStk:        88 kB
VmExe:       124 kB
VmLib:      5032 kB
VmPTE:      1276 kB

と VmRSS は 500MB 弱なのに対して VmHWM が 860 MB ほどあります。この場合 top や ps の値で一時的に 500MB 程度の値を見て「500 MB くらい」と判断するのは早計ですね。ピーク時に 860 MB まで行っているので、その分を加味しておく必要があります。

また、VmRSS にはプロセスが他プロセスと共有しているメモリ領域のサイズも含まれるので、複数プロセスに渡って利用している実メモリサイズを調べる場合はその点も考慮する必要があるでしょう。詳しくは Linux のプロセスが Copy on Write で共有しているメモリのサイズを調べる - naoyaのはてなダイアリー に記しています。

/proc//status の出力の詳細を知る

/proc//status はプロセスのメモリ利用状況を詳細に出力するので、重宝します。各行の意味するところを正確に把握しておきたいところです。Linux カーネルソースの Documentation/filesystems/proc.txt に一応ドキュメントがありますが、残念ながら詳細な言及はありません。

そこで、ソースを見ます。少し古いですが、linux-2.6.23 のソースを見ていきます。/proc//status を read すると、fs/proc/array.c にある proc_pid_status() 関数が呼ばれます。

int proc_pid_status(struct task_struct *task, char * buffer)
{
    char * orig = buffer;
    struct mm_struct *mm = get_task_mm(task);

    buffer = task_name(task, buffer);
    buffer = task_state(task, buffer);

    if (mm) {
        buffer = task_mem(mm, buffer);
        mmput(mm);
    }
    buffer = task_sig(task, buffer);
    buffer = task_cap(task, buffer);
    buffer = cpuset_task_status_allowed(task, buffer);
#if defined(CONFIG_S390)
    buffer = task_show_regs(task, buffer);
#endif
    return buffer - orig;
}

引数の task は /proc//status で指定した PID のプロセスのプロセスディスクリプタ (task_struct 構造体)で、task->mm でメモリディスクリプタ (mm_struct 構造体) が得られます。status の出力で表示されているメモリ関連の行の値はメモリディスクリプタに収められています。

proc_pid_status() では get_task_mm(task) でメモリディスクリプタを取得し、task_mm(mm, buffer) でメモリディスクリプタ内から必要な値を取得し、出力を作っています。task_mm() は以下のような実装になっていました。

char *task_mem(struct mm_struct *mm, char *buffer)
{
    unsigned long data, text, lib;
    unsigned long hiwater_vm, total_vm, hiwater_rss, total_rss;

    /*
     * Note: to minimize their overhead, mm maintains hiwater_vm and
     * hiwater_rss only when about to *lower* total_vm or rss.  Any
     * collector of these hiwater stats must therefore get total_vm
     * and rss too, which will usually be the higher.  Barriers? not
     * worth the effort, such snapshots can always be inconsistent.
     */
    hiwater_vm = total_vm = mm->total_vm;
    if (hiwater_vm < mm->hiwater_vm)
        hiwater_vm = mm->hiwater_vm;
    hiwater_rss = total_rss = get_mm_rss(mm);
    if (hiwater_rss < mm->hiwater_rss)
        hiwater_rss = mm->hiwater_rss;

    data = mm->total_vm - mm->shared_vm - mm->stack_vm;
    text = (PAGE_ALIGN(mm->end_code) - (mm->start_code & PAGE_MASK)) >> 10;
    lib = (mm->exec_vm << (PAGE_SHIFT-10)) - text;
    buffer += sprintf(buffer,
        "VmPeak:\t%8lu kB\n"
        "VmSize:\t%8lu kB\n"
        "VmLck:\t%8lu kB\n"
        "VmHWM:\t%8lu kB\n"
        "VmRSS:\t%8lu kB\n"
        "VmData:\t%8lu kB\n"
        "VmStk:\t%8lu kB\n"
        "VmExe:\t%8lu kB\n"
        "VmLib:\t%8lu kB\n"
        "VmPTE:\t%8lu kB\n",
        hiwater_vm << (PAGE_SHIFT-10),
        (total_vm - mm->reserved_vm) << (PAGE_SHIFT-10),
        mm->locked_vm << (PAGE_SHIFT-10),
        hiwater_rss << (PAGE_SHIFT-10),
        total_rss << (PAGE_SHIFT-10),
        data << (PAGE_SHIFT-10),
        mm->stack_vm << (PAGE_SHIFT-10), text, lib,
        (PTRS_PER_PTE*sizeof(pte_t)*mm->nr_ptes) >> 10);
    return buffer;
}

この実装を見ることで、status の各行の意味は明確になるでしょう。

VmPeak は mm->hiwater_vmmm->total_vm で高い値を示したもの。詳解 Linuxカーネル 第3版 によると、mm->hiwater_vm は「このプロセスのメモリリージョンがある時点で使っていたページフレームの最大数」です。mm->total_vm は「プロセスアドレス空間全体の大きさをページ数として表す」メンバです。

VmHWM は mm->hiwater_rssmm->rss のどちらかで高い値を示した物。mm->hiwater_rss は「このプロセスがある時点で使っていたページフレームの最大数」で、mm->rss は「プロセスに割り当てられているページフレームの数」です。ページフレームのサイズは固定なので、ページフレーム数 → バイト数に変換するとメモリサイズになります。

なお、get_mm_rss(mm) は mm から mm->file_rssmm->anon_rss メンバの値を取得するマクロ、get_mm_counter() はメモリディスクリプタから任意のメンバの値を取得するマクロで以下のような実装になっています。CPU の数によっては操作をアトミックにするという抽象化が行われています。

#if NR_CPUS >= CONFIG_SPLIT_PTLOCK_CPUS
#define get_mm_counter(mm, member) ((unsigned long)atomic_long_read(&(mm)->_##member))
#else
#define get_mm_counter(mm, member) ((mm)->_##member)
#endif

#define get_mm_rss(mm)                  \
    (get_mm_counter(mm, file_rss) + get_mm_counter(mm, anon_rss))

他の行も同様にメモリディスクリプタの各メンバの値から計算されています。説明は省略します。

まとめ

  • Linux においてあるプロセスが利用しているメモリ領域については /proc//status が参考になります
  • /proc//status の各行の意味は procfs のソースコードから判断できます
  • VmHWM や VmRSS が、物理メモリ量を考慮する場合には特に重要な指標になります
  • Resident なメモリ領域の更に詳細なステータスについては id:naoya:20080212:1202830671 が参考になると思います

参考文献

詳解 Linuxカーネル 第3版

詳解 Linuxカーネル 第3版

*1:メモリサイズと記載していますが、実際にはページ数が基礎になっていて、表示の際にページ数がメモリ領域のサイズに変換されています。