TenForward

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

Linux 3.3 の新機能 Per-cgroup TCP buffer limits (3)

Linux 3.3 の新機能 Per-cgroup TCP buffer limits (2) - TenForwardの日記 の続編.かなり間が開いてしまって,自分でも忘れかけてます.前回と同様に,カーネルのコードを読んだりしていますが,私はその辺りの知識は殆どありませんので,間違いがある場合は指摘していただけるとありがたいです.今回は特に自信がないので是非間違ってるよとか教えていただけるとありがたいです.

前回は memory.kmem.tcp.limit_in_bytes の制限がどのように効いてくるのかについて調べました.

今回は 第 1 回 で実験した結果,謎の結果となっていた kmem.tcp.usage_in_bytes を調べてみました.

kmem.tcp.usage_in_bytes の値の取得

kmem.tcp.usage_in_bytes の値は net/ipv4/tcp_memcontrol.c の最初の方の定義で以下のように定義されており,値は tcp_cgroup_read 関数で取得するとなっています.

static struct cftype tcp_files[] = {
    : (snip)
	{
		.name = "kmem.tcp.usage_in_bytes",
		.read_u64 = tcp_cgroup_read,
		.private = RES_USAGE,
	},
    : (snip)
};

tcp_cgroup_read 内では RES_USAGE を取得する場合は tcp_read_usage を呼んでいます.

static u64 tcp_cgroup_read(struct cgroup *cont, struct cftype *cft)
{
    : (snip)
	case RES_USAGE:
		val = tcp_read_usage(memcg);
		break;
    : (snip)
}

そして tcp_read_usage です.最後の return の所で res_counter_read_u64 によって res_counter (&tcp->tcp_memory_allocatedです) の RES_USAGE を取得しています.

static u64 tcp_read_usage(struct mem_cgroup *memcg)
{
    : (snip)
	return res_counter_read_u64(&tcp->tcp_memory_allocated, RES_USAGE);
}

長々と書きましたが,単に res_counter 構造体 (&tcp->tcp_memory_allocated) の usage の値を取ってきているだけです.

usage の値の変化させる関数へ到達するまで

では,Per-cgroup TCP buffer limits 機能の res_counter の usage 値はどのように加算されているのか,を追ってみました.

カーネル付属文書の cgroups/resource_counter.txt を見ると res_counter_charge か res_counter_charge_locked 辺りを使うみたいです.そこで res_counter_charge で grep をかけると,include/net/sock.h 内の memcg_memory_allocated_add というなんか加算してるっぽい関数が出てきます.res_counter_charge でも res_counter_charge_locked でもなく res_counter_charge_nofail という名前ですが.

static inline void memcg_memory_allocated_add(struct cg_proto *prot,
					      unsigned long amt,
					      int *parent_status)
{
	struct res_counter *fail;
	int ret;
 
	ret = res_counter_charge_nofail(prot->memory_allocated,
					amt << PAGE_SHIFT, &fail);
	if (ret < 0)
		*parent_status = OVER_LIMIT;
}

memcg_memory_allocated_add はどこから呼ばれているかと言うと,同じく sock.h の sk_memory_allocated_add という関数内で呼ばれています.

static inline long
sk_memory_allocated_add(struct sock *sk, int amt, int *parent_status)
{
	struct proto *prot = sk->sk_prot;

	if (mem_cgroup_sockets_enabled && sk->sk_cgrp) {
		memcg_memory_allocated_add(sk->sk_cgrp, amt, parent_status);
		/* update the root cgroup regardless */
		atomic_long_add_return(amt, prot->memory_allocated);
		return memcg_memory_allocated_read(sk->sk_cgrp);
	}

	return atomic_long_add_return(amt, prot->memory_allocated);
}

sk_memory_allocated_add はというと,同じく sock.h の __sk_mem_schedule から呼ばれています.これは (たぶん),ソケットの wmem とか rmem とかが制限値に達してないかのチェックを行ったり,Pressure 状態に入ったり出たり,wmem と rmem の値を増やしたりということをしています.これがどこから呼び出されて... というのは調べてませんが,ソケットのメモリの増減なんかをしている所が元で res_counter の usage の値が変化していることはわかります.

usage の値を変化させる所

