Linux cgroup机制分析之cpuset subsystem

2023-11-09 09:58

本文主要是介绍Linux cgroup机制分析之cpuset subsystem,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

------------------------------------------
本文系本站原创,欢迎转载!
转载请注明出处:http://ericxiao.cublog.cn/
------------------------------------------
一:前言
前面已经分析了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:scan_for_empty_cpusets()
这一个要分析的就是scan_for_empty_cpusets(),它用来扫描空的cpuset,将它空集cpuset下的task移到它的上级非空的cpuset的,代码如下:

static void scan_for_empty_cpusets(struct cpuset *root)

{
LIST_HEAD(queue);
struct cpuset *cp;  /* scans cpusets being updated */
struct cpuset *child;   /* scans child cpusets of cp */
struct cgroup *cont;
nodemask_t oldmems;

    list_add_tail((struct list_head *)&root->stack_list, &queue);

/*遍历所有的cpuset*/
while (!list_empty(&queue)) {

        cp = list_first_entry(&queue, struct cpuset, stack_list);

list_del(queue.next);

        list_for_each_entry(cont, &cp->css.cgroup->children, sibling) {

child = cgroup_cs(cont);

            list_add_tail(&child->stack_list, &queue);

}

        /* Continue past cpusets with all cpus, mems online */

/*所包含的cpuset 和内存结点如果都是正常的*/

        if (cpus_subset(cp->cpus_allowed, cpu_online_map) &&

            nodes_subset(cp->mems_allowed, node_states[N_HIGH_MEMORY]))

continue;
/*之前的mems_allowed*/
oldmems = cp->mems_allowed;

        /* Remove offline cpus and mems from this cpuset. */

/*丢弃掉已经移除的内存结点和cpu*/
mutex_lock(&callback_mutex);

        cpus_and(cp->cpus_allowed, cp->cpus_allowed, cpu_online_map);

        nodes_and(cp->mems_allowed, cp->mems_allowed,

node_states[N_HIGH_MEMORY]);
mutex_unlock(&callback_mutex);

        /* Move tasks from the empty cpuset to a parent */

/*如果调整之后的cpu和内存结点信息为空*/
if (cpus_empty(cp->cpus_allowed) ||

             nodes_empty(cp->mems_allowed))

remove_tasks_in_empty_cpuset(cp);
/*更新cpuset下进程的cpu和内存结点信息*/
else {
update_tasks_cpumask(cp, NULL);

            update_tasks_nodemask(cp, &oldmems);

}
}
}
首先要看懂这个函数,必须要了解cgroup的架构了,关于这部份,请参阅本站的另一篇文档《linux cgroup机制分析之框架分析》.cpuset-> stack_list成员在这里派上用场了,它就是用来链入临时链表中。我们从代码中可以看到,它是一个从top_cpuset往下层的层次遍次。
对于遍历到的每一个cpuset,
1:如果cpuset的cpu和memory信息都是正常的(分别是cpu_online_map和node_states[N_HIGH_MEMORY]的子集)那就用不着更新了。
2:丢弃掉已经离线的cpu和memory.(也就是与cpu_online_map和n ode_states[N_HIGH_MEMORY]取交集)。
3:如果调整之后的cpuset中cpu或者是memory为空,就要处理它下面的所关联进程的了。这是在remove_tasks_in_empty_cpuset()中处理的.
4:如果调整之后的cpuset的cpu和memory都不都为空。说明它所关联的进程还有资源可用,只需更新所关联进程的mems_allowed和cpus_allowed位图即可。这是在update_tasks_cpumask()和update_tasks_nodemask()中处理的。
下面来分析一下scan_for_empty_cpusets()中调用的几个子函数.

3.3.1.1: remove_tasks_in_empty_cpuset()

代码如下:

static void remove_tasks_in_empty_cpuset(struct cpuset *cs)

{
struct cpuset *parent;
/*

     * The cgroup's css_sets list is in use if there are tasks

     * in the cpuset; the list is empty if there are none;

     * the cs->css.refcnt seems always 0.

*/
/*如果这个cpuset下没有关联的进程*/

    if (list_empty(&cs->css.cgroup->css_sets))

return;
/*

     * Find its next-highest non-empty parent, (top cpuset

     * has online cpus, so can't be empty).

*/
/*向上找到一个cpu和mems不为空的cpuset*/
parent = cs->parent;

    while (cpus_empty(parent->cpus_allowed) ||

nodes_empty(parent->mems_allowed))
parent = parent->parent;
/*将cpuset中的进程移到parent上*/
move_member_tasks_to_cpuset(cs, parent);
}
如果cpuset中有关联的进程,但cpuset允许的相关资源为空,那么就向上找到有资源的cpuset,并将其关联的task移到找到的cpuset中。对照代码中的注释,应该很好理解,这里就不详细分析了。
Move_member_tasks_to_cpuset()代码如下:

static void move_member_tasks_to_cpuset(struct cpuset *from, struct cpuset *to)

{
struct cpuset_hotplug_scanner scan;
scan.scan.cg = from->css.cgroup;

    scan.scan.test_task = NULL; /* select all tasks in cgroup */

    scan.scan.process_task = cpuset_do_move_task;

scan.scan.heap = NULL;
scan.to = to->css.cgroup;
if (cgroup_scan_tasks(&scan.scan))

        printk(KERN_ERR "move_member_tasks_to_cpuset: "

                "cgroup_scan_tasks failed\n");

}
这里涉及到cgroup中的另外一个接口cgroup_scan_tasks()。这个接口在后面再来详细分析,这里先大概说一下,它就是一个遍历cgroup中关联进程的迭代器。对cgroup中关联的每个进程都会调用回调函数scan.scan.process_task.在上面的这段代码中也就是cpuset_do_move_task().代码如下:

static void cpuset_do_move_task(struct task_struct *tsk,

struct cgroup_scanner *scan)
{
struct cpuset_hotplug_scanner *chsp;

    chsp = container_of(scan, struct cpuset_hotplug_scanner, scan);

cgroup_attach_task(chsp->to, tsk);
}
在这个函数中,调用了cgroup_attach_task()将进程关联到了chsp->to.chsp->to也就是我们在上面的代码中看到的parent.

3.3.1.2: update_tasks_cpumask()

这个函数用来更新cpuset下所有进程的cpu信息,代码如下:

static void update_tasks_cpumask(struct cpuset *cs, struct ptr_heap *heap)

