kk Blog —— 通用基础


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

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。

devices子系统

http://www.cnblogs.com/lisperl/archive/2012/04/24/2468170.html

使用devices 子系统可以允许或者拒绝cgroup中的进程访问设备。devices子系统有三个控制文件:devices.allow,devices.deny,devices.list。devices.allow用于指定cgroup中的进程可以访问的设备,devices.deny用于指定cgroup中的进程不能访问的设备,devices.list用于报告cgroup中的进程访问的设备。devices.allow文件中包含若干条目,每个条目有四个字段:type、major、minor 和 access。type、major 和 minor 字段中使用的值对应 Linux 分配的设备。

type指定设备类型:
a - 应用所有设备,可以是字符设备,也可以是块设备
b- 指定块设备
c - 指定字符设备

major和minor指定设备的主次设备号。

access 则指定相应的权限:
r - 允许任务从指定设备中读取
w - 允许任务写入指定设备
m - 允许任务生成还不存在的设备文件

devices子系统是通过提供device whilelist 来实现的。与其他子系统一样,devices子系统也有一个内嵌了cgroup_subsystem_state的结构来管理资源。在devices子系统中,这个结构是:

1
2
3
4
struct dev_cgroup {
	struct cgroup_subsys_state css;
	struct list_head whitelist;
};

这个结构体除了通用的cgroup_subsystem_state之外,就只有一个链表指针,而这个链表指针指向了该cgroup中的进程可以访问的devices whilelist。

下面我们来看一下devices子系统如何管理whilelist。在devices子系统中,定义了一个叫dev_whitelist_item的结构来管理可以访问的device,对应于devices.allow中的一个条目。这个结构体的定义如下:

1
2
3
4
5
6
7
struct dev_whitelist_item {
	u32 major, minor;
	short type;
	short access;
	struct list_head list;
	struct rcu_head rcu;
};

major,minor用于指定设备的主次设备号,type用于指定设备类型,type取值可以是:

1
2
3
#define DEV_BLOCK 1
#define DEV_CHAR  2
#define DEV_ALL   4 

对应于之前devices.allow文件中三种情况。

access用于相应的访问权限,access取值可以是:

1
2
3
#define ACC_MKNOD 1
#define ACC_READ  2
#define ACC_WRITE 4

也和之前devices.allow文件中的情况对应。

List字段用于将该结构体连到相应的dev_cgroup中whitelist指向的链表。

通过以上数据结构,devices子系统就能管理一个cgroup的进程可以访问的devices了。 光有数据结构还不行,还要有具体实现才行。devices子系统通过实现两个函数供内核调用来实现控制cgroup中的进程能够访问的devices。首先我们来第一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int devcgroup_inode_permission(struct inode *inode, int mask)
{
	struct dev_cgroup *dev_cgroup;
	struct dev_whitelist_item *wh;
 
	dev_t device = inode->i_rdev;
	if (!device)
		return 0;
	if (!S_ISBLK(inode->i_mode) && !S_ISCHR(inode->i_mode))
		return 0;
 
	rcu_read_lock();
 
	dev_cgroup = task_devcgroup(current);
 
	list_for_each_entry_rcu(wh, &dev_cgroup->whitelist, list) {
		if (wh->type & DEV_ALL)
			goto found;
		if ((wh->type & DEV_BLOCK) && !S_ISBLK(inode->i_mode))
			continue;
		if ((wh->type & DEV_CHAR) && !S_ISCHR(inode->i_mode))
			continue;
		if (wh->major != ~0 && wh->major != imajor(inode))
			continue;
		if (wh->minor != ~0 && wh->minor != iminor(inode))
			continue;
 
		if ((mask & MAY_WRITE) && !(wh->access & ACC_WRITE))
			continue;
		if ((mask & MAY_READ) && !(wh->access & ACC_READ))
			continue;
		found:
			rcu_read_unlock();
		return 0;
	}
 
	rcu_read_unlock();
 
	return -EPERM;
}

我们来简单分析一下这个函数,首先如果该inode对应的不是devices,直接返回0,如果既不是块设备也不是字符设备,也返回0,因为devices只控制块设备和字符设备的访问,其他情况不管。接着获得当前进程的dev_cgroup,然后在dev_cgroup中whitelist指针的链表中查找,如果找到对应设备而且mask指定的权限和设备的权限一致就返回0,如果没有找到就返回错误。

这个函数是针对inode节点存在的情况,通过对比权限来控制cgroup中的进程能够访问的devices。还有一个情况是inode不存在,在这种情况下,一个进程要访问一个设备就必须通过mknod建立相应的设备文件。为了达到对这种情况的控制,devices子系统导出了第二个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int devcgroup_inode_mknod(int mode, dev_t dev)
{
	struct dev_cgroup *dev_cgroup;
	struct dev_whitelist_item *wh;
 
	if (!S_ISBLK(mode) && !S_ISCHR(mode))
		return 0;
 
	rcu_read_lock();
 
	dev_cgroup = task_devcgroup(current);
 
	list_for_each_entry_rcu(wh, &dev_cgroup->whitelist, list) {
		if (wh->type & DEV_ALL)
			goto found;
		if ((wh->type & DEV_BLOCK) && !S_ISBLK(mode))
			continue;
		if ((wh->type & DEV_CHAR) && !S_ISCHR(mode))
			continue;
		if (wh->major != ~0 && wh->major != MAJOR(dev))
			continue;
		if (wh->minor != ~0 && wh->minor != MINOR(dev))
			continue;
 
		if (!(wh->access & ACC_MKNOD))
			continue;
		found:
			rcu_read_unlock();
		return 0;
	}
 
	rcu_read_unlock();
 
	return -EPERM;
}

这个函数的实现跟第一个函数类似,这里就不赘述了。

下面我们再来看一下devices子系统本身的一些东西。跟其他子系统一样,devices同样实现了一个cgroup_subsys:

1
2
3
4
5
6
7
8
struct cgroup_subsys devices_subsys = {
	.name = "devices",
	.can_attach = devcgroup_can_attach,
	.create = devcgroup_create,
	.destroy = devcgroup_destroy,
	.populate = devcgroup_populate,
	.subsys_id = devices_subsys_id,
};

devices相应的三个控制文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static struct cftype dev_cgroup_files[] = {
	{
		.name = "allow",
		.write_string  = devcgroup_access_write,
		.private = DEVCG_ALLOW,
	},
	{
		.name = "deny",
		.write_string = devcgroup_access_write,
		.private = DEVCG_DENY,
	},
	{
		.name = "list",
		.read_seq_string = devcgroup_seq_read,
		.private = DEVCG_LIST,
	},
};

其中allow和deny都是通过devcgroup_access_write实现的,只是通过private字段区分,因为二者的实现逻辑有相同的地方。devcgroup_access_write最终通过调用devcgroup_update_access来实现。在devcgroup_update_access根据写入的内容构造一个dev_whitelist_item ,然后根据文件类型做不同的处理:

1
2
3
4
5
6
7
8
9
10
11
switch (filetype) {
	case DEVCG_ALLOW:
		if (!parent_has_perm(devcgroup, &wh))
			return -EPERM;
		return dev_whitelist_add(devcgroup, &wh);
	case DEVCG_DENY:
		dev_whitelist_rm(devcgroup, &wh);
		break;
	default:
		return -EINVAL;
}

allow的话,就将item加入whitelist,deny的话,就将item从whitelist中删去。