自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Linux內(nèi)核同步機制:解鎖并發(fā)編程的奧秘

系統(tǒng) Linux
在 Linux 內(nèi)核這個 “交通指揮中心” 里,當多個進程或線程如同川流不息的車輛,試圖同時訪問共享資源時,問題就出現(xiàn)了。想象一下,兩條道路上的車輛都想同時通過一個狹窄的路口,如果沒有合理的交通規(guī)則,必然會導致?lián)矶律踔僚鲎病?/div>

在當今的數(shù)字時代,多核處理器早已成為計算機系統(tǒng)的標配,從我們?nèi)粘^k公的電腦,到數(shù)據(jù)中心里龐大的服務器集群,它們無處不在。這一硬件層面的發(fā)展,使得計算機系統(tǒng)能夠同時處理多個任務,極大地提升了計算效率。就如同繁忙的交通樞紐,多車道并行,車輛往來穿梭,看似混亂卻又有序運

然而,在 Linux 內(nèi)核這個 “交通指揮中心” 里,當多個進程或線程如同川流不息的車輛,試圖同時訪問共享資源時,問題就出現(xiàn)了。想象一下,兩條道路上的車輛都想同時通過一個狹窄的路口,如果沒有合理的交通規(guī)則,必然會導致?lián)矶律踔僚鲎?。?Linux 內(nèi)核中,這些共享資源就如同這個狹窄路口,而進程和線程的并發(fā)訪問如果缺乏有效的管理,就會引發(fā)數(shù)據(jù)不一致、程序崩潰等嚴重問題。這不僅會影響系統(tǒng)的穩(wěn)定性,還可能導致關鍵業(yè)務的中斷,造成不可估量的損失。

那么,Linux 內(nèi)核是如何在復雜的并發(fā)環(huán)境中,確保共享資源的安全訪問,維持系統(tǒng)的高效穩(wěn)定運行的呢?答案就在于其精心設計的同步機制。它就像一套精密的交通指揮系統(tǒng),通過各種規(guī)則和信號,引導著進程和線程這些 “車輛” 有序地通過共享資源這個 “路口”。接下來,就讓我們一同深入 Linux 內(nèi)核同步機制的世界,探尋其中的奧秘,解鎖并發(fā)編程的關鍵技巧,為構(gòu)建更穩(wěn)定、高效的系統(tǒng)奠定堅實基礎。

常用的 Linux 內(nèi)核同步機制有原子操作、Per-CPU 變量、內(nèi)存屏障、自旋鎖、Mutex 鎖、信號量和 RCU 等,后面幾種鎖實現(xiàn)會依賴于前三種基礎同步機制。在正式開始分析具體的內(nèi)核同步機制實現(xiàn)之前,需要先澄清一些基本概念。

一、基本概念

1.1 同步機制

既然是同步機制,那就首先要搞明白什么是同步。同步是指用于實現(xiàn)控制多個執(zhí)行路徑按照一定的規(guī)則或順序訪問某些系統(tǒng)資源的機制。所謂執(zhí)行路徑,就是在 CPU 上運行的代碼流。我們知道,CPU 調(diào)度的最小單位是線程,可以是用戶態(tài)線程,也可以是內(nèi)核線程,甚至是中斷服務程序。所以,執(zhí)行路徑在這里就包括用戶態(tài)線程、內(nèi)核線程和中斷服務程序。執(zhí)行路徑、執(zhí)行單元、控制路徑等等,叫法不同,但本質(zhì)都一樣。那為什么需要同步機制呢?請繼續(xù)往下看。

1.2 并發(fā)與競態(tài)

并發(fā)是指兩個以上的執(zhí)行路徑同時被執(zhí)行,而并發(fā)的執(zhí)行路徑對共享資源(硬件資源和軟件上的全局變量等)的訪問則很容易導致競態(tài)。例如,現(xiàn)在系統(tǒng)有一個 LED 燈可以由 APP 控制,APP1 控制燈亮一秒滅一秒,APP2 控制燈亮 500ms 滅 1500ms。如果 APP1 和 APP2 分別在 CPU1 和 CPU2 上并發(fā)運行,LED 燈的行為會是什么樣的呢?很有可能 LED 燈的亮滅節(jié)奏都不會如這兩個 APP 所愿,APP1 在關掉 LED 燈時,很有可能恰逢 APP2 正要打開 LED 燈。很明顯,APP1 和 APP2 對 LED 燈這個資源產(chǎn)生了競爭關系。競態(tài)是危險的,如果不加以約束,輕則只是程序運行結(jié)果不符合預期,重則系統(tǒng)崩潰。

在操作系統(tǒng)中,更復雜、更混亂的并發(fā)大量存在,而同步機制正是為了解決并發(fā)和競態(tài)問題。同步機制通過保護臨界區(qū)(訪問共享資源的代碼區(qū)域)達到對共享資源互斥訪問的目的,所謂互斥訪問,是指一個執(zhí)行路徑在訪問共享資源時,另一個執(zhí)行路徑被禁止去訪問。關于并發(fā)與競態(tài),有個生活例子很貼切。假如你和你的同事張小三都要上廁所,但是公司只有一個洗手間而且也只有一個坑。當張小三進入廁所關起門的那一刻起,你就無法進去了,只能在門外侯著。

當小三哥出來后你才能進去解決你的問題。這里,公司廁所就是共享資源,你和張小三同時需要這個共享資源就是并發(fā),你們對廁所的使用需求就構(gòu)成了競態(tài),而廁所的門就是一種同步機制,他在用你就不能用了

總結(jié)如下圖:

圖片圖片

1.3 中斷與搶占

中斷本身的概念很簡單,本文不予解釋。當然,這并不是說 Linux 內(nèi)核的中斷部分也很簡單。事實上,Linux 內(nèi)核的中斷子系統(tǒng)也相當復雜,因為中斷對于操作系統(tǒng)來說實在是太重要了。以后有機會,筆者計劃開專題再來介紹。對于同步機制的代碼分析來說,了解中斷的概念即可,不需要深入分析內(nèi)核的具體代碼實現(xiàn)。

搶占屬于進程調(diào)度的概念,Linux 內(nèi)核從 2.6 版本開始支持搶占調(diào)度。進程調(diào)度(管理)是 Linux 內(nèi)核最核心的子系統(tǒng)之一,異常龐大,本文只簡單介紹基本概念,對于同步機制的代碼分析已然足夠。通俗地說,搶占是指一個正愉快地運行在 CPU 上的 task(可以是用戶態(tài)進程,也可以是內(nèi)核線程) 被另一個 task(通常是更高優(yōu)先級)奪去 CPU 執(zhí)行權的故事。

中斷和搶占之間有著比較曖昧的關系,簡單來說,搶占依賴中斷。如果當前 CPU 禁止了本地中斷,那么也意味著禁止了本 CPU 上的搶占。但反過來,禁掉搶占并不影響中斷。Linux 內(nèi)核中用 preempt_enable() 宏函數(shù)來開啟本 CPU 的搶占,用 preempt_disable() 來禁掉本 CPU 的搶占。

這里,“本 CPU” 這個描述其實不太準確,更嚴謹?shù)恼f法是運行在當前 CPU 上的 task。preempt_enable() 和 preempt_disable() 的具體實現(xiàn)展開來介紹的話也可以單獨成文了,筆者沒有深究過,就不班門弄斧了,感興趣的讀者可以去 RTFSC。不管是用戶態(tài)搶占還是內(nèi)核態(tài)搶占,并不是什么代碼位置都能發(fā)生,而是有搶占時機的,也就是所謂的搶占點。搶占時機如下:

用戶態(tài)搶占

1、從系統(tǒng)調(diào)用返回用戶空間時;

2、從中斷(異常)處理程序返回用戶空間時。

內(nèi)核態(tài)搶占:

1、當一個中斷處理程序退出,返回到內(nèi)核態(tài)時;

2、task 顯式調(diào)用 schedule();

3、task 發(fā)生阻塞(此時由調(diào)度器完成調(diào)度)。

1.4 編譯亂序與編譯屏障

編譯器(compiler)的工作就是優(yōu)化我們的代碼以提高性能。這包括在不改變程序行為的情況下重新排列指令。因為 compiler 不知道什么樣的代碼需要線程安全(thread-safe),所以 compiler 假設我們的代碼都是單線程執(zhí)行(single-threaded),并且進行指令重排優(yōu)化并保證是單線程安全的。因此,當你不需要 compiler 重新排序指令的時候,你需要顯式告訴 compiler,我不需要重排。否則,它可不會聽你的。本篇文章中,我們一起探究 compiler 關于指令重排的優(yōu)化規(guī)則。

注:測試使用 aarch64-linux-gnu-gcc 版本:7.3.0

編譯器指令重排(Compiler Instruction Reordering)

compiler 的主要工作就是將對人們可讀的源碼轉(zhuǎn)化成機器語言,機器語言就是對 CPU 可讀的代碼。因此,compiler 可以在背后做些不為人知的事情。我們考慮下面的 C語言代碼:

int a, b;
 
void foo(void)
{
    a = b + 1;
    b = 0;
}

使用 aarch64-linux-gnu-gcc 在不優(yōu)化代碼的情況下編譯上述代碼,使用 objdump 工具查看 foo() 反匯編結(jié)果

<foo>:
    ...
    ldr w0, [x0]       //load b to w0
    add w1, w0, #0x1
    ...
    str w1, [x0]       //a = b + 1
    ...
    str wzr, [x0]      //b = 0

我們應該知道 Linux 默認編譯優(yōu)化選項是 -O2,因此我們采用 -O2 優(yōu)化選項編譯上述代碼,并反匯編得到如下匯編結(jié)果:

<foo>:
    ...
    ldr w2, [x0]       //load b to w2
    str wzr, [x0]      //b = 0
    add w0, w2, #0x1
    str w0, [x1]       //a = b + 1

比較優(yōu)化和不優(yōu)化的結(jié)果,我們可以發(fā)現(xiàn):在不優(yōu)化的情況下,a 和 b 的寫入內(nèi)存順序符合代碼順序(program order);但是 -O2 優(yōu)化后,a 和 b 的寫入順序和 program order 是相反的。-O2 優(yōu)化后的代碼轉(zhuǎn)換成 C 語言可以看作如下形式:

int a, b;
 
void foo(void)
{
    register int reg = b;
 
    b = 0;
    a = reg + 1;
}

這就是 compiler reordering(編譯器重排)。為什么可以這么做呢?對于單線程來說,a 和 b 的寫入順序,compiler 認為沒有任何問題。并且最終的結(jié)果也是正確的(a == 1 && b == 0)。這種 compiler reordering 在大部分情況下是沒有問題的。但是在某些情況下可能會引入問題。例如我們使用一個全局變量 flag 標記共享數(shù)據(jù) data 是否就緒。由于 compiler reordering,可能會引入問題??紤]下面的代碼(無鎖編程):

int flag, data;
 
void write_data(int value)
{
    data = value;
    flag = 1;
}

如果 compiler 產(chǎn)生的匯編代碼是 flag 比 data 先寫入內(nèi)存,那么,即使是單核系統(tǒng)上,我們也會有問題。在 flag 置 1 之后,data 寫 45 之前,系統(tǒng)發(fā)生搶占。另一個進程發(fā)現(xiàn) flag 已經(jīng)置 1,認為 data 的數(shù)據(jù)已經(jīng)準備就緒。但是實際上讀取 data 的值并不是 45。為什么 compiler 還會這么操作呢?因為,compiler 并不知道 data 和 flag 之間有嚴格的依賴關系。這種邏輯關系是我們?nèi)藶閺娂拥?。我們?nèi)绾伪苊膺@種優(yōu)化呢?

顯式編譯器屏障(Explicit Compiler Barriers)

為了解決上述變量之間存在依賴關系導致 compiler 錯誤優(yōu)化。compiler 為我們提供了編譯器屏障(compiler barriers),可用來告訴 compiler 不要 reorder。我們繼續(xù)使用上面的 foo() 函數(shù)作為演示實驗,在代碼之間插入 compiler barriers。

#define barrier() __asm__ __volatile__("": : :"memory")
 
int a, b;
 
void foo(void)
{
    a = b + 1;
    barrier();
    b = 0;
}

barrier() 就是 compiler 提供的屏障,作用是告訴 compiler 內(nèi)存中的值已經(jīng)改變,之前對內(nèi)存的緩存(緩存到寄存器)都需要拋棄,barrier() 之后的內(nèi)存操作需要重新從內(nèi)存 load,而不能使用之前寄存器緩存的值。并且可以防止 compiler 優(yōu)化 barrier() 前后的內(nèi)存訪問順序。barrier() 就像是代碼中的一道不可逾越的屏障,barrier() 前的 load/store 操作不能跑到 barrier() 后面;同樣,barrier() 后面的 load/store 操作不能在 barrier() 之前。依然使用 -O2 優(yōu)化選項編譯上述代碼,反匯編得到如下結(jié)果:

<foo>:
    ...
    ldr w2, [x0]       //load b to w2
    add w2, w2, #0x1
    str w2, [x1]       //a = a + 1
    str wzr, [x0]      //b = 0
    ...

我們可以看到插入 compiler barriers 之后,a 和 b 的寫入順序和 program order 一致。因此,當我們的代碼中需要嚴格的內(nèi)存順序,就需要考慮 compiler barriers。

隱式編譯器屏障(Implied Compiler Barriers)

除了顯示的插入 compiler barriers 之外,還有別的方法阻止 compiler reordering。例如 CPU barriers 指令,同樣會阻止 compiler reordering。后續(xù)我們再考慮 CPU barriers。除此以外,當某個函數(shù)內(nèi)部包含 compiler barriers 時,該函數(shù)也會充當 compiler barriers 的作用。即使這個函數(shù)被 inline,也是這樣。例如上面插入 barrier() 的 foo() 函數(shù),當其他函數(shù)調(diào)用 foo() 時,foo() 就相當于 compiler barriers??紤]下面的代碼:

int a, b, c;
 
void fun(void)
{
    c = 2;
    barrier();
}
 
void foo(void)
{
    a = b + 1;
    fun(); /* fun() call acts as compiler barriers */
    b = 0;
}

fun() 函數(shù)包含 barrier(),因此 foo() 函數(shù)中 fun() 調(diào)用也表現(xiàn)出 compiler barriers 的作用,同樣可以保證 a 和 b 的寫入順序。如果 fun() 函數(shù)不包含 barrier(),結(jié)果又會怎么樣呢?實際上,大多數(shù)的函數(shù)調(diào)用都表現(xiàn)出 compiler barriers 的作用。但是,這不包含 inline 的函數(shù)。因此,fun() 如果被 inline 進 foo(),那么 fun() 就不具有 compiler barriers 的作用。

如果被調(diào)用的函數(shù)是一個外部函數(shù),其副作用會比 compiler barriers 還要強。因為 compiler 不知道函數(shù)的副作用是什么。它必須忘記它對內(nèi)存所作的任何假設,即使這些假設對該函數(shù)可能是可見的。我們看一下下面的代碼片段,printf() 一定是一個外部的函數(shù)。

int a, b;
 
void foo(void)
{
    a = 5;
    printf("smcdef");
    b = a;
}

同樣使用 -O2 優(yōu)化選項編譯代碼,objdump 反匯編得到如下結(jié)果:

<foo>:
    ...
    mov w2, #0x5              //#5
    str w2, [x19]             //a = 5
    bl 640 <__printf_chk@plt> //printf()
    ldr w1, [x19]             //reload a to w1
    ...
    str w1, [x0]              //b = a

compiler 不能假設 printf() 不會使用或者修改 a 變量。因此在調(diào)用 printf() 之前會將 a 寫 5,以保證 printf() 可能會用到新值。在 printf() 調(diào)用之后,重新從內(nèi)存中 load a 的值,然后賦值給變量 b。重新 load a 的原因是 compiler 也不知道 printf() 會不會修改 a 的值。

因此,我們可以看到即使存在 compiler reordering,但是還是有很多限制。當我們需要考慮 compiler barriers 時,一定要顯示的插入 barrier(),而不是依靠函數(shù)調(diào)用附加的隱式 compiler barriers。因為,誰也無法保證調(diào)用的函數(shù)不會被 compiler 優(yōu)化成 inline 方式。

barrier() 除了防止編譯亂序,還能做什么。

barriers() 作用除了防止 compiler reordering 之外,還有什么妙用嗎?我們考慮下面的代碼片段:

int run = 1;
 
void foo(void)
{
    while (run)
        ;
}

run 是個全局變量,foo() 在一個進程中執(zhí)行,一直循環(huán)。我們期望的結(jié)果是 foo() 一直等到其他進程修改 run 的值為 0 才退出循環(huán)。實際 compiler 編譯的代碼和我們會達到我們預期的結(jié)果嗎?我們看一下匯編代碼:

0000000000000748 <foo>:
748: 90000080 adrp x0, 10000
74c: f947e800 ldr x0, [x0, #4048]
750: b9400000 ldr w0, [x0]            //load run to w0
754: d503201f nop
758: 35000000 cbnz w0, 758 <foo+0x10> //if (w0) while (1);
75c: d65f03c0 ret

匯編代碼可以轉(zhuǎn)換成如下的 C 語言形式:

int run = 1;
 
void foo(void)
{
    register int reg = run;
 
    if (reg)
        while (1)
            ;
}

compiler 首先將 run 加載到一個寄存器 reg 中,然后判斷 reg 是否滿足循環(huán)條件,如果滿足就一直循環(huán)。但是循環(huán)過程中,寄存器 reg 的值并沒有變化。因此,即使其他進程修改 run 的值為 0,也不能使 foo() 退出循環(huán)。很明顯,這不是我們想要的結(jié)果。我們繼續(xù)看一下加入 barrier() 后的結(jié)果:

0000000000000748 <foo>:
748: 90000080 adrp x0, 10000
74c: f947e800 ldr x0, [x0, #4048]
750: b9400001 ldr w1, [x0]            //load run to w0
754: 34000061 cbz w1, 760 <foo+0x18>
758: b9400001 ldr w1, [x0]            //load run to w0
75c: 35ffffe1 cbnz w1, 758 <foo+0x10> //if (w0) goto 758
760: d65f03c0 ret

可以看到加入 barrier() 后的結(jié)果真是我們想要的。每一次循環(huán)都會從內(nèi)存中重新 load run 的值。因此,當有其他進程修改 run 的值為 0 的時候,foo() 可以正常退出循環(huán)。為什么加入 barrier() 后的匯編代碼就是正確的呢?因為 barrier() 作用是告訴 compiler 內(nèi)存中的值已經(jīng)變化,后面的操作都需要重新從內(nèi)存 load,而不能使用寄存器緩存的值。因此,這里的 run 變量會從內(nèi)存重新 load,然后判斷循環(huán)條件。這樣,其他進程修改 run 變量,foo() 就可以看得見了。

在 Linux kernel 中,提供了 cpu_relax() 函數(shù),該函數(shù)在 ARM64 平臺定義如下:

static inline void cpu_relax(void)
{
    asm volatile("yield" ::: "memory");
}

我們可以看出,cpu_relax() 是在 barrier() 的基礎上又插入一條匯編指令 yield。在 kernel 中,我們經(jīng)常會看到一些類似上面舉例的 while 循環(huán),循環(huán)條件是個全局變量。為了避免上述所說問題,我們就會在循環(huán)中插入 cpu_relax() 調(diào)用。

int run = 1;
 
void foo(void)
{
    while (run)
        cpu_relax();
}

當然也可以使用 Linux 提供的 READ_ONCE()。例如,下面的修改也同樣可以達到我們預期的效果。

int run = 1;
 
void foo(void)
{
    while (READ_ONCE(run)) /* similar to while (*(volatile int *)&run) */
        ;
}

當然你也可以修改 run 的定義為 volatile int run,就會得到如下代碼。同樣可以達到預期目的。

volatile int run = 1;
 
void foo(void)
{
    while (run);
}

二、同步機制的起源

在深入探討 Linux 內(nèi)核同步機制之前,我們先來理解一下并發(fā)(Concurrency)與競態(tài)(Race Condition)的概念,因為它們是同步機制存在的根本原因。

2.1 并發(fā)的多種形式

并發(fā),簡單來說,就是指多個執(zhí)行單元同時、并行地被執(zhí)行 。在 Linux 系統(tǒng)中,并發(fā)主要有以下幾種場景:

SMP 多 CPU:對稱多處理器(SMP)是一種緊耦合、共享存儲的系統(tǒng)模型,多個 CPU 使用共同的系統(tǒng)總線,可以訪問共同的外設和存儲器 。在這種情況下,兩個 CPU 之間的進程、中斷都有并發(fā)的可能性。例如,CPU0 上的進程 A 和 CPU1 上的進程 B 可能同時訪問共享內(nèi)存中的同一數(shù)據(jù)。

單 CPU 內(nèi)進程與搶占進程:在單個 CPU 中,雖然同一時刻只能有一個進程在運行,但進程的執(zhí)行可能會被打斷。比如,一個進程在執(zhí)行過程中,可能會因為時間片耗盡,或者被另一個高優(yōu)先級的進程搶占。當高優(yōu)先級進程與被打斷的進程共同訪問共享資源時,就可能產(chǎn)生競態(tài)。比如進程 A 正在訪問一個全局變量,還沒來得及修改完,就被進程 B 搶占,進程 B 也對這個全局變量進行訪問和修改,就可能導致數(shù)據(jù)混亂。

中斷與進程:中斷可以打斷正在執(zhí)行的進程 。如果中斷服務程序也訪問進程正在訪問的共享資源,就很容易產(chǎn)生競態(tài)。比如,進程正在向串口發(fā)送數(shù)據(jù),這時一個中斷發(fā)生,中斷服務程序也嘗試向串口發(fā)送數(shù)據(jù),就會導致串口數(shù)據(jù)發(fā)送錯誤。

2.2 競態(tài)帶來的問題

當多個并發(fā)執(zhí)行單元訪問共享資源時,競態(tài)就可能出現(xiàn)。競態(tài)會導致程序出現(xiàn)不可預測的行為,比如數(shù)據(jù)不一致、程序崩潰等 。我們來看一個簡單的例子,假設有兩個進程 P1 和 P2,它們都要對一個共享變量 count 進行加 1 操作。代碼可能如下:

// 共享變量
int count = 0;

// 進程P1的操作
void process1() {
    int temp = count; // 讀取count的值
    temp = temp + 1; // 對temp加1
    count = temp; // 將temp的值寫回count
}

// 進程P2的操作
void process2() {
    int temp = count; // 讀取count的值
    temp = temp + 1; // 對temp加1
    count = temp; // 將temp的值寫回count
}

如果這兩個進程并發(fā)執(zhí)行,正常情況下,count 最終的值應該是 2。但由于競態(tài)的存在,可能會出現(xiàn)以下情況:

  1. 進程 P1 讀取 count 的值,此時 temp 為 0。
  2. 進程 P2 讀取 count 的值,此時 temp 也為 0,因為 P1 還沒有將修改后的值寫回 count。
  3. 進程 P1 對 temp加 1,然后將 temp 的值寫回 count,此時 count 為 1。
  4. 進程P2對temp加1(此時 temp 還是 0),然后將temp的值寫回 count,此時 count 還是 1,而不是 2。

這就是競態(tài)導致的數(shù)據(jù)錯誤。在實際的 Linux 內(nèi)核中,共享資源可能是硬件設備、全局變量、文件系統(tǒng)等,競態(tài)帶來的問題會更加復雜和嚴重,可能導致系統(tǒng)不穩(wěn)定、數(shù)據(jù)丟失等問題 。因此,為了保證系統(tǒng)的正確性和穩(wěn)定性,Linux 內(nèi)核需要一套有效的同步機制來解決競態(tài)問題。

三、常見同步機制解析

為了解決并發(fā)與競態(tài)問題,Linux 內(nèi)核提供了多種同步機制 ,每種機制都有其獨特的工作原理和適用場景。下面我們來詳細了解一下這些同步機制。

3.1 自旋鎖(Spinlocks)

自旋鎖是一種比較簡單的同步機制 。當一個線程嘗試獲取自旋鎖時,如果鎖已經(jīng)被其他線程持有,那么該線程不會進入阻塞狀態(tài),而是在原地不斷地循環(huán)檢查鎖是否可用,這個過程就叫做 “自旋” 。就好像你去餐廳吃飯,發(fā)現(xiàn)你喜歡的那桌還被別人占著,你又特別想坐那桌,于是你就站在旁邊一直盯著,等那桌人吃完離開,你馬上就能坐過去,這個一直盯著等待的過程就類似自旋。

自旋鎖適用于鎖持有時間非常短的場景 ,因為它避免了線程上下文切換的開銷。在多處理器系統(tǒng)中,當一個線程在自旋等待鎖時,其他處理器核心可以繼續(xù)執(zhí)行其他任務,不會因為線程阻塞而導致 CPU 資源浪費 。比如在一些對共享硬件資源的短時間訪問場景中,自旋鎖就非常適用。假設多個線程需要訪問共享的網(wǎng)卡設備寄存器,對寄存器的操作通常非???,使用自旋鎖可以讓線程快速獲取鎖并完成操作,避免了線程上下文切換帶來的開銷。

自旋鎖也有其局限性。如果鎖持有時間較長,線程會一直自旋,不斷消耗 CPU 資源,導致系統(tǒng)性能下降 。所以在使用自旋鎖時,需要根據(jù)實際情況謹慎選擇。

自旋鎖的API有:

  • spin_lock_init(x)該宏用于初始化自旋鎖x。自旋鎖在真正使用前必須先初始化。該宏用于動態(tài)初始化。
  • DEFINE_SPINLOCK(x)該宏聲明一個自旋鎖x并初始化它。該宏在2.6.11中第一次被定義,在先前的內(nèi)核中并沒有該宏。
  • SPIN_LOCK_UNLOCKED該宏用于靜態(tài)初始化一個自旋鎖。
  • DEFINE_SPINLOCK(x)等同于spinlock_t x = SPIN_LOCK_UNLOCKEDspin_is_locked(x)該宏用于判斷自旋鎖x是否已經(jīng)被某執(zhí)行單元保持(即被鎖),如果是,返回真,否則返回假。
  • spin_unlock_wait(x)該宏用于等待自旋鎖x變得沒有被任何執(zhí)行單元保持,如果沒有任何執(zhí)行單元保持該自旋鎖,該宏立即返回,否則將循環(huán)在那里,直到該自旋鎖被保持者釋放。
  • spin_trylock(lock)該宏盡力獲得自旋鎖lock,如果能立即獲得鎖,它獲得鎖并返回真,否則不能立即獲得鎖,立即返回假。它不會自旋等待lock被釋放。
  • spin_lock(lock)該宏用于獲得自旋鎖lock,如果能夠立即獲得鎖,它就馬上返回,否則,它將自旋在那里,直到該自旋鎖的保持者釋放,這時,它獲得鎖并返回??傊?,只有它獲得鎖才返回。
  • spin_lock_irqsave(lock, flags)該宏獲得自旋鎖的同時把標志寄存器的值保存到變量flags中并失效本地中斷。
  • spin_lock_irq(lock)該宏類似于spin_lock_irqsave,只是該宏不保存標志寄存器的值。
  • spin_lock_bh(lock)該宏在得到自旋鎖的同時失效本地軟中斷。
  • spin_unlock(lock)該宏釋放自旋鎖lock,它與spin_trylock或spin_lock配對使用。如果spin_trylock返回假,表明沒有獲得自旋鎖,因此不必使用spin_unlock釋放。
  • spin_unlock_irqrestore(lock, flags)該宏釋放自旋鎖lock的同時,也恢復標志寄存器的值為變量flags保存的值。它與spin_lock_irqsave配對使用。
  • spin_unlock_irq(lock)該宏釋放自旋鎖lock的同時,也使能本地中斷。它與spin_lock_irq配對應用。
  • spin_unlock(lock)該宏釋放自旋鎖lock,它與spin_trylock或spin_lock配對使用。如果spin_trylock返回假,表明沒有獲得自旋鎖,因此不必使用spin_unlock釋放。
  • spin_unlock_irqrestore(lock, flags)該宏釋放自旋鎖lock的同時,也恢復標志寄存器的值為變量flags保存的值。它與spin_lock_irqsave配對使用。
  • spin_unlock_irq(lock)該宏釋放自旋鎖lock的同時,也使能本地中斷。它與spin_lock_irq配對應用。
  • spin_unlock_bh(lock)該宏釋放自旋鎖lock的同時,也使能本地的軟中斷。它與spin_lock_bh配對使用。
  • spin_trylock_irqsave(lock, flags) 該宏如果獲得自旋鎖lock,它也將保存標志寄存器的值到變量flags中,并且失效本地中斷,如果沒有獲得鎖,它什么也不做。因此如果能夠立即獲得鎖,它等同于spin_lock_irqsave,如果不能獲得鎖,它等同于spin_trylock。如果該宏獲得自旋鎖lock,那需要使用spin_unlock_irqrestore來釋放。
  • spin_unlock_bh(lock)該宏釋放自旋鎖lock的同時,也使能本地的軟中斷。它與spin_lock_bh配對使用。
  • spin_trylock_irqsave(lock, flags) 該宏如果獲得自旋鎖lock,它也將保存標志寄存器的值到變量flags中,并且失效本地中斷,如果沒有獲得鎖,它什么也不做。因此如果能夠立即獲得鎖,它等同于spin_lock_irqsave,如果不能獲得鎖,它等同于spin_trylock。如果該宏獲得自旋鎖lock,那需要使用spin_unlock_irqrestore來釋放。
  • spin_can_lock(lock)該宏用于判斷自旋鎖lock是否能夠被鎖,它實際是spin_is_locked取反。如果lock沒有被鎖,它返回真,否則,返回假。該宏在2.6.11中第一次被定義,在先前的內(nèi)核中并沒有該宏。

獲得自旋鎖和釋放自旋鎖有好幾個版本,因此讓讀者知道在什么樣的情況下使用什么版本的獲得和釋放鎖的宏是非常必要的。

如果被保護的共享資源只在進程上下文訪問和軟中斷上下文訪問,那么當在進程上下文訪問共享資源時,可能被軟中斷打斷,從而可能進入軟中斷上下文來對被保護的共享資源訪問,因此對于這種情況,對共享資源的訪問必須使用spin_lock_bh和spin_unlock_bh來保護。

當然使用spin_lock_irq和spin_unlock_irq以及spin_lock_irqsave和spin_unlock_irqrestore也可以,它們失效了本地硬中斷,失效硬中斷隱式地也失效了軟中斷。但是使用spin_lock_bh和spin_unlock_bh是最恰當?shù)?,它比其他兩個快。

如果被保護的共享資源只在進程上下文和tasklet或timer上下文訪問,那么應該使用與上面情況相同的獲得和釋放鎖的宏,因為tasklet和timer是用軟中斷實現(xiàn)的。

如果被保護的共享資源只在一個tasklet或timer上下文訪問,那么不需要任何自旋鎖保護,因為同一個tasklet或timer只能在一個CPU上運行,即使是在SMP環(huán)境下也是如此。實際上tasklet在調(diào)用tasklet_schedule標記其需要被調(diào)度時已經(jīng)把該tasklet綁定到當前CPU,因此同一個tasklet決不可能同時在其他CPU上運行。timer也是在其被使用add_timer添加到timer隊列中時已經(jīng)被幫定到當前CPU,所以同一個timer絕不可能運行在其他CPU上。當然同一個tasklet有兩個實例同時運行在同一個CPU就更不可能了。

如果被保護的共享資源只在兩個或多個tasklet或timer上下文訪問,那么對共享資源的訪問僅需要用spin_lock和spin_unlock來保護,不必使用_bh版本,因為當tasklet或timer運行時,不可能有其他tasklet或timer在當前CPU上運行。

如果被保護的共享資源只在一個軟中斷(tasklet和timer除外)上下文訪問,那么這個共享資源需要用spin_lock和spin_unlock來保護,因為同樣的軟中斷可以同時在不同的CPU上運行。

如果被保護的共享資源在兩個或多個軟中斷上下文訪問,那么這個共享資源當然更需要用spin_lock和spin_unlock來保護,不同的軟中斷能夠同時在不同的CPU上運行。

如果被保護的共享資源在軟中斷(包括tasklet和timer)或進程上下文和硬中斷上下文訪問,那么在軟中斷或進程上下文訪問期間,可能被硬中斷打斷,從而進入硬中斷上下文對共享資源進行訪問,因此,在進程或軟中斷上下文需要使用spin_lock_irq和spin_unlock_irq來保護對共享資源的訪問。

而在中斷處理句柄中使用什么版本,需依情況而定,如果只有一個中斷處理句柄訪問該共享資源,那么在中斷處理句柄中僅需要spin_lock和spin_unlock來保護對共享資源的訪問就可以了。

因為在執(zhí)行中斷處理句柄期間,不可能被同一CPU上的軟中斷或進程打斷。但是如果有不同的中斷處理句柄訪問該共享資源,那么需要在中斷處理句柄中使用spin_lock_irq和spin_unlock_irq來保護對共享資源的訪問。

在使用spin_lock_irq和spin_unlock_irq的情況下,完全可以用spin_lock_irqsave和spin_unlock_irqrestore取代,那具體應該使用哪一個也需要依情況而定,如果可以確信在對共享資源訪問前中斷是使能的,那么使用spin_lock_irq更好一些。

因為它比spin_lock_irqsave要快一些,但是如果你不能確定是否中斷使能,那么使用spin_lock_irqsave和spin_unlock_irqrestore更好,因為它將恢復訪問共享資源前的中斷標志而不是直接使能中斷。

當然,有些情況下需要在訪問共享資源時必須中斷失效,而訪問完后必須中斷使能,這樣的情形使用spin_lock_irq和spin_unlock_irq最好。

需要特別提醒讀者,spin_lock用于阻止在不同CPU上的執(zhí)行單元對共享資源的同時訪問以及不同進程上下文互相搶占導致的對共享資源的非同步訪問,而中斷失效和軟中斷失效卻是為了阻止在同一CPU上軟中斷或中斷對共享資源的非同步訪問。

3.2 互斥鎖(Mutexes)

互斥鎖,也叫互斥量 ,是一種用于實現(xiàn)線程間互斥訪問的同步機制 。它的工作原理是,當一個線程獲取到互斥鎖后,其他線程如果也嘗試獲取該鎖,就會被阻塞,直到持有鎖的線程釋放鎖 。這就好比一個公共衛(wèi)生間,一次只能允許一個人使用,當有人進入衛(wèi)生間并鎖上門后,其他人就只能在外面排隊等待,直到里面的人出來打開門,外面的人才有機會進去使用。

與自旋鎖不同,互斥鎖適用于那些可能會阻塞很長時間的場景 。當線程獲取不到鎖時,它會被操作系統(tǒng)掛起,讓出 CPU 資源,不會像自旋鎖那樣一直占用 CPU 進行無效的等待 。在涉及大量計算或者 IO 操作的代碼段中,使用互斥鎖可以避免 CPU 資源的浪費。比如在數(shù)據(jù)庫操作中,一個線程需要長時間占用數(shù)據(jù)庫連接執(zhí)行復雜的查詢或者事務操作,這時使用互斥鎖來保護數(shù)據(jù)庫連接資源,其他線程在獲取不到鎖時會被阻塞,直到當前線程完成數(shù)據(jù)庫操作并釋放鎖,這樣可以有效地管理資源,提高系統(tǒng)的整體性能。

3.3 讀寫鎖(Read-Write Locks)

讀寫鎖是一種特殊的同步機制,它允許多個線程同時進行讀操作,但只允許一個線程進行寫操作 。當有線程正在進行寫操作時,其他線程無論是讀操作還是寫操作都將被阻塞,直到寫操作完成并釋放鎖 。這就像圖書館的一本熱門書籍,很多人可以同時閱讀這本書,但如果有人要對這本書進行修改(比如添加批注或者修正錯誤),就必須先獨占這本書,其他人在修改期間不能閱讀也不能修改,直到修改完成。

讀寫鎖的優(yōu)勢在于它能顯著提高并發(fā)性能,特別是在讀取頻繁而寫入較少的場景中 。在一個在線商城系統(tǒng)中,商品信息的展示(讀操作)非常頻繁,而商品信息的更新(寫操作)相對較少。使用讀寫鎖,多個用戶可以同時讀取商品信息,而當商家需要更新商品信息時,只需要獲取寫鎖,保證寫操作的原子性和數(shù)據(jù)一致性,這樣可以大大提高系統(tǒng)的并發(fā)處理能力,提升用戶體驗。

讀寫信號量的相關API有:

  1. DECLARE_RWSEM(name)該宏聲明一個讀寫信號量name并對其進行初始化。
  2. void init_rwsem(struct rw_semaphore *sem);該函數(shù)對讀寫信號量sem進行初始化。
  3. void down_read(struct rw_semaphore *sem);讀者調(diào)用該函數(shù)來得到讀寫信號量sem。該函數(shù)會導致調(diào)用者睡眠,因此只能在進程上下文使用。
  4. int down_read_trylock(struct rw_semaphore *sem);該函數(shù)類似于down_read,只是它不會導致調(diào)用者睡眠。它盡力得到讀寫信號量sem,如果能夠立即得到,它就得到該讀寫信號量,并且返回1,否則表示不能立刻得到該信號量,返回0。因此,它也可以在中斷上下文使用。
  5. void down_write(struct rw_semaphore *sem);寫者使用該函數(shù)來得到讀寫信號量sem,它也會導致調(diào)用者睡眠,因此只能在進程上下文使用。
  6. int down_write_trylock(struct rw_semaphore *sem);該函數(shù)類似于down_write,只是它不會導致調(diào)用者睡眠。該函數(shù)盡力得到讀寫信號量,如果能夠立刻獲得,就獲得該讀寫信號量并且返回1,否則表示無法立刻獲得,返回0。它可以在中斷上下文使用。
  7. void up_read(struct rw_semaphore *sem);讀者使用該函數(shù)釋放讀寫信號量sem。它與down_read或down_read_trylock配對使用。如果down_read_trylock返回0,不需要調(diào)用up_read來釋放讀寫信號量,因為根本就沒有獲得信號量。
  8. void up_write(struct rw_semaphore *sem);寫者調(diào)用該函數(shù)釋放信號量sem。它與down_write或down_write_trylock配對使用。如果down_write_trylock返回0,不需要調(diào)用up_write,因為返回0表示沒有獲得該讀寫信號量。
  9. void downgrade_write(struct rw_semaphore *sem);該函數(shù)用于把寫者降級為讀者,這有時是必要的。因為寫者是排他性的,因此在寫者保持讀寫信號量期間,任何讀者或?qū)懻叨紝o法訪問該讀寫信號量保護的共享資源,對于那些當前條件下不需要寫訪問的寫者,降級為讀者將,使得等待訪問的讀者能夠立刻訪問,從而增加了并發(fā)性,提高了效率。對于那些當前條件下不需要寫訪問的寫者,降級為讀者將,使得等待訪問的讀者能夠立刻訪問,從而增加了并發(fā)性,提高了效率。讀寫信號量適于在讀多寫少的情況下使用,在linux內(nèi)核中對進程的內(nèi)存映像描述結(jié)構(gòu)的訪問就使用了讀寫信號量進行保護。

