linux内存管理-per cpu数据管理

per-CPU是2.6内核中引入的,它是一种典型的空间换时间的方案,通过为每个处理器都分配自己的内存区间来避免并发问题, 访问per-CPU变量几乎不需要锁,只需要微不足道的原子操作.
per-cpu数据管理

每个处理器都在其自己的副本上工作,这些副本是如何生成的呢?

静态分配的per-CPU数据

静态分配的per-CPU结构设计分为两个阶段:编译阶段和运行时阶段

使用DEFINE_PER_CPU可以静态分配per-cpu结构,它通过编译器指示__attribute__((__section__(".data.percpu")))在编译阶段将所有的per-CPU放入到data.percpu的section中,它实际上只生成了一个CPU原本。
以x86上为例,在arch/x86_64/kernel/vmlinux.lds.S链接脚本有如下内容,__per_cpu_start表示section加载时的起始地址,__per_cpu_end表示section加载时的结束地址:

    . = ALIGN(CONFIG_X86_L1_CACHE_BYTES);                                       
  __per_cpu_start = .;                                                          
  .data.percpu  : AT(ADDR(.data.percpu) - LOAD_OFFSET) { *(.data.percpu) }      
  __per_cpu_end = .;                                                            
  . = ALIGN(4096);                                                              
  __init_end = .;

由上面链接脚本发现, .data.percpu Section是在init数据段的,在系统初始化结束后将被回收。那么,系统如何维持per-CPU数据呢?这个任务在运行时完成。在系统初始化阶段setup_per_cpu_areas会为每个CPU分配一片( __per_cpu_end - __per_cpu_start)大小的内存,然后将 data.percpu段中的CPU原本拷贝NR_CPU份到这块内存中,在_cpu_pda数组中记录当前CPU per-cpu副本地址区域和原本的地址区域的差值data_offset= cpu ptr - __per_cpu_start。系统通过per_cpu访问per-CPU变量的时候就会根据自己的cpu_id找到对应的地址偏移data_offset,最终cpu访问的地址就是自己私有的区间:addr = data_offset + __per_cpu_start + &data

动态分配的per-CPU数据

动态per-CPU结构相对于静态结构来说,设计上更直观,但效率上要低一些。每次调用alloc_percpu(type)的时候会生成一个维度为NR_CPUS的指针数组,每个指针指向一个kzalloc/kmalloc_node出来的type型对象。Linux在这里采取了一个优化手段:如果第i个cpu所属的node在线(linux支持cpu的hot-plug),那么就采用kmalloc_node来分配空间,这个空间与cpu i的亲和性很高;如果cpu i不在线,则采用通用的kzalloc分配了,对于UMA没啥鸟用。下面是空间分配代码:

void *percpu_populate(void *__pdata, size_t size, gfp_t gfp, int cpu)                                           
{                                                                                                               
    struct percpu_data *pdata = __percpu_disguise(__pdata);                                                     
    int node = cpu_to_node(cpu);                                                                                
                                                                                                                
    BUG_ON(pdata->ptrs[cpu]);                                                                                   
    if (node_online(node)) {                                                                                    
        /* FIXME: kzalloc_node(size, gfp, node) */                                                              
        pdata->ptrs[cpu] = kmalloc_node(size, gfp, node);                                                       
        if (pdata->ptrs[cpu])                                                                                   
            memset(pdata->ptrs[cpu], 0, size);                                                                  
    } else                                                                                                      
        pdata->ptrs[cpu] = kzalloc(size, gfp);                                                                  
    return pdata->ptrs[cpu];                                                                                    
} 

对于动态生成的per-CPU变量需要用per_cpu_ptr来访问。

在后续的版本实现中,又有改动,变来变去绕的头都大了,没啥实际改动需求不建议去读这些代码了.
对于静态分配的per-cpu数据,实际上是根据cpu副本的地址和原本的地址的差值来进行数据地址和cpu副本数据地址的互相转换,在2.6版本的内核又使用了void *ptrs[NR_CPUS]来保存cpu的副本地址,这样就需要使用两套接口了,虽然原理相同但是两套接口就比较别扭了,后来终于统一了接口,改日再说.

per-CPU的使用

1.对于数据精度要求不高的场景,例如统计网卡收发包数据

使用per-CPU数据时如何避免并发访问

per-CPU解决的问题是多CPU间并发访问同一个数据的问题,但是在单个CPU上不同上下文都会访问的数据仍然需要加锁操作,例如在中断上下文和进程上下文中都会操作的,那么操作时就需要禁止中断的操作.
除了通过为per-CPU分配私有的空间之外,它还需要在访问时任务调度到其他cpu上,例如:

  1. task1在cpu1上访问per-CPU数据 data,获取到了它在cpu1上的数据
  2. task1被调度到cpu2上,继续访问在cpu1上per-CPU数据
  3. task2在cpu1上访问per-CPU数据 data,获取到了它在cpu1上的数据
  4. 此时task1和task2对于data的访问就会出现不确定现象.

我们知道中断上下文在哪个cpu上触发的就会在该cpu上执行,不会出现balance的情况.只有进程上下文才可能会被负载均衡到其他的cpu上,通过禁止抢占的方式来避免task在使用per-cpu数据中间其他任务抢占,而没有在被当前CPU运行的任务都有可能被负载均衡到其他CPU上.

类似方案

  1. 在32位OS上的临时映射,参考临时映射节
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章