首页 > 代码库 > 第十章 內核同步的方法

第十章 內核同步的方法

原子操作

1. 原子操作可以保證指令以原子的方式執行——執行過程不被打斷。

2. 兩個原子操作絕對不可能併發地訪問同一個變量。大多數體繫結構會提供支持原子操作的簡單算數指令,即使沒有,也會爲單步執行提供鎖內存總線的指令,確保其他改變內存的操作不會同時發生。

3. 原子操作分爲兩種,一種是針對整數的,另一種是針對單獨的位。

4. 針對單獨位的操作,提供了一套原子操作和一套非原子操作,非原子操作函數的特點函數名字前綴多了兩個下劃線。比如test_bit()對應的非原子形式是__test_bit()。

5. 在編寫代碼時,能使用原子操作時,就儘量不要使用複雜的加鎖機制。這樣給系統帶來的開銷小,對高速緩存行(cache-line)的影響也小。

6. 原子性和順序性

  • 原子性:如一個整數的初始值是42,然後又置爲365,那麼讀取這個整數肯定會返回42或者365,而絕不會是二者的混合。即讀總是返回一個完整的字,這或者發生在寫操作前,或者之後,絕不可能發生在寫的過程中。原子性確保指令執行期間不被打斷,要麼全部執行完,要麼根本不執行。

  • 順序性:比如要求讀必須在特定的寫之前完成。即確保即使兩條或多條指令出現再獨立的執行線程中,甚至獨立的處理器上,它們本該的執行順序卻依然要保持。通過屏障(barrier)指令實現。

7. atomic_t類型即便在64位體繫結構下也是32位的,若要使用64爲的原子變量,則要使用atomic64_t。但是多數的32位體系機構不支持atomic64_t類型。故爲了便於在Linux支持的各種體繫結構之間移植代碼,開發者應該使用32位的atomic_t類型。把64位的atomic64_t類型留給那些特殊體繫結構和需要64位的代碼

8. 從指定的地址開始搜索第一個被設置(或未被設置)的位:

  • int find_first_bit(unsigned long *addr, unsigned int size)

  • int find_first_zero_bit(unsigned long *addr, unsigned int size)

  • 上面的兩個函數,第二個參數是要搜索的總位數,返回值位第一個被設置(或沒被設置的)位的位號

  • 如果搜索範圍僅限與一個字,則使用_ffs()和ffz(),它們只需要給定一個要搜索的地址做參數

自旋鎖

9. 一個被爭用的自旋鎖使得請求它的線程在等待鎖重新可用時自旋(特別浪費處理器時間),故自旋鎖不應被長時間持有

10. 自旋鎖設計的初衷:在短期間內進行輕量級加鎖,持有自旋鎖的時間最好小於完成兩次上下文切換的耗時(跟信號量對比)。

11. Linux內核實現的自旋鎖不可遞歸。

12. 自旋鎖爲SMP機器提供了防止併發訪問所需的保護機制。

13. 而在單處理器機器上,編譯的時候不會加入自旋鎖,僅被當作一個設置內核搶佔機制是否被啓用的開關。如果禁止內核搶佔,那麼在編譯時自旋鎖會被完全剔除出內核。

14. 自旋鎖可以在中斷處理函數中使用,而信號量不行。但是需要注意:

  • 在中斷處理函數中使用自旋鎖的時候,一定要在獲取鎖之前,首先禁止本地中斷(當前CPU的中斷請求,不用管其他CPU上的中斷)。原因是:如果不禁止本地中斷,那麼中斷處理程序會打斷正持有鎖的內核代碼,有可能會試圖爭用這個已經被持有的自旋鎖,這樣,中斷處理程序就會自旋,等待鎖重新可用,由於此時都在同一個CPU上,鎖的持有者在這個中斷處理程序執行完畢前不可能執行,這就是雙重請求死鎖。如果中斷發生在不同的CPU上,即使中斷處理程序在同一個鎖上自旋,也不會妨礙鎖的持有者(在不同的CPU上)最終釋放鎖。

  • 內核提供了spin_lock_irq_save和spin_unlock_irqrestore來實現這一目的

  • 對於單處理器系統,雖然在編譯時拋棄了鎖機制(退化爲只開啓或者關閉內核搶佔),如果在中斷處理程序中也使用了自旋鎖,同樣需要在獲取自旋鎖之前關閉本地中斷,以禁止中斷處理程序訪問共享數據。

