linux的同步与互斥
臨界區:(critical?region)
所謂臨界區就是訪問和操作共享數據的代碼段。
并發有偽并發(單處理器)和真并發(多處理器)之分,但是都會造成競爭條件。
同步:(synchronization)
避免并發(多個執行線程并發訪問同一個資源)和防止競爭條件(兩個執行線程處于同一臨界區)被稱為同步。
用戶空間之所以需要同步,是因為用戶程序會被調度程序搶占和重新調度。
造成并發執行的原因:
1?中斷
2?軟中斷和tasklet
3?內核搶占——任務的優先級
4?睡眠及用戶空間的同步
5?SMP
在編寫代碼的開始階段就應該設計恰當的鎖
我們要給數據而不是代碼加鎖。大多數內核數據結構都需要加鎖!而局部自動變量,動態分配的數據結構不需要加鎖。
Linux信號量的實現
0.概念:
信號量:?一個信號量本質上是一個整數值,?它和一對函數聯合使用,?這一對函數通常成為P和V,?也就是我們所說的P/V操作.
·?當一個進程希望進入臨界區時,?對臨界區的信號量執行P操作:
·?如果信號量的值大于0,?則該值會減1,?而進程可以繼續;
·?如果信號量的值等于0(或更小),?進程必須等待直到其他人釋放該信號量.
·?當一個進程完成對臨界區的操作時,?對臨界區信號量執行V操作:
?將信號量的值加1,?并在必要時喚醒等待的進程.
·?在這種模式下,?一個信號量有時也稱為一個"互斥體"(mutex),?它是互斥的簡稱.
規則:
·?當信號量用于互斥時(即避免多個進程同時操作一個臨界區),?信號量的值應該初始化為1.
·?信號量在任何時刻只能由單個進程或線程擁有.
1.?定義:
頭文件:?<asm/semaphore.h>
數據類型:?struct?semaphore
直接創建:
?void?sema_init(struct?semaphore?*sem,?int?val);?/*?其中val是信號量的初始值?*/
輔助宏:
?DECLARE_MUTEX(name);?????????????????/*?把一個稱為name的信號量變量初始化為1?*/
DECLARE_MUTEX_LOCKED(name);?/*?把一個稱為name的信號量變量初始化為0?*/
動態分配:
/*?用于運行時的初始化?*/
void?init_MUTEX(struct?semaphore?*sem);
void?init_MUTEX_LOCKED(struct?semaphore?*sem);
在Linux世界中,?P函數被稱為down,?指的是該函數減小了信號量的值,?它也許會將調用者置于休眠狀態,?然后等待信號量變得可用,?之后再授予調用者對被保護資源的訪問權限.?down函數有三個版本:
void?down(struct?semaphore?*sem);?????/*?減小信號量的值,?并在必要時一直等待。無法獲得信號量時,會導致調用進程休眠*/
作為通常規則,?我們不應該使用非中斷版本.?非中斷操作是建立不可殺進程的好方法(ps輸出中的"D?state")
void?down_interruptible(struct?semephore?*sem);??/*?可中斷版本,?常用。?無法獲得信號量時,會導致調用進程休眠*/
可中斷版本幾乎是我們始終要使用的版本。
使用該函數,?如果得到,返回0;如果操作被中斷,?該函數會返回非0值(-EINTR),?而調用者不會擁有該信號量.
因此對該函數的正確使用需要始終檢查返回值,?并做出相應的響應.
void?down_trylock(struct?semaphore?*sem);
/*?永遠不會休眠,?如信號量在調用時不可獲得,?立即返回非0值?*/?
后來的內核又增加了:
int?down_killable(struct?semaphore?*sem);
/*無法獲得信號量時,會導致調用進程休眠*,操作可被fatal信號中斷,?函數會返回非0值(-EINTR),?而調用者不會擁有該信號量.?*/
int?down_timeout(struct?semaphore?*sem,?long?jiffies);
/*在指定時間內獲取該信號量,會導致調用進程休眠,無法獲取時返回-ETIME*/
當一個線程成功調用down函數的某個版本之后,?就稱為該線程擁有了該信號量,?可以訪問被該信號量保護的臨界區.?當互斥操作完成后,?必須釋放該信號量.
Linux的V函數是up:
/*?調用up之后,?調用者不再擁有該信號量?*/
void?up(struct?semaphore?*sem);
2.?舉例:
我們擁有一個共享數據結構:
struct?st_data
{
????char?name[32];
????char?data[128];
????int?data_len;
};
這個數據結構被多個進程同時訪問.
為了避免這些進程在訪問該結構時產生競態,?我們在該結構的底部為其加上信號量:
struct?st_data
{
????char?name[32];??????????????/*?name?*/
????char?data[128];??????????????/*?data?*/
????int?data_len;??????????????????/*?data?length?*/
????struct?semaphore?sem;?/*?semaphore?*/
};
信號量在使用前必須進行初始化,?而且是在共享數據其他部分可用前初始化.?因此,?我們在其他數據賦值之前調用init_MUTEX,?否則會建立一個競態,?即在信號量準備好之前,?有代碼可能會訪問它們.
st_data?data;
init_MUTEX(&data->sem);
setup_data(&data);?/*?初始化數據?*/
...
接下來,?我們必須仔細檢查代碼,?確保在不擁有該信號量的時候不會訪問data數據結構.?例如,?在data_write的開始處加入:
if?(down_interruptible(&data->sem))???
?????return?-ERESTARTSYS;
這是檢查down_interruptible的返回值,?如果返回非0值,?說明操作被中斷.?這種情況下,?通常要做的工作是返回-ERESTARTSYS.?在得到這個返回值后,?內核會從頭重新啟動該調用,?或者將該錯誤返回給用戶.
如果我們返回-ERESTARTSYS,?則必須首先撤銷已經做出的修改,?這樣,?系統調用才可正確重試.?如果無法撤銷這些操作,?則應該返回-EINTR,?表明中斷.
不管data_write能否完成其他工作,?它都必須釋放信號量:
out:
????up(&data->sem);
????return?retval;
在data_write中有幾個地方可能會產生錯誤,?包括內存分配失敗等.?在這些情況下,?代碼會執行goto?out,?確保正確的完成信號量的釋放工作.
讀取/寫入信號量
信號量對所有調用者執行互斥操作,?而不管線程想做什么(讀/寫).?正如這樣,?我們可以把任務劃分為兩種類型:?讀取和寫入.?多個并發的讀取應該是被允許的,?因為只讀任務可并行完成他們的工作,?這樣做可以大大提高性能.
如此,?便有了Linux內核提供的一種特殊信號量類型,?稱為"rwsem"(reader/writer?semaphore).?雖然在驅動程序中使用rwsem的機會相對比較少,?但偶爾也比較有用.
頭文件:?<linux/rwsem.h>
數據類型:?struct?rw_semaphore
一個rwsem對象必須用一下函數顯式地初始化:
void?init_rwsem(struct?rw_semaphore?*sem);
對受保護資源的只讀訪問,?可和其他讀取者并發地訪問.?可用的接口有:
void?down_read(struct?rw_semaphore?*sem);/*?可能會將調用進程置于不可中斷的休眠?*/
int?down_read_trylock(struct?rw_semaphore?*sem);/*?不會阻塞等待,授予訪問時返回非0,?其他情況返回0??注意他的返回值用法和其它函數的不同*/
void?up_read(struct?rw_semaphore?*sem);?/*?釋放rw_sem對象?*/
針對寫入者的接口類似于讀取者:
void?down_write(struct?rw_semaphore?*sem);
int?down_write_trylock(struct?rw_semaphore?*sem);
void?up_write(struct?rw_semaphore?*sem);
這3個函數與讀取者的對應函數行為相同,?他們提供的是寫入訪問.
/*?當某個快速改變獲得寫入者鎖,
??*其后有更長時間的只讀訪問的時候,
??*我們可以調用該函數來允許其他讀取者訪問?*/
void?downgrade_write(struct?rw_semaphore?*sem);
讀取/寫入信號量特點:
·?一個rwsem可以允許一個寫入者或無限多個讀取者擁有該信號量.
·?寫入者具有更高的優先級,?某個寫入者試圖進入臨界區,?在其完成工作之前,?不會允許讀取者獲得訪問.
·?如果大量寫入者競爭該信號量,?會導致讀取者"餓死".
·?最好在很少需要寫訪問且寫入者只會短期擁有信號量什使用rwsem.
Completion(完成量)
有時,?由于信號量輕松up操作之后,?會產生過長時間的操作,?為了避免信號量執行down操作導致的長時間阻塞,?內核提供了一組completion(完成)接口解決這種問題.
Completion是一種輕量級的機制,?它允許一個線程告訴另一個線程某個工作已經完成.
頭文件:?<linux/completion.h>
數據類型:?struct?completion
一個completion對象可以通過兩種方法創建和初始化:
DECLARE_COMPLETION(my_completion);
或者,?如果必須動態,?則使用:
struct?completion?my_completion;
/*?...?*/
init_completion(&my_completion);
對completion操作的接口有:
等待completion的操作可以用:
/*?執行一個非中斷等待?*/
void?wait_for_completion(struct?completion?*c);
實際的completion事件調用可以用:
/*?只會喚醒一個等待線程?*/
void?complete(struct?completion?*c);
/*?喚醒所有等待線程?*/
void?complete_all(struct?completion?*c);
但在大多數情況下,?只會有一個等待者,?此時兩個函數效果相同.
一個completion通常是一個單次(one-shot)設備,?它只會被使用一次然后被丟棄.觸發事件明確時,?如果沒有使用complete_all,?可以重復使用一個completion結構.如果使用了complete_all,?則必須在重復使用該結構之前重新初始化它.
可以用下面的宏進行重新初始化Completion
INIT_COMPLETION(struct?completion?*c)?;
Completion機制的典型是模塊退出時的內核線程終止.?這時,?驅動程序內部由一個while(1)循環完成,?當內核準備清楚該模塊時,?exit函數會令線程退出并等待completion.?此時,?內核可調用一個特殊函數:
void?complete_and_exit(struct?completion?*c,?long?retval);
/*?其中retval是exit的返回值,?可用于錯誤代碼檢查?*/
linux中自旋鎖(spinlock)的實現
信號量對互斥來講是非常有用的,?但它并不是內核提供的唯一工具.
大多數鎖定通過稱為"自旋鎖"的機制實現.?和信號量不同,?自旋鎖可以在不能休眠的代碼中使用,?比如:?中斷處理例程.?在正確使用的情況下,?自旋鎖通常可以提供比信號量更高的性能,?但它也有一組不同的使用限制.
1.?概念:
自旋鎖在概念上非常簡單,?一個自旋鎖就是一個互斥設備,?它只有兩個值:?"鎖定"和"解鎖".?它通常實現為某個整數值中的單個位.
·?希望獲得某特定鎖的代碼,?測試相關的位.?(原子操作)
·?如果鎖可用,?則"鎖定"位被設置,?而代碼繼續進入臨界區.
·?相反,?如果鎖被其他人獲得,?則代碼進入忙循環并重復檢查這個鎖,?直到該鎖可用為止.
這個循環就是自旋鎖的"自旋"部分.?當存在自旋鎖時,?等待執行忙循環的處理器做不了任何有用的工作.
核心原則:任何擁有自旋鎖的代碼都必須是原子的。它不能休眠。
另外一個重要原則:自旋鎖必須在可能的最短時間內擁有。
在擁有鎖時,要注意調用的每一個函數是否會引起睡眠。是否會有中斷服務程序獲得相同的鎖(需禁止本地CPU中斷)
2.?自旋鎖基本API:
頭文件:?<linux/spinlock.h>
數據類型:?spinlock_t
初始化,?以下兩種方法:
??????靜態(編譯時):?spinlock_t?my_lock?=?SPIN_LOCK_UNLOCKED;
??????動態(運行時):?void?spin_lock_init(spinlock_t?*lock);
獲得鎖:?void?spin_lock(spinlock_t?*lock);
釋放鎖:?void?spin_unlock(spinlock_t?*lock);?
3.?自旋鎖其他API:
鎖定一個自旋鎖的函數實際有4個:
void?spin_lock(spinlock_t?*lock);?????/*?基本函數?*/
void?spin_lock_irqsave(spinlock_t?*lock,?unsigned?long?flags);/*?會在獲得自旋鎖之前禁止本地中斷,?而先前的中斷狀態保存在flags中?*/
void?spin_lock_irq(spinlock_t?*lock);/*?同上,?但無跟蹤標志?*/
void?spin_lock_bh(spinlock_t?*lock);/*?在獲得鎖之前禁止軟件中斷?*/
對應的,?也有4個unlock函數:
void?spin_unlock(spinlock_t?*lock);
/*?這里面的flags必須和傳遞給spinlock_lock_irqsave的是同一個變量,
?*?還必須在同一函數中調用,?否則代碼可能在某些架構上出現問題?*/
void?spin_unlock_irqstore(spinlock_t?*lock,?unsigned?long?flags);
void?spin_unlock_irq(spinlock_t?*lock);
void?spin_unlock_bh(spinlock_t?*lock);
同時,?還有2個非阻塞的自旋鎖操作:
int?spin_trylock(spinlock_t?*lock);??????/*成功獲得返回非0,否則返回0*/
int?spin_trylock_bh(spinlock_t?*lock);?/*成功獲得返回非0,否則返回0*/
對禁止中斷的情況,?沒有對應的"try"版本.
注意:《linux內核設計與實現》9.2節?自旋鎖?中論述:
自旋鎖更多的是為多處理器提供防止并發訪問所需要的保護機制。
在單處理器機器上,編譯的時候并不會加入自旋鎖。它僅僅被當作一個設置內核搶占機制是否被啟用的開關。如果禁止內核搶占,那么在編譯時自旋鎖會被完全剔除出內核。
觀察內核代碼發現:在自旋鎖與CONFIG_SMP和CONFIG_PREEMPT兩個配置選項有很大的關聯。
而在自旋鎖的各個操作函數中都會調用preempt_disable()來禁止搶占(若CONFIG_PREEMPT選項關閉,則preempt_disable為空操作)
內核搶占代碼使用自旋鎖作為非搶占區域的標記。
Linux?2.4.x及以前的版本都是非搶占式內核方式,如果編譯成單處理器系統,在同一時間只有一個進程在執行,除非它自己放棄,不然只有通過"中斷"才能中斷其執行。因此,在單處理器非搶占式內核中,如果需要修改某個重要的數據結構,或者執行某些關鍵代碼,只需要禁止中斷。
但是在對稱多處理器,僅僅禁止某個CPU的中斷是不夠的,當然我們也可以將所有CPU的中斷都禁止,但這樣做開銷很大,整個系統的性能會明顯下降。
此外,即使在單處理器上,如果內核是搶占式的,也可能出現不同進程上下文同時進入臨界區的情況。為此,Linux內核中提供了"自旋鎖(spinlock)"的同步機制。
?自旋鎖和信號量的比較
| 需求 | 建議的加鎖方法 |
| 低開銷加鎖 | 優先使用自旋鎖 |
| 短期加鎖 | 優先使用自旋鎖 |
| 長期加鎖 | 優先使用信號量 |
| 中斷上下文加鎖 | 使用自旋鎖 |
| 持有鎖需要睡眠 | 使用信號量 |
讀取/寫入自旋鎖
頭文件:?<linux/spinlock.h>
數據類型:?rwlock_t
初始化,?以下兩種方法:
rwlock_t?my_lock?=?RW_LOCK_UNLOCKED;????/*Static?way*/
rwlock_t?my_lock;
rw_lock_init(&my_lock);??/*Dynamic?way*/
讀取相關的自旋鎖的函數:
void?read_lock(rwlock_t?*lock);
void?read_lock_irqsave(rwlock_t?*lock,?unsigned?long?flags);
void?read_lock_irq(rwlock_t?*lock);/*?同上,?但無跟蹤標志?*/
void?read_lock_bh(rwlock_t?*lock);/*?在獲得鎖之前禁止軟件中斷?*/
void?read_unlock(rwlock_t?*lock);
voidread_unlock_irqrestore(rwlock_t?*lock,?unsigned?long?flags);
voidread_unlock_irq(rwlock_t?*lock);
voidread_unlock_bh(rwlock_t?*lock);
沒有read_trylock函數可用。
寫入相關的自旋鎖的函數:
void?write_lock(rwlock_t?*lock);
voidwrite_lock_irqsave(rwlock_t?*lock,?unsigned?long?flags);
voidwrite_lock_irq(rwlock_t?*lock);/*?同上,?但無跟蹤標志?*/
voidwrite_lock_bh(rwlock_t?*lock);/*?在獲得鎖之前禁止軟件中斷?*/
voidwrite_unlock(rwlock_t?*lock);
voidwrite_unlock_irqrestore(rwlock_t?*lock,?unsigned?long?flags);
voidwrite_unlock_irq(rwlock_t?*lock);
void?write_unlock_bh(rwlock_t?*lock);
排隊自旋鎖(FIFO?Ticket?Spinlock)
排隊自旋鎖(FIFO?Ticket?Spinlock)是?Linux?內核?2.6.25?版本中引入的一種新型自旋鎖,它解決了傳統自旋鎖由于無序競爭導致的“公平性”問題。
參考:Linux?內核的排隊自旋鎖(FIFO?Ticket?Spinlock)
內核處理方式我的理解如下:
將自旋鎖的計數整數分成兩個域:Next域和Owner域,兩個域的位數相同。分別保存未來鎖申請者和當前鎖持有者的票據序號(Ticket?Number),初始時Next?=?Owner?=?0。
當申請者申請排隊自旋鎖時,它會獲得一個票據號,是當前Next值,然后Next增1;然后比較申請者的票據號和Owner值,若相等,則申請者可以獲得該鎖,否則忙等待;
當申請者處理完后釋放排隊自旋鎖時,會使該鎖的Owner值增1。等待的下一個申請者會發現這一變化,從忙等待狀態中退出,并獲得該鎖。
線程將嚴格地按照申請順序拿到Next票據號,然后隨著Owner的增加,票據號?=?Owner的申請者依次獲取排隊自旋鎖,從而完全解決了“不公平”問題。
排序自旋鎖和銀行的拿號排隊的原理是相同的。
死鎖
產生死鎖的原因:資源競爭以及進程推進順序非法
產生死鎖的4個必要條件:
·?互斥條件
·?請求保持條件
·?不可剝奪條件
·?環路條件
死鎖的預防:預先靜態分配法?和?資源有序分配法
免鎖方法
1、循環緩沖區——免鎖算法
在設備驅動程序中使用相當普遍,特別是網絡適配器(處理交換數據)
一個生產者將數據放入數組的結尾,消費者從數組的另一端移走數據,當到達數組尾部時,生產者繞回到數組頭部。沒有多個生產者或消費者的情況下,循環緩沖區不需要加鎖。
內核中有一個通用的循環緩沖區實現,參閱<linux/kfifo.h>
參考:Linux設備驅動程序學習(3-補)-Linux中的循環緩沖區
2、原子變量
有時,共享的資源可能恰好是個簡單的整數值。
內核提供了一種原子的整數類型,稱為:atomic_t
定義在:<asm/atomic.h>
這是一個與CPU架構有關的變量。
atomic_t數據項必須只能通過下面的函數來訪問
初始化:
void?atomic_set(atomic_t?*v,?int?i);
atomic_t?v?=?ATOMIC_INIT(0);
讀取變量值:
int?atomic_read(atomic_t?*v);
增減操作:(注意不返回操作后的結果)
void?atomic_add(int?i,?atomic_t?*v);??/*?v=+i?*/
void?atomic_sub(int?i,?atomic_t?*v);?/*?v=-i?*/
void?atomic_inc(atomic_t?*v);???/*?v++?*/
void?atomic_dec(atomic_t?*v);??/*?v--?*/
附加操作:
操作后并測試原子值,若為0,則返回true,否則返回false
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);
操作后并測試原子值,若為負,則返回true,否則返回false
int?atomic_add_negative(int?i,?atomic_t?*v);
操作后并返回原子值給調用者
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);
3、位操作
內核提供了一組可原子地修改和測試單個位的函數。
原子位操作只要硬件允許,就可以使用單個機器指令來執行,并且不需要禁止中斷。所以位操作也是依賴于具體的架構,在<asm/bitops.h>中聲明
nr通常定義為int型或unsigned?long;addr通常是unsigned?long*?或?void*
void?set_bit(nr,?void?*addr);???/*設置addr指向的數據項的第nr位*/
void?clear_bit(nr,?void?*addr);?/*清除addr指向的數據項的第nr位*/
void?change_bit(nr,?void?*addr);/*切換addr指向的數據項的第nr位*/
test_bit(nr,?void?*addr);/*返回指定位的當前值,唯一不必原子方式實現的位操作函數*/
int?test_and_set_bit(nr,?void?*addr);??/*設定指定位,并返回指定位的先前值*/
int?test_and_clear_bit(nr,?void?*addr);/*清除指定位,并返回指定位的先前值*/
int?test_and_change_bit(nr,?void?*addr);/*切換指定位,并返回指定位的先前值*/
在新代碼中應該優先使用自旋鎖,因為自旋鎖已經很好的調試過,并且易讀性比位操作更好。
4、順序鎖(seqlock)
當要保護的資源很小、很簡單、會頻繁被訪問而且寫入訪問很少發生且必須快速時,就可以使用seqlock。
seqlock通常不能用于保護包含有指針的數據結構。
seqlock在<linux/seqlock.h>中定義。
seqlock_t?lock1?=?SEQLOCK_UNLOCKED;
seqlock_t?lock2;
seqlock_init(&lock2);
seqlock允許讀取者對資源的自由訪問,但需要讀取者檢查是否和寫入著發生沖突,沖突發生時,需要重試訪問資源。讀取者的代碼如下:
unsigned?int?seq;
do?{
???????seq?=?read_seqbegin(&the_lock);????/*?獲得一個(無符號的)整數順序值seq*/
??????/*?Do?what?you?need?to?do?*/
}?while?read_seqretry(&the_lock,?seq);??/*退出時,seq和當前值比較,若不等則重試*/
在中斷處理例程中,使用IRQ安全版本:
unsigned?int?read_seqbegin_irqsave(seqlock_t?*lock,?unsigned?long?flags);
int?read_seqretry_irqrestore(seqlock_t?*lock,?unsigned?int?seq,?unsigned?long?flags);
寫入者在進入由seqlock保護的臨界區時獲得一個互斥鎖。(寫入鎖使用自旋鎖實現)
void?write_seqlock(seqlock_t?*lock);
釋放鎖:
void?write_sequnlock(seqlock_t?*lock);
常用變種:
void?write_seqlock_irqsave(seqlock_t?*lock,?unsigned?long?flags);
void?write_seqlock_irq(seqlock_t?*lock);
void?write_seqlock_bh(seqlock_t?*lock);
void?write_sequnlock_irqrestore(seqlock_t?*lock,?unsigned?long?flags);
void?write_sequnlock_irq(seqlock_t?*lock);
void?write_sequnlock_bh(seqlock_t?*lock);
5、讀取-復制-更新(read-copy-update,RCU)
一種高級的互斥機制,它針對經常發生讀取而很少寫入的情形的優化。被保護的資源應該通過指針訪問,而對這些資源的訪問應該通過指針進行,而對這些資源的引用必須僅有原子代碼擁有。在需要修改該數據結構時,寫入線程首先復制,然后修改副本,子后用新的版本替代相關指針。當內核確信老的版本上沒有其他引用時,可以釋放老的版本。
參見RCU的白皮書
(http://www.rdrop.com/users/paulmck/rclock/intro/rclock_intro.html).
以及內核頭文件?<linux/rcupdate.h>
應用實例參考網絡路由表
參考文獻:
1.Linux?Device?Driver?3rd
2.Linux?Kernel?Develoment?2nd
3.Linux內核的同步機制
總結
以上是生活随笔為你收集整理的linux的同步与互斥的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 云计算的关键技术
- 下一篇: 设计模式在C语言中的应用--读nginx