per-CPU是2.6內核中引入的,它是一種典型的空間換時間的方案,通過爲每個處理器都分配自己的內存區間來避免併發問題, 訪問per-CPU變量幾乎不需要鎖,只需要微不足道的原子操作.
每個處理器都在其自己的副本上工作,這些副本是如何生成的呢?
靜態分配的per-CPU數據
靜態分配的per-CPU結構設計分爲兩個階段:編譯階段和運行時階段
使用DEFINE_PER_CPU可以靜態分配per-cpu結構,它通過編譯器指示__attribute__((__section__(".data.percpu")))
在編譯階段將所有的per-CPU放入到data.percpu
的section中,它實際上只生成了一個CPU原本。
以x86上爲例,在arch/x86_64/kernel/vmlinux.lds.S鏈接腳本有如下內容,__per_cpu_start表示section加載時的起始地址,__per_cpu_end表示section加載時的結束地址:
. = ALIGN(CONFIG_X86_L1_CACHE_BYTES);
__per_cpu_start = .;
.data.percpu : AT(ADDR(.data.percpu) - LOAD_OFFSET) { *(.data.percpu) }
__per_cpu_end = .;
. = ALIGN(4096);
__init_end = .;
由上面鏈接腳本發現, .data.percpu
Section是在init數據段的,在系統初始化結束後將被回收。那麼,系統如何維持per-CPU數據呢?這個任務在運行時完成。在系統初始化階段setup_per_cpu_areas會爲每個CPU分配一片( __per_cpu_end - __per_cpu_start)大小的內存,然後將 data.percpu
段中的CPU原本拷貝NR_CPU份到這塊內存中,在_cpu_pda
數組中記錄當前CPU per-cpu副本地址區域和原本的地址區域的差值data_offset= cpu ptr - __per_cpu_start
。系統通過per_cpu訪問per-CPU變量的時候就會根據自己的cpu_id找到對應的地址偏移data_offset,最終cpu訪問的地址就是自己私有的區間:addr = data_offset + __per_cpu_start + &data
。
動態分配的per-CPU數據
動態per-CPU結構相對於靜態結構來說,設計上更直觀,但效率上要低一些。每次調用alloc_percpu(type)的時候會生成一個維度爲NR_CPUS的指針數組,每個指針指向一個kzalloc/kmalloc_node出來的type型對象。Linux在這裏採取了一個優化手段:如果第i個cpu所屬的node在線(linux支持cpu的hot-plug),那麼就採用kmalloc_node來分配空間,這個空間與cpu i的親和性很高;如果cpu i不在線,則採用通用的kzalloc分配了,對於UMA沒啥鳥用。下面是空間分配代碼:
void *percpu_populate(void *__pdata, size_t size, gfp_t gfp, int cpu)
{
struct percpu_data *pdata = __percpu_disguise(__pdata);
int node = cpu_to_node(cpu);
BUG_ON(pdata->ptrs[cpu]);
if (node_online(node)) {
/* FIXME: kzalloc_node(size, gfp, node) */
pdata->ptrs[cpu] = kmalloc_node(size, gfp, node);
if (pdata->ptrs[cpu])
memset(pdata->ptrs[cpu], 0, size);
} else
pdata->ptrs[cpu] = kzalloc(size, gfp);
return pdata->ptrs[cpu];
}
對於動態生成的per-CPU變量需要用per_cpu_ptr來訪問。
在後續的版本實現中,又有改動,變來變去繞的頭都大了,沒啥實際改動需求不建議去讀這些代碼了.
對於靜態分配的per-cpu數據,實際上是根據cpu副本的地址和原本的地址的差值來進行數據地址和cpu副本數據地址的互相轉換,在2.6版本的內核又使用了void *ptrs[NR_CPUS]
來保存cpu的副本地址,這樣就需要使用兩套接口了,雖然原理相同但是兩套接口就比較彆扭了,後來終於統一了接口,改日再說.
per-CPU的使用
1.對於數據精度要求不高的場景,例如統計網卡收發包數據
使用per-CPU數據時如何避免併發訪問
per-CPU解決的問題是多CPU間併發訪問同一個數據的問題,但是在單個CPU上不同上下文都會訪問的數據仍然需要加鎖操作,例如在中斷上下文和進程上下文中都會操作的,那麼操作時就需要禁止中斷的操作.
除了通過爲per-CPU分配私有的空間之外,它還需要在訪問時任務調度到其他cpu上,例如:
- task1在cpu1上訪問per-CPU數據 data,獲取到了它在cpu1上的數據
- task1被調度到cpu2上,繼續訪問在cpu1上per-CPU數據
- task2在cpu1上訪問per-CPU數據 data,獲取到了它在cpu1上的數據
- 此時task1和task2對於data的訪問就會出現不確定現象.
我們知道中斷上下文在哪個cpu上觸發的就會在該cpu上執行,不會出現balance的情況.只有進程上下文才可能會被負載均衡到其他的cpu上,通過禁止搶佔的方式來避免task在使用per-cpu數據中間其他任務搶佔,而沒有在被當前CPU運行的任務都有可能被負載均衡到其他CPU上.
類似方案
- 在32位OS上的臨時映射,參考臨時映射節