kk Blog —— 通用基础

date [-d @int|str] [+%s|"+%F %T"]

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中删去。

cpu子系统

http://www.cnblogs.com/lisperl/archive/2012/04/23/2466721.html

cpu子系统用于控制cgroup中所有进程可以使用的cpu时间片。附加了cpu子系统的hierarchy下面建立的cgroup的目录下都有一个cpu.shares的文件,对其写入整数值可以控制该cgroup获得的时间片。例如:在两个 cgroup 中都将 cpu.shares 设定为 1 的任务将有相同的 CPU 时间,但在 cgroup 中将 cpu.shares 设定为 2 的任务可使用的 CPU 时间是在 cgroup 中将 cpu.shares 设定为 1 的任务可使用的 CPU 时间的两倍。

cpu子系统是通过Linux CFS调度器实现的。所以在介绍cpu子系统之前,先简单说一下CFS调度器。按照作者Ingo Molnar的说法:"CFS百分之八十的工作可以用一句话概括:CFS在真实的硬件上模拟了完全理想的多任务处理器"。在“完全理想的多任务处理器”下,每个进程都能同时获得CPU的执行时间。当系统中有两个进程时,CPU的计算时间被分成两份,每个进程获得50%。然而在实际的硬件上,当一个进程占用CPU时,其它进程就必须等待。所以CFS将惩罚当前进程,使其它进程能够在下次调度时尽可能取代当前进程。最终实现所有进程的公平调度。

CFS调度器将所有状态为RUNABLE的进程都被插入红黑树。在每个调度点,CFS调度器都会选择红黑树的最左边的叶子节点作为下一个将获得cpu的进程。 那红黑树的键值是怎么计算的呢?红黑树的键值是进程所谓的虚拟运行时间。一个进程的虚拟运行时间是进程时间运行的时间按整个红黑树中所有的进程数量normalized的结果。

每次tick中断,CFS调度器都要更新进程的虚拟运行时间,然后调整当前进程在红黑树中的位置,调整完成后如果发现当前进程不再是最左边的叶子,就标记need_resched 标志,中断返回时就会调用scheduler()完成进程切换。

最后再说一下,进程的优先级和进程虚拟运行时间的关系。前面提到了,每次tick中断,CFS调度器都要更新进程的虚拟运行时间。那这个时间是怎么计算的呢?CFS首先计算出进程的时间运行时间delta_exec,然后计算normalized后的delta_exec_weighted,最后再将delta_exec_weighted加到进程的虚拟运行时间上。跟进程优先级有关的就是delta_exec_weighted,delta_exec_weighted=delta_exec_weighted*NICE_0_LOAD/se->load,其中NICE_0_LOAD是个常量,而se->load跟进程的nice值成反比,因此进程优先级越高(nice值越小)则se->load越大,则计算出来的delta_exec_weighted越小,这样进程优先级高的进程就可以获得更多的cpu时间。

介绍完CFS调度器,我们开始介绍cpu子系统是如何通过CFS调度器实现的。CFS调度器不仅支持基于进程的调度,还支持基于进程组的组调度。CFS中定义了一个task_group的数据结构来管理组调度。

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
struct task_group {
	struct cgroup_subsys_state css;
 
#ifdef CONFIG_FAIR_GROUP_SCHED
	/* schedulable entities of this group on each cpu */
	struct sched_entity **se;
	/* runqueue "owned" by this group on each cpu */
	struct cfs_rq **cfs_rq;
	unsigned long shares;
#endif
 
#ifdef CONFIG_RT_GROUP_SCHED
	struct sched_rt_entity **rt_se;
	struct rt_rq **rt_rq;
 
	struct rt_bandwidth rt_bandwidth;
#endif
 
	struct rcu_head rcu;
	struct list_head list;
 
	struct task_group *parent;
	struct list_head siblings;
	struct list_head children;
};

task_group中内嵌了一个cgroup_subsys_state,也就是说进程可以通过cgroup_subsys_state来获取它所在的task_group,同样地cgroup也可以通过cgroup_subsys_state来获取它对应的task_group,因此进程和cgroup都存在了一组cgroup_subsys_state指针。

