Linux併發控制(看內核源碼....)
1.linux併發與競態
在linux設備驅動中--存在多個進程對資源共享併發訪問,併發會導致競態。
併發--多個執行單元同時、並行被執行,而執行單元對共享資源的訪問會導致競態。
競態出現的情況:1.SMP對稱多處理(Symmetrical Multi-Processing)的多個CPU---cpu與cpu之間,進程與進程之間,中斷與中斷之間。
2.單cpu內進程與搶佔它的進程
3.中斷與進程之間
解決競態的途徑是保證對共享資源的互斥訪問
方式:中斷屏蔽、原子操作、自旋鎖、信號量、互斥體等。
2.編譯亂序和執行亂序
2.1 編譯亂序
主要出現的原因:編譯器可以對訪存指令進行亂序,減少邏輯上不必要的訪存,以提高Cache命中率和CPU的Load/Store單元的工作效率。
解決編譯亂序可以通過barrier()--編譯屏障,可以阻擋編譯器的優化。
2.2 執行亂序
高級的CPU可以根據自己的緩存的組織特性,將訪存指令重新排序,連續地址的訪問可能會先執行,因爲這樣緩存命中率較高。
ARM處理器的屏障指令包括:
1.DMB(數據內存屏障):在DMB之後的顯示內存訪問執行前,保證所有在DMB指令之前的內存訪問完成;
2.DSB(數據同步屏障):等待所有在DSB指令之前的指令完成;
3.ISB(指令同步屏障): Flush流水線,使得所有ISB之後執行的指令都是從緩存或內存中獲得的;
3.中斷屏蔽
對於單cpu, 在進入臨界區之前屏蔽系統中的中斷時避免競態的一種簡單而有效的方法。(驅動程序中不推薦使用)
cpu一般都具備中斷關閉和打開的功能,這項功能保證了正在執行的內核執行路線不被中斷處理程序所搶佔,防止了某些競態的發生。
中斷屏蔽使得中斷和進程之間的併發不再發生。由於linux內核調度(時間片)等操作都依賴中斷來實現,內核搶佔進程之間的併發也就得以避免。
常見操作:
local_irq_enable()
local_irq_disable()
//禁止中斷,並保存/恢復狀態
local_irq_save()
local_irq_restore()
//禁止中斷底部
lock_bh_enable()
lock_bh_disable()
4.原子操作
保證對一個整形數據的修改是排他性的。
對於ARM處理器底層使用LDREX和STREX指令實現原子操作。
主要有整形原子操作和位原子操作。
常見操作:
atomic_set();
atomic_read()
atomic_add()
atomic_sub()
atomic_inc()
atomic_dec()
atomic_inc_and_test()
atomic_dec_and_test()
atomic_sub_and_test()
set_bit()
clear_bit()
change_bit()
test_bit()
test_and_set_bit()
test_and_clear_bit()
test_and_change_bit()
5.自旋鎖---在多處理器系統能夠提供對共享數據的保護
spin_lock-->raw_spin_lock-->_raw_spin_lock-->__raw_spin_lock-->do_raw_spin_lock-->arch_spin_trylock;
spin Lock是一種對臨界資源進行互斥訪問的手段.
關於spin_unlock_irq()--->主要應用在中斷處理程序中,它做主要的工作是關閉中斷、停止搶佔、獲取鎖;
常見操作:
spinloc_t lock;//定義自旋鎖
spin_lock_init(lock);//初始化自旋鎖
spin_lock(lock)//獲取自旋鎖,如果鎖已經被佔用,就原地等待
spin_trylock(lock)//獲取自旋鎖,如果鎖已經被佔用,立即返回
spin_unlock(lock);//釋放自旋鎖
//下面三個主要使用的原因:如果一個進程持有自旋鎖spinlock,而中斷髮生後進程休眠,進入中斷處理程序中執行,剛好使用同一個spinlock
//這個時候中斷處理程序就會阻塞,造成死鎖。
//一般需要在進程上下文中調用spin_lock_irq()這類函數,來阻止中斷和內核搶佔。
//在中斷上下文中使用spin_lock();
spin_lock_irq()
spin_unlock_irq()
spin_lock_irqsave() //關閉本地的中斷,多核編程中不使用
spin_unlock_irqrestore()
spin_lock_bh()
spin_unlock_bh()
注意:
1.自旋鎖應用在佔有鎖較短的時間內,否則會降低系統性能(忙則等待)
2.不能兩次或兩次以上獲取同一個鎖
3.在自旋鎖鎖定期間不能調用引起進程調度的函數
6.讀寫鎖
允許任意數量的讀取者同時進入臨界區,但是寫入者必須進行互斥訪問。
如果當前進程正在寫,那麼其他進程就不能讀,也不能寫
如果當前進程正在讀,那麼其他進程可以讀,但是不能寫
常見操作:
//定義及初始化
rwlock_t my_rwlock;
rwlock_init()
//讀操作
read_lock()
read_lock_irqsave()
read_lock_irq()
read_lock_bh()
//寫操作
write_lock()
write_lock_irqsave()
write_lock_irq()
write_lock_bh()
7.順序鎖
順序鎖是對讀寫鎖的一種優化,若使用順序鎖,讀執行單元不會被寫執行單元阻塞。
8.RCU(Read_Copy_Update)--讀寫更新鎖(***)
RCU(Read-Copy Update),顧名思義就是讀-拷貝修改,它是基於其原理命名的。
對於被RCU保護的共享數據結構,讀者不需要獲得任何鎖就可以訪問它,但寫者在訪問它時首先拷貝一個副本,然後對副本進行修改,
最後使用一個回調(callback)機制在適當的時機把指向原來數據的指針重新指向新的被修改的數據。
這個時機就是所有引用該數據的CPU都退出對共享數據的操作。
常用操作:
rcu_read_lock()
rcu_read_unlock()
synchronize_rcu()-->call_rcu() //又名call_rcu_sched()
rcu_assign_pointer()
rcu_dereference()
8.信號量(semaphore)
linux中信號量是一種睡眠鎖。如果有一個任務試圖獲得不可用的信號量時,信號量會將其推進一個等待隊列,讓其睡眠。
當信號量可用時,處於等待隊列中的任務被喚醒,獲得信號量。
注意:
信號量適用於鎖被長時間佔用。
由於執行線程在鎖被爭用時會睡眠,信號量只能在進程上下文中使用,不能再中斷上下文中使用的。
佔有信號量的同事不能佔用自旋鎖,等待信號量的會導致進程睡眠,而持有自旋鎖時不能睡眠。
常見操作:
struct semaphore name;
sema_init(&name, count);//初始化信號量
init_MUTEX(&name) //以計數值爲1初始化動態創建的信號量(互斥信號量)
down_interrupt(&name)//爭用一個信號量,如果不可用,將進程設置爲可中斷-task_interruptible狀態-進入睡眠。(多用)
down(&name)//爭用一個信號量,如果不可用,將進程設置爲不可中斷-task_uninterruptible狀態-進入睡眠。(少用)
up(&name)//釋放指定的信號量,如果睡眠隊列不爲空,則喚醒其中的一個任務。
9.互斥鎖(mutex)
linux爭對 count=1 的信號量重新定義了一個數據結構mutex
常用操作:
struct mutex my_mutex; //定義互斥鎖
mutex_init() //初始化互斥鎖
mutex_lock() //獲取互斥鎖,如果鎖被佔用,就將進程設置爲不可中斷睡眠
mutex_lock_interrputible() //獲取互斥鎖,如果鎖被佔用,將進程設置爲可中斷睡眠。
mutex_trylock() //獲取互斥鎖,如果鎖被佔用,就返回
mutex_unlock() //釋放互斥鎖
10.完成量 (completion)