在Linux中,每一個進程都用一個類型為task_t或struct task_struct的結(jié)構(gòu)來描述,該結(jié)構(gòu)的類型為struct mm_struct的字段mm描述了進程的內(nèi)存映像,特別是mm_struct結(jié)構(gòu)的mmap字段維護了整個進程的內(nèi)存塊列表,該列表將在進程生存期間被大量地遍利或修改。結(jié)構(gòu)的mmap字段維護了整個進程的內(nèi)存塊列表,該列表將在進程生存期間被大量地遍利或修改。

因此mm_struct結(jié)構(gòu)就有一個字段mmap_sem來對mmap的訪問進行保護,mmap_sem就是一個讀寫信號量,在proc文件系統(tǒng)里有很多進程內(nèi)存使用情況的接口,通過它們能夠查看某一進程的內(nèi)存使用情況,命令free、ps和top都是通過proc來得到內(nèi)存使用信息的,proc接口就使用down_read和up_read來讀取進程的mmap信息。

當進程動態(tài)地分配或釋放內(nèi)存時,需要修改mmap來反映分配或釋放后的內(nèi)存映像,因此動態(tài)內(nèi)存分配或釋放操作需要以寫者身份獲得讀寫信號量mmap_sem來對mmap進行更新。系統(tǒng)調(diào)用brk和munmap就使用了down_write和up_write來保護對mmap的訪問。