struct sched_entity **se是一个指针数组,存了一组指向该task_group在每个cpu的调度实体(即一个struct sched_entity)。

struct cfs_rq **cfs_rq也是一个指针数组,存在一组指向该task_group在每个cpu上所拥有的一个可调度的进程队列。

Parent、siblings和children三个指针负责将task_group 连成一颗树,这个跟cgroup树类似。

有了这个数据结构,我们来CFS在调度的时候是怎么处理进程组的。我们还是从CFS对tick中断的处理开始。

CFS对tick中断的处理在task_tick_fair中进行,在task_tick_fair中有:

1
2
3
4
for_each_sched_entity(se) {
	cfs_rq = cfs_rq_of(se);
	entity_tick(cfs_rq, se, queued);
}

我们首先来看一下在组调度的情况下,for_each_sched_entity是怎么定义的:

1
2
#define for_each_sched_entity(se) \
	for (; se; se = se->parent)

即从当前进程的se开始,沿着task_group树从下到上对se调用entity_tick,即更新各个se的虚拟运行时间。

在非组调度情况下,

1
2
#define for_each_sched_entity(se) \
	for (; se; se = NULL)

即只会对当前se做处理。

CFS处理完tick中断后,如果有必要就会进行调度,CFS调度是通过pick_next_task_fair函数选择下一个运行的进程的。在pick_next_task_fair中有:

1
2
3
4
5
do {
	se = pick_next_entity(cfs_rq);
	set_next_entity(cfs_rq, se);
	cfs_rq = group_cfs_rq(se);
} while (cfs_rq);

在这个循环中,首先从当前的队列选一个se,这个跟非组调度一样的(红黑树最左边的节点),再将se设置成下一个运行的se,再从该se获取该se对应的task_group拥有的cfs_rq(如果该se对应一个进程而非一个task_group的话,cfs_rq会变成NULL),继续这个过程直到cfs_rq为空,即当se对应的是一个进程。

简而言之,同一层的task_group跟进程被当成同样的调度实体来选择,当被选到的是task_group时,则对task_group的孩子节点重复这个过程,直到选到一个运行的进程。因此当设置一个cgroup的shares值时,该cgroup当作一个整体和剩下的进程或其他cgroup分享cpu时间。比如,我在根cgroup下建立cgroup A,将其shares值设1024,再建立cgroup B,将其shares设为2048,再将一些进程分别加入到这两个cgroup中,则长期调度的结果应该是A:B:C=1:2:1(即cpu占用时间,其中C是系统中为加入到A或B的进程)。

引起CFS调度的除了tick中断外,还有就是有新的进程加入可运行队列这种情况。CFS处理这个情况的函数是enqueue_task_fair,在enqueue_task_fair中有:

1
2
3
4
5
6
7
for_each_sched_entity(se) {
	if (se->on_rq)
		break;
	cfs_rq = cfs_rq_of(se);
	enqueue_entity(cfs_rq, se, flags);
	flags = ENQUEUE_WAKEUP;
}

我们前面已经看过for_each_sched_entity在组调度下的定义了,这里是将当前se和se的直系祖先节点都加入到红黑树(enqueue_entity),而在非组调度情况下,只需要将当前se本身加入即可。造成这种差异的原因,在于在pick_next_task_fair中选择se时,是从上往下的,如果一个se的祖先节点不在红黑树中,它永远都不会被选中。而在非组调度的情况下,se之间并没有父子关系,所有se都是平等独立,在pick_next_task_fair,第一次选中的肯定就是进程,不需要向下迭代。

类似的处理还发生在将一个se出列(dequeue_task_fair)和put_prev_task_fair中。

以上是cpu系统通过CFS调度器实现以cgroup为单位的cpu时间片分享,下面我们来看一下cpu子系统本身。cpu子系统通过一个cgroup_subsys结构体来管理:

1
2
3
4
5
6
7
8
9
10
struct cgroup_subsys cpu_cgroup_subsys = {
	.name = "cpu",
	.create = cpu_cgroup_create,
	.destroy = cpu_cgroup_destroy,
	.can_attach = cpu_cgroup_can_attach,
	.attach = cpu_cgroup_attach,
	.populate = cpu_cgroup_populate,
	.subsys_id = cpu_cgroup_subsys_id,
	.early_init = 1,
};

Cpu_cgroup_subsys其实是对抽象的cgroup_subsys的实现,其中的函数指针指向了特定于cpu子系统的实现。这里再说一下,Cgroups的整体设计。当用户使用cgroup文件系统,创建cgroup的时候,会调用cgroup目录操作的mkdir指针指向的函数,该函数调用了cgroup_create,而cgroup_create会根据该cgroup关联的子系统,分别调用对应的子系统实现的create指针指向的函数。即做了两次转换,一次从系统通用命令到cgroup文件系统,另一次从cgroup文件系统再特定的子系统实现。

Cgroups中除了通用的控制文件外,每个子系统还有自己的控制文件,子系统也是通过cftype来管理这些控制文件。Cpu子系统很重要的一个文件就是cpu.shares文件,因为就是通过这个文件的数值来调节cgroup所占用的cpu时间。Shares文件对应的cftype结构为:

1
2
3
4
5
6
7
#ifdef CONFIG_FAIR_GROUP_SCHED
{
	.name = "shares",
	.read_u64 = cpu_shares_read_u64,
	.write_u64 = cpu_shares_write_u64,
},
#endif

当对cgroup目录下的文件进行操作时,该结构体中定义的函数指针指向的函数就会被调用.下面我们就在看看这个两个函数的实现吗,从而发现shares文件的值是如何起作用的。

1
2
3
4
5
6
static u64 cpu_shares_read_u64(struct cgroup *cgrp, struct cftype *cft)
{
	struct task_group *tg = cgroup_tg(cgrp);
 
	return (u64) tg->shares;
}

比较简单,简单的读取task_group中存储的shares就行了。

1
2
3
4
5
static int cpu_shares_write_u64(struct cgroup *cgrp, struct cftype *cftype,
				u64 shareval)
{
	return sched_group_set_shares(cgroup_tg(cgrp), shareval);
}

则是设定cgroup对应的task_group的shares值。

那这个shares值是怎么起作用的呢?在sched_group_set_shares中有:

1
2
3
4
5
6
7
8
tg->shares = shares;
for_each_possible_cpu(i) {
	/*
	 * force a rebalance
	 */
	cfs_rq_set_shares(tg->cfs_rq[i], 0);
	set_se_shares(tg->se[i], shares);
}

cfs_rq_set_shares强制做一次cpu SMP负载均衡。

真正起作用的是在set_se_shares中,它调用了set_se_shares,在set_se_shares中有:

1
2
se->load.weight = shares;
se->load.inv_weight = 0;

根据之前我们分析的CFS的调度原理可以知道,load.weight的值越大,算出来的虚拟运行时间越小,进程能使用的cpu时间越多。这样以来,shares值最终就是通过调度实体的load值来起作用的。

cgroup文件系统

http://www.cnblogs.com/lisperl/archive/2012/04/23/2466151.html

Cgroups用户空间管理

Cgroups用户空间的管理是通过cgroup文件系统实现的。

比如要创建一个层级:

1
mount -t cgroup -o cpu,cpuset,memory cpu_and_mem /cgroup/cpu_and_mem

这个命令就创建一个名为cpu_and_mem的层级,这个层级上附加了cpu,cpuset,memory三个子系统,并把层级挂载到了/cgroup/cpu_and_mem.

创建一个cgroup:

1
2
cd /cgroup/cpu_and_mem
mkdir foo

通过以上两个命令,我们就在刚才创建的层级下创建了一个叫foo的cgroup。

你再cd foo,然后ls

