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

一文吃透Linux同步管理,讓你的系統穩如泰山

系統 Linux
Linux 同步管理機制通過一系列的策略和方法,協調多個進程或線程對共享資源的訪問,防止出現競態條件、死鎖等問題。它就像是一位經驗豐富的交通警察,在復雜的交通路口指揮車輛有序通行,確保每個 “車輛”(進程或線程)都能安全、高效地到達目的地,從而維護數據的一致性和系統的整體運行秩序。

在當今的計算機世界中,多任務處理和多核處理器已成為標配。無論是在服務器端處理大量并發請求,還是在嵌入式系統中高效運行多個任務,Linux 操作系統都扮演著至關重要的角色。而在這復雜的多任務、多核環境下,Linux 的同步管理機制就如同精密儀器中的校準裝置,對數據一致性和系統穩定性起著關鍵作用。

想象一下,多個進程或線程如同忙碌的工人,同時對共享資源進行操作。如果沒有有效的同步管理,就好比施工現場沒有指揮,工人各自為政,必然會導致資源競爭、數據不一致等問題。比如在一個多線程的數據庫應用中,若多個線程同時對數據庫進行讀寫操作而沒有同步機制,可能會導致數據失、臟讀等嚴重問題,進而影響整個系統的正常運行 。再比如在服務器端,多個用戶的請求并發到達,若不能對這些請求進行有序處理,很可能會導致服務器響應混亂,甚至崩潰。

Linux 同步管理機制通過一系列的策略和方法,協調多個進程或線程對共享資源的訪問,防止出現競態條件、死鎖等問題。它就像是一位經驗豐富的交通警察,在復雜的交通路口指揮車輛有序通行,確保每個 “車輛”(進程或線程)都能安全、高效地到達目的地,從而維護數據的一致性和系統的整體運行秩序。在多核和多處理器系統中,良好的同步機制更是能夠高效地管理資源訪問,避免緩存一致性問題和偽共享問題,對提升系統性能起著至關重要的作用。接下來,讓我們深入探討 Linux 同步管理的具體方法和實現。

一、Linux 同步管理基礎概念

在 Linux 系統中,同步機制是用于實現控制多個執行路徑按照一定的規則或順序訪問某些系統資源的機制 。這里所說的執行路徑,指的是在 CPU 上運行的代碼流。我們知道,CPU 調度的最小單位是線程,它既可以是用戶態線程,也可以是內核線程,甚至中斷服務程序也包含在內。

簡單來說,同步機制就像是一場精心安排的音樂會,每個演奏者(執行路徑)都需要按照既定的樂譜(規則)在合適的時間演奏,以確保整個音樂(系統資源訪問)和諧有序 。例如,在一個多線程的文件讀寫程序中,多個線程都可能嘗試對同一個文件進行讀寫操作。如果沒有同步機制,這些線程可能會同時修改文件內容,導致數據混亂。而通過同步機制,我們可以確保在同一時刻只有一個線程能夠對文件進行寫入操作,其他線程需要等待,從而保證文件數據的一致性 。

(1)并發與競態

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

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

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

總結如下圖:

圖片圖片

(2)中斷與搶占

中斷是指計算機在執行程序的過程中,當出現某些緊急事件時,暫時停止當前程序的執行,轉去處理這些事件,處理完畢后再返回原來的程序繼續執行 。比如,當有新的數據到達網絡接口卡時,會產生一個中斷信號,通知 CPU 進行處理 。搶占則屬于進程調度的概念,Linux 內核從 2.6 版本開始支持搶占調度。通俗地講,搶占就是一個正在 CPU 上愉快運行的任務(可以是用戶態進程,也可以是內核線程)被另一個通常是更高優先級的任務奪去 CPU 執行權 。中斷和搶占之間有著密切的關系,搶占依賴中斷 。

如果當前 CPU 禁止了本地中斷,那么也就意味著禁止了本 CPU 上的搶占。但反過來,禁掉搶占并不影響中斷 。例如,在一個實時控制系統中,可能會有一些高優先級的中斷任務需要立即處理。當這些中斷發生時,CPU 會暫停當前正在執行的任務,轉而執行中斷處理程序,這就體現了中斷對任務執行的影響。而搶占機制則確保了高優先級的任務能夠及時獲得 CPU 資源,提高系統的響應性能 。

用戶態搶占:

  • 從系統調用返回用戶空間時;
  • 從中斷(異常)處理程序返回用戶空間時。

內核態搶占:

  1. 當一個中斷處理程序退出,返回到內核態時;
  2. task 顯式調用 schedule();
  3. task 發生阻塞(此時由調度器完成調度)。

在 Linux 系統中,為了確保多任務環境下數據的一致性和系統的穩定性,提供了多種同步管理方法,每種方法都有其獨特的應用場景和實現原理。下面將詳細介紹原子操作、內存屏障、自旋鎖、信號量、互斥鎖和 RCU(讀 - 復制 - 更新)等常見的同步機制。

二、原子操作(Atomic Operation)

所謂原子操作,就是該操作絕不會在執行完畢前被任何其他任務或事件打斷,也就說,它的最小的執行單位,不可能有比它更小的執行單位,因此這里的原子實際是使用了物理學里的物質微粒的概念。

原子操作需要硬件的支持,因此是架構相關的,其API和原子類型的定義都定義在內核源碼樹的include/asm/atomic.h文件中,它們都使用匯編語言實現,因為C語言并不能實現這樣的操作。

原子操作主要用于實現資源計數,很多引用計數(refcnt)就是通過原子操作實現的。原子類型定義如下:

typedef struct { 
volatile int counter; 
} atomic_t;

volatile修飾字段告訴gcc不要對該類型的數據做優化處理,對它的訪問都是對內存的訪問,而不是對寄存器的訪問。

原子操作API包括:

  1. tomic_read(atomic_t * v);該函數對原子類型的變量進行原子讀操作,它返回原子類型的變量v的值。
  2. atomic_set(atomic_t * v, int i);該函數設置原子類型的變量v的值為i。
  3. void atomic_add(int i, atomic_t *v);該函數給原子類型的變量v增加值i。
  4. atomic_sub(int i, atomic_t *v);該函數從原子類型的變量v中減去i。
  5. int atomic_sub_and_test(int i, atomic_t *v);該函數從原子類型的變量v中減去i,并判斷結果是否為0,如果為0,返回真,否則返回假。
  6. void atomic_inc(atomic_t *v);該函數對原子類型變量v原子地增加1。
  7. void atomic_dec(atomic_t *v);該函數對原子類型的變量v原子地減1。
  8. int atomic_dec_and_test(atomic_t *v);該函數對原子類型的變量v原子地減1,并判斷結果是否為0,如果為0,返回真,否則返回假。
  9. int atomic_inc_and_test(atomic_t *v);該函數對原子類型的變量v原子地增加1,并判斷結果是否為0,如果為0,返回真,否則返回假。
  10. int atomic_add_negative(int i, atomic_t *v);該函數對原子類型的變量v原子地增加I,并判斷結果是否為負數,如果是,返回真,否則返回假。
  11. int atomic_add_return(int i, atomic_t *v);該函數對原子類型的變量v原子地增加i,并且返回指向v的指針。
  12. int atomic_sub_return(int i, atomic_t *v);該函數從原子類型的變量v中減去i,并且返回指向v的指針。
  13. int atomic_inc_return(atomic_t * v);該函數對原子類型的變量v原子地增加1并且返回指向v的指針。
  14. 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實現)。

三、內存屏障(Memory Barrier)

內存屏障是一種重要的同步機制,它主要用于解決內存亂序訪問的問題 。在 ARM 架構中,存在 3 類內存屏障指令,分別是數據存儲器隔離(Data Memory Barrier,DMB)、數據同步隔離(Data Synchronization Barrier,DSB)和指令同步隔離(Instruction Synchronization Barrier,ISB) 。DMB 指令保證僅當所有在它前面的存儲器訪問操作都執行完畢后,才提交在它后面的存儲器訪問操作 。也就是說,DMB 指令確保了在它之前和之后的內存訪問操作的順序性,但不保證內存訪問的完成順序 。