3.4 信號量(Semaphores)

信號量是一個整數(shù)值,它可以用來控制對共享資源的訪問 。信號量主要有兩個作用:一是實現(xiàn)互斥,二是控制并發(fā)訪問的數(shù)量 。信號量內(nèi)部維護一個計數(shù)器,當線程請求訪問共享資源時,會嘗試獲取信號量,如果計數(shù)器大于 0,則線程可以獲取信號量并繼續(xù)執(zhí)行,同時計數(shù)器減一;如果計數(shù)器為 0,則線程會被阻塞,直到有其他線程釋放信號量,使得計數(shù)器增加 。這就像一個停車場,停車場有一定數(shù)量的停車位(信號量的初始值),每輛車進入停車場(線程請求資源)時,會占用一個停車位,停車位數(shù)量減一,如果停車位滿了(計數(shù)器為 0),新的車輛就只能在外面等待,直到有車輛離開停車場(線程釋放資源),停車位數(shù)量增加,等待的車輛才有機會進入。

在限制線程訪問文件資源數(shù)量的場景中,信號量就非常有用 。假設一個系統(tǒng)中,同時只允許5個線程對某個文件進行讀寫操作,我們可以創(chuàng)建一個初始值為5的信號量 。每個線程在訪問文件前,先獲取信號量,如果獲取成功則可以訪問文件,同時信號量的計數(shù)器減一;當線程完成文件訪問后,釋放信號量,計數(shù)器加一。這樣就可以有效地控制同時訪問文件的線程數(shù)量,避免資源的過度競爭和沖突 。

信號量的API有:

  • DECLARE_MUTEX(name)該宏聲明一個信號量name并初始化它的值為0,即聲明一個互斥鎖。
  • DECLARE_MUTEX_LOCKED(name)該宏聲明一個互斥鎖name,但把它的初始值設置為0,即鎖在創(chuàng)建時就處在已鎖狀態(tài)。因此對于這種鎖,一般是先釋放后獲得。
  • void sema_init (struct semaphore *sem, int val);該函用于數(shù)初始化設置信號量的初值,它設置信號量sem的值為val。
  • void init_MUTEX (struct semaphore *sem);該函數(shù)用于初始化一個互斥鎖,即它把信號量sem的值設置為1。
  • void init_MUTEX_LOCKED (struct semaphore *sem);該函數(shù)也用于初始化一個互斥鎖,但它把信號量sem的值設置為0,即一開始就處在已鎖狀態(tài)。
  • void down(struct semaphore * sem);該函數(shù)用于獲得信號量sem,它會導致睡眠,因此不能在中斷上下文(包括IRQ上下文和softirq上下文)使用該函數(shù)。該函數(shù)將把sem的值減1,如果信號量sem的值非負,就直接返回,否則調(diào)用者將被掛起,直到別的任務釋放該信號量才能繼續(xù)運行。
  • int down_interruptible(struct semaphore * sem);該函數(shù)功能與down類似,不同之處為,down不會被信號(signal)打斷,但down_interruptible能被信號打斷,因此該函數(shù)有返回值來區(qū)分是正常返回還是被信號中斷,如果返回0,表示獲得信號量正常返回,如果被信號打斷,返回-EINTR。
  • int down_trylock(struct semaphore * sem);該函數(shù)試著獲得信號量sem,如果能夠立刻獲得,它就獲得該信號量并返回0,否則,表示不能獲得信號量sem,返回值為非0值。因此,它不會導致調(diào)用者睡眠,可以在中斷上下文使用。
  • void up(struct semaphore * sem);該函數(shù)釋放信號量sem,即把sem的值加1,如果sem的值為非正數(shù),表明有任務等待該信號量,因此喚醒這些等待者。

信號量在絕大部分情況下作為互斥鎖使用,下面以console驅(qū)動系統(tǒng)為例說明信號量的使用。

在內(nèi)核源碼樹的kernel/printk.c中,使用宏DECLARE_MUTEX聲明了一個互斥鎖console_sem,它用于保護console驅(qū)動列表console_drivers以及同步對整個console驅(qū)動系統(tǒng)的訪問。