你会发现一些文件,这是cgroups相关子系统的控制文件,你可以读取这些控制文件,这些控制文件存储的值就是对相应的cgrouop的控制信息,你也可以写控制文件来更改控制信息。 在这些文件中,有一个叫tasks的文件,里面的包含了所有属于这个cgroup的进程的进程号。

在刚才创建的foo下,你cat tasks,应该是空的,因为此时这个cgroup里面还没有进程。你cd /cgroup/cpu_and_mem 再cat tasks,你可以看到系统中所有进程的进程号,这是因为每创建一个层级的时候,系统的所有进程都会自动被加到该层级的根cgroup里面。Tasks文件不仅可以读,还可以写,你将一个进程的进程号写入到某个cgroup目录下的tasks里面,你就将这个进程加入了相应的cgroup。

Cgroup文件系统的实现

在讲cgroup文件系统的实现之前,必须简单的介绍一下Linux VFS。

VFS是所谓的虚拟文件系统转换,是一个内核软件层,用来处理与Unix标准文件系统的所有系统调用。VFS对用户提供统一的读写等文件操作调用接口,当用户调用读写等函数时,内核则调用特定的文件系统实现。具体而言,文件在内核内存中是一个file数据结构来表示的。这个数据结构包含一个f_op的字段,该字段中包含了一组指向特定文件系统实现的函数指针。当用户执行read()操作时,内核调用sys_read(),然后sys_read()查找到指向该文件属于的文件系统的读函数指针,并调用它,即file->f_op->read().

VFS其实是面向对象的,在这里,对象是一个软件结构,既定义数据也定义了之上的操作。处于效率,Linux并没有采用C++之类的面向对象的语言,而是采用了C的结构体,然后在结构体里面定义了一系列函数指针,这些函数指针对应于对象的方法。

VFS文件系统定义了以下对象模型:
超级块对象(superblock object) 存放已安装文件系统的有关信息。 索引节点对象(inode object) 存放关于具体文件的一般信息。 文件对象(file object) 存放打开文件与进程之间的交互信息 目录项对象(dentry object) 存放目录项与对应文件进行链接的有关信息。

基于VFS实现的文件系统,都必须实现定义这些对象,并实现这些对象中定义的函数指针。cgroup文件系统也不例外,下面我们来看cgroups中这些对象的定义。

cgroup文件系统的定义:

1
2
3
4
5
static struct file_system_type cgroup_fs_type = {
	.name = "cgroup",
	.get_sb = cgroup_get_sb,
	.kill_sb = cgroup_kill_sb,
};

这里有定义了两个函数指针,定义了一个文件系统必须实现了的两个操作get_sb,kill_sb,即获得超级块和释放超级块。这两个操作会在使用mount系统调用挂载cgroup文件系统时使用。

cgroup 超级块的定义:

1
2
3
4
5
6
static const struct super_operations cgroup_ops = {
	.statfs = simple_statfs,
	.drop_inode = generic_delete_inode,
	.show_options = cgroup_show_options,
	.remount_fs = cgroup_remount,
};

Cgroup 索引块定义:

1
2
3
4
5
6
static const struct inode_operations cgroup_dir_inode_operations = {
	.lookup = simple_lookup,
	.mkdir = cgroup_mkdir,
	.rmdir = cgroup_rmdir,
	.rename = cgroup_rename,
};

在cgroup文件系统中,使用mkdir创建cgroup或者用rmdir删除cgroup时,就会调用相应的函数指针指向的函数。比如:使用mkdir创建cgroup时,会调用cgroup_mkdir,然后在cgroup_mkdir中再调用具体实现的cgroup_create函数。

Cgroup 文件操作定义:

1
2
3
4
5
6
7
static const struct file_operations cgroup_file_operations = {
	.read = cgroup_file_read,
	.write = cgroup_file_write,
	.llseek = generic_file_llseek,
	.open = cgroup_file_open,
	.release = cgroup_file_release,
};

