相關概念
競爭條件
多個執行線程(進程/線程/中斷處理程序)並發(並行)訪問共享資源,因為執行順序不一樣造成結果不一樣的情況,稱為競爭條件(race condition)
舉例說明
#include
using namespace std;
int i = 0;
void thread1(){
//for(int x=0;x<100000;x++)
i++;
}
void thread2(){
//for(int x=0;x<100000;x++)
i++;
}
int main(){
std::thread t1(thread1);
std::thread t2(thread1);
t1.join();
t2.join();
printf(" i = %d\n",i);
}
t1,t2兩個線程並發執行時有可能發生競爭條件(當然這個程序發生競爭條件概率很低,但如果你把注釋去掉,競爭條件就非常容易發生了)
i++;這條語句一般需要3條機器指令
MOV EAX,i
INC EAX
MOV EAX,i
如果按照如下順序執行,是沒有有問題的,打印出的i是2
thread 1thread 2
MOV EAX,i(i=0)
INC EAX
MOV EAX,i(i=1)
MOV EAX,i(i=1)
INC EAX
MOV EAX,i(i=2)
但是兩個線程很可能按照如下順序執行,此時就會出現競爭條件,打印出的i是1
thread 1thread 2
MOV EAX,i (i=0)
INC EAX
MOV EAX,i (i=0)
MOV EAX,i (i=1)
INC EAX
MOV EAX,i (i=1)
臨界區(critical section)是指 訪問和操作共享資源的代碼段,例子裡就是兩個線程的i++語句
為避免競爭條件,臨界區必須原子地執行,執行結束前不能被打斷。一般會對臨界區加鎖,使用信號量,條件變量等,例子中的簡單操作也可以使用原子變量。
原子性的理解
一般書上對原子性的解釋為執行結束前不能被打斷。這句話如何理解呢,我個人的理解是:
原子變量這種,使用鎖總線的方式來實現,相關的若干條指令是連續執行的;使用鎖(自旋鎖/睡眠鎖),信號量,條件變量/完成變量這些,並不是說臨界區的所有指令連續執行,可能在臨界區執行到了一半,因為調度搶占或者中斷,CPU不繼續在臨界區內執行,也就是執行被打斷了。
但是這種情況依舊能保證避免競爭條件,原因在於,如果被打斷的進程之外的進程被調度,該進程要想訪問臨界區的時候,被鎖或者信號量等“攔住”,是進入不了臨界區的,直到原被打斷的臨界區執行完畢,釋放鎖/信號量等,其他進程才有可能進入臨界區。
造成並發的原因
中斷:中斷幾乎可以在任何時刻異步發生,也就可能隨時打斷當前正在執行的代碼軟中斷和tasklet:內核能在任何時候喚醒或調度軟中斷和tasklet,打斷當前正在執行的代碼內核搶占:Linux內核具有搶占性,所以內核的任務可能被另一任務搶占睡眠及用戶空間的同步:在內核執行的進程可能會睡眠,會喚醒調度程
序,從而調度一個新的用戶進程執行 SMP,兩個或多個處理器可以同時執行代碼
內核中的同步手段
加鎖
為了避免競爭條件,內核提供了幾個機制用於同步,基本思路就是加鎖,臨界區互斥訪問,信號量也可以支持多於一個線程並發訪問。
鎖的爭用
指所被占用時,有其他線程試圖獲得該鎖。鎖的爭用程度越高,系統性能也就會越低(加鎖會降低系統性能,比如睡眠鎖,自旋鎖),但是為保證正確性有必須使用。要降低鎖的爭用,加鎖粒度要盡可能小,也就是臨界區要盡量小。
幾種同步方法簡介
同步方法主要接口備注
原子整數(32bit)操作atomic_t v;/*定義 v*/
atomic_t u = ATOMIC_INIT(0);/*定義u,初始化為0*/
atomic_set(&v, 4);/* v=4 */
atomic_add(2, &v);/* v=v+2 */
atomic_inc(&v);/* v=v+1 */
原子整數(64bit)操作atomic64_t v;
原子位操作unsigned long word = 0;對普通的指針進行操作
set_bit(0,&word); //第0位被設置__set_bit形式為非原子操作
set_bit(1,&word); //第1位被設置如果不需要原子操作,非原子操作更快一些
printk("%ul\n",word);//打印3《linux內核設計與實現》P147
clear_bit(1,&word);//清空第1位
change_bit(0,&word);//反轉第0位
自旋鎖DEFINE_SPINLOCK(mr_lock);自旋鎖在同一時刻只能由一個線程位於臨界區
spin_lock(&mr_lock);可為多處理器提供並發訪問所需的保護機制
/*臨界區*/單處理機,當作一個內核搶占是否開啟的開關,如果禁止內核搶占,編譯時自旋鎖會被完全剔除內核
spin_unlock(&mr_lock);自旋鎖不可遞歸
自旋鎖和下半部下半部和進程上下文共享數據/下半部和中斷處理程序共享數據,需要加鎖下半部可以搶占進程上下文,中斷處理程序可以搶占下半部
讀寫自旋鎖DEFINE_RWLOCK(mr_rwlock);
read_lock(&mr_rwlock);所有讀者共享
/*臨界區(只讀)*/
read_unlock(&mr_rwlock);
write_lock(&mr_rwlock);寫者相互排斥,寫者和讀者互斥
/*臨界區(讀寫)*/
write_unlock(&mr_rwlock);
信號量struct semophore name;爭用的線程會睡眠,因此適合鎖會被長時間占有的情況
sema_init(&name, count);在進程上下文中使用
static DECLARE_MUTEX(name);
init_MUTEX(sem)動態初始化互斥信號量
static DECLARE_MUTEX(mr_sem);
if(down_interruptible(&mr_sem)){ /*信號被接收,信號量還未獲取*/ }
/*臨界區*/
up(&mr_sem);//釋放給定的信號量
讀寫信號量static DECLARE_RWSEM(mr_rwsem);靜態定義
init_rwsem(struct rw_semaphore *sem);動態初始化
down_read(&mr_rwsem);
/*臨界區(只讀)*/
up_read(&mr_rwsem);
down_write(&mr_rwsem);
/*臨界區(讀寫)*/
up_write(&mr_rwsem);
互斥體DEFINE_MUTEX(name);用於處理互斥的睡眠鎖
mutex_init(&mutex);動態初始化
mutex_lock(&mutex)不能遞歸上鎖和解鎖
/*臨界區*/不能再中斷或下半部執行
mutex_unlock(&mutex)
完成變量DECLARE_COMPLETION(mr_comp)等待,知道任務完成,發出信號
init_completion();動態創建
等待任務調用wait_for_completion();
產生事件的任務調用complete();//發送信號喚醒特定事件
關於大內核鎖BLK,順序鎖的內容見《Linux內核設計與實現》P160
禁止搶占
內核搶占代碼使用自旋鎖作為非搶占區域的標記
可以禁用搶占
更簡潔的解決每個處理器上的數據訪問問題,get_cpu()獲取處理器編號,返回處理器編號前會先關閉內核搶占
int cpu;
cpu = get_cpu;//禁止內核搶占,將CPU設置為當前cpu
/*對每一個cpu的數據進行操作*
/*在給予內核搶占性,“cpu”可改變,所以不再有效*/
put_up();
順序和屏障
保證順序的原因
多處理器之間,硬件設備的同步問題時,需要在程序代碼中按照指定的順序讀取內存和寫入內存。和硬件交互時,常需要保證給定讀操作在其他讀/寫操作之前多處理器上,可能需要按照寫數據的順序讀數據。
為了效率,編譯器(編譯優化)和處理器(亂序執行)可能對讀和寫重新排序。
所有可能重新排序讀寫的cpu提供機器指令,確保按順序執行。也可以指示編譯器不進行給定點周圍的指令序列進行重新排序。
e.g.
//可能重新排序
a=1;
b=2;
//不可能重新排序
a=1;
b=a;
舉例解釋保證順序的原因
a=1,b=2
線程1線程2
a=3;–
mb();–
b=4;c=b;
–rmb();
–d=a;
如果沒有內存屏障,某些處理器上,可能c接受了b的新值4,d接受了a原來的值1
幾個方法
完整接口列表
截圖自《Linux內核設計與實現》第10章