pagemon でプロセスのメモリマッピングを覗き見る

pagemon というツールを知ったので、使い方を調べつつ何かしら学習ネタにできないかと考えた

smackerelofopinion.blogspot.jp

基本的な使い方

-p で観察したいプロセスの pid を指定して起動する。特権が必要なので sudo なり root なりで実行するナリ。

sudo pagemon -p <pid>

f:id:hiboma:20170907220324j:plain

するとプロセスの仮想アドレスと対応するページフレームの情報を取れる!

続きを読む

ps コマンドの %CPU がどのように計算されるかソースを追う (3) - top との算出方法の違い

hiboma.hatenadiary.jp

hiboma.hatenadiary.jp

上のエントリを書いみたものの、腑に落ちない疑問が残ったので、検証をしてみて整頓した

疑問

前回確かめたのはps%CPU が指すのは、プロセス(タスク) が開始してから消費した user 時間 + system 時間の割合だった。

  • ps が出すのは単位時間(例: 1秒とか ) あたりの CPU使用率 ではないよな? 一方で top は指定した間隔秒あたりの CPU使用率 を出すはず
  • ブロックする時間( sleep、ディスク I/O 、カーネル内のセマフォ等のロック待ち、スケジューラのレイテンシ, … ) が増えると、%CPU の数値は下がるよな?

… と疑問が残ってしまった。手を動かして確かめるのがよいだろう

検証

CPUバウンドなワンライナーCPU使用率pstop とで計測して、数値の違いを見ていく

続きを読む

ps コマンドの %CPU がどのように計算されるかソースを追う (2) - gdb で検証

hiboma.hatenadiary.jp

先のエントリでは ps コマンドのソースを読み、pr_cpu という関数で %CPU が計算されていると書いた。が、実行時に本当にその関数が呼び出されているのかどうかを確かめていなかった

ソースを誤読している可能性を排除するために、検証をしよう

gdb で検証

どういうアプローチで確かめるかを考えたが、gdb で調べてみたら良い感じだったので その手順をのせる

前準備

procps-ng の debuginfo パッケージを入れる

[vagrant@localhost ~]$ sudo debuginfo-install procps-ng
=====================================================================================================================================================================================
 Package                                                   アーキテクチャー               バージョン                                    リポジトリー                            容量
=====================================================================================================================================================================================
インストール中:
 glibc-debuginfo                                           x86_64                         2.17-157.el7_3.5                              base-debuginfo                         9.3 M
 ncurses-debuginfo                                         x86_64                         5.9-13.20130511.el7                           base-debuginfo                         1.1 M
 procps-ng-debuginfo                                       x86_64                         3.3.10-10.el7                                 base-debuginfo                         546 k
 systemd-debuginfo                                         x86_64                         219-30.el7_3.9                                base-debuginfo                          18 M
 yum-plugin-auto-update-debug-info                         noarch                         1.1.31-40.el7                                 base                                    26 k
依存性関連でのインストールをします:
 glibc-debuginfo-common                                    x86_64                         2.17-157.el7_3.5                              base-debuginfo                         9.4 M

トランザクションの要約
=====================================================================================================================================================================================
インストール  5 パッケージ (+1 個の依存関係のパッケージ)

gdb で ps を実行する

$ gdb $( which ps ) 

pr_pcpuブレークポイントをしかけよう。 (注: gdb に慣れていない人向けに省略記法をなるべく避けて記述する )

(gdb) break pr_pcpu
Breakpoint 1 at 0x405570: file output.c, line 499.

「ps で pid = 1 の %CPU を表示する」という引数で実行する

(gdb) run -o pcpu 1
Starting program: /usr/bin/ps -o pcpu 1
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
%CPU

