每CPU变量

每CPU变量(per-cpu-variable)是内核中一种重要的同步机制。顾名思义,每CPU变量就是为每个CPU构造一个变量的副本,这样多个CPU相互操作各自的副本,互不干涉。比如我们标识当前进程的变量current_task就被声明为每CPU变量。

每CPU变量的特点:
1、用于多个CPU之间的同步,如果是单核结构,每CPU变量没有任何用处。
2、每CPU变量不能用于多个CPU相互协作的场景。(每个CPU的副本都是独立的)
3、每CPU变量不能解决由中断或延迟函数导致的同步问题
4、访问每CPU变量的时候,一定要确保关闭进程抢占,否则一个进程被抢占后可能会更换CPU运行,这会导致每CPU变量的引用错误。

我们可以用数组来实现每CPU变量吗?比如,我们要保护变量var,我们可以声明int var[NR_CPUS],CPU num就访问var[num]不就可以了吗?
显然,每CPU变量的实现不会这么简单。理由:我们知道为了加快内存访问,处理器中设计了硬件高速缓存(也就是CPU的cache),每个处理器都会有一个硬件高速缓存。如果每CPU变量用数组来实现,那么任何一个CPU修改了其中的内容,都会导致其他CPU的高速缓存中对应的块失效。而频繁的失效会导致性能急剧的下降。

每CPU变量分为静态和动态两种,静态的每CPU变量使用DEFINE_PER_CPU声明,在编译的时候分配空间;而动态的使用alloc_percpu和free_percpu来分配回收存储空间。下面我们来看看Linux中的具体实现:

DECLARE_PER_CPU(type, name)  静态分配一个每CPU数组,数组名为name,结构类型为type

per_cpu(name, cpu)           为CPU选择一个每CPU数组元素,CPU由参数CPU指定,数组名为name

_get_per_cpu(name)           选择每CPU数组name的本地CPU元素

per_cpu_var(name)            先禁用内核抢占,然后在每CPU数组name中,为本地CPU选择元素

put_cpu_var(name)            启用内核抢占

alloc_percpu(type)           动态分配type类型数据结构的每CPU数组,并返回它的地址

free_percpu(pointer)         释放被动态分配的每CPU数组

per_cpu_ptr(pointer, cpu)    返回每CPU数组中与参数cpu对应的cpu元素地址

通常情况下,静态声明的每CPU变量都会被编译在ELF文件中的以“.data..percpu”开头的段中(默认情况就是.data.percpu,也可以使用DEFINE_PER_CPU_SECTION(type,name,sec)来指定段的后缀)

#ifdef CONFIG_SMP
#define PER_CPU_BASE_SECTION ".data..percpu"
#else
#define PER_CPU_BASE_SECTION ".data"
#endif
#define __PCPU_ATTRS(sec)						\
	__percpu __attribute__((section(PER_CPU_BASE_SECTION sec)))	\
	PER_CPU_ATTRIBUTES
#define DEFINE_PER_CPU_SECTION(type, name, sec)				\
	__PCPU_ATTRS(sec) PER_CPU_DEF_ATTRIBUTES			\
	__typeof__(type) name
#define DEFINE_PER_CPU(type, name)					\
	DEFINE_PER_CPU_SECTION(type, name, "")
备注:每CPU变量的声明和普通变量的声明一样,主要的区别是使用了__attribute__((section(PER_CPU_BASE_SECTION sec)))来指定该变量被放置的段中,普通变量默认会被放置data段或者bss段中。

看到这里有一个问题:如果我们只是声明了一个变量,那么如何有多个副本的呢?奥妙在于内核加载的过程。
一般情况下,ELF文件中的每一个段在内存中只会有一个副本,而.data.percpu段再加载后,又被复制了NR_CPUS次,一个每CPU变量的多个副本在内存中是不会相邻。示意图如下:


/arch/i386/kernel/vmlinux.lds.S根据指明的section属性,我们知道在内核启动过程中该节所占的存储空间被释放了

SECTIONS
{

...

 __per_cpu_start = .;
  .data.percpu  : { *(.data.percpu) }
  __per_cpu_end = .;

...

}

具体的代码参加start_kernel中调用的setup_per_cpu_areas函数。代码如下:

static void __init setup_per_cpu_areas(void)
{
	unsigned long size, i;
	char *ptr;
	/* Created by linker magic */
	extern char __per_cpu_start[], __per_cpu_end[];

	/* Copy section for each CPU (we discard the original) */
	size = ALIGN(__per_cpu_end - __per_cpu_start, SMP_CACHE_BYTES);
#ifdef CONFIG_MODULES
	if (size < PERCPU_ENOUGH_ROOM)
		size = PERCPU_ENOUGH_ROOM;
#endif

	ptr = alloc_bootmem(size * NR_CPUS);

	for (i = 0; i < NR_CPUS; i++, ptr += size) {
		__per_cpu_offset[i] = ptr - __per_cpu_start;
		memcpy(ptr, __per_cpu_start, __per_cpu_end - __per_cpu_start);
	}
}

备注:__per_cpu_offset数组中记录了每个CPU的percpu区域的开始地址。我们访问每CPU变量就要依靠__per_cpu_offset中的地址。

动态每CPU变量
了解了静态的每CPU变量的实现机制后,就很容易想到动态的每CPU变量的实现方法了。实际上,在setup_per_cpu_areas的时候,我们会为每个CPU都多申请一部分空间留作动态分配每CPU变量之用(一个场景就是内核模块中的每CPU变量)。相对于静态的每CPU变量,我们需要额外管理内存的分配和回收。

每CPU变量的访问

#define RELOC_HIDE(ptr, off)					\
  ({ unsigned long __ptr;					\
     __ptr = (unsigned long) (ptr);				\
    (typeof(ptr)) (__ptr + (off)); })
#define DEFINE_PER_CPU(type, name) \
    __attribute__((__section__(".data.percpu"))) __typeof__(type) per_cpu__##name

/* var is in discarded region: offset to particular copy we want */
#define per_cpu(var, cpu) (*RELOC_HIDE(&per_cpu__##var, __per_cpu_offset[cpu]))
#define __get_cpu_var(var) per_cpu(var, smp_processor_id())
这里所谓的变量per_cpu__##var,其实就是一个偏移量,标识该变量的地址。 per_cpu_offset指的是每个cpu副本的私有数据区相对于 __per_cpu_start的offset




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值