0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

Linux內(nèi)核同步機制:引入Per-CPU變量的意義

454398 ? 來源:linuxer ? 作者:蝸窩科技 ? 2020-10-11 11:37 ? 次閱讀

一、源由:為何引入Per-CPU變量?

1、lock bus帶來的性能問題

ARM平臺上,ARMv6之前,SWP和SWPB指令被用來支持對shared memory的訪問:

SWP , , []

Rn中保存了SWP指令要操作的內(nèi)存地址,通過該指令可以將Rn指定的內(nèi)存數(shù)據(jù)加載到Rt寄存器,同時將Rt2寄存器中的數(shù)值保存到Rn指定的內(nèi)存中去。

我們在原子操作那篇文檔中描述的read-modify-write的問題本質(zhì)上是一個保持對內(nèi)存read和write訪問的原子性的問題。也就是說對內(nèi)存的讀和寫的訪問不能被打斷。對該問題的解決可以通過硬件、軟件或者軟硬件結(jié)合的方法來進行。早期的ARM CPU給出的方案就是依賴硬件:SWP這個匯編指令執(zhí)行了一次讀內(nèi)存操作、一次寫內(nèi)存操作,但是從程序員的角度看,SWP這條指令就是原子的,讀寫之間不會被任何的異步事件打斷。具體底層的硬件是如何做的呢?這時候,硬件會提供一個lock signal,在進行memory操作的時候設(shè)定lock信號,告訴總線這是一個不可被中斷的內(nèi)存訪問,直到完成了SWP需要進行的兩次內(nèi)存訪問之后再clear lock信號。

lock memory bus對多核系統(tǒng)的性能造成嚴(yán)重的影響(系統(tǒng)中其他的processor對那條被lock的memory bus的訪問就被hold住了),如何解決這個問題?最好的鎖機制就是不使用鎖,因此解決這個問題可以使用釜底抽薪的方法,那就是不在系統(tǒng)中的多個processor之間共享數(shù)據(jù),給每一個CPU分配一個不就OK了嗎。

當(dāng)然,隨著技術(shù)的發(fā)展,在ARMv6之后的ARM CPU已經(jīng)不推薦使用SWP這樣的指令,而是提供了LDREX和STREX這樣的指令。這種方法是使用軟硬件結(jié)合的方法來解決原子操作問題,看起來代碼比較復(fù)雜,但是系統(tǒng)的性能可以得到提升。其實,從硬件角度看,LDREX和STREX這樣的指令也是采用了lock-free的做法。OK,由于不再lock bus,看起來Per-CPU變量存在的基礎(chǔ)被打破了。不過考慮cache的操作,實際上它還是有意義的。

2、cache的影響

在The Memory Hierarchy文檔中,我們已經(jīng)了解了關(guān)于memory一些基礎(chǔ)的知識,一些基礎(chǔ)的內(nèi)容,這里就不再重復(fù)了。我們假設(shè)一個多核系統(tǒng)中的cache如下:

每個CPU都有自己的L1 cache(包括data cache和instruction cache),所有的CPU共用一個L2 cache。L1、L2以及main memory的訪問速度之間的差異都是非常大,最高的性能的情況下當(dāng)然是L1 cache hit,這樣就不需要訪問下一階memory來加載cache line。

我們首先看在多個CPU之間共享內(nèi)存的情況。這種情況下,任何一個CPU如果修改了共享內(nèi)存就會導(dǎo)致所有其他CPU的L1 cache上對應(yīng)的cache line變成invalid(硬件完成)。雖然對性能造成影響,但是系統(tǒng)必須這么做,因為需要維持cache的同步。將一個共享memory變成Per-CPU memory本質(zhì)上是一個耗費更多memory來解決performance的方法。當(dāng)一個在多個CPU之間共享的變量變成每個CPU都有屬于自己的一個私有的變量的時候,我們就不必考慮來自多個CPU上的并發(fā),僅僅考慮本CPU上的并發(fā)就OK了。當(dāng)然,還有一點要注意,那就是在訪問Per-CPU變量的時候,不能調(diào)度,當(dāng)然更準(zhǔn)確的說法是該task不能調(diào)度到其他CPU上去。目前的內(nèi)核的做法是在訪問Per-CPU變量的時候disable preemptive,雖然沒有能夠完全避免使用鎖的機制(disable preemptive也是一種鎖的機制),但毫無疑問,這是一種代價比較小的鎖。