在cgroup文件系统中,对目录下的控制文件进行操作时,会调用该结构体中指针指向的函数。比如:对文件进行读操作时,会调用cgroup_file_read,在cgroup_file_read中,会根据需要调用该文件对应的cftype结构体定义的对应读函数。

我们再来看cgroup文件系统中的cgroups控制文件。Cgroups定义一个cftype的结构体来管理控制文件。下面我们来看cftype的定义:

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
struct cftype {
	char name[MAX_CFTYPE_NAME];
	int private; /*
	mode_t mode;
	size_t max_write_len;
 
	int (*open)(struct inode *inode, struct file *file);
	ssize_t (*read)(struct cgroup *cgrp, struct cftype *cft,
	struct file *file,
	char __user *buf, size_t nbytes, loff_t *ppos);
	u64 (*read_u64)(struct cgroup *cgrp, struct cftype *cft);
	s64 (*read_s64)(struct cgroup *cgrp, struct cftype *cft);
	int (*read_map)(struct cgroup *cont, struct cftype *cft,
	struct cgroup_map_cb *cb);
	int (*read_seq_string)(struct cgroup *cont, struct cftype *cft,
				struct seq_file *m);
 
	ssize_t (*write)(struct cgroup *cgrp, struct cftype *cft,
				struct file *file,
				const char __user *buf, size_t nbytes, loff_t *ppos);
	int (*write_u64)(struct cgroup *cgrp, struct cftype *cft, u64 val);
	int (*write_s64)(struct cgroup *cgrp, struct cftype *cft, s64 val);
	int (*write_string)(struct cgroup *cgrp, struct cftype *cft,
				const char *buffer);
	int (*trigger)(struct cgroup *cgrp, unsigned int event);
 
	int (*release)(struct inode *inode, struct file *file);
	int (*register_event)(struct cgroup *cgrp, struct cftype *cft,
	struct eventfd_ctx *eventfd, const char *args); /*
	void (*unregister_event)(struct cgroup *cgrp, struct cftype *cft,
	struct eventfd_ctx *eventfd);
};

cftype中除了定义文件的名字和相关权限标记外,主要是定义了对文件进行操作的函数指针。不同的文件可以有不同的操作,对文件进行操作时,相关函数指针指向的函数会被调用。

综合上面的分析,cgroups通过实现cgroup文件系统来为用户提供管理cgroup的工具,而cgroup文件系统是基于Linux VFS实现的。相应地,cgroups为控制文件定义了相应的数据结构cftype,对其操作由cgroup文件系统定义的通过操作捕获,再调用cftype定义的具体实现。

cgroups 数据结构设计

http://www.cnblogs.com/lisperl/archive/2012/04/18/2455027.html

我们从进程出发来剖析cgroups相关数据结构之间的关系。 在Linux中,管理进程的数据结构是task_struct,其中与cgroups有关的:

1
2
3
4
5
6
#ifdef CONFIG_CGROUPS
	/* Control Group info protected by css_set_lock */
	struct css_set *cgroups;
	/* cg_list protected by css_set_lock and tsk->alloc_lock */
	struct list_head cg_list;
#endif

其中cgroups指针指向了一个css_set结构,而css_set存储了与进程相关的cgroups信息。Cg_list是一个嵌入的list_head结构,用于将连到同一个css_set的进程组织成一个链表。下面我们来看css_set的结构:

1
2
3
4
5
6
7
8
struct css_set {
	atomic_t refcount;
	struct hlist_node hlist;
	struct list_head tasks;
	struct list_head cg_links;
	struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];
	struct rcu_head rcu_head;
};

其中refcount是该css_set的引用数,因为一个css_set可以被多个进程共用,只要这些进程的cgroups信息相同,比如:在所有已创建的层级里面都在同一个cgroup里的进程。

hlist是嵌入的hlist_node,用于把所有css_set组织成一个hash表,这样内核可以快速查找特定的css_set。

tasks指向所有连到此css_set的进程连成的链表。

cg_links指向一个由struct cg_cgroup_link连成的链表。

