Linux cgroup机制分析之cpuset subsystem 【转】

本文介绍了Linux内核中的cpuset机制,主要涉及cpuset数据结构及其与cgroup的关系。cpuset_mems_generation作为全局变量用于对比,确保任务分配内存时使用最新的mems_allowed。当cpuset的mems_allowed改变时,会更新任务的cpuset_mems_generation,从而触发内存分配前的更新。此外,文章还提及了cpuset初始化、任务迁移以及内存分配的相关函数和流程。
摘要由CSDN通过智能技术生成
一:前言
前面已经分析了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&#
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值