kk Blog —— 通用基础


date [-d @int|str] [+%s|"+%F %T"]
netstat -ltunp
sar -n DEV 1

memory子系统

http://www.cnblogs.com/lisperl/archive/2012/04/28/2474872.html

memory 子系统可以设定 cgroup 中任务使用的内存限制,并自动生成由那些任务使用的内存资源报告。memory子系统是通过linux的resource counter机制实现的。下面我们就先来看一下resource counter机制。

resource counter是内核为子系统提供的一种资源管理机制。这个机制的实现包括了用于记录资源的数据结构和相关函数。Resource counter定义了一个res_counter的结构体来管理特定资源,定义如下:

1
2
3
4
5
6
7
8
9
struct res_counter {
	unsigned long long usage;
	unsigned long long max_usage;
	unsigned long long limit;
	unsigned long long soft_limit;
	unsigned long long failcnt; /*
	spinlock_t lock;
	struct res_counter *parent;
};

Usage用于记录当前已使用的资源,max_usage用于记录使用过的最大资源量,limit用于设置资源的使用上限,进程组不能使用超过这个限制的资源,soft_limit用于设定一个软上限,进程组使用的资源可以超过这个限制,failcnt用于记录资源分配失败的次数,管理可以根据这个记录,调整上限值。Parent指向父节点,这个变量用于处理层次性的资源管理。

除了这个关键的数据结构,resource counter还定义了一系列相关的函数。下面我们来看几个关键的函数。

1
2
3
4
5
6
7
void res_counter_init(struct res_counter *counter, struct res_counter *parent)
{
	spin_lock_init(&counter->lock);
	counter->limit = RESOURCE_MAX;
	counter->soft_limit = RESOURCE_MAX;
	counter->parent = parent;
}

这个函数用于初始化一个res_counter。

第二个关键的函数是int res_counter_charge(struct res_counter *counter, unsigned long val, struct res_counter **limit_fail_at)。当资源将要被分配的时候,资源就要被记录到相应的res_counter里。这个函数作用就是记录进程组使用的资源。在这个函数中有:

1
2
3
4
5
6
7
8
9
for (c = counter; c != NULL; c = c->parent) {
	spin_lock(&c->lock);
	ret = res_counter_charge_locked(c, val);
	spin_unlock(&c->lock);
	if (ret < 0) {
		*limit_fail_at = c;
		goto undo;
	}
}

在这个循环里,从当前res_counter开始,从下往上逐层增加资源的使用量。我们来看一下res_counter_charge_locked这个函数,这个函数顾名思义就是在加锁的情况下增加使用量。实现如下:

1
2
3
4
5
6
7
8
9
10
11
{
	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;
}

首先判断是否已经超过使用上限,如果是的话就增加失败次数,返回相关代码;否则就增加使用量的值,如果这个值已经超过历史最大值,则更新最大值。

第三个关键的函数是void res_counter_uncharge(struct res_counter *counter, unsigned long val)。当资源被归还到系统的时候,要在相应的res_counter减轻相应的使用量。这个函数作用就在于在于此。实现如下:

1
2
3
4
5
for (c = counter; c != NULL; c = c->parent) {
	spin_lock(&c->lock);
	res_counter_uncharge_locked(c, val);
	spin_unlock(&c->lock);
}

从当前counter开始,从下往上逐层减少使用量,其中调用了res_counter_uncharge_locked,这个函数的作用就是在加锁的情况下减少相应的counter的使用量。

有这些数据结构和函数,只需要在内核分配资源的时候,植入相应的charge函数,释放资源时,植入相应的uncharge函数,就能实现对资源的控制了。

介绍完resource counter,我们再来看memory子系统是利用resource counter实现对内存资源的管理的。

memory子系统定义了一个叫mem_cgroup的结构体来管理cgroup相关的内存使用信息,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct mem_cgroup {
	struct cgroup_subsys_state css;
	struct res_counter res;
	struct res_counter memsw;
	struct mem_cgroup_lru_info info;
	spinlock_t reclaim_param_lock;
	int prev_priority;
	int last_scanned_child;
	bool use_hierarchy;
	atomic_t oom_lock;
	atomic_t refcnt;
	unsigned int swappiness;
	int oom_kill_disable;
	bool memsw_is_minimum;
	struct mutex thresholds_lock;
	struct mem_cgroup_thresholds thresholds;
	struct mem_cgroup_thresholds memsw_thresholds;
	struct list_head oom_notify;
	unsigned long  move_charge_at_immigrate;
	struct mem_cgroup_stat_cpu *stat;
};

跟其他子系统一样,mem_cgroup也包含了一个cgroup_subsys_state成员,便于task或cgroup获取mem_cgroup。

mem_cgroup中包含了两个res_counter成员,分别用于管理memory资源和memory+swap资源,如果memsw_is_minimum为true,则res.limit=memsw.limit,即当进程组使用的内存超过memory的限制时,不能通过swap来缓解。

use_hierarchy则用来标记资源控制和记录时是否是层次性的。

oom_kill_disable则表示是否使用oom-killer。

oom_notify指向一个oom notifier event fd链表。

另外memory子系统还定义了一个叫page_cgroup的结构体:

1
2
3
4
5
6
struct page_cgroup {
	unsigned long flags;
	struct mem_cgroup *mem_cgroup;
	struct page *page;
	struct list_head lru; /* per cgroup LRU list */
};

