linux內存管理-per cpu數據管理

per-CPU是2.6內核中引入的,它是一種典型的空間換時間的方案,通過爲每個處理器都分配自己的內存區間來避免併發問題, 訪問per-CPU變量幾乎不需要鎖,只需要微不足道的原子操作.
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上,例如:

  1. task1在cpu1上訪問per-CPU數據 data,獲取到了它在cpu1上的數據
  2. task1被調度到cpu2上,繼續訪問在cpu1上per-CPU數據
  3. task2在cpu1上訪問per-CPU數據 data,獲取到了它在cpu1上的數據
  4. 此時task1和task2對於data的訪問就會出現不確定現象.

我們知道中斷上下文在哪個cpu上觸發的就會在該cpu上執行,不會出現balance的情況.只有進程上下文才可能會被負載均衡到其他的cpu上,通過禁止搶佔的方式來避免task在使用per-cpu數據中間其他任務搶佔,而沒有在被當前CPU運行的任務都有可能被負載均衡到其他CPU上.

類似方案

  1. 在32位OS上的臨時映射,參考臨時映射節
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章