{
struct cgroup_scanner scan;
/*遍历cpuset 下的所有task.
*对每一个task调用cpuset_change_cpumask()
*/
scan.cg = cs->css.cgroup;
scan.test_task = cpuset_test_cpumask;
scan.process_task = cpuset_change_cpumask;
scan.heap = heap;
cgroup_scan_tasks(&scan);
}
Cgroup_scan_tasks()这个接口我们在上面已经讨论过来,对cpuset中的每一个进程都会调用cpuset_change_cpumask().代码如下:

static void cpuset_change_cpumask(struct task_struct *tsk,

                  struct cgroup_scanner *scan)

{

    set_cpus_allowed_ptr(tsk, &((cgroup_cs(scan->cg))->cpus_allowed));

}
该函数很简单,就是设置进程的cpus_allowed域,在下次进程被调度回来的时候,就会切换到允许的cpu上面运行。
3.3.1.3:update_tasks_nodemask()
该函数用来更新cpuset下的task的memory node信息。代码如下:

static int update_tasks_nodemask(struct cpuset *cs, const nodemask_t *oldmem)

{
struct task_struct *p;
struct mm_struct **mmarray;
int i, n, ntasks;
int migrate;
int fudge;
struct cgroup_iter it;
int retval;
cpuset_being_rebound = cs;      /* causes mpol_dup() rebind */
/*fudge是为mmarray[ ]提供适当多余的长度*/

    fudge = 10;             /* spare mmarray[] slots */

fudge += cpus_weight(cs->cpus_allowed); /* imagine one fork-bomb/cpu */
retval = -ENOMEM;
/*

     * Allocate mmarray[] to hold mm reference for each task

     * in cpuset cs.  Can't kmalloc GFP_KERNEL while holding

     * tasklist_lock.  We could use GFP_ATOMIC, but with a

     * few more lines of code, we can retry until we get a big

     * enough mmarray[] w/o using GFP_ATOMIC.

*/
/*取得cpuset中task 的个数,这里加上fudge是为了防止在
*操作的过程中,又fork出了一些新的进程,分配空间不够
*/
while (1) {

        ntasks = cgroup_task_count(cs->css.cgroup);  /* guess */

ntasks += fudge;

        mmarray = kmalloc(ntasks * sizeof(*mmarray), GFP_KERNEL);

if (!mmarray)
goto done;
read_lock(&tasklist_lock);      /* block fork */

        if (cgroup_task_count(cs->css.cgroup) <= ntasks)

break;              /* got enough */
read_unlock(&tasklist_lock);        /* try again */
kfree(mmarray);
}
n = 0;

    /* Load up mmarray[] with mm reference for each task in cpuset. */

/*将cpuset下的所有进程的mm都保存至mmarray[ ]中
*n用来计算所取得task的个数
*/

    cgroup_iter_start(cs->css.cgroup, &it);

    while ((p = cgroup_iter_next(cs->css.cgroup, &it))) {

struct mm_struct *mm;
if (n >= ntasks) {
printk(KERN_WARNING

                "Cpuset mempolicy rebind incomplete.\n");

break;
}
mm = get_task_mm(p);
if (!mm)
continue;
mmarray[n++] = mm;
}
cgroup_iter_end(cs->css.cgroup, &it);
read_unlock(&tasklist_lock);
/*

     * Now that we've dropped the tasklist spinlock, we can

     * rebind the vma mempolicies of each mm in mmarray[] to their

     * new cpuset, and release that mm.  The mpol_rebind_mm()

     * call takes mmap_sem, which we couldn't take while holding

     * tasklist_lock.  Forks can happen again now - the mpol_dup()

     * cpuset_being_rebound check will catch such forks, and rebind

     * their vma mempolicies too.  Because we still hold the global

     * cgroup_mutex, we know that no other rebind effort will

     * be contending for the global variable cpuset_being_rebound.

     * It's ok if we rebind the same mm twice; mpol_rebind_mm()

     * is idempotent.  Also migrate pages in each mm to new nodes.

*/
/*
*更新进程的内存分配策略
*如果设置了CS_MEMORY_MIGRATE,就表示需要将进程的
*内存空间从旧结点移动到新结点上
*/
migrate = is_memory_migrate(cs);
for (i = 0; i < n; i++) {
struct mm_struct *mm = mmarray[i];

        mpol_rebind_mm(mm, &cs->mems_allowed);

if (migrate)

            cpuset_migrate_mm(mm, oldmem, &cs->mems_allowed);

mmput(mm);
}

    /* We're done rebinding vmas to this cpuset's new mems_allowed. */

kfree(mmarray);
cpuset_being_rebound = NULL;
retval = 0;
done:
return retval;
}
根据代码中的注释,应该比较容易理解这段代码。在这里涉及到一个新的东西:cgroup_iter。这也是我们之前遇到的Cgroup_scan_tasks()中所使用的迭代器,这部份我们在后面分析Cgroup_scan_tasks()代码的时候再来详细分析。
另外,这里还涉及到mmpolicy 的一些接口,比如mpol_rebind_mm()cpuset_migrate_mm()à do_migrate_pages()这里就不再分析了。感兴趣的,可自行阅读其源代码。
此外,在这个函数中还涉及到一个全局cpuset_being_rebound.它在mpol_dup()拷贝当前进程的内存分存policy的时候会用到。
回到cpuset_track_online_cpus()中,在上面已经分析完了scan_for_empty_cpusets().现在来分析其它的子函数。

3.3.2: generate_sched_domains()

该函数用来取得cpuset中的调度域信息,将取得的调度域信息保存进它的两上函数中,如下示:

static int generate_sched_domains(cpumask_t **domains,

            struct sched_domain_attr **attributes)

{
LIST_HEAD(q);       /* queue of cpusets to be scanned */
struct cpuset *cp;  /* scans q */
struct cpuset **csa;    /* array of all cpuset ptrs */

    int csn;        /* how many cpuset ptrs in csa so far */

    int i, j, k;        /* indices for partition finding loops */

cpumask_t *doms;    /* resulting partition; i.e. sched domains */
struct sched_domain_attr *dattr;  /* attributes for custom domains */
int ndoms = 0;      /* number of sched domains in result */

    int nslot;      /* next empty doms[] cpumask_t slot */

doms = NULL;
dattr = NULL;
csa = NULL;

    /* Special case for the 99% of systems with one, full, sched domain */

/*如果top_cpuset设置了CS_SCHED_LOAD_BALANCE
*说明要在系统全部的cpu间实现sched balance*/

    if (is_sched_load_balance(&top_cpuset)) {

        doms = kmalloc(sizeof(cpumask_t), GFP_KERNEL);

if (!doms)
goto done;

        dattr = kmalloc(sizeof(struct sched_domain_attr), GFP_KERNEL);

if (dattr) {
*dattr = SD_ATTR_INIT;

            /* 取得top_cpuset以及它下面子层的最大relax_domain_level */

            update_domain_attr_tree(dattr, &top_cpuset);

}
/* 顶层的cpus_allowed */
*doms = top_cpuset.cpus_allowed;
ndoms = 1;
goto done;
}
/* cpuset数组*/