其中定義了函數(shù)acquire_console_sem來獲得互斥鎖console_sem,定義了release_console_sem來釋放互斥鎖console_sem,定義了函數(shù)try_acquire_console_sem來盡力得到互斥鎖console_sem。這三個函數(shù)實際上是分別對函數(shù)down,up和down_trylock的簡單包裝。

需要訪問console_drivers驅(qū)動列表時就需要使用acquire_console_sem來保護console_drivers列表,當訪問完該列表后,就調(diào)用release_console_sem釋放信號量console_sem。

函數(shù)console_unblank,console_device,console_stop,console_start,register_console和unregister_console都需要訪問console_drivers,因此它們都使用函數(shù)對acquire_console_sem和release_console_sem來對console_drivers進行保護。

3.5 原子操作(Atomic Operations)

原子操作是指那些不可被中斷的操作 ,即它們的執(zhí)行是一個完整的、不可分割的單元,不會被其他任務或事件打斷 。在多線程編程中,原子操作可以保證對共享資源的訪問是線程安全的,避免了競態(tài)條件的發(fā)生 。例如,在實現(xiàn)資源計數(shù)和引用計數(shù)方面,原子操作就發(fā)揮著重要作用 。

假設有一個共享資源,多個線程可能會對其引用計數(shù)進行增加或減少操作,如果這些操作不是原子的,就可能會出現(xiàn)競態(tài)條件,導致引用計數(shù)錯誤。而使用原子操作,就可以確保每次對引用計數(shù)的修改都是原子的,不會受到其他線程的干擾,從而保證了資源計數(shù)的準確性和一致性 。在 C 語言中,可以使用atomic庫來實現(xiàn)原子操作 ,比如atomic_fetch_add函數(shù)可以原子地對一個變量進行加法操作 。原子類型定義如下:

typedef struct { 
volatile int counter; 
} atomic_t;

volatile修飾字段告訴gcc不要對該類型的數(shù)據(jù)做優(yōu)化處理,對它的訪問都是對內(nèi)存的訪問,而不是對寄存器的訪問。原子操作API包括:

  • tomic_read(atomic_t * v);該函數(shù)對原子類型的變量進行原子讀操作,它返回原子類型的變量v的值。
  • atomic_set(atomic_t * v, int i);該函數(shù)設置原子類型的變量v的值為i。
  • void atomic_add(int i, atomic_t *v);該函數(shù)給原子類型的變量v增加值i。
  • atomic_sub(int i, atomic_t *v);該函數(shù)從原子類型的變量v中減去i。
  • int atomic_sub_and_test(int i, atomic_t *v);該函數(shù)從原子類型的變量v中減去i,并判斷結(jié)果是否為0,如果為0,返回真,否則返回假。
  • void atomic_inc(atomic_t *v);該函數(shù)對原子類型變量v原子地增加1。
  • void atomic_dec(atomic_t *v);該函數(shù)對原子類型的變量v原子地減1。
  • int atomic_dec_and_test(atomic_t *v);該函數(shù)對原子類型的變量v原子地減1,并判斷結(jié)果是否為0,如果為0,返回真,否則返回假。
  • int atomic_inc_and_test(atomic_t *v);該函數(shù)對原子類型的變量v原子地增加1,并判斷結(jié)果是否為0,如果為0,返回真,否則返回假。
  • int atomic_add_negative(int i, atomic_t *v);該函數(shù)對原子類型的變量v原子地增加I,并判斷結(jié)果是否為負數(shù),如果是,返回真,否則返回假。
  • int atomic_add_return(int i, atomic_t *v);該函數(shù)對原子類型的變量v原子地增加i,并且返回指向v的指針。
  • int atomic_sub_return(int i, atomic_t *v);該函數(shù)從原子類型的變量v中減去i,并且返回指向v的指針。
  • int atomic_inc_return(atomic_t * v);該函數(shù)對原子類型的變量v原子地增加1并且返回指向v的指針。
  • int atomic_dec_return(atomic_t * v);該函數(shù)對原子類型的變量v原子地減1并且返回指向v的指針。

原子操作通常用于實現(xiàn)資源的引用計數(shù),在TCP/IP協(xié)議棧的IP碎片處理中,就使用了引用計數(shù),碎片隊列結(jié)構(gòu)struct ipq描述了一個IP碎片,字段refcnt就是引用計數(shù)器,它的類型為atomic_t,當創(chuàng)建IP碎片時(在函數(shù)ip_frag_create中),使用atomic_set函數(shù)把它設置為1,當引用該IP碎片時,就使用函數(shù)atomic_inc把引用計數(shù)加1。

當不需要引用該IP碎片時,就使用函數(shù)ipq_put來釋放該IP碎片,ipq_put使用函數(shù)atomic_dec_and_test把引用計數(shù)減1并判斷引用計數(shù)是否為0,如果是就釋放IP碎片。函數(shù)ipq_kill把IP碎片從ipq隊列中刪除,并把該刪除的IP碎片的引用計數(shù)減1(通過使用函數(shù)atomic_dec實現(xiàn))。

四、同步機制的選擇與應用場景

在Linux內(nèi)核的實際應用中,選擇合適的同步機制至關重要,這就如同在不同的路況下選擇合適的交通工具一樣 。不同的同步機制適用于不同的場景,我們需要根據(jù)具體的需求和條件來做出決策。

自旋鎖由于其自旋等待的特性,適合用于臨界區(qū)執(zhí)行時間非常短且競爭不激烈的場景 。在多核處理器系統(tǒng)中,當線程對共享資源的訪問時間極短,如對一些硬件寄存器的快速讀寫操作,使用自旋鎖可以避免線程上下文切換的開銷,提高系統(tǒng)的響應速度 。因為線程在自旋等待時,雖然會占用 CPU 資源,但由于臨界區(qū)執(zhí)行時間短,很快就能獲取鎖并完成操作,相比于線程上下文切換的開銷,這種自旋等待的成本是可以接受的。如果臨界區(qū)執(zhí)行時間較長,線程長時間自旋會浪費大量的 CPU 資源,導致系統(tǒng)性能下降,所以自旋鎖不適合長時間持有鎖的場景 。

互斥鎖則適用于臨界區(qū)可能會阻塞很長時間的場景 。當涉及到大量的計算、IO 操作或者需要等待外部資源時,使用互斥鎖可以讓線程在獲取不到鎖時進入阻塞狀態(tài),讓出 CPU 資源給其他線程,避免 CPU 資源的浪費 。在一個網(wǎng)絡服務器中,當線程需要從網(wǎng)絡中讀取大量數(shù)據(jù)或者向數(shù)據(jù)庫寫入數(shù)據(jù)時,這些操作通常會花費較長的時間,此時使用互斥鎖來保護相關的資源,能夠有效地管理線程的執(zhí)行順序,保證系統(tǒng)的穩(wěn)定性 。因為在這種情況下,線程上下文切換的開銷相對較小,而讓線程阻塞等待可以避免 CPU 資源被無效占用,提高系統(tǒng)的整體效率 。

讀寫鎖適用于讀取頻繁而寫入較少的場景 。在一個實時監(jiān)控系統(tǒng)中,大量的線程可能需要頻繁讀取監(jiān)控數(shù)據(jù),但只有少數(shù)線程會偶爾更新這些數(shù)據(jù) 。使用讀寫鎖,多個讀線程可以同時獲取讀鎖,并發(fā)地讀取數(shù)據(jù),而寫線程在需要更新數(shù)據(jù)時,獲取寫鎖,獨占資源進行寫入操作,這樣可以大大提高系統(tǒng)的并發(fā)性能 。因為讀操作不會修改數(shù)據(jù),所以多個讀線程同時進行讀操作不會產(chǎn)生數(shù)據(jù)沖突,而寫操作則需要保證原子性和數(shù)據(jù)一致性,讀寫鎖正好滿足了這種需求 。

