歡迎光臨
每天分享高質量文章

漫畫 | Linux 併發、競態、互斥鎖、自旋鎖、訊號量都是什麼鬼?

來自:嵌入式Linux(微訊號:Linux-c-world)

1、鎖的由來?

學習linux的時候,肯定會遇到各種和鎖相關的知識,有時候自己學好了一點,感覺半桶水的自己已經可以華山論劍了,又突然冒出一個新的知識點,我看到新知識點的時候,有時間也是一臉的懵逼,在大學開始寫微控制器的跑裸機程式碼,完全不懂這個鎖在作業系統裡面是什麼鬼,從微控制器到嵌入式Linux,還有一個任務系統,不懂的同學建議百度看看。

 

2、什麼是併發和競態?

在早期的Linux核心中,併發源相對較少。核心不支援對稱多處理器(SMP)系統,唯一導致併發問題的原因是中斷

隨著處理器的CPU核越來越多,這要求系統對事件迅速做出響應。為適應現代硬體和應用的需求,Linux核心已經發展到可以同時進行更多事情的地步。這種演變帶來了更大的可伸縮性。但是,這也大大複雜化了核心程式設計的任務。裝置驅動程式員現在必須從一開始就將併發性考慮到他們的設中,而且他們需要深刻的理解併發問題,並利用核心提供的工具處理這類問題。

Spinlocks and Atomic Context

Imagine for a moment that your driver acquires a spinlockand goes
about its business within its critical section. Somewhere in the
middle, your driver loses the processor.Perhaps it has called a
function (copy_from_user, say) that puts the process to sleep. Or,
perhaps, kernel preemption kicks in, and a higher-priority process
pushes your code aside. Your code is now holding a lockthat it will
not release any time in the foreseeable future. If some other thread
tries to obtain the same lock, it will, in the best case, wait
(spinning in the processor) for a very long time. In the worst
case,the system could deadlock entirely. Most readers would agree that
this scenario is best avoided. Therefore, the core rule that applies
to spinlocks is that any code must, while holding a spinlock, be
atomic.It cannot sleep; in fact, it cannot relinquish the processor
for any reason except toservice interrupts (and sometimes not even
then).

2.1 併發與競態概念

上面扯淡完了進入正題

什麼是併發: 併發是指多個執行任務同時、並行被執行。
什麼是競態: 字面意思是競爭,併發的執行單元對共享資源(硬體資源和軟體上的全域性變數,靜態變數等)的訪問容易發生競態。

舉例一個字元裝置的缺陷: 對於一個虛擬的字元裝置驅動,假設一個執行單元A對其寫入300個字元‘a’,而另一個執行單元B對其寫入300個字元‘b’,第三個執行單元讀取所有字元。如果A、B被順序序列執行那麼C讀出的則不會出錯,但如果A、B併發執行,那結果則是我們不可料想的。

競態發生的情況

對稱多處理器(SMP)的多個CPU: SMP是一種緊耦合、共享儲存的系統模型,它的特點是多個CPU使用共同的系統匯流排,因此可以訪問共同的外設和儲存器。

單CPU內行程與搶佔它的行程: Linux 2.6的核心支援搶佔排程,一個行程在核心執行的時候可能被另一高優先順序行程打斷。

中斷(硬中斷、軟中斷、tasklet、低半部)與行程之間:中斷可以打斷正在執行的行程,處理中斷的程式和被打斷的行程間也可能發生競態。

競態的解決辦法

解決競態問題的途徑是保證對共享資源的互斥訪問。訪問共享資源的程式碼區域稱為臨界區,臨界區要互斥機制保護。Linux裝置驅動中常見的互斥機制有以下方式:中斷遮蔽、原子操作、自旋鎖和訊號量等。

Most readers would agree that this scenario is best avoided.
Therefore, the core rule that applies to spinlocks is that any code
must, while holding a spinlock, be atomic.It cannot sleep; in fact, it
cannot relinquish the processor for any reason except toservice
interrupts (and sometimes not even then).

對上面死鎖理解不夠深入的,可以細細評味這段英文。

3、討論下死鎖

死鎖的問題是開發中稍不小心就可能遇到的,在SMP系統裡面,如果有一個CPU被死鎖了,還有其他CPU可以繼續執行,就像一個車子,有一個輪子爆胎了,理論上還是可以跑的,就是開不快,或者開快的話就會容易掛逼。

3.1 多行程排程導致死鎖

之前的文章https://mp.weixin.qq.com/s/au-ZXnTgpOO3mYRyKir01w

出現以下四種情況會產生死鎖:

1、相互排斥。一個執行緒或行程永遠佔有共享資源,比如,獨佔該資源。
2、迴圈等待。例如,行程A在等待行程B,行程B在等待行程C,而行程C又在等待行程A。
3、部分分配。資源被部分分配,例如,行程A和B都需要訪問一個檔案,同時需要用到印表機,行程A得到了這個檔案資源,行程B得到了印表機資源,但兩個行程都不能獲得全部的資源了。
4、缺少優先權。一個行程獲得了該資源但是一直不釋放該資源,即使該行程處於阻塞狀態。
具體使用的場景會更加複雜,要需要按實際分析,對號入座~

3.2 單執行緒導致死鎖

單執行緒導致死鎖的情況一般是由於呼叫了引起阻塞的函式,比如(copy_from_user()、copy_to_ser()、和kmalloc()),阻塞後進行系統排程,排程的過程中有可能又呼叫了之前獲取鎖的函式,這樣必然導致死鎖。

還有一種就是自旋鎖函式在沒有釋放鎖馬上又進行申請同一個自旋鎖,這樣的低階問題也是會導致自旋鎖。

4、互斥鎖和自旋鎖、訊號量的區別?

互斥鎖和互斥量 在我的理解裡沒啥區別,不同叫法。廣義上講可以值所有實現互斥作用的同步機制。狹義上講指的就是mutex這種特定的二元鎖機制。互斥鎖的作用就是互斥,mutual exclusive,是用來保護臨界區(critical section)的 。所謂臨界區就是程式碼的一個區間,如果兩個執行緒同時執行就有可能出問題,所以需要互斥鎖來保護。

訊號量(semaphore) 是一種更高階的同步機制,mutex(互斥鎖) 可以說是 semaphore(訊號量) 在僅取值0/1時的特例。Semaphore可以有更多的取值空間,用來實現更加複雜的同步,而不單單是執行緒間互斥。

自旋鎖 是一種 互斥鎖 的實現方式而已,相比一般的互斥鎖會在等待期間放棄cpu,自旋鎖(spinlock) 則是不斷迴圈並測試鎖的狀態,這樣就一直佔著cpu。所以相比於自旋鎖和訊號量,在申請鎖失敗的話,自旋鎖會不斷的查詢,申請執行緒不會進入休眠,訊號量和互斥鎖如果申請鎖失敗的話執行緒進入休眠,如果申請鎖被釋放後會喚醒休眠的執行緒。

同步鎖 好像沒啥特殊說法,你可以理解為能實現同步作用的都可以叫同步鎖,比如訊號量。最後,不要鑽這些名詞的牛角尖,更重要的是理解這些東西背後的原理,叫什麼名字並沒有什麼好說的。這些東西在不同的語言和平臺上又有可能會有不同的叫法,其實本質上就這麼回事。

5、如何解決競態引起的問題?

上面我們已經分析了競態產生的原因、發生的情況以及解決辦法,下麵我們對常見的解決辦法一一分析。

1、中斷遮蔽

1、基本概念:在單CPU中避免競態的一種簡單方法是在進入臨界區之前遮蔽系統的中斷。由於linux的非同步I/O、行程排程等很多內容都依靠中斷,所以我們應該儘快的執行完臨界區的程式碼,換句話就是臨界區程式碼應該儘量少。

2、具體操作: linux核心提供了下麵具體方法

Local_irq_disable();//遮蔽中斷
Local_irq_enable();//開啟中斷
Local_irq_save(flags);//禁止中斷並儲存當前cpu的中斷位資訊 

2、原子操作

1、基本概念:原子操作指在執行過程中不會被別的程式碼中斷的操作。

2、具體操作:linux核心提供了一系列的函式來實現核心中的原子操作,這些操作分為兩類,一類是整型原子操作,另一類是位原子操作,其都依賴底層CPU的原子操作實現,所以這些函式與CPU架構有密切關係。

1) 整型原子操作

  • a)設定原子變數的值

atomic_t v = ATOMIC_INIT(0);//定義原子變數v並初始化為0
void atomic_set(atomic_t *v, int i);//設定原子變數值為i 

 

  • b)獲取原子變數的值

atomic_read(atomic_t *v);//傳回原子變數v的值 

 

  • c)原子變數加、減操作

void atomic_add(int i, atomic_t *v);//原子變數v增加i
void atomic_sub(int I, atomic_t *v);//原子變數v減少i 

 

  • d)原子變數自增、自減

void atomic_inc(atomic_t *v);//原子變數v自增1
void atomic_dec(atomic_t *v);//原子變數v自減1 

 

  • e)操作並測試