話を戻して res_counter_charge_nofail という,どうやら usage 値を増加させている所っぽい関数を見てみます.すると,ここの中から先に紹介した res_counter_charge_locked (res_counter-> lock を取得した状態で呼ぶ関数) が呼ばれています.

int res_counter_charge_nofail(struct res_counter *counter, unsigned long val,
			      struct res_counter **limit_fail_at)
{
	int ret, r;
	unsigned long flags;
	struct res_counter *c;
 
	r = ret = 0;
	*limit_fail_at = NULL;
	local_irq_save(flags);
	for (c = counter; c != NULL; c = c->parent) {
		spin_lock(&c->lock);
		r = res_counter_charge_locked(c, val);
		if (r)
			c->usage += val;
		spin_unlock(&c->lock);
		if (r < 0 && ret == 0) {
			*limit_fail_at = c;
			ret = r;
		}
	}
	local_irq_restore(flags);
 
	return ret;
}

res_counter_charge_locked の処理はわかりやすいです.現在の usage 値に,新たに足されるメモリ量を足した値が limit より大きくなると,failcnt のカウンタを 1 あげて,エラーとして -ENOMEM (Out of Memory) を返しています.そうでなければ,普通に usage に値を加えて,その値が max_usage より大きければ max_usage の値を新たに現在の値を加えた状態の usage 値にします.まさしく usage 値のカウントアップをしています.

int res_counter_charge_locked(struct res_counter *counter, unsigned long val)
{
	if (counter->usage + val > counter->limit) {
		counter->failcnt++;
		return -ENOMEM;
	}
 
	counter->usage += val;
	if (counter->usage > counter->max_usage)
		counter->max_usage = counter->usage;
	return 0;
}

さて,ENOMEM エラーが返った後の res_counter_charge_nofail 関数の処理を見てみると

		r = res_counter_charge_locked(c, val);
		if (r)
			c->usage += val;

おっと,エラーが返っても (= 0 以外が返っても),res_counter->usage の値に val を加えています.つまり max_usage の値が確定した後に,max_usage の値を更新することなく usage が変化しています.これが max_usage より usage が大きくなった原因ではないでしょうか.

ここで limit 超えているのになんで値を足してるのかというと,結局は呼び出し元の __sk_mem_schedule 関数で,この処理の後,通常の tcp_mem の 3 つの値を使った処理 (Pressure状態の判定とか limit の判定とか実際のメモリ確保とか) が行われているようです.limit を超えた場合の処理もされているようです.ここでも limit に達したからパツンと切るわけでなく,最小限のメモリは与えるとかの処理がされているのではないでしょうか (想像.送信と受信バッファで処理が違ったりします).この辺りまでは追ってませんが,送受信のバッファなので,最低限与える必要のあるバッファとかあるからでしょうか...

この後,limit を超えていた場合は __sk_mem_schedule で

suppress_allocation:

	if (kind == SK_MEM_SEND && sk->sk_type == SOCK_STREAM) {
		sk_stream_moderate_sndbuf(sk);

		/* Fail only if socket is _under_ its sndbuf.
		 * In this case we cannot block, so that we have to fail.
		 */
		if (sk->sk_wmem_queued + size >= sk->sk_sndbuf)
			return 1;
	}

	trace_sock_exceed_buf_limit(sk, prot, allocated);

	/* Alas. Undo changes. */
	sk->sk_forward_alloc -= amt * SK_MEM_QUANTUM;

	sk_memory_allocated_sub(sk, amt);

	return 0;

という limit になった時に飛んでくる処理があって,加えた量をマイナスした後,res_counter の値もちゃんとマイナスした値に減らされている (sk_memory_allocated_sub の処理を追うと res_counter_uncharge が呼ばれる) ので,max_usage と usage の間にあんなに差が出るのか良く分かりませんが,少なくとも値が異なる理由とは言えそうです.

static inline void
sk_memory_allocated_sub(struct sock *sk, int amt)
{
	struct proto *prot = sk->sk_prot;

	if (mem_cgroup_sockets_enabled && sk->sk_cgrp)
		memcg_memory_allocated_sub(sk->sk_cgrp, amt);

	atomic_long_sub(amt, prot->memory_allocated);
}
static inline void memcg_memory_allocated_sub(struct cg_proto *prot,
					      unsigned long amt)
{
	res_counter_uncharge(prot->memory_allocated, amt << PAGE_SHIFT);
}