走行距離 80km、獲得標高 813m。夏休みにたくさん走った反動か、短い距離で満足
続きを読む先のエントリでは ps
コマンドのソースを読み、pr_cpu
という関数で %CPU
が計算されていると書いた。が、実行時に本当にその関数が呼び出されているのかどうかを確かめていなかった
ソースを誤読している可能性を排除するために、検証をしよう
どういうアプローチで確かめるかを考えたが、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 $( 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]
こんなんでよいだろか
先のエントリの中で、mmap(2) しているファイルのページ(キャッシュ?) は sysctl vm.drop_caches
で破棄されないことを確かめたのだが、どのように実装されているのかソースを確かめる
なお、 vm.drop_caches の実装を追いかけている すてきなエントリがあるので、細部を追いたい人はこちらを参照していただきたい
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; }
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 - 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);
/* * 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;
枝葉に別れて細かい疑問がわいてきたが、発散しそうなので ここまでにとどめておく