int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);  
int atomic_sub_and_test(int i,atomic_t *v);

/*上述三個函式對原子變數v自增、自減和減操作(沒有加)後測試其是否為0,如果為0傳回true,否則傳回false*/ 

 

  • f) 操作並傳回

int atomic_add_return(int i,atomic_t *v);
int atomic_sub_return(int i,atomic_t *v);  
int atomic_inc_return(atomic_t *v);
int atomic_dec_return(atomic_t *v);

/*上述函式對原子變數v進行自增、自減、加、減操作,並傳回新的值*/ 

2) 位原子操作

  • a)設定位

void set_bit(nr,void *addr);//設定addr地址的第nr位,即向該位寫入1。 

 

  • b)清除位

void clear_bit(nr,void *addr);//清除addr地址的第nr位,即向該位寫入0。 

 

  • c)改變位

void change_bit(nr,void *addr);//對addr地址的第nr取反 

 

  • d)測試位

int test_bit(nr,void *addr);//傳回addr地址的第nr位 

 

  • e) 測試並操作位

int test_and_set_bit(nr,void *addr);
int test_and_clear_bit(nr,void *addr)int test_and_change_bit(nr,void *addr);
/*上述函式等同於執行test_bit後,再執行xxx_bit函式*/ 

3、自旋鎖

1、基本概念: 自旋鎖是一種對臨界資源進行互斥訪問的手段。
2、工作原理: 為獲得自旋鎖,在某CPU上執行的程式碼需先執行一個原子操作,該操作測試並設定某個記憶體變數,由於其為原子操作,所以在該操作完成之前其他執行單元不可能訪問這個記憶體變數,如果測試結果表明已經空閑,則程式獲得這個自旋鎖並繼續執行,如果測試結果表明該鎖仍被佔用,程式將在一個小的迴圈內重覆這個“測試並設定”操作,即進行所謂的“自旋”,通俗的說就是在“原地打轉”。
3、具體操作: linux核心中與自旋鎖相關的操作主要有:

  • 1)定義自旋鎖

spinlock_t lock

 

  • 2)初始自旋鎖

spin_lock_init(lock); 

 

  • 3)獲得自旋鎖

spin_lock(lock);//獲得自旋鎖lock
spin_trylock(lock);//嘗試獲取lock如果不能獲得鎖,傳回假值,不在原地打轉。 

 

  • 4)釋放自旋鎖

spin_unlock(lock);//釋放自旋鎖  

為保證我們執行臨界區程式碼的時候不被中斷等影響我們的自旋鎖又衍生了下麵的內容

  • 5)自旋鎖衍生

spin_lock_irq() = spin_lock() + local_irq_disable()
spin_unlock_irq() = spin_unlock() + local_irq_enable()
spin_lock_irqsave() = spin_lock() + local_irq_save() 
spin_unlock_irqrestore() = spin_unlock() + local_irq_restore()
spin_lock_bh() = spin_lock() + local_bh_disable()
spin_unlock_bh() = spin_unlock() + local_bh_disable() 

4、使用註意事項:

  • 1)自旋鎖實質是忙等鎖,因此在佔用鎖時間極短的情況下,使用鎖才是合理的,反之則會影響系統效能。

  • 2)自旋鎖可能導致系統死鎖。

  • 3)自旋鎖鎖定期間不能呼叫可能引起行程排程的函式。

4、讀寫自旋鎖

1、基本概念: 為解決自旋鎖中不能允許多個單元併發讀的操作,衍生出了讀寫自旋鎖,其不允許寫操作併發,但允許讀操作併發。

2、具體操作: linux核心中與讀寫自旋鎖相關的操作主要有:

  • 1)定義和初始化讀寫自旋鎖

rwlock_t my_rwlock = RW_LOCK_UNLOCKED;//靜態初始化
rwlock_t my_rwlock;
rwlock_init(&my;_rwlock);//動態初始化 

 

  • 2)讀鎖定

read_lock();
read_lock_irqsave();
read_lock_irq();
read_lock_bh(); 

 

  • 3)讀解鎖

read_unlock();
read_unlock_irqrestore();
read_unlock_irq();
read_unlock_bh(); 

 

  • 4)寫鎖定

write_lock();
write_lock_irqsave();
write_lock_irq();
write_lock_bh();
write_trylock(); 

 

  • 5)寫解鎖

write_unlock();
write_unlock_irqrestore();
write_unlock_irq();
write_unlock_bh(); 

5、順序鎖