    csa = kmalloc(number_of_cpusets * sizeof(cp), GFP_KERNEL);

if (!csa)
goto done;
csn = 0;
/*遍历整个cpuset tree,将设置了CS_SCHED_LOAD_BALANCE
*的cpuset放入csa[]中. csn表示cpuset 的项数*/

    list_add(&top_cpuset.stack_list, &q);

while (!list_empty(&q)) {
struct cgroup *cont;
struct cpuset *child;   /* scans child cpusets of cp */

        cp = list_first_entry(&q, struct cpuset, stack_list);

list_del(q.next);
if (cpus_empty(cp->cpus_allowed))
continue;
/*

         * All child cpusets contain a subset of the parent's cpus, so

         * just skip them, and then we call update_domain_attr_tree()

         * to calc relax_domain_level of the corresponding sched

         * domain.

*/
if (is_sched_load_balance(cp)) {
csa[csn++] = cp;
continue;
}

        list_for_each_entry(cont, &cp->css.cgroup->children, sibling) {

child = cgroup_cs(cont);

            list_add_tail(&child->stack_list, &q);

}
}
/*将csa[]中的cpuset->pn设置为所在的数组项*/
for (i = 0; i < csn; i++)
csa[i]->pn = i;
ndoms = csn;
restart:

    /* Find the best partition (set of sched domains) */

/*遍历csa数组中的cpuset.将有交叉的cpuset->pn设为相同
*ndoms即为csa中没有交叉的cpuset的cpuset 个数*/
for (i = 0; i < csn; i++) {
struct cpuset *a = csa[i];
int apn = a->pn;
for (j = 0; j < csn; j++) {
struct cpuset *b = csa[j];
int bpn = b->pn;

            if (apn != bpn && cpusets_overlap(a, b)) {

for (k = 0; k < csn; k++) {
struct cpuset *c = csa[k];
if (c->pn == bpn)
c->pn = apn;
}
ndoms--;    /* one less element */
goto restart;
}
}
}
/*

     * Now we know how many domains to create.

     * Convert <csn, csa> to <ndoms, doms> and populate cpu masks.

*/
/*有多少个不交叉的设置了CS_SCHED_LOAD_BALANCE的cpuset
*就有多少个调度域*/

    doms = kmalloc(ndoms * sizeof(cpumask_t), GFP_KERNEL);

if (!doms)
goto done;
/*

     * The rest of the code, including the scheduler, can deal with

     * dattr==NULL case. No need to abort if alloc fails.

*/
/*有多少个调度域,就有多少个调度域属性*/

    dattr = kmalloc(ndoms * sizeof(struct sched_domain_attr), GFP_KERNEL);

/*填充doms和dattr,分别为同一项的cpu_allowed合集和
*该层cpuset下面最大relax_domain_level 值
*/
for (nslot = 0, i = 0; i < csn; i++) {
struct cpuset *a = csa[i];
cpumask_t *dp;
int apn = a->pn;
if (apn < 0) {
/* Skip completed partitions */
continue;
}
dp = doms + nslot;
/*按理说,nslot不可能毛坯地ndoms.因为ndoms代表调度域的个数

          *而nslot是cas中pn不相同的cpuset项数-1 .因为nslot是从0开始计数的*/

if (nslot == ndoms) {
static int warnings = 10;
if (warnings) {
printk(KERN_WARNING
"rebuild_sched_domains confused:"

                  " nslot %d, ndoms %d, csn %d, i %d,"

                  " apn %d\n",

                  nslot, ndoms, csn, i, apn);

warnings--;
}
continue;
}
cpus_clear(*dp);
if (dattr)
*(dattr + nslot) = SD_ATTR_INIT;
for (j = i; j < csn; j++) {
struct cpuset *b = csa[j];
if (apn == b->pn) {

                cpus_or(*dp, *dp, b->cpus_allowed);

if (dattr)

                    update_domain_attr_tree(dattr + nslot, b);

/* Done with this partition */
b->pn = -1;
}
}
nslot++;
}
BUG_ON(nslot != ndoms);
done:
kfree(csa);
/*

     * Fallback to the default domain if kmalloc() failed.

     * See comments in partition_sched_domains().

*/
if (doms == NULL)
ndoms = 1;

    *domains    = doms;

*attributes = dattr;
return ndoms;
}
这个函数比较简单,就不详细分析了。请对照添加的注释自行分析。
至此,cpuset的初始化就分析完了.
四:cpuset中的相关操作
下面来分析cpuset中的相关操作,

Cpuset subsystem的结构如下:

struct cgroup_subsys cpuset_subsys = {

.name = "cpuset",
.create = cpuset_create,
.destroy = cpuset_destroy,
.can_attach = cpuset_can_attach,
.attach = cpuset_attach,
.populate = cpuset_populate,
.post_clone = cpuset_post_clone,
.subsys_id = cpuset_subsys_id,
.early_init = 1,
};
根据上面的结构再结合我们之前分析过的cgroup子系统,可以得知相关的操作流程。
4.1:创建cgroup时
经过前面的分析,我们知道在创建cgroup的时候会调用subsystem的create接口。在cpuset中对应就是cpuset_create().代码如下:

static struct cgroup_subsys_state *cpuset_create(

struct cgroup_subsys *ss,
struct cgroup *cont)
{
struct cpuset *cs;
struct cpuset *parent;
/*如果是根目录.返回top_cpuset即可.*/
if (!cont->parent) {

        /* This is early initialization for the top cgroup */

        top_cpuset.mems_generation = cpuset_mems_generation++;

return &top_cpuset.css;
}
/*取得父结点的cpuset*/
parent = cgroup_cs(cont->parent);
/*分配并初始化一个cpuset*/
cs = kmalloc(sizeof(*cs), GFP_KERNEL);
if (!cs)
return ERR_PTR(-ENOMEM);
cpuset_update_task_memory_state();
cs->flags = 0;
if (is_spread_page(parent))

        set_bit(CS_SPREAD_PAGE, &cs->flags);

if (is_spread_slab(parent))

        set_bit(CS_SPREAD_SLAB, &cs->flags);

    set_bit(CS_SCHED_LOAD_BALANCE, &cs->flags);

/*清空cpus_allowed and mems_allowed*/
cpus_clear(cs->cpus_allowed);
nodes_clear(cs->mems_allowed);

