什么是per-CPU变量?
per-CPU变量主要用在多处理器系统中,用来为系统中的每个CPU生成一个变量副本,per-CPU变量对于每个处理器都有一个互相独立的副本。per-CPU变量分为静态分配与动态分配两种,静态分配是指在编译内核期间分配好的per-CPU变量,动态分配是指运行期间调用per-CPU memory allocator 分配的per-CPU变量。
Linux使用Chunk数据结构来管理per-CPU变量的分配,当要分配某个size的per-CPU变量时,每个CPU的per-CPU变量副本都在同一个chunk当中分配,如果一个chunk分配满了,那么会再新增一个chunk.
为了便于分配与查找,Linux按照每个Chunk的空闲空间的size将Chunk链接到不同的list中,每次分配时从满足allocate size要求且空闲空间最小的list中的chunk中进行分配。(There are usually many small percpu allocations many of them being as small as 4 bytes. The allocator organizes chunks into lists according to free size and tries to allocate from the fullest one.)
下面是源码分析:
参考源代码: Linux 3.10
Linux实现per-CPU模块的源文件主要位于 /include/linux/percpu.h 和 mm/percpu.c中。
struct Chunk定义如下:
struct pcpu_chunk {
struct list_head list; /* linked to pcpu_slot lists */
int free_size; /* free bytes in the chunk */
int contig_hint; /* max contiguous size hint */
void *base_addr; /* base address of this chunk */
int map_used; /* # of map entries used */
int map_alloc; /* # of map entries allocated */
int *map; /* allocation map */
void *data; /* chunk data */
bool immutable; /* no [de]population allowed */
unsigned long populated[]; /* populated bitmap */
};
list: 用于将每个Chunk链接到pcpu_slot lists中,pcpu_slot是一个list_head数组,Linux将所有的Chunk按照其空闲空间的大小链入pcpu_slot数组对应的list中。
free_size: 表示此Chunk空闲空间的大小。
contig_hint: 最大的连续的空闲size
base_addr: 这个Chunk所管理的memory的起始地址(虚拟地址)
map_used: map数组中的已使用成员个数
map_alloc: map数组的大小
map: 用于分配size的数组
Chunk使用map数组来实现指定size的分配,每个数组成员是一个int类型的值,记录了一个分配好的size或一个free size(正数表示可分配的size,负数表示已经分配出去的size),map数组大小初始化为PCPU_DFL_MAP_ALLOC,map_used初始化为1,表示只用了一个map成员来记录,因此初始化后map数组只有map[0]有效,大小为整个Chunk可供分配的size(正数),表示现在Chunk为空。在运行的过程中每当有新的size分配请求,Chunk会在map数组里寻找满足要求的空闲的size,找到后分配指定的size并记录在map数组中,最后将空闲的size减去己分配的size,必要的话会根据情况将某些空闲的size合并。随着不断的进行各种size的动态分配,map_used会一直增长,当初始化的map大小不够用的时候map数组的大小也会增长。(Allocation state in each chunk is kept using an array of integers on chunk->map. A positive value in the map represents a free region and negative allocated. Allocation inside a chunk is done by scanning this map sequentially and serving the first matching entry. )
data: 指向为Chunk分配的vms结构的指针。Chunk管理的memory本质上还是以page的形式分配的(first Chunk除外)。
immutable: 一个布尔变量,置1表示不可再allocate和map page.
populated[]: unsigned long 数组用于记录已经map成功的page
前面说过,Linux使用Chunk数据结构来管理per-CPU变量的分配,现在假设系统有Nr个CPU,那么意味着在外部调用per-cpu allocater分配变量时,Chunk可以同时为Nr个CPU分配per-CPU变量。而实际情况还要复杂一些,linux还要对Nr个CPU分组,这个后面会结合code讨论。
Linux用一组全局变量记录CPU的信息及per-cpu allocater最大可分配内存的信息:
static int pcpu_unit_pages __read_mostly;
每个Chunk中单个CPU可供分配per-cpu变量的内存的大小,单位page。(很明显对于每个CPU,这个值是一样的,因为per-cpu变量是对所有的CPU同时分配的)
static int pcpu_unit_size __read_mostly;
每个Chunk中单个CPU可供分配per-cpu变量的内存的大小,单位byte。单个CPU可供分配per-cpu变量的内存的大小称为一个unit。
static int pcpu_nr_units __read_mostly;
每个Chunk中unit的数量,也就是系统中CPU的数量。
static int pcpu_atom_size __read_mostly;
用于align的size。
static struct list_head *pcpu_slot __read_mostly;
pcpu_slot是list_head数组,按照空闲空间的大小链接各个Chunk到其中不同的list_head中。
static int pcpu_nr_slots __read_mostly;
pcpu_slot数组的size。
static size_t pcpu_chunk_struct_size __read_mostly;
Chunk结构的size,在分配新的Chunk时用到。
void *pcpu_base_addr __read_mostly;
EXPORT_SYMBOL_GPL(pcpu_base_addr);
第一个Chunk所管理内存的基地址,(the address of the first chunk which starts with the kernel static area.)
前面知道全局变量pcpu_uint_size表示单个CPU可供分配的内存大小,又由前面叙述可知: 当要分配某个size的per-CPU变量时,每个CPU的per-CPU变量副本都在同一个chunk当中分配。因此每个Chunk管理的内存大小必须为Nr x (pcpu_uint_size)。
per-cpu变量分为静态分配(编译时分配)与动态分配(运行时分配),先来看静态分配:
per-cpu变量的静态分配通过将变量定义在特殊的数据段中来实现(include/linux/percpu-defs.h):
#define DECLARE_PER_CPU(type, name) \
DECLARE_PER_CPU_SECTION(type, name, "")
#define DEFINE_PER_CPU(type, name) \
DEFINE_PER_CPU_SECTION(type, name, "")
这两个宏静态声明与分配一个类型为type的per-cpu变量。DECLARE_PER_CPU_SECTION和DEFINE_PER_CPU_SECTION又分别定义为:
#define DECLARE_PER_CPU_SECTION(type, name, sec) \
extern __PCPU_ATTRS(sec) __typeof__(type) name
#define DEFINE_PER_CPU_SECTION(type, name, sec) \
__PCPU_ATTRS(sec) PER_CPU_DEF_ATTRIBUTES \
__typeof__(type) name
其中__PCPU_ATTRS定义为:
#define __PCPU_ATTRS(sec) \
__percpu __attribute__((section(PER_CPU_BASE_SECTION sec))) \
PER_CPU_ATTRIBUTES
__percpu是个编译扩展类型,在include/linux/compile.h文件中,__percpu是空的。而传进来的sec也是空的,PER_CPU_ATTRIBUTES也是空的,前面PER_CPU_DEF_ATTRIBUTES还是空的,所以DEFINE_PER_CPU(type, name)展开就是:
__attribute__((section(PER_CPU_BASE_SECTION sec)))
__typeof__(type) name
其中,PER_CPU_BASE_SECTION定义在(include/linux/asm-generic/percpu.h).
#define PER_CPU_BASE_SECTION ".data..percpu"
DEFINE_PER_CPU(type, name)最后展开就是: