成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

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

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

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

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

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

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

一、基本概念

1.1 同步機(jī)制

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

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

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

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

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

總結(jié)如下圖:

圖片圖片

1.3 中斷與搶占

中斷本身的概念很簡(jiǎn)單,本文不予解釋。當(dāng)然,這并不是說(shuō) Linux 內(nèi)核的中斷部分也很簡(jiǎn)單。事實(shí)上,Linux 內(nèi)核的中斷子系統(tǒng)也相當(dāng)復(fù)雜,因?yàn)橹袛鄬?duì)于操作系統(tǒng)來(lái)說(shuō)實(shí)在是太重要了。以后有機(jī)會(huì),筆者計(jì)劃開(kāi)專(zhuān)題再來(lái)介紹。對(duì)于同步機(jī)制的代碼分析來(lái)說(shuō),了解中斷的概念即可,不需要深入分析內(nèi)核的具體代碼實(shí)現(xiàn)。

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

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

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

用戶態(tài)搶占

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

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

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

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

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

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

1.4 編譯亂序與編譯屏障

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

注:測(cè)試使用 aarch64-linux-gnu-gcc 版本:7.3.0

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

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

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

我們應(yīng)該知道 Linux 默認(rèn)編譯優(yōu)化選項(xiàng)是 -O2,因此我們采用 -O2 優(yōu)化選項(xiàng)編譯上述代碼,并反匯編得到如下匯編結(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 的寫(xiě)入內(nèi)存順序符合代碼順序(program order);但是 -O2 優(yōu)化后,a 和 b 的寫(xiě)入順序和 program order 是相反的。-O2 優(yōu)化后的代碼轉(zhuǎn)換成 C 語(yǔ)言可以看作如下形式:

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

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

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

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

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

為了解決上述變量之間存在依賴關(guān)系導(dǎo)致 compiler 錯(cuò)誤優(yōu)化。compiler 為我們提供了編譯器屏障(compiler barriers),可用來(lái)告訴 compiler 不要 reorder。我們繼續(xù)使用上面的 foo() 函數(shù)作為演示實(shí)驗(yàn),在代碼之間插入 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)改變,之前對(duì)內(nèi)存的緩存(緩存到寄存器)都需要拋棄,barrier() 之后的內(nèi)存操作需要重新從內(nèi)存 load,而不能使用之前寄存器緩存的值。并且可以防止 compiler 優(yōu)化 barrier() 前后的內(nèi)存訪問(wèn)順序。barrier() 就像是代碼中的一道不可逾越的屏障,barrier() 前的 load/store 操作不能跑到 barrier() 后面;同樣,barrier() 后面的 load/store 操作不能在 barrier() 之前。依然使用 -O2 優(yōu)化選項(xiàng)編譯上述代碼,反匯編得到如下結(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 的寫(xiě)入順序和 program order 一致。因此,當(dāng)我們的代碼中需要嚴(yán)格的內(nèi)存順序,就需要考慮 compiler barriers。

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

除了顯示的插入 compiler barriers 之外,還有別的方法阻止 compiler reordering。例如 CPU barriers 指令,同樣會(huì)阻止 compiler reordering。后續(xù)我們?cè)倏紤] CPU barriers。除此以外,當(dāng)某個(gè)函數(shù)內(nèi)部包含 compiler barriers 時(shí),該函數(shù)也會(huì)充當(dāng) compiler barriers 的作用。即使這個(gè)函數(shù)被 inline,也是這樣。例如上面插入 barrier() 的 foo() 函數(shù),當(dāng)其他函數(shù)調(diào)用 foo() 時(shí),foo() 就相當(dāng)于 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 的寫(xiě)入順序。如果 fun() 函數(shù)不包含 barrier(),結(jié)果又會(huì)怎么樣呢?實(shí)際上,大多數(shù)的函數(shù)調(diào)用都表現(xiàn)出 compiler barriers 的作用。但是,這不包含 inline 的函數(shù)。因此,fun() 如果被 inline 進(jìn) foo(),那么 fun() 就不具有 compiler barriers 的作用。

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

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

