per-CPU是2.6内核中引入的,它是一种典型的空间换时间的方案,通过为每个处理器都分配自己的内存区间来避免并发问题, 访问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上,例如:
- task1在cpu1上访问per-CPU数据 data,获取到了它在cpu1上的数据
- task1被调度到cpu2上,继续访问在cpu1上per-CPU数据
- task2在cpu1上访问per-CPU数据 data,获取到了它在cpu1上的数据
- 此时task1和task2对于data的访问就会出现不确定现象.
我们知道中断上下文在哪个cpu上触发的就会在该cpu上执行,不会出现balance的情况.只有进程上下文才可能会被负载均衡到其他的cpu上,通过禁止抢占的方式来避免task在使用per-cpu数据中间其他任务抢占,而没有在被当前CPU运行的任务都有可能被负载均衡到其他CPU上.
类似方案
- 在32位OS上的临时映射,参考临时映射节