DSB 指令比 DMB 指令更加嚴格,僅當所有在它前面的存儲器訪問操作都執行完畢后,才會執行在它后面的指令 。這意味著,任何指令都要等待 DSB 前面的存儲訪問完成,包括緩存操作等 。ISB 指令則是最嚴格的同步指令,它會清洗流水線,以保證所有它前面的指令都執行完畢之后,才執行它后面的指令 。在 Linux 內核中,也提供了一些內存屏障函數,如 barrier、mb、rmb、wmb 等 。

barrier 函數用于阻止編譯器對內存訪問指令進行重排 ,它告訴編譯器不要為了性能優化而將 barrier 前后的代碼順序打亂 。mb 函數則同時具備讀內存屏障和寫內存屏障的功能,它確保了在 mb 之前的內存訪問操作都完成后,才會執行在 mb 之后的內存訪問操作 。rmb 函數是讀內存屏障,保證了在 rmb 之前的讀操作都完成后,才會執行在 rmb 之后的讀操作 。wmb 函數是寫內存屏障,確保了在 wmb 之前的寫操作都完成后,才會執行在 wmb 之后的寫操作 。

這些內存屏障函數在多處理器系統中,對于保證內存訪問的順序性和數據的一致性起著至關重要的作用 。例如,在多線程編程中,當一個線程修改了共享變量的值后,使用 wmb 函數可以確保其他線程能夠正確地讀取到這個修改后的值,避免了由于內存亂序訪問而導致的數據不一致問題 。

在 Linux 系統中,根據其作用和功能,內存屏障主要分為以下三種類型:

①全屏障(Full Barrier)

全屏障,也稱作強內存屏障 ,它的功能最為強大。全屏障可以阻止屏障兩邊的讀寫操作進行重排序,確保在屏障之前的所有讀寫操作,都在屏障之后的讀寫操作之前完成。在 x86 架構中,全屏障的實現指令是mfence 。當 CPU 執行到mfence指令時,會將之前所有的存儲和加載操作都按順序完成,才會繼續執行后面的指令。例如:

// 線程1
x = 1;  // 寫操作1
mfence();  // 全屏障
y = 2;  // 寫操作2

// 線程2
if (y == 2) {  // 讀操作1
    assert(x == 1);  // 讀操作2
}

在這個例子中,由于mfence全屏障的存在,線程 1 中x = 1的寫操作一定會在線程 2 讀取y的值之前完成,從而保證了線程 2 在讀取y為 2 時,x的值也已經被正確地更新為 1,避免了由于指令重排序導致的數據不一致問題。全屏障在需要嚴格保證內存操作順序的場景中非常有用,比如在實現一些關鍵的同步機制或者對共享資源的復雜操作時。

②讀取屏障(Read/Load Barrier)

讀取屏障的作用是確保在該屏障之前的所有讀取操作,必須在該屏障之后的讀取操作之前完成 。它主要用于控制讀取操作的順序,防止讀取操作的重排序。在 x86 架構中,讀取屏障對應的指令是lfence 。例如:

// 線程1
int a = shared_variable1;  // 讀操作A
lfence();  // 讀取屏障
int b = shared_variable2;  // 讀操作B

在上述代碼中,lfence讀取屏障保證了讀操作 A 一定會在讀操作 B 之前完成。即使處理器可能有優化策略,也不能將讀操作 B 提前到讀操作 A 之前執行。讀取屏障在多線程環境中,當讀取操作的順序對程序邏輯有重要影響時非常關鍵。比如在一些依賴于特定讀取順序的算法實現中,或者在讀取共享狀態變量時,為了確保獲取到正確的狀態信息,就需要使用讀取屏障來保證讀取操作的順序性 。

③寫入屏障(Write/Store Barrier)

一個寫內存屏障可以提供這樣的保證,站在系統中的其它組件的角度來看,在屏障之前的寫操作看起來將在屏障后的寫操作之前發生。

如果映射到上面的例子來說,首先,寫內存屏障會對處理器指令重排序做出一些限制,也就是在寫內存屏障之前的寫入指令一定不會被重排序到寫內存屏障之后的寫入指令之后。其次,在執行寫內存屏障之后的寫入指令之前,一定要保證清空當前CPU存儲緩沖中的所有寫操作,將它們全部“提交”到緩存中。這樣的話系統中的其它組件(包括別的CPU),就可以保證在看到寫內存屏障之后的寫入數據之前先看到寫內存屏障之前的寫入數據。