同樣使用 -O2 優(yōu)化選項(xiàng)編譯代碼,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 不能假設(shè) printf() 不會(huì)使用或者修改 a 變量。因此在調(diào)用 printf() 之前會(huì)將 a 寫(xiě) 5,以保證 printf() 可能會(huì)用到新值。在 printf() 調(diào)用之后,重新從內(nèi)存中 load a 的值,然后賦值給變量 b。重新 load a 的原因是 compiler 也不知道 printf() 會(huì)不會(huì)修改 a 的值。

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

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

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

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

run 是個(gè)全局變量,foo() 在一個(gè)進(jìn)程中執(zhí)行,一直循環(huán)。我們期望的結(jié)果是 foo() 一直等到其他進(jìn)程修改 run 的值為 0 才退出循環(huán)。實(shí)際 compiler 編譯的代碼和我們會(huì)達(dá)到我們預(yù)期的結(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 語(yǔ)言形式:

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

compiler 首先將 run 加載到一個(gè)寄存器 reg 中,然后判斷 reg 是否滿足循環(huán)條件,如果滿足就一直循環(huán)。但是循環(huán)過(guò)程中,寄存器 reg 的值并沒(méi)有變化。因此,即使其他進(jìn)程修改 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)都會(huì)從內(nèi)存中重新 load run 的值。因此,當(dāng)有其他進(jìn)程修改 run 的值為 0 的時(shí)候,foo() 可以正常退出循環(huán)。為什么加入 barrier() 后的匯編代碼就是正確的呢?因?yàn)?barrier() 作用是告訴 compiler 內(nèi)存中的值已經(jīng)變化,后面的操作都需要重新從內(nèi)存 load,而不能使用寄存器緩存的值。因此,這里的 run 變量會(huì)從內(nèi)存重新 load,然后判斷循環(huán)條件。這樣,其他進(jìn)程修改 run 變量,foo() 就可以看得見(jiàn)了。

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

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

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

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

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

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

當(dāng)然你也可以修改 run 的定義為 volatile int run就會(huì)得到如下代碼。同樣可以達(dá)到預(yù)期目的。

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

二、同步機(jī)制的起源

在深入探討 Linux 內(nèi)核同步機(jī)制之前,我們先來(lái)理解一下并發(fā)(Concurrency)與競(jìng)態(tài)(Race Condition)的概念,因?yàn)樗鼈兪峭綑C(jī)制存在的根本原因。

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

并發(fā),簡(jiǎn)單來(lái)說(shuō),就是指多個(gè)執(zhí)行單元同時(shí)、并行地被執(zhí)行 。在 Linux 系統(tǒng)中,并發(fā)主要有以下幾種場(chǎng)景:

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

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

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

2.2 競(jìng)態(tài)帶來(lái)的問(wèn)題

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

// 共享變量
int count = 0;

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

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

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

  1. 進(jìn)程 P1 讀取 count 的值,此時(shí) temp 為 0。
  2. 進(jìn)程 P2 讀取 count 的值,此時(shí) temp 也為 0,因?yàn)?P1 還沒(méi)有將修改后的值寫(xiě)回 count。
  3. 進(jìn)程 P1 對(duì) temp加 1,然后將 temp 的值寫(xiě)回 count,此時(shí) count 為 1。
  4. 進(jìn)程P2對(duì)temp加1(此時(shí) temp 還是 0),然后將temp的值寫(xiě)回 count,此時(shí) count 還是 1,而不是 2。

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

三、常見(jiàn)同步機(jī)制解析

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

3.1 自旋鎖(Spinlocks)

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

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

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

自旋鎖的API有:

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

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

如果被保護(hù)的共享資源只在進(jìn)程上下文訪問(wèn)和軟中斷上下文訪問(wèn),那么當(dāng)在進(jìn)程上下文訪問(wèn)共享資源時(shí),可能被軟中斷打斷,從而可能進(jìn)入軟中斷上下文來(lái)對(duì)被保護(hù)的共享資源訪問(wèn),因此對(duì)于這種情況,對(duì)共享資源的訪問(wèn)必須使用spin_lock_bh和spin_unlock_bh來(lái)保護(hù)。