    cs->mems_generation = cpuset_mems_generation++;

fmeter_init(&cs->fmeter);
cs->relax_domain_level = -1;
/*设置父结点*/
cs->parent = parent;
number_of_cpusets++;
return &cs->css ;
}
上面的代码比较简单,在这里是返回cpuset->css.因此就可以根据cgroup_subsys_state这个结构找到所属的cpuset结构。
另外,我们在这里也可以看到,新建一个cpuset,它的mems_allowed和cpus_allowed都是空的。而relax_domain_level则是默认值-1.
4.2:关联进程时
在为cgroup关联进程的时候,首先会调用subsys->can_attach()来判断进程是否能够关联到cgroup。返回0说明可以。如果可以关联的时候,还会调用subsys->attach()来对进程进行关联。下面分别来分析这两个接口.

4.2.1: cpuset_can_attach()

代码如下:

static int cpuset_can_attach(struct cgroup_subsys *ss,

                 struct cgroup *cont, struct task_struct *tsk)

{
struct cpuset *cs = cgroup_cs(cont);
/*如果此cpuset中允许的资源为空,进程无法运行,不可关联*/

    if (cpus_empty(cs->cpus_allowed) || nodes_empty(cs->mems_allowed))

return -ENOSPC;
/*如果进程已经指定了绑定的cpu.
*如果指定绑定的cpu集不同于cpuset中的cpu集,不可关联*/
if (tsk->flags & PF_THREAD_BOUND) {
cpumask_t mask;
mutex_lock(&callback_mutex);
mask = cs->cpus_allowed;
mutex_unlock(&callback_mutex);

        if (!cpus_equal(tsk->cpus_allowed, mask))

return -EINVAL;
}
/*进行常规安全性检查*/

    return security_task_setscheduler(tsk, 0, NULL);

}
这函数比较简单,就不详细分析了。
4.2.2: cpuset_attach()
代码如下:

static void cpuset_attach(struct cgroup_subsys *ss,

              struct cgroup *cont, struct cgroup *oldcont,

              struct task_struct *tsk)

{
cpumask_t cpus;
nodemask_t from, to;
struct mm_struct *mm;
struct cpuset *cs = cgroup_cs(cont);
struct cpuset *oldcs = cgroup_cs(oldcont);
int err;
/*cs:是进程即将要移到的cpuset. oldcs是进程之前所在的cpuset*/
/*更新进程的cpu位图*/
mutex_lock(&callback_mutex);
guarantee_online_cpus(cs, &cpus);
err = set_cpus_allowed_ptr(tsk, &cpus);
mutex_unlock(&callback_mutex);
if (err)
return;
/*更新进程的内存结点位图.如果定义了CS_MEMORY_MIGRATE
*还需要将进程从旧结点移动到新结点中
*/
from = oldcs->mems_allowed;
to = cs->mems_allowed;
mm = get_task_mm(tsk);
if (mm) {
mpol_rebind_mm(mm, &to);
if (is_memory_migrate(cs))

            cpuset_migrate_mm(mm, &from, &to);

mmput(mm);
}
这个函数也比较简单,请参照代码注释自行分析。
4.3:创建操作文件时

当cpuset在创建时,会在其文件系统下创建操作文件,相应的会调用subsys-> populate().代码如下:

static int cpuset_populate(struct cgroup_subsys *ss, struct cgroup *cont)

{
int err;

    err = cgroup_add_files(cont, ss, files, ARRAY_SIZE(files));

if (err)
return err;

    /* memory_pressure_enabled is in root cpuset only */

if (!cont->parent)
err = cgroup_add_file(cont, ss,
&cft_memory_pressure_enabled);
return err;
}
从代码中可以看到,cpuset顶层多了一个文件,相应的cftype结构为cft_memory_pressure_enabled.如下所示:

static struct cftype cft_memory_pressure_enabled = {

.name = "memory_pressure_enabled",
.read_u64 = cpuset_read_u64,
.write_u64 = cpuset_write_u64,
.private = FILE_MEMORY_PRESSURE_ENABLED,
};

也就是一个名为” memory_pressure_enabled”的文件。

在所有cpuset目录下都有的文件为file对应的cftype,结构如下示:

static struct cftype files[] = {

{
.name = "cpus",
.read = cpuset_common_file_read,
.write_string = cpuset_write_resmask,
.max_write_len = (100U + 6 * NR_CPUS),
.private = FILE_CPULIST,
},
{
.name = "mems",
.read = cpuset_common_file_read,
.write_string = cpuset_write_resmask,

        .max_write_len = (100U + 6 * MAX_NUMNODES),

.private = FILE_MEMLIST,
},
{
.name = "cpu_exclusive",
.read_u64 = cpuset_read_u64,
.write_u64 = cpuset_write_u64,
.private = FILE_CPU_EXCLUSIVE,
},
{
.name = "mem_exclusive",

        .read_u64 = cpuset_read_u64,

.write_u64 = cpuset_write_u64,
.private = FILE_MEM_EXCLUSIVE,
},
{
.name = "mem_hardwall",
.read_u64 = cpuset_read_u64,
.write_u64 = cpuset_write_u64,
.private = FILE_MEM_HARDWALL,
},
{
.name = "sched_load_balance",
.read_u64 = cpuset_read_u64,
.write_u64 = cpuset_write_u64,
.private = FILE_SCHED_LOAD_BALANCE,
},
{

        .name = "sched_relax_domain_level",

.read_s64 = cpuset_read_s64,
.write_s64 = cpuset_write_s64,
.private = FILE_SCHED_RELAX_DOMAIN_LEVEL,
},
{
.name = "memory_migrate",
.read_u64 = cpuset_read_u64,
.write_u64 = cpuset_write_u64,
.private = FILE_MEMORY_MIGRATE,
},
{
.name = "memory_pressure",
.read_u64 = cpuset_read_u64,
.write_u64 = cpuset_write_u64,
.private = FILE_MEMORY_PRESSURE,
},
{
.name = "memory_spread_page",
.read_u64 = cpuset_read_u64,
.write_u64 = cpuset_write_u64,
.private = FILE_SPREAD_PAGE,
},
{
.name = "memory_spread_slab",
.read_u64 = cpuset_read_u64,
.write_u64 = cpuset_write_u64,
.private = FILE_SPREAD_SLAB,
},
}

也就是名为cpus, mems, cpu_exclusive, mem_exclusive, mem_hardwall, sched_load_balance, sched_relax_domain_level, memory_migrate, memory_pressure, memory_spread_page, memory_spread_slab这几个文件。

其中有几个文件代表的含义我们在上面已经分析过了,如:cpus,mems,sched_load_balance.sched_relax_domain_level,memory_migreate, memory_spread_page和memory_spread_slab.下面我们重点分析一下其它文件是代表的意义。

五:cpuset中的文件操作
5.1: memory_pressure_enabled文件
我们从顶层目录看起,对于cpuset subsystem而言,顶层有个特有的文件,即memory_pressure_enabled.这个文件的含义为:是否计算cpuset中内存压力.何所谓内存压力?就是指当前系统的空闲内存不能满足当前的内存分配请求的速率.有关内存压力计算的细节可以参考kernel自带的文档.
文件对应的cftype如下示:

static struct cftype cft_memory_pressure_enabled = {

.name = "memory_pressure_enabled",
.read_u64 = cpuset_read_u64,
.write_u64 = cpuset_write_u64,
.private = FILE_MEMORY_PRESSURE_ENABLED,
};
从上面看到读操作的接口为cpuset_read_u64,写操作的接口为cpuset_write_u64.我们在之后也可以看到,cpuset中的大部份文件都是用的两个接口,它是根据它的private成员来区分各项操作的,
先来分析读操作:

static u64 cpuset_read_u64(struct cgroup *cont, struct cftype *cft)

{
struct cpuset *cs = cgroup_cs(cont);
cpuset_filetype_t type = cft->private;
switch (type) {
case FILE_CPU_EXCLUSIVE:
return is_cpu_exclusive(cs);
case FILE_MEM_EXCLUSIVE:
return is_mem_exclusive(cs);
case FILE_MEM_HARDWALL:
return is_mem_hardwall(cs);
case FILE_SCHED_LOAD_BALANCE:
return is_sched_load_balance(cs);
case FILE_MEMORY_MIGRATE:
return is_memory_migrate(cs);
case FILE_MEMORY_PRESSURE_ENABLED:
return cpuset_memory_pressure_enabled;
case FILE_MEMORY_PRESSURE:

        return fmeter_getrate(&cs->fmeter);

case FILE_SPREAD_PAGE:
return is_spread_page(cs);
case FILE_SPREAD_SLAB:
return is_spread_slab(cs);
default:
BUG();
}
/* Unreachable but makes gcc happy */
return 0;
}
对应到memory_pressure_enable文件,对应的private域为FILE_MEMORY_PRESSURE_ENABLED.即返回cpuset_memory_pressure_enable的值.这个变量定义如下:

int cpuset_memory_pressure_enabled

虽然它是一个int型数据,但它是一个bool型的,只有0,1两种可能.从写操作就可以看到.
写操作的接口为: cpuset_write_u64().代码如下:

static int cpuset_write_u64(struct cgroup *cgrp, struct cftype *cft, u64 val)

{
int retval = 0;
struct cpuset *cs = cgroup_cs(cgrp);
cpuset_filetype_t type = cft->private;
if (!cgroup_lock_live_group(cgrp))
return -ENODEV;
switch (type) {
case FILE_CPU_EXCLUSIVE:

        retval = update_flag(CS_CPU_EXCLUSIVE, cs, val);

break;
case FILE_MEM_EXCLUSIVE:

        retval = update_flag(CS_MEM_EXCLUSIVE, cs, val);

break;
case FILE_MEM_HARDWALL:

        retval = update_flag(CS_MEM_HARDWALL, cs, val);

break;
case FILE_SCHED_LOAD_BALANCE:

        retval = update_flag(CS_SCHED_LOAD_BALANCE, cs, val);

break;
case FILE_MEMORY_MIGRATE:

        retval = update_flag(CS_MEMORY_MIGRATE, cs, val);

break;
case FILE_MEMORY_PRESSURE_ENABLED:
cpuset_memory_pressure_enabled = !!val;
break;
case FILE_MEMORY_PRESSURE:
retval = -EACCES;
break;
case FILE_SPREAD_PAGE:

        retval = update_flag(CS_SPREAD_PAGE, cs, val);

        cs->mems_generation = cpuset_mems_generation++;

break;
case FILE_SPREAD_SLAB:

        retval = update_flag(CS_SPREAD_SLAB, cs, val);

        cs->mems_generation = cpuset_mems_generation++;

break;
default:
retval = -EINVAL;
break;
}
cgroup_unlock();
return retval;
}
对应的memory_pressure_enable文件,它的操作为:

cpuset_memory_pressure_enabled = !!val

即就是设置cpuset_memory_pressure_enabled的值.如果写入为0,该值为0,如果写入其它数,该值为1.
综合上面的分析,它主要是对cpuset_memory_pressure_enabled进行操作,那么这个变量有什么作用呢?下面来分析一下.
在__alloc_pages_internal()中,如果当前内存不能满足内存分配请求的要求,就会调用cpuset_memory_pressure_bump().代码如下所示:

#define cpuset_memory_pressure_bump()               \

do {                            \
if (cpuset_memory_pressure_enabled)     \
__cpuset_memory_pressure_bump();    \
} while (0)
它实际上就是一个宏定义.如果启用了memory pressure,也就是cpuset_memroy_pressue_enable为1时.就会执行__cpuset_memroy_pressure_bump().代码如下:

void __cpuset_memory_pressure_bump(void)

{
task_lock(current);
fmeter_markevent(&task_cs(current)->fmeter);
task_unlock(current);
}
在这里我们就看到cpuset->fmeter成员的意义,它就是用来计算内存压力的.fmeter_markevent()就不分析了,它无非就是根据请求时内存不足速率来计算压力值.最后计算出来的压力值会保存在fmeter.val中.
5.2: memory_pressure文件
memory_pressure文件用来查看当前cpuset节点的内存压力值.cftype结构如下:
{
.name = "memory_pressure",
.read_u64 = cpuset_read_u64,
.write_u64 = cpuset_write_u64,
.private = FILE_MEMORY_PRESSURE,
},
操作接口跟之前分析的是一样的.
读操作:

static u64 cpuset_read_u64(struct cgroup *cont, struct cftype *cft)

{
......
......
case FILE_MEMORY_PRESSURE:

        return fmeter_getrate(&cs->fmeter);

......
Fmeter_getrate()代码如下:

static int fmeter_getrate(struct fmeter *fmp)

{
int val;
spin_lock(&fmp->lock);
fmeter_update(fmp);
val = fmp->val;
spin_unlock(&fmp->lock);
return val;
}
它就是返回了当前节下的内存压力值.
写操作:

static int cpuset_write_u64(struct cgroup *cgrp, struct cftype *cft, u64 val)

{
......
......
case FILE_MEMORY_PRESSURE:
retval = -EACCES;
......
从此可看到,这个文件是不可写的.
5.3:cpus文件
Cpus文件可以用来配置与cpuset的绑定cpu.对应的cftype结构如下:
{
.name = "cpus",
.read = cpuset_common_file_read,
.write_string = cpuset_write_resmask,
.max_write_len = (100U + 6 * NR_CPUS),
.private = FILE_CPULIST,
}
读操作接口为cpuset_common_file_read().代码如下:

static ssize_t cpuset_common_file_read(struct cgroup *cont,

struct cftype *cft,
struct file *file,
char __user *buf,
size_t nbytes, loff_t *ppos)
{
struct cpuset *cs = cgroup_cs(cont);
cpuset_filetype_t type = cft->private;
char *page;
ssize_t retval = 0;
char *s;

    if (!(page = (char *)__get_free_page(GFP_TEMPORARY)))

return -ENOMEM;
s = page;
switch (type) {
case FILE_CPULIST:
/*将cpuset->cpus_allowed转换为字串存放s中*/
s += cpuset_sprintf_cpulist(s, cs);
break;
case FILE_MEMLIST:
/*将cpuset->memsallowd转换为字串存放在s 中*/
s += cpuset_sprintf_memlist(s, cs);
break;
default:
retval = -EINVAL;
goto out;
}
/*以\n结尾*/
*s++ = '\n';
/*copy 到用户空间*/

    retval = simple_read_from_buffer(buf, nbytes, ppos, page, s - page);

out:
free_page((unsigned long)page);
return retval;
}
这个接口是与mems文件共用的.代码比较简单,这里就不详细分析了.就是接cpuset->cpus_allowed输出.
写操作入口为:

static int cpuset_write_resmask(struct cgroup *cgrp, struct cftype *cft,

const char *buf)
{
int retval = 0;
/*加锁要操作的cgroup*/
if (!cgroup_lock_live_group(cgrp))
return -ENODEV;
switch (cft->private) {
case FILE_CPULIST:
/*更新cpuset的cpus_allowed*/

        retval = update_cpumask(cgroup_cs(cgrp), buf);

break;
case FILE_MEMLIST:
/*更新cpuset的mems_allowed*/

        retval = update_nodemask(cgroup_cs(cgrp), buf);

break;
default:
retval = -EINVAL;
break;
}
cgroup_unlock();
return retval;
}
对应如果是cpus,流程转入到update_cpumask().代码如下:

static int update_cpumask(struct cpuset *cs, const char *buf)

{
struct ptr_heap heap;
struct cpuset trialcs;
int retval;
int is_load_balanced;

    /* top_cpuset.cpus_allowed tracks cpu_online_map; it's read-only */

/*顶层的cpuset是read-only的*/
if (cs == &top_cpuset)
return -EACCES;
/*trialcs是cs的一个拷贝*/
trialcs = *cs;
/*

     * An empty cpus_allowed is ok only if the cpuset has no tasks.

     * Since cpulist_parse() fails on an empty mask, we special case

     * that parsing.  The validate_change() call ensures that cpusets

     * with tasks have cpus.

*/
/*如果写入的是空字串.清空cpus_allowed*/
if (!*buf) {
cpus_clear(trialcs.cpus_allowed);
} else {
/*解析buf 中的位图信息,并将其存入到副本的cpus_allowed中*/

        retval = cpulist_parse(buf, trialcs.cpus_allowed);

if (retval < 0)
return retval;
/*如果要更新的cpus_allowed信息不是cpu_online_map的一个子集*/

        if (!cpus_subset(trialcs.cpus_allowed, cpu_online_map))

return -EINVAL;
}
/*检验cs是否可以更新为triaics的位图信息*/
retval = validate_change(cs, &trialcs);
if (retval < 0)
return retval;

    /* Nothing to do if the cpus didn't change */

/*如果要改更的cpus_allowed是相同的.那用不着更改了*/

    if (cpus_equal(cs->cpus_allowed, trialcs.cpus_allowed))

return 0;
/*初始化堆排序*/

    retval = heap_init(&heap, PAGE_SIZE, GFP_KERNEL, NULL);

if (retval)
return retval;
/*是否设置了CS_SCHED_LOAD_BALANCE标志*/

    is_load_balanced = is_sched_load_balance(&trialcs);

/*更改cpuset->cpus_allowed*/
mutex_lock(&callback_mutex);
cs->cpus_allowed = trialcs.cpus_allowed;
mutex_unlock(&callback_mutex);
/*

     * Scan tasks in the cpuset, and update the cpumasks of any

     * that need an update.

*/
/* 因为进程所在的cpuset的cpus_allowed信息更改了
* 所以需要更改里面进程的所有cpus_allowed信息
*/
update_tasks_cpumask(cs, &heap);
/*释放heap的空间*/
heap_free(&heap);
/*如果设置了CS_SCHED_LOAD_BALANCE*/
if (is_load_balanced)
async_rebuild_sched_domains();
return 0;
}
代码注释详细给出了各部份的操作,这里就不加详细分析了,因为它里面涉及到的重要的接口,我们在上面就已经分析过了.
5.4:其它文件
Mems文件操作和cpus类似,所以就不在详细分析了,其它文件都是对一些标志的设定.这些标志我们在之前都分析过,而且这部份代码也比较简单,所以也不加详细分析了.自行阅读即可.
六:遗留问题.
6.1: cgroup_scan_tasks()
我们在之前的分析为了流程的连贯性跳过了cgroup_scan_tasks()的分析.其实这个函数的功能就是遍历cgroup中的task,然后对这个task调用指定的一个函数.这里涉及到一个数据结构struct cgroup_scanner.如下示:

struct cgroup_scanner {

/*要扫描的cgroup*/
struct cgroup *cg;
/*测试该task,用来判断是否是想要处理的task*/

    int (*test_task)(struct task_struct *p, struct cgroup_scanner *scan);

/*task的处理函数*/
void (*process_task)(struct task_struct *p,
struct cgroup_scanner *scan);
/*排序用的堆.可以指定,也可以由系统默认构建*/
struct ptr_heap *heap;
};

首先,对每个cgroup中的task.先调用struct cgroup_scanner->test_task().如果返回1,表示是我们希望处理的task,所以接着接用struct cgroup_scanner->process_task().在这里的heap跟我们进程结构里的堆栈中的堆是不同的.这里是堆排序,相当于是一个二叉树.有关堆排序方面的东西在<<算法导论>>上有详细的描述.

来看一下代码:

int cgroup_scan_tasks(struct cgroup_scanner *scan)

{
int retval, i;
struct cgroup_iter it;
struct task_struct *p, *dropped;

    /* Never dereference latest_task, since it's not refcounted */

struct task_struct *latest_task = NULL;
struct ptr_heap tmp_heap;
struct ptr_heap *heap;
struct timespec latest_time = { 0, 0 };
if (scan->heap) {

        /* The caller supplied our heap and pre-allocated its memory */

heap = scan->heap;
heap->gt = &started_after;
} else {

        /* We need to allocate our own heap memory */

heap = &tmp_heap;

        retval = heap_init(heap, PAGE_SIZE, GFP_KERNEL, &started_after);

if (retval)
/* cannot allocate the heap */
return retval;
}
如果scan->heap不为空,说明用户已经自己指定的heap,只需要设置好heap中元素的比较函数heap->gt()就可以了.如果scan->heap为空.那就需要系统默认分配一个heap,并对其初始化.
子函数heap_init()很简单,如下:
int heap_init(struct ptr_heap *heap, size_t size, gfp_t gfp_mask,
int (*gt)(void *, void *))
{

    heap->ptrs = kmalloc(size, gfp_mask);

if (!heap->ptrs)
return -ENOMEM;
heap->size = 0;

    heap->max = size / sizeof(void *);

heap->gt = gt;
return 0;
}
Heap->ptrs是一个二级指针,也可以将它看成是一个指针数据.heap->size是表示存放区域的大小,heap->max是表示存放对象的个数,它的大小等于总空间除以每个指针的大小,即(sizeof(void *)),heap->gt是比较函数,用来确定元素在堆中的位置。
again:
/*

     * Scan tasks in the cgroup, using the scanner's "test_task" callback

     * to determine which are of interest, and using the scanner's

     * "process_task" callback to process any of them that need an update.

     * Since we don't want to hold any locks during the task updates,

     * gather tasks to be processed in a heap structure.

     * The heap is sorted by descending task start time.

     * If the statically-sized heap fills up, we overflow tasks that

     * started later, and in future iterations only consider tasks that

     * started after the latest task in the previous pass. This

     * guarantees forward progress and that we don't miss any tasks.

*/
heap->size = 0;
/*调用cgroup iter来取得cgroup中的所有task*/
cgroup_iter_start(scan->cg, &it);

    while ((p = cgroup_iter_next(scan->cg, &it))) {

/*

         * Only affect tasks that qualify per the caller's callback,

         * if he provided one

*/
/*调用test_task来测试该task是否是需要update*/

        if (scan->test_task && !scan->test_task(p, scan))

continue;
/*

         * Only process tasks that started after the last task

         * we processed

*/

        if (!started_after_time(p, &latest_time, latest_task))

continue;
/*将进程p添加到heap中*/
dropped = heap_insert(heap, p);
/*添加成功.增加task的引用计数*/
if (dropped == NULL) {
/*
* The new task was inserted; the heap wasn't
* previously full
*/
get_task_struct(p);
}
/*heap已经满了,它踢出了一个*/
/*如果踢出的不和要加入的相等,要更其它们的引用计数*/
else if (dropped != p) {
/*
* The new task was inserted, and pushed out a
* different task
*/
get_task_struct(p);
put_task_struct(dropped);
}
/*

         * Else the new task was newer than anything already in

         * the heap and wasn't inserted

*/
/*如果是要加入的task加入失败.不需要做任何处理,处理下一个task*/
}
/*cgroup iter使用完成*/
cgroup_iter_end(scan->cg, &it);
cgroup_iter在分析cgroup框架的时候已经分析过,它就是一个遍历cgroup中task的迭代器.在上述代码中可以看到,要将进程加到heap中,要满足二个条件:
1:如果scan->test_task被设置的话,那么scan->test_task()必须要返回1.
2:必须started_after_time()不为0.这个函数定义如下示:
static inline int started_after_time(struct task_struct *t1,
struct timespec *time,
struct task_struct *t2)
如果ti>time返回1.如果t1等于time,那么当t1>t2的时候返回1.
结合上面的代码, latest_task设置为NULL, latest_time设置成了0,0.因此在刚开始的时候,所有的task都会满足started_after_time().
当heap满了的时候,就是丢掉了一个heap->gt()值最大项.也就是heap_insert()返回不为空的时候.
在这个函数中,heap->gt为started_after().代码如下:
static inline int started_after(void *p1, void *p2)
{

    struct task_struct *t1 = p1;

    struct task_struct *t2 = p2;

    return started_after_time(t1, &t2->start_time, t2);

}
从此可以看到,它就是将现在heap中task->start_time或者是task最大项丢出来了.
在后续的处理中,lastst_time.laters_task被更新成如下所示:
if (i == 0) {

                latest_time = q->start_time;

                latest_task = q;

}
它就是将它们的值设为了当前heap中的相关最大值.
综合上面的分析.在满了的时候被丢出来的task对应的task->start_time或者task一定会大于heap中的最大匹配值.因此这些被挤出来的task在下一次遍历的时候就会被加进heap,而那些已经处理过的,就不能添加进去了.
/*现在要处理的task已经都放入了heap中.update heap中的task*/
if (heap->size) {
for (i = 0; i < heap->size; i++) {

            struct task_struct *q = heap->ptrs[i];

if (i == 0) {
latest_time = q->start_time;
latest_task = q;
}

            /* Process the task per the caller's callback */

scan->process_task(q, scan);
put_task_struct(q);
}
/*

         * If we had to process any tasks at all, scan again

         * in case some of them were in the middle of forking

         * children that didn't get processed.

         * Not the most efficient way to do it, but it avoids

         * having to take callback_mutex in the fork path

*/
/*在前面的处理中,可能因为各种原因还有其它的task

           *末被处理,跳转到前面再处理一次

           */

goto again;
}
现在heap中已经有了数据了,就调用heap->>process_task处理heap中的task.在它末尾有一个goto again.它是返回到函数的最前面,来处理那些被挤出来的task.
/*如果heap是在本函数中分配的空间.释放之*/
if (heap == &tmp_heap)
heap_free(&tmp_heap);
return 0;
}
Heap中已经没有数据了,说明cgroup中的task已经全部都处理完了.如果heap是系统分配的,那么释放掉它的空间.
这段代码中涉及到的堆排序算法,鉴于篇幅原因,这里就不详细分析了.不理解代码的可以参考<<算法导论>>的第七章.
6.2:关于CS_MEM_HARDWALL标志
CS_MEM_HARDWALL标志有一些特殊的处理.有这里有必要单独指出来.
在页面分配器中,有如下代码片段:

static struct page *

get_page_from_freelist(gfp_t gfp_mask, nodemask_t *nodemask, unsigned int order,

        struct zonelist *zonelist, int high_zoneidx, int alloc_flags)

{
......
......

    if ((alloc_flags & ALLOC_CPUSET) &&

            !cpuset_zone_allowed_softwall(zone, gfp_mask))

goto try_next_zone;
......
......
}
上面的这段代码对是否可以在zone上分配内存的判断.如果定义了ALLOC_CPUSET分配标志,那么必须要受cpuset的限制.跟踪cpuset_zone_allowed_softwall()代码如下示:

static int inline cpuset_zone_allowed_softwall(struct zone *z, gfp_t gfp_mask)

{
return number_of_cpusets <= 1 ||

        __cpuset_zone_allowed_softwall(z, gfp_mask);

}
如果系统中总共才1个cpuset(top_cpuset),那就没必要进行下面的判断了,如果有很多cpuset,流程转入__cpuset_zone_allowed_softwall().代码如下:

int __cpuset_zone_allowed_softwall(struct zone *z, gfp_t gfp_mask)

{

    int node;           /* node that zone z is on */

const struct cpuset *cs;    /* current cpuset ancestors */

    int allowed;            /* is allocation in zone z allowed? */

/*如果在中断环境或者在设置__CFP_THISNODE的情况下.允许*/

    if (in_interrupt() || (gfp_mask & __GFP_THISNODE))

return 1;
/*该zone所在的节点*/
node = zone_to_nid(z);
/*在没有带__GFP_HARDWALL情况下会引起睡眠*/

    might_sleep_if(!(gfp_mask & __GFP_HARDWALL));

/*如果要分配的结点是当进程所允许的.允许*/

    if (node_isset(node, current->mems_allowed))

return 1;
/*

     * Allow tasks that have access to memory reserves because they have

     * been OOM killed to get memory anywhere.

*/
/*如果当前进程含有TIF_MEMDIE.允许*/
if (unlikely(test_thread_flag(TIF_MEMDIE)))
return 1;
/*如果带了__GFP_HARDWALL标志.表示只能在该进程所属的cpuset
*的结点上分配内存.运行到这里,说明当前进程
*所在的cpuset并没有包含这个内存节点,这个结点是不允许的
*/
if (gfp_mask & __GFP_HARDWALL)  /* If hardwall request, stop here */
return 0;
/*进程正在退出了*/

    if (current->flags & PF_EXITING) /* Let dying task have memory */

return 1;

    /* Not hardwall and node outside mems_allowed: scan up cpusets */

/*运行到这里的话.说明进程所属的cpuset 没有包含这个结点
* 且又没有指定__CFP_HARDWALL标记.可以从它的父结点中选择
* 内存结点
*/
mutex_lock(&callback_mutex);
task_lock(current);

    cs = nearest_hardwall_ancestor(task_cs(current));

task_unlock(current);
/*判断找到当前zone所在节点是否在cs->mems_allowed中
*如果是,返回1.否则返回0
*/

    allowed = node_isset(node, cs->mems_allowed);

mutex_unlock(&callback_mutex);
return allowed;
}
这个函数是对是否可以在这个zone上分配内存的判断.有以下情况:
1:在中断环境下,或者是用户使用了__GFP_THISNODE指明在该node上分配.这很好理解.中断环境中的内存分配请求应该是尽量满足的.
2:如果该zone所在node是cpuset中所规定的,毫无疑问,可以分配.(cpuset中的mems_allowed反映在所关联进程的task->mems_allowed中)
3:进程包含TIF_MEMDIE标志
在系统内存极度紧张的时候,连一些系统服务都不能满足了,那就必须要选择一个进程终止,这个选择出的进程就会被设置TIF_MEMDIE标志.这类进程马上就要被kill或者正在被kill.
4: 带有__GFP_HARDWALL标志.且不为上面所说的几种条件,不能在这个节点上进行分配,没有商量的余地.
5:进程带有PF_EXITING标志,说明进程正在退出了.满足.
6:其它的情况,判断就会进入到nearest_hardwall_ancestor().代码如下:

static const struct cpuset *nearest_hardwall_ancestor(const struct cpuset *cs)

{

    while (!(is_mem_exclusive(cs) || is_mem_hardwall(cs)) && cs->parent)

cs = cs->parent;
return cs;
}
从上面可以看到,如果当前cpuset没有设置CS_MEM_EXCLUSIVE或者CS_MEM_HARDWALL.就可以找到它的最上层的没有设置这两个标志的cpuset.如果请求的节点在经过调整之后的cpuset中,满足.
我们在这里看到的CS_MEM_HARDWALL和CS_MEM_EXCLUSIVE的功能,它们的区别是, CS_MEM_EXCLUSIVE使cpuset拥有独立的内存结点,而CS_MEM_HARDWALL却没有这个限制.
七:小结
Cpuset是一个在大系统上常用的功能.这部份涉及到进程调度和内存分配方面的东西,如果对这些周边知识有不了解的地方.可以参阅本站的其它文档.

这篇关于Linux cgroup机制分析之cpuset subsystem的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/375468

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

linux-基础知识3

打包和压缩 zip 安装zip软件包 yum -y install zip unzip 压缩打包命令: zip -q -r -d -u 压缩包文件名 目录和文件名列表 -q:不显示命令执行过程-r:递归处理,打包各级子目录和文件-u:把文件增加/替换到压缩包中-d:从压缩包中删除指定的文件 解压:unzip 压缩包名 打包文件 把压缩包从服务器下载到本地 把压缩包上传到服务器(zip

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

Linux_kernel驱动开发11

一、改回nfs方式挂载根文件系统         在产品将要上线之前,需要制作不同类型格式的根文件系统         在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统         优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命         【1】重启上位机nfs服务         sudo service nfs-kernel-server resta

SWAP作物生长模型安装教程、数据制备、敏感性分析、气候变化影响、R模型敏感性分析与贝叶斯优化、Fortran源代码分析、气候数据降尺度与变化影响分析

查看原文>>>全流程SWAP农业模型数据制备、敏感性分析及气候变化影响实践技术应用 SWAP模型是由荷兰瓦赫宁根大学开发的先进农作物模型,它综合考虑了土壤-水分-大气以及植被间的相互作用;是一种描述作物生长过程的一种机理性作物生长模型。它不但运用Richard方程,使其能够精确的模拟土壤中水分的运动,而且耦合了WOFOST作物模型使作物的生长描述更为科学。 本文让更多的科研人员和农业工作者

MOLE 2.5 分析分子通道和孔隙

软件介绍 生物大分子通道和孔隙在生物学中发挥着重要作用,例如在分子识别和酶底物特异性方面。 我们介绍了一种名为 MOLE 2.5 的高级软件工具,该工具旨在分析分子通道和孔隙。 与其他可用软件工具的基准测试表明,MOLE 2.5 相比更快、更强大、功能更丰富。作为一项新功能,MOLE 2.5 可以估算已识别通道的物理化学性质。 软件下载 https://pan.quark.cn/s/57