めもめも

このブログに記載の内容は個人の見解であり、必ずしも所属組織の立場、戦略、意見を代表するものではありません。

/proc/meminfoを考える

通りすがりの貴方・・・・

/proc/meminfoのあっちの値とこっちの値を足したら、なんでそっちの値と同じにならないの・・・・

と悩んだことありますよね?

/proc/meminfoは、カーネルが内部的に管理している枠組みでのメモリ情報をそのまま出しているので、残念ながらユーザ視点で知りたいメモリ情報とは一致しません。

とはいえ、変な解釈をして無意味に悩まないために、それぞれの値の意味合いと項目間の関係を知っておくのは有意義です。私の理解の範囲で、それらの関係をまとめていきます。

#私の理解も完璧ではないので、間違いあればやさしくご指摘お願いします。

参考資料 http://mkosaki.blog46.fc2.com/blog-entry-1007.html

2011/09/07 追記: tmpfsがSwapCachedに含まれるのは幻想でした。tmpfs=Shmemに修正しました。

まずは、全体。RHEL6.1の環境です。

# uname -a
Linux kakinoha 2.6.32-131.12.1.el6.x86_64 #1 SMP Sun Jul 31 16:44:56 EDT 2011 x86_64 x86_64 x86_64 GNU/Linux
# cat /proc/meminfo 
MemTotal:        8069288 kB
MemFree:         6269572 kB
Buffers:            7740 kB
Cached:           101324 kB
SwapCached:          796 kB
Active:           943536 kB
Inactive:         235008 kB
Active(anon):     892084 kB
Inactive(anon):   201448 kB
Active(file):      51452 kB
Inactive(file):    33560 kB
Unevictable:           0 kB
Mlocked:               0 kB
SwapTotal:       2097144 kB
SwapFree:        2093596 kB
Dirty:                 0 kB
Writeback:             0 kB
AnonPages:       1068776 kB
Mapped:            50044 kB
Shmem:             24052 kB
Slab:             513180 kB
SReclaimable:     242496 kB
SUnreclaim:       270684 kB
KernelStack:        3040 kB
PageTables:        39048 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:     6131788 kB
Committed_AS:    3309080 kB
VmallocTotal:   34359738367 kB
VmallocUsed:      375884 kB
VmallocChunk:   34359351896 kB
HardwareCorrupted:     0 kB
AnonHugePages:    819200 kB
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB
DirectMap4k:       12288 kB
DirectMap2M:     8261632 kB

この部分の解釈はさすがに異論はないでしょう。

MemTotal:        8069288 kB  カーネルが認識している全物理メモリ
MemFree:         6269572 kB  いかなる用途にも使われていないメモリ

メモリの使われ方の分類は、大雑把には、こんな感じになります。

MemTotal = MemFree + File-backedなメモリ + Anonymousなメモリ + カーネル空間が使うメモリ
                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                            ^--ユーザ空間が使うメモリ

File-backedというのは、ディスクからメモリに読み込んだファイルなど、メモリを開放したくなったら、その内容をディスクに書き戻せば開放できるタイプのメモリです。Anonymousというのはそれ以外のメモリで、メモリを開放したくなったら、Swap領域に書き出さないと開放できないタイプのメモリ。

この大雑把な分類ごとに見ていきます。

File-backedなメモリ

まずは誰もが「ディスクキャッシュとして使われている」と解釈したくなるメモリ領域。

Buffers:            7740 kB  ディスクキャッシュぽい何か。
Cached:           101324 kB  ディスクキャッシュぽい何か。

といいつつ、いきなり怪しい表現ですね。。。っぽいって何だ?

実は、一般にユーザが考える「ディスクキャッシュ」、すなわち、物理ディスク上のファイルアクセスに伴うキャッシュメモリの使用量はこちらの2つの合計を見た方がよいです。

Active(file):      51452 kB
Inactive(file):    33560 kB

"Buffers + Cached"には、それ以外の動作に伴って確保されたキャッシュメモリも含まれるので、上記の合計よりも大きくなります。どんなものがあるかというと、

  • tmpfsなどのメモリを利用したファイルシステムによる使用メモリ
  • 内部的にtmpfs機能を利用している共有メモリの使用メモリ

あれ?共有メモリの使用量って、これじゃないの?

Shmem:             24052 kB

そうでした。。。Shmemは実体としては、tmpfsの事だと思って下さい。tmpfsの実装がそもそもmm/shmem.cですから。

ここまでを図示するとこんな感じ。

---------------------------------------------
|            Buffers / Cached               | 
---------------------------------------------
| Active(file) / Inactive(file) |   Shmem   |
---------------------------------------------

なんとなく公式が見えてきましたね。

Buffers + Cached = Active(file) + Inactive(file) + Shmem

Anonymousなメモリ

Anonymousなメモリは、ずばりここ?

Active(anon):     892084 kB
Inactive(anon):   201448 kB