15. spin_lock_irq和spin_unlock_irq

  • 如果可以確定本地中斷在加鎖前是激活的,那就不用在解鎖後恢復中斷以前的狀態了。就可以使用spin_unlock_irq解鎖時無條件地激活中斷

  • 在不確定當前本地中斷狀態的情況下,不推薦使用上面的兩個函數。

16. 內核配置宏CONFIG_DEBUG_SPINLOCK可以用來調試使用自旋鎖的代碼

17. 完整的自旋鎖操作列表

方法描述
spin_lock()獲取指定的自旋鎖
spin_unlock()禁止本地中斷並獲取指定的鎖
spin_lock_irqsave()保存本地中斷的當前狀態,禁止本地中斷,並獲取指定的鎖
spin_unlock()釋放指定的鎖
spin_unlock_irq()釋放指定的鎖,並激活本地中斷
spin_unlock_irqstore()釋放指定的鎖,並讓本地中斷恢復到以前的狀態
spin_lock_init()動態初始化指定的spinlock_t
spin_trylock()試圖獲取指定的鎖,若未獲取,返回非0
spin_is_locked()若指定的鎖當前正在被獲取,返回非0,否則返回0

18. 自旋鎖和下半部

  • spin_lock_bh 獲取指定的鎖,並禁止所有下半部的執行。spin_unlock_bh執行相反的操作

  • 由於下半部可以搶佔進程上下文的代碼,故當下半部和進程上下文共享數據時,必須對進程上下文中的共享數據進行保護,所以需要在進程上下文加鎖的同時還要禁止下半部的執行

  • 同樣,由於中斷處理程序可以搶佔下半部,所以若中斷處理程序和下半部共享數據,那麼就必須在下半部中獲取恰當的鎖的同時還要禁止中斷

  • 由於同類的tasklet不會在不同的CPU上同時執行,所以對於同類的tasklet中共享數據不需要保護;

  • 由於tasklet不會在同一個CPU上發生互相搶佔,故當數據在不同種類的tasklet共享時,只需要在訪問下半部的數據前先獲得一個普通的自旋鎖即可,而不需要禁止禁止下半部

  • 對於軟中斷,由於同一個軟中斷可以在不同的CPU上同時執行,而且在同一個CPU上不會發生軟中斷互相搶佔,所以無論是否同種類的軟中斷,如果數據被軟中斷共享,就必須得到鎖的保護,而沒有必要禁止下半部的執行。

19. read_lock和write_lock

  • 一個或多個讀任務可以併發地持有讀者鎖

  • 用於寫的鎖最多只能被一個寫任務持有,而且此時不能有併發的讀操作

  • 若在中斷處理函數中只有讀操作而沒有寫操作,就可以使用read_lock而不是read_lock_irqsave。但是此時write_lock_irqsave來禁止在寫操作時發生中斷,因爲此時如果不禁止本地中斷,中斷處理函數裏的讀操作就有可能死鎖在寫鎖上。

  • 同理,若讀者正在進行操作,包含寫操作的中斷發生了,由於讀鎖還沒有全部被釋放,故中斷裏的寫操作就會自旋,而讀操作只能在包含寫操作的中斷返回後才能繼續,纔可能釋放讀鎖,此時死鎖就會發生。如果有這種情況,讀者應該使用read_lock_irqsave而不是read_lock

  • 讀寫自旋鎖方法列表

方法描述
read_lock()獲得指定的讀鎖
read_lock_irq()禁止本地中斷並獲得指定讀鎖
read_lock_irqsave()存儲本地中斷的當前狀態,禁止本地中斷並獲得指定讀鎖
read_unlock()釋放指定的讀鎖
read_unlock_irq()釋放指定的讀鎖並激活本地中斷
read_unlock_irqstore()釋放指定的讀鎖並將本地中斷恢復到指定的前狀態
write_lock()獲得指定的寫鎖
write_lock_irq()禁止本地中斷並獲得指定的寫鎖
write_lock_irqsave()存儲本地中斷的當前狀態,禁止本地中斷並獲得指定寫鎖
write_unlock()釋放指定的寫鎖
write_unlock_irq()釋放指定的寫鎖並激活本地中斷
write_unlock_irqstore()釋放指定的寫鎖並將本地中斷恢復到指定的前狀態
write_trylock()試圖獲得指定的寫鎖;若寫鎖不可用,返回非0值
rwlock_init()初始化指定的rwlock_t