當(dāng)然使用spin_lock_irq和spin_unlock_irq以及spin_lock_irqsave和spin_unlock_irqrestore也可以,它們失效了本地硬中斷,失效硬中斷隱式地也失效了軟中斷。但是使用spin_lock_bh和spin_unlock_bh是最恰當(dāng)?shù)模绕渌麅蓚€(gè)快。

如果被保護(hù)的共享資源只在進(jìn)程上下文和tasklet或timer上下文訪問(wèn),那么應(yīng)該使用與上面情況相同的獲得和釋放鎖的宏,因?yàn)閠asklet和timer是用軟中斷實(shí)現(xiàn)的。

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

如果被保護(hù)的共享資源只在兩個(gè)或多個(gè)tasklet或timer上下文訪問(wèn),那么對(duì)共享資源的訪問(wèn)僅需要用spin_lock和spin_unlock來(lái)保護(hù),不必使用_bh版本,因?yàn)楫?dāng)tasklet或timer運(yùn)行時(shí),不可能有其他tasklet或timer在當(dāng)前CPU上運(yùn)行。

如果被保護(hù)的共享資源只在一個(gè)軟中斷(tasklet和timer除外)上下文訪問(wèn),那么這個(gè)共享資源需要用spin_lock和spin_unlock來(lái)保護(hù),因?yàn)橥瑯拥能浿袛嗫梢酝瑫r(shí)在不同的CPU上運(yùn)行。

如果被保護(hù)的共享資源在兩個(gè)或多個(gè)軟中斷上下文訪問(wèn),那么這個(gè)共享資源當(dāng)然更需要用spin_lock和spin_unlock來(lái)保護(hù),不同的軟中斷能夠同時(shí)在不同的CPU上運(yùn)行。

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

而在中斷處理句柄中使用什么版本,需依情況而定,如果只有一個(gè)中斷處理句柄訪問(wèn)該共享資源,那么在中斷處理句柄中僅需要spin_lock和spin_unlock來(lái)保護(hù)對(duì)共享資源的訪問(wèn)就可以了。

因?yàn)樵趫?zhí)行中斷處理句柄期間,不可能被同一CPU上的軟中斷或進(jìn)程打斷。但是如果有不同的中斷處理句柄訪問(wèn)該共享資源,那么需要在中斷處理句柄中使用spin_lock_irq和spin_unlock_irq來(lái)保護(hù)對(duì)共享資源的訪問(wèn)。

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

因?yàn)樗萻pin_lock_irqsave要快一些,但是如果你不能確定是否中斷使能,那么使用spin_lock_irqsave和spin_unlock_irqrestore更好,因?yàn)樗鼘⒒謴?fù)訪問(wèn)共享資源前的中斷標(biāo)志而不是直接使能中斷。

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

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

3.2 互斥鎖(Mutexes)

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

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

3.3 讀寫(xiě)鎖(Read-Write Locks)

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

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

讀寫(xiě)信號(hào)量的相關(guān)API有:

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

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

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

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

3.4 信號(hào)量(Semaphores)

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

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

信號(hào)量的API有:

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

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

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

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

需要訪問(wèn)console_drivers驅(qū)動(dòng)列表時(shí)就需要使用acquire_console_sem來(lái)保護(hù)console_drivers列表,當(dāng)訪問(wèn)完該列表后,就調(diào)用release_console_sem釋放信號(hào)量console_sem。

函數(shù)console_unblank,console_device,console_stop,console_start,register_console和unregister_console都需要訪問(wèn)console_drivers,因此它們都使用函數(shù)對(duì)acquire_console_sem和release_console_sem來(lái)對(duì)console_drivers進(jìn)行保護(hù)。

3.5 原子操作(Atomic Operations)

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

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

typedef struct { 
volatile int counter; 
} atomic_t;

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

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

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

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

