Linux kernel のバグを追おう: memcg 編(3)

前回の記事の続きです。

前回は、mitigation のパッチが percpu_stats_free_rwork_fn() の中で二つの per-CPU 統計領域を解放していることを見ていきました。その二つとは、memcg と NUMA node の組み合わせごとに持つ lruvec_stats_percpu と、memcg ごとに持つ vmstats_percpu です。

今回は、その二つの構造体を開いて、中に何が記録されているのかを見ていきます。引用するコードは、前回と同じく linux-next の next-20220421 時点のものです。

では、見ていきましょう!

両者の構造体は、どちらも include/linux/memcontrol.h にあります。vmstats_percpu の型は、struct memcg_vmstats_percpu という名前です。

struct lruvec_stats_percpu {
	/* Local (CPU and cgroup) state */
	long state[NR_VM_NODE_STAT_ITEMS];

	/* Delta calculation for lockless upward propagation */
	long state_prev[NR_VM_NODE_STAT_ITEMS];
};

struct memcg_vmstats_percpu {
	/* Local (CPU and cgroup) page state & events */
	long			state[MEMCG_NR_STAT];
	unsigned long		events[NR_VM_EVENT_ITEMS];

	/* Delta calculation for lockless upward propagation */
	long			state_prev[MEMCG_NR_STAT];
	unsigned long		events_prev[NR_VM_EVENT_ITEMS];

	/* Cgroup1: threshold notifications & softlimit tree updates */
	unsigned long		nr_page_events;
	unsigned long		targets[MEM_CGROUP_NTARGETS];
};

各構造体の state には、それぞれの統計値が記録されています。ただし、state はただの long の配列です。では、state[0]state[1] に何が記録されるのか。そして、それはどうやって決まるのでしょうか。

それを知るために、配列の長さとして使われている NR_VM_NODE_STAT_ITEMSMEMCG_NR_STAT の定義を見てみます。NR_VM_NODE_STAT_ITEMSinclude/linux/mmzone.hMEMCG_NR_STATinclude/linux/memcontrol.h で、次のように定義されています。

enum node_stat_item {
	NR_LRU_BASE,
	NR_INACTIVE_ANON = NR_LRU_BASE,
	NR_ACTIVE_ANON,
	NR_INACTIVE_FILE,
	NR_ACTIVE_FILE,
	NR_UNEVICTABLE,
	/* 省略 */
	NR_VM_NODE_STAT_ITEMS
};

enum memcg_stat_item {
	MEMCG_SWAP = NR_VM_NODE_STAT_ITEMS,
	MEMCG_SOCK,
	MEMCG_PERCPU_B,
	MEMCG_VMALLOC,
	MEMCG_KMEM,
	MEMCG_NR_STAT,
};

調べてみると、これらの enum の値がそのまま state の添字として使われていました。例えば、次のようにして統計値へアクセスします。

state[NR_INACTIVE_ANON]
state[NR_ACTIVE_ANON]

C の enum は、値を指定しなければ 0 から順番に値が割り当てられます。ここでは NR_LRU_BASE が 0 で、NR_INACTIVE_ANON にも同じ値が設定されています。そのため、state[0]、つまり state[NR_INACTIVE_ANON] には、inactive anonymous page の数が記録されます。そして、enum node_stat_item の最後にある NR_VM_NODE_STAT_ITEMS は、enum node_stat_item に並ぶ統計項目の総数を表します。これを次のように配列の長さとして使っているわけです。

long state[NR_VM_NODE_STAT_ITEMS];

enum の最後の値を、そのまま配列の長さとして使う。うまいですね。

次に enum memcg_stat_item を見てみます。二つの enum は別々に定義されていますが、MEMCG_SWAP = NR_VM_NODE_STAT_ITEMS とすることで、添字の番号が続くようになっています。

NR_VM_NODE_STAT_ITEMSN とすると、次のような並びになります。

enum node_stat_item
  0      NR_INACTIVE_ANON
  1      NR_ACTIVE_ANON
  ...
  N - 1  node_stat_item の最後の統計項目
  N      NR_VM_NODE_STAT_ITEMS ← node_stat_item の統計項目数

         │ MEMCG_SWAP = NR_VM_NODE_STAT_ITEMS

enum memcg_stat_item
  N      MEMCG_SWAP
  N + 1  MEMCG_SOCK
  N + 2  MEMCG_PERCPU_B
  N + 3  MEMCG_VMALLOC
  N + 4  MEMCG_KMEM
  N + 5  MEMCG_NR_STAT ← 全体の統計項目数

配列の長さと添字の対応をまとめると、次のようになります。MEMCG_ で始まる項目は、接頭辞を省いて書いています。

lruvec_stats_percpu (memcg × NUMA node ごと)
  ┌───────────────────────────┐
  │ node_stat_item : 0 .. N-1 │   長さ N = NR_VM_NODE_STAT_ITEMS
  └───────────────────────────┘

memcg_vmstats_percpu (memcg ごと)
  ┌───────────────────────────┬──────────┬──────────┬──────────┬──────────┬──────────┐
  │ node_stat_item : 0 .. N-1 │   SWAP   │   SOCK   │ PERCPU_B │ VMALLOC  │   KMEM   │
  └───────────────────────────┴──────────┴──────────┴──────────┴──────────┴──────────┘
                                   N         N+1        N+2        N+3        N+4
                                   長さ MEMCG_NR_STAT = N + 5

lruvec_stats_percpustate は前半の N 個だけを持ち、memcg_vmstats_percpustate は、そこへ swap や socket memory といった memcg 固有の 5 項目を足した長さになっています。

同じ state[NR_INACTIVE_ANON] でも、lruvec_stats_percpu には memcg と特定の NUMA node の組み合わせごとの値が CPU ごとに、memcg_vmstats_percpu には NUMA node を区別しない memcg ごとの値が CPU ごとに記録されます。

二つの正体は分かりました。しかし、dying 状態の memcg はまだ page から参照されています。memcg 本体が生きているのに、その中の統計領域だけを解放して本当に大丈夫なのでしょうか。

前回と今回で、二つの free_percpu() が何を解放しているのかを見てきました。次回は、その解放処理がどこから、いつ呼ばれるのかを追いながら、この疑問を考えていきます。

今回は以上になります。