Linux內核同步機制:解鎖并發編程的奧秘
在當今的數字時代,多核處理器早已成為計算機系統的標配,從我們日常辦公的電腦,到數據中心里龐大的服務器集群,它們無處不在。這一硬件層面的發展,使得計算機系統能夠同時處理多個任務,極大地提升了計算效率。就如同繁忙的交通樞紐,多車道并行,車輛往來穿梭,看似混亂卻又有序運
然而,在 Linux 內核這個 “交通指揮中心” 里,當多個進程或線程如同川流不息的車輛,試圖同時訪問共享資源時,問題就出現了。想象一下,兩條道路上的車輛都想同時通過一個狹窄的路口,如果沒有合理的交通規則,必然會導致擁堵甚至碰撞。在 Linux 內核中,這些共享資源就如同這個狹窄路口,而進程和線程的并發訪問如果缺乏有效的管理,就會引發數據不一致、程序崩潰等嚴重問題。這不僅會影響系統的穩定性,還可能導致關鍵業務的中斷,造成不可估量的損失。
那么,Linux 內核是如何在復雜的并發環境中,確保共享資源的安全訪問,維持系統的高效穩定運行的呢?答案就在于其精心設計的同步機制。它就像一套精密的交通指揮系統,通過各種規則和信號,引導著進程和線程這些 “車輛” 有序地通過共享資源這個 “路口”。接下來,就讓我們一同深入 Linux 內核同步機制的世界,探尋其中的奧秘,解鎖并發編程的關鍵技巧,為構建更穩定、高效的系統奠定堅實基礎。
常用的 Linux 內核同步機制有原子操作、Per-CPU 變量、內存屏障、自旋鎖、Mutex 鎖、信號量和 RCU 等,后面幾種鎖實現會依賴于前三種基礎同步機制。在正式開始分析具體的內核同步機制實現之前,需要先澄清一些基本概念。
一、基本概念
1.1 同步機制
既然是同步機制,那就首先要搞明白什么是同步。同步是指用于實現控制多個執行路徑按照一定的規則或順序訪問某些系統資源的機制。所謂執行路徑,就是在 CPU 上運行的代碼流。我們知道,CPU 調度的最小單位是線程,可以是用戶態線程,也可以是內核線程,甚至是中斷服務程序。所以,執行路徑在這里就包括用戶態線程、內核線程和中斷服務程序。執行路徑、執行單元、控制路徑等等,叫法不同,但本質都一樣。那為什么需要同步機制呢?請繼續往下看。
1.2 并發與競態
并發是指兩個以上的執行路徑同時被執行,而并發的執行路徑對共享資源(硬件資源和軟件上的全局變量等)的訪問則很容易導致競態。例如,現在系統有一個 LED 燈可以由 APP 控制,APP1 控制燈亮一秒滅一秒,APP2 控制燈亮 500ms 滅 1500ms。如果 APP1 和 APP2 分別在 CPU1 和 CPU2 上并發運行,LED 燈的行為會是什么樣的呢?很有可能 LED 燈的亮滅節奏都不會如這兩個 APP 所愿,APP1 在關掉 LED 燈時,很有可能恰逢 APP2 正要打開 LED 燈。很明顯,APP1 和 APP2 對 LED 燈這個資源產生了競爭關系。競態是危險的,如果不加以約束,輕則只是程序運行結果不符合預期,重則系統崩潰。
在操作系統中,更復雜、更混亂的并發大量存在,而同步機制正是為了解決并發和競態問題。同步機制通過保護臨界區(訪問共享資源的代碼區域)達到對共享資源互斥訪問的目的,所謂互斥訪問,是指一個執行路徑在訪問共享資源時,另一個執行路徑被禁止去訪問。關于并發與競態,有個生活例子很貼切。假如你和你的同事張小三都要上廁所,但是公司只有一個洗手間而且也只有一個坑。當張小三進入廁所關起門的那一刻起,你就無法進去了,只能在門外侯著。
當小三哥出來后你才能進去解決你的問題。這里,公司廁所就是共享資源,你和張小三同時需要這個共享資源就是并發,你們對廁所的使用需求就構成了競態,而廁所的門就是一種同步機制,他在用你就不能用了
總結如下圖:
圖片
1.3 中斷與搶占
中斷本身的概念很簡單,本文不予解釋。當然,這并不是說 Linux 內核的中斷部分也很簡單。事實上,Linux 內核的中斷子系統也相當復雜,因為中斷對于操作系統來說實在是太重要了。以后有機會,筆者計劃開專題再來介紹。對于同步機制的代碼分析來說,了解中斷的概念即可,不需要深入分析內核的具體代碼實現。
搶占屬于進程調度的概念,Linux 內核從 2.6 版本開始支持搶占調度。進程調度(管理)是 Linux 內核最核心的子系統之一,異常龐大,本文只簡單介紹基本概念,對于同步機制的代碼分析已然足夠。通俗地說,搶占是指一個正愉快地運行在 CPU 上的 task(可以是用戶態進程,也可以是內核線程) 被另一個 task(通常是更高優先級)奪去 CPU 執行權的故事。
中斷和搶占之間有著比較曖昧的關系,簡單來說,搶占依賴中斷。如果當前 CPU 禁止了本地中斷,那么也意味著禁止了本 CPU 上的搶占。但反過來,禁掉搶占并不影響中斷。Linux 內核中用 preempt_enable() 宏函數來開啟本 CPU 的搶占,用 preempt_disable() 來禁掉本 CPU 的搶占。
這里,“本 CPU” 這個描述其實不太準確,更嚴謹的說法是運行在當前 CPU 上的 task。preempt_enable() 和 preempt_disable() 的具體實現展開來介紹的話也可以單獨成文了,筆者沒有深究過,就不班門弄斧了,感興趣的讀者可以去 RTFSC。不管是用戶態搶占還是內核態搶占,并不是什么代碼位置都能發生,而是有搶占時機的,也就是所謂的搶占點。搶占時機如下:
用戶態搶占
1、從系統調用返回用戶空間時;
2、從中斷(異常)處理程序返回用戶空間時。
內核態搶占:
1、當一個中斷處理程序退出,返回到內核態時;
2、task 顯式調用 schedule();
3、task 發生阻塞(此時由調度器完成調度)。
1.4 編譯亂序與編譯屏障
編譯器(compiler)的工作就是優化我們的代碼以提高性能。這包括在不改變程序行為的情況下重新排列指令。因為 compiler 不知道什么樣的代碼需要線程安全(thread-safe),所以 compiler 假設我們的代碼都是單線程執行(single-threaded),并且進行指令重排優化并保證是單線程安全的。因此,當你不需要 compiler 重新排序指令的時候,你需要顯式告訴 compiler,我不需要重排。否則,它可不會聽你的。本篇文章中,我們一起探究 compiler 關于指令重排的優化規則。
注:測試使用 aarch64-linux-gnu-gcc 版本:7.3.0
編譯器指令重排(Compiler Instruction Reordering)
compiler 的主要工作就是將對人們可讀的源碼轉化成機器語言,機器語言就是對 CPU 可讀的代碼。因此,compiler 可以在背后做些不為人知的事情。我們考慮下面的 C語言代碼:
int a, b;
void foo(void)
{
a = b + 1;
b = 0;
}使用 aarch64-linux-gnu-gcc 在不優化代碼的情況下編譯上述代碼,使用 objdump 工具查看 foo() 反匯編結果
<foo>:
...
ldr w0, [x0] //load b to w0
add w1, w0, #0x1
...
str w1, [x0] //a = b + 1
...
str wzr, [x0] //b = 0我們應該知道 Linux 默認編譯優化選項是 -O2,因此我們采用 -O2 優化選項編譯上述代碼,并反匯編得到如下匯編結果:
<foo>:
...
ldr w2, [x0] //load b to w2
str wzr, [x0] //b = 0
add w0, w2, #0x1
str w0, [x1] //a = b + 1比較優化和不優化的結果,我們可以發現:在不優化的情況下,a 和 b 的寫入內存順序符合代碼順序(program order);但是 -O2 優化后,a 和 b 的寫入順序和 program order 是相反的。-O2 優化后的代碼轉換成 C 語言可以看作如下形式:
int a, b;
void foo(void)
{
register int reg = b;
b = 0;
a = reg + 1;
}這就是 compiler reordering(編譯器重排)。為什么可以這么做呢?對于單線程來說,a 和 b 的寫入順序,compiler 認為沒有任何問題。并且最終的結果也是正確的(a == 1 && b == 0)。這種 compiler reordering 在大部分情況下是沒有問題的。但是在某些情況下可能會引入問題。例如我們使用一個全局變量 flag 標記共享數據 data 是否就緒。由于 compiler reordering,可能會引入問題。考慮下面的代碼(無鎖編程):
int flag, data;
void write_data(int value)
{
data = value;
flag = 1;
}如果 compiler 產生的匯編代碼是 flag 比 data 先寫入內存,那么,即使是單核系統上,我們也會有問題。在 flag 置 1 之后,data 寫 45 之前,系統發生搶占。另一個進程發現 flag 已經置 1,認為 data 的數據已經準備就緒。但是實際上讀取 data 的值并不是 45。為什么 compiler 還會這么操作呢?因為,compiler 并不知道 data 和 flag 之間有嚴格的依賴關系。這種邏輯關系是我們人為強加的。我們如何避免這種優化呢?
顯式編譯器屏障(Explicit Compiler Barriers)
為了解決上述變量之間存在依賴關系導致 compiler 錯誤優化。compiler 為我們提供了編譯器屏障(compiler barriers),可用來告訴 compiler 不要 reorder。我們繼續使用上面的 foo() 函數作為演示實驗,在代碼之間插入 compiler barriers。
#define barrier() __asm__ __volatile__("": : :"memory")
int a, b;
void foo(void)
{
a = b + 1;
barrier();
b = 0;
}barrier() 就是 compiler 提供的屏障,作用是告訴 compiler 內存中的值已經改變,之前對內存的緩存(緩存到寄存器)都需要拋棄,barrier() 之后的內存操作需要重新從內存 load,而不能使用之前寄存器緩存的值。并且可以防止 compiler 優化 barrier() 前后的內存訪問順序。barrier() 就像是代碼中的一道不可逾越的屏障,barrier() 前的 load/store 操作不能跑到 barrier() 后面;同樣,barrier() 后面的 load/store 操作不能在 barrier() 之前。依然使用 -O2 優化選項編譯上述代碼,反匯編得到如下結果:
<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 一致。因此,當我們的代碼中需要嚴格的內存順序,就需要考慮 compiler barriers。
隱式編譯器屏障(Implied Compiler Barriers)
除了顯示的插入 compiler barriers 之外,還有別的方法阻止 compiler reordering。例如 CPU barriers 指令,同樣會阻止 compiler reordering。后續我們再考慮 CPU barriers。除此以外,當某個函數內部包含 compiler barriers 時,該函數也會充當 compiler barriers 的作用。即使這個函數被 inline,也是這樣。例如上面插入 barrier() 的 foo() 函數,當其他函數調用 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() 函數包含 barrier(),因此 foo() 函數中 fun() 調用也表現出 compiler barriers 的作用,同樣可以保證 a 和 b 的寫入順序。如果 fun() 函數不包含 barrier(),結果又會怎么樣呢?實際上,大多數的函數調用都表現出 compiler barriers 的作用。但是,這不包含 inline 的函數。因此,fun() 如果被 inline 進 foo(),那么 fun() 就不具有 compiler barriers 的作用。
如果被調用的函數是一個外部函數,其副作用會比 compiler barriers 還要強。因為 compiler 不知道函數的副作用是什么。它必須忘記它對內存所作的任何假設,即使這些假設對該函數可能是可見的。我們看一下下面的代碼片段,printf() 一定是一個外部的函數。
int a, b;
void foo(void)
{
a = 5;
printf("smcdef");
b = a;
}同樣使用 -O2 優化選項編譯代碼,objdump 反匯編得到如下結果:
<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 = acompiler 不能假設 printf() 不會使用或者修改 a 變量。因此在調用 printf() 之前會將 a 寫 5,以保證 printf() 可能會用到新值。在 printf() 調用之后,重新從內存中 load a 的值,然后賦值給變量 b。重新 load a 的原因是 compiler 也不知道 printf() 會不會修改 a 的值。
因此,我們可以看到即使存在 compiler reordering,但是還是有很多限制。當我們需要考慮 compiler barriers 時,一定要顯示的插入 barrier(),而不是依靠函數調用附加的隱式 compiler barriers。因為,誰也無法保證調用的函數不會被 compiler 優化成 inline 方式。
barrier() 除了防止編譯亂序,還能做什么。
barriers() 作用除了防止 compiler reordering 之外,還有什么妙用嗎?我們考慮下面的代碼片段:
int run = 1;
void foo(void)
{
while (run)
;
}run 是個全局變量,foo() 在一個進程中執行,一直循環。我們期望的結果是 foo() 一直等到其他進程修改 run 的值為 0 才退出循環。實際 compiler 編譯的代碼和我們會達到我們預期的結果嗎?我們看一下匯編代碼:
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匯編代碼可以轉換成如下的 C 語言形式:
int run = 1;
void foo(void)
{
register int reg = run;
if (reg)
while (1)
;
}compiler 首先將 run 加載到一個寄存器 reg 中,然后判斷 reg 是否滿足循環條件,如果滿足就一直循環。但是循環過程中,寄存器 reg 的值并沒有變化。因此,即使其他進程修改 run 的值為 0,也不能使 foo() 退出循環。很明顯,這不是我們想要的結果。我們繼續看一下加入 barrier() 后的結果:
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() 后的結果真是我們想要的。每一次循環都會從內存中重新 load run 的值。因此,當有其他進程修改 run 的值為 0 的時候,foo() 可以正常退出循環。為什么加入 barrier() 后的匯編代碼就是正確的呢?因為 barrier() 作用是告訴 compiler 內存中的值已經變化,后面的操作都需要重新從內存 load,而不能使用寄存器緩存的值。因此,這里的 run 變量會從內存重新 load,然后判斷循環條件。這樣,其他進程修改 run 變量,foo() 就可以看得見了。
在 Linux kernel 中,提供了 cpu_relax() 函數,該函數在 ARM64 平臺定義如下:
static inline void cpu_relax(void)
{
asm volatile("yield" ::: "memory");
}我們可以看出,cpu_relax() 是在 barrier() 的基礎上又插入一條匯編指令 yield。在 kernel 中,我們經常會看到一些類似上面舉例的 while 循環,循環條件是個全局變量。為了避免上述所說問題,我們就會在循環中插入 cpu_relax() 調用。
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 內核同步機制之前,我們先來理解一下并發(Concurrency)與競態(Race Condition)的概念,因為它們是同步機制存在的根本原因。
2.1 并發的多種形式
并發,簡單來說,就是指多個執行單元同時、并行地被執行 。在 Linux 系統中,并發主要有以下幾種場景:
SMP 多 CPU:對稱多處理器(SMP)是一種緊耦合、共享存儲的系統模型,多個 CPU 使用共同的系統總線,可以訪問共同的外設和存儲器 。在這種情況下,兩個 CPU 之間的進程、中斷都有并發的可能性。例如,CPU0 上的進程 A 和 CPU1 上的進程 B 可能同時訪問共享內存中的同一數據。
單 CPU 內進程與搶占進程:在單個 CPU 中,雖然同一時刻只能有一個進程在運行,但進程的執行可能會被打斷。比如,一個進程在執行過程中,可能會因為時間片耗盡,或者被另一個高優先級的進程搶占。當高優先級進程與被打斷的進程共同訪問共享資源時,就可能產生競態。比如進程 A 正在訪問一個全局變量,還沒來得及修改完,就被進程 B 搶占,進程 B 也對這個全局變量進行訪問和修改,就可能導致數據混亂。
中斷與進程:中斷可以打斷正在執行的進程 。如果中斷服務程序也訪問進程正在訪問的共享資源,就很容易產生競態。比如,進程正在向串口發送數據,這時一個中斷發生,中斷服務程序也嘗試向串口發送數據,就會導致串口數據發送錯誤。
2.2 競態帶來的問題
當多個并發執行單元訪問共享資源時,競態就可能出現。競態會導致程序出現不可預測的行為,比如數據不一致、程序崩潰等 。我們來看一個簡單的例子,假設有兩個進程 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
}如果這兩個進程并發執行,正常情況下,count 最終的值應該是 2。但由于競態的存在,可能會出現以下情況:
- 進程 P1 讀取 count 的值,此時 temp 為 0。
- 進程 P2 讀取 count 的值,此時 temp 也為 0,因為 P1 還沒有將修改后的值寫回 count。
- 進程 P1 對 temp加 1,然后將 temp 的值寫回 count,此時 count 為 1。
- 進程P2對temp加1(此時 temp 還是 0),然后將temp的值寫回 count,此時 count 還是 1,而不是 2。
這就是競態導致的數據錯誤。在實際的 Linux 內核中,共享資源可能是硬件設備、全局變量、文件系統等,競態帶來的問題會更加復雜和嚴重,可能導致系統不穩定、數據丟失等問題 。因此,為了保證系統的正確性和穩定性,Linux 內核需要一套有效的同步機制來解決競態問題。
三、常見同步機制解析
為了解決并發與競態問題,Linux 內核提供了多種同步機制 ,每種機制都有其獨特的工作原理和適用場景。下面我們來詳細了解一下這些同步機制。
3.1 自旋鎖(Spinlocks)
自旋鎖是一種比較簡單的同步機制 。當一個線程嘗試獲取自旋鎖時,如果鎖已經被其他線程持有,那么該線程不會進入阻塞狀態,而是在原地不斷地循環檢查鎖是否可用,這個過程就叫做 “自旋” 。就好像你去餐廳吃飯,發現你喜歡的那桌還被別人占著,你又特別想坐那桌,于是你就站在旁邊一直盯著,等那桌人吃完離開,你馬上就能坐過去,這個一直盯著等待的過程就類似自旋。
自旋鎖適用于鎖持有時間非常短的場景 ,因為它避免了線程上下文切換的開銷。在多處理器系統中,當一個線程在自旋等待鎖時,其他處理器核心可以繼續執行其他任務,不會因為線程阻塞而導致 CPU 資源浪費 。比如在一些對共享硬件資源的短時間訪問場景中,自旋鎖就非常適用。假設多個線程需要訪問共享的網卡設備寄存器,對寄存器的操作通常非常快,使用自旋鎖可以讓線程快速獲取鎖并完成操作,避免了線程上下文切換帶來的開銷。
自旋鎖也有其局限性。如果鎖持有時間較長,線程會一直自旋,不斷消耗 CPU 資源,導致系統性能下降 。所以在使用自旋鎖時,需要根據實際情況謹慎選擇。
自旋鎖的API有:
- spin_lock_init(x)該宏用于初始化自旋鎖x。自旋鎖在真正使用前必須先初始化。該宏用于動態初始化。
- DEFINE_SPINLOCK(x)該宏聲明一個自旋鎖x并初始化它。該宏在2.6.11中第一次被定義,在先前的內核中并沒有該宏。
- SPIN_LOCK_UNLOCKED該宏用于靜態初始化一個自旋鎖。
- DEFINE_SPINLOCK(x)等同于spinlock_t x = SPIN_LOCK_UNLOCKEDspin_is_locked(x)該宏用于判斷自旋鎖x是否已經被某執行單元保持(即被鎖),如果是,返回真,否則返回假。
- spin_unlock_wait(x)該宏用于等待自旋鎖x變得沒有被任何執行單元保持,如果沒有任何執行單元保持該自旋鎖,該宏立即返回,否則將循環在那里,直到該自旋鎖被保持者釋放。
- 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中第一次被定義,在先前的內核中并沒有該宏。
獲得自旋鎖和釋放自旋鎖有好幾個版本,因此讓讀者知道在什么樣的情況下使用什么版本的獲得和釋放鎖的宏是非常必要的。
如果被保護的共享資源只在進程上下文訪問和軟中斷上下文訪問,那么當在進程上下文訪問共享資源時,可能被軟中斷打斷,從而可能進入軟中斷上下文來對被保護的共享資源訪問,因此對于這種情況,對共享資源的訪問必須使用spin_lock_bh和spin_unlock_bh來保護。
當然使用spin_lock_irq和spin_unlock_irq以及spin_lock_irqsave和spin_unlock_irqrestore也可以,它們失效了本地硬中斷,失效硬中斷隱式地也失效了軟中斷。但是使用spin_lock_bh和spin_unlock_bh是最恰當的,它比其他兩個快。
如果被保護的共享資源只在進程上下文和tasklet或timer上下文訪問,那么應該使用與上面情況相同的獲得和釋放鎖的宏,因為tasklet和timer是用軟中斷實現的。
如果被保護的共享資源只在一個tasklet或timer上下文訪問,那么不需要任何自旋鎖保護,因為同一個tasklet或timer只能在一個CPU上運行,即使是在SMP環境下也是如此。實際上tasklet在調用tasklet_schedule標記其需要被調度時已經把該tasklet綁定到當前CPU,因此同一個tasklet決不可能同時在其他CPU上運行。timer也是在其被使用add_timer添加到timer隊列中時已經被幫定到當前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來保護對共享資源的訪問就可以了。
因為在執行中斷處理句柄期間,不可能被同一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上的執行單元對共享資源的同時訪問以及不同進程上下文互相搶占導致的對共享資源的非同步訪問,而中斷失效和軟中斷失效卻是為了阻止在同一CPU上軟中斷或中斷對共享資源的非同步訪問。
3.2 互斥鎖(Mutexes)
互斥鎖,也叫互斥量 ,是一種用于實現線程間互斥訪問的同步機制 。它的工作原理是,當一個線程獲取到互斥鎖后,其他線程如果也嘗試獲取該鎖,就會被阻塞,直到持有鎖的線程釋放鎖 。這就好比一個公共衛生間,一次只能允許一個人使用,當有人進入衛生間并鎖上門后,其他人就只能在外面排隊等待,直到里面的人出來打開門,外面的人才有機會進去使用。
與自旋鎖不同,互斥鎖適用于那些可能會阻塞很長時間的場景 。當線程獲取不到鎖時,它會被操作系統掛起,讓出 CPU 資源,不會像自旋鎖那樣一直占用 CPU 進行無效的等待 。在涉及大量計算或者 IO 操作的代碼段中,使用互斥鎖可以避免 CPU 資源的浪費。比如在數據庫操作中,一個線程需要長時間占用數據庫連接執行復雜的查詢或者事務操作,這時使用互斥鎖來保護數據庫連接資源,其他線程在獲取不到鎖時會被阻塞,直到當前線程完成數據庫操作并釋放鎖,這樣可以有效地管理資源,提高系統的整體性能。
3.3 讀寫鎖(Read-Write Locks)
讀寫鎖是一種特殊的同步機制,它允許多個線程同時進行讀操作,但只允許一個線程進行寫操作 。當有線程正在進行寫操作時,其他線程無論是讀操作還是寫操作都將被阻塞,直到寫操作完成并釋放鎖 。這就像圖書館的一本熱門書籍,很多人可以同時閱讀這本書,但如果有人要對這本書進行修改(比如添加批注或者修正錯誤),就必須先獨占這本書,其他人在修改期間不能閱讀也不能修改,直到修改完成。
讀寫鎖的優勢在于它能顯著提高并發性能,特別是在讀取頻繁而寫入較少的場景中 。在一個在線商城系統中,商品信息的展示(讀操作)非常頻繁,而商品信息的更新(寫操作)相對較少。使用讀寫鎖,多個用戶可以同時讀取商品信息,而當商家需要更新商品信息時,只需要獲取寫鎖,保證寫操作的原子性和數據一致性,這樣可以大大提高系統的并發處理能力,提升用戶體驗。
讀寫信號量的相關API有:
- DECLARE_RWSEM(name)該宏聲明一個讀寫信號量name并對其進行初始化。
- void init_rwsem(struct rw_semaphore *sem);該函數對讀寫信號量sem進行初始化。
- void down_read(struct rw_semaphore *sem);讀者調用該函數來得到讀寫信號量sem。該函數會導致調用者睡眠,因此只能在進程上下文使用。
- int down_read_trylock(struct rw_semaphore *sem);該函數類似于down_read,只是它不會導致調用者睡眠。它盡力得到讀寫信號量sem,如果能夠立即得到,它就得到該讀寫信號量,并且返回1,否則表示不能立刻得到該信號量,返回0。因此,它也可以在中斷上下文使用。
- void down_write(struct rw_semaphore *sem);寫者使用該函數來得到讀寫信號量sem,它也會導致調用者睡眠,因此只能在進程上下文使用。
- int down_write_trylock(struct rw_semaphore *sem);該函數類似于down_write,只是它不會導致調用者睡眠。該函數盡力得到讀寫信號量,如果能夠立刻獲得,就獲得該讀寫信號量并且返回1,否則表示無法立刻獲得,返回0。它可以在中斷上下文使用。
- void up_read(struct rw_semaphore *sem);讀者使用該函數釋放讀寫信號量sem。它與down_read或down_read_trylock配對使用。如果down_read_trylock返回0,不需要調用up_read來釋放讀寫信號量,因為根本就沒有獲得信號量。
- void up_write(struct rw_semaphore *sem);寫者調用該函數釋放信號量sem。它與down_write或down_write_trylock配對使用。如果down_write_trylock返回0,不需要調用up_write,因為返回0表示沒有獲得該讀寫信號量。
- void downgrade_write(struct rw_semaphore *sem);該函數用于把寫者降級為讀者,這有時是必要的。因為寫者是排他性的,因此在寫者保持讀寫信號量期間,任何讀者或寫者都將無法訪問該讀寫信號量保護的共享資源,對于那些當前條件下不需要寫訪問的寫者,降級為讀者將,使得等待訪問的讀者能夠立刻訪問,從而增加了并發性,提高了效率。對于那些當前條件下不需要寫訪問的寫者,降級為讀者將,使得等待訪問的讀者能夠立刻訪問,從而增加了并發性,提高了效率。讀寫信號量適于在讀多寫少的情況下使用,在linux內核中對進程的內存映像描述結構的訪問就使用了讀寫信號量進行保護。
在Linux中,每一個進程都用一個類型為task_t或struct task_struct的結構來描述,該結構的類型為struct mm_struct的字段mm描述了進程的內存映像,特別是mm_struct結構的mmap字段維護了整個進程的內存塊列表,該列表將在進程生存期間被大量地遍利或修改。結構的mmap字段維護了整個進程的內存塊列表,該列表將在進程生存期間被大量地遍利或修改。
因此mm_struct結構就有一個字段mmap_sem來對mmap的訪問進行保護,mmap_sem就是一個讀寫信號量,在proc文件系統里有很多進程內存使用情況的接口,通過它們能夠查看某一進程的內存使用情況,命令free、ps和top都是通過proc來得到內存使用信息的,proc接口就使用down_read和up_read來讀取進程的mmap信息。
當進程動態地分配或釋放內存時,需要修改mmap來反映分配或釋放后的內存映像,因此動態內存分配或釋放操作需要以寫者身份獲得讀寫信號量mmap_sem來對mmap進行更新。系統調用brk和munmap就使用了down_write和up_write來保護對mmap的訪問。
3.4 信號量(Semaphores)
信號量是一個整數值,它可以用來控制對共享資源的訪問 。信號量主要有兩個作用:一是實現互斥,二是控制并發訪問的數量 。信號量內部維護一個計數器,當線程請求訪問共享資源時,會嘗試獲取信號量,如果計數器大于 0,則線程可以獲取信號量并繼續執行,同時計數器減一;如果計數器為 0,則線程會被阻塞,直到有其他線程釋放信號量,使得計數器增加 。這就像一個停車場,停車場有一定數量的停車位(信號量的初始值),每輛車進入停車場(線程請求資源)時,會占用一個停車位,停車位數量減一,如果停車位滿了(計數器為 0),新的車輛就只能在外面等待,直到有車輛離開停車場(線程釋放資源),停車位數量增加,等待的車輛才有機會進入。
在限制線程訪問文件資源數量的場景中,信號量就非常有用 。假設一個系統中,同時只允許5個線程對某個文件進行讀寫操作,我們可以創建一個初始值為5的信號量 。每個線程在訪問文件前,先獲取信號量,如果獲取成功則可以訪問文件,同時信號量的計數器減一;當線程完成文件訪問后,釋放信號量,計數器加一。這樣就可以有效地控制同時訪問文件的線程數量,避免資源的過度競爭和沖突 。
信號量的API有:
- DECLARE_MUTEX(name)該宏聲明一個信號量name并初始化它的值為0,即聲明一個互斥鎖。
- DECLARE_MUTEX_LOCKED(name)該宏聲明一個互斥鎖name,但把它的初始值設置為0,即鎖在創建時就處在已鎖狀態。因此對于這種鎖,一般是先釋放后獲得。
- void sema_init (struct semaphore *sem, int val);該函用于數初始化設置信號量的初值,它設置信號量sem的值為val。
- void init_MUTEX (struct semaphore *sem);該函數用于初始化一個互斥鎖,即它把信號量sem的值設置為1。
- void init_MUTEX_LOCKED (struct semaphore *sem);該函數也用于初始化一個互斥鎖,但它把信號量sem的值設置為0,即一開始就處在已鎖狀態。
- void down(struct semaphore * sem);該函數用于獲得信號量sem,它會導致睡眠,因此不能在中斷上下文(包括IRQ上下文和softirq上下文)使用該函數。該函數將把sem的值減1,如果信號量sem的值非負,就直接返回,否則調用者將被掛起,直到別的任務釋放該信號量才能繼續運行。
- int down_interruptible(struct semaphore * sem);該函數功能與down類似,不同之處為,down不會被信號(signal)打斷,但down_interruptible能被信號打斷,因此該函數有返回值來區分是正常返回還是被信號中斷,如果返回0,表示獲得信號量正常返回,如果被信號打斷,返回-EINTR。
- int down_trylock(struct semaphore * sem);該函數試著獲得信號量sem,如果能夠立刻獲得,它就獲得該信號量并返回0,否則,表示不能獲得信號量sem,返回值為非0值。因此,它不會導致調用者睡眠,可以在中斷上下文使用。
- void up(struct semaphore * sem);該函數釋放信號量sem,即把sem的值加1,如果sem的值為非正數,表明有任務等待該信號量,因此喚醒這些等待者。
信號量在絕大部分情況下作為互斥鎖使用,下面以console驅動系統為例說明信號量的使用。
在內核源碼樹的kernel/printk.c中,使用宏DECLARE_MUTEX聲明了一個互斥鎖console_sem,它用于保護console驅動列表console_drivers以及同步對整個console驅動系統的訪問。
其中定義了函數acquire_console_sem來獲得互斥鎖console_sem,定義了release_console_sem來釋放互斥鎖console_sem,定義了函數try_acquire_console_sem來盡力得到互斥鎖console_sem。這三個函數實際上是分別對函數down,up和down_trylock的簡單包裝。
需要訪問console_drivers驅動列表時就需要使用acquire_console_sem來保護console_drivers列表,當訪問完該列表后,就調用release_console_sem釋放信號量console_sem。
函數console_unblank,console_device,console_stop,console_start,register_console和unregister_console都需要訪問console_drivers,因此它們都使用函數對acquire_console_sem和release_console_sem來對console_drivers進行保護。
3.5 原子操作(Atomic Operations)
原子操作是指那些不可被中斷的操作 ,即它們的執行是一個完整的、不可分割的單元,不會被其他任務或事件打斷 。在多線程編程中,原子操作可以保證對共享資源的訪問是線程安全的,避免了競態條件的發生 。例如,在實現資源計數和引用計數方面,原子操作就發揮著重要作用 。
假設有一個共享資源,多個線程可能會對其引用計數進行增加或減少操作,如果這些操作不是原子的,就可能會出現競態條件,導致引用計數錯誤。而使用原子操作,就可以確保每次對引用計數的修改都是原子的,不會受到其他線程的干擾,從而保證了資源計數的準確性和一致性 。在 C 語言中,可以使用atomic庫來實現原子操作 ,比如atomic_fetch_add函數可以原子地對一個變量進行加法操作 。原子類型定義如下:
typedef struct {
volatile int counter;
} atomic_t;volatile修飾字段告訴gcc不要對該類型的數據做優化處理,對它的訪問都是對內存的訪問,而不是對寄存器的訪問。原子操作API包括:
- tomic_read(atomic_t * v);該函數對原子類型的變量進行原子讀操作,它返回原子類型的變量v的值。
- atomic_set(atomic_t * v, int i);該函數設置原子類型的變量v的值為i。
- void atomic_add(int i, atomic_t *v);該函數給原子類型的變量v增加值i。
- atomic_sub(int i, atomic_t *v);該函數從原子類型的變量v中減去i。
- int atomic_sub_and_test(int i, atomic_t *v);該函數從原子類型的變量v中減去i,并判斷結果是否為0,如果為0,返回真,否則返回假。
- void atomic_inc(atomic_t *v);該函數對原子類型變量v原子地增加1。
- void atomic_dec(atomic_t *v);該函數對原子類型的變量v原子地減1。
- int atomic_dec_and_test(atomic_t *v);該函數對原子類型的變量v原子地減1,并判斷結果是否為0,如果為0,返回真,否則返回假。
- int atomic_inc_and_test(atomic_t *v);該函數對原子類型的變量v原子地增加1,并判斷結果是否為0,如果為0,返回真,否則返回假。
- int atomic_add_negative(int i, atomic_t *v);該函數對原子類型的變量v原子地增加I,并判斷結果是否為負數,如果是,返回真,否則返回假。
- int atomic_add_return(int i, atomic_t *v);該函數對原子類型的變量v原子地增加i,并且返回指向v的指針。
- int atomic_sub_return(int i, atomic_t *v);該函數從原子類型的變量v中減去i,并且返回指向v的指針。
- int atomic_inc_return(atomic_t * v);該函數對原子類型的變量v原子地增加1并且返回指向v的指針。
- int atomic_dec_return(atomic_t * v);該函數對原子類型的變量v原子地減1并且返回指向v的指針。
原子操作通常用于實現資源的引用計數,在TCP/IP協議棧的IP碎片處理中,就使用了引用計數,碎片隊列結構struct ipq描述了一個IP碎片,字段refcnt就是引用計數器,它的類型為atomic_t,當創建IP碎片時(在函數ip_frag_create中),使用atomic_set函數把它設置為1,當引用該IP碎片時,就使用函數atomic_inc把引用計數加1。
當不需要引用該IP碎片時,就使用函數ipq_put來釋放該IP碎片,ipq_put使用函數atomic_dec_and_test把引用計數減1并判斷引用計數是否為0,如果是就釋放IP碎片。函數ipq_kill把IP碎片從ipq隊列中刪除,并把該刪除的IP碎片的引用計數減1(通過使用函數atomic_dec實現)。
四、同步機制的選擇與應用場景
在Linux內核的實際應用中,選擇合適的同步機制至關重要,這就如同在不同的路況下選擇合適的交通工具一樣 。不同的同步機制適用于不同的場景,我們需要根據具體的需求和條件來做出決策。
自旋鎖由于其自旋等待的特性,適合用于臨界區執行時間非常短且競爭不激烈的場景 。在多核處理器系統中,當線程對共享資源的訪問時間極短,如對一些硬件寄存器的快速讀寫操作,使用自旋鎖可以避免線程上下文切換的開銷,提高系統的響應速度 。因為線程在自旋等待時,雖然會占用 CPU 資源,但由于臨界區執行時間短,很快就能獲取鎖并完成操作,相比于線程上下文切換的開銷,這種自旋等待的成本是可以接受的。如果臨界區執行時間較長,線程長時間自旋會浪費大量的 CPU 資源,導致系統性能下降,所以自旋鎖不適合長時間持有鎖的場景 。
互斥鎖則適用于臨界區可能會阻塞很長時間的場景 。當涉及到大量的計算、IO 操作或者需要等待外部資源時,使用互斥鎖可以讓線程在獲取不到鎖時進入阻塞狀態,讓出 CPU 資源給其他線程,避免 CPU 資源的浪費 。在一個網絡服務器中,當線程需要從網絡中讀取大量數據或者向數據庫寫入數據時,這些操作通常會花費較長的時間,此時使用互斥鎖來保護相關的資源,能夠有效地管理線程的執行順序,保證系統的穩定性 。因為在這種情況下,線程上下文切換的開銷相對較小,而讓線程阻塞等待可以避免 CPU 資源被無效占用,提高系統的整體效率 。
讀寫鎖適用于讀取頻繁而寫入較少的場景 。在一個實時監控系統中,大量的線程可能需要頻繁讀取監控數據,但只有少數線程會偶爾更新這些數據 。使用讀寫鎖,多個讀線程可以同時獲取讀鎖,并發地讀取數據,而寫線程在需要更新數據時,獲取寫鎖,獨占資源進行寫入操作,這樣可以大大提高系統的并發性能 。因為讀操作不會修改數據,所以多個讀線程同時進行讀操作不會產生數據沖突,而寫操作則需要保證原子性和數據一致性,讀寫鎖正好滿足了這種需求 。
信號量則常用于控制對共享資源的訪問數量 。在一個文件服務器中,為了避免過多的線程同時訪問同一個文件導致文件系統負載過高,我們可以使用信號量來限制同時訪問文件的線程數量 。通過設置信號量的初始值為允許同時訪問的最大線程數,每個線程在訪問文件前先獲取信號量,訪問完成后釋放信號量,這樣就可以有效地控制對文件資源的訪問,保證系統的穩定性 。因為信號量的計數器可以精確地控制并發訪問的數量,避免資源的過度競爭和沖突 。
五、實際案例分析
5.1 TCP 連接管理
在 Linux 內核的網絡協議棧中,同步機制起著關鍵的作用 。以 TCP 協議的連接管理為例,當多個線程同時處理 TCP 連接的建立、斷開和數據傳輸時,就需要使用同步機制來保證數據的一致性和操作的正確性 。在處理 TCP 連接請求時,可能會有多個線程同時接收到連接請求,這時候就需要使用自旋鎖來快速地對共享的連接隊列進行操作,確保每個連接請求都能被正確處理,避免出現重復處理或者數據混亂的情況 。
由于連接請求的處理通常非常快,使用自旋鎖可以避免線程上下文切換的開銷,提高系統的性能 。而在進行 TCP 數據傳輸時,由于數據傳輸可能會受到網絡延遲等因素的影響,需要較長的時間,這時候就會使用互斥鎖來保護數據緩沖區等共享資源,確保數據的正確讀寫 。因為在數據傳輸過程中,線程可能需要等待網絡響應,使用互斥鎖可以讓線程在等待時進入阻塞狀態,讓出 CPU 資源,提高系統的整體效率 。
我們將創建一個簡單的TCP連接請求處理程序,使用自旋鎖保護共享的連接隊列,代碼實現示例:
#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函數模擬處理TCP連接請求。它在修改共享隊列之前獲取自旋鎖,并在完成后釋放。
5.2 文件讀寫操作
在文件系統中,同步機制也不可或缺 。以文件的讀寫操作為例,當多個進程同時對一個文件進行讀寫時,就需要使用合適的同步機制 。對于文件的讀取操作,由于讀取操作不會修改文件內容,多個進程可以同時進行讀取,這時候可以使用讀寫鎖的讀鎖來提高并發性能 。而當有進程需要對文件進行寫入操作時,為了保證數據的一致性,就需要獲取讀寫鎖的寫鎖,獨占文件進行寫入 。在文件系統的元數據管理中,如文件的創建、刪除和目錄的遍歷等操作,由于這些操作涉及到對文件系統關鍵數據結構的修改,需要保證原子性和一致性,通常會使用互斥鎖來保護相關的操作 。因為這些操作可能會涉及到復雜的文件系統操作和磁盤 IO,使用互斥鎖可以有效地管理線程的執行順序,避免出現數據不一致的情況 。
接下來是一個簡化版的文件讀寫操作示例,使用互斥鎖和讀寫鎖來確保線程安全,代碼實現示例:
#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類型的讀寫鎖來控制對文件緩沖區的并發訪問。
- 在讀取時,通過調用down_read獲取讀鎖,以允許多個線程同時讀取而不阻塞;在寫入時,通過調用down_write獲取獨占寫鎖,以保證數據一致性。
通過這以上兩個簡單示例,可以看到在Linux內核中如何應用不同的同步機制來管理資源競爭,以提高性能和數據一致性。






