二、接口

1、靜態(tài)聲明和定義Per-CPU變量的API如下表所示:

聲明和定義Per-CPU變量的API 描述
DECLARE_PER_CPU(type, name)
DEFINE_PER_CPU(type, name)
普通的、沒有特殊要求的per cpu變量定義接口函數(shù)。沒有對齊的要求
DECLARE_PER_CPU_FIRST(type, name)
DEFINE_PER_CPU_FIRST(type, name)
通過該API定義的per cpu變量位于整個per cpu相關(guān)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變量而設(shè)定的API接口
DECLARE_PER_CPU_READ_MOSTLY(type, name)
DEFINE_PER_CPU_READ_MOSTLY(type, name)
通過該API定義的per cpu變量是read mostly的

看到這樣“豐富多彩”的Per-CPU變量的API,你是不是已經(jīng)醉了。這些定義使用在不同的場合,主要的factor包括:

-該變量在section中的位置

-該變量的對齊方式

-該變量對SMP和UP的處理不同

-訪問per cpu的形態(tài)

例如:如果你準(zhǔn)備定義的per cpu變量是要求按照page對齊的,那么在定義該per cpu變量的時候需要使用DECLARE_PER_CPU_PAGE_ALIGNED。如果只要求在SMP的情況下對齊到cache line,那么使用DECLARE_PER_CPU_SHARED_ALIGNED來定義該per cpu變量。

2、訪問靜態(tài)聲明和定義Per-CPU變量的API

靜態(tài)定義的per cpu變量不能象普通變量那樣進行訪問,需要使用特定的接口函數(shù),具體如下:

get_cpu_var(var)

put_cpu_var(var)

上面這兩個接口函數(shù)已經(jīng)內(nèi)嵌了鎖的機制(preempt disable),用戶可以直接調(diào)用該接口進行本CPU上該變量副本的訪問。如果用戶確認(rèn)當(dāng)前的執(zhí)行環(huán)境已經(jīng)是preempt disable(例如持有spinlock),那么可以使用lock-free版本的Per-CPU變量的API:__get_cpu_var。

3、動態(tài)分配Per-CPU變量的API如下表所示:

動態(tài)分配和釋放Per-CPU變量的API 描述
alloc_percpu(type) 分配類型是type的per cpu變量,返回per cpu變量的地址(注意:不是各個CPU上的副本)
void free_percpu(void __percpu *ptr) 釋放ptr指向的per cpu變量空間

4、訪問動態(tài)分配Per-CPU變量的API如下表所示:

訪問Per-CPU變量的API 描述
get_cpu_ptr 這個接口是和訪問靜態(tài)Per-CPU變量的get_cpu_var接口是類似的,當(dāng)然,這個接口是for 動態(tài)分配Per-CPU變量
put_cpu_ptr 同上
per_cpu_ptr(ptr, cpu) 根據(jù)per cpu變量的地址和cpu number,返回指定CPU number上該per cpu變量的地址

三、實現(xiàn)

1、靜態(tài)Per-CPU變量定義

我們以DEFINE_PER_CPU的實現(xiàn)為例子,描述linux kernel中如何實現(xiàn)靜態(tài)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變量的屬性,當(dāng)然,如果arch specific的percpu代碼不定義,那么在general arch-independent的代碼中(include/asm-generic/percpu.h)會定義為空。這里可以順便提一下Per-CPU變量的軟件層次:

