如果一個進程正處於臨界區時被非自願搶占了,而且這個新調度的進程隨後也進入同一個臨界區,前後兩個進程之間就會產生競爭,其實兩者並不真是同時發生的,但它們相互交叉進行,所以叫做偽並發。如果有一台支持對稱多處理器(SMP)的機器,那麼兩個進程就可以真正地在臨界區中同時執行,這就叫真並發。
並發產生的原因有:第一,中斷;第二,軟中斷和tasklet;第三,內核搶占;第四,睡眠及與用戶空間的同步;第五,對稱多處理。
設計鎖在開始階段都很粗,但當鎖的爭用問題變得嚴重時,設計就向更加細的加鎖方向發展。那麼什麼樣的數據需要加鎖呢?其一,如果有其他執行線程可以訪問這些數據,那麼就給這些數據加上某種形式的鎖;。其二,如果任何其他什麼東西都能看到它,那麼就要鎖住它。其三,幾乎訪問所有的內核全局變量和共享數據都需要某種形式的同步方法。
每個線程都在等待其中的一個資源,但所有的資源都已經被占用了,所有線程都在相互等待,但它們永遠不會釋放已經占有的資源,於是任何線程都無法繼續,這就是死鎖。
死鎖產生的原因主要有三點:第一,使用不可剝奪設備(比如打印機、掃描儀);第二,進程在已經占有一個設備的基礎上又提出使用新設備,而該新設備又被其他進程占有了,導致進程運行暫停;第三,進程之間連環等待資源。如A等B,B等C,C等A。
避免死鎖的辦法:其一,按順序加鎖,如果有兩個或多個鎖曾在同一時間裡被請求,那麼以後其他函數請求它們時也必須按照前次加鎖的順序加鎖。其二,防止發生饑餓,也就是代碼要保證能結束。其三,不要重復請求同一個鎖。其四,所以進程在開始運行前,申請需要占用的全部設備,防止中途因申請不到而暫停。其五,在進程申請不到繼續運行所需要的設備時,必須釋放已經占用的資源。
原子操作
內核提供兩組原子操作的接口,一個針對整數,一個針對位進行操作。在編寫代碼時,能使用原子操作時,就盡量不要使用復雜的加鎖機制。非原子位函數與原子位函數的操作完全相同,但是前者不保證原子性,且其名前綴多兩個下劃線。例如與test_bit函數對應的非原子形式__test_bit函數,如果你不需要原子性操作(比如你已經鎖保護了自己的數據),那麼這些非原子位函數將比原子位函數執行得更快些。
自旋鎖
如果一個執行線程試圖獲得一個已經持有的自旋鎖,那麼該線程就會一直進行忙循環,旋轉,等待鎖重新可用,所以自旋鎖的作用是在短期內進行輕量級加鎖,持有自旋鎖的世界最好小於兩次上下文切換的耗時,否則還不如信號量呢。如果
寫和讀不能清楚分開,那麼使用一般自旋鎖就可以了,不要使用讀寫自旋鎖。總之,如果加鎖時間不長並且代碼不會睡眠(比如中斷處理程序),利用自旋鎖是最佳的。如果加鎖時間可能很長或者代碼在持有鎖時可能睡眠,那麼最好用信號量完成加鎖。
自旋鎖可用使用在中斷程序中,在中斷程序中使用自旋鎖前,一定要禁止本地中斷,否則中斷程序就會打斷正持有鎖的內核代碼,有可能去試圖爭用這個已經被持有的鎖,從而導致雙重請求死鎖。我們使用的最單純的自旋鎖是spin_lock和spin_unlock函數,而我們使用帶處理本地中斷鎖的是spin_lock_irqsave和spin_unlock_irqrestore函數。
自旋鎖和下半部的問題
由於下半部(或中斷)可以搶占進程上下文中的代碼,所以當下半部(或中斷)和進程上下文共享數據時,必須對進程上下文中的共享數據進行保護,所以需要加鎖的同時還有禁止下半部(或中斷)的執行。同類的tasklet不可能同時運行,所以對於同類tasklet中的共享數據不需要保護,但是當數據被兩個不同種類的tasklet共享時,就需要在訪問下半部中的數據前先獲得一個普通的自旋鎖,這裡不需要要禁止下半部分。對於軟中斷,無論是否同種類型,如果數據被軟中斷共享,那麼它必須得到鎖的保護,這裡也不需要禁止下半部。
信號量
信號量適用於鎖被長時間持有的情況,持有信號量的代碼可以被搶占,可以在持有信號量時去睡眠,但是當占用信號量的時候不能同時占有自旋鎖,因為在等待信號量時可能會睡眠,而在持有自旋鎖時是不允許睡眠的。
信號量可以同時允許任意數量的鎖持有者,通常只有一個持有者的信號量叫互斥信號量,我們常用dowm_inperruptible函數獲取信號量,它將把調用進程設置為TASK_INTERRUPTIBLE狀態進入睡眠,如果用dowm函數獲取信號量,它則將把調用進程設置為TASK_UNINTERRUPTIBLE狀態進入睡眠,但這樣進程在等待信號量的時候就不再響應信號了,所以,常用dowm_inperruptible函數獲取信號量,用up函數釋放信號量。