圖片圖片

寫入屏障用于確保在該屏障之前的所有寫入操作,必須在該屏障之后的寫入操作之前完成 。它主要關注寫入操作的順序,防止寫入操作的重排序。在 x86 架構中,寫入屏障的指令是sfence 。例如:

// 線程1
shared_variable1 = 10;  // 寫操作C
sfence();  // 寫入屏障
shared_variable2 = 20;  // 寫操作D

這里,sfence寫入屏障確保了寫操作 C 一定會在寫操作 D 之前完成。無論編譯器如何優化或者處理器如何執行指令,都不會改變這兩個寫操作的順序。寫入屏障在多線程同時修改共享數據時非常重要,它可以保證數據的更新按照預期的順序進行,避免由于寫入順序混亂導致的數據不一致問題。比如在更新一些關聯的共享變量時,使用寫入屏障可以確保先更新的變量對其他線程可見后,再進行后續變量的更新 。

四、自旋鎖(spinlock)

自旋鎖是一種用于多線程同步的機制,其作用是保證在同一時刻只有一個執行單元能夠進入臨界區,從而確保復雜操作不受其他執行單元的干擾順利完成 。自旋鎖的結構定義較為復雜,在 Linux 內核中,相關結構體層層嵌套 。以較新版本內核為例,spinlock_t 通過 union 關聯到 raw_spinlock,而 raw_spinlock 又包含 arch_spinlock_t 。arch_spinlock_t 利用 union 將 u32 類型的 slock 與 struct __raw_tickets 結構體相關聯,結構體中的 u16 類型的 owner 和 next 成員用于實現 FIFO(先進先出)機制 。

當一個線程嘗試獲取自旋鎖時,如果鎖已經被其他線程占用,它不會立即進入睡眠狀態,而是在一個循環中不斷檢查鎖是否可用,即所謂的 “自旋” 。這種方式避免了線程切換的開銷,特別適合短時間內的鎖競爭場景 。自旋鎖的 FIFO 機制保證了等待鎖的線程按照先來后到的順序獲取鎖,從而實現了公平性 。例如,多個線程同時請求獲取自旋鎖,先請求的線程會先獲取到鎖,后請求的線程則需要等待 。

然而,自旋鎖也存在一些問題,比如在鎖被長時間持有時,等待的線程會持續自旋,浪費 CPU 資源 。為了應對中斷死鎖問題,自旋鎖提供了不同類型的操作函數 。如 spin_lock_irqsave 在獲取鎖之前會保存當前中斷狀態并禁用中斷,spin_unlock_irqrestore 則會恢復之前保存的中斷狀態 。這樣可以防止在持有自旋鎖期間發生中斷,從而避免死鎖的發生 。

