Linux 4.11から入った参照カウンタのAPIめも

security things in Linux v4.11を読んでて参照カウンタ用のAPIが4.11に入ったとのことなので軽くめもです。

利用にはinclude/refcount.hをインクルードします。実装はlib/refcount.cです。

機能的には変数は実態はatomic_t型のrefcount_tという型が作られてます。そして、値の増減時にオーバフローとアンダフローのチェックが行われます。基本的な使い方はatomic_tと同じ感じです。わりと大きな違いとしては、atomic_t型のatomic_inc()は更新前の値が0の場合実行後は1になりますが(期待通りですね)、refcount_inc()は更新前の値が0だと更新は行わず、WARN_ON_ONCEによる警告メッセージが出ます。参照カウンタのカウンタ変数はatomic_tと同じくでunsigned int型です。

atomic_inc()は純粋にinc命令を実行してますね。

static __always_inline void atomic_inc(atomic_t *v)
{
    asm volatile(LOCK_PREFIX "incl %0"
             : "+m" (v->counter));
}

refcount_inc()の場合は、refcount_add_not_zero()が呼ばれます。この関数がfalseを返すと警告がでます。

void refcount_add(unsigned int i, refcount_t *r)
{
    WARN_ONCE(!refcount_add_not_zero(i, r), "refcount_t: addition on 0; use-after-free.\n");

refcount_add_not_zero()は更新前の値が0だったらfalseを返します。このようなチェックはrefcount_inc()だけではなくて他の関数でも行われています。例えば、デクリメントするときに元の値が0かチェックしたり。

refcount_t型の変数の初期化は静的に行う場合はREFCOUNT_INITマクロで、動的に行うならrefcount_set()で行います。

こんな感じで使えます。

gist.github.com

実行結果

masami@saga:~/codes/refcounter_test$ dmesg
[156191.116677] refcounter_test: refcounter_test_init start
[156191.116686] refcounter_test: refcounter_test_run: cpu 2
[156191.116689] refcounter_test: refcounter_test_run: cpu 6
[156191.116692] refcounter_test: refcounter_test_run: cpu 3
[156191.116694] refcounter_test: refcounter_test_run: cpu 0
[156191.116697] refcounter_test: refcounter_test_run: cpu 4
[156191.116699] refcounter_test: refcounter_test_run: cpu 5
[156191.116701] refcounter_test: refcounter_test_run: cpu 11
[156191.116703] refcounter_test: refcounter_test_run: cpu 7
[156191.116705] refcounter_test: refcounter_test_run: cpu 8
[156191.116707] refcounter_test: refcounter_test_run: cpu 9
[156191.116709] refcounter_test: refcounter_test_run: cpu 10
[156191.116713] refcounter_test: refcounter_test_run: cpu 1
[156191.116715] refcounter_test: counter1: 13
[156191.116715] refcounter_test: counter2: 13
[156191.116716] refcounter_test: non_atomic_counter: 12
[156196.491170] refcounter_test: refcounter_test_cleanup bye

( ´ー`)フゥー...

memcgで実際に課金してるところめも

mm/memcontorl.cのmem_cgroup_charge_statistics()のとこです。カーネルは毎度ながら4.1.15。

mem_cgroup_charge_statistics()のコードはこんな感じです。見たままですね。

 828 static void mem_cgroup_charge_statistics(struct mem_cgroup *memcg,
 829                                          struct page *page,
 830                                          int nr_pages)
 831 {
 832         /*
 833          * Here, RSS means 'mapped anon' and anon's SwapCache. Shmem/tmpfs is
 834          * counted as CACHE even if it's on ANON LRU.
 835          */
 836         if (PageAnon(page))
 837                 __this_cpu_add(memcg->stat->count[MEM_CGROUP_STAT_RSS],
 838                                 nr_pages);
 839         else
 840                 __this_cpu_add(memcg->stat->count[MEM_CGROUP_STAT_CACHE],
 841                                 nr_pages);
 842 
 843         if (PageTransHuge(page))
 844                 __this_cpu_add(memcg->stat->count[MEM_CGROUP_STAT_RSS_HUGE],
 845                                 nr_pages);
 846 
 847         /* pagein of a big page is an event. So, ignore page size */
 848         if (nr_pages > 0)
 849                 __this_cpu_inc(memcg->stat->events[MEM_CGROUP_EVENTS_PGPGIN]);
 850         else {
 851                 __this_cpu_inc(memcg->stat->events[MEM_CGROUP_EVENTS_PGPGOUT]);
 852                 nr_pages = -nr_pages; /* for event */
 853         }
 854 
 855         __this_cpu_add(memcg->stat->nr_page_events, nr_pages);
 856 }

chargeはmem_cgroup構造体のメンバ変数statに対して行っています。変数はmem_cgroup構造体のこの部分で定義されていて、__percpuが付いているのでcpu毎にデータがあります。この構造体はmm/memcontrol.cで宣言されていてるので他では使わないようです。

 321         /*
 322          * percpu counter.
 323          */
 324         struct mem_cgroup_stat_cpu __percpu *stat;

mem_cgroup_stat_cpu構造体もmm/memcontrol.cにあります。内容はこのようになってます。

 128 struct mem_cgroup_stat_cpu {
 129         long count[MEM_CGROUP_STAT_NSTATS];
 130         unsigned long events[MEMCG_NR_EVENTS];
 131         unsigned long nr_page_events;
 132         unsigned long targets[MEM_CGROUP_NTARGETS];
 133 };

targetsについては今回のコード中では使用されてません。 MEM_CGROUP_STAT_NSTATSなどはenum型でinclude/linux/memcontrol.hにて定義されています。

  32 /*
  33  * The corresponding mem_cgroup_stat_names is defined in mm/memcontrol.c,
  34  * These two lists should keep in accord with each other.
  35  */
  36 enum mem_cgroup_stat_index {
  37         /*
  38          * For MEM_CONTAINER_TYPE_ALL, usage = pagecache + rss.
  39          */
  40         MEM_CGROUP_STAT_CACHE,          /* # of pages charged as cache */
  41         MEM_CGROUP_STAT_RSS,            /* # of pages charged as anon rss */
  42         MEM_CGROUP_STAT_RSS_HUGE,       /* # of pages charged as anon huge */
  43         MEM_CGROUP_STAT_FILE_MAPPED,    /* # of pages charged as file rss */
  44         MEM_CGROUP_STAT_WRITEBACK,      /* # of pages under writeback */
  45         MEM_CGROUP_STAT_SWAP,           /* # of pages, swapped out */
  46         MEM_CGROUP_STAT_NSTATS,
  47 };

例えば、RSSページの場合は↓なので、インデックス1番目の要素にnr_pagesを足してます。

__this_cpu_add(memcg->stat->count[MEM_CGROUP_STAT_RSS], nr_pages);

nr_pagesはmem_cgroup_commit_charge()で設定していて、THPでない場合は1、THPの場合は以下の部分で計算してます。

5542         if (PageTransHuge(page)) {
5543                 nr_pages <<= compound_order(page);
5544                 VM_BUG_ON_PAGE(!PageTransHuge(page), page);
5545         }

mem_cgroup_charge_statistics()の前半はページの種別に応じたchargeを行っていて、後半のところはpagein/pageoutのどちらのイベントかで処理が変わりますが、基本はそれぞれのイベント発生回数を増やしてますね。pageoutの場合はnr_pagesの符号を負にしてます。 そして、最後にnr_page_eventsにnr_pagesを足します。pageout時は引く感じですね。

 855         __this_cpu_add(memcg->stat->nr_page_events, nr_pages);

( ´ー`)フゥー...

IoTエンジニア養成読本

IoTエンジニア養成読本

commit_charge()の処理

mem_cgroup_commit_charge()から呼ばれるcommit_charge()の処理のめも。

呼び出しはこうですね。mem_cgroup_commit_charge()で渡された引数がそのまま渡ります。

5522 void mem_cgroup_commit_charge(struct page *page, struct mem_cgroup *memcg,
5523                               bool lrucare)
~~~略~~~
5540         commit_charge(page, memcg, lrucare);

commit_charge()はmem_cgroup_commit_charge()と同じくmm/memcontrol.cにあります。処理自体は数行しかありません。

2419 static void commit_charge(struct page *page, struct mem_cgroup *memcg,
2420                           bool lrucare)
2421 {
2422         int isolated;
2423 
2424         VM_BUG_ON_PAGE(page->mem_cgroup, page);
2425 

page->mem_cgroup変数が設定されているとエラーになります。commit_charge()はこの変数を設定するのが役割なので、先に設定されているということはバグということなんでしょう。

2426         /*
2427          * In some cases, SwapCache and FUSE(splice_buf->radixtree), the page
2428          * may already be on some other mem_cgroup's LRU.  Take care of it.
2429          */
2430         if (lrucare)
2431                 lock_page_lru(page, &isolated);
2432 

pageがすでにLRUリストにある場合はロックを取ってます。

2433         /*
2434          * Nobody should be changing or seriously looking at
2435          * page->mem_cgroup at this point:
2436          *
2437          * - the page is uncharged
2438          *
2439          * - the page is off-LRU
2440          *
2441          * - an anonymous fault has exclusive page access, except for
2442          *   a locked page table
2443          *
2444          * - a page cache insertion, a swapin fault, or a migration
2445          *   have the page locked
2446          */
2447         page->mem_cgroup = memcg;
2448

ここでpage->mem_cgroupにmem_cgroup_commit_charge()を呼んだところが渡したmemcgの構造体をpage構造体に設定します。

2449         if (lrucare)
2450                 unlock_page_lru(page, isolated);
2451 }

最後にロックを取っていた場合はアンロックします。

ここで設定したmem_cgroup構造体はinclude/linux/mm_types.hにあるpage構造体で以下のように定義されています。

 169 #ifdef CONFIG_MEMCG
 170         struct mem_cgroup *mem_cgroup;
 171 #endif

commit_charge()は処理が全然ないので今日はこれにて終了

Raspberry Piで学ぶ ROSロボット入門

Raspberry Piで学ぶ ROSロボット入門

mem_cgroup_commit_charge()の処理

カーネルは4.1.15。mem_cgroup_commit_charge()はchargeの処理で呼ばれる関数。

今回はここで使われる関数の詳細は調べないで、大まかな流れを把握するのが目標。

LXRで検索するとmm/filemap.cとかmm/swapfile.c、mm/memory.cなんかから呼ばれてます。 mem_cgroup_commit_charge()自体は40行ほどの関数。引数はcharge対象のpage構造体、所属するmemcgの構造体、最後のlrucareはpageがすでにLRUで管理されているかを示すbool値です。

5522 void mem_cgroup_commit_charge(struct page *page, struct mem_cgroup *memcg,
5523                               bool lrucare)
5524 {
5525         unsigned int nr_pages = 1;
5526 

処理を見ていきます。

5527         VM_BUG_ON_PAGE(!page->mapping, page);
5528         VM_BUG_ON_PAGE(PageLRU(page) && !lrucare, page);
5529 

最初にassertがあります。VM_BUG_ON_PAGEマクロは1番目の引数がif文に使われる条件で、2番目に渡しているpage構造体は値をdumpするのに使います。 最初のチェックは見たままで、2番めのほうはPageLRUマクロではpageがLRUに乗ってるのに、lrucareはfalseっていう矛盾のチェックです。

5530         if (mem_cgroup_disabled())
5531                 return;

memcgが使われてないなら何もしないですよね。

5532         /*
5533          * Swap faults will attempt to charge the same page multiple
5534          * times.  But reuse_swap_page() might have removed the page
5535          * from swapcache already, so we can't check PageSwapCache().
5536          */
5537         if (!memcg)
5538                 return;
5539

swapの処理でcharge済みのページを再度chargeしないようにらしいです。 コメントによるとreuse_swap_page()の場合、swapcacheからpageがなくなっているのでPageSwapCache()を使ってチェックできないと。

5540         commit_charge(page, memcg, lrucare);
5541

commit_charge()は名前の割にchargeの処理してないように見えるけど?今回はここの実装は見ないのでこんな関数を呼ぶよという程度で。

5542         if (PageTransHuge(page)) {
5543                 nr_pages <<= compound_order(page);
5544                 VM_BUG_ON_PAGE(!PageTransHuge(page), page);
5545         }
5546

THP(Transparent Huge Pages)の場合のチェックですね。

5547         local_irq_disable();
5548         mem_cgroup_charge_statistics(memcg, page, nr_pages);
5549         memcg_check_events(memcg, page);
5550         local_irq_enable();
5551

割り込みを無効にして、mem_cgroup_charge_statistics()で実際に値を増やしてますね。↓のような処理があるので。

 836         if (PageAnon(page))
 837                 __this_cpu_add(memcg->stat->count[MEM_CGROUP_STAT_RSS],
 838                                 nr_pages);

次にmemcg_check_events()を呼んで、chargeしたことによりsoft limitを超えるかチェックして、超えたら何か処理してます。ここまでやったら割り込みを有効に戻します。

5552         if (do_swap_account && PageSwapCache(page)) {
5553                 swp_entry_t entry = { .val = page_private(page) };
5554                 /*
5555                  * The swap entry might not get freed for a long time,
5556                  * let's not wait for it.  The page already received a
5557                  * memory+swap charge, drop the swap entry duplicate.
5558                  */
5559                 mem_cgroup_uncharge_swap(entry);
5560         }
5561 }

chargeしたのがswap時の場合ですね。if文の条件が真の場合はすでにcharge済みなのでmem_cgroup_uncharge_swap()でunchargeして今回のcharge分を戻します。

( ´ー`)フゥー...

ガベージコレクション

ガベージコレクション

mem_cgroup_charge_common()はだいぶ前に消えていた (; ・`д・´) ナ、ナンダッテー !! (`・д´・ (`・д´・ ;)

昨日の続きです。

4.10にあるドキュメントには課金の処理ではmem_cgroup_charge_common()を使うよーなんて書いてあったんですが、この関数はだいぶ昔になくなってました。

3.15で消えたようです。さらに言うと、このパッチで使用している__mem_cgroup_commit_charge()は3.17で消えたようです。 github.com

3.17の開発時に課金周りの処理が書き換えられたみたいで、このパッチで__mem_cgroup_commit_charge()からcommit_charge()に変わってました。 github.com

commit_charge()は4.10にもありました。やったね!

IoTエンジニア養成読本

IoTエンジニア養成読本

Documentation/cgroup-v1/memory.txtのめも

コードではなくてドキュメントもたまには読みましょうということで、Documentation/cgroup-v1/memory.txtです。

accounting == 課金です。以下のめもは本文を自分が分かれば良い程度に大雑把に意訳した感じです。 機械翻訳はとかしてないので安心ですね?

2.2. Accounting

Figure 1の説明のところ。

  1. 課金は各cgroupの単位
  2. 各mm_struct構造体は自分がどのcgroupに属しているかわかる
  3. 各page(構造体だよな)はpage_cgroupへのポインタを持っていて、ここからどのcgroupに属しているかわかる

課金は以下の流れで行われる。

  1. mem_cgroup_charge_common()が呼ばれて、最低限のデータの設定、メモリの使用量が設定値を超えるかチェック
  2. 超えるようならこのcgroupを対象にページ回収の処理を行う。詳細はreclaimのセクションで。
  3. すべてが上手いこと行くようなら、page_cgroupの更新をする。page_cgroupはLRUを持ってる。page_cgroupはbootやメモリホットプラグでメモリが追加されたときにインスタンスが確保される

2.2.1 Accounting details

すべての無名ページ(RSS)とページキャッシュは課金される。reclaimableじゃないページとLRUで管理されないようなページは課金されない。ようは通常の仮想メモリ管理下にあるページが課金される。 RSSのページはページフォルト時に課金されるけど、すでに課金されてた場合は除く。ページキャッシュはinode(radix-tree)に入れられるときに課金する。プロセスのページテーブルにマップするときは重複して課金しないように気をつけないといけない。

RSSページは完全にunmmapするときに課金した分を減らす。ページキャッシュはradix-treeから削除するときに減らす。unaccountedって日本語でなんて言えばいいか思い浮かばなかった(´・ω・`) もしRSSページがkswapdによってunmapされた場合、SwapCacheとしてシステムによって完全に解放されるまで存在する可能性がある。こういったSwapCacheも課金の対象になる。swapされたページはmapされるまでは課金されない。

カーネルはswap inの先読みや、複数のswapされたページを一度に読みこむんだけど、このときにpage faultが起きる場合があり、この場合は課金しないようにする必要がある。

ページのマイグレーション時には課金の情報はkeepされる。

LRUで管理されるページを課金するのは利用中のページを管理するため。VM視点で行くとLRUで管理していないページはVMの管理外にある。

(´-`).。oO(2.3はまた次回

詳解 システム・パフォーマンス

詳解 システム・パフォーマンス

memory cgroupの初期化処理辺りを読む

誰得なめも。バージョンは4.1.15。

__initがついているのは以下の3関数。

  1. mem_cgroup_init()
  2. enable_swap_account()
  3. mem_cgroup_swap_init()

当たり前だけど、__initがあるのでカーネル起動時の初期化で呼ばれる。

mem_cgroup_init()はこのような関数。

5774 static int __init mem_cgroup_init(void)
5775 {
5776         int cpu, node;
5777 
5778         hotcpu_notifier(memcg_cpu_hotplug_callback, 0);
5779 
5780         for_each_possible_cpu(cpu)
5781                 INIT_WORK(&per_cpu_ptr(&memcg_stock, cpu)->work,
5782                           drain_local_stock);
5783 
5784         for_each_node(node) {
5785                 struct mem_cgroup_tree_per_node *rtpn;
5786                 int zone;
5787 
5788                 rtpn = kzalloc_node(sizeof(*rtpn), GFP_KERNEL,
5789                                     node_online(node) ? node : NUMA_NO_NODE);
5790 
5791                 for (zone = 0; zone < MAX_NR_ZONES; zone++) {
5792                         struct mem_cgroup_tree_per_zone *rtpz;
5793 
5794                         rtpz = &rtpn->rb_tree_per_zone[zone];
5795                         rtpz->rb_root = RB_ROOT;
5796                         spin_lock_init(&rtpz->lock);
5797                 }
5798                 soft_limit_tree.rb_tree_per_node[node] = rtpn;
5799         }
5800 
5801         return 0;
5802 }
5803 subsys_initcall(mem_cgroup_init);

hotcpu_notifier()はcpu hotplugの処理なので無視して、最初のfor_each_possible_cpuマクロはper cpuなデータであるstruct memcg_stock_pcpの初期化。memcg_stock_pcp構造体はこのような構造体。現段階では初期化処理しか追っていないので、この構造体の使用目的は未確認。とりあえず、ワークキューがあるのと(struct work_struct)、nr_pagesというどう考えでもページ数の数を保持するであろうメンバ変数があるのでページ関連の操作で使われるのでしょう。

2055 struct memcg_stock_pcp {
2056         struct mem_cgroup *cached; /* this never be root cgroup */
2057         unsigned int nr_pages;
2058         struct work_struct work;
2059         unsigned long flags;
2060 #define FLUSHING_CACHED_CHARGE  0
2061 };
2062 static DEFINE_PER_CPU(struct memcg_stock_pcp, memcg_stock);

次のfor_each_nodeマクロでメモリノード単位で処理が行われる。ここで出てくるmem_cgroup_tree_per_node構造体はこんな感じなんだけど、mm/memcontrol.cで定義されてるので他では使われない模様。赤黒木なデータ構造なんですね。

 172 struct mem_cgroup_tree_per_node {
 173         struct mem_cgroup_tree_per_zone rb_tree_per_zone[MAX_NR_ZONES];
 174 };

そして、各ゾーン(これはZONE_NORMALとかですね。)毎にrbツリーの初期化をします。

5791                 for (zone = 0; zone < MAX_NR_ZONES; zone++) {
5792                         struct mem_cgroup_tree_per_zone *rtpz;
5793 
5794                         rtpz = &rtpn->rb_tree_per_zone[zone];
5795                         rtpz->rb_root = RB_ROOT;
5796                         spin_lock_init(&rtpz->lock);
5797                 }

初期化したものはsoft_limit_treeという変数のrb_tree_per_node配列に突っ込みます。

5798                 soft_limit_tree.rb_tree_per_node[node] = rtpn;

soft_limit_treeはこう宣言されていて基本は参照だけみたいです。

 180 static struct mem_cgroup_tree soft_limit_tree __read_mostly;

ちなみに、MAX_NR_ZONESはinclude/generated配下のファイルにあるのでビルド時に定義されるようです。

./include/generated/bounds.h:10:#define MAX_NR_ZONES 4 /* __MAX_NR_ZONES        # */

2つ目の初期化処理はenable_swap_account()で、これはカーネルの起動時のコマンドラインで渡されたswapaccountの処理ですね。really_do_swap_account変数に値を設定するだけです。

5877 static int __init enable_swap_account(char *s)
5878 {
5879         if (!strcmp(s, "1"))
5880                 really_do_swap_account = 1;
5881         else if (!strcmp(s, "0"))
5882                 really_do_swap_account = 0;
5883         return 1;
5884 }
5885 __setup("swapaccount=", enable_swap_account);
5886 

このコマンドライン引数がなければ呼ばれないですけど。その場合はデフォルト値があるのでそちらが使われます。

5870 /* for remember boot option*/
5871 #ifdef CONFIG_MEMCG_SWAP_ENABLED
5872 static int really_do_swap_account __initdata = 1;
5873 #else
5874 static int really_do_swap_account __initdata;
5875 #endif

3つ目はmem_cgroup_swap_init()です。memory cgroupがenableで、really_do_swap_accountが1の場合(あえてswapaccount=0を設定しなければ大抵1になってるのでは)に処理があります。

5914 static int __init mem_cgroup_swap_init(void)
5915 {
5916         if (!mem_cgroup_disabled() && really_do_swap_account) {
5917                 do_swap_account = 1;
5918                 WARN_ON(cgroup_add_legacy_cftypes(&memory_cgrp_subsys,
5919                                                   memsw_cgroup_files));
5920         }
5921         return 0;
5922 }
5923 subsys_initcall(mem_cgroup_swap_init);

do_swap_accountを1に設定しているので、swapもアカウンティングするよというのと、cgroup_add_legacy_cftypes()を呼んでいるのでmemory cgroupのファイル作成処理があります。作成するのは以下のファイルです。/sys/kernel/fs/cgroup/memoryに以下のファイルが作られます。

5887 static struct cftype memsw_cgroup_files[] = {
5888         {
5889                 .name = "memsw.usage_in_bytes",
5890                 .private = MEMFILE_PRIVATE(_MEMSWAP, RES_USAGE),
5891                 .read_u64 = mem_cgroup_read_u64,
5892         },
5893         {
5894                 .name = "memsw.max_usage_in_bytes",
5895                 .private = MEMFILE_PRIVATE(_MEMSWAP, RES_MAX_USAGE),
5896                 .write = mem_cgroup_reset,
5897                 .read_u64 = mem_cgroup_read_u64,
5898         },
5899         {
5900                 .name = "memsw.limit_in_bytes",
5901                 .private = MEMFILE_PRIVATE(_MEMSWAP, RES_LIMIT),
5902                 .write = mem_cgroup_write,
5903                 .read_u64 = mem_cgroup_read_u64,
5904         },
5905         {
5906                 .name = "memsw.failcnt",
5907                 .private = MEMFILE_PRIVATE(_MEMSWAP, RES_FAILCNT),
5908                 .write = mem_cgroup_reset,
5909                 .read_u64 = mem_cgroup_read_u64,
5910         },
5911         { },    /* terminate */
5912 };

cgroup_add_legacy_cftypes()に渡しているmemory_cgrp_subsysは以下のようになってます。

  75 struct cgroup_subsys memory_cgrp_subsys __read_mostly;
  76 EXPORT_SYMBOL(memory_cgrp_subsys);

初期化処理はこんな感じですね。