一:前言
前面已经分析了cgroup的框架,下面来分析cpuset子系统.所谓cpuset,就是在用户空间中操作cgroup文件系统来执行进程与cpu和进程与内存结点之间的绑定.有关cpuset的详细描述可以参考文档: linux-2.6.28-rc7/Documentation/cpusets.txt.本文从cpuset的源代码角度来对cpuset进行详细分析.以下的代码分析是基于linux-2.6.28.
二:cpuset的数据结构
每一个cpuset都对应着一个struct cpuset结构,如下示:
struct cpuset {
/*用于从cgroup到cpuset的转换*/
struct cgroup_subsys_state css;
/*cpuset的标志*/
unsigned long flags; /* "unsigned long" so bitops work */
/*该cpuset所绑定的cpu*/
cpumask_t cpus_allowed; /* CPUs allowed to tasks in cpuset */
/*该cpuset所绑定的内存结点*/
nodemask_t mems_allowed; /* Memory Nodes allowed to tasks */
/*cpuset的父结点*/
struct cpuset *parent; /* my parent */
/*
* Copy of global cpuset_mems_generation as of the most
* recent time this cpuset changed its mems_allowed.
*/
/*是当前cpuset_mems_generation的拷贝.每更新一次
*mems_allowed,cpuset_mems_generation就会加1
*/
int mems_generation;
/*用于memory_pressure*/
struct fmeter fmeter; /* memory_pressure filter */
/* partition number for rebuild_sched_domains() */
/*对应调度域的分区号*/
int pn;
/* for custom sched domain */
/*与sched domain相关*/
int relax_domain_level;
/* used for walking a cpuset heirarchy */
/*用来遍历所有的cpuset*/
struct list_head stack_list;
}
这个数据结构中的成员含义现在没必要深究,到代码分析遇到的时候再来详细讲解.在这里我们要注意的是struct cpuset中内嵌了struct cgroup_subsys_state css.也就是说,我们可以从struct cgroup_subsys_state css的地址导出struct cpuset的地址.故内核中,从cpuset到cgroup的转换有以下关系:
static inline struct cpuset *cgroup_cs(struct cgroup *cont)
{
return container_of(cgroup_subsys_state(cont, cpuset_subsys_id),
struct cpuset, css);
}
Cgroup_subsys_state()代码如下:
static inline struct cgroup_subsys_state *cgroup_subsys_state(
struct cgroup *cgrp, int subsys_id)
{
return cgrp->subsys[subsys_id];
}
即从cgroup中求得对应的cgroup_subsys_state.再用container_of宏利用地址偏移求得cpuset.
另外,在内核中还有下面这个函数:
static inline struct cpuset *task_cs(struct task_struct *task)
{
return container_of(task_subsys_state(task, cpuset_subsys_id),
struct cpuset, css);
}
同理,从struct task_struct->cgroup得到cgroup_subsys_state结构.再取得cpuset.
三:cpuset的初始化
Cpuset的初始化分为三部份.如下所示:
asmlinkage void __init start_kernel(void)
{
……
……
cpuset_init_early();
……
cpuset_init();
……
}
Start_kernel() à kernel_init() à cpuset_init_smp()
下面依次分析这些初始化函数.
3.1:Cpuset_init_eary()
该函数代码如下:
int __init cpuset_init_early(void)
{
top_cpuset.mems_generation = cpuset_mems_generation++;
return 0;
}
该函数十分简单,就是初始化top_cpuset.mems_generation.在这里我们遇到了前面分析cpuset数据结构中提到的全局变量cpuset_mems_generation.它的定义如下:
/*
* Increment this integer everytime any cpuset changes its
* mems_allowed value. Users of cpusets can track this generation
* number, and avoid having to lock and reload mems_allowed unless
* the cpuset they're using changes generation.
*
* A single, global generation is needed because cpuset_attach_task() could
* reattach a task to a different cpuset, which must not have its
* generation numbers aliased with those of that tasks previous cpuset.
*
* Generations are needed for mems_allowed because one task cannot
* modify another's memory placement. So we must enable every task,
* on every visit to __alloc_pages(), to efficiently check whether
* its current->cpuset->mems_allowed has changed, requiring an update
* of its current->mems_allowed.
*
* Since writes to cpuset_mems_generation are guarded by the cgroup lock
* there is no need to mark it atomic.
*/
static int cpuset_mems_generation;
注释上说的很详细,简而言之,全局变量cpuset_mems_generation就是起一个对比作用,它在每次改更了cpuset的mems_allowed都是加1.然后进程在关联cpuset的时候,会将task->cpuset_mems_generation.设置成进程所在cpuset的cpuset->cpuset_mems_generation的值.每次cpuset中的mems_allowed发生更改的时候,都会将cpuset-> mems_generation设置成当前cpuset_mems_generation的值.这样,进程在分配内存的时候就会对比task->cpuset_mems_generation和cpuset->cpuset_mems_generation的值,如果不相等,说明cpuset的mems_allowed的值发生了更改,所以在分配内存之前首先就要更新进程的mems_allowed.举个例子:
alloc_pages()->alloc_pages_current()->cpuset_update_task_memory_state().重点来跟踪一下cpuset_update_task_memory_state().代码如下:
void cpuset_update_task_memory_state(void)
{
int my_cpusets_mem_gen;
struct task_struct *tsk = current;
struct cpuset *cs;
/*取得进程对应的cpuset的,然后求得要对比的mems_generation*/
/*在这里要注意访问top_cpuset和其它cpuset的区别.访问top_cpuset
*的时候不必要持rcu .因为它是一个静态结构.永远都不会被释放
*因此无论什么访问他都是安全的
*/
if (task_cs(tsk) == &top_cpuset) {
/* Don't need rcu for top_cpuset. It's never freed. */
my_cpusets_mem_gen = top_cpuset.mems_generation;
} else {
rcu_read_lock();
my_cpusets_mem_gen = task_cs(tsk)->mems_generation;
rcu_read_unlock();
}
/*如果所在cpuset的mems_generaton不和进程的cpuset_mems_generation相同
*说明进程所在的cpuset的mems_allowed发生了改变.所以要更改进程
*的mems_allowed.
*/
if (my_cpusets_mem_gen != tsk->cpuset_mems_generation) {
mutex_lock(&callback_mutex);
task_lock(tsk);
cs = task_cs(tsk); /* Maybe changed when task not locked */
/*更新进程的mems_allowed*/
guarantee_online_mems(cs, &tsk->mems_allowed);
/*更新进程的cpuset_mems_generation*/
tsk->cpuset_mems_generation = cs->mems_generation;
/*PF_SPREAD_PAGE和PF_SPREAD_SLAB*/
if (is_spread_page(cs))
tsk->flags |= PF_SPREAD_PAGE;
else
tsk->flags &= ~PF_SPREAD_PAGE;
if (is_spread_slab(cs))
tsk->flags |= PF_SPREAD_SLAB;
else
tsk->flags &= ~PF_SPREAD_SLAB;
task_unlock(tsk);
mutex_unlock(&callback_mutex);
/*重新绑定进程和允许的内存结点*/
mpol_rebind_task(tsk, &tsk->mems_allowed);
}
}
这个函数就是用来在请求内存的判断进程的cpuset->mems_allowed有没有更改.如果有更改就更新进程的相关域.最后再重新绑定进程到允许的内存结点.
在这里,我们遇到了cpuset的两个标志.一个是is_spread_page()测试的CS_SPREAD_PAGE和is_spread_slab()测试的CS_SPREAD_SLAB.这两个标识是什么意思呢?从代码中可以看到,它就是对应进程的PF_SPREAD_PAGE和PF_SPREAD_SLAB.它的作用是在为页面缓页或者是inode分配空间的时候,平均使用进程所允许使用的内存结点.举个例子:
__page_cache_alloc() à cpuset_mem_spread_node():
int cpuset_mem_spread_node(void)
{
int node;
node = next_node(current->cpuset_mem_spread_rotor, current->mems_allowed);
if (node == MAX_NUMNODES)
node = first_node(current->mems_allowed);
current->cpuset_mem_spread_rotor = node;
return node;
}
看到是怎么找分配节点了吧?代码中current->cpuset_mem_spread_rotor是上次文件缓存分配的内存结点.它就是轮流使用进程所允许的内存结点.
返回到cpuset_update_task_memory_state()中,看一下里面涉及到的几个子函数:
guarantee_online_mems()用来更新进程的mems_allowed.代码如下:
static void guarantee_online_mems(const struct cpuset *cs, nodemask_t *pmask)
{
while (cs && !nodes_intersects(cs->mems_allowed,
node_states[N_HIGH_MEMORY]))
cs = cs->parent;
if (cs)
nodes_and(*pmask, cs->mems_allowed,
node_states[N_HIGH_MEMORY]);
else
*pmask = node_states[N_HIGH_MEMORY];
BUG_ON(!nodes_intersects(*pmask, node_states[N_HIGH_MEMORY]));
}
在内核中,所有在线的内存结点都存放在node_states[N_HIGH_MEMORY].这个函数的作用就是到所允许的在线的内存结点.何所谓”在线的”内存结点呢?听说过热插拨吧?服务器上的内存也是这样的,可以运态插拨的.
另一个重要的子函数是mpol_rebind_task(),它将进程与所允许的内存结点重新绑定.也就是移动旧节点的数值到新结点中.这个结点是mempolicy方面的东西了.在这里不做详细讲解了.可以自行跟踪看一下,代码很简单的.
分析完全局变量cpuset_mems_generation的作用之后,来看下一个初始化函数.
3.2: cpuset_init()
Cpuset_init()代码如下:
int __init cpuset_init(void)
{
int err = 0;
/*初始化top_cpuset的cpus_allowed和mems_allowed
*将它初始化成系统中的所有cpu和所有的内存节点
*/
cpus_setall(top_cpuset.cpus_allowed);
nodes_setall(top_cpuset.mems_allowed);
/*初始化top_cpuset.fmeter*/
fmeter_init(&top_cpuset.fmeter);
/*因为更改了top_cpuset->mems_allowed
*所以要更新cpuset_mems_generation
*/
top_cpuset.mems_generation = cpuset_mems_generation++;
/*设置top_cpuset的CS_SCHED_LOAD_BALANCE*/
set_bit(CS_SCHED_LOAD_BALANCE, &top_cpuset.flags);
/*设置top_spuset.relax_domain_level*/
top_cpuset.relax_domain_level = -1;
/*注意cpuset 文件系统*/
err = register_filesystem(&cpuset_fs_type);
if (err < 0)
return err;
/*cpuset 个数计数*/
number_of_cpusets = 1;
return 0;
}
在这里主要初始化了顶层cpuset的相关信息.在这里,我们又遇到了几个标志.下面一一讲解:
CS_SCHED_LOAD_BALANCE:
Cpuset中cpu的负载均衡标志.如果cpuset设置了此标志,表示该cpuset下的cpu在调度的时候,实现负载均衡.
relax_domain_level:
它是调度域的一个标志,表示在NUMA中负载均衡时寻找空闲CPU的标志.有以下几种取值:
-1 : no request. use system default or follow request of others.
0 : no search.
1 : search siblings (hyperthreads in a core).
2 : search cores in a package.
3 : search cpus in a node [= system wide on non-NUMA system]
( 4 : search nodes in a chunk of node [on NUMA system] )
( 5 : search system wide [on NUMA system] )
在这个函数还出现了fmeter.有关fmeter我们之后等遇到再来分析.
另外,cpuset还对应一个文件系统,这是为了兼容cgroup之前的cpuset操作.跟踪这个文件系统看一下:
static struct file_system_type cpuset_fs_type = {
.name = "cpuset",
.get_sb = cpuset_get_sb,
};
Cpuset_get_sb()代码如下;
static int cpuset_get_sb(struct file_system_type *fs_type,
int flags, const char *unused_dev_name,
void *data, struct vfsmount *mnt)
{
struct file_system_type *cgroup_fs = get_fs_type("cgroup");
int ret = -ENODEV;
if (cgroup_fs) {
char mountopts[] =
"cpuset,noprefix,"
"release_agent=/sbin/cpuset_release_agent";
ret = cgroup_fs->get_sb(cgroup_fs, flags,
unused_dev_name, mountopts, mnt);
put_filesystem(cgroup_fs);
}
return ret;
}
可见就是使用cpuset,noprefix,release_agent=/sbin/cpuset_release_agent选项挂载cgroup文件系统.
即相当于如下操作:
Mount –t cgroup cgroup –o puset,noprefix,release_agent=/sbin/cpuset_release_agent mount_dir
其中,mount_dir指文件系统挂载点.
3.3: cpuset_init_smp()
代码如下:
void __init cpuset_init_smp(void)
{
top_cpuset.cpus_allowed = cpu_online_map;
top_cpuset.mems_allowed = node_states[N_HIGH_MEMORY];
hotcpu_notifier(cpuset_track_online_cpus, 0);
hotplug_memory_notifier(cpuset_track_online_nodes, 10);
}
它将cpus_allowed和mems_allwed更新为在线的cpu和在线的内存结点.最后为cpu热插拨和内存热插拨注册了hook.来看一下.
在分析这两个hook之前,有必要提醒一下,在这个hook里面涉及的一些子函数有些是cpuset中一些核心的函数.在之后对cpuset的流程进行分析的时候,有很多地方都会调用这两个hook中的子函数.因此理解这部份代码是理解整个cpuset子系统的关键。好了,闲言少叙,转入正题.
Cpu hotplug对应的hook为cpuset_track_online_cpus.代码如下:
static int cpuset_track_online_cpus(struct notifier_block *unused_nb,
unsigned long phase, void *unused_cpu)
{
struct sched_domain_attr *attr;
cpumask_t *doms;
int ndoms;
/*只处理CPU_ONLINE,CPU_ONLINE_FROZEN,CPU_DEAD,CPU_DEAD_FROZEM*/
switch (phase) {
case CPU_ONLINE:
case CPU_ONLINE_FROZEN:
case CPU_DEAD:
case CPU_DEAD_FROZEN:
break;
default:
return NOTIFY_DONE;
}
/*更新top_cpuset.cpus_allowed*/
cgroup_lock();
top_cpuset.cpus_allowed = cpu_online_map;
scan_for_empty_cpusets(&top_cpuset);
/*更新cpuset 调度域*/
ndoms = generate_sched_domains(&doms, &attr);
cgroup_unlock();
/* Have scheduler rebuild the domains */
/*更新scheduler的调度域信息*/
partition_sched_domains(ndoms, doms, attr);
return NOTIFY_OK;
}
这个函数是对应cpu hotplug的处理,如果系统中的cpu发生了改变,比如添加/删除,就必须要修正cpuset中的cpu信息.首先,我们在之前分析过,top_cpuset中包含了所有的cpu和memory node,因此首先要修正top_cpuset中的cpu信息,其次,系统中cpu发生改变,有可能引起某些cpuse中的cpu信息变为了空值,因此要对这些空值cpuset下的进程进行处理。同理,也要更新调度域信息。下面一一来分析里面涉及到的子函数。
3.3.1&#