卷一 内核源代码分析 第一章 linux内核对cortex A9多核处理器的支持 1.2 Percpu内存管理 图书试读版-请勿转载


作者 [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);



}

你可能感兴趣的:(kernel,多核,linux内核,arm-linux,smp)