自旋鎖的API有:

  1. spin_lock_init(x)該宏用于初始化自旋鎖x。自旋鎖在真正使用前必須先初始化。該宏用于動態初始化。
  2. DEFINE_SPINLOCK(x)該宏聲明一個自旋鎖x并初始化它。該宏在2.6.11中第一次被定義,在先前的內核中并沒有該宏。
  3. SPIN_LOCK_UNLOCKED該宏用于靜態初始化一個自旋鎖。
  4. DEFINE_SPINLOCK(x)等同于spinlock_t x = SPIN_LOCK_UNLOCKEDspin_is_locked(x)該宏用于判斷自旋鎖x是否已經被某執行單元保持(即被鎖),如果是,返回真,否則返回假。
  5. spin_unlock_wait(x)該宏用于等待自旋鎖x變得沒有被任何執行單元保持,如果沒有任何執行單元保持該自旋鎖,該宏立即返回,否則將循環在那里,直到該自旋鎖被保持者釋放。
  6. spin_trylock(lock)該宏盡力獲得自旋鎖lock,如果能立即獲得鎖,它獲得鎖并返回真,否則不能立即獲得鎖,立即返回假。它不會自旋等待lock被釋放。
  7. spin_lock(lock)該宏用于獲得自旋鎖lock,如果能夠立即獲得鎖,它就馬上返回,否則,它將自旋在那里,直到該自旋鎖的保持者釋放,這時,它獲得鎖并返回。總之,只有它獲得鎖才返回。
  8. spin_lock_irqsave(lock, flags)該宏獲得自旋鎖的同時把標志寄存器的值保存到變量flags中并失效本地中斷。
  9. spin_lock_irq(lock)該宏類似于spin_lock_irqsave,只是該宏不保存標志寄存器的值。
  10. spin_lock_bh(lock)該宏在得到自旋鎖的同時失效本地軟中斷。
  11. spin_unlock(lock)該宏釋放自旋鎖lock,它與spin_trylock或spin_lock配對使用。如果spin_trylock返回假,表明沒有獲得自旋鎖,因此不必使用spin_unlock釋放。
  12. spin_unlock_irqrestore(lock, flags)該宏釋放自旋鎖lock的同時,也恢復標志寄存器的值為變量flags保存的值。它與spin_lock_irqsave配對使用。
  13. spin_unlock_irq(lock)該宏釋放自旋鎖lock的同時,也使能本地中斷。它與spin_lock_irq配對應用。
  14. spin_unlock(lock)該宏釋放自旋鎖lock,它與spin_trylock或spin_lock配對使用。如果spin_trylock返回假,表明沒有獲得自旋鎖,因此不必使用spin_unlock釋放。
  15. spin_unlock_irqrestore(lock, flags)該宏釋放自旋鎖lock的同時,也恢復標志寄存器的值為變量flags保存的值。它與spin_lock_irqsave配對使用。
  16. spin_unlock_irq(lock)該宏釋放自旋鎖lock的同時,也使能本地中斷。它與spin_lock_irq配對應用。
  17. spin_unlock_bh(lock)該宏釋放自旋鎖lock的同時,也使能本地的軟中斷。它與spin_lock_bh配對使用。
  18. spin_trylock_irqsave(lock, flags) 該宏如果獲得自旋鎖lock,它也將保存標志寄存器的值到變量flags中,并且失效本地中斷,如果沒有獲得鎖,它什么也不做。因此如果能夠立即獲得鎖,它等同于spin_lock_irqsave,如果不能獲得鎖,它等同于spin_trylock。如果該宏獲得自旋鎖lock,那需要使用spin_unlock_irqrestore來釋放。
  19. spin_unlock_bh(lock)該宏釋放自旋鎖lock的同時,也使能本地的軟中斷。它與spin_lock_bh配對使用。
  20. spin_trylock_irqsave(lock, flags) 該宏如果獲得自旋鎖lock,它也將保存標志寄存器的值到變量flags中,并且失效本地中斷,如果沒有獲得鎖,它什么也不做。因此如果能夠立即獲得鎖,它等同于spin_lock_irqsave,如果不能獲得鎖,它等同于spin_trylock。如果該宏獲得自旋鎖lock,那需要使用spin_unlock_irqrestore來釋放。
  21. 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上軟中斷或中斷對共享資源的非同步訪問。

五、信號量(semaphore)

Linux內核的信號量在概念和原理上與用戶態的System V的IPC機制信號量是一樣的,但是它絕不可能在內核之外使用,因此它與System V的IPC機制信號量毫不相干。

信號量在創建時需要設置一個初始值,表示同時可以有幾個任務可以訪問該信號量保護的共享資源,初始值為1就變成互斥鎖(Mutex),即同時只能有一個任務可以訪問信號量保護的共享資源。

一個任務要想訪問共享資源,首先必須得到信號量,獲取信號量的操作將把信號量的值減1,若當前信號量的值為負數,表明無法獲得信號量,該任務必須掛起在該信號量的等待隊列等待該信號量可用;若當前信號量的值為非負數,表示可以獲得信號量,因而可以立刻訪問被該信號量保護的共享資源。

當任務訪問完被信號量保護的共享資源后,必須釋放信號量,釋放信號量通過把信號量的值加1實現,如果信號量的值為非正數,表明有任務等待當前信號量,因此它也喚醒所有等待該信號量的任務。

信號量的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進行保護。

六、互斥鎖(Mutex)