はい。ですが、例によって、あやしい人がここにいます。

AnonPages:       1068776 kB

足し算すると分かるように、AnonPages < Active(anon) + Inactive(anon)です。差分はどこに???

カーネルソースを見ると、AnonPagesは、rmapで管理されているAnonymousページの分量を表示しています。rmapというのは、このAnonymousページはどのユーザプロセスのどの論理アドレスから参照されているかを逆引きする機能です。

ということは、特定のユーザプロセスに紐付いていない、でも、file-backedではないメモリがあるはず。。。。

答えはきっとShmem(tmpfs)。つまり、こんな感じではなかろうかと。

---------------------------------------------
|        Active(anon) + Inactive(anon)      | 
---------------------------------------------
|   Shmem   |          AnonPages            |
---------------------------------------------

公式的には、こんな感じ。

Active(anon) + Inactive(anon) = Shmem + AnonPages

うーん。実際に計算したら、数百kBの誤差が出る。。。。

--------------------------------------------------
|           Active(anon) + Inactive(anon)        | 
--------------------------------------------------
|   Shmem   |          AnonPages              |??|
--------------------------------------------------

#誤差の正体は後ほど考察。

ちなにみ、tmpfsがInactive(anon)に突っ込まれるのはこのあたり。

mm/filemap.c

int add_to_page_cache_lru(struct page *page, struct address_space *mapping,
                                pgoff_t offset, gfp_t gfp_mask)
{
...
        ret = add_to_page_cache(page, mapping, offset, gfp_mask);
        if (ret == 0) {
                if (page_is_file_cache(page))
                        lru_cache_add_file(page);
                else
                        lru_cache_add_anon(page); // ここ
        }
        return ret;
}

ファイルシステムなのでメモリ的にはキャッシュなんだけど、実体のファイルが存在しないのでメモリ管理上はanonに突っ込まれているあたりが、どっちつかずの切なさを感じさせてくれます。(ちなみに「ここ」んところは、KOSAKIさんのパッチでした。https://lkml.org/lkml/2010/5/19/74

ユーザ空間のメモリをいったんまとめる

ここまでにできた2つの図式を統合するとこうなります。それっぽい感じになりました。

-----------------------------------------------------------------------------
|            Buffers / Cached               |           AnonPages        |??|
-----------------------------------------------------------------------------
| Active(file) + Inactive(file) |        Active(anon) + Inactive(anon)      | 
-----------------------------------------------------------------------------
|                               |   Shmem   |                               |
-----------------------------------------------------------------------------

カーネル空間が使うメモリ

ここまでの議論が正しいとすると、最初の関係式にもどって、こういう公式になります。

カーネル空間が使うメモリ = MemTotal - MemFree - Buffers - Cached - AnonPages - ??
                     = MemTotal - MemFree - Active - Inactive
where
 Active = Active(anon) + Active(file)
 Inactive = Inactive(anon) + Inactive(file)

とはいえ、カーネル空間が使うメモリの中身も気になります。残念ながら、その全ての項目が/proc/meminfoには現れない気がしますが、代表的な項目はこのあたりかと。

Slab:             513180 kB  スラブアロケータが確保したメモリ
KernelStack:        3040 kB  カーネル空間で使うスタック領域
PageTables:        39048 kB  ページテーブル
VmallocUsed:      375884 kB (ここからioremap分を引いたもの)※別途説明。

カーネルが内部的に変数などに割り当てて使用するメモリ領域は、基本的には、スラブアロケータという仕組みで事前に確保して、そこから小分けされます。ですので、まずは、Slabを見るとカーネルが使っているメモリ量の当たりがつきます。KernelStackとPageTablesはその名の通り。

混乱しがちなのが、VmallocUsed。スラブアロケータを通さずに、カーネルの論理アドレス空間にダイレクトにメモリをマッピングして使うのがこの領域です。ただし、物理メモリ以外に、外部デバイスのIOメモリがマッピング(ioremap)されたりしているので、この値は、物理メモリの使用量以上になっています。

幸いにもVmallocのより詳細な情報は、/proc/vmallocinfoからとれるので、次のおまじないを唱えると、物理メモリがマッピングされているvmalloc領域だけのメモリ量(kB)が計算できます。

# echo "( $(cat /proc/vmallocinfo | grep vmalloc | awk '{print $2}' | paste -s -d "+") )/ 1024" | bc

と、言っておいて大切な注意です。実は、/proc/vmallocinfoにアクセスすると、運が悪いとKernel Panicするかもしれないバグが先月見つかってしまいました。http://t.co/CfHPrM6 RHELにパッチがあたるにはいましばし時間がかかると思うので、上のおまじないを唱える際は、すこし気をつけてください。

ここの所の誤差の正体

--------------------------------------------------
|           Active(anon) + Inactive(anon)        | 
--------------------------------------------------
|   Shmem   |          AnonPages              |??|
--------------------------------------------------

<その1>
新しくできたAnonymousページがActive(anon)/Inactive(anon)に追加される部分では、先のmm/filemap.cの例にあるように、lru_cache_add_*()などの関数が利用されます。この関数の中身を追っていくと、

mm/swap.c

void __lru_cache_add(struct page *page, enum lru_list lru)
{
        struct pagevec *pvec = &get_cpu_var(lru_add_pvecs)[lru];

        page_cache_get(page);
        if (!pagevec_add(pvec, page)) // pagevecにpageを追加していって、いっぱいになったら、
                ____pagevec_lru_add(pvec, lru); // pagevecからLRU(Active/Inactiveリスト)に追加
        put_cpu_var(lru_add_pvecs);
}

という感じで、一旦、pagevecにキャッシュされてからActive(anon)/Inactive(anon)にまとめて追加される構造になっています。pagevecでキャッシュされている途中のpageは、Active(anon)/Inactive(anon)に現れないので、これは、Active(anon)/Inactive(anon)が小さく見える要因になります。(今の場合はとは逆のズレですが。)

<その2>
Swap-outされたメモリページをSwap-inする処理の中で、まだプロセスからアクセスされていないページも先にSwap-inしてしまう先読み処理が走ります。先読みされたページは、Active(anon)/Inactive(anon)に登録されますが、実際にプロセスからアクセスされるまでは、rmapされないので、AnonPagesには反映されません。これは、今のようにAnonPagesが小さく見える要因になります。

// Swap-inの開始
// mm/memory.c
static int do_swap_page(struct mm_struct *mm, struct vm_area_struct *vma,
                unsigned long address, pte_t *page_table, pmd_t *pmd,
                unsigned int flags, pte_t orig_pte)
{
...
        page = lookup_swap_cache(entry); // すでに先読みされているか確認
        if (!page) {
                grab_swap_token(mm); /* Contend for token _before_ read-in */
                page = swapin_readahead(entry,
                                        GFP_HIGHUSER_MOVABLE, vma, address);
                // まだ先読みされてなければ、ここで先読みを開始
...
         set_pte_at(mm, address, page_table, pte);
         do_page_add_anon_rmap(page, vma, address, exclusive);
         // アクセスされたページのPTEをセットしてrmapに登録
         // 先読みだけされてアクセスされていないページはrmap登録されない点に注意

// 先読みする所
// mm/swap_state.c
struct page *swapin_readahead(swp_entry_t entry, gfp_t gfp_mask,
                        struct vm_area_struct *vma, unsigned long addr)
{
...
                page = read_swap_cache_async(swp_entry(swp_type(entry), offset),
                                                gfp_mask, vma, addr);

struct page *read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
                        struct vm_area_struct *vma, unsigned long addr)
{
...
                err = __add_to_swap_cache(new_page, entry);
                if (likely(!err)) {
                        radix_tree_preload_end();
                        /*
                         * Initiate read into locked page and return.
                         */
                        lru_cache_add_anon(new_page); // ここでInactive(anon)にぶら下がる
                        swap_readpage(new_page);
                        return new_page;
                }

ちなみに、先読みしたページは「スワップキャッシュ」と呼ばれ、そのサイズはmeminfoのSwapCachedに表示されます。ただし、rmapされてAnonPagesに仲間入りした後も(対応するデータがディスクのSwap領域に残っている間は)スワップキャッシュとして認識されるので、下記のような関係になります。

-------------------------------------------------
|           Active(anon) + Inactive(anon)       |
-------------------------------------------------
|   Shmem   |          AnonPages             |??|
-------------------------------------------------
                                  | SwapCached|
                                  -------------
                                      rmap済み|未rmap

冒頭にあげた/proc/meminfoの出力例では、SwapCachedが約800kBで、上記のズレが約700kBなので確かにそれっぽい気になります。

<その3>
ユーザ空間のメモリページは、基本的には下記のどれかのLRUリストにぶらさがります。

Active(anon):     892084 kB
Inactive(anon):   201448 kB
Active(file):      51452 kB
Inactive(file):    33560 kB

が。。。。実はもうひとつ、回収不能ページ(解放できないディスクキャッシュとか、Swap-outできないAnonymousページとか)だけを特別にぶら下げるLRUリストがあります。

Unevictable:           0 kB

(参考)
http://lwn.net/Articles/286485/
http://www.atmarkit.co.jp/flinux/rensai/watch2008/watchmemb.html

今回は0kBなので誤差には影響していませんが、これもユーザ空間のメモリ使用量として加える必要があります。もはや複雑すぎて図示できませんが、無理やり描くとこんな感じ。

---------------------------------------------------------------------------------
|            Buffers / Cached           |        AnonPages                   |??|
---------------------------------------------------------------------------------
| Active(file) + Inactive(file)  | Unevictalbe |  Active(anon) + Inactive(anon) |
---------------------------------------------------------------------------------