linux的并發(fā)和競態(tài)管理

1 并發(fā)和競態(tài)產(chǎn)生的原因

并發(fā)是操作系統(tǒng)編程中的核心問題之一。我們必須要能解決對共享資源的并發(fā)訪問。

并發(fā)產(chǎn)生資源競爭的情況如下:

  • 中斷和進(jìn)程之間
  • 不同優(yōu)先級的進(jìn)程之間
  • 不同優(yōu)先級的中斷之間
  • 多核的進(jìn)程之間

訪問共享資源(硬件資源,靜態(tài)變量,全局變量)的代碼區(qū)域稱為臨界區(qū)(critical section)。臨界區(qū)需要用某種互斥機(jī)制加以保護(hù)。包括中斷屏蔽,原子操作,自旋鎖,信號(hào)量,互斥體等。

2 中斷屏蔽

local_irq_disable();
// critical section
local_irq_enable(); 

這種中斷的保護(hù),只適用于單核CPU,如果多核CPU,則改中斷屏蔽無法屏蔽其它核上的中斷調(diào)度。也就起不到保護(hù)作用。當(dāng)前,大部分的CPU都是多核CPU,因此使用這種中斷屏蔽的方式往往意味著bug。這種中斷屏蔽方式跟自旋鎖聯(lián)合使用,才能起到作用。

3 原子操作

列出幾個(gè)原子操作API:

  該函數(shù)給原子類型的變量v增加值i。
void atomic_add(int i, atomic_t *v);

  該函數(shù)從原子類型的變量v中減去i。
atomic_sub(int i, atomic_t *v);

  該函數(shù)從原子類型的變量v中減去i,并判斷結(jié)果是否為0,如果為0,返回真,否則返回假。
int atomic_sub_and_test(int i, atomic_t *v);

  該函數(shù)對原子類型變量v原子地增加1。
void atomic_inc(atomic_t *v);

  該函數(shù)對原子類型的變量v原子地減1。
void atomic_dec(atomic_t *v);

  該函數(shù)對原子類型的變量v原子地減1,并判斷結(jié)果是否為0,如果為0,成功,否則失敗。
int atomic_dec_and_test(atomic_t *v);

  該函數(shù)對原子類型的變量v原子地增加1,并判斷結(jié)果是否為0,如果為0,成功,否則失敗。
int atomic_inc_and_test(atomic_t *v);

原子操作保證當(dāng)前對于數(shù)據(jù)的操作是一個(gè)不可分割的整體,原子操作既適用于多核的情況也適用于單核的情況。但是原子操作只適用于整型和位的并發(fā)操作保護(hù)。并且需要CPU的指令集支持這樣的鎖內(nèi)存單元操作。

4.自旋鎖

自旋鎖要解決的問題場景:

  1. 單核CPU內(nèi)不同優(yōu)先級進(jìn)程之間搶占(內(nèi)核可搶占)的問題,使用spin_lock/spin_unlock即可。當(dāng)某進(jìn)程持有自旋鎖時(shí),機(jī)制上禁止高優(yōu)先級進(jìn)程的搶斷(否則有可能死鎖)。

  2. 多核CPU間進(jìn)程搶占的問題,使用spin_lock/spin_unlock即可。

  3. 單核CPU內(nèi)進(jìn)程被中斷搶占的問題,為了防止死鎖,需要在進(jìn)程中使用自旋鎖同時(shí)禁用中斷,演化為spin_lock_irqsave/spin_unlock_irqrestore

  4. 多核CPU之間的進(jìn)程被中斷搶占的問題,需要在中斷服務(wù)程序中加spin_lock/spin_unlock。

總結(jié)一句話:進(jìn)程和中斷可能訪問同一片臨界資源,我們一般需要在進(jìn)程上下文中調(diào)用spin_lock_irqsave/spin_unlock_irqrestore,在中斷上下文中調(diào)用spin_lock/spin_unlock。

4.1 讀寫自旋鎖

多數(shù)并發(fā)讀數(shù)據(jù)的場景下,其實(shí)不需要加鎖,加鎖反而會(huì)降低效率。因此衍生出了讀寫自旋鎖。 它的原理也很簡單,保證只有一個(gè)進(jìn)程在寫(寫之間不會(huì)并發(fā)),在讀鎖碰到讀鎖時(shí)放行,讀鎖碰到寫鎖時(shí)等待。

部分函數(shù)

rwlock_t my_rwlock;
rwlock_init(&my_rwlock);
// 讀鎖
void read_lock(rwlock_t *lock);
void read_unlock(rwlock_t *lock);
// 寫鎖
void write_lock(rwlock_t *lock);
void write_unlock(rwlock_t *lock);     

4.2 順序鎖

相對于讀寫鎖,順序鎖放開了讀和寫的并發(fā),但是仍然要保證寫和寫之間時(shí)互斥的。此時(shí)可能讀到不完整的數(shù)據(jù),因此需要根據(jù)本次讀操作的結(jié)果自行進(jìn)行數(shù)據(jù)重讀。