Breakpoint 1, pr_pcpu (outbuf=0x7ffff7f9c090 "%CPU\n", pp=0x617840 <buf.6305>) at output.c:499
499    static int pr_pcpu(char *restrict const outbuf, const proc_t *restrict const pp){

ブレークポイントで停止した。pr_pcpu が呼び出されているのは間違いないようだ。

(gdb) backtrace 
#0  pr_pcpu (outbuf=0x7ffff7f9c090 "%CPU\n", pp=0x617840 <buf.6305>) at output.c:499
#1  0x0000000000407964 in show_one_proc (p=0x617840 <buf.6305>, fmt=0x63b370) at output.c:2026
#2  0x0000000000402eb8 in main (argc=4, argv=<optimized out>) at display.c:378

ソースコードをリストする。ちょっと行数がズレてる

(gdb) list
494      if(seconds) pcpu = (total_time * 100ULL / Hertz) / seconds;
495      if (pcpu > 99U) pcpu = 99U;
496      return snprintf(outbuf, COLWID, "%2u", pcpu);
497    }
498    /* normal %CPU in ##.# format. */
499    static int pr_pcpu(char *restrict const outbuf, const proc_t *restrict const pp){
500      unsigned long long total_time;   /* jiffies used by this process */
501      unsigned pcpu = 0;               /* scaled %cpu, 999 means 99.9% */
502      unsigned long long seconds;      /* seconds of process life */
503      total_time = pp->utime + pp->stime;

const proc_t *restrict const pp にプロセスの情報が入っているので、覗いてみよう。 pid = 1 ( systemd ) の /proc 以下のデータがまとまっている構造体だとわかる。

(gdb) print pp
$3 = (const proc_t * const) 0x617840 <buf.6305>

(gdb) p *pp
$2 = {tid = 1, ppid = 0, maj_delta = 0, min_delta = 0, pcpu = 0, state = 83 'S', pad_1 = 0 '\000', pad_2 = 0 '\000', pad_3 = 0 '\000', utime = 274, stime = 377, cutime = 2176, 
  cstime = 9878, start_time = 1, signal = '0' <repeats 16 times>, "\000", blocked = "7be3c0fe28014a03\000", sigignore = '0' <repeats 12 times>, "1000\000", 
  sigcatch = "00000001800004ec\000", _sigpnd = '0' <repeats 16 times>, "\000", start_code = 1, end_code = 1, start_stack = 0, kstk_esp = 0, kstk_eip = 0, 
  wchan = 18446744073709551615, priority = 20, nice = 0, rss = 969, alarm = 0, size = 0, resident = 0, share = 0, trs = 0, lrs = 0, drs = 0, dt = 0, vm_size = 45940, vm_lock = 0, 
  vm_rss = 3876, vm_data = 2344, vm_stack = 132, vm_swap = 988, vm_exe = 1296, vm_lib = 3640, rtprio = 0, sched = 0, vsize = 47042560, rss_rlim = 18446744073709551615, 
  flags = 4202752, min_flt = 70402, maj_flt = 308, cmin_flt = 3267216, cmaj_flt = 2059, environ = 0x0, cmdline = 0x0, cgroup = 0x0, supgid = 0x63b510 "-", supgrp = 0x0, 
  euser = '\000' <repeats 32 times>, ruser = '\000' <repeats 32 times>, suser = '\000' <repeats 32 times>, fuser = '\000' <repeats 32 times>, rgroup = '\000' <repeats 32 times>, 
  egroup = '\000' <repeats 32 times>, sgroup = '\000' <repeats 32 times>, fgroup = '\000' <repeats 32 times>, cmd = "systemd\000\000\000\000\000\000\000\000", ring = 0x0, 
  next = 0x0, pgrp = 1, session = 1, nlwp = 1, tgid = 1, tty = 0, euid = 0, egid = 0, ruid = 0, rgid = 0, suid = 0, sgid = 0, fuid = 0, fgid = 0, tpgid = -1, exit_signal = 17, 
  processor = 0, ns = {0, 0, 0, 0, 0, 0}, sd_mach = 0x0, sd_ouid = 0x0, sd_seat = 0x0, sd_sess = 0x0, sd_slice = 0x0, sd_unit = 0x0, sd_uunit = 0x0}

ブレークポイントから再開すると CPU使用率0.0 が表示して終了する

(gdb) continue
Continuing.
 0.0
[Inferior 1 (process 16371) exited normally]

こんなんでよいだろか

感想

  • ソースを読んだ上で、このような動作の検証を重ねておけば 勘違いや・誤りが無いことをたしかに出来るだろうか。実際に動かしてみたもので確かめないと不安になるというのは #ITエンジニアあるある

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

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

f:id:hiboma:20170829173329p:plain

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

man を調べる

man 1 ps では次のように説明されている

続きを読む

mmap(2) したファイルのページキャッシュの reclaim

hiboma.hatenadiary.jp

sysctl vm.drop_caches=1mmap(2) しているファイルのページキャッシュを破棄することはできなかったが、他にはどういう方法があるんだっけ?

( 何か困っているケースがあるわけでなくて挙動を確かめたいという興味・関心で調べている )

memory pressure をかける

1. 大きな無名ページをもつプロセスを作る

ホストの RAM と同程度の無名ページを使うプロセスを作れば、ページキャッシュを追い出せる ( reclaim されると書いた方が正確だろうか )

続きを読む

/proc/$pid/status の RssFile の挙動を確かめる (2) - mmap(2) したファイルのページキャッシュは vm.drop_caches で破棄されないのをソースで追う

hiboma.hatenadiary.jp

先のエントリの中で、mmap(2) しているファイルのページ(キャッシュ?) は sysctl vm.drop_caches で破棄されないことを確かめたのだが、どのように実装されているのかソースを確かめる

vm.drop_caches の詳細は?

なお、 vm.drop_caches の実装を追いかけている すてきなエントリがあるので、細部を追いたい人はこちらを参照していただきたい

qiita.com

ソースのバージョン

CentOS7.3 3.10.0-514.26.2.el7.x86_64 を読んだ

ソースを追いかける

sysctl vm.drop_caches=1 した際にページキャッシュを破棄するので、 エントリポイントである sysctl のハンドラから追いかける。細部には立ち入らない ( Qiita を読んで! )

int drop_caches_sysctl_handler(ctl_table *table, int write,
    void __user *buffer, size_t *length, loff_t *ppos)
{
    int ret;

    ret = proc_dointvec_minmax(table, write, buffer, length, ppos);
    if (ret)
        return ret;
    if (write) {
        static int stfu;

        if (sysctl_drop_caches & 1) {
            iterate_supers(drop_pagecache_sb, NULL); ⭐
            count_vm_event(DROP_PAGECACHE);
        }
        if (sysctl_drop_caches & 2) {
            drop_slab();
            count_vm_event(DROP_SLAB);
        }
        if (!stfu) {
            pr_info("%s (%d): drop_caches: %d\n",
                current->comm, task_pid_nr(current),
                sysctl_drop_caches);
        }
        stfu |= sysctl_drop_caches & 4;
    }
    return 0;
}

drop_pagecache_sb

superblock ごとに inode をイテレートして invalidate_mapping_pages でページを破棄していく

/* A global variable is a bit ugly, but it keeps the code simple */
int sysctl_drop_caches;

static void drop_pagecache_sb(struct super_block *sb, void *unused)
{
    struct inode *inode, *toput_inode = NULL;

    spin_lock(&inode_sb_list_lock);
    list_for_each_entry(inode, &sb->s_inodes, i_sb_list) {
        spin_lock(&inode->i_lock);
        if ((inode->i_state & (I_FREEING|I_WILL_FREE|I_NEW)) ||
            (inode->i_mapping->nrpages == 0)) {
            spin_unlock(&inode->i_lock);
            continue;
        }
        __iget(inode);
        spin_unlock(&inode->i_lock);
        spin_unlock(&inode_sb_list_lock);
        invalidate_mapping_pages(inode->i_mapping, 0, -1); ⭐
        iput(toput_inode);
        toput_inode = inode;
        spin_lock(&inode_sb_list_lock);
    }
    spin_unlock(&inode_sb_list_lock);
    iput(toput_inode);
}

invalidate_mapping_pages

d.hatena.ne.jp

を書いた時に調べたことのあるメソッドだった。

/**
 * invalidate_mapping_pages - Invalidate all the unlocked pages of one inode
 * @mapping: the address_space which holds the pages to invalidate
 * @start: the offset 'from' which to invalidate
 * @end: the offset 'to' which to invalidate (inclusive)
 *
 * This function only removes the unlocked pages, if you want to
 * remove all the pages of one inode, you must call truncate_inode_pages.
 *
 * invalidate_mapping_pages() will not block on IO activity. It will not
 * invalidate pages which are dirty, locked, under writeback or mapped into
 * pagetables.
 */
unsigned long invalidate_mapping_pages(struct address_space *mapping,
        pgoff_t start, pgoff_t end)
{
    pgoff_t indices[PAGEVEC_SIZE];
    struct pagevec pvec;
    pgoff_t index = start;
    unsigned long ret;
    unsigned long count = 0;
    int i;

    pagevec_init(&pvec, 0);
    while (index <= end && __pagevec_lookup(&pvec, mapping, index,
            min(end - index, (pgoff_t)PAGEVEC_SIZE - 1) + 1,
            indices)) {
        mem_cgroup_uncharge_start();
        for (i = 0; i < pagevec_count(&pvec); i++) {
            struct page *page = pvec.pages[i];

            /* We rely upon deletion not changing page->index */
            index = indices[i];
            if (index > end)
                break;

            if (radix_tree_exceptional_entry(page)) {
                clear_exceptional_entry(mapping, index, page);
                continue;
            }

            if (!trylock_page(page))
                continue;
            WARN_ON(page->index != index);
            ret = invalidate_inode_page(page); ⭐
            unlock_page(page);
            /*
            * Invalidation is a hint that the page is no longer
            * of interest and try to speed up its reclaim.
            */
            if (!ret)
                deactivate_page(page);
            count += ret;
        }
        pagevec_remove_exceptionals(&pvec);
        pagevec_release(&pvec);
        mem_cgroup_uncharge_end();
        cond_resched();
        index++;
    }
    return count;
}
EXPORT_SYMBOL(invalidate_mapping_pages);

invalidate_inode_page

/*
 * Safely invalidate one page from its pagecache mapping.
 * It only drops clean, unused pages. The page must be locked.
 *
 * Returns 1 if the page is successfully invalidated, otherwise 0.
 */
int invalidate_inode_page(struct page *page)
{
    struct address_space *mapping = page_mapping(page);
    if (!mapping)
        return 0;
    if (PageDirty(page) || PageWriteback(page))
        return 0;
    if (page_mapped(page)) ⭐
        return 0;
    return invalidate_complete_page(mapping, page);
}

page_mapped によって「ページテーブルに map されているか (プロセスのアドレス空間に属していると解釈してOK? ) 」を確かめ、 map されていたら invalidate_complete_page しない、つまり、ページキャッシュの破棄をしない

/*
 * Return true if this page is mapped into pagetables.
 */
static inline int page_mapped(struct page *page)
{
    return atomic_read(&(page)->_mapcount) >= 0;
}

mmap(2) したページの場合は page_mapped ( _mapcount ) の結果が 0 以上のカウントになり、除外されるのだろう

struct page {

// ...

            union {
                /*
                * Count of ptes mapped in mms, to show when
                * page is mapped & limit reverse map searches.
                *
                * Extra information about page type may be
                * stored here for pages that are never mapped,
                * in which case the value MUST BE <= -2.
                * See page-flags.h for more details.
                */
                atomic_t _mapcount;

枝葉に別れて細かい疑問がわいてきたが、発散しそうなので ここまでにとどめておく