SZ神庙

从此开始,遁入幻想

2017年1月14日

Linux 秘术记闻>

Linux内核同步之每CPU变量

在阅读Linux内核源代码的时候,经常会碰到诸如per_cpu()、early_per_cpu()等宏,这些宏是什么意思呢?

一.什么是每cpu变量

前一篇文章介绍了用自旋锁实现内核同步的方法。实际上,无论是自旋锁、互斥锁、信号量还是其他的一些同步技术,都不可避免地会带来一定的同步开销。

最简单的一种同步方法就是把内核变量声明为每CPU变量,这种情况下,每个CPU保存变量的一份副本,且只能访问自己拥有的那份副本。这就从根本上消除了数据竞争。

每CPU变量的局限性在于,它只能在特殊场合使用,也就是各CPU上的该变量在逻辑上是独立的时候。

二. 每CPU变量的初始化

DEFINE_PER_CPU宏用于声明每CPU变量:

其中type是变量的类型,而name是变量的名称。

DEFINE_PER_CPU_SECTION宏的定义则如下:

可见,对于SMP环境,DEFINE_PER_CPU宏实际上将变量定义到.data..percpu这个数据段内,而对于非SMP环境,则简单地将变量定义到.data数据段内,实际上也就是普通的全局变量。在SMP环境下,内核在启动时调用setup_per_cpu_areas()函数来为每个CPU加载.data..percpu数据段。setup_per_cpu_areas()函数和具体硬件平台有关。在x86平台下,该函数代码如下:

这个函数首先读取最大CPU数目、实际CPU数目等信息,然后检查pcpu_chosen_fc,,即首个数据块分配器。这个分配器在内核中通过percpu_alloc命令初始化。percpu_alloc命令的处理函数定义于mm/percpu.c中:

而percpu_alloc_setup函数根据命令行参数负责设置pcpu_chosen_fc,默认值为auto。接下来,根据分配器是否为auto,内核分别使用内置分配器和页分配器来分配percpu变量的存储空间:

2.1 内置分配器

内置分配器使用pcpu_embed_first_chunk()函数:

各参数意义如下:

  • PERCPU_FIRST_CHUNK_RESERVE:为静态percpu变量保存的空间大小。
  • dyn_size:动态分配空间时最少释放的空间大小。
  • atom_size:原子分配大小。
  • pcpu_cpu_distance:用于计算各cpu间distance的回调函数。
  • pcpu_fc_alloc:负责分配内存的函数。
  • pcpu_fc_free:负责释放内存的函数。

2.2 页分配器

当pcpu_chosen_fc为auto时,内核采用页分配器:

在完成了percpu首个数据块的分配后,内核调用setup_percpu_segment()函数来设置percpu数据段。完成这些工作以后,内核就为每个cpu加载好了.data..percpu数据段的内容。

三.每cpu变量的访问

Linux使用put_cpu_var和get_cpu_var()宏来访问每cpu变量。get_cpu_var()代码如下:

per_cpu_ptr宏返回第二个参数指定的cpu的每cpu变量的地址:

可见,这个宏主要做的操作是:

  1. 调用__verify_pcpu_ptr(ptr)来将ptr转为const void __percpu *类型。
  2. 调用SHIFT_PERCPU_PTR宏,这个宏调用RELOC_HIDE宏,返回ptr+offset,从而得到所要访问的percpu变量的地址。