对Linux内核中percpu data进行分析

最近在看Linux源代码,Linux是将所有处理器的任务状态段存放在init_tss这一结构中,不料Linux内核将init_tss定义percpu data,所以深入对percpu data进行分析。以下是我的分析过程,其中我觉得最重要的就是那幅图和最后的计算分析。

介绍:在多处理器中,为了保证内存访问的正确性,保护某个cpu私有的数据不被其他cpu所访问,同时避免CPU同步的发生;将这些数据存放到指定的字节(section中,详见AT&T汇编,我在这里就将这一区域称为镜像),然后为每个CPU保留这个镜像的副本,每个cpu只能访问它自己的副本。下面以init_tss为例子详细讲解percpu数据。

init_tss数组中存放的是tss。在Linux内核中,每个cpu均有一个独立的全局段描述符表(GDT),每个GDT里面有唯一一个任务状态段(TSS),而所有CPUTSS均存储在init_tss中。在Linux中,TSS段只用于在进程由用户态向内核态切换时获得内核栈的地址和I/O访问时的权限确定过程中(详情参见ULK中第三章进程中的任务状态段);这并不像原始的Intel设计那样,将任务切换的现场保存在TSS中,而是需要额外的字段(thread_struct)来保存进程切换的现场(详情参见ULK中第三章进程中的任务状态段)。

 

源代码分析:

首先在vimlinuz.lds.S中,为所有的“percpu”变量定义一个独立的字节section: .data.percpu

. =ALIGN(32);

__per_cpu_start = . ;

.data.percpu : { *(.data.percpu) }

__per_cpu_end = . ;

percpu的数据就存放在__per_cpu_start__per_cpu_end之间,注意这里只是保留了一个cpu所需存放percpu数据的空间,仅仅作为一个镜像使用(我是这样子理解的,后面在内核初始化时会通过start_kernel函数调用setup_per_cpu_areas来为每个cpu定义一个percpu副本,在后面会详细介绍到setup_per_cpu_areas函数)。

DEFINE_PER_CPU:这个宏用来静态定义"每个CPU"对象,并定义per_cpu__get_cpu_var两个宏来获取每个cpu私有的init_tss

#ifdefCONFIG_SMP

#defineDEFINE_PER_CPU(type, name) \

   __attribute__((__section__(".data.percpu"))) __typeof__(type)per_cpu__##name

#defineper_cpu(var, cpu) (*RELOC_HIDE(&per_cpu__##var, __per_cpu_offset[cpu]))

#define__get_cpu_var(var) per_cpu(var, smp_processor_id())

 

#else

 DEFINE_PER_CPU(type, name) \

   __typeof__(type) per_cpu__##name

 

#define per_cpu(var, cpu)          (*((void)cpu, &per_cpu__##var)) /*逗号表达式,返回per_cpu__##var的地址*/

#define __get_cpu_var(var)         per_cpu__##var                           /*返回per_cpu_##var的地址*/

#endif

 

/*

**定义RELOC_HIDE宏,通过该宏能够将返回指定cpudper_cpu_##var的首地址

*/

#ifndefRELOC_HIDE

# defineRELOC_HIDE(ptr, off)                  \

 ({ unsigned long __ptr;                   \

    __ptr = (unsigned long) (ptr);             \

   (typeof(ptr)) (__ptr + (off)); })

#endif

 

__per_cpu_offset记录的是内核为每个CPU分配的存储空间相对__per_cpu_start的偏移。

通过.data.percpu这个镜像,Linux在启动内核时通过start_kernel函数调用setup_per_cpu_areas函数来为每个cpu创建一个.data.percpu的副本。setup_per_cpu_areas函数定义如下:

#ifndefCONFIG_SMP

/*对于单处理器的情况*/

staticinline void setup_per_cpu_areas(void) { }

#else

/*对于多处理器的情况*/

#ifdef__GENERIC_PER_CPU

/*每个cpupercpu数据存储地址相对__per_cpu_start的偏移*/

unsignedlong __per_cpu_offset[NR_CPUS];

 

EXPORT_SYMBOL(__per_cpu_offset);

 

staticvoid __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 discardthe original) */

        /*这个ALIGN宏估计和SMP中的cache有关,可能设定cpupercpu数据的大小不能超过缓存大小吧,我现在还没弄明白它*/

   size = ALIGN(__per_cpu_end -__per_cpu_start, SMP_CACHE_BYTES);

#ifdefCONFIG_MODULES

   if (size < PERCPU_ENOUGH_ROOM)

       size = PERCPU_ENOUGH_ROOM;

#endif

 

   ptr = alloc_bootmem(size * NR_CPUS);

        /*计算每个cpu的相对.data.percpu镜像首地址__per_cpu_start的偏移,并创建副本*/

   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);

   }

}

#endif /*!__GENERIC_PER_CPU */

.data.percpu镜像和副本的关系如图所示:

 


 

下面以init_tss为例子介绍__per_cpu_offset的计算以及如何确定#cpu1init_tss首地址的过程。

#cpu1percpu相对__per_cpu_start的偏移的计算:

__per_cpu_offset[1] = ptr + size - __per_cpu_start

.data.percpu镜像中init_tss的首地址相对__per_cpu_start的偏移(data.percpu_init_tss_offset)计算:

data.percpu_init_tss_offset = init_tss -__per_cpu_start

那么需要确定#cpu1中的init_tss首地址(#cpu1_init_tss)时应该是:

#cpu1_init_tss=ptr+size + data.percpu_init_tss_offset= ptr + size + init_tss - __per_cpu_start = __per_cpu_offset[1] + init_tss

对比RELOC_HIDE宏,可以发现RELOC_HIDE宏正是利用指定cpu的偏移以及对应类型在.data.percpu镜像中的首地址来确定指定cpuvar的首地址的。(Linux的设计真是很巧妙)


阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页