此结构体可以看作是mem_map的一个扩展,每个page_cgroup都和所有的page关联,而其中的mem_cgroup成员,则将page与特定的mem_cgroup关联起来。

我们知道在linux系统中,page结构体是用来管理物理页框的,一个物理页框对应一个page结构体,而每个进程中的task_struct中都有一个mm_struct来管理进程的内存信息。每个mm_struct知道它属于的进程,进而知道所属的mem_cgroup,而每个page都知道它属于的page_cgroup,进而也知道所属的mem_cgroup,而内存使用量的计算是按cgroup为单位的,这样以来,内存资源的管理就可以实现了。

memory子系统既然是通过resource counter实现的,那肯定会在内存分配给进程时进行charge操作的。下面我们就来看一下这些charge操作:

1.page fault发生时,有两种情况内核需要给进程分配新的页框。一种是进程请求调页(demand paging),另一种是copy on write。内核在handle_pte_fault中进行处理。其中,do_linear_fault处理pte不存在且页面线性映射了文件的情况,do_anonymous_page处理pte不存在且页面没有映射文件的情况,do_nonlinear_fault处理pte存在且页面非线性映射文件的情况,do_wp_page则处理copy on write的情况。其中do_linear_fault和do_nonlinear_fault都会调用do_fault来处理。Memory子系统则do_fault、do_anonymous_page、do_wp_page植入mem_cgroup_newpage_charge来进行charge操作。

2.内核在handle_pte_fault中进行处理时,还有一种情况是pte存在且页又没有映射文件。这种情况说明页面之前在内存中,但是后面被换出到swap空间了。内核用do_swap_page函数处理这种情况,memory子系统在do_swap_page加入了mem_cgroup_try_charge_swapin函数进行charge。mem_cgroup_try_charge_swapin是处理页面换入时的charge的,当执行swapoff系统调用(关掉swap空间),内核也会执行页面换入操作,因此mem_cgroup_try_charge_swapin也被植入到了相应的函数中。

3.当内核将page加入到page cache中时,也需要进行charge操作,mem_cgroup_cache_charge函数正是处理这种情况,它被植入到系统处理page cache的add_to_page_cache_locked函数中。

4.最后mem_cgroup_prepare_migration是用于处理内存迁移中的charge操作。

除了charge操作,memory子系统还需要处理相应的uncharge操作。下面我们来看一下uncharge操作:

1.mem_cgroup_uncharge_page用于当匿名页完全unmaped的时候。但是如果该page是swap cache的话,uncharge操作延迟到mem_cgroup_uncharge_swapcache被调用时执行。

2.mem_cgroup_uncharge_cache_page用于page cache从radix-tree删除的时候。但是如果该page是swap cache的话,uncharge操作延迟到mem_cgroup_uncharge_swapcache被调用时执行。

