多CPU的私有数据存储

来源:互联网 发布:2016年网络第一红歌 编辑:程序博客网 时间:2024/04/29 20:49

 

/*will be free after init*/

.=ALIGN(4096);

__init_begin=.;

/*省略*/

.ALIGN(32);

__per_cpu_start=.;

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

__per_cpu_end=.;

.=ALIGN(4096);

__init_end=.;

/*freed after init ends here*/

这说明__per_cpu_start和__per_cpu_end标识.data.percpu这个section的开头和结尾

并且,整个.data.percpu这个section都在__init_begin和__init_end之间,
也就是说,该section所占内存会在系统启动后释放(free)掉

因为有
#define DEFINE_PER_CPU(type, name)
__attribute__((__section__(".data.percpu"))) __typeof__(type) per_cpu__##name

所以
static DEFINE_PER_CPU(struct runqueue, runqueues);
会扩展成
__attribute__((__section__(".data.percpu"))) __typeof__(struct runqueue)
per_cpu__runqueues;
也就是在.data.percpu这个section中定义了一个变量per_cpu__runqueues,
其类型是struct runqueue。事实上,这里所谓的变量per_cpu__runqueues,
其实就是一个偏移量,标识该变量的地址。

--------------------
其次,系统启动后,在start_kernel()中会调用如下函数

unsigned long __per_cpu_offset[NR_CPUS];static void __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 discard the original) */size = ALIGN(__per_cpu_end - __per_cpu_start, SMP_CACHE_BYTES);#ifdef CONFIG_MODULESif (size < PERCPU_ENOUGH_ROOM)size = PERCPU_ENOUGH_ROOM;#endifptr = alloc_bootmem(size * NR_CPUS);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);}}

在该函数中,为每个CPU分配一段专有数据区,并将.data.percpu中的数据拷贝到其中,
每个CPU各有一份。由于数据从__per_cpu_start处转移到各CPU自己的专有数据区中了,
因此存取其中的变量就不能再用原先的值了,比如存取per_cpu__runqueues
就不能再用per_cpu__runqueues了,需要做一个偏移量的调整,
即需要加上各CPU自己的专有数据区首地址相对于__per_cpu_start的偏移量。
在这里也就是__per_cpu_offset[i],其中CPU i的专有数据区相对于
__per_cpu_start的偏移量为__per_cpu_offset[i]。
这样,就可以方便地计算专有数据区中各变量的新地址,比如对于per_cpu_runqueues,
其新地址即变成per_cpu_runqueues+__per_cpu_offset[i]。

经过这样的处理,.data.percpu这个section在系统初始化后就可以释放了。

--------------------
再看如何存取per cpu的变量

/* This macro obfuscates arithmetic on a variable address so that gcc   shouldn't recognize the original var, and make assumptions about it */#define RELOC_HIDE(ptr, off)  ({ unsigned long __ptr;    __asm__ ("" : "=g"(__ptr) : "0"(ptr));    (typeof(ptr)) (__ptr + (off)); })/* var is in discarded region: offset to particular copy we want */#define per_cpu(var, cpu) (*RELOC_HIDE(&per_cpu__##var, __per_cpu_offset[cpu]))#define __get_cpu_var(var) per_cpu(var, smp_processor_id())#define get_cpu_var(var) (*({ preempt_disable(); &__get_cpu_var(var); }))

对于__get_cpu_var(runqueues),将等效地扩展为
__per_cpu_offset[smp_processor_id()] + per_cpu__runqueues
并且是一个lvalue,也就是说可以进行赋值操作。
这正好是上述per_cpu__runqueues变量在对应CPU的专有数据区中的新地址。

由于不同的per cpu变量有不同的偏移量,并且不同的CPU其专有数据区首地址不同,
因此,通过__get_cpu_var()便访问到了不同的变量。

 

Linux per_cpu机制的详解

针对IA64体系结构

在Linux操作系统中,特别是针对SMP或者NUMA架构的多CPU系统的时候,描述每个CPU的私有数据的时候,Linux操作系统提供了per_cpu机制。

per_cpu机制就是让每个CPU都有自己的私有数据段,便于保护与访问。

通过宏DEFINE_PER_CPU,定义这种私有数据,只不过这种私有数据放在特定的数据段中。
#define DEFINE_PER_CPU(type, name)     \
__attribute__((__section__(".data.percpu")))   \
PER_CPU_ATTRIBUTES __typeof__(type) per_cpu__##name
在GCC中__typeof__相当于typeof(检查数据类型),用__typeof__为了兼容性。