部分函數(shù)

// 寫鎖
void write_seqlock(seqlock_t *sl);
void write_sequnlock(seqlock_t *sl);
// 讀鎖
unsigned read_seqbegin(const seqlock_t *sl);
int read_seqretry(const seqlock_t *sl, unsigned iv);
// 重讀
do {
    seqnum = read_seqbegin(&seqlock_a);
    /* 讀操作代碼塊 */
    ...
} while (read_seqretry(&seqlock_a, seqnum)); 

5 信號(hào)量(Semaphore)

struct semaphore sem; // 定義名稱為sem的信號(hào)量,是一種面向?qū)ο蟮乃枷?,定義了一個(gè)semaphore對象。    void sema_init(struct semaphore *sem, int val); // 初始化信號(hào)量
void down(struct semaphore * sem); // 獲得信號(hào)量
void up(struct semaphore * sem); // 釋放信號(hào)量

其中,val的值為信號(hào)量的個(gè)數(shù),也就是同時(shí)允許多少個(gè)進(jìn)程共享這個(gè)資源。

  • 如果val = 0,表示同時(shí)只允許1個(gè)進(jìn)程共享某個(gè)資源;
  • 如果val = 1,表示同時(shí)只允許2個(gè)進(jìn)程共享某個(gè)資源;
  • 依次類推,如果val = n,表示同時(shí)只允許n個(gè)進(jìn)程共享某個(gè)資源。

其中

val = 0的情況,一般用于互斥鎖。

val >= 1的情況,一般用于多個(gè)進(jìn)程間保持同步,即保證時(shí)序問題(例如,A、B進(jìn)程必須等待C進(jìn)程初始完資源池后才能正常運(yùn)行),類似一種生產(chǎn)者/消費(fèi)者模式,如果當(dāng)前進(jìn)程操作完成了,通過釋放信號(hào)量來廣播通知其它等待進(jìn)程去執(zhí)行各自操作。

由于新的Linux內(nèi)核傾向于直接使用mutex作為互斥手段,信號(hào)量用作互斥不再被推薦使用。

對于進(jìn)程間的時(shí)序問題(例如,一個(gè)進(jìn)程的某些執(zhí)行必須位于另一個(gè)進(jìn)程之后),信號(hào)量仍然是一種不錯(cuò)的選擇。

6 互斥體(mutex)

mutex是使用最多的場景之一。

struct mutex my_mutex; /* 定義mutex */
mutex_init(&my_mutex); /* 初始化mutex */
mutex_lock(&my_mutex); /* 獲取mutex */
... /* 臨界資源 */
mutex_unlock(&my_mutex); /* 釋放mutex */ 

和自旋鎖的差異:

  1. 互斥體和自旋鎖屬于不同層次的互斥手段,前者的實(shí)現(xiàn)依賴于后者。在互斥體本身的實(shí)現(xiàn)上,為了保證互斥體結(jié)構(gòu)存取的原子性,需要自旋鎖來互斥。所以自旋鎖屬于更底層的手段。

  2. 若臨界區(qū)比較小,宜使用自旋鎖(若使用互斥體,則進(jìn)程上下文切換會(huì)浪費(fèi)資源),若臨界區(qū)很大,應(yīng)使用互斥體(若使用自旋鎖,則等待鎖的進(jìn)程“自旋”對CPU消耗會(huì)較多)。

  3. 互斥體鎖保護(hù)的臨界區(qū)可以包含引起阻塞的代碼(如果sleep,copy_to_user)等,而自旋鎖則不行。因?yàn)樽枞馕吨M(jìn)程切換,如果持有自旋鎖階段進(jìn)程切換出去后,另一個(gè)企圖獲取該鎖的進(jìn)程就會(huì)一直在"旋轉(zhuǎn)",從而發(fā)生死鎖。

  4. 如果在中斷服務(wù)中使用互斥體,則很可能會(huì)獲取不到鎖,從而進(jìn)入睡眠。因此中斷服務(wù)中只能使用自旋鎖,因?yàn)樽孕i不會(huì)導(dǎo)致睡眠。

7 完成量(Completion)

struct completion my_completion; // 定義完成量
init_completion(&my_completion); // 初始化完成量
void wait_for_completion(struct completion *c); // 等待完成量
void complete(struct completion *c); // 喚醒完成量
void complete_all(struct completion *c); // 喚醒所有等待的完成量 

跟信號(hào)量很類似。

8 總結(jié)

并發(fā)和競態(tài)廣泛存在,中斷屏蔽、原子操作、自旋鎖和互斥體都是解決并發(fā)問題的機(jī)制。

中斷屏蔽很少單獨(dú)被使用,原子操作只能針對整數(shù)進(jìn)行,因此自旋鎖和互斥體應(yīng)用最為廣泛。 自旋鎖會(huì)導(dǎo)致死循環(huán),鎖定期間不允許阻塞,因此要求鎖定的臨界區(qū)小?;コ怏w允許臨界區(qū)阻塞,可以適用于臨界區(qū)大的情況。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容