3.mem_cgroup_uncharge_swapcache用于swap cache从radix-tree删除的时候。Charge的资源会被算到swap_cgroup,如果mem+swap controller被禁用了,就不需要这样做了。

4.mem_cgroup_uncharge_swap用于swap_entry的引用数减到0的时候。这个函数主要在mem+swap controller可用的情况下使用的。

5.mem_cgroup_end_migration用于内存迁移结束时相关的uncharge操作。

Charge函数最终都是通过调用mem_cgroup_try_charge来实现的。在mem_cgroup_try_charge函数中,调用res_counter_charge(&mem->res, csize, &fail_res)对memory进行charge,调用res_counter_charge(&mem->memsw, csize, &fail_res)对memory+swap进行charge。

Uncharge函数最终都是通过调用do_uncharge来实现的。在do_uncharge中,分别调用res_counter_uncharge(&mem->res,PAGE_SIZE)和res_counter_uncharge(&mem->memsw, PAGE_SIZE)来uncharge memory和memory+swap。

跟其他子系统一样,memory子系统也实现了一个cgroup_subsys。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct cgroup_subsys mem_cgroup_subsys = {
	.name = "memory",
	.subsys_id = mem_cgroup_subsys_id,
	.create = mem_cgroup_create,
	.pre_destroy = mem_cgroup_pre_destroy,
	.destroy = mem_cgroup_destroy,
	.populate = mem_cgroup_populate,
	.can_attach = mem_cgroup_can_attach,
	.cancel_attach = mem_cgroup_cancel_attach,
	.attach = mem_cgroup_move_task,
	.early_init = 0,
	.use_id = 1,
};

Memory子系统中重要的文件有

1
2
3
4
5
6
7
memsw.limit_in_bytes
{
	.name = "memsw.limit_in_bytes",
	.private = MEMFILE_PRIVATE(_MEMSWAP, RES_LIMIT),
	.write_string = mem_cgroup_write,
	.read_u64 = mem_cgroup_read,
},

这个文件用于设定memory+swap上限值。

Limit_in_bytes

1
2
3
4
5
6
{
	.name = "limit_in_bytes",
	.private = MEMFILE_PRIVATE(_MEM, RES_LIMIT),
	.write_string = mem_cgroup_write,
	.read_u64 = mem_cgroup_read,
},

这个文件用于设定memory上限值。

ns子系统

http://www.cnblogs.com/lisperl/archive/2012/04/26/2471776.html

ns子系统是一个比较特殊的子系统。特殊在哪儿呢,首先ns子系统没有自己的控制文件,其次ns子系统没有属于自己的状态信息,这点从ns子系统的ns_cgroup的定义可以看出:

1
2
3
struct ns_cgroup {
	struct cgroup_subsys_state css;
};

它只有一个cgroup_subsys_state成员。

最后ns子系统的实现也比较简单,只是提供了一个ns_cgroup_clone函数,在copy_process和unshare_nsproxy_namespaces被调用。而ns_cgroup_clone函数本身的实现也很简单,只是在当前的cgroup下创建了一个子cgroup,该子cgroup完全clone了当前cgroup的信息,然后将当前的进程移到新建立的cgroup中。

这样看来,好像ns子系统没什么意义,其实不然。要想了解ns子系统的意义,就要分析一下ns_cgroup_clone被调用的时机了。我们来看copy_process中的代码:

1
2
3
4
5
if (current->nsproxy != p->nsproxy) {
	retval = ns_cgroup_clone(p, pid);
	if (retval)
		goto bad_fork_free_pid;
}

copy_process是在do_fork中被调用的,作用在于为子进程复制父进程的相关信息。这段意思就是当前进程(即父进程)和子进程的命名空间不同时,调用ns_cgroup_clone。这样以来,ns子系统的作用就清楚了,ns子系统实际上是提供了一种同命名空间的进程聚类的机制。具有相同命名空间的进程会在相同cgroup中。