四、同步機(jī)制的選擇與應(yīng)用場(chǎng)景

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

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

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

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

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

五、實(shí)際案例分析

5.1 TCP 連接管理

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

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

我們將創(chuàng)建一個(gè)簡(jiǎn)單的TCP連接請(qǐng)求處理程序,使用自旋鎖保護(hù)共享的連接隊(duì)列,代碼實(shí)現(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類(lèi)型的自旋鎖來(lái)保護(hù)對(duì)共享資源(連接隊(duì)列)的訪問(wèn)。
  • handle_connection_request函數(shù)模擬處理TCP連接請(qǐng)求。它在修改共享隊(duì)列之前獲取自旋鎖,并在完成后釋放。

5.2 文件讀寫(xiě)操作

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

接下來(lái)是一個(gè)簡(jiǎn)化版的文件讀寫(xiě)操作示例,使用互斥鎖和讀寫(xiě)鎖來(lái)確保線程安全,代碼實(shí)現(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); // 獲取寫(xiě)鎖
    memcpy(file_buffer, buffer, size);
    up_write(&file_rwsem);   // 釋放寫(xiě)鎖
}

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類(lèi)型的讀寫(xiě)鎖來(lái)控制對(duì)文件緩沖區(qū)的并發(fā)訪問(wèn)。
  • 在讀取時(shí),通過(guò)調(diào)用down_read獲取讀鎖,以允許多個(gè)線程同時(shí)讀取而不阻塞;在寫(xiě)入時(shí),通過(guò)調(diào)用down_write獲取獨(dú)占寫(xiě)鎖,以保證數(shù)據(jù)一致性。

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

責(zé)任編輯:武曉燕 來(lái)源: 深度Linux
相關(guān)推薦

2025-02-26 09:55:59

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

2024-07-25 11:53:53

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同步機(jī)制多線程編程

2023-11-22 13:18:02

Linux調(diào)度

2024-06-28 08:45:58

2019-11-22 18:52:31

進(jìn)程同步機(jī)制編程語(yǔ)言

2012-07-09 09:25:13

ibmdw

2017-12-15 10:20:56

MySQLInnoDB同步機(jī)制

2011-11-23 10:09:19

Java線程機(jī)制

2009-08-12 13:37:01

Java synchr

2024-07-08 12:51:05

2010-03-15 16:31:34

Java多線程

2023-09-26 10:30:57

Linux編程

2021-10-08 20:30:12

ZooKeeper選舉機(jī)制

2024-01-22 09:00:00

編程C++代碼

2023-11-24 11:15:21

協(xié)程編程
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

主站蜘蛛池模板: 久久69精品久久久久久国产越南 | 中文字幕一区在线 | 天天爽天天干 | 日本二区 | 狠狠色狠狠色综合系列 | 天天综合日日夜夜 | 久久性色 | 久久精品视频在线观看 | 伦理午夜电影免费观看 | 欧美在线精品一区 | 亚洲欧美日韩电影 | 在线色 | 色婷婷精品国产一区二区三区 | 免费成人高清在线视频 | 亚洲自拍偷拍视频 | 午夜视频网站 | 久久精品色欧美aⅴ一区二区 | 欧美精品一区二区三区一线天视频 | 日本免费小视频 | 超碰在线免费av | 人人看人人干 | 国产亚洲精品精品国产亚洲综合 | 国产精品一区二区三区久久久 | 日韩欧美国产一区二区 | 亚洲第一av| 噜久寡妇噜噜久久寡妇 | 欧美日韩亚洲国产 | 国产精品美女久久久免费 | 夜夜草av| 国产精品一区在线观看 | 国产亚洲精品综合一区 | 国产精品小视频在线观看 | 国产精品高清在线 | 成人在线精品视频 | 又黄又爽的网站 | 免费污视频 | 羞羞视频网站免费观看 | 午夜在线视频一区二区三区 | 国产精品爱久久久久久久 | 在线日韩 | 亚洲天堂久久新 |