控制组剖析——文件系统的观点
程序员文章站
2022-07-13 16:09:10
...
控制组剖析——文件系统的观点
前言
在前面的文章中介绍了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对象来表示,控制组文件系统类型定义如下:
cgroup_fs_type定义了安装和卸载文件系统的接口以及文件系统的名称,下面将详细介绍安装和卸载控制组文件系统的实现.
2.1.1 cgroup_mount实现算法
首先分析安装文件系统的函数cgroup_mount的实现算法流程:
从上面的算法流程,可知挂载过程主要包括:获得或者分配控制组文件系统超级块,创建cgourfs_root结构并初始化之,将所有任务关联到新创建的层次结构根控制组,在控制组文件系统根目录下创建所需的控制文件.
2.1.2 cgroup_kill_sb实现算法
申请资源总是一件复杂的事情,因为向内核申请某些资源时,内核不一定拥有或者乐意将资源分配出来.但是释放资源却是很容易的事情.卸载控制组文件系统没有挂载时候那样的复杂,其主要涉及释放超级块结构,解除子系统和层次结构的链接以及任务和层次结构的链接等,其算法流程如下:
卸载控制组文件系统的算法实现比较简单,主要就是释放占用的资源,其中较为重要的是检查层次结构是否符合卸载条件.挂载和卸载控制组文件系统均调用了rebind_subsystems函数,该函数实现了建立和解除层次结构和子系统间的链接,具体实现可参考附录.
2.2 超级块对象
内核使用超级块对象表示已经安装的文件系统,超级块的结构较大,在此不给出结构定义,可自行参考内核源码.控制组实现代码中处理超级块的函数主要包括:
1)
//sget用于测试超级块是否为需要寻找的超级块,如果满足条件返回1,否则返回0.
//如果层次结构名称和链接的子系统相同,那么就是目标超级块,由于比较简单,故不给出代码实现.
int cgroup_test_super(struct super_block *sb, void *data);
2)
//重新设置超级块结构.
//该函数和cgroup_test_super一起作为sget函数的参数,在查找到需要的超级块后对其进行重新初始化.
由于控制组文件系统并没有对应真实设备,而是虚拟设备,故调用set_anon_super()函数来为超级块分配设备号.虚拟设备块大小设定为缓冲页大小,魔幻数为预定义的CGROUP_SUPER_MAGIC.需额外注意的是s_op数据项,该数据项指向了超级块操作对象结构.超级块操作对象定义了超级块相关操作接口,在控制组文件系统中定义如下:
从上可知,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).索引节点操作对象的实现如下:
由上可知,索引对象节点实现了lookup,mkdir,rmdir和rename接口.cgroup_lookup()函数通过调用d_add(dentry,NULL)来实现,下面着重关注mkdir,rmdir和rename的实现.
2.3.1 cgroup_mkdir实现
cgroup_mkdir函数实现较为简单其主要通过cgroup_create函数实现,下面是cgroup_mkdir的代码:
在此需注意的是为关联控制组和目录,目录项中d_fsdata数据项保存对应的控制组指针,而控制组中dentry则指向了对应的目录项.下面是cgroup_create的实现算法:
据上面流程可知,cgroup_create函数主要负责创建新控制组并将其添加到层次结构,以及创建新控制组对应的目录和控制文件.
2.3.2 cgroup_rmdir实现
该函数主要用于删除目录项和控制组,被删除目录项(控制组)需满足如下条件:
a) 控制组无子控制组
b) 控制组引用计数为0
c) 与控制组关联的css引用计数必须为1,即css没有被除控制组外其他对象引用.
当调用cgroup_rmdir时,目标控制组满足条件a,b时可能不满足条件c,这时会返回-EBUSY.为减少返回-EBUSY,系统将当前调用rmdir的进程添加到等待队列中,并设定为可中断等待状态.故当任务被挂起后,再次被唤醒时,需检查任务被唤醒的原因,如果被信号唤醒,则返回-EINTR.由于任务被睡眠过,故需重新检查控制组是否满足被删除的条件.删除控制组的操作较为繁琐,但是代码原理不是很复杂,下面给出判断控制组是否可被删除的代码:
2.3.3 cgroup_rename实现
cgroup_rename实现了文件重命名操作.控制组文件系统只允许对文件夹(控制组)进行重命名,不支持普通文件的重命名,这是因为普通文件对应于和控制组相关联的控制文件,显然控制文件不能够被重命名,下面是实现代码:
2.4 文件操作对象
当创建控制文件时,inode中数据项i_fop则指向了cgroup_file_operations,其定义如下:
由定义可知cgroup_file_operations定义了控制组文件的一般操作接口,其中需提到的是release对应于文件的关闭,该接口没有使用close命名是因为文件对象可能被多个任务所共享,当某个任务关闭文件时会减少引用计数,只有引用计数为0时才能关闭文件.下面分别介绍open,release,read,write操作实现代码,llseek接口直接调用系统定义的generic_file_llseek实现,在此不做介绍.
2.4.1 打开控制文件
打开文件(控制文件)由cgroup_file_open()实现,该函数主要调用控制文件对象接口来操作文件,其代码如下:
上面的代码不算复杂,但是遇到了几个没有解释过的结构和函数,下面据做一些的分析,首先看看cgroup_seqfile_state结构,
该结构只有两个数据项cft和cgroup,根据cgroup_file_open对其的初始化代码可知,该结构保存了被打开文件对应的cftype对象和控制组对象指针.
在cgroup_file_open实现中,还将file->f_ope指向cgroup_seqfile_operations,其定义了文件操作对象,具体实现如下:
seq_read和seq_lseek为系统实现的默认序列文件read和llseek操作,在此不做介绍.下面仅仅介绍cgroup_seqfile_release的实现:
上面的代码只有三行,但从代码可知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接口实现,下面为其实现代码:
2.4.3 读控制文件
控制文件的读操作通过cgroup_file_read()函数实现,该函数通过调用cftype对象定义的接口来实现.根据该函数的实现可知,cftype对象中几个读接口操作存在一定的优先级,优先顺序为read > read_u64 > read_s64,cgroup_file_read()实现代码较为简单,代码如下:
在上面中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()实现代码根据读接口优先级调用相应接口,实现较为简单,代码如下:
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!
前言
在前面的文章中介绍了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!