Subsys是一个指针数组,存储一组指向cgroup_subsys_state的指针。一个cgroup_subsys_state就是进程与一个特定子系统相关的信息。通过这个指针数组,进程就可以获得相应的cgroups控制信息了。

下面我们就来看cgroup_subsys_state的结构:

1
2
3
4
5
6
struct cgroup_subsys_state {
	struct cgroup *cgroup;
	atomic_t refcnt;
	unsigned long flags;
	struct css_id *id;
};

cgroup指针指向了一个cgroup结构,也就是进程属于的cgroup。进程受到子系统的控制,实际上是通过加入到特定的cgroup实现的,因为cgroup在特定的层级上,而子系统又是附加到曾经上的。通过以上三个结构,进程就可以和cgroup连接起来了:task_struct->css_set->cgroup_subsys_state->cgroup。

下面我们再来看cgroup的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct cgroup {
	unsigned long flags;       
	atomic_t count;
	struct list_head sibling;  
	struct list_head children; 
	struct cgroup *parent;     
	struct dentry *dentry;     
	struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];
	struct cgroupfs_root *root;
	struct cgroup *top_cgroup;
	struct list_head css_sets;
	struct list_head release_list;
	struct list_head pidlists;
	struct mutex pidlist_mutex;
	struct rcu_head rcu_head;
	struct list_head event_list;
	spinlock_t event_list_lock;
};

sibling,children和parent三个嵌入的list_head负责将同一层级的cgroup连接成一颗cgroup树。

subsys是一个指针数组,存储一组指向cgroup_subsys_state的指针。这组指针指向了此cgroup跟各个子系统相关的信息,这个跟css_set中的道理是一样的。

root指向了一个cgroupfs_root的结构,就是cgroup所在的层级对应的结构体。这样以来,之前谈到的几个cgroups概念就全部联系起来了。

top_cgroup指向了所在层级的根cgroup,也就是创建层级时自动创建的那个cgroup。

css_set指向一个由struct cg_cgroup_link连成的链表,跟css_set中cg_links一样。

下面我们来分析一个css_set和cgroup之间的关系。我们先看一下 cg_cgroup_link的结构

1
2
3
4
5
6
struct cg_cgroup_link {
	struct list_head cgrp_link_list;
	struct cgroup *cgrp;
	struct list_head cg_link_list;
	struct css_set *cg;
};

cgrp_link_list连入到cgroup->css_set指向的链表,cgrp则指向此cg_cgroup_link相关的cgroup。

Cg_link_list则连入到css_set->cg_links指向的链表,cg则指向此cg_cgroup_link相关的css_set。

那为什么要这样设计呢?

那是因为cgroup和css_set是一个多对多的关系,必须添加一个中间结构来将两者联系起来,这跟数据库模式设计是一个道理。cg_cgroup_link中的cgrp和cg就是此结构体的联合主键,而cgrp_link_list和cg_link_list分别连入到cgroup和css_set相应的链表,使得能从cgroup或css_set都可以进行遍历查询。

那为什么cgroup和css_set是多对多的关系呢?

一个进程对应css_set,一个css_set就存储了一组进程(应该有可能被几个进程共享,所以是一组)跟各个子系统相关的信息,但是这些信息有可能不是从一个cgroup那里获得的,因为一个进程可以同时属于几个cgroup,只要这些cgroup不在同一个层级。举个例子:我们创建一个层级A,A上面附加了cpu和memory两个子系统,进程B属于A的根cgroup;然后我们再创建一个层级C,C上面附加了ns和blkio两个子系统,进程B同样属于C的根cgroup;那么进程B对应的cpu和memory的信息是从A的根cgroup获得的,ns和blkio信息则是从C的根cgroup获得的。因此,一个css_set存储的cgroup_subsys_state可以对应多个cgroup。另一方面,cgroup也存储了一组cgroup_subsys_state,这一组cgroup_subsys_state则是cgroup从所在的层级附加的子系统获得的。一个cgroup中可以有多个进程,而这些进程的css_set不一定都相同,因为有些进程可能还加入了其他cgroup。但是同一个cgroup中的进程与该cgroup关联的cgroup_subsys_state都受到该cgroup的管理(cgroups中进程控制是以cgroup为单位的)的,所以一个cgrouop也可以对应多个css_set。