互斥鎖在功能上和信號量 count 為 1 時的行為較為相似,都用于保證在同一時刻只有一個線程能夠訪問共享資源 。然而,由于性能等方面的原因,互斥鎖重新進行了實現 。在 Linux 內核中,互斥鎖的結構定義包含多個關鍵成員 。其中,owner 用于記錄當前持有鎖的線程;count 用于表示鎖的狀態,0 表示鎖被占用,1 表示鎖可用;wait_list 同樣是一個等待隊列,用于存放等待獲取鎖的線程 。

互斥鎖具有一些獨特的特點,例如在競爭不激烈的情況下,它會切換為自旋等待 。這是因為在短時間內,鎖很可能會被釋放,自旋等待可以避免線程切換的開銷 ?;コ怄i相關的 API 主要有 mutex_lock、mutex_unlock 和 mutex_trylock 。mutex_lock 用于獲取互斥鎖,如果鎖已被持有,當前線程將被阻塞;mutex_unlock 用于釋放互斥鎖,允許其他線程獲??;mutex_trylock 則是嘗試獲取互斥鎖,如果鎖被持有,它會立即返回失敗,而不會阻塞線程 。在實際應用中,比如在一個多線程的數據庫連接池管理中,互斥鎖可以用于保護連接池的共享資源,確保每次只有一個線程能夠獲取或釋放數據庫連接 。

互斥鎖的工作原理基于操作系統提供的原子操作和線程調度機制。當一個線程執行到需要訪問共享資源的代碼段時,它會調用互斥鎖的加鎖函數(如std::mutex的lock方法)。此時,互斥鎖會檢查自身的狀態,如果當前處于未鎖定狀態,它會將自己標記為已鎖定,并允許該線程進入臨界區訪問共享資源。這個標記過程是通過原子操作實現的,確保在多線程環境下不會出現競爭條件。例如,在一個多線程的文件讀寫操作中,當一個線程獲取到互斥鎖后,就可以安全地對文件進行寫入,避免其他線程同時寫入導致文件內容混亂。

如果互斥鎖已經被其他線程鎖定,那么調用加鎖函數的線程會被操作系統掛起,放入等待隊列中,進入阻塞狀態。此時,該線程會讓出 CPU 資源,以便其他線程能夠繼續執行,避免了無效的 CPU 占用。就像在一條單行道上,當一輛車已經在行駛時,其他車輛只能在路口等待,直到前面的車通過。

當持有鎖的線程完成對共享資源的訪問后,它會調用互斥鎖的解鎖函數(如std::mutex的unlock方法) 。解鎖操作會將互斥鎖的狀態標記為未鎖定,并從等待隊列中喚醒一個等待的線程(如果有線程在等待)。被喚醒的線程會重新競爭 CPU 資源,當它獲得 CPU 時間片后,會再次嘗試獲取互斥鎖。一旦獲取成功,就可以進入臨界區訪問共享資源。例如,在一個多線程的數據庫操作中,當一個線程完成對數據庫的更新操作并釋放互斥鎖后,等待隊列中的另一個線程就有機會獲取鎖,進行查詢或其他操作。

七、RCU(Read-Copy-Update)

RCU 是一種高效的讀寫同步機制,可看作是讀寫鎖的高性能版本 。其核心理念在于讀取操作無需加鎖,從而大大提高了讀取的效率 。這是因為在很多場景下,讀取操作不會對數據的一致性造成影響,所以可以允許多個讀取操作同時進行 。RCU 的適用場景主要是讀多寫少的情況 。以文件系統為例,在文件系統中,經常需要進行文件查找等讀取操作,而對文件的修改相對較少 。在這種場景下,使用 RCU 可以顯著提升系統的性能 。

當進行讀取操作時,讀者直接訪問共享數據,不需要加鎖 。而當進行更新操作時,寫者會創建新的副本,并在合適的時間替換舊數據 。為了確保數據的一致性,RCU 引入了寬限期的概念 。在寬限期內,所有在更新操作之前開始的讀取操作都要完成,之后才能回收舊數據 。下面通過一個簡單的代碼示例來深入理解 RCU 的工作原理:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/rcupdate.h>
#include <linux/slab.h>

// 定義一個數據結構
struct my_data {
    int value;
    struct rcu_head rcu;
};

