一文吃透Linux同步管理,讓你的系統(tǒng)穩(wěn)如泰山
在當(dāng)今的計(jì)算機(jī)世界中,多任務(wù)處理和多核處理器已成為標(biāo)配。無(wú)論是在服務(wù)器端處理大量并發(fā)請(qǐng)求,還是在嵌入式系統(tǒng)中高效運(yùn)行多個(gè)任務(wù),Linux 操作系統(tǒng)都扮演著至關(guān)重要的角色。而在這復(fù)雜的多任務(wù)、多核環(huán)境下,Linux 的同步管理機(jī)制就如同精密儀器中的校準(zhǔn)裝置,對(duì)數(shù)據(jù)一致性和系統(tǒng)穩(wěn)定性起著關(guān)鍵作用。
想象一下,多個(gè)進(jìn)程或線程如同忙碌的工人,同時(shí)對(duì)共享資源進(jìn)行操作。如果沒(méi)有有效的同步管理,就好比施工現(xiàn)場(chǎng)沒(méi)有指揮,工人各自為政,必然會(huì)導(dǎo)致資源競(jìng)爭(zhēng)、數(shù)據(jù)不一致等問(wèn)題。比如在一個(gè)多線程的數(shù)據(jù)庫(kù)應(yīng)用中,若多個(gè)線程同時(shí)對(duì)數(shù)據(jù)庫(kù)進(jìn)行讀寫操作而沒(méi)有同步機(jī)制,可能會(huì)導(dǎo)致數(shù)據(jù)失、臟讀等嚴(yán)重問(wèn)題,進(jìn)而影響整個(gè)系統(tǒng)的正常運(yùn)行 。再比如在服務(wù)器端,多個(gè)用戶的請(qǐng)求并發(fā)到達(dá),若不能對(duì)這些請(qǐng)求進(jìn)行有序處理,很可能會(huì)導(dǎo)致服務(wù)器響應(yīng)混亂,甚至崩潰。
Linux 同步管理機(jī)制通過(guò)一系列的策略和方法,協(xié)調(diào)多個(gè)進(jìn)程或線程對(duì)共享資源的訪問(wèn),防止出現(xiàn)競(jìng)態(tài)條件、死鎖等問(wèn)題。它就像是一位經(jīng)驗(yàn)豐富的交通警察,在復(fù)雜的交通路口指揮車輛有序通行,確保每個(gè) “車輛”(進(jìn)程或線程)都能安全、高效地到達(dá)目的地,從而維護(hù)數(shù)據(jù)的一致性和系統(tǒng)的整體運(yùn)行秩序。在多核和多處理器系統(tǒng)中,良好的同步機(jī)制更是能夠高效地管理資源訪問(wèn),避免緩存一致性問(wèn)題和偽共享問(wèn)題,對(duì)提升系統(tǒng)性能起著至關(guān)重要的作用。接下來(lái),讓我們深入探討 Linux 同步管理的具體方法和實(shí)現(xiàn)。
一、Linux 同步管理基礎(chǔ)概念
在 Linux 系統(tǒng)中,同步機(jī)制是用于實(shí)現(xiàn)控制多個(gè)執(zhí)行路徑按照一定的規(guī)則或順序訪問(wèn)某些系統(tǒng)資源的機(jī)制 。這里所說(shuō)的執(zhí)行路徑,指的是在 CPU 上運(yùn)行的代碼流。我們知道,CPU 調(diào)度的最小單位是線程,它既可以是用戶態(tài)線程,也可以是內(nèi)核線程,甚至中斷服務(wù)程序也包含在內(nèi)。
簡(jiǎn)單來(lái)說(shuō),同步機(jī)制就像是一場(chǎng)精心安排的音樂(lè)會(huì),每個(gè)演奏者(執(zhí)行路徑)都需要按照既定的樂(lè)譜(規(guī)則)在合適的時(shí)間演奏,以確保整個(gè)音樂(lè)(系統(tǒng)資源訪問(wèn))和諧有序 。例如,在一個(gè)多線程的文件讀寫程序中,多個(gè)線程都可能嘗試對(duì)同一個(gè)文件進(jìn)行讀寫操作。如果沒(méi)有同步機(jī)制,這些線程可能會(huì)同時(shí)修改文件內(nèi)容,導(dǎo)致數(shù)據(jù)混亂。而通過(guò)同步機(jī)制,我們可以確保在同一時(shí)刻只有一個(gè)線程能夠?qū)ξ募M(jìn)行寫入操作,其他線程需要等待,從而保證文件數(shù)據(jù)的一致性 。
(1)并發(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)起門的那一刻起,你就無(wú)法進(jìn)去了,只能在門外侯著。
當(dāng)小三哥出來(lái)后你才能進(jìn)去解決你的問(wèn)題。這里,公司廁所就是共享資源,你和張小三同時(shí)需要這個(gè)共享資源就是并發(fā),你們對(duì)廁所的使用需求就構(gòu)成了競(jìng)態(tài),而廁所的門就是一種同步機(jī)制,他在用你就不能用了。
總結(jié)如下圖:
圖片
(2)中斷與搶占
中斷是指計(jì)算機(jī)在執(zhí)行程序的過(guò)程中,當(dāng)出現(xiàn)某些緊急事件時(shí),暫時(shí)停止當(dāng)前程序的執(zhí)行,轉(zhuǎn)去處理這些事件,處理完畢后再返回原來(lái)的程序繼續(xù)執(zhí)行 。比如,當(dāng)有新的數(shù)據(jù)到達(dá)網(wǎng)絡(luò)接口卡時(shí),會(huì)產(chǎn)生一個(gè)中斷信號(hào),通知 CPU 進(jìn)行處理 。搶占則屬于進(jìn)程調(diào)度的概念,Linux 內(nèi)核從 2.6 版本開(kāi)始支持搶占調(diào)度。通俗地講,搶占就是一個(gè)正在 CPU 上愉快運(yùn)行的任務(wù)(可以是用戶態(tài)進(jìn)程,也可以是內(nèi)核線程)被另一個(gè)通常是更高優(yōu)先級(jí)的任務(wù)奪去 CPU 執(zhí)行權(quán) 。中斷和搶占之間有著密切的關(guān)系,搶占依賴中斷 。
如果當(dāng)前 CPU 禁止了本地中斷,那么也就意味著禁止了本 CPU 上的搶占。但反過(guò)來(lái),禁掉搶占并不影響中斷 。例如,在一個(gè)實(shí)時(shí)控制系統(tǒng)中,可能會(huì)有一些高優(yōu)先級(jí)的中斷任務(wù)需要立即處理。當(dāng)這些中斷發(fā)生時(shí),CPU 會(huì)暫停當(dāng)前正在執(zhí)行的任務(wù),轉(zhuǎn)而執(zhí)行中斷處理程序,這就體現(xiàn)了中斷對(duì)任務(wù)執(zhí)行的影響。而搶占機(jī)制則確保了高優(yōu)先級(jí)的任務(wù)能夠及時(shí)獲得 CPU 資源,提高系統(tǒng)的響應(yīng)性能 。
用戶態(tài)搶占:
- 從系統(tǒng)調(diào)用返回用戶空間時(shí);
- 從中斷(異常)處理程序返回用戶空間時(shí)。
內(nèi)核態(tài)搶占:
- 當(dāng)一個(gè)中斷處理程序退出,返回到內(nèi)核態(tài)時(shí);
- task 顯式調(diào)用 schedule();
- task 發(fā)生阻塞(此時(shí)由調(diào)度器完成調(diào)度)。
在 Linux 系統(tǒng)中,為了確保多任務(wù)環(huán)境下數(shù)據(jù)的一致性和系統(tǒng)的穩(wěn)定性,提供了多種同步管理方法,每種方法都有其獨(dú)特的應(yīng)用場(chǎng)景和實(shí)現(xiàn)原理。下面將詳細(xì)介紹原子操作、內(nèi)存屏障、自旋鎖、信號(hào)量、互斥鎖和 RCU(讀 - 復(fù)制 - 更新)等常見(jiàn)的同步機(jī)制。
二、原子操作(Atomic Operation)
所謂原子操作,就是該操作絕不會(huì)在執(zhí)行完畢前被任何其他任務(wù)或事件打斷,也就說(shuō),它的最小的執(zhí)行單位,不可能有比它更小的執(zhí)行單位,因此這里的原子實(shí)際是使用了物理學(xué)里的物質(zhì)微粒的概念。
原子操作需要硬件的支持,因此是架構(gòu)相關(guān)的,其API和原子類型的定義都定義在內(nèi)核源碼樹(shù)的include/asm/atomic.h文件中,它們都使用匯編語(yǔ)言實(shí)現(xiàn),因?yàn)镃語(yǔ)言并不能實(shí)現(xiàn)這樣的操作。
原子操作主要用于實(shí)現(xiàn)資源計(jì)數(shù),很多引用計(jì)數(shù)(refcnt)就是通過(guò)原子操作實(shí)現(xiàn)的。原子類型定義如下:
typedef struct {
volatile int counter;
} atomic_t;
volatile修飾字段告訴gcc不要對(duì)該類型的數(shù)據(jù)做優(yōu)化處理,對(duì)它的訪問(wèn)都是對(duì)內(nèi)存的訪問(wèn),而不是對(duì)寄存器的訪問(wèn)。
原子操作API包括:
- tomic_read(atomic_t * v);該函數(shù)對(duì)原子類型的變量進(jìn)行原子讀操作,它返回原子類型的變量v的值。
- atomic_set(atomic_t * v, int i);該函數(shù)設(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ù)對(duì)原子類型變量v原子地增加1。
- void atomic_dec(atomic_t *v);該函數(shù)對(duì)原子類型的變量v原子地減1。
- int atomic_dec_and_test(atomic_t *v);該函數(shù)對(duì)原子類型的變量v原子地減1,并判斷結(jié)果是否為0,如果為0,返回真,否則返回假。
- int atomic_inc_and_test(atomic_t *v);該函數(shù)對(duì)原子類型的變量v原子地增加1,并判斷結(jié)果是否為0,如果為0,返回真,否則返回假。
- int atomic_add_negative(int i, atomic_t *v);該函數(shù)對(duì)原子類型的變量v原子地增加I,并判斷結(jié)果是否為負(fù)數(shù),如果是,返回真,否則返回假。
- int atomic_add_return(int i, atomic_t *v);該函數(shù)對(duì)原子類型的變量v原子地增加i,并且返回指向v的指針。
- int atomic_sub_return(int i, atomic_t *v);該函數(shù)從原子類型的變量v中減去i,并且返回指向v的指針。
- int atomic_inc_return(atomic_t * v);該函數(shù)對(duì)原子類型的變量v原子地增加1并且返回指向v的指針。
- int atomic_dec_return(atomic_t * v);該函數(shù)對(duì)原子類型的變量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ù)器,它的類型為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))。
三、內(nèi)存屏障(Memory Barrier)
內(nèi)存屏障是一種重要的同步機(jī)制,它主要用于解決內(nèi)存亂序訪問(wèn)的問(wèn)題 。在 ARM 架構(gòu)中,存在 3 類內(nèi)存屏障指令,分別是數(shù)據(jù)存儲(chǔ)器隔離(Data Memory Barrier,DMB)、數(shù)據(jù)同步隔離(Data Synchronization Barrier,DSB)和指令同步隔離(Instruction Synchronization Barrier,ISB) 。DMB 指令保證僅當(dāng)所有在它前面的存儲(chǔ)器訪問(wèn)操作都執(zhí)行完畢后,才提交在它后面的存儲(chǔ)器訪問(wèn)操作 。也就是說(shuō),DMB 指令確保了在它之前和之后的內(nèi)存訪問(wèn)操作的順序性,但不保證內(nèi)存訪問(wèn)的完成順序 。
DSB 指令比 DMB 指令更加嚴(yán)格,僅當(dāng)所有在它前面的存儲(chǔ)器訪問(wèn)操作都執(zhí)行完畢后,才會(huì)執(zhí)行在它后面的指令 。這意味著,任何指令都要等待 DSB 前面的存儲(chǔ)訪問(wèn)完成,包括緩存操作等 。ISB 指令則是最嚴(yán)格的同步指令,它會(huì)清洗流水線,以保證所有它前面的指令都執(zhí)行完畢之后,才執(zhí)行它后面的指令 。在 Linux 內(nèi)核中,也提供了一些內(nèi)存屏障函數(shù),如 barrier、mb、rmb、wmb 等 。
barrier 函數(shù)用于阻止編譯器對(duì)內(nèi)存訪問(wèn)指令進(jìn)行重排 ,它告訴編譯器不要為了性能優(yōu)化而將 barrier 前后的代碼順序打亂 。mb 函數(shù)則同時(shí)具備讀內(nèi)存屏障和寫內(nèi)存屏障的功能,它確保了在 mb 之前的內(nèi)存訪問(wèn)操作都完成后,才會(huì)執(zhí)行在 mb 之后的內(nèi)存訪問(wèn)操作 。rmb 函數(shù)是讀內(nèi)存屏障,保證了在 rmb 之前的讀操作都完成后,才會(huì)執(zhí)行在 rmb 之后的讀操作 。wmb 函數(shù)是寫內(nèi)存屏障,確保了在 wmb 之前的寫操作都完成后,才會(huì)執(zhí)行在 wmb 之后的寫操作 。
這些內(nèi)存屏障函數(shù)在多處理器系統(tǒng)中,對(duì)于保證內(nèi)存訪問(wèn)的順序性和數(shù)據(jù)的一致性起著至關(guān)重要的作用 。例如,在多線程編程中,當(dāng)一個(gè)線程修改了共享變量的值后,使用 wmb 函數(shù)可以確保其他線程能夠正確地讀取到這個(gè)修改后的值,避免了由于內(nèi)存亂序訪問(wèn)而導(dǎo)致的數(shù)據(jù)不一致問(wèn)題 。
在 Linux 系統(tǒng)中,根據(jù)其作用和功能,內(nèi)存屏障主要分為以下三種類型:
①全屏障(Full Barrier)
全屏障,也稱作強(qiáng)內(nèi)存屏障 ,它的功能最為強(qiáng)大。全屏障可以阻止屏障兩邊的讀寫操作進(jìn)行重排序,確保在屏障之前的所有讀寫操作,都在屏障之后的讀寫操作之前完成。在 x86 架構(gòu)中,全屏障的實(shí)現(xiàn)指令是mfence 。當(dāng) CPU 執(zhí)行到mfence指令時(shí),會(huì)將之前所有的存儲(chǔ)和加載操作都按順序完成,才會(huì)繼續(xù)執(zhí)行后面的指令。例如:
// 線程1
x = 1; // 寫操作1
mfence(); // 全屏障
y = 2; // 寫操作2
// 線程2
if (y == 2) { // 讀操作1
assert(x == 1); // 讀操作2
}
在這個(gè)例子中,由于mfence全屏障的存在,線程 1 中x = 1的寫操作一定會(huì)在線程 2 讀取y的值之前完成,從而保證了線程 2 在讀取y為 2 時(shí),x的值也已經(jīng)被正確地更新為 1,避免了由于指令重排序?qū)е碌臄?shù)據(jù)不一致問(wèn)題。全屏障在需要嚴(yán)格保證內(nèi)存操作順序的場(chǎng)景中非常有用,比如在實(shí)現(xiàn)一些關(guān)鍵的同步機(jī)制或者對(duì)共享資源的復(fù)雜操作時(shí)。
②讀取屏障(Read/Load Barrier)
讀取屏障的作用是確保在該屏障之前的所有讀取操作,必須在該屏障之后的讀取操作之前完成 。它主要用于控制讀取操作的順序,防止讀取操作的重排序。在 x86 架構(gòu)中,讀取屏障對(duì)應(yīng)的指令是lfence 。例如:
// 線程1
int a = shared_variable1; // 讀操作A
lfence(); // 讀取屏障
int b = shared_variable2; // 讀操作B
在上述代碼中,lfence讀取屏障保證了讀操作 A 一定會(huì)在讀操作 B 之前完成。即使處理器可能有優(yōu)化策略,也不能將讀操作 B 提前到讀操作 A 之前執(zhí)行。讀取屏障在多線程環(huán)境中,當(dāng)讀取操作的順序?qū)Τ绦蜻壿嬘兄匾绊憰r(shí)非常關(guān)鍵。比如在一些依賴于特定讀取順序的算法實(shí)現(xiàn)中,或者在讀取共享狀態(tài)變量時(shí),為了確保獲取到正確的狀態(tài)信息,就需要使用讀取屏障來(lái)保證讀取操作的順序性 。
③寫入屏障(Write/Store Barrier)
一個(gè)寫內(nèi)存屏障可以提供這樣的保證,站在系統(tǒng)中的其它組件的角度來(lái)看,在屏障之前的寫操作看起來(lái)將在屏障后的寫操作之前發(fā)生。
如果映射到上面的例子來(lái)說(shuō),首先,寫內(nèi)存屏障會(huì)對(duì)處理器指令重排序做出一些限制,也就是在寫內(nèi)存屏障之前的寫入指令一定不會(huì)被重排序到寫內(nèi)存屏障之后的寫入指令之后。其次,在執(zhí)行寫內(nèi)存屏障之后的寫入指令之前,一定要保證清空當(dāng)前CPU存儲(chǔ)緩沖中的所有寫操作,將它們?nèi)俊疤峤弧钡骄彺嬷小_@樣的話系統(tǒng)中的其它組件(包括別的CPU),就可以保證在看到寫內(nèi)存屏障之后的寫入數(shù)據(jù)之前先看到寫內(nèi)存屏障之前的寫入數(shù)據(jù)。
圖片
寫入屏障用于確保在該屏障之前的所有寫入操作,必須在該屏障之后的寫入操作之前完成 。它主要關(guān)注寫入操作的順序,防止寫入操作的重排序。在 x86 架構(gòu)中,寫入屏障的指令是sfence 。例如:
// 線程1
shared_variable1 = 10; // 寫操作C
sfence(); // 寫入屏障
shared_variable2 = 20; // 寫操作D
這里,sfence寫入屏障確保了寫操作 C 一定會(huì)在寫操作 D 之前完成。無(wú)論編譯器如何優(yōu)化或者處理器如何執(zhí)行指令,都不會(huì)改變這兩個(gè)寫操作的順序。寫入屏障在多線程同時(shí)修改共享數(shù)據(jù)時(shí)非常重要,它可以保證數(shù)據(jù)的更新按照預(yù)期的順序進(jìn)行,避免由于寫入順序混亂導(dǎo)致的數(shù)據(jù)不一致問(wèn)題。比如在更新一些關(guān)聯(lián)的共享變量時(shí),使用寫入屏障可以確保先更新的變量對(duì)其他線程可見(jiàn)后,再進(jìn)行后續(xù)變量的更新 。
四、自旋鎖(spinlock)
自旋鎖是一種用于多線程同步的機(jī)制,其作用是保證在同一時(shí)刻只有一個(gè)執(zhí)行單元能夠進(jìn)入臨界區(qū),從而確保復(fù)雜操作不受其他執(zhí)行單元的干擾順利完成 。自旋鎖的結(jié)構(gòu)定義較為復(fù)雜,在 Linux 內(nèi)核中,相關(guān)結(jié)構(gòu)體層層嵌套 。以較新版本內(nèi)核為例,spinlock_t 通過(guò) union 關(guān)聯(lián)到 raw_spinlock,而 raw_spinlock 又包含 arch_spinlock_t 。arch_spinlock_t 利用 union 將 u32 類型的 slock 與 struct __raw_tickets 結(jié)構(gòu)體相關(guān)聯(lián),結(jié)構(gòu)體中的 u16 類型的 owner 和 next 成員用于實(shí)現(xiàn) FIFO(先進(jìn)先出)機(jī)制 。
當(dāng)一個(gè)線程嘗試獲取自旋鎖時(shí),如果鎖已經(jīng)被其他線程占用,它不會(huì)立即進(jìn)入睡眠狀態(tài),而是在一個(gè)循環(huán)中不斷檢查鎖是否可用,即所謂的 “自旋” 。這種方式避免了線程切換的開(kāi)銷,特別適合短時(shí)間內(nèi)的鎖競(jìng)爭(zhēng)場(chǎng)景 。自旋鎖的 FIFO 機(jī)制保證了等待鎖的線程按照先來(lái)后到的順序獲取鎖,從而實(shí)現(xiàn)了公平性 。例如,多個(gè)線程同時(shí)請(qǐng)求獲取自旋鎖,先請(qǐng)求的線程會(huì)先獲取到鎖,后請(qǐng)求的線程則需要等待 。
然而,自旋鎖也存在一些問(wèn)題,比如在鎖被長(zhǎng)時(shí)間持有時(shí),等待的線程會(huì)持續(xù)自旋,浪費(fèi) CPU 資源 。為了應(yīng)對(duì)中斷死鎖問(wèn)題,自旋鎖提供了不同類型的操作函數(shù) 。如 spin_lock_irqsave 在獲取鎖之前會(huì)保存當(dāng)前中斷狀態(tài)并禁用中斷,spin_unlock_irqrestore 則會(huì)恢復(fù)之前保存的中斷狀態(tài) 。這樣可以防止在持有自旋鎖期間發(fā)生中斷,從而避免死鎖的發(fā)生 。
自旋鎖的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)該宏類似于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)。
五、信號(hào)量(semaphore)
Linux內(nèi)核的信號(hào)量在概念和原理上與用戶態(tài)的System V的IPC機(jī)制信號(hào)量是一樣的,但是它絕不可能在內(nèi)核之外使用,因此它與System V的IPC機(jī)制信號(hào)量毫不相干。
信號(hào)量在創(chuàng)建時(shí)需要設(shè)置一個(gè)初始值,表示同時(shí)可以有幾個(gè)任務(wù)可以訪問(wèn)該信號(hào)量保護(hù)的共享資源,初始值為1就變成互斥鎖(Mutex),即同時(shí)只能有一個(gè)任務(wù)可以訪問(wèn)信號(hào)量保護(hù)的共享資源。
一個(gè)任務(wù)要想訪問(wèn)共享資源,首先必須得到信號(hào)量,獲取信號(hào)量的操作將把信號(hào)量的值減1,若當(dāng)前信號(hào)量的值為負(fù)數(shù),表明無(wú)法獲得信號(hào)量,該任務(wù)必須掛起在該信號(hào)量的等待隊(duì)列等待該信號(hào)量可用;若當(dāng)前信號(hào)量的值為非負(fù)數(shù),表示可以獲得信號(hào)量,因而可以立刻訪問(wèn)被該信號(hào)量保護(hù)的共享資源。
當(dāng)任務(wù)訪問(wèn)完被信號(hào)量保護(hù)的共享資源后,必須釋放信號(hào)量,釋放信號(hào)量通過(guò)把信號(hào)量的值加1實(shí)現(xiàn),如果信號(hào)量的值為非正數(shù),表明有任務(wù)等待當(dāng)前信號(hào)量,因此它也喚醒所有等待該信號(hào)量的任務(wù)。
信號(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類似,不同之處為,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ù)。
六、互斥鎖(Mutex)
互斥鎖在功能上和信號(hào)量 count 為 1 時(shí)的行為較為相似,都用于保證在同一時(shí)刻只有一個(gè)線程能夠訪問(wèn)共享資源 。然而,由于性能等方面的原因,互斥鎖重新進(jìn)行了實(shí)現(xiàn) 。在 Linux 內(nèi)核中,互斥鎖的結(jié)構(gòu)定義包含多個(gè)關(guān)鍵成員 。其中,owner 用于記錄當(dāng)前持有鎖的線程;count 用于表示鎖的狀態(tài),0 表示鎖被占用,1 表示鎖可用;wait_list 同樣是一個(gè)等待隊(duì)列,用于存放等待獲取鎖的線程 。
互斥鎖具有一些獨(dú)特的特點(diǎn),例如在競(jìng)爭(zhēng)不激烈的情況下,它會(huì)切換為自旋等待 。這是因?yàn)樵诙虝r(shí)間內(nèi),鎖很可能會(huì)被釋放,自旋等待可以避免線程切換的開(kāi)銷 。互斥鎖相關(guān)的 API 主要有 mutex_lock、mutex_unlock 和 mutex_trylock 。mutex_lock 用于獲取互斥鎖,如果鎖已被持有,當(dāng)前線程將被阻塞;mutex_unlock 用于釋放互斥鎖,允許其他線程獲??;mutex_trylock 則是嘗試獲取互斥鎖,如果鎖被持有,它會(huì)立即返回失敗,而不會(huì)阻塞線程 。在實(shí)際應(yīng)用中,比如在一個(gè)多線程的數(shù)據(jù)庫(kù)連接池管理中,互斥鎖可以用于保護(hù)連接池的共享資源,確保每次只有一個(gè)線程能夠獲取或釋放數(shù)據(jù)庫(kù)連接 。
互斥鎖的工作原理基于操作系統(tǒng)提供的原子操作和線程調(diào)度機(jī)制。當(dāng)一個(gè)線程執(zhí)行到需要訪問(wèn)共享資源的代碼段時(shí),它會(huì)調(diào)用互斥鎖的加鎖函數(shù)(如std::mutex的lock方法)。此時(shí),互斥鎖會(huì)檢查自身的狀態(tài),如果當(dāng)前處于未鎖定狀態(tài),它會(huì)將自己標(biāo)記為已鎖定,并允許該線程進(jìn)入臨界區(qū)訪問(wèn)共享資源。這個(gè)標(biāo)記過(guò)程是通過(guò)原子操作實(shí)現(xiàn)的,確保在多線程環(huán)境下不會(huì)出現(xiàn)競(jìng)爭(zhēng)條件。例如,在一個(gè)多線程的文件讀寫操作中,當(dāng)一個(gè)線程獲取到互斥鎖后,就可以安全地對(duì)文件進(jìn)行寫入,避免其他線程同時(shí)寫入導(dǎo)致文件內(nèi)容混亂。
如果互斥鎖已經(jīng)被其他線程鎖定,那么調(diào)用加鎖函數(shù)的線程會(huì)被操作系統(tǒng)掛起,放入等待隊(duì)列中,進(jìn)入阻塞狀態(tài)。此時(shí),該線程會(huì)讓出 CPU 資源,以便其他線程能夠繼續(xù)執(zhí)行,避免了無(wú)效的 CPU 占用。就像在一條單行道上,當(dāng)一輛車已經(jīng)在行駛時(shí),其他車輛只能在路口等待,直到前面的車通過(guò)。
當(dāng)持有鎖的線程完成對(duì)共享資源的訪問(wèn)后,它會(huì)調(diào)用互斥鎖的解鎖函數(shù)(如std::mutex的unlock方法) 。解鎖操作會(huì)將互斥鎖的狀態(tài)標(biāo)記為未鎖定,并從等待隊(duì)列中喚醒一個(gè)等待的線程(如果有線程在等待)。被喚醒的線程會(huì)重新競(jìng)爭(zhēng) CPU 資源,當(dāng)它獲得 CPU 時(shí)間片后,會(huì)再次嘗試獲取互斥鎖。一旦獲取成功,就可以進(jìn)入臨界區(qū)訪問(wèn)共享資源。例如,在一個(gè)多線程的數(shù)據(jù)庫(kù)操作中,當(dāng)一個(gè)線程完成對(duì)數(shù)據(jù)庫(kù)的更新操作并釋放互斥鎖后,等待隊(duì)列中的另一個(gè)線程就有機(jī)會(huì)獲取鎖,進(jìn)行查詢或其他操作。
七、RCU(Read-Copy-Update)
RCU 是一種高效的讀寫同步機(jī)制,可看作是讀寫鎖的高性能版本 。其核心理念在于讀取操作無(wú)需加鎖,從而大大提高了讀取的效率 。這是因?yàn)樵诤芏鄨?chǎng)景下,讀取操作不會(huì)對(duì)數(shù)據(jù)的一致性造成影響,所以可以允許多個(gè)讀取操作同時(shí)進(jìn)行 。RCU 的適用場(chǎng)景主要是讀多寫少的情況 。以文件系統(tǒng)為例,在文件系統(tǒng)中,經(jīng)常需要進(jìn)行文件查找等讀取操作,而對(duì)文件的修改相對(duì)較少 。在這種場(chǎng)景下,使用 RCU 可以顯著提升系統(tǒng)的性能 。
當(dāng)進(jìn)行讀取操作時(shí),讀者直接訪問(wèn)共享數(shù)據(jù),不需要加鎖 。而當(dāng)進(jìn)行更新操作時(shí),寫者會(huì)創(chuàng)建新的副本,并在合適的時(shí)間替換舊數(shù)據(jù) 。為了確保數(shù)據(jù)的一致性,RCU 引入了寬限期的概念 。在寬限期內(nèi),所有在更新操作之前開(kāi)始的讀取操作都要完成,之后才能回收舊數(shù)據(jù) 。下面通過(guò)一個(gè)簡(jiǎn)單的代碼示例來(lái)深入理解 RCU 的工作原理:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/rcupdate.h>
#include <linux/slab.h>
// 定義一個(gè)數(shù)據(jù)結(jié)構(gòu)
struct my_data {
int value;
struct rcu_head rcu;
};
// 定義一個(gè)全局指針,用于指向共享數(shù)據(jù)
struct my_data *rcu_pointer;
// 讀者函數(shù)
void reader(void) {
struct my_data *data;
// 標(biāo)記RCU讀臨界區(qū)開(kāi)始
rcu_read_lock();
// 獲取共享數(shù)據(jù)指針
data = rcu_dereference(rcu_pointer);
if (data) {
// 訪問(wèn)數(shù)據(jù)
printk(KERN_INFO "Reader: value = %d\n", data->value);
}
// 標(biāo)記RCU讀臨界區(qū)結(jié)束
rcu_read_unlock();
}
// 寫者函數(shù)
void writer(void) {
struct my_data *new_data, *old_data;
// 分配新的內(nèi)存空間
new_data = kmalloc(sizeof(struct my_data), GFP_KERNEL);
if (!new_data) {
return;
}
new_data->value = 42;
// 更新共享數(shù)據(jù)指針
rcu_assign_pointer(rcu_pointer, new_data);
// 等待所有讀者完成對(duì)舊數(shù)據(jù)的訪問(wèn)
synchronize_rcu();
// 獲取舊數(shù)據(jù)指針
old_data = rcu_dereference_raw(rcu_pointer);
if (old_data) {
// 釋放舊數(shù)據(jù)內(nèi)存空間
kfree(old_data);
}
}
static int __init rcu_demo_init(void) {
// 初始化共享數(shù)據(jù)指針
rcu_pointer = kmalloc(sizeof(struct my_data), GFP_KERNEL);
if (!rcu_pointer) {
return -ENOMEM;
}
rcu_pointer->value = 10;
printk(KERN_INFO "RCU demo initialized\n");
return 0;
}
static void __exit rcu_demo_exit(void) {
struct my_data *data;
// 獲取共享數(shù)據(jù)指針
data = rcu_dereference_raw(rcu_pointer);
if (data) {
// 釋放共享數(shù)據(jù)內(nèi)存空間
kfree(data);
}
printk(KERN_INFO "RCU demo exited\n");
}
module_init(rcu_demo_init);
module_exit(rcu_demo_exit);
MODULE_LICENSE("GPL");
在上述代碼中,reader 函數(shù)表示讀取操作,通過(guò) rcu_read_lock 和 rcu_read_unlock 標(biāo)記 RCU 讀臨界區(qū) 。在臨界區(qū)內(nèi),使用 rcu_dereference 獲取共享數(shù)據(jù)指針,這樣可以確保在讀取過(guò)程中,即使數(shù)據(jù)被更新,也能讀取到有效的數(shù)據(jù) 。writer 函數(shù)表示寫入操作,首先分配新的內(nèi)存空間并初始化數(shù)據(jù),然后使用 rcu_assign_pointer 更新共享數(shù)據(jù)指針 。
接著,通過(guò) synchronize_rcu 等待所有讀者完成對(duì)舊數(shù)據(jù)的訪問(wèn),之后才釋放舊數(shù)據(jù)的內(nèi)存空間 。通過(guò)這個(gè)例子可以清晰地看到 RCU 是如何在保證數(shù)據(jù)一致性的前提下,實(shí)現(xiàn)高效的讀寫操作的 。為了更直觀地理解 RCU 的原理,我們來(lái)看一個(gè)簡(jiǎn)單的示意圖:
|------------------|
| 讀者1 |
|------------------|
| 讀者2 |
|------------------|
| 寫者 |
|------------------|
| 寬限期 |
|------------------|
在這個(gè)示意圖中,讀者 1 和讀者 2 在讀取數(shù)據(jù)時(shí)不需要加鎖,可以同時(shí)進(jìn)行 。當(dāng)寫者進(jìn)行數(shù)據(jù)更新時(shí),會(huì)創(chuàng)建新的數(shù)據(jù)副本并更新指針 。然后進(jìn)入寬限期,在寬限期內(nèi),等待所有讀者完成對(duì)舊數(shù)據(jù)的訪問(wèn) 。只有當(dāng)寬限期結(jié)束后,才會(huì)回收舊數(shù)據(jù) 。這樣就實(shí)現(xiàn)了在多讀少寫場(chǎng)景下的高效同步 。
八、Linux 同步管理工具
在 Linux 系統(tǒng)中,有許多實(shí)用的同步管理工具,其中 rsync 是一款備受青睞的強(qiáng)大工具 。rsync 的全稱為 “remote synchronize”,即遠(yuǎn)程同步 。它不僅支持本地文件系統(tǒng)之間的同步,還能通過(guò)網(wǎng)絡(luò)進(jìn)行遠(yuǎn)程文件同步,在數(shù)據(jù)備份、鏡像服務(wù)器搭建等場(chǎng)景中應(yīng)用廣泛 。
rsync 的核心優(yōu)勢(shì)在于其高效的增量同步功能 。它采用了獨(dú)特的 “Rsync 算法”,在同步文件時(shí),并非每次都傳輸整個(gè)文件,而是僅傳送源文件和目標(biāo)文件之間的差異部分 。這一特性使得 rsync 在處理大文件或大量文件同步時(shí),能夠顯著節(jié)省帶寬和時(shí)間,大大提高了同步效率 。例如,在一個(gè)包含眾多文件的大型項(xiàng)目中,若僅對(duì)其中部分文件進(jìn)行了修改,使用 rsync 進(jìn)行同步時(shí),它只會(huì)傳輸這些被修改的文件部分,而不會(huì)重復(fù)傳輸未改變的文件內(nèi)容 。
rsync 的使用方式十分靈活,支持多種運(yùn)行模式 。它可以通過(guò) rsh、ssh 等遠(yuǎn)程 shell 方式進(jìn)行文件傳輸,這種方式利用了系統(tǒng)已有的安全連接機(jī)制,確保數(shù)據(jù)傳輸?shù)陌踩?。同時(shí),rsync 也能以 daemon 模式運(yùn)行,在這種模式下,rsync server 會(huì)打開(kāi)一個(gè) 873 端口,等待客戶端連接 。連接時(shí),服務(wù)器會(huì)檢查口令是否相符,通過(guò)驗(yàn)證后即可開(kāi)始文件傳輸 。這種模式適用于需要長(zhǎng)期提供同步服務(wù)的場(chǎng)景,比如企業(yè)內(nèi)部的文件服務(wù)器 。
下面來(lái)看一些 rsync 的基本語(yǔ)法和常見(jiàn)選項(xiàng)用法:
①本地文件同步
將目錄 source_dir 同步到 destination_dir,使用歸檔模式并顯示詳細(xì)輸出信息:
rsync -av source_dir/ destination_dir/
在這個(gè)命令中,-a表示歸檔模式,它等同于-rlptgoD,可以保留文件的權(quán)限、符號(hào)鏈接、時(shí)間戳等屬性 。-v表示顯示詳細(xì)的傳輸信息 。source_dir/表示同步目錄下的所有內(nèi)容(注意尾部斜杠的作用,加上斜杠表示只同步目錄內(nèi)的內(nèi)容,不包含該目錄本身;不加斜杠則會(huì)包含整個(gè)目錄一起同步) 。destination_dir/為目標(biāo)目錄 。
②遠(yuǎn)程文件同步(通過(guò) SSH)
將本地目錄/home/user/documents/同步到遠(yuǎn)程服務(wù)器remote_host的/backup/documents/目錄:
rsync -av -e ssh /home/user/documents/ user@remote_host:/backup/documents/
這里的-e ssh表示使用 SSH 作為遠(yuǎn)程傳輸協(xié)議,通過(guò) SSH 連接到遠(yuǎn)程主機(jī)進(jìn)行文件同步,這對(duì)于安全的遠(yuǎn)程同步非常有用 。
③增量同步并刪除目標(biāo)中多余的文件
使/backup/documents/與/home/user/documents/完全一致,刪除/backup/documents/中多余的文件:
rsync -av --delete /home/user/documents/ /backup/documents/
--delete選項(xiàng)會(huì)在目標(biāo)路徑中刪除源路徑不存在的文件,確保目標(biāo)與源保持一致 。
④壓縮傳輸
在傳輸過(guò)程中壓縮數(shù)據(jù),減少帶寬占用,將本地文件同步到遠(yuǎn)程主機(jī):
rsync -avz /home/user/documents/ user@remote_host:/backup/documents/
-z選項(xiàng)表示在傳輸過(guò)程中壓縮數(shù)據(jù) 。