那为什么要这样一个结构呢?

从前面的分析,我们可以看出从task到cgroup是很容易定位的,但是从cgroup获取此cgroup的所有的task就必须通过这个结构了。每个进程都会指向一个css_set,而与这个css_set关联的所有进程都会链入到css_set->tasks链表.而cgroup又通过一个中间结构cg_cgroup_link来寻找所有与之关联的所有css_set,从而可以得到与cgroup关联的所有进程。

最后让我们看一下层级和子系统对应的结构体。层级对应的结构体是cgroupfs_root:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct cgroupfs_root {
	struct super_block *sb;
	unsigned long subsys_bits;
	int hierarchy_id;
	unsigned long actual_subsys_bits;
	struct list_head subsys_list;
	struct cgroup top_cgroup;
	int number_of_cgroups;
	struct list_head root_list;
	unsigned long flags;
	char release_agent_path[PATH_MAX];
	char name[MAX_CGROUP_ROOT_NAMELEN];
};

sb指向该层级关联的文件系统超级块

subsys_bits和actual_subsys_bits分别指向将要附加到层级的子系统和现在实际附加到层级的子系统,在子系统附加到层级时使用

hierarchy_id是该层级唯一的id

top_cgroup指向该层级的根cgroup

number_of_cgroups记录该层级cgroup的个数

root_list是一个嵌入的list_head,用于将系统所有的层级连成链表

子系统对应的结构体是cgroup_subsys:

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
struct cgroup_subsys {
	struct cgroup_subsys_state *(*create)(struct cgroup_subsys *ss,
								struct cgroup *cgrp);
	int (*pre_destroy)(struct cgroup_subsys *ss, struct cgroup *cgrp);
	void (*destroy)(struct cgroup_subsys *ss, struct cgroup *cgrp);
	int (*can_attach)(struct cgroup_subsys *ss, struct cgroup *cgrp,
				struct task_struct *tsk, bool threadgroup);
	void (*cancel_attach)(struct cgroup_subsys *ss, struct cgroup *cgrp,
				struct task_struct *tsk, bool threadgroup);
	void (*attach)(struct cgroup_subsys *ss, struct cgroup *cgrp,
			struct cgroup *old_cgrp, struct task_struct *tsk,
			bool threadgroup);
	void (*fork)(struct cgroup_subsys *ss, struct task_struct *task);
	void (*exit)(struct cgroup_subsys *ss, struct task_struct *task);
	int (*populate)(struct cgroup_subsys *ss,
			struct cgroup *cgrp);
	void (*post_clone)(struct cgroup_subsys *ss, struct cgroup *cgrp);
	void (*bind)(struct cgroup_subsys *ss, struct cgroup *root);
 
	int subsys_id;
	int active;
	int disabled;
	int early_init;
	bool use_id;
#define MAX_CGROUP_TYPE_NAMELEN 32
	const char *name;
	struct mutex hierarchy_mutex;
	struct lock_class_key subsys_key;
	struct cgroupfs_root *root;
	struct list_head sibling;
	struct idr idr;
	spinlock_t id_lock;
	struct module *module;
};

Cgroup_subsys定义了一组操作,让各个子系统根据各自的需要去实现。这个相当于C++中抽象基类,然后各个特定的子系统对应cgroup_subsys则是实现了相应操作的子类。类似的思想还被用在了cgroup_subsys_state中,cgroup_subsys_state并未定义控制信息,而只是定义了各个子系统都需要的共同信息,比如该cgroup_subsys_state从属的cgroup。然后各个子系统再根据各自的需要去定义自己的进程控制信息结构体,最后在各自的结构体中将cgroup_subsys_state包含进去,这样通过Linux内核的container_of等宏就可以通过cgroup_subsys_state来获取相应的结构体。