// 定義一個全局指針,用于指向共享數據
struct my_data *rcu_pointer;

// 讀者函數
void reader(void) {
    struct my_data *data;
    // 標記RCU讀臨界區開始
    rcu_read_lock();
    // 獲取共享數據指針
    data = rcu_dereference(rcu_pointer);
    if (data) {
        // 訪問數據
        printk(KERN_INFO "Reader: value = %d\n", data->value);
    }
    // 標記RCU讀臨界區結束
    rcu_read_unlock();
}

// 寫者函數
void writer(void) {
    struct my_data *new_data, *old_data;
    // 分配新的內存空間
    new_data = kmalloc(sizeof(struct my_data), GFP_KERNEL);
    if (!new_data) {
        return;
    }
    new_data->value = 42;
    // 更新共享數據指針
    rcu_assign_pointer(rcu_pointer, new_data);
    // 等待所有讀者完成對舊數據的訪問
    synchronize_rcu();
    // 獲取舊數據指針
    old_data = rcu_dereference_raw(rcu_pointer);
    if (old_data) {
        // 釋放舊數據內存空間
        kfree(old_data);
    }
}

static int __init rcu_demo_init(void) {
    // 初始化共享數據指針
    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;
    // 獲取共享數據指針
    data = rcu_dereference_raw(rcu_pointer);
    if (data) {
        // 釋放共享數據內存空間
        kfree(data);
    }
    printk(KERN_INFO "RCU demo exited\n");
}

module_init(rcu_demo_init);
module_exit(rcu_demo_exit);
MODULE_LICENSE("GPL");

在上述代碼中,reader 函數表示讀取操作,通過 rcu_read_lock 和 rcu_read_unlock 標記 RCU 讀臨界區 。在臨界區內,使用 rcu_dereference 獲取共享數據指針,這樣可以確保在讀取過程中,即使數據被更新,也能讀取到有效的數據 。writer 函數表示寫入操作,首先分配新的內存空間并初始化數據,然后使用 rcu_assign_pointer 更新共享數據指針 。

接著,通過 synchronize_rcu 等待所有讀者完成對舊數據的訪問,之后才釋放舊數據的內存空間 。通過這個例子可以清晰地看到 RCU 是如何在保證數據一致性的前提下,實現高效的讀寫操作的 。為了更直觀地理解 RCU 的原理,我們來看一個簡單的示意圖:

|------------------|
| 讀者1            |
|------------------|
| 讀者2            |
|------------------|
| 寫者             |
|------------------|
| 寬限期           |
|------------------|

在這個示意圖中,讀者 1 和讀者 2 在讀取數據時不需要加鎖,可以同時進行 。當寫者進行數據更新時,會創建新的數據副本并更新指針 。然后進入寬限期,在寬限期內,等待所有讀者完成對舊數據的訪問 。只有當寬限期結束后,才會回收舊數據 。這樣就實現了在多讀少寫場景下的高效同步 。

八、Linux 同步管理工具

在 Linux 系統中,有許多實用的同步管理工具,其中 rsync 是一款備受青睞的強大工具 。rsync 的全稱為 “remote synchronize”,即遠程同步 。它不僅支持本地文件系統之間的同步,還能通過網絡進行遠程文件同步,在數據備份、鏡像服務器搭建等場景中應用廣泛 。

rsync 的核心優勢在于其高效的增量同步功能 。它采用了獨特的 “Rsync 算法”,在同步文件時,并非每次都傳輸整個文件,而是僅傳送源文件和目標文件之間的差異部分 。這一特性使得 rsync 在處理大文件或大量文件同步時,能夠顯著節省帶寬和時間,大大提高了同步效率 。例如,在一個包含眾多文件的大型項目中,若僅對其中部分文件進行了修改,使用 rsync 進行同步時,它只會傳輸這些被修改的文件部分,而不會重復傳輸未改變的文件內容 。

rsync 的使用方式十分靈活,支持多種運行模式 。它可以通過 rsh、ssh 等遠程 shell 方式進行文件傳輸,這種方式利用了系統已有的安全連接機制,確保數據傳輸的安全性 。同時,rsync 也能以 daemon 模式運行,在這種模式下,rsync server 會打開一個 873 端口,等待客戶端連接 。連接時,服務器會檢查口令是否相符,通過驗證后即可開始文件傳輸 。這種模式適用于需要長期提供同步服務的場景,比如企業內部的文件服務器 。