那什么时候,父进程fork出的子进程会拥有不同的命名空间呢,这就设计到了Linux的命名空间的机制了,在这里就不详细讲了。简单说来就是,在调用fork时,加入了特殊flag(比如NEWPID,NEWNS)时,内核会为子进程创建不同的命令空间。

除了这种情况外,ns_cgroup_clone在unshare_nsproxy_namespaces用到了。unshare_nsproxy_namespaces函数被sys_unshare调用,实际上是对unshare系统调用的实现。当指定相应标记时,unshare系统调用会为调用的进程创建不同的命名空间,因此调用ns_cgroup_clone为其创建新的cgroup。

freezer子系统

http://www.cnblogs.com/lisperl/archive/2012/04/25/2469587.html

freezer子系统用于挂起和恢复cgroup中的进程。freezer有一个控制文件:freezer.state,将FROZEN写入该文件,可以将cgroup中的进程挂起,将THAWED写入该文件,可以将已挂起的进程恢复。该文件可能读出的值有三种,其中两种就是前面已提到的FROZEN和THAWED,分别代表进程已挂起和已恢复(正常运行),还有一种可能的值为FREEZING,显示该值表示该cgroup中有些进程现在不能被frozen。当这些不能被frozen的进程从该cgroup中消失的时候,FREEZING会变成FROZEN,或者手动将FROZEN或THAWED写入一次。

Freezer子系统用来管理cgroup状态的数据结构:

1
2
3
4
5
struct freezer {
	struct cgroup_subsys_state css;
	enum freezer_state state;
	spinlock_t lock; /* protects _writes_ to state */
};

其中内嵌一个cgroup_subsys_state,便于从cgroup或task获得freezer结构,另一个字段存储cgroup当前的状态。

Freezer子系统是通过对freezer.state文件进行写入来控制进程的,那我们就从这个文件的cftype定义出发。

1
2
3
4
5
6
7
static struct cftype files[] = {
	{
		.name = "state",
		.read_seq_string = freezer_read,
		.write_string = freezer_write,
	},
};

从文件读取是freezer_read实现的,该函数比较简单,主要就是从freezer结构体从读出状态,但是对FREEZING状态做了特殊处理:

1
2
3
4
5
6
7
state = freezer->state;
if (state == CGROUP_FREEZING) {
	/* We change from FREEZING to FROZEN lazily if the cgroup was
	 * only partially frozen when we exitted write. */
	update_freezer_state(cgroup, freezer);
	state = freezer->state;
}

如果是FREEZING状态,则需要更新状态(因为之前不能frozen的进程可能已经不在了)。我们来看update_freezer_state:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cgroup_iter_start(cgroup, &it);
while ((task = cgroup_iter_next(cgroup, &it))) {
	ntotal++;
	if (is_task_frozen_enough(task))
		nfrozen++;
}
 
/*
 * Transition to FROZEN when no new tasks can be added ensures
 * that we never exist in the FROZEN state while there are unfrozen
 * tasks.
 */
if (nfrozen == ntotal)
	freezer->state = CGROUP_FROZEN;
else if (nfrozen > 0)
	freezer->state = CGROUP_FREEZING;
else
	freezer->state = CGROUP_THAWED;
cgroup_iter_end(cgroup, &it);

这里对该cgroup所有的进程迭代了一遍,分别统计进程数和已经frozen的进程数,然后根据统计结果改变状态。

下面我们来看对freezer.state写入的情况,该情况由freezer_write来处理,该函数中从写入值获取目标状态,然后调用freezer_change_state(cgroup, goal_state)来完成操作。在freezer_change_state中,根据goal_state分别调用不同的实现函数:

1
2
3
4
5
6
7
8
9
10
switch (goal_state) {
case CGROUP_THAWED:
	unfreeze_cgroup(cgroup, freezer);
	break;
case CGROUP_FROZEN:
	retval = try_to_freeze_cgroup(cgroup, freezer);
	break;
default:
	BUG();
}

我们先来看frozen的情况,该情况由try_to_freeze_cgroup来处理,该函数中有:

1
2
3
4
5
6
7
8
9
10
11
12
13
freezer->state = CGROUP_FREEZING;
cgroup_iter_start(cgroup, &it);
while ((task = cgroup_iter_next(cgroup, &it))) {
	if (!freeze_task(task, true))
		continue;
	if (is_task_frozen_enough(task))
		continue;
	if (!freezing(task) && !freezer_should_skip(task))
		num_cant_freeze_now++;
}
cgroup_iter_end(cgroup, &it);
 
return num_cant_freeze_now ? -EBUSY : 0;

首先将当前状态设成CGROUP_FREEZING,然后对cgroup中的进程进行迭代,while循环中对进程进行freeze操作,如果成功直接进行下一次迭代,如果不成功则进行进一步的判断,如果是进程已经frozen了,那也直接进行下一次迭代,如果不是,则进行计数。最后根据计数结果进行返回,如果所有进程都顺利frozen,则返回0,否则返回-EBUSY表示有进程不能被frozen。

下面我们来看free_task这个函数,在这个函数中对task进行freeze操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (!freezing(p)) {
	rmb();
	if (frozen(p))
		return false;
 
	if (!sig_only || should_send_signal(p))
		set_freeze_flag(p);
	else
		return false;
}
 
if (should_send_signal(p)) {
	if (!signal_pending(p))
		fake_signal_wake_up(p);
} else if (sig_only) {
	return false;
} else {
	wake_up_state(p, TASK_INTERRUPTIBLE);
}

return true;

首先检查进程是不是已经被标记为正在freezing,如果不是再做判断。如果进程已经被frozen,则返回false。如果进程不是sig_only的或者可以发送信号(即进程无PF_FREEZER_NOSIG 标记),则设置进程的TIF_FREEZE标记。

然后根据进程是否有PF_FREEZER_NOSIG 标记进行进一步处理,如果无这个标记,则给进程发送一个信号,唤醒进程,让进程处理TIF_FREEZE,即进行freeze操作,如果有这个标记,则如果进程是sig_only的,返回false(即不能完成free操作),否则直接唤醒进程去处理TIF_FREEZE。

总结一下,对于我们这个freezer子系统的调用来说,sig_only=true,那么能成功的执行过程就是set_freeze_flag(p)->fake_signal_wake_up(p)。

下面我们来看thaw 进程的情况,该情况由unfreeze_cgroup处理,在unfreeze_cgroup中有

1
2
3
4
5
6
7
cgroup_iter_start(cgroup, &it);
while ((task = cgroup_iter_next(cgroup, &it))) {
	thaw_process(task);
}
cgroup_iter_end(cgroup, &it);
 
freezer->state = CGROUP_THAWED;

对该cgroup中所有的进程调用thaw_process,我们来看thaw_process。该函数中有:

1
2
3
4
5
if (__thaw_process(p) == 1) {
	task_unlock(p);
	wake_up_process(p);
	return 1;
}

其中__thaw_process中

1
2
3
4
5
if (frozen(p)) {
	p->flags &= ~PF_FROZEN;
	return 1;
}
clear_freeze_flag(p);

如果进程已经frozen,则清掉其frozen标记,如果不是的话,说明进程已经设置了TIF_FREEZE,但还没有frozen,所以只需要清掉TIF_FREEZE即可。

回到thaw_process中,清掉了相关标记后,只需要唤醒进程,然后内核会自动处理。

最后,我们再来看看freezer子系统结构体的定义:

1
2
3
4
5
6
7
8
9
10
11
struct cgroup_subsys freezer_subsys = {
	.name = "freezer",
	.create = freezer_create,
	.destroy = freezer_destroy,
	.populate = freezer_populate,
	.subsys_id = freezer_subsys_id,
	.can_attach = freezer_can_attach,
	.attach = NULL,
	.fork = freezer_fork,
	.exit = NULL,
};

这里说一下can_attach,can_attach是在一个进程加入到一个cgroup之前调用的,检查是否可以attach,freezer_can_attach中对cgroup当前的状态做了检查,如果是frozen就返回错误,这说明不能将一个进程加入到一个frozen的cgroup。