1、基本概念: 順序鎖是對讀寫鎖的一種最佳化,如果使用順序鎖,讀執行單元在寫執行單元對被順序鎖保護的共享資源進行寫操作時仍然可以繼續讀,不必等待寫執行單元的完成,寫執行單元也不需等待讀執行單元完成在進行寫操作。
2、註意事項: 順序鎖保護的共享資源不含有指標,因為在寫執行單元可能使得指標失效,但讀執行單元如果此時訪問該指標,將導致oops。
3、具體操作: linux核心中與順序鎖相關的操作主要有:

  • 1)寫執行單元獲得順序鎖

write_seqlock();
write_tryseqlock();
write_seqlock_irqsave();
write_seqlock_irq();
write_seqlock_bh(); 

 

2)寫執行單元釋放順序鎖

write_sequnlock();
write_sequnlock_irqrestore();
write_sequnlock_irq();
write_sequnlock_bh(); 

 

  • 3)讀執行單元開始

read_seqbegin();
read_seqbegin_irqsave();//local_irq_save + read_seqbegin 
  • 4)讀執行單元重讀

read_seqretry ();
read_seqretry_irqrestore (); 

6、RCU(讀-複製-更新)

1、基本概念: RCU可以看做是讀寫鎖的高效能版本,相比讀寫鎖,RCU的優點在於即允許多個讀執行單元同時訪問被保護資料,又允許多個讀執行單元和多個寫執行單元同時訪問被保護的資料。
2、註意事項: RCU不能代替讀寫鎖。
3、具體操作: linux核心中與RCU相關的操作主要有:

  • 1)讀鎖定

rcu_read_lock ();
rcu_read_lock_bh (); 

 

  • 2)讀解鎖

rcu_read_unlock ();
rcu_read_unlock_bh (); 

 

  • 3)同步RCU

synchronize_rcu ();//由RCU寫執行單元呼叫
synchronize_sched();//可以保證中斷處理函式處理完畢,不能保證軟中斷處理結束 

 

  • 4)掛接回呼

call_rcu ();
call_rcu_bh (); 

有關RCU的操作還有很多,大家可以參考網路。

7、訊號量

1、基本概念: 訊號量用於保護臨界區的常用方法與自旋鎖類似,但不同的是當獲取不到訊號量時,行程不會原地打轉而是進入休眠等待狀態。
2、具體操作: linux核心中與訊號量相關的操作主要有:

  • 1)定義訊號量

Struct semaphore sem; 

 

  • 2)初始化訊號量

void sema_init(struct semaphore *sem, int val);//初始化sem為val,當然還有系統定義的其他宏初始化,這裡不列舉 

 

  • 3)獲得訊號量

void down(struct semaphore *sem);//獲得訊號量sem,其會導致睡眠,並不能被訊號打斷 
int down_interruptible(struct semaphore *sem);//進入睡眠可以被訊號打斷 
int down_trylock(struct semaphore *sem);//不會睡眠 

 

  • 4)釋放訊號量

void up(struct semaphore *sem);//釋放訊號量,喚醒等待行程 

註:當訊號量被初始為0時,其可以用於同步。

8.Completion用於同步

1、基本概念: linux中的同步機制。

2、具體操作: linux核心中與Completion相關的操作主要有:

  • 1)定義Completion

struct completion *my_completion; 

 

  • 2)初始化Completion

void init_completion(struct completion *x)

 

  • 3)等待Completion

void wait_for_completion(struct completion *)

 

  • 4)喚醒Completion

void complete(struct completion *);//喚醒一個 
void complete_all(struct completion *);//喚醒該Completion的所有執行單元 

9、讀寫訊號量

1、基本概念: 與自旋鎖和讀寫自旋鎖的關係類似
2、具體操作: linux核心中與讀寫訊號量相關的操作主要有:

  • 1)定義和初始化讀寫自旋鎖

struct rw_semaphore sem; 
init_rwsem(&sem;); 

 

  • 2)讀訊號量獲取

down_read ();
down_read_trylock(); 

 

  • 3)讀訊號量釋放

up_read (); 

 

  • 4)寫訊號量獲取

down_write ();
down_write_trylock (); 

 

  • 5)寫訊號量釋放

 up_write(); 

10、互斥體

1、基本概念: 用來實現互斥操作

2、具體操作: linux核心中與互斥體相關的操作主要有:

  • 1)定義和初始化互斥體

struct mutex lock;
mutex_init(&lock); 

 

  • 2)獲取互斥體

void mutex_lock(struct mutex *lock);
int mutex_lock_interruptible(struct mutex *lock)int mutex_lock_killable(struct mutex *lock)

 

  • 3)釋放互斥體

void mutex_unlock(struct mutex *lock)
贊(0)

分享創造快樂