(1)arch-independent interface。在include/linux/percpu.h文件中,定義了內(nèi)核其他模塊要使用per cpu機制使用的接口API以及相關(guān)數(shù)據(jù)結(jié)構(gòu)的定義。內(nèi)核其他模塊需要使用per cpu變量接口的時候需要include該頭文件

(2)arch-general interface。在include/asm-generic/percpu.h文件中。如果所有的arch相關(guān)的定義都是一樣的,那么就把它抽取出來,放到asm-generic目錄下。毫無疑問,這個文件定義的接口和數(shù)據(jù)結(jié)構(gòu)是硬件相關(guān)的,只不過軟件抽象各個arch-specific的內(nèi)容,形成一個arch general layer。一般來說,我們不需要直接include該頭文件,include/linux/percpu.h會include該頭文件。

(3)arch-specific。這是和硬件相關(guān)的接口,在arch/arm/include/asm/percpu.h,定義了ARM平臺中,具體和per cpu相關(guān)的接口代碼。

我們回到正題,看看__PCPU_ATTRS的定義:

#define __PCPU_ATTRS(sec) \
__percpu __attribute__((section(PER_CPU_BASE_SECTION sec))) \
PER_CPU_ATTRIBUTES

PER_CPU_BASE_SECTION 定義了基礎(chǔ)的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

雖然有各種各樣的靜態(tài)Per-CPU變量定義方法,但是都是類似的,只不過是放在不同的section中,屬性不同而已,這里就不看其他的實現(xiàn)了,直接給出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

了解了靜態(tài)定義Per-CPU變量的實現(xiàn),但是為何要引入這么多的section呢?對于kernel中的普通變量,經(jīng)過了編譯和鏈接后,會被放置到.data或者.bss段,系統(tǒng)在初始化的時候會準(zhǔn)備好一切(例如clear bss),由于per cpu變量的特殊性,內(nèi)核將這些變量放置到了其他的section,位于kernel address space中__per_cpu_start和__per_cpu_end之間,我們稱之Per-CPU變量的原始變量(我也想不出什么好詞了)。

只有Per-CPU變量的原始變量還是不夠的,必須為每一個CPU建立一個副本,怎么建?直接靜態(tài)定義一個NR_CPUS的數(shù)組?NR_CPUS定義了系統(tǒng)支持的最大的processor的個數(shù),并不是實際中系統(tǒng)processor的數(shù)目,這樣的定義非常浪費內(nèi)存。此外,靜態(tài)定義的數(shù)據(jù)在內(nèi)存中連續(xù),對于UMA系統(tǒng)而言是OK的,對于NUMA系統(tǒng),每個CPU上的Per-CPU變量的副本應(yīng)該位于它訪問最快的那段memory上,也就是說Per-CPU變量的各個CPU副本可能是散布在整個內(nèi)存地址空間的,而這些空間之間是有空洞的。本質(zhì)上,副本per cpu內(nèi)存的分配歸屬于內(nèi)存管理子系統(tǒng),因此,分配per cpu變量副本的內(nèi)存本文不會詳述,大致的思路如下:

內(nèi)存管理子系統(tǒng)會根據(jù)當(dāng)前的內(nèi)存配置為每一個CPU分配一大塊memory,對于UMA,這個memory也是位于main memory,對于NUMA,有可能是分配最靠近該CPU的memory(也就是說該cpu訪問這段內(nèi)存最快),但無論如何,這些都是內(nèi)存管理子系統(tǒng)需要考慮的。無論靜態(tài)還是動態(tài)per cpu變量的分配,其機制都是一樣的,只不過,對于靜態(tài)per cpu變量,需要在系統(tǒng)初始化的時候,對應(yīng)per cpu section,預(yù)先動態(tài)分配一個同樣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內(nèi)核的那些per cpu變量,必然位于__per_cpu_start和__per_cpu_end之間的per cpu section。在系統(tǒng)初始化的時候(setup_per_cpu_areas),分配per cpu memory chunk,并將per cpu section copy到每一個chunk中。