例如1:在Linux操作系统中,用于描述每个IA64CPU信息的数据结构的定义:
DEFINE_PER_CPU(struct cpuinfo_ia64, cpu_info);

例如2:描述每个CPU的状态的变量的定义:*
* State for each CPU
*/
DEFINE_PER_CPU(int, cpu_state);

系统如何为每个CPU保留这些私有数据的?
在start_kernel函数中调用执行函数setup_per_cpu_areas( ),setup_per_cpu_areas( )定义如下:
static void __init setup_per_cpu_areas(void)
{
unsigned long size, i;
char *ptr;
unsigned long nr_possible_cpus = num_possible_cpus();
/* Copy section for each CPU (we discard the original) */
size = ALIGN(PERCPU_ENOUGH_ROOM, PAGE_SIZE);
ptr = alloc_bootmem_pages(size * nr_possible_cpus);
for_each_possible_cpu(i) {
   __per_cpu_offset = ptr - __per_cpu_start;
   memcpy(ptr, __per_cpu_start, __per_cpu_end - __per_cpu_start);
   ptr += size;
         }
}
setup_per_cpu_areas( )的功能就是将.data.percpu中的数据拷贝到每个CPU的数据段中,每个CPU一份。
其中CPU n 对应的专有数据区的首地址为__per_cpu_offset[n]。

在Linux操作系统中,在系统启动的时候会生成以__per_cpu_start标识开头和__per_cpu_end标识结尾的数据段,在执行完函数setup_per_cpu_areas( )后,将.data.percpu中的数据拷贝到每个CPU数据段后,这个数据段就会释放(free)掉。
.data.percpu定义如下(在arch/ia64/kernel/vmlinux.lds.S):
{
   __per_cpu_start = .;
   *(.data.percpu)
   *(.data.percpu.shared_aligned)
   __per_cpu_end = .;
}

如何存取每个CPU的这些私有数据呢?
第一种方法:取制定cpu的制定变量
/*
* A percpu variable may point to a discarded regions. The following are
* established ways to produce a usable pointer from the percpu variable
* offset.
*/
#define per_cpu(var, cpu) \
(*SHIFT_PERCPU_PTR(&per_cpu_var(var), per_cpu_offset(cpu)))
|
|
#define SHIFT_PERCPU_PTR(__p, __offset) RELOC_HIDE((__p), (__offset))
                                                                        |
                                                                         |
                                                                       #define RELOC_HIDE(ptr, off)     \
   ({ unsigned long __ptr;     \
     __asm__ ("" : "=r"(__ptr) : "0"(ptr));   \
     (typeof(ptr)) (__ptr + (off)); })
第二种方法:取当前CPU的制定变量
#define __get_cpu_var(var) \
(*SHIFT_PERCPU_PTR(&per_cpu_var(var), my_cpu_offset))
第三种方法:读后写的方式读取当前CPU的制定变量
#define __raw_get_cpu_var(var) \
(*SHIFT_PERCPU_PTR(&per_cpu_var(var), __my_cpu_offset))

 

 

 

一 SMP Interrupts

/proc/irq/{number}/smp_affinity

在多 CPU 的环境中,还有一个中断平衡的问题,比如,网卡中断会教给哪个 CPU 处理,这个参数控制哪些 CPU 可以绑定 IRQ 中断。其中的 {number} 是对应设备的中断编号,可以用下面的命令找出:

cat /proc/interrupt

比如,一般 eth0 的 IRQ 编号是 16,所以控制 eth0 中断绑定的 /proc 文件名是 /proc/irq/16/smp_affinity。上面这个命令还可以看到某些中断对应的CPU处理的次数,缺省的时候肯定是不平衡的。

设置其值的方法很简单,smp_affinity 自身是一个位掩码(bitmask),特定的位对应特定的 CPU,这样,01 就意味着只有第一个 CPU 可以处理对应的中断,而 0f(0x1111)意味着四个 CPU 都会参与中断处理。

几乎所有外设都有这个参数设置,可以关注一下。

这个数值的推荐设置,其实在很大程度上,让专门的CPU处理专门的中断是效率最高的,比如,给磁盘IO一个CPU,给网卡一个CPU,这样是比较合理的。