信號量則常用于控制對共享資源的訪問數(shù)量 。在一個文件服務器中,為了避免過多的線程同時訪問同一個文件導致文件系統(tǒng)負載過高,我們可以使用信號量來限制同時訪問文件的線程數(shù)量 。通過設置信號量的初始值為允許同時訪問的最大線程數(shù),每個線程在訪問文件前先獲取信號量,訪問完成后釋放信號量,這樣就可以有效地控制對文件資源的訪問,保證系統(tǒng)的穩(wěn)定性 。因為信號量的計數(shù)器可以精確地控制并發(fā)訪問的數(shù)量,避免資源的過度競爭和沖突 。

五、實際案例分析

5.1 TCP 連接管理

在 Linux 內(nèi)核的網(wǎng)絡協(xié)議棧中,同步機制起著關鍵的作用 。以 TCP 協(xié)議的連接管理為例,當多個線程同時處理 TCP 連接的建立、斷開和數(shù)據(jù)傳輸時,就需要使用同步機制來保證數(shù)據(jù)的一致性和操作的正確性 。在處理 TCP 連接請求時,可能會有多個線程同時接收到連接請求,這時候就需要使用自旋鎖來快速地對共享的連接隊列進行操作,確保每個連接請求都能被正確處理,避免出現(xiàn)重復處理或者數(shù)據(jù)混亂的情況 。

由于連接請求的處理通常非???,使用自旋鎖可以避免線程上下文切換的開銷,提高系統(tǒng)的性能 。而在進行 TCP 數(shù)據(jù)傳輸時,由于數(shù)據(jù)傳輸可能會受到網(wǎng)絡延遲等因素的影響,需要較長的時間,這時候就會使用互斥鎖來保護數(shù)據(jù)緩沖區(qū)等共享資源,確保數(shù)據(jù)的正確讀寫 。因為在數(shù)據(jù)傳輸過程中,線程可能需要等待網(wǎng)絡響應,使用互斥鎖可以讓線程在等待時進入阻塞狀態(tài),讓出 CPU 資源,提高系統(tǒng)的整體效率 。

我們將創(chuàng)建一個簡單的TCP連接請求處理程序,使用自旋鎖保護共享的連接隊列,代碼實現(xiàn)示例:

#include <linux/spinlock.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

#define MAX_CONNECTIONS 10

struct connection {
    int conn_id;
};

struct connection connection_queue[MAX_CONNECTIONS];
int queue_count = 0;
spinlock_t conn_lock;

void handle_connection_request(int conn_id) {
    spin_lock(&conn_lock);
    if (queue_count < MAX_CONNECTIONS) {
        connection_queue[queue_count].conn_id = conn_id;
        queue_count++;
        printk(KERN_INFO "Handled connection request: %d\n", conn_id);
    } else {
        printk(KERN_WARNING "Connection queue is full!\n");
    }
    spin_unlock(&conn_lock);
}

static int __init my_module_init(void) {
    spin_lock_init(&conn_lock);
    return 0;
}

static void __exit my_module_exit(void) {
    // Cleanup code here
}

module_init(my_module_init);
module_exit(my_module_exit);

MODULE_LICENSE("GPL");
  • 使用spinlock_t類型的自旋鎖來保護對共享資源(連接隊列)的訪問。
  • handle_connection_request函數(shù)模擬處理TCP連接請求。它在修改共享隊列之前獲取自旋鎖,并在完成后釋放。

5.2 文件讀寫操作

在文件系統(tǒng)中,同步機制也不可或缺 。以文件的讀寫操作為例,當多個進程同時對一個文件進行讀寫時,就需要使用合適的同步機制 。對于文件的讀取操作,由于讀取操作不會修改文件內(nèi)容,多個進程可以同時進行讀取,這時候可以使用讀寫鎖的讀鎖來提高并發(fā)性能 。而當有進程需要對文件進行寫入操作時,為了保證數(shù)據(jù)的一致性,就需要獲取讀寫鎖的寫鎖,獨占文件進行寫入 。在文件系統(tǒng)的元數(shù)據(jù)管理中,如文件的創(chuàng)建、刪除和目錄的遍歷等操作,由于這些操作涉及到對文件系統(tǒng)關鍵數(shù)據(jù)結(jié)構(gòu)的修改,需要保證原子性和一致性,通常會使用互斥鎖來保護相關的操作 。因為這些操作可能會涉及到復雜的文件系統(tǒng)操作和磁盤 IO,使用互斥鎖可以有效地管理線程的執(zhí)行順序,避免出現(xiàn)數(shù)據(jù)不一致的情況 。

接下來是一個簡化版的文件讀寫操作示例,使用互斥鎖和讀寫鎖來確保線程安全,代碼實現(xiàn)示例:

#include <linux/fs.h>
#include <linux/mutex.h>
#include <linux/rwsem.h>
#include <linux/uaccess.h>

struct rw_semaphore file_rwsem;
char file_buffer[1024];

void read_file(char *buffer, size_t size) {
    down_read(&file_rwsem); // 獲取讀鎖
    memcpy(buffer, file_buffer, size);
    up_read(&file_rwsem);   // 釋放讀鎖
}

void write_file(const char *buffer, size_t size) {
    down_write(&file_rwsem); // 獲取寫鎖
    memcpy(file_buffer, buffer, size);
    up_write(&file_rwsem);   // 釋放寫鎖
}

static int __init my_file_module_init(void) {
    init_rwsem(&file_rwsem);
    return 0;
}

static void __exit my_file_module_exit(void) {
    // Cleanup code here
}

module_init(my_file_module_init);
module_exit(my_file_module_exit);

MODULE_LICENSE("GPL");
  • 使用rw_semaphore類型的讀寫鎖來控制對文件緩沖區(qū)的并發(fā)訪問。
  • 在讀取時,通過調(diào)用down_read獲取讀鎖,以允許多個線程同時讀取而不阻塞;在寫入時,通過調(diào)用down_write獲取獨占寫鎖,以保證數(shù)據(jù)一致性。

通過這以上兩個簡單示例,可以看到在Linux內(nèi)核中如何應用不同的同步機制來管理資源競爭,以提高性能和數(shù)據(jù)一致性。

責任編輯:武曉燕 來源: 深度Linux
相關推薦

2024-07-25 11:53:53

2025-02-26 09:55:59

Linux內(nèi)核并發(fā)

2016-09-20 15:21:35

LinuxInnoDBMysql

2012-07-27 10:02:39

C#

2024-02-02 18:29:54

C++線程編程

2024-07-05 08:32:36

2019-05-27 14:40:43

Java同步機制多線程編程

2024-06-28 08:45:58

2023-11-22 13:18:02

Linux調(diào)度

2019-11-22 18:52:31

進程同步機制編程語言

2012-07-09 09:25:13

ibmdw

2017-12-15 10:20:56

MySQLInnoDB同步機制

2011-11-23 10:09:19

Java線程機制

2009-08-12 13:37:01

Java synchr

2010-03-15 16:31:34

Java多線程

2024-07-08 12:51:05

2023-09-26 10:30:57

Linux編程

2021-10-08 20:30:12

ZooKeeper選舉機制

2023-11-24 11:15:21

協(xié)程編程

2024-01-22 09:00:00

編程C++代碼
點贊
收藏

51CTO技術棧公眾號