2、訪問靜態(tài)定義的per cpu變量

代碼如下:

#define get_cpu_var(var) (*({ \
preempt_disable(); \
&__get_cpu_var(var); }))

再看到get_cpu_var和__get_cpu_var這兩個符號,相信廣大人民群眾已經(jīng)相當(dāng)?shù)氖煜?,一個持有鎖的版本,一個lock-free的版本。為防止當(dāng)前task由于搶占而調(diào)度到其他的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)轉(zhuǎn)成實際的per cpu變量副本的地址。實際上,per cpu內(nèi)存管理模塊可以保證原始的per cpu變量的地址和各個CPU上的per cpu變量副本的地址有簡單的線性關(guān)系(就是一個固定的offset)。__my_cpu_offset這個宏定義就是和offset相關(guān)的,如果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的數(shù)組中(下面只是數(shù)組聲明,具體定義在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寄存器中,這樣是為了提升系統(tǒng)性能。

3、動態(tài)分配per cpu變量

這部分內(nèi)容留給內(nèi)存管理子系統(tǒng)吧。
編輯:hfy

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • ARM
    ARM
    +關(guān)注

    關(guān)注

    134

    文章

    9098

    瀏覽量

    367707
  • 寄存器
    +關(guān)注

    關(guān)注

    31

    文章

    5343

    瀏覽量

    120448
  • cpu
    cpu
    +關(guān)注

    關(guān)注

    68

    文章

    10870

    瀏覽量

    211901
  • Linux
    +關(guān)注

    關(guān)注

    87

    文章

    11310

    瀏覽量

    209620
收藏 人收藏

    評論

    相關(guān)推薦

    詳解linux內(nèi)核中的mutex同步機制

    linux內(nèi)核中,互斥量(mutex,即mutual exclusion)是一種保證串行化的睡眠鎖機制。和spinlock的語義類似,都是允許一個執(zhí)行線索進入臨界區(qū),不同的是當(dāng)無法獲得鎖的時候
    的頭像 發(fā)表于 05-13 08:56 ?6854次閱讀
    詳解<b class='flag-5'>linux</b><b class='flag-5'>內(nèi)核</b>中的mutex<b class='flag-5'>同步機制</b>

    Linux內(nèi)核同步機制mutex詳解

    linux內(nèi)核中,互斥量mutex是一種保證CPU串行運行的睡眠鎖機制。和spinlock類似,都是同一個時刻只有一個線程進入臨界資源,不同的是,當(dāng)無法獲取鎖的時候,spinlock
    發(fā)表于 06-26 16:05 ?1133次閱讀

    Linux內(nèi)核同步機制

    在現(xiàn)代操作系統(tǒng)里,同一時間可能有多個內(nèi)核執(zhí)行流在執(zhí)行,因此內(nèi)核其實象多進程多線程編程一樣也需要一些同步機制同步各執(zhí)行單元對共享數(shù)據(jù)的訪問。尤其是在多處理器系統(tǒng)上,更需要一些
    發(fā)表于 08-06 07:08

    Vulkan同步機制和圖形轉(zhuǎn)換的風(fēng)險

    Vulkan同步機制和圖形-計算-圖形轉(zhuǎn)換的風(fēng)險(一)
    發(fā)表于 01-21 06:17

    RTT中的消息同步機制是如何實現(xiàn)的?

    RTT中的消息同步機制是如何實現(xiàn)的
    發(fā)表于 11-02 07:00

    Linux內(nèi)核同步機制的自旋鎖原理

    一、自旋鎖 自旋鎖是專為防止多處理器并發(fā)而引入的一種鎖,它在內(nèi)核中大量應(yīng)用于中斷處理等部分(對于單處理器來說,防止中斷處理中的并發(fā)可簡單采用關(guān)閉中
    發(fā)表于 06-08 14:50 ?1309次閱讀

    linux內(nèi)核機制有哪些

    路徑(進程)以交錯的方式運行。對于這些交錯路徑執(zhí)行的內(nèi)核路徑,如不采取必要的同步措施,將會對一些關(guān)鍵數(shù)據(jù)結(jié)構(gòu)進行交錯訪問和修改,從而導(dǎo)致這些數(shù)據(jù)結(jié)構(gòu)狀態(tài)的不一致,進而導(dǎo)致系統(tǒng)崩潰。因此,為了確保系統(tǒng)高效穩(wěn)定有序地運行,linux
    發(fā)表于 11-14 15:25 ?5570次閱讀
    <b class='flag-5'>linux</b><b class='flag-5'>內(nèi)核</b><b class='flag-5'>機制</b>有哪些

    linux內(nèi)核機制

    在現(xiàn)代操作系統(tǒng)里,同一時間可能有多個內(nèi)核執(zhí)行流在執(zhí)行,因此內(nèi)核其實象多進程多線程編程一樣也需要一些同步機制同步各執(zhí)行單元對共享數(shù)據(jù)的訪問。尤其是在多處理器系統(tǒng)上,更需要一些
    發(fā)表于 11-14 15:52 ?7138次閱讀

    你知道linux 同步機制的complete?

    Linux內(nèi)核中,completion是一種簡單的同步機制,標(biāo)志"things may proceed"。 要使用completion,必須在文件中包含,同時創(chuàng)建一個類型為struct completion的
    發(fā)表于 04-24 11:45 ?1287次閱讀

    你了解Linux內(nèi)核同步機制?

    在現(xiàn)代操作系統(tǒng)里,同一時間可能有多個內(nèi)核執(zhí)行流在執(zhí)行,因此內(nèi)核其實象多進程多線程編程一樣也需要一些同步機制同步各執(zhí)行單元對共享數(shù)據(jù)的訪問。
    發(fā)表于 05-12 08:26 ?636次閱讀

    可以了解并學(xué)習(xí)Linux 內(nèi)核同步機制

    Linux內(nèi)核同步機制,挺復(fù)雜的一個東西,常用的有自旋鎖,信號量,互斥體,原子操作,順序鎖,RCU,內(nèi)存屏障等。
    發(fā)表于 05-14 14:10 ?705次閱讀

    Linux內(nèi)核中有哪些鎖

    LInux操作系統(tǒng)里,同一時間可能有多個內(nèi)核執(zhí)行流在執(zhí)行,因此內(nèi)核其實象多進程多線程編程一樣也需要一些同步機制同步各執(zhí)行單元對共享數(shù)據(jù)的
    的頭像 發(fā)表于 02-24 15:26 ?3455次閱讀

    Linux內(nèi)核同步機制

    在現(xiàn)代操作系統(tǒng)里,同一時間可能有多個內(nèi)核執(zhí)行流在執(zhí)行,因此內(nèi)核其實像多進程多線程編程一樣也需要一些同步機制同步各執(zhí)行單元對共享數(shù)據(jù)的訪問,尤其是在多處理器系統(tǒng)上,更需要一些
    的頭像 發(fā)表于 09-22 09:46 ?2296次閱讀
    <b class='flag-5'>Linux</b><b class='flag-5'>內(nèi)核</b>的<b class='flag-5'>同步機制</b>

    關(guān)于Linux kernel同步機制的這些知識點你不得不知道

    同步就是進程與進程之間,進程與系統(tǒng)資源之間的交互。由于 Linux內(nèi)核采用的是多任務(wù),所以在多個進程之間,必須要有同步機制來保證彼此協(xié)調(diào)。
    的頭像 發(fā)表于 04-21 14:42 ?826次閱讀

    淺談Linux kernel中的同步機制

    同步就是進程與進程之間,進程與系統(tǒng)資源之間的交互。由于 Linux內(nèi)核采用的是多任務(wù),所以在多個進程之間,必須要有同步機制來保證彼此協(xié)調(diào)。
    的頭像 發(fā)表于 05-04 17:06 ?925次閱讀