信號量 smeaphone

20. Linux信號量是一種睡眠鎖,不會禁止內核搶佔,故持有信號量的代碼可以被搶佔。這意味着信號量不會對調度的等待時間帶來負面影響

21. 讀寫信號量的睡眠都不會被信號打斷

22. 跟讀寫自旋鎖一樣,除非代碼中的讀和寫可以明白無誤地分割開來,否則最好不使用它。

互斥體 mutex

23. 是一種實現了互斥的特定睡眠鎖,即互斥體是一種信號量

24. 任何時刻只有一個任務可以持有mutex

25. 在同一上下文中上鎖和解鎖

26. 遞歸地上鎖和解鎖是不允許的,同樣也不能再去解鎖一個已經被解開的mutex

27. 當持有一個mutex時,進程不可以退出

28. mutex不能在中斷或者下半部中使用,即使mutex_trylock也不行

29. 可以打開內核宏CONFIG_DEBUG_MUTEXES來檢查互斥體的使用

30. 除非mutex的某個約束妨礙你使用,否則相比信號量要優先使用mutex

31. 互斥體和自旋鎖的比較

需求建議的加鎖方法
低開銷加鎖優先使用自旋鎖
短期加鎖優先使用自旋鎖
長期加鎖優先使用互斥體
中斷上下文加鎖使用自旋鎖
持有鎖需要睡眠使用互斥體

完成變量 completion variable

32. 如果在內核中一個任務需要發出信號通知另一個任務發生了某個特定事件,利用完成變量是使兩個任務得以同步的簡單方法。

33. 通常用法:

A common usage is to have a completion variable dynamically created as a member of a

data structure. Kernel code waiting for the initialization of the data structure calls

wait_for_completion().When the initialization is complete, the waiting tasks are awakened via a call to completion().

順序鎖 (seq鎖)

34. 用於讀寫共享數據。

35. 實現方法主要依靠一個序列計數器,當有數據被寫入時,會得到一個鎖,並且序列值會增加。在讀取數據之前和之後,序列號都會被讀取。如果讀取的序列號值相同,意味着在讀操作進行的過程中沒有寫操作打斷過。由於序列號初始值是0,在獲得和釋放寫順序鎖的時候都會使序列號加一,故如果讀取的值是偶數,那麼表明當前沒有寫操作在進行,否則如果當前有寫操作正在進行,那麼read_seqbegin不會返回,知道讀到的是偶數爲止。

To define a seq lock:

  1. seqlock_t mr_seq_lock = DEFINE_SEQLOCK(mr_seq_lock);
The write path is then
  1. write_seqlock(&mr_seq_lock);
  2. /* write lock is obtained... */
  3. write_sequnlock(&mr_seq_lock);
This looks like normal spin lock code.The oddness comes in with the read path, which is quite a bit different:
  1. unsigned long seq;
  2. do {
  3. seq = read_seqbegin(&mr_seq_lock);
  4. /* read data here ... */
  5. } while (read_seqretry(&mr_seq_lock, seq));

上面的函數read_seqretry會將當前的序列號跟seq比較,如果不相等,那麼就返回非0.

36. 使用seq鎖的建議

  • 你的數據存在很多讀者

  • 你的數據寫着很少

  • 雖然寫者很少,但是你希望寫優先與讀,而且不允許讀者讓寫者飢餓

  • 你的數據很簡單,如簡單結構,甚至是簡單的整形——在某些場合,你是不能使用原子量的

37. 使用seq鎖的經典案例就是jiffies。

禁止搶佔

38. 內核搶佔代碼使用自旋鎖作爲非搶佔區域的標記。即如果一個自旋鎖被持有,內核變不能進行搶佔。

39.  只需要禁止內核搶佔,而不需要自旋鎖的情況:per-processor data。因爲per-processor data對每個CPU都是唯一的,這樣per-processor data就不需要擔心SMP併發,也就不需要持有自旋鎖,但是因爲內核是搶佔的,那麼當前CPU完全可能會調度一個新的任務訪問同一個變量,這種情況即便是單CPU系統上也會存在針對這種問題,就可以通過preempt_disable禁止內核搶佔。(我們知道,如果搶佔計數非0,就不會調度下一個task運行)

完。



来自为知笔记(Wiz)


第十章 內核同步的方法