一、源由:為何引入Per-CPU變量?
1、lock bus帶來的性能問題
在ARM平臺上,ARMv6之前,SWP和SWPB指令被用來支持對shared memory的訪問:
SWP
Rn中保存了SWP指令要操作的內存地址,通過該指令可以將Rn指定的內存數據加載到Rt寄存器,同時將Rt2寄存器中的數值保存到Rn指定的內存中去。
我們在原子操作那篇文檔中描述的read-modify-write的問題本質上是一個保持對內存read和write訪問的原子性的問題。也就是說對內存的讀和寫的訪問不能被打斷。對該問題的解決可以通過硬件、軟件或者軟硬件結合的方法來進行。早期的ARM CPU給出的方案就是依賴硬件:SWP這個匯編指令執行了一次讀內存操作、一次寫內存操作,但是從程序員的角度看,SWP這條指令就是原子的,讀寫之間不會被任何的異步事件打斷。具體底層的硬件是如何做的呢?這時候,硬件會提供一個lock signal,在進行memory操作的時候設定lock信號,告訴總線這是一個不可被中斷的內存訪問,直到完成了SWP需要進行的兩次內存訪問之后再clear lock信號。
lock memory bus對多核系統的性能造成嚴重的影響(系統中其他的processor對那條被lock的memory bus的訪問就被hold住了),如何解決這個問題?最好的鎖機制就是不使用鎖,因此解決這個問題可以使用釜底抽薪的方法,那就是不在系統中的多個processor之間共享數據,給每一個CPU分配一個不就OK了嗎。
當然,隨著技術的發展,在ARMv6之后的ARM CPU已經不推薦使用SWP這樣的指令,而是提供了LDREX和STREX這樣的指令。這種方法是使用軟硬件結合的方法來解決原子操作問題,看起來代碼比較復雜,但是系統的性能可以得到提升。其實,從硬件角度看,LDREX和STREX這樣的指令也是采用了lock-free的做法。OK,由于不再lock bus,看起來Per-CPU變量存在的基礎被打破了。不過考慮cache的操作,實際上它還是有意義的。
2、cache的影響
在The Memory Hierarchy文檔中,我們已經了解了關于memory一些基礎的知識,一些基礎的內容,這里就不再重復了。我們假設一個多核系統中的cache如下:
每個CPU都有自己的L1 cache(包括data cache和instruction cache),所有的CPU共用一個L2 cache。L1、L2以及main memory的訪問速度之間的差異都是非常大,最高的性能的情況下當然是L1 cache hit,這樣就不需要訪問下一階memory來加載cache line。
我們首先看在多個CPU之間共享內存的情況。這種情況下,任何一個CPU如果修改了共享內存就會導致所有其他CPU的L1 cache上對應的cache line變成invalid(硬件完成)。雖然對性能造成影響,但是系統必須這么做,因為需要維持cache的同步。將一個共享memory變成Per-CPU memory本質上是一個耗費更多memory來解決performance的方法。當一個在多個CPU之間共享的變量變成每個CPU都有屬于自己的一個私有的變量的時候,我們就不必考慮來自多個CPU上的并發,僅僅考慮本CPU上的并發就OK了。當然,還有一點要注意,那就是在訪問Per-CPU變量的時候,不能調度,當然更準確的說法是該task不能調度到其他CPU上去。目前的內核的做法是在訪問Per-CPU變量的時候disable preemptive,雖然沒有能夠完全避免使用鎖的機制(disable preemptive也是一種鎖的機制),但毫無疑問,這是一種代價比較小的鎖。
二、接口
1、靜態聲明和定義Per-CPU變量的API如下表所示:
聲明和定義Per-CPU變量的API | 描述 |
DECLARE_PER_CPU(type, name) DEFINE_PER_CPU(type, name) |
普通的、沒有特殊要求的per cpu變量定義接口函數。沒有對齊的要求 |
DECLARE_PER_CPU_FIRST(type, name) DEFINE_PER_CPU_FIRST(type, name) |
通過該API定義的per cpu變量位于整個per cpu相關section的最前面。 |
DECLARE_PER_CPU_SHARED_ALIGNED(type, name) DEFINE_PER_CPU_SHARED_ALIGNED(type, name) |
通過該API定義的per cpu變量在SMP的情況下會對齊到L1 cache line ,對于UP,不需要對齊到cachine line |
DECLARE_PER_CPU_ALIGNED(type, name) DEFINE_PER_CPU_ALIGNED(type, name) |
無論SMP或者UP,都是需要對齊到L1 cache line |
DECLARE_PER_CPU_PAGE_ALIGNED(type, name) DEFINE_PER_CPU_PAGE_ALIGNED(type, name) |
為定義page aligned per cpu變量而設定的API接口 |
DECLARE_PER_CPU_READ_MOSTLY(type, name) DEFINE_PER_CPU_READ_MOSTLY(type, name) |
通過該API定義的per cpu變量是read mostly的 |
看到這樣“豐富多彩”的Per-CPU變量的API,你是不是已經醉了。這些定義使用在不同的場合,主要的factor包括:
-該變量在section中的位置
-該變量的對齊方式
-該變量對SMP和UP的處理不同
-訪問per cpu的形態
例如:如果你準備定義的per cpu變量是要求按照page對齊的,那么在定義該per cpu變量的時候需要使用DECLARE_PER_CPU_PAGE_ALIGNED。如果只要求在SMP的情況下對齊到cache line,那么使用DECLARE_PER_CPU_SHARED_ALIGNED來定義該per cpu變量。
2、訪問靜態聲明和定義Per-CPU變量的API
靜態定義的per cpu變量不能象普通變量那樣進行訪問,需要使用特定的接口函數,具體如下:
get_cpu_var(var)
put_cpu_var(var)
上面這兩個接口函數已經內嵌了鎖的機制(preempt disable),用戶可以直接調用該接口進行本CPU上該變量副本的訪問。如果用戶確認當前的執行環境已經是preempt disable(例如持有spinlock),那么可以使用lock-free版本的Per-CPU變量的API:__get_cpu_var。
3、動態分配Per-CPU變量的API如下表所示:
動態分配和釋放Per-CPU變量的API | 描述 |
alloc_percpu(type) | 分配類型是type的per cpu變量,返回per cpu變量的地址(注意:不是各個CPU上的副本) |
void free_percpu(void __percpu *ptr) | 釋放ptr指向的per cpu變量空間 |
4、訪問動態分配Per-CPU變量的API如下表所示:
訪問Per-CPU變量的API | 描述 |
get_cpu_ptr | 這個接口是和訪問靜態Per-CPU變量的get_cpu_var接口是類似的,當然,這個接口是for 動態分配Per-CPU變量 |
put_cpu_ptr | 同上 |
per_cpu_ptr(ptr, cpu) | 根據per cpu變量的地址和cpu number,返回指定CPU number上該per cpu變量的地址 |
三、實現
1、靜態Per-CPU變量定義
我們以DEFINE_PER_CPU的實現為例子,描述linux kernel中如何實現靜態Per-CPU變量定義。具體代碼如下:
#define DEFINE_PER_CPU(type, name) \
DEFINE_PER_CPU_SECTION(type, name, "")
type就是變量的類型,name是per cpu變量符號。DEFINE_PER_CPU_SECTION宏可以把一個per cpu變量放到指定的section中,具體代碼如下:
#define DEFINE_PER_CPU_SECTION(type, name, sec) \
__PCPU_ATTRS(sec) PER_CPU_DEF_ATTRIBUTES \-----安排section
__typeof__(type) name----------------------定義變量
在這里具體arch specific的percpu代碼中(arch/arm/include/asm/percpu.h)可以定義PER_CPU_DEF_ATTRIBUTES,以便控制該per cpu變量的屬性,當然,如果arch specific的percpu代碼不定義,那么在general arch-independent的代碼中(include/asm-generic/percpu.h)會定義為空。這里可以順便提一下Per-CPU變量的軟件層次:
(1)arch-independent interface。在include/linux/percpu.h文件中,定義了內核其他模塊要使用per cpu機制使用的接口API以及相關數據結構的定義。內核其他模塊需要使用per cpu變量接口的時候需要include該頭文件
(2)arch-general interface。在include/asm-generic/percpu.h文件中。如果所有的arch相關的定義都是一樣的,那么就把它抽取出來,放到asm-generic目錄下。毫無疑問,這個文件定義的接口和數據結構是硬件相關的,只不過軟件抽象各個arch-specific的內容,形成一個arch general layer。一般來說,我們不需要直接include該頭文件,include/linux/percpu.h會include該頭文件。
(3)arch-specific。這是和硬件相關的接口,在arch/arm/include/asm/percpu.h,定義了ARM平臺中,具體和per cpu相關的接口代碼。
我們回到正題,看看__PCPU_ATTRS的定義:
#define __PCPU_ATTRS(sec) \
__percpu __attribute__((section(PER_CPU_BASE_SECTION sec))) \
PER_CPU_ATTRIBUTES
PER_CPU_BASE_SECTION 定義了基礎的section name symbol,定義如下:
#ifndef PER_CPU_BASE_SECTION
#ifdef CONFIG_SMP
#define PER_CPU_BASE_SECTION ".data..percpu"
#else
#define PER_CPU_BASE_SECTION ".data"
#endif
#endif
雖然有各種各樣的靜態Per-CPU變量定義方法,但是都是類似的,只不過是放在不同的section中,屬性不同而已,這里就不看其他的實現了,直接給出section的安排:
(1)普通per cpu變量的section安排
SMP | UP | |
Build-in kernel | ".data..percpu" section | ".data" section |
defined in module | ".data..percpu" section | ".data" section |
(2)first per cpu變量的section安排
SMP | UP | |
Build-in kernel | ".data..percpu..first" section | ".data" section |
defined in module | ".data..percpu..first" section | ".data" section |
(3)SMP shared aligned per cpu變量的section安排
SMP | UP | |
Build-in kernel | ".data..percpu..shared_aligned" section | ".data" section |
defined in module | ".data..percpu" section | ".data" section |
(4)aligned per cpu變量的section安排
SMP | UP | |
Build-in kernel | ".data..percpu..shared_aligned" section | ".data..shared_aligned" section |
defined in module | ".data..percpu" section | ".data..shared_aligned" section |
(5)page aligned per cpu變量的section安排
SMP | UP | |
Build-in kernel | ".data..percpu..page_aligned" section | ".data..page_aligned" section |
defined in module | ".data..percpu..page_aligned" section | ".data..page_aligned" section |
(6)read mostly per cpu變量的section安排
SMP | UP | |
Build-in kernel | ".data..percpu..readmostly" section | ".data..readmostly" section |
defined in module | ".data..percpu..readmostly" section | ".data..readmostly" section |
了解了靜態定義Per-CPU變量的實現,但是為何要引入這么多的section呢?對于kernel中的普通變量,經過了編譯和鏈接后,會被放置到.data或者.bss段,系統在初始化的時候會準備好一切(例如clear bss),由于per cpu變量的特殊性,內核將這些變量放置到了其他的section,位于kernel address space中__per_cpu_start和__per_cpu_end之間,我們稱之Per-CPU變量的原始變量(我也想不出什么好詞了)。
只有Per-CPU變量的原始變量還是不夠的,必須為每一個CPU建立一個副本,怎么建?直接靜態定義一個NR_CPUS的數組?NR_CPUS定義了系統支持的最大的processor的個數,并不是實際中系統processor的數目,這樣的定義非常浪費內存。此外,靜態定義的數據在內存中連續,對于UMA系統而言是OK的,對于NUMA系統,每個CPU上的Per-CPU變量的副本應該位于它訪問最快的那段memory上,也就是說Per-CPU變量的各個CPU副本可能是散布在整個內存地址空間的,而這些空間之間是有空洞的。本質上,副本per cpu內存的分配歸屬于內存管理子系統,因此,分配per cpu變量副本的內存本文不會詳述,大致的思路如下:
內存管理子系統會根據當前的內存配置為每一個CPU分配一大塊memory,對于UMA,這個memory也是位于main memory,對于NUMA,有可能是分配最靠近該CPU的memory(也就是說該cpu訪問這段內存最快),但無論如何,這些都是內存管理子系統需要考慮的。無論靜態還是動態per cpu變量的分配,其機制都是一樣的,只不過,對于靜態per cpu變量,需要在系統初始化的時候,對應per cpu section,預先動態分配一個同樣size的per cpu chunk。在vmlinux.lds.h文件中,定義了percpu section的排列情況:
#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) = .;
對于build in內核的那些per cpu變量,必然位于__per_cpu_start和__per_cpu_end之間的per cpu section。在系統初始化的時候(setup_per_cpu_areas),分配per cpu memory chunk,并將per cpu section copy到每一個chunk中。
2、訪問靜態定義的per cpu變量
代碼如下:
#define get_cpu_var(var) (*({ \
preempt_disable(); \
&__get_cpu_var(var); }))
再看到get_cpu_var和__get_cpu_var這兩個符號,相信廣大人民群眾已經相當的熟悉,一個持有鎖的版本,一個lock-free的版本。為防止當前task由于搶占而調度到其他的CPU上,在訪問per cpu memory的時候都需要使用preempt_disable這樣的鎖的機制。我們來看__get_cpu_var:
#define __get_cpu_var(var) (*this_cpu_ptr(&(var)))
#define this_cpu_ptr(ptr) __this_cpu_ptr(ptr)
對于ARM平臺,我們沒有定義__this_cpu_ptr,因此采用asm-general版本的:
#define __this_cpu_ptr(ptr) SHIFT_PERCPU_PTR(ptr, __my_cpu_offset)
SHIFT_PERCPU_PTR這個宏定義從字面上就可以看出它是可以從原始的per cpu變量的地址,通過簡單的變換(SHIFT)轉成實際的per cpu變量副本的地址。實際上,per cpu內存管理模塊可以保證原始的per cpu變量的地址和各個CPU上的per cpu變量副本的地址有簡單的線性關系(就是一個固定的offset)。__my_cpu_offset這個宏定義就是和offset相關的,如果arch specific沒有定義,那么可以采用asm general版本的,如下:
#define __my_cpu_offset per_cpu_offset(raw_smp_processor_id())
raw_smp_processor_id可以獲取本CPU的ID,如果沒有arch specific沒有定義__per_cpu_offset這個宏,那么offset保存在__per_cpu_offset的數組中(下面只是數組聲明,具體定義在mm/percpu.c文件中),如下:
#ifndef __per_cpu_offset
extern unsigned long __per_cpu_offset[NR_CPUS];
#define per_cpu_offset(x) (__per_cpu_offset[x])
#endif
對于ARMV6K和ARMv7版本,offset保存在TPIDRPRW寄存器中,這樣是為了提升系統性能。
3、動態分配per cpu變量
這部分內容留給內存管理子系統吧。
編輯:hfy
-
ARM
+關注
關注
134文章
9057瀏覽量
366877 -
寄存器
+關注
關注
31文章
5325瀏覽量
120052 -
cpu
+關注
關注
68文章
10829瀏覽量
211194 -
Linux
+關注
關注
87文章
11232瀏覽量
208952
發布評論請先 登錄
相關推薦
評論