下面來看一些 rsync 的基本語法和常見選項用法:

①本地文件同步

將目錄 source_dir 同步到 destination_dir,使用歸檔模式并顯示詳細輸出信息:

rsync -av source_dir/ destination_dir/

在這個命令中,-a表示歸檔模式,它等同于-rlptgoD,可以保留文件的權限、符號鏈接、時間戳等屬性 。-v表示顯示詳細的傳輸信息 。source_dir/表示同步目錄下的所有內容(注意尾部斜杠的作用,加上斜杠表示只同步目錄內的內容,不包含該目錄本身;不加斜杠則會包含整個目錄一起同步) 。destination_dir/為目標目錄 。

②遠程文件同步(通過 SSH)

將本地目錄/home/user/documents/同步到遠程服務器remote_host的/backup/documents/目錄:

rsync -av -e ssh /home/user/documents/ user@remote_host:/backup/documents/

這里的-e ssh表示使用 SSH 作為遠程傳輸協議,通過 SSH 連接到遠程主機進行文件同步,這對于安全的遠程同步非常有用 。

③增量同步并刪除目標中多余的文件

使/backup/documents/與/home/user/documents/完全一致,刪除/backup/documents/中多余的文件:

rsync -av --delete /home/user/documents/ /backup/documents/

--delete選項會在目標路徑中刪除源路徑不存在的文件,確保目標與源保持一致 。

④壓縮傳輸

在傳輸過程中壓縮數據,減少帶寬占用,將本地文件同步到遠程主機:

rsync -avz /home/user/documents/ user@remote_host:/backup/documents/

-z選項表示在傳輸過程中壓縮數據 。

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

2021-09-10 16:10:21

panda透視表語言

2025-04-01 07:50:00

Dinero.js前端開發

2022-09-29 10:26:59

iOSScaffoldflutter

2025-05-21 09:32:28

2023-06-16 07:17:14

固態硬盤機械硬盤企業

2021-01-21 14:26:56

大數據互聯網大數據應用

2025-06-04 03:21:00

RAGRetrievalGeneratio

2023-12-15 15:55:24

Linux線程同步

2024-09-26 07:27:27

2023-03-10 22:08:20

2024-08-09 08:41:14

2021-05-12 18:22:36

Linux 內存管理

2025-04-09 05:22:00

2021-04-27 11:28:21

React.t事件元素

2024-09-18 13:57:15

2018-02-02 11:17:42

IaaSPaaSSaaS

2018-05-21 10:20:22

人工智能機器學習神經網絡

2021-10-20 08:49:30

Vuexvue.js狀態管理模式

2022-03-18 09:45:43

Git分支Linux
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产一区二区影院 | 国产精品久久久久久亚洲调教 | 国产羞羞视频在线观看 | 欧美激情视频网站 | 性色综合 | 欧美成人精品一区二区男人看 | 欧洲一区二区视频 | 91久久精品一区二区二区 | 日韩中文字幕区 | 久久久久久久久久久久91 | 成人av一区二区在线观看 | 中文字幕久久精品 | 久久久久国产精品一区二区 | 久久国产综合 | 人人做人人澡人人爽欧美 | 免费av直接看 | 国产日韩亚洲欧美 | 黄色永久免费 | 秋霞av国产精品一区 | 国内久久 | 久久久久久91香蕉国产 | 日韩激情在线 | 日本不卡在线视频 | 亚洲欧美日韩中文在线 | 欧美久久久久久久 | 干干天天 | 亚洲欧美日韩精品久久亚洲区 | 亚洲综合在 | 亚洲 中文 欧美 日韩 在线观看 | 性精品| 高清色| 天天久久 | 欧美亚洲另类在线 | 欧美一级网站 | 欧美亚洲国产一区 | 欧美一区二区成人 | 特黄特色大片免费视频观看 | 99久久免费精品国产男女高不卡 | 成人无遮挡毛片免费看 | 国产亚洲一区精品 | 精品欧美一区二区三区久久久 |