TenForward

技術ブログ。はてなダイアリーから移転しました

Linux 3.8 で改良された memory cgroup (2)

ここに書いてあるのはカーネル付属のドキュメントに書かれてある事ですので,正確な情報はそちらをご参照ください. ;)

前回 (Linux 3.8 で改良された memory cgroup (1) - TenForwardの日記) 試したように,Linux 3.8 からカーネルメモリに対して制限をかけられるようになりました.カーネルメモリのうち,以前 (Linux 3.3 の新機能 Per-cgroup TCP buffer limits (3) - TenForwardの日記),TCP バッファの制限が Cgroup 毎にかけられるようになってましたが,今回はスタックページ,スラブページのカウントと制限が可能になりました.

この機能は通常の cgroup の機能と違って,パフォーマンスに影響を与えないために,少し機能が制限 (というか,他のものと少し動きガ違う) されています.また,他にも覚えておいた方が良い動きがあります.

root グループに対する制限,使用量

メモリの制限は root グループ (cgroupファイルシステムのトップディレクトリ) に対しては効きません.これはカーネルメモリに限らずメモリコントローラ全般に言えることです.

root グループに対して制限値を設定しようとすると (-EINVAL) エラーとなります.これは mm/memcontrol.c 内を見れば分かります.

static int mem_cgroup_write(struct cgroup *cont, struct cftype *cft,
			    const char *buffer)
{
  : (略)
	switch (name) {
	case RES_LIMIT:
		if (mem_cgroup_is_root(memcg)) { /* Can't set limit on root */
			ret = -EINVAL;
			break;
		}
  : (以下略)

使用量については,root グループについてはカウントされるかもしれないし,されないかもしれないとドキュメントには書かれています.試しにグループを一つ作って制限を設定し,その時の root グループの usage_in_bytes を見てみると以下のようになりました.階層構造を有効にしていますので,子グループの消費量は親グループ (ここでいう root) にも反映されているはずですが,そうはなっていません (親の方が値が小さい).

root@enterprise:/sys/fs/cgroup/memory# echo 1 > memory.use_hierarchy             (階層構造を有効にする)
root@enterprise:/sys/fs/cgroup/memory# mkdir 01                                  (group "01" を作成)
root@enterprise:/sys/fs/cgroup/memory# cd 01
root@enterprise:/sys/fs/cgroup/memory/01# echo 200M > memory.kmem.limit_in_bytes ("01" のカーネルメモリの制限を 200MB に設定)
root@enterprise:/sys/fs/cgroup/memory/01# cat memory.kmem.limit_in_bytes         (設定の確認)
209715200
root@enterprise:/sys/fs/cgroup/memory/01# echo 17310 > tasks                     (プロセスを "01" に登録)
root@enterprise:/sys/fs/cgroup/memory/01# cat memory.kmem.usage_in_bytes         ("01" グループのカーネルメモリの消費量を確認)
811008
root@enterprise:/sys/fs/cgroup/memory/01# cat ../memory.kmem.usage_in_bytes      (root グループのカーネルメモリの消費量を確認)
794624

これを試しに root グループを含まない親子関係の "01" グループの下の "0101" グループで試してみると,

root@enterprise:/sys/fs/cgroup/memory/01/0101# cat memory.kmem.usage_in_bytes
8192
root@enterprise:/sys/fs/cgroup/memory/01/0101# cat ../memory.kmem.usage_in_bytes
622592
  (ここで何か処理を実行)
root@enterprise:/sys/fs/cgroup/memory/01/0101# cat memory.kmem.usage_in_bytes
151552
root@enterprise:/sys/fs/cgroup/memory/01/0101# cat ../memory.kmem.usage_in_bytes
765952
  • 0101 (子) グループの増加量は 151552 - 8192 = 143360
  • 01 (親) グループの増加量は 765952 - 622592 = 143360

と親子で同じ量だけ増加しているのがわかります.

使用量のカウントは制限を設定してから始まる

グループを作成しても使用量のカウントはされません.memory.kmem.usage_in_bytes は 0 のまま.制限を設定してからここに数字が出てきます.以下のように何か行う度に memory.usage_in_bytes の値は変化しますが,memory.kmem.usage_in_bytes は 0 のままです.

root@enterprise:/sys/fs/cgroup/memory# mkdir 01
root@enterprise:/sys/fs/cgroup/memory# cd 01
root@enterprise:/sys/fs/cgroup/memory/01# echo 24158 > tasks 
root@enterprise:/sys/fs/cgroup/memory/01# cat memory.kmem.usage_in_bytes 
0
root@enterprise:/sys/fs/cgroup/memory/01# cat memory.usage_in_bytes 
290816
  (ここで何か処理を実行)
root@enterprise:/sys/fs/cgroup/memory/01# cat memory.kmem.usage_in_bytes 
0
root@enterprise:/sys/fs/cgroup/memory/01# cat memory.usage_in_bytes 
479232

制限してから試すと以下のようにきちんと memory.kmem.usage_in_bytes も増えているのがわかります.

root@enterprise:/sys/fs/cgroup/memory/01# cat tasks 
root@enterprise:/sys/fs/cgroup/memory/01# echo 100M > memory.kmem.limit_in_bytes 
root@enterprise:/sys/fs/cgroup/memory/01# cat memory.kmem.limit_in_bytes
104857600
root@enterprise:/sys/fs/cgroup/memory/01# echo 25535 > tasks 
  (何か処理を実行)
root@enterprise:/sys/fs/cgroup/memory/01# cat memory.kmem.usage_in_bytes
155648
root@enterprise:/sys/fs/cgroup/memory/01# cat memory.usage_in_bytes
364544

一度カウントが始まると,そのタスクがなくなっても,グループが削除されるまでカウント自体は行われます.

また,一度制限を設定し,その後 echo -1 > memory.kmem.limit_in_bytes を実行して制限がない状態にしてもカウントは行われます.

カーネルメモリに対する制限が設定できないケース

以下のような状態の時はカーネルメモリに対する制限は設定出来ません

既にグループが子グループを持っている場合

子グループを作成した後に制限を設定しようとすると以下のようになります.

root@enterprise:/sys/fs/cgroup/memory# mkdir 01
root@enterprise:/sys/fs/cgroup/memory# cd 01
root@enterprise:/sys/fs/cgroup/memory/01# echo 1 > memory.use_hierarchy
root@enterprise:/sys/fs/cgroup/memory/01# mkdir 0101
root@enterprise:/sys/fs/cgroup/memory/01# echo 1M > memory.kmem.limit_in_bytes
bash: echo: 書き込みエラー: デバイスもしくはリソースがビジー状態です
既にグループにタスクが登録されている場合

グループにタスクを登録した後に制限を設定しようとしても以下のようにエラーになります.

root@enterprise:/sys/fs/cgroup/memory# mkdir 01
root@enterprise:/sys/fs/cgroup/memory# cd 01
root@enterprise:/sys/fs/cgroup/memory/01# echo 23055 > tasks 
root@enterprise:/sys/fs/cgroup/memory/01# echo 1M > memory.kmem.limit_in_bytes
bash: echo: 書き込みエラー: デバイスもしくはリソースがビジー状態です

これは mm/memcontrol.c の memcg_update_kmem_limit 関数を見ればわかります (-EBUSYが返るように書かれています).

static int memcg_update_kmem_limit(struct cgroup *cont, u64 val)
{
	int ret = -EINVAL;
#ifdef CONFIG_MEMCG_KMEM
	bool must_inc_static_branch = false;

	struct mem_cgroup *memcg = mem_cgroup_from_cont(cont);

	cgroup_lock();
	mutex_lock(&set_limit_mutex);
	if (!memcg->kmem_account_flags && val != RESOURCE_MAX) {
		if (cgroup_task_count(cont) || (memcg->use_hierarchy &&
						!list_empty(&cont->children))) { ← (ここ)
			ret = -EBUSY;
			goto out;
		}

以上の内側の if 文で

  • task に登録されているものがあるかどうか (cgroup_task_count() 関数)
  • 階層構造をサポートしていて,子グループがいるかどうか

のチェックを行っているのが分かるかと思います.

カーネルメモリの使用量はグローバルの使用量にも足される

memory.kmem.usage_in_bytes に足された値は,同時に memory.usage_in_bytes にも足されます.

これは mm/memcontrol.c の memcg_charge_kmem 関数を見ればそのようになっているのがわかります (多分 ^^;).

static int memcg_charge_kmem(struct mem_cgroup *memcg, gfp_t gfp, u64 size)
{
	struct res_counter *fail_res;
	struct mem_cgroup *_memcg;
	int ret = 0;
	bool may_oom;

	ret = res_counter_charge(&memcg->kmem, size, &fail_res);      ←ここでカーネルメモリのカウンタに足す
	if (ret)
		return ret;

	/*
	 * Conditions under which we can wait for the oom_killer. Those are
	 * the same conditions tested by the core page allocator
	 */
	may_oom = (gfp & __GFP_FS) && !(gfp & __GFP_NORETRY);

	_memcg = memcg;
	ret = __mem_cgroup_try_charge(NULL, gfp, size >> PAGE_SHIFT,  ←ここでグローバルなカウンタ (memory.usage_in_bytes) にも足す
				      &_memcg, may_oom);
  : (以下略)

カーネルメモリの制限はしたくないけど,使用量はモニタリングしたい場合,

  • memory.limit_in_bytes < memory.kmem.limit_in_bytes

という設定を行えば実現できます (前述のようにカーネルメモリの制限を設定しないとカウントされないため).(一度制限を設定したあとに -1 を書いて制限を解除してもいいけど)

__mem_cgroup_try_charge 関数についてはもう少し調べたい所.

(続くかも?)