作者 [email protected]
1.2 Percpu内存管理
随着处理器核心的增加,内核中系统中并发的线程也随之增加,这样对一些共享数据的同时访问机率也就增加,就避免不了spin_lock的使用,而且往往处理器核心越多造成的麻烦越大。Percpu内存对这种数据无能为力,但是内核中有些数据只是处理器局部可见,这种数据不会被别的处理器访问到,不需要加以spin_lock而直接访问。Percpu内存适用于这种数据,而实际上处理器局部数据的实际分配还是通过内核基本slab进行,而Percp内存则是用来存放指向处理器局部数据的指针。
1.2.1 处理器静态定义局部数据
首先在链接文件arch/arm/kernel/vmlinux.lds.S中包含了:
PERCPU_SECTION(32)
展开该宏:
#define PERCPU_SECTION(cacheline)
\
. = ALIGN(PAGE_SIZE);
\
.data..percpu
: AT(ADDR(.data..percpu) - LOAD_OFFSET) {
\
VMLINUX_SYMBOL(__per_cpu_load) = .;
\
PERCPU_INPUT(cacheline)
\
}
#define PERCPU_INPUT(cacheline)
\
VMLINUX_SYMBOL(__per_cpu_start) = .;
\
*(.data..percpu..first)
\
. = ALIGN(PAGE_SIZE);
\
*(.data..percpu..page_aligned)
\
. = ALIGN(cacheline);
\
*(.data..percpu..readmostly)
\
. = ALIGN(cacheline);
\
*(.data..percpu)
\
*(.data..percpu..shared_aligned)
\
VMLINUX_SYMBOL(__per_cpu_end) = .;
该宏的作用是声明了以 “.data..percpu..first”,“.data..percpu..readmostly”,“.data..percpu”等为名字的数据段,内核代码里如果有变量的属性指出放在该数据段的时候,链接时将其分配到对应的数据段。而且为了能够在内核里直接访问这段区域,定义两个变量__per_cpu_start,和__per_cpu_end。
而另一方面:
#define DEFINE_PER_CPU(type, name)
\
DEFINE_PER_CPU_SECTION(type, name, "")
#define DEFINE_PER_CPU_SECTION(type, name, sec)
\
__PCPU_ATTRS(sec) PER_CPU_DEF_ATTRIBUTES
\
__typeof__(type) name
#define __PCPU_ATTRS(sec)
\
__percpu __attribute__((section(PER_CPU_BASE_SECTION sec)))
\
PER_CPU_ATTRIBUTES
#define PER_CPU_BASE_SECTION ".data..percpu"
根据以上宏定义展开之:可以得到
__attribute__((section(.data..percpu)))
__typeof__(type) name
可见宏“DEFINE_PER_CPU(type, name)”的作用就是将类型为“type”的“name”变量放倒“.data..percpu”数据段。
同样方法可以推出:宏“DECLARE_PER_CPU_READ_MOSTLY(type, name)” 的作用就是将类型为“type”的“name”变量放倒“.data..percpu..readmostly”数据段。这种数据段是cacheline对齐,可以大大提高cache利用率,很适合频繁读取数据,不过在笔者分析的这个内核版本3.0.13中,内核里这种数据还没有大规模使用的。而且cacheline定义为32,对于不同架构可以使用时可以适当调整。
另外宏“DECLARE_PER_CPU_FIRST(type, name)”对应“.data..percpu..first”数据段。
宏“DECLARE_PER_CPU_PAGE_ALIGNED”对应“.data..percpu..first”数据段。…
然后在内核初始化过程中:
void __init setup_per_cpu_areas(void)
{
rc = pcpu_embed_first_chunk(PERCPU_MODULE_RESERVE,
PERCPU_DYNAMIC_RESERVE, PAGE_SIZE, NULL,
pcpu_dfl_fc_alloc, pcpu_dfl_fc_free);
delta = (unsigned long)pcpu_base_addr - (unsigned long)__per_cpu_start;
/*设置每个cpu的__per_cpu_offset 指针*/
for_each_possible_cpu(cpu)
__per_cpu_offset[cpu] = delta + pcpu_unit_offsets[cpu];
}
再看对percpu数据的引用:
#define per_cpu(var, cpu) \
(*SHIFT_PERCPU_PTR(&(var), per_cpu_offset(cpu)))
#define SHIFT_PERCPU_PTR(__p, __offset)
({
\
__verify_pcpu_ptr((__p));
\
RELOC_HIDE((typeof(*(__p)) __kernel __force *)(__p), (__offset)); \
})
#define per_cpu_offset(x) (__per_cpu_offset[x])
/*选用一个简单易看得RELOC_HIDE 宏定义*/
# define RELOC_HIDE(ptr, off)
\
({ unsigned long __ptr;
\
__ptr = (unsigned long) (ptr);
\
(typeof(ptr)) (__ptr + (off)); })
可见是以当前cpu局部数据“__per_cpu_offset[x]”为基地址的相对寻址。
1.2.2 percpu内存管理的建立
在建立percpu内存管理机制之前要整理出该架构下的处理器信息,包括处理器如何分组、每组对应的处理器位图、静态定义的percpu变量占用内存区域、每颗处理器percpu虚拟内存递进基本单位等信息。本文仅以双核CA9处理器作为分析目标。
对于处理器的分组信息,内核使用“struct pcpu_group_info”结构表示:
struct pcpu_group_info {
/* 该组的处理器数目,对于双核CA9处理器,该值为2 */
int
nr_units;
/*组内处理器数目×处理器percpu虚拟内存递进基本单位*/
unsigned long
base_offset;
/*组内处理器对应数组,双核CA9架构下该数组长度为2*/
unsigned int
*cpu_map;
};
整体的percpu内存管理信息被收集在“struct pcpu_alloc_info”结构中:
struct pcpu_alloc_info {
//静态定义的percpu变量占用内存区域长度
size_t
static_size;
/*预留区域,在percpu内存分配指定为预留区域分配时,将使用该区域*/
size_t
reserved_size;
size_t
dyn_size;
/*每颗处理器的percpu虚拟内存递进基本单位*/
size_t
unit_size;
…
/*该架构下的处理器分组数目,CA9双核架构下该值为1*/
int
nr_groups;
/*该架构下的处理器分组信息,CA9双核架构下该数组长度为1*/
struct pcpu_group_info
groups[];
};
接下来构建静态定义的percpu变量创建percpu区域
/*该函数被“void __init setup_per_cpu_areas(void)”调用,对于CA9架构其参数“cpu_distance_fn”为“NULL”,参数“alloc_fn”为“pcpu_dfl_fc_alloc”*/
int __init pcpu_embed_first_chunk(size_t reserved_size, size_t dyn_size,
size_t atom_size,
pcpu_fc_cpu_distance_fn_t cpu_distance_fn,
pcpu_fc_alloc_fn_t alloc_fn,
pcpu_fc_free_fn_t free_fn)
{
void *base = (void *)ULONG_MAX;
void **areas = NULL;
struct pcpu_alloc_info *ai;
size_t size_sum, areas_size, max_distance;
int group, i, rc;
/*收集整理该架构下的percpu信息,结果放在“struct pcpu_alloc_info”结构中*/
ai = pcpu_build_alloc_info(reserved_size, dyn_size, atom_size,
cpu_distance_fn);
…
//静态定义变量占用空间+reserved空间+动态分配空间
size_sum = ai->static_size + ai->reserved_size + ai->dyn_size;
…
/*针对每个group操作 */
for (group = 0; group < ai->nr_groups; group++) {
struct pcpu_group_info *gi = &ai->groups[group];
unsigned int cpu = NR_CPUS;
void *ptr;
…
/* 为该group分配percpu内存区域。长度为处理器数目X每颗处理器的percpu递进单位。函数“pcpu_dfl_fc_alloc”是从bootmem里取得内存,得到的是物理内存 */
ptr = alloc_fn(cpu, gi->nr_units * ai->unit_size, atom_size);
…
areas[group] = ptr;
base = min(ptr, base);
//为每颗处理器建立其percpu区域
for (i = 0; i < gi->nr_units; i++, ptr += ai->unit_size) {
if (gi->cpu_map[i] == NR_CPUS) {
/*检查组内处理器器,对于没有用到处理器释放其percpu区域 */
free_fn(ptr, ai->unit_size);
continue;
}
/*将定态定义的percpu变量拷贝到每颗处理器percpu区域*/
memcpy(ptr, __per_cpu_load, ai->static_size);
/*为每颗处理器释放掉多余的空间,多余的空间是指ai->unit_size 减去静态定义变量占用空间+reserved空间+动态分配空间*/
free_fn(ptr + size_sum, ai->unit_size - size_sum);
}
}
/* 处理器架构相关的计算,对于CA9双核架构,ai->nr_groups 为1,且“ai->groups[group].base_offset”和“max_distance” 这里的计算结果都为“0”*/
max_distance = 0;
for (group = 0; group < ai->nr_groups; group++) {
ai->groups[group].base_offset = areas[group] - base;
max_distance = max_t(size_t, max_distance,
ai->groups[group].base_offset);
}
max_distance += ai->unit_size;
…
//建立可动态分配的percpu内存区域
rc = pcpu_setup_first_chunk(ai, base);
…
}
//建立可动态分配的percpu内存区域
int __init pcpu_setup_first_chunk(const struct pcpu_alloc_info *ai,
void *base_addr)
{
static char cpus_buf[4096] __initdata;
static int smap[PERCPU_DYNAMIC_EARLY_SLOTS] __initdata;
static int dmap[PERCPU_DYNAMIC_EARLY_SLOTS] __initdata;
….
for (cpu = 0; cpu < nr_cpu_ids; cpu++)
unit_map[cpu] = UINT_MAX;
pcpu_first_unit_cpu = NR_CPUS;
/*针对每一group的每一颗处理器,对于对于双核CA9,“ai->nr_groups”值为“0”“gi->nr_units”值为2*/
for (group = 0, unit = 0; group < ai->nr_groups; group++, unit += i) {
const struct pcpu_group_info *gi = &ai->groups[group];
//该组处理器的percpu偏移量,对于双核CA9,该值为“0”
group_offsets[group] = gi->base_offset;
//该组处理器占用的虚拟地址空间
group_sizes[group] = gi->nr_units * ai->unit_size;
//针对组内的每颗处理器
for (i = 0; i < gi->nr_units; i++) {
cpu = gi->cpu_map[i];
if (cpu == NR_CPUS)
continue;
…
unit_map[cpu] = unit + i;
//计算每颗处理器的percpu虚拟空间偏移量
unit_off[cpu] = gi->base_offset + i * ai->unit_size;
…
}
}
pcpu_nr_units = unit;
…
//记录下全局参数,留在“pcpu_alloc”时使用
pcpu_nr_groups = ai->nr_groups;
…
pcpu_unit_offsets = unit_off;
/* determine basic parameters */
pcpu_unit_pages = ai->unit_size >> PAGE_SHIFT;
pcpu_unit_size = pcpu_unit_pages << PAGE_SHIFT;
pcpu_atom_size = ai->atom_size;
pcpu_chunk_struct_size = sizeof(struct pcpu_chunk) +
BITS_TO_LONGS(pcpu_unit_pages) * sizeof(unsigned long);
/*
构建“pcpu_slot”数组,不同size的chunck挂在不同“pcpu_slot”项目中
*/
pcpu_nr_slots = __pcpu_size_to_slot(pcpu_unit_size) + 2;
pcpu_slot = alloc_bootmem(pcpu_nr_slots * sizeof(pcpu_slot[0]));
//初始化“pcpu_slot”数组链头
for (i = 0; i < pcpu_nr_slots; i++)
INIT_LIST_HEAD(&pcpu_slot[i]);
/*
构建静态chunck即“pcpu_reserved_chunk”,该区域的物理内存以及虚拟地址都在“int __init pcpu_embed_first_chunk(…)”里分配了。
*/
schunk = alloc_bootmem(pcpu_chunk_struct_size);
…
schunk->immutable = true;
//物理内存已经分配这里标志之
bitmap_fill(schunk->populated, pcpu_unit_pages);
…
if (ai->reserved_size) {
//reserved的空间,在指定reserved分配时使用
schunk->free_size = ai->reserved_size;
pcpu_reserved_chunk = schunk;
//定义的静态变量的空间也算进来
pcpu_reserved_chunk_limit = ai->static_size + ai->reserved_size;
} else {
schunk->free_size = dyn_size;
dyn_size = 0;
/* dynamic area covered */
}
schunk->contig_hint = schunk->free_size;
schunk->map[schunk->map_used++] = -ai->static_size;
if (schunk->free_size)
schunk->map[schunk->map_used++] = schunk->free_size;
/* 动态分配空间,这里构建第一个chunck,该chunk是第一次步进时静态变量空间和reserved空间使用后剩下的*/
if (dyn_size) {
dchunk = alloc_bootmem(pcpu_chunk_struct_size);
INIT_LIST_HEAD(&dchunk->list);
…
//记录下来分配的物理页
bitmap_fill(dchunk->populated, pcpu_unit_pages);
dchunk->contig_hint = dchunk->free_size = dyn_size;
/*map指针更新将静态变量空间和reserved空间甩在后面*/
dchunk->map[dchunk->map_used++] = -pcpu_reserved_chunk_limit;
dchunk->map[dchunk->map_used++] = dchunk->free_size;
}
/* 把第一个chunk链接进对应的slot链表,reserverd 的空间有自己单独的chunk:pcpu_reserved_chunk */
pcpu_first_chunk = dchunk ?: schunk;
pcpu_chunk_relocate(pcpu_first_chunk, -1);
/* we're done */
pcpu_base_addr = base_addr;
return 0;
}
1.2.3 percpu动态分配内存空间
关于percpu动态分配内存空间有以下基本概念
每颗处理器的自己percpu动态分配内存空间都有不同的虚拟地址空间。否则同一线程在不同处理器上运行时修改页表页目录项的开销太大。
Chunk记录每颗处理器一次步进得到虚拟地址空间,对于一颗处理器来说一次步进长度是“ai->unit_size”,percpu内存的虚拟地址以chunk为基础
Chunk中每一次配出的内存用其map[]指向
Chunk根据其free的内存长度挂到Slot数组的对应链表上
每次步进仅得到虚拟地址空间,在完成一次分配时才得到对应的物理内存
本书仅考虑percpu-vm的情况
/*动态分配函数 */
static void __percpu *pcpu_alloc(size_t size, size_t align, bool reserved)
{
static int warn_limit = 10;
struct pcpu_chunk *chunk;
const char *err;
int slot, off, new_alloc;
unsigned long flags;
mutex_lock(&pcpu_alloc_mutex);
spin_lock_irqsave(&pcpu_lock, flags);
/* 指定reserved分配,从pcpu_reserved_chunk进行 ,较简单不讨论*/
if (reserved && pcpu_reserved_chunk) {
…
}
restart:
/* 根据需要分配内存块的大小索引slot数组找到对应链表 */
for (slot = pcpu_size_to_slot(size); slot < pcpu_nr_slots; slot++) {
list_for_each_entry(chunk, &pcpu_slot[slot], list) {
//在该链表中进一步寻找符合尺寸要求的chunk
if (size > chunk->contig_hint)
continue;
/*chunck用数组“int
*map”记录每次分配的内存块,若该数组用完(该chunk仍然还有自由空间),则需要增长该“int *map”数组。*/
new_alloc = pcpu_need_to_extend(chunk);
if (new_alloc) {
spin_unlock_irqrestore(&pcpu_lock, flags);
//扩展“int *map”数组。
if (pcpu_extend_area_map(chunk,
new_alloc) < 0) {
…
}
spin_lock_irqsave(&pcpu_lock, flags);
goto restart;
}
/*在该chunk里分配虚拟内存空间:分割最后一段自由空间,然后重新将该chunk挂到slot数组对应链表中*/
off = pcpu_alloc_area(chunk, size, align);
//off大于0表示分配成功
if (off >= 0)
goto area_found;
}
}
spin_unlock_irqrestore(&pcpu_lock, flags);
/*创建一个新的chunk,这里进行的是虚拟地址空间的分配 */
chunk = pcpu_create_chunk();
…
spin_lock_irqsave(&pcpu_lock, flags);
//把一个全新的chunk挂到slot数组对应链表中
pcpu_chunk_relocate(chunk, -1);
goto restart;
area_found:
spin_unlock_irqrestore(&pcpu_lock, flags);
/* 一次percpu内存分配成功,这里要检查该段区域对应物理页是否已经分配,否者将为该区域分配对应的物理页并作填充L1 L2页表项*/
if (pcpu_populate_chunk(chunk, off, size)) {
…
}
mutex_unlock(&pcpu_alloc_mutex);
/* return address relative to base address */
return __addr_to_pcpu_ptr(chunk->base_addr + off);
…
}