Chinaunix首页 | 论坛 | 博客
  • 博客访问: 154728
  • 博文数量: 41
  • 博客积分: 2246
  • 博客等级: 大尉
  • 技术积分: 375
  • 用 户 组: 普通用户
  • 注册时间: 2009-08-27 15:45
文章分类
文章存档

2012年(4)

2011年(1)

2010年(11)

2009年(25)

分类: LINUX

2010-07-08 18:32:31

------------------------------------------
本文系本站原创,欢迎转载!
转载请注明出处: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 to 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是一个在大系统上常用的功能.这部份涉及到进程调度和内存分配方面的东西,如果对这些周边知识有不了解的地方.可以参阅本站的其它 文档.
阅读(2014) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~