现在的服务器一般都是多核了,但是中断很多时候都是只用一个核,如果有些中断要求比较高,可以把它独立分配给一个cpu使用。

查看irq资源

cat /proc/interrupts

           CPU0       CPU1       CPU2       CPU3         0:        131          0          0       1914   IO-APIC-edge      timer  1:          0          0          0          2   IO-APIC-edge      i8042  6:          0          0          0          3   IO-APIC-edge      floppy  8:          0          0          0          0   IO-APIC-edge      rtc  9:          0          0          0          1   IO-APIC-fasteoi   acpi 12:          0          0          0          4   IO-APIC-edge      i8042 16:          0          0          0         88   IO-APIC-fasteoi   uhci_hcd:usb1 18:          0          0          0          0   IO-APIC-fasteoi   uhci_hcd:usb2 19:          0          0          0          0   IO-APIC-fasteoi   uhci_hcd:usb3 20:          0          0          0    3632390   IO-APIC-fasteoi   eth0 21:          0          0          0     286964   IO-APIC-fasteoi   eth1 22:          0          0          0        122   IO-APIC-fasteoi   ehci_hcd:usb4, ide0 23:          0          0          0      71154   IO-APIC-fasteoi   megaraid 24:      22742   71684193          0  501949119   IO-APIC-fasteoi   wct4xxpNMI:          0          0          0          0   Non-maskable interruptsLOC:    2928977    1633788    6945258    8115638   Local timer interruptsRES:       1507       2361       3804       3442   Rescheduling interruptsCAL:        263        226        288        168   function call interruptsTLB:       5488       4201       5293       3658   TLB shootdownsTRM:          0          0          0          0   Thermal event interruptsSPU:          0          0          0          0   Spurious interruptsERR:          0MIS:          0

wct4xxp 就是E1卡TE410P,这个对中端要求比较高,所以分配到独立的cpu来处理,irq号是24

cat /proc/irq/24/smp_affinity
00000003

smp_affinity 文件默认是全部ffffffff,8个f就是16的8次方位,一般一台机就几只cpu,所以够了,echo 3 > /proc/irq/24/smp_affinity 就是分配第一第二只cpu给该irq。

二 SMP BOOT

smp系统初始化:

1.BP(一个负责启动的cpu)接到bios的执行权,执行内核初始化代码(中断、cpu、mem管理等等),这个阶段其他cpu是歇着的。

2.BP进入32位模式,start_kernel,设置处理器间中断,然后发送中断(IPI)让其他cpu接受到中断信号执行中断处理程序,这个中断处理程序就跳到了内核初始化代码处。他们分别执行这段代码,不过设置了sign变量,不会再初始化内核管理部分了。到了0进程代码处,此时他们也初始化了。

3.上面的过程执行完,各个cpu的状态都一致了,都到了0进程,可以自己干事儿了。他们之间没有主次之分,是平等的。

smp系统的关键:

cpu之间协调通过apic(高级程序中断控制器)。这种高级的中断控制器分为IO-apic,本地apic。IO-apic类似交换机,传递cpu之间的中断信号,决定分发给各个本地apic。

本地apic:接收外部中断,IO-apic发来的中断,内部中断。

处理器间中断:处理器发给其他处理器的中断,是他们协作的基础。

处理器间的协调、通信(IPI):

内核中,有专门的函数、宏可以给其他处理器发送中断,包括:让别的处理器重新调度等,总之能影响别人。

1, CALL_FUNCTION_VECTOR:发往自己除外的所有CPU,强制它
们执行指定的函数;
2, RESCHEDULE_VECTOR:使被中断的CPU重新调度;
3, INVLIDATE_TLB_VECTOR:使被中断的CPU废弃自己的TLB缓
存内容。
4, ERROR_APIC_VECTOR:错误中断。
5, SPUROUS_APIC_VECTOR:假中断

linux中的进程:

传统的进程是一个独立的占据内存的实体,现在的进程已经包括内核线程。clone出来的进程有些是多个进程共享内存的。

但是调度是基于进程的(也就是线程),同一个2进程的程序,有多个线程,其实对应于多个进程。

因此top显示的是线程。一个线程作为一个调度单位,同一时刻只能运行在一个cpu上面。

因此,top中的结果,表示一个线程在某个cpu上的运行情况。

 

原创粉丝点击