`
xiaohui_p
  • 浏览: 16602 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

控制组剖析——文件系统的观点

阅读更多
控制组剖析——文件系统的观点

前言
在前面的文章中介绍了cgroup设计中的主要数据结构以及数据结构间的关联,并简要分析了子系统的实现.在文中还提到内核采用控制组文件系统对控制组进行管理,本文中将从文件系统的角度对cgroup进行剖析.

1 VFS简介

虚拟文件系统(VFS)也称为虚拟文件系统转换(Virtual Filesystem Switch),它是一个内核软件层,用来处理与Unix标准文件系统相关的所有系统调用.VFS蕴涵的主要思想是引入了一个通用文件模型(common file model),该模型可表示所有支持的文件系统,其反映了传统Unix文件系统提供的文件模型.通用文件模型由如下对象结构组成:
超级块对象(superblock object):存放已经安装的文件系统有关信息.一般情况下对应于文件系统中的文件系统控制块.
索引节点对象(inode object):存放关于具体文件的信息,该结构中包含了一个索引节点号,它唯一标志了一个存在的文件.
目录项对象(dentry object):存放文件名和具体文件进行链接的有关信息.
文件对象(file object):存放打开文件和进程之间交互信息.
在下面将从VFS入手,介绍控制组文件系统的设计与实现.

2 控制组文件系统

2.1 注册文件系统

为使内核能够识别所安装的文件系统,目标文件系统的源码必须包含在内核映像,或者以模块的形式动态装载.VFS为追踪内核中所有的文件系统,内核使用文件系统类型注册机制来实现.系统中每个注册的文件系统使用file_system_type对象来表示,控制组文件系统类型定义如下:
struct file_system_type cgroup_fs_type={
	.name = "cgroup",//文件系统类型名称
	.mount = cgroup_mount,//安装文件系统接口
	.kill_sb = cgroup_kill_sb,//卸载文件系统
};

cgroup_fs_type定义了安装和卸载文件系统的接口以及文件系统的名称,下面将详细介绍安装和卸载控制组文件系统的实现.

2.1.1 cgroup_mount实现算法

首先分析安装文件系统的函数cgroup_mount的实现算法流程:
1) 调用parse_cgroupfs_options接口分析mount命令参数,并将结构保存在struct cgroup_fs_opts类型的结构opts中,如果出错则进行出错处理.
2) 调用cgroup_root_from_opts函数,根据opts中的信息来动态分配一个cgroupfs_root结构并初始化,其指针保存在opts的new_root数据项.
3) 调用sget来查找目标超级块sb,如果目标超级块存在则返回该超级块,否则分配新的超级块(超级块中数据项s_fs_info保存了对应的cgroupfs_root结构指针).
4) 如果超级块为最新创建的超级块(即sb->s_fs_info==new_root):
	a. 调用cgroup_get_rootdir为控制块sb分配根目录项和根目录inode结点.
	b. 检查指定的层次结构名称是否重复,若发生重复则出错.
	c. 分配css_set_count(系统中css_set结构数)个cg_cgroup_link结构,用于将所有任务关联到新创建的层次结构top_cgroup.
	d. 调用rebind_subsystem将子系统链接到层次结构.
	e. 将层次结构添加到全局层次结构链表(cgroupfs_root.root_list<====>roots),并将根目录对象数据项d_fsdata指向根控制组,并将根控制组目录设定为根目录.
	f. 调用link_css_set将所有任务关联到根控制组(利用c中分配的cg_cgroup_link进行关联).
	g. 调用cgroup_populate_dir来创建根目录下的控制文件.
   否则:
	a. 调用cgroup_drop_root来释放分配的层次结构等资源.
	b. 调用drop_parsed_module_refcounts来减少模块子系统模块的引用计数.
5) 释放opts结构中分配的用于保存release_agent和name字符串的空间.
6) 调用dget来获得超级块根目录指针并返回(增加引用计数).

从上面的算法流程,可知挂载过程主要包括:获得或者分配控制组文件系统超级块,创建cgourfs_root结构并初始化之,将所有任务关联到新创建的层次结构根控制组,在控制组文件系统根目录下创建所需的控制文件.

2.1.2 cgroup_kill_sb实现算法

申请资源总是一件复杂的事情,因为向内核申请某些资源时,内核不一定拥有或者乐意将资源分配出来.但是释放资源却是很容易的事情.卸载控制组文件系统没有挂载时候那样的复杂,其主要涉及释放超级块结构,解除子系统和层次结构的链接以及任务和层次结构的链接等,其算法流程如下:
1) 进行合法性检查,卸载系统时必须满足:层次结构中只包含根控制组.
2) 调用rebind_subsystems(root,0)来解除子系统和层次结构的链接.
3) 释放和top_cgroup关联的cg_cgroup_link结构,从而断开任务和层次结构的关联.
4) 将cgroupfs_root从全局层次结构链表中删除.
5) 调用kill_litter_sb来释放超级块结构.
6) 调用cgroup_drop_root来释放cgroupfs_root占用的资源.

卸载控制组文件系统的算法实现比较简单,主要就是释放占用的资源,其中较为重要的是检查层次结构是否符合卸载条件.挂载和卸载控制组文件系统均调用了rebind_subsystems函数,该函数实现了建立和解除层次结构和子系统间的链接,具体实现可参考附录.

2.2 超级块对象

内核使用超级块对象表示已经安装的文件系统,超级块的结构较大,在此不给出结构定义,可自行参考内核源码.控制组实现代码中处理超级块的函数主要包括:
1)
//sget用于测试超级块是否为需要寻找的超级块,如果满足条件返回1,否则返回0.
//如果层次结构名称和链接的子系统相同,那么就是目标超级块,由于比较简单,故不给出代码实现.
int cgroup_test_super(struct super_block *sb, void *data);

2)
//重新设置超级块结构.
//该函数和cgroup_test_super一起作为sget函数的参数,在查找到需要的超级块后对其进行重新初始化.
static int cgroup_set_super(struct super_block *sb,void *data){
	int ret;
	struct cgroupfs_sb_opts *opts = data;
	if(!opts->new_root)
		return -EINVAL;
	BUG_ON(!opts->subsys_bits && !opts->none);
	//设定无名超级块(控制组文件系统并不对应真实设备,只是虚拟设备)
	ret = set_anon_super(sb,NULL);
	if(ret)
		return ret;
	//将控制组cgroupfs_root结构保存在super_block结构s_fs_info数据项中
	//从而将超级块和cgroupfs_root结构关联起来,从这里也可以看出cgroupfs_root结构命名的由来了.
	sb->s_fs_info = opts->new_root;
	opts->new_root->sb = sb;
	sb->s_blocksize = PAGE_CACHE_SIZE;
	sb->s_blocksize_bits = PAGE_CACHE_SHIFT;
	sb->s_magic = CGROUP_SUPER_MAGIC;
	sb->s_op = &cgroup_ops;
	return 0;
}

由于控制组文件系统并没有对应真实设备,而是虚拟设备,故调用set_anon_super()函数来为超级块分配设备号.虚拟设备块大小设定为缓冲页大小,魔幻数为预定义的CGROUP_SUPER_MAGIC.需额外注意的是s_op数据项,该数据项指向了超级块操作对象结构.超级块操作对象定义了超级块相关操作接口,在控制组文件系统中定义如下:
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_opsd定义了statfs(获取超级块信息)、drop_inode(删除inode)、show_options(显示挂载信息)和remount_fs(重新挂载文件系统接口).simple_statfs()为系统定义的获取超级块信息的接口,该函数用户获取文件系统魔幻数,块大小以及文件系统名最大长度等。generic_delete_inode()也是系统默认定义的删除inode节点的接口,永远返回成功.

3)
static int cgroup_show_options(struct seq_file *seq, struct vfsmount *vfs),该函数打印层次结构信息,主要包括挂载的子系统、控制文件命名是否有前缀、release_agent路径和层次结构名称、是否设定clone_children标记等.该函数通过vfs->mnt_sb获得层次结构超级块sb,然后根据sb->s_fs_info来获得cgroupfs_root结构,从而获得层次结构信息.

4)
static int cgroup_remount(struct super_block *sb, int *flags, void *data),该函数用于重新设定链接到层次结构的子系统和release_agent,其算法流程较为简单:
1) 调用parse_cgroupfs_options()分析remount命令参数.
2) 检查重新挂载请求是否合法:不允许修改层次结构名称和标记,只允许重设链接的子系统和release_agent.
3) 调用rebind_subsystems重新链接子系统,如果需重设release_agent则重设该项.

2.3 索引节点对象

控制组文件系统通过cgroup_new_inode()分配索引节点,其调用系统接口new_inode分配索引节点.由于控制组文件系统操作对象没有实现alloc_inode接口(sb->s_op->alloc_inode),new_inode调用kmem_cache_alloc从inode_cachep(索引节点slab)中分配inode节点.成功申请索引节点后,设定索引节点的索引节点号、访问权限、用户ID、组ID和访问、创建、修改时间等.
控制组文件系统调用cgroup_create_file()函数创建目录(对应于控制组)和控制文件,其主要过程包括:调用cgroup_new_inode()申请索引节点,接着根据文件类型初始化索引对象的i_op和i_fop数据项,最后调用d_instantiate()系统接口将目录项和索引对象关联起来.当创建目录时,i_op和i_fop数据项初始化为(inode为cgroup_new_inode返回的索引对象指针):
inode->i_op=&cgroup_dir_inode_operations;
inode->i_fop=&simple_dir_operations;
当创建控制文件时,仅需初始化i_fop数据项:
inode->i_fop=&cgroup_file_operations;
i_op和i_fop分别指向索引节点操作对象(struct inode_operations)和缺省文件操作对象(struct file_operations).索引节点操作对象的实现如下:
static const struct inode_operations cgroup_dir_inode_operations = {
	.lookup = cgroup_lookup,
	.mkdir = cgroup_mkdir,
	.rmdir = cgroup_rmdir,
	.rename = cgroup_rename,
};

由上可知,索引对象节点实现了lookup,mkdir,rmdir和rename接口.cgroup_lookup()函数通过调用d_add(dentry,NULL)来实现,下面着重关注mkdir,rmdir和rename的实现.

2.3.1 cgroup_mkdir实现

cgroup_mkdir函数实现较为简单其主要通过cgroup_create函数实现,下面是cgroup_mkdir的代码:
static int cgroup_mkdir(struct inode *dir, struct dentry *dentry, int mode){
	struct cgroup *c_parent = dentry->d_parent->d_fsdata;
	return cgroup_create(c_parent, dentry, mode | S_IFDIR);
}

在此需注意的是为关联控制组和目录,目录项中d_fsdata数据项保存对应的控制组指针,而控制组中dentry则指向了对应的目录项.下面是cgroup_create的实现算法:
//@parent:新控制组的父控制组
//@dentry:新控制组对应的目录项
//@mode:目录项对应inode节点的访问权限
static long cgroup_create(cgroup *parent, dentry *dentry, int mode);
1) 获取控制组所在层次结构的cgroupfs_root和super_block结构
cgroupfs_root * root = parent->root;//根据父控制组结构获得层次结构cgroupfs_root
super_block *sb = root->sb;
2) 调用kzalloc分配新控制组结构.
3) 增加超级块结构的活动计数.
4) 初始化新控制组结构:
a. 调用init_cgroup_housekeeping()函数初始化控制组结构.
b. 将新控制组加入到层次结构,主要设定parent,root和top_cgroup数据项.
c. 检查父控制组是否设定了CGRP_NOTIFY_ON_RELEASE和CGRP_CLONE_CHILDREN标志;如果设定了这些标志,则在子控制组中设定这些标志.
d. 调用连接到层次结构子系统的create接口,建立子系统和控制组的联系,如果设定了CGRP_CLONE_CHILDREN标记并且子系统设定了post_clone接口,则调用之.
e. 将新控制组添加到父控制组子控制组双向链表中(list_add(&cgrp->sibling,&cgrp->parent->children).
f. 调用cgroup_create_dir()创建新控制组对应的目录,调用cgroup_populate_dir()创建相关控制文件.

据上面流程可知,cgroup_create函数主要负责创建新控制组并将其添加到层次结构,以及创建新控制组对应的目录和控制文件.

2.3.2 cgroup_rmdir实现

该函数主要用于删除目录项和控制组,被删除目录项(控制组)需满足如下条件:
a) 控制组无子控制组
b) 控制组引用计数为0
c) 与控制组关联的css引用计数必须为1,即css没有被除控制组外其他对象引用.
当调用cgroup_rmdir时,目标控制组满足条件a,b时可能不满足条件c,这时会返回-EBUSY.为减少返回-EBUSY,系统将当前调用rmdir的进程添加到等待队列中,并设定为可中断等待状态.故当任务被挂起后,再次被唤醒时,需检查任务被唤醒的原因,如果被信号唤醒,则返回-EINTR.由于任务被睡眠过,故需重新检查控制组是否满足被删除的条件.删除控制组的操作较为繁琐,但是代码原理不是很复杂,下面给出判断控制组是否可被删除的代码:
static int cgroup_rmdir(struct inode *unused,struct dentry *dentry){
	//目录对象d_fsdata域保存了目录对应的控制组指针	
	struct cgroup *cgrp = dentry->d_fsdata;
	struct dentry *d;
	struct cgroup *parent;
	//定义等待队列的数据项
	DEFINE_WAIT(wait);
	struct cgroup_event *event, *tmp;
again:
	//需要对控制组信息进行方位,获得互斥锁
	mutex_lock(&cgroup_mutex);
	//控制组的引用计数不为0,控制组忙碌中
	if(atomic_read(&cgrp->count)!=0){
		mutex_unlock(&cgroup_mutex);
		return -EBUSY;
	}
	//控制组还有子控制组
	if(!list_empty(&cgrp->children)){
		mutex_unlock(&cgroup_mutex);
		return -EBUSY;
	}
	//设定控制组将等待被删除标志,表示有进程请求删除该控制组
	set_bit(CGRP_WAIT_ON_RMDIR,&cgrp->flags);
	//调用子系统的pre_destroy接口,通知子系统控制组将会被删除,由于当前任务可能会睡眠,
	//该接口在删除前可能被调用多次.
	ret = cgroup_call_pre_destroy(cgrp);
	if(ret){//子系统pre_destroy返回错误信息,禁止删除控制组
		clear_bit(CGRP_WAIT_ON_RMDIR,&cgrp->flags);
		return ret;
	}
	//下面需要对控制组进行修改操作,所以需要首先获得cgroup_mutex
	mutex_lock(&cgroup_mutex);
	parent = cgrp->parent;
	//再次重新检查,因此此前的一些操作并没有获得cgroup_mutex,有些人可能创建了子控制组或者其他的事情
	if(atomic_read(&cgrp->count || !list_empty(&cgrp->children)){
		clear_bit(CGRP_WAIT_ON_RMDIR,&cgrp->flags);//清除有任务等待将控制组删除标记
		mutex_unlock(&cgroup_mutex);
		return -EBUSY;
	}
	//将当前任务增加到等待队列中,并设定为可中断等待方式
	prepare_to_wait(&cgroup_rmdir_waitq,&wait,TASK_INTERRUPTIBLE);
	//清空控制组相关的css的引用计数,只有在所有引用计数均为1时该函数才能清空css引用计数
	if(!cgroup_clear_css_refs(cgrp)){
		mutex_unlock(&cgroup_mutex)
		//由于存在其他任务调用了cgroup_wakeup_rmdir_waiter(),所以需要重新确定是否还设定了该标志
		if(test_bit(CGRP_WAIT_ON_RMDIR,&cgrp->flags))
			schedule();
		finish_wait(&cgroup_rmdir_waitq,&wait);
		clear_bit(CGRP_WAIT_ON_RMDIR,&cgrp->flags);
		//检查是否被信号唤醒
		if(signal_pending(current))
			return -EINTR;
		goto again;//需要重新检查是否允许删除
	}
	//到这里便可以安全的删除控制组了
	1) 调用finish_wait()将当前任务从等待队列中移除.
	2) 清除控制组CGRP_WAIT_ON_RMDIR标记,并设置CGRP_REMOVED标记,只是控制组被删除
	3) 将控制组从父控制组的子控制组双向链表中删除,并从release_list中移除
	4) 删除控制组对应的目录项
	5) 设定父控制组CGRP_RELEASEABLE标记,并调用check_for_release(parent)
	6) 注销控制组相关的事件并通知用户空间
	return 0;
}


2.3.3 cgroup_rename实现

cgroup_rename实现了文件重命名操作.控制组文件系统只允许对文件夹(控制组)进行重命名,不支持普通文件的重命名,这是因为普通文件对应于和控制组相关联的控制文件,显然控制文件不能够被重命名,下面是实现代码:
static int cgroup_rename(struct inode *old_dir, struct dentry *old_dentry, struct inode *new_dir, struct dentry *new_dentry){
	//检查被重命名文件是否为目录,如果不为目录则出错返回.
	//通过该项检查使得只能对目录进行重命名,禁止对控制文件的重命名.
	if(!S_ISDIR(old->dentry->d_inode->i_mode))
		return -ENOTDIR;
	//检查目标目录项是否已经和目录(控制组)相关联,如果关联则出错.
	//很显然,不允许用一个控制组去"覆盖"另外的控制组.
	if(new_dentry->d_inode)
		return -EEXIST;perf_event
	//修改后的目录应该保持父目录不变,进而保持控制组间的父子关系.
	if(old_dir != new_dir)
		return -EIO;
	//调用系统实现的简单rename实现,主要检查new_dentry是否为空
	return simple_rename(old_dir, old_dentry, new_dir, new_dentry);
}


2.4 文件操作对象

当创建控制文件时,inode中数据项i_fop则指向了cgroup_file_operations,其定义如下:
static 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_file_operations定义了控制组文件的一般操作接口,其中需提到的是release对应于文件的关闭,该接口没有使用close命名是因为文件对象可能被多个任务所共享,当某个任务关闭文件时会减少引用计数,只有引用计数为0时才能关闭文件.下面分别介绍open,release,read,write操作实现代码,llseek接口直接调用系统定义的generic_file_llseek实现,在此不做介绍.

2.4.1 打开控制文件

打开文件(控制文件)由cgroup_file_open()实现,该函数主要调用控制文件对象接口来操作文件,其代码如下:
static int cgroup_file_open(struct inode *inode,struct file *file){
	int err;
	struct cftype *cft;
	//调用系统定义的文件打开操作,主要对inode,file进行合法性检查
	err = generic_file_open(inode,file);
	if(err)
		return err;
	//获取文件对应的cftype对象,该项保存在file->f_dentry->d_fsdata
	//如果为目录文件该数据项中保存cgroup对象指针.
	cft = __d_cft(file->f_dentry);
	//如果定义了read_map和read_seq_string接口,则需要设定文件操作对象为序列操作文件
	if(cft->read_map || cft->read_seq_string){
		struct cgroup_seqfile_state *state= kzalloc(sizeof(*state),GFP_USER);
		if(!state)
			return -ENOMEM;
		state->cft = cft;
		//目录想d_fsdata中保存控制组指针
		state->cgroup = __d_cgrp(file->f_dentry->d_parent);
		file->f_op = &cgroup_seqfile_operations;
		//调用single_open接口,来打开文件
		err = single_open(file,cgroup_seqfile_show,state);
		if(err < 0)
			kfree(state);
	}
	//如果用户自定义了open接口则调用
	else if(cft->open)
		err = cft->open(inode,file);
	//用户没有额外的要求,那么就成功
	else
		err = 0;
	return err;
}

上面的代码不算复杂,但是遇到了几个没有解释过的结构和函数,下面据做一些的分析,首先看看cgroup_seqfile_state结构,
struct cgroup_seqfile_state{
	cftype *cft;
	cgroup *cgroup;
};

该结构只有两个数据项cft和cgroup,根据cgroup_file_open对其的初始化代码可知,该结构保存了被打开文件对应的cftype对象和控制组对象指针.

在cgroup_file_open实现中,还将file->f_ope指向cgroup_seqfile_operations,其定义了文件操作对象,具体实现如下:
struct file_operations cgroup_seqfile_operations = {
	.read = seq_read,
	.write = cgroup_file_write,
	.llseek = seq_lseek,
	.release = cgroup_seqfile_release,
};

seq_read和seq_lseek为系统实现的默认序列文件read和llseek操作,在此不做介绍.下面仅仅介绍cgroup_seqfile_release的实现:
static int cgroup_seqfile_release(struct inode *inode,struct file *file){
	struct seq_file *seq = file->private_data;
	kfree(seq->private);
	return single_release(inode,file);
}

上面的代码只有三行,但从代码可知file对象的private_data数据项保存了序列文件指针,single_release和single_open相对应,主要用于解除file对象和seqfile对象的关联.第二行代码调用kfree释放了一些东西,但是释放了什么呢?这就需要去查看single_open实现代码了,通过查看发现该项保存了single_open第三个参数传递的指针,结合上面代码可知其保存了cgroup_seqfile_state对象的指针,这样在操作文件时就能很容易的找到cgroup和cftype了.

还有一个尚未解释清楚的是cgroup_seqfile_show函数的功能,single_open()将该函数设定为序列文件操作的show接口,当读序列文件时用于显示信息.其实现会根据cftype中的read_map和read_seq_string是否设定来调用之.

2.4.2 释放控制文件

cgroup_file_release()实现了文件操作对象的release接口,该接口用于释放文件对象.cgroup_file_release()通过调用cftype的release接口实现,下面为其实现代码:
static int cgroup_file_release(struct inode *inode, struct file *file){
	//获取文件对应的cftype对象结构
	struct cftype *cft = __d_cft(file->f_dentry);
	//如果控制文件对象定义了release则定义它
	if(cft->release)
		return cft->release(inode,file);
	return 0;
}


2.4.3 读控制文件

控制文件的读操作通过cgroup_file_read()函数实现,该函数通过调用cftype对象定义的接口来实现.根据该函数的实现可知,cftype对象中几个读接口操作存在一定的优先级,优先顺序为read > read_u64 > read_s64,cgroup_file_read()实现代码较为简单,代码如下:
static ssize_t cgroup_file_read(struct file *file, char __user *buf, size_t nbytes, loff_t *ppos){
	struct cftype *cft = __d_cft(file->f_dentry);
	struct cgroup *cgrp = __d_cgrp(file->f_dentry->d_parent);
	if(cgroup_is_removed(cgrp))
		return -ENODEV;
	if(cft->read)
		return cft->read(cgrp,cft,file,buf,nbytes,ppos);
	if(cft->read_u64)
		return cgroup_read_u64(cgrp,cft,file,buf,nbytes,ppos);
	if(cft->read_s64)
		return cgroup_read_s64(cgrp,cft,file,buf,nbytes,ppos);
	return -EINVAL;
}

在上面中cgroup_read_u64和cgroup_read_s64仅仅将read_u64和read_s64封装起来,将读取的数值格式化为字符串后返回给用户空间.

2.4.4 写控制文件

和读控制文件类似,cgroup_file_write()实现了对控制文件的写操作,该函数通过调用cftype对象定义的写控制文件接口实现,这些写接口同样也存在一定的优先级,优先顺序为:write > write_u64 > write_s64 > write_string > trigger, cgroup_file_write()实现代码根据读接口优先级调用相应接口,实现较为简单,代码如下:
static ssize_t cgroup_file_write(struct file *file, const char __user *buf, size_t nbytes, loff_t *ppos){
	//获取文件对应的cftype对象和cgroup对象指针
	struct cftype *cft = __d_cft(file->f_dentry);
	struct cgroup *cgrp = __d_cdt(file->f_dentry->d_parent);
	if(cgroup_is_removed(cgrp))
		return -ENODEV;
	if(cft->write)
		return cft->write(cgrp,cft,file,buf,nbytes,ppos);
	if(cft->write_u64 || cft->write_s64)
		return cgroup_write_X64(cgrp,cft,file,buf,nbytes,ppos);
	if(cft->write_string)
		return cgroup_write_string(cgrp,cft,file,buf,nbytes,ppos);
	if(cft->trigger){
		int ret = cft->trigger(cgrp,(unsigned int)cft->private);
		return ret ? ret : nbytes;
	}
	return -EINVAL;
}

cgroup_write_X64和cgroup_write_string分别封装了cftype相关接口,主要负责从用户空间拷贝数据,如果需要还会将数据转换为期望的格式,然后调用cftype接口实现写操作.cftype对象的trigger接口定义了在不关注实际写内容时从用户空间获得一些反馈(kick)信息.

3 控制组proc接口

为便于查看控制组信息,控制组文件系统在proc系统中创建了/proc/cgroups和/proc/<pid>/cgroup文件./proc/cgroups统计子系统和层次结构的信息,其格式为:
subsys_name  hierarchy  num_cgroups  enabled
其中,subsys_name为子系统名称,hierarchy为子系统连接到层次结构id,num_cgroups为层次结构中控制组数量,enabled表示子系统是否被打开.
/proc/<pid>/cgroup统计了任务所属层次结构信息,其输出格式为:
hierarchy_id:subsys_root_name:cgroup_path
其中hierarchy_id为任务所属的层次结构id,subsys_root_name为连接到层次结构的子系统和层次结构名称,cgroup_path:为控制组对应目录的路径.
由于每个任务可同时属于多个层次结构故输出信息可能为多行.
以上统计信息分别由proc_cgroupstats_show()和proc_cgroup_show()函数输出,由于代码较为简单,就不再对其进行详细论述.

4 小结

本文主要从文件系统的角度论述了控制组的组织管理,分别介绍了控制组文件系统注册、超级块管理、索引节点操作、文件操作的实现,并简要介绍了控制组在proc文件系统中的接口.通过控制组文件系统用户对控制组的操作会更加简单,比如使用mkdir命令即可创建子控制组,当需要销毁控制组时,只需使用rmdir命令将控制组对应的目录删除即可,当然,在删除目录之前必须保证没有子控制组,如果待删除的控制组不是根控制组,还需保证控制组中没有任务存在.

5 附录

5.1 rebind_subsystems实现

rebind_subsystems()实现了层次结构和子系统的重绑定,子系统重绑定时需满足如下条件:
1) 如果将子系统连接到层次结构,那么必须保证该层次结构没有链接到除rootnode以外的其他层次结构.
2) 如果解除子系统和层次结构的链接,那么解除链接后需将子系统链接到rootnode.
cgroupfs_root结构的actual_subsys_bits数据项保存了链接到层次结构的子系统位图,通过位运算很容易得到需链接和解除链接的子系统位图:
removed_bits = root->actual_subsys_bits & ~final_bits;
added_bits = final_bits & ~root->actual_subsys_bits;
在计算出removed_bits和added_bits后,需验证需要添加的子系统是空闲的(当前链接到rootnode),如果通过验证,则根据上述规则添加和解除子系统.

5.2 文件系统和层次结构的关联

为关联文件系统和层次结构,文件系统和层次结构对象间有如下关联关系,在下面的表示中A.a<---->B.b表示通过结构A的数据项a可找到相关的结构B,反之,通过结构B中数据项b可找到相关结构A.
1) 超级块和控制组系统根
superblock.s_fs_info<------>cgroupfs_root.sb
2) 目录和控制组
dentry.d_fsdata<------->cgroup.dentry
3) 目录和控制文件
dentry.d_fsdata-------->cftype

结束语
将层次结构作为文件系统不仅符合控制组间的树形层次结构,更方便了用户对控制组的管理和信息交互.从用户的观点来看,层次结构和普通的文件系统没有两样,对控制组的操作转换为对文件的操作,简化了用户空间对控制组进行管理的复杂度,从而允许用户空间使用简单的rmdir,mkdir等命令即可实现对控制组的管理.

冗长的论述有时会让人厌烦,希望您可以忍受我杂乱无章的表述,good luck!
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics