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_ITEMS と MEMCG_NR_STAT の定義を見てみます。NR_VM_NODE_STAT_ITEMS は include/linux/mmzone.h、MEMCG_NR_STAT は include/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_ITEMS を N とすると、次のような並びになります。
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_percpu の state は前半の N 個だけを持ち、memcg_vmstats_percpu の state は、そこへ 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() が何を解放しているのかを見てきました。次回は、その解放処理がどこから、いつ呼ばれるのかを追いながら、この疑問を考えていきます。
今回は以上になります。