1. linux有沒有多進程間的讀寫鎖
Linux共享內存可以不用加鎖,不過需要一種機制來標記共享內存的讀寫狀態;也就是說要讓兩個進程知道:1)負責寫入的進程,必須知道當前共享內存是否可以寫入,上一次的寫入內容是否有被負責讀取的進程讀走;2)負責讀取的進程,必須知道當前共享內存是否需要讀取,防止重復讀取。一般的這種標記機制是通過以下方式來簡單實現:1)通過讀寫鎖來控制;2)共享內存上設置一個地方,專門存放當前共享內存的讀寫狀態;
2. linux rcu鎖問題怎麼查
眾所周知,為了保護共享數據,需要一些同步機制,如自旋鎖(spinlock),讀寫鎖(rwlock),它們使用起來非常簡單,而且是一種很有效的同步機制,在UNIX系統和Linux系統中得到了廣泛的使用。但是隨著計算機硬體的快速發展,獲得這種鎖的開銷相對於CPU的速度在成倍地增加,原因很簡單,CPU的速度與訪問內存的速度差距越來越大,而這種鎖使用了原子操作指令,它需要原子地訪問內存,也就說獲得鎖的開銷與訪存速度相關,另外在大部分非x86架構上獲取鎖使用了內存柵(Memory Barrier),這會導致處理器流水線停滯或刷新,因此它的開銷相對於CPU速度而言就越來越大。
在操作系統中,數據一致性訪問是一個非常重要的部分,通常我們可以採用鎖機制實現數據的一致性訪問。例如,semaphore、spinlock機制,在訪問共享數據時,首先訪問鎖資源,在獲取鎖資源的前提下才能實現數據的訪問。這種原理很簡單,根本的思想就是在訪問臨界資源時,首先訪問一個全局的變數(鎖),通過全局變數的狀態來控制線程對臨界資源的訪問。但是,這種思想是需要硬體支持的,硬體需要配合實現全局變數(鎖)的讀-修改-寫,現代CPU都會提供這樣的原子化指令。採用鎖機制實現數據訪問的一致性存在如下兩個問題:
1、 效率問題。鎖機制的實現需要對內存的原子化訪問,這種訪問操作會破壞流水線操作,降低了流水線效率。這是影響性能的一個因素。另外,在採用讀寫鎖機制的情況下,寫鎖是排他鎖,無法實現寫鎖與讀鎖的並發操作,在某些應用下回降低性能。
2、 擴展性問題。當系統中CPU數量增多的時候,採用鎖機制實現數據的同步訪問效率偏低。並且隨著CPU數量的增多,效率降低,由此可見鎖機制實現的數據一致性訪問擴展性差。
為了解決上述問題,Linux中引進了RCU機制。該機制在多CPU的平台上比較適用,對於讀多寫少的應用尤其適用。RCU的思路實際上很簡單,下面對其進行描述:
1、 對於讀操作,可以直接對共享資源進行訪問,但是前提是需要CPU支持訪存操作的原子化,現代CPU對這一點都做了保證。但是RCU的讀操作上下文是不可搶占的(這一點在下面解釋),所以讀訪問共享資源時可以採用read_rcu_lock(),該函數的工作是停止搶占。
2、 對於寫操作,其需要將原來的老數據作一次備份(),然後對備份數據進行修改,修改完畢之後再用新數據更新老數據,更新老數據時採用了rcu_assign_pointer()宏,在該函數中首先屏障一下memory,然後修改老數據。這個操作完成之後,需要進行老數據資源的回收。操作線程向系統注冊回收方法,等待回收。採用數據備份的方法可以實現讀者與寫者之間的並發操作,但是不能解決多個寫著之間的同步,所以當存在多個寫者時,需要通過鎖機制對其進行互斥,也就是在同一時刻只能存在一個寫者。
3、 在RCU機制中存在一個垃圾回收的daemon,當共享資源被update之後,可以採用該daemon實現老數據資源的回收。回收時間點就是在update之前的所有的讀者全部退出。由此可見寫者在update之後是需要睡眠等待的,需要等待讀者完成操作,如果在這個時刻讀者被搶占或者睡眠,那麼很可能會導致系統死鎖。因為此時寫者在等待讀者,讀者被搶占或者睡眠,如果正在運行的線程需要訪問讀者和寫者已經佔用的資源,那麼死鎖的條件就很有可能形成了。
3. 分析怎樣實現的對共享存儲區的互斥和同步
你說的這個是遠程監控和備份,需要在PC1上登錄客戶端軟體通過花生殼或其他域名伺服器域名解析出去,然後你在PC2上面通過解析出來的域名登錄上去進行監控和存儲即可。不過遠程監控效果很一般,圖像也不連續,除非你是光線上網這樣才會好一點。
4. 如何對讀寫鎖進行處理
信號量強調的是線程(或進程)間的同步:「信號量用在多線程多任務同步的,一個線程完成了某一個動作就通過信號量告訴別的線程,別的線程再進行某些動作(大家都 在sem_wait的時候,就阻塞在那裡)。當信號量為單值信號量是,也可以完成一個資源的互斥訪問。
有名信號量:可以用於不同進程間或多線程間的互斥與同步
創建打開有名信號量
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
成功返回信號量指針;失敗返回SEM_FAILED,設置errnoname是文件路徑名,但不能寫成/tmp/a.sem這樣的形式,因為在linux下,sem都是在/dev/shm目錄下,可寫成"/mysem"或"mysem",創建出來的文件都 是"/dev/shm/sem.mysem",mode設置為0666,value設置為信號量的初始值.所需信號燈等已存在條件下指定O_CREAT|O_EXCL卻是個錯誤。
關閉信號量,進程終止時,會自動調用它
int sem_close(sem_t *sem);
成功返回0;失敗返回-1,設置errno
刪除信號量,立即刪除信號量名字,當其他進程都關閉它時,銷毀它
int sem_unlink(const char *name);
等待信號量,測試信號量的值,如果其值小於或等於0,那麼就等待(阻塞);一旦其值變為大於0就將它減1,並返回
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
成功返回0;失敗返回-1,設置errno
當信號量的值為0時,sem_trywait立即返回,設置errno為EAGAIN。如果被某個信號中斷,sem_wait會過早地返回,設置errno為EINTR
發出信號量,給它的值加1,然後喚醒正在等待該信號量的進程或線程
int sem_post(sem_t *sem);
成功返回0;失敗返回-1,不會改變它的值,設置errno,該函數是非同步信號安全的,可以在信號處理程序里調用它無名信號量,用於進程體內各線程間的互斥和同步,使用如下API(無名信號量,基於內存的信號量)
(1)、sem_init
功能:用於創建一個信號量,並初始化信號量的值。
頭文件:
函數原型: int sem_init (sem_t* sem, int pshared, unsigned int value);
函數傳入值: sem:信號量。pshared:決定信號量能否在幾個進程間共享。由於目前LINUX還沒有實現進程間共享信息量,所以這個值只能取0。
(2)其他函數。
int sem_wait (sem_t* sem);
int sem_trywait (sem_t* sem);
int sem_post (sem_t* sem);
int sem_getvalue (sem_t* sem);
int sem_destroy (sem_t* sem);
功能:sem_wait和sem_trywait相當於P操作,它們都能將信號量的值減一,兩者的區別在於若信號量的值小於零時,sem_wait將會阻塞進程,而sem_trywait則會立即返回。sem_post相當於V操作,它將信號量的值加一,同時發出喚醒的信號給等待的進程(或線程)。
sem_getvalue 得到信號量的值。
sem_destroy 摧毀信號量。
如果某個基於內存的信號燈是在不同進程間同步的,該信號燈必須存放在共享內存區中,這要只要該共享內存區存在,該信號燈就存在。
互斥鎖(又名互斥量)強調的是資源的訪問互斥:互斥鎖是用在多線程多任務互斥的,一個線程佔用了某一個資源,那麼別的線程就無法訪問,直到這個線程unlock,其他的線程才開始可以利用這個資源。比如對全局變數的訪問,有時要加鎖,操作完了,在解鎖。有的時候鎖和信號量會同時使用的」
也就是說,信號量不一定是鎖定某一個資源,而是流程上的概念,比如:有A,B兩個線程,B線程要等A線程完成某一任務以後再進行自己下面的步驟,這個任務並不一定是鎖定某一資源,還可以是進行一些計算或者數據處理之類。而線程互斥量則是「鎖住某一資源」的概念,在鎖定期間內,其他線程無法對被保護的數據進行操作。在有些情況下兩者可以互換。
在linux下, 線程的互斥量數據類型是pthread_mutex_t. 在使用前, 要對它進行初始化:
對於靜態分配的互斥量, 可以把它設置為PTHREAD_MUTEX_INITIALIZER, 或者調用pthread_mutex_init.
對於動態分配的互斥量, 在申請內存(malloc)之後, 通過pthread_mutex_init進行初始化, 並且在釋放內存(free)前需要調用pthread_mutex_destroy.
原型:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restric attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
頭文件:
返回值: 成功則返回0, 出錯則返回錯誤編號.
說明: 如果使用默認的屬性初始化互斥量, 只需把attr設為NULL. 其他值在以後講解.
首先說一下加鎖函數:
頭文件:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
返回值: 成功則返回0, 出錯則返回錯誤編號.
說 明: 具體說一下trylock函數, 這個函數是非阻塞調用模式, 也就是說, 如果互斥量沒被鎖住, trylock函數將把互斥量加鎖, 並獲得對共享資源的訪問許可權; 如果互斥量 被鎖住了, trylock函數將不會阻塞等待而直接返回EBUSY, 表示共享資源處於忙狀態.
再說一下解所函數:
頭文件:
原型: int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值: 成功則返回0, 出錯則返回錯誤編號.
條件變數常與互斥鎖同時使用,達到線程同步的目的:條件變數通過允許線程阻塞和等待另一個線程發送信號的方法彌補了互斥鎖的不足。在發 送信號時,如果沒有線程 等待在該條件變數上,那麼信號將丟失;而信號量有計數值,每次信號量post操作都會被記錄
互斥鎖必須是誰上鎖就由誰來解鎖,而信號量的wait和post操作不必由同一個線程執行。
2. 互斥鎖要麼被鎖住,要麼被解開,和二值信號量類似
3. sem_post是各種同步技巧中,唯一一個能在信號處理程序中安全調用的函數
4. 互斥鎖是為上鎖而優化的;條件變數是為等待而優化的; 信號量既可用於上鎖,也可用於等待,因此會有更多的開銷和更高的復雜性
5. 互斥鎖,條件變數都只用於同一個進程的各線程間,而信號量(有名信號量)可用於不同進程間的同步。當信號量用於進程間同步時,要求信號量建立在共享內存區。
6. 信號量有計數值,每次信號量post操作都會被記錄,而條件變數在發送信號時,如果沒有線程在等待該條件變數,那麼信號將丟失。
讀寫鎖
讀寫鎖與互斥量類似,不過讀寫鎖允許更高的並行性。互斥量要麼是鎖住狀態要麼是不加鎖狀態,而且一次只有一個線程可以對其加鎖。
讀寫鎖可以由三種狀態:讀模式下加鎖狀態、寫模式下加鎖狀態、不加鎖狀態。一次只有一個線程可以佔有寫模式的讀寫鎖,但是多個線程可以同時佔有讀模式的讀寫
鎖。
在讀寫鎖是寫加鎖狀態時,在這個鎖被解鎖之前,所有試圖對這個鎖加鎖的線程都會被阻塞。當讀寫鎖在讀加鎖狀態時,所有試圖以讀模式對它進行加鎖的線程都可以得到訪問權,但是如果線程希望以寫模式對此鎖進行加鎖,它必須阻塞直到所有的線程釋放讀鎖。雖然讀寫鎖的實現各不相同,但當讀寫鎖處於讀模式鎖住狀態時,如果有另外的線程試圖以寫模式加鎖,讀寫鎖通常會阻塞隨後的讀模式鎖請求。這樣可以避免讀模式鎖長期佔用,而等待的寫模式鎖請求一直得不到滿足。
讀寫鎖非常適合於對數據結構讀的次數遠大於寫的情況。當讀寫鎖在寫模式下時,它所保護的數據結構就可以被安全地修改,因為當前只有一個線程可以在寫模式下擁 有這個鎖。當讀寫鎖在讀狀態下時,只要線程獲取了讀模式下的讀寫鎖,該鎖所保護的數據結構可以被多個獲得讀模式鎖的線程讀取。
讀寫鎖也叫做共享-獨占鎖,當讀寫鎖以讀模式鎖住時,它是以共享模式鎖住的;當他以寫模式鎖住時,它是以獨占模式鎖住的。
初始化和銷毀:
#include
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
成功則返回0, 出錯則返回錯誤編號.
同互斥量以上, 在釋放讀寫鎖佔用的內存之前, 需要先通過thread_rwlock_destroy對讀寫鎖進行清理工作, 釋放由init分配的資源.
讀和寫:
#include
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
成功則返回0, 出錯則返回錯誤編號.
這3個函數分別實現獲取讀鎖, 獲取寫鎖和釋放鎖的操作. 獲取鎖的兩個函數是阻塞操作, 同樣, 非阻塞的函數為:
#include
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
成功則返回0, 出錯則返回錯誤編號.
非阻塞的獲取鎖操作, 如果可以獲取則返回0, 否則返回錯誤的EBUSY.
雖然讀寫鎖提高了並行性,但是就速度而言並不比互斥量快.
可能這也是即使有讀寫鎖存在還會使用互斥量的原因,因為他在速度方面略勝一籌。這就需要我們在寫程序的時候綜合考慮速度和並行性並找到一個折中。
比如: 假設使用互斥量需要0.5秒,使用讀寫鎖需要0.8秒。在類似學生管理系統這類中,可能百分之九十的時間都是查詢操作,那麼假如現在突然來個個20個請求,如果使用的是互斥量,那麼最後的那個查詢請求被滿足需要10後。這樣,估計沒人能受得了。而使用讀寫鎖,應為 讀鎖能夠多次獲得。所以所有的20個請求,每個請求都能在1秒左右得到滿足。
也就是說,在一些寫操作比較多或是本身需要同步的地方並不多的程序中我們應該使用互斥量,而在讀操作遠大於寫操作的一些程序中我們應該使用讀寫鎖來進行同步
條件變數(condition)
條件變數與互斥量一起使用時,允許線程以無競爭的方式等待特定的條件發生。
條件本身是由互斥量保護的。線程在改變條件狀態前必須首先鎖住互斥量,其它線程在獲得互斥量之前不會察覺到這種改變,因此必須鎖定互斥量以後才能計算條件。
條件的檢測是在互斥鎖的保護下進行的。如果一個條件為假,一個線程自動阻塞,並釋放等待狀態改變的互斥鎖。如果另一個線程改變了條件,它發信號給關聯的條件
變數,喚醒一個或多個等待它的線程,重新獲得互斥鎖,重新評價條件。如果兩進程共享可讀寫的內存,條件變數可以被用來實現這兩進程間的線程同步。
1. 初始化:
條件變數採用的數據類型是pthread_cond_t, 在使用之前必須要進行初始化, 這包括兩種方式:
靜態: 可以把常量PTHREAD_COND_INITIALIZER給靜態分配的條件變數.
動態: pthread_cond_init函數, 是釋放動態條件變數的內存空間之前, 要用pthread_cond_destroy對其進行清理.
#include
int pthread_cond_init(pthread_cond_t *restrict cond, pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
成功則返回0, 出錯則返回錯誤編號.
注意:條件變數佔用的空間並未被釋放。
當pthread_cond_init的attr參數為NULL時, 會創建一個默認屬性的條件變數; 非默認情況以後討論.
2. 等待條件:
#include
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restric mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict timeout);
成功則返回0, 出錯則返回錯誤編號.
這兩個函數分別是阻塞等待和超時等待.
等待條件函數等待條件變為真, 傳遞給pthread_cond_wait的互斥量對條件進行保護, 調用者把鎖住的互斥量傳遞給函數. 函數把調用線程放到等待條件的線程列表上, 然後對互斥量解鎖, 這兩個操作是原子的. 這樣 便關閉了條件檢查和線程進入休眠狀態等待條件改變這兩個操作之間的時間通道, 這樣線程就不會錯過條件的任何變化.
當pthread_cond_wait返回時, 互斥量再次被鎖住.
pthread_cond_wait函數的返回並不意味著條件的值一定發生了變化,必須重新檢查條件的值。
pthread_cond_wait函數返回時,相應的互斥鎖將被當前線程鎖定,即使是函數出錯返回。
阻塞在條件變數上的線程被喚醒以後,直到pthread_cond_wait()函數返回之前條件的值都有可能發生變化。所以函數返回以後,在鎖定相應的互斥鎖之前,必須重新測試條 件值。最好的測試方法是循環調用pthread_cond_wait函數,並把滿足條件的表達式置為循環的終止條件。如:
pthread_mutex_lock();
while (condition_is_false)
pthread_cond_wait();
pthread_mutex_unlock();
阻塞在同一個條件變數上的不同線程被釋放的次序是不一定的。
注意:pthread_cond_wait()函數是退出點,如果在調用這個函數時,已有一個掛起的退出請求,且線程允許退出,這個線程將被終止並開始執行善後處理函數,而這時和條 件變數相關的互斥鎖仍將處在鎖定狀態。
pthread_cond_timedwait函數到了一定的時間,即使條件未發生也會解除阻塞。這個時間由參數abstime指定。函數返回時,相應的互斥鎖往往是鎖定的,即使是函數出錯返回。
注意:pthread_cond_timedwait函數也是退出點。
超時時間參數是指一天中的某個時刻。使用舉例:
pthread_timestruc_t to;
to.tv_sec = time(NULL) + TIMEOUT;
to.tv_nsec = 0;
超時返回的錯誤碼是ETIMEDOUT。
3. 通知條件:
#include
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
成功則返回0, 出錯則返回錯誤編號.
這兩個函數用於通知線程條件已經滿足. 調用這兩個函數, 也稱向線程或條件發送信號. 必須注意, 一定要在改變條件狀態以後再給線程發送信號.
5. 如何實現linux下多線程之間的互斥與同步
Linux設備驅動中必須解決的一個問題是多個進程對共享資源的並發訪問,並發訪問會導致競態,linux提供了多種解決競態問題的方式,這些方式適合不同的應用場景。
Linux內核是多進程、多線程的操作系統,它提供了相當完整的內核同步方法。內核同步方法列表如下:
中斷屏蔽
原子操作
自旋鎖
讀寫自旋鎖
順序鎖
信號量
讀寫信號量
BKL(大內核鎖)
Seq鎖
一、並發與競態:
定義:
並發(concurrency)指的是多個執行單元同時、並行被執行,而並發的執行單元對共享資源(硬體資源和軟體上的全局變數、靜態變數等)的訪問則很容易導致競態(race conditions)。
在linux中,主要的競態發生在如下幾種情況:
1、對稱多處理器(SMP)多個CPU
特點是多個CPU使用共同的系統匯流排,因此可訪問共同的外設和存儲器。
2、單CPU內進程與搶占它的進程
3、中斷(硬中斷、軟中斷、Tasklet、底半部)與進程之間
只要並發的多個執行單元存在對共享資源的訪問,競態就有可能發生。
如果中斷處理程序訪問進程正在訪問的資源,則競態也會會發生。
多個中斷之間本身也可能引起並發而導致競態(中斷被更高優先順序的中斷打斷)。
解決競態問題的途徑是保證對共享資源的互斥訪問,所謂互斥訪問就是指一個執行單元在訪問共享資源的時候,其他的執行單元都被禁止訪問。
訪問共享資源的代碼區域被稱為臨界區,臨界區需要以某種互斥機制加以保護,中斷屏蔽,原子操作,自旋鎖,和信號量都是linux設備驅動中可採用的互斥途徑。
臨界區和競爭條件:
所謂臨界區(critical regions)就是訪問和操作共享數據的代碼段,為了避免在臨界區中並發訪問,編程者必須保證這些代碼原子地執行——也就是說,代碼在執行結束前不可被打斷,就如同整個臨界區是一個不可分割的指令一樣,如果兩個執行線程有可能處於同一個臨界區中,那麼就是程序包含一個bug,如果這種情況發生了,我們就稱之為競爭條件(race conditions),避免並發和防止競爭條件被稱為同步。
死鎖:
死鎖的產生需要一定條件:要有一個或多個執行線程和一個或多個資源,每個線程都在等待其中的一個資源,但所有的資源都已經被佔用了,所有線程都在相互等待,但它們永遠不會釋放已經佔有的資源,於是任何線程都無法繼續,這便意味著死鎖的發生。
二、中斷屏蔽
在單CPU范圍內避免競態的一種簡單方法是在進入臨界區之前屏蔽系統的中斷。
由於linux內核的進程調度等操作都依賴中斷來實現,內核搶占進程之間的並發也就得以避免了。
中斷屏蔽的使用方法:
local_irq_disable()//屏蔽中斷
//臨界區
local_irq_enable()//開中斷
特點:
由於linux系統的非同步IO,進程調度等很多重要操作都依賴於中斷,在屏蔽中斷期間所有的中斷都無法得到處理,因此長時間的屏蔽是很危險的,有可能造成數據丟失甚至系統崩潰,這就要求在屏蔽中斷之後,當前的內核執行路徑應當盡快地執行完臨界區的代碼。
中斷屏蔽只能禁止本CPU內的中斷,因此,並不能解決多CPU引發的競態,所以單獨使用中斷屏蔽並不是一個值得推薦的避免競態的方法,它一般和自旋鎖配合使用。
三、原子操作
定義:原子操作指的是在執行過程中不會被別的代碼路徑所中斷的操作。
(原子原本指的是不可分割的微粒,所以原子操作也就是不能夠被分割的指令)
(它保證指令以「原子」的方式執行而不能被打斷)
原子操作是不可分割的,在執行完畢不會被任何其它任務或事件中斷。在單處理器系統(UniProcessor)中,能夠在單條指令中完成的操作都可以認為是" 原子操作",因為中斷只能發生於指令之間。這也是某些CPU指令系統中引入了test_and_set、test_and_clear等指令用於臨界資源互斥的原因。但是,在對稱多處理器(Symmetric Multi-Processor)結構中就不同了,由於系統中有多個處理器在獨立地運行,即使能在單條指令中完成的操作也有可能受到干擾。我們以decl (遞減指令)為例,這是一個典型的"讀-改-寫"過程,涉及兩次內存訪問。
通俗理解:
原子操作,顧名思義,就是說像原子一樣不可再細分。一個操作是原子操作,意思就是說這個操作是以原子的方式被執行,要一口氣執行完,執行過程不能夠被OS的其他行為打斷,是一個整體的過程,在其執行過程中,OS的其它行為是插不進來的。
分類:linux內核提供了一系列函數來實現內核中的原子操作,分為整型原子操作和位原子操作,共同點是:在任何情況下操作都是原子的,內核代碼可以安全的調用它們而不被打斷。
原子整數操作:
針對整數的原子操作只能對atomic_t類型的數據進行處理,在這里之所以引入了一個特殊的數據類型,而沒有直接使用C語言的int型,主要是出於兩個原因:
第一、讓原子函數只接受atomic_t類型的操作數,可以確保原子操作只與這種特殊類型數據一起使用,同時,這也確保了該類型的數據不會被傳遞給其它任何非原子函數;
第二、使用atomic_t類型確保編譯器不對相應的值進行訪問優化——這點使得原子操作最終接收到正確的內存地址,而不是一個別名,最後就是在不同體系結構上實現原子操作的時候,使用atomic_t可以屏蔽其間的差異。
原子整數操作最常見的用途就是實現計數器。
另一點需要說明原子操作只能保證操作是原子的,要麼完成,要麼不完成,不會有操作一半的可能,但原子操作並不能保證操作的順序性,即它不能保證兩個操作是按某個順序完成的。如果要保證原子操作的順序性,請使用內存屏障指令。
atomic_t和ATOMIC_INIT(i)定義
typedef struct { volatile int counter; } atomic_t;
#define ATOMIC_INIT(i) { (i) }
在你編寫代碼的時候,能使用原子操作的時候,就盡量不要使用復雜的加鎖機制,對多數體系結構來講,原子操作與更復雜的同步方法相比較,給系統帶來的開銷小,對高速緩存行的影響也小,但是,對於那些有高性能要求的代碼,對多種同步方法進行測試比較,不失為一種明智的作法。
原子位操作:
針對位這一級數據進行操作的函數,是對普通的內存地址進行操作的。它的參數是一個指針和一個位號。
為方便其間,內核還提供了一組與上述操作對應的非原子位函數,非原子位函數與原子位函數的操作完全相同,但是,前者不保證原子性,且其名字前綴多兩個下劃線。例如,與test_bit()對應的非原子形式是_test_bit(),如果你不需要原子性操作(比如,如果你已經用鎖保護了自己的數據),那麼這些非原子的位函數相比原子的位函數可能會執行得更快些。
四、自旋鎖
自旋鎖的引入:
如 果每個臨界區都能像增加變數這樣簡單就好了,可惜現實不是這樣,而是臨界區可以跨越多個函數,例如:先得從一個數據結果中移出數據,對其進行格式轉換和解 析,最後再把它加入到另一個數據結構中,整個執行過程必須是原子的,在數據被更新完畢之前,不能有其他代碼讀取這些數據,顯然,簡單的原子操作是無能為力 的(在單處理器系統(UniProcessor)中,能夠在單條指令中完成的操作都可以認為是" 原子操作",因為中斷只能發生於指令之間),這就需要使用更為復雜的同步方法——鎖來提供保護。
自旋鎖的介紹:
Linux內核中最常見的鎖是自旋鎖(spin lock),自旋鎖最多隻能被一個可執行線程持有,如果一個執行線程試圖獲得一個被爭用(已經被持有)的自旋鎖,那麼該線程就會一直進行忙循環—旋轉—等待鎖重新可用,要是鎖未被爭用,請求鎖的執行線程便能立刻得到它,繼續執行,在任意時間,自旋鎖都可以防止多於一個的執行線程同時進入理解區,注意同一個鎖可以用在多個位置—例如,對於給定數據的所有訪問都可以得到保護和同步。
一個被爭用的自旋鎖使得請求它的線程在等待鎖重新可用時自旋(特別浪費處理器時間),所以自旋鎖不應該被長時間持有,事實上,這點正是使用自旋鎖的初衷,在短期間內進行輕量級加鎖,還可以採取另外的方式來處理對鎖的爭用:讓請求線程睡眠,直到鎖重新可用時再喚醒它,這樣處理器就不必循環等待,可以去執行其他代碼,這也會帶來一定的開銷——這里有兩次明顯的上下文切換, 被阻塞的線程要換出和換入。因此,持有自旋鎖的時間最好小於完成兩次上下文切換的耗時,當然我們大多數人不會無聊到去測量上下文切換的耗時,所以我們讓持 有自旋鎖的時間應盡可能的短就可以了,信號量可以提供上述第二種機制,它使得在發生爭用時,等待的線程能投入睡眠,而不是旋轉。
自旋鎖可以使用在中斷處理程序中(此處不能使用信號量,因為它們會導致睡眠),在中斷處理程序中使用自旋鎖時,一定要在獲取鎖之前,首先禁止本地中斷(在 當前處理器上的中斷請求),否則,中斷處理程序就會打斷正持有鎖的內核代碼,有可能會試圖去爭用這個已經持有的自旋鎖,這樣以來,中斷處理程序就會自旋, 等待該鎖重新可用,但是鎖的持有者在這個中斷處理程序執行完畢前不可能運行,這正是我們在前一章節中提到的雙重請求死鎖,注意,需要關閉的只是當前處理器上的中斷,如果中斷發生在不同的處理器上,即使中斷處理程序在同一鎖上自旋,也不會妨礙鎖的持有者(在不同處理器上)最終釋放鎖。
自旋鎖的簡單理解:
理解自旋鎖最簡單的方法是把它作為一個變數看待,該變數把一個臨界區或者標記為「我當前正在運行,請稍等一會」或者標記為「我當前不在運行,可以被使用」。如果A執行單元首先進入常式,它將持有自旋鎖,當B執行單元試圖進入同一個常式時,將獲知自旋鎖已被持有,需等到A執行單元釋放後才能進入。
自旋鎖的API函數:
其實介紹的幾種信號量和互斥機制,其底層源碼都是使用自旋鎖,可以理解為自旋鎖的再包裝。所以從這里就可以理解為什麼自旋鎖通常可以提供比信號量更高的性能。
自旋鎖是一個互斥設備,他只能會兩個值:「鎖定」和「解鎖」。它通常實現為某個整數之中的單個位。
「測試並設置」的操作必須以原子方式完成。
任何時候,只要內核代碼擁有自旋鎖,在相關CPU上的搶占就會被禁止。
適用於自旋鎖的核心規則:
(1)任何擁有自旋鎖的代碼都必須使原子的,除服務中斷外(某些情況下也不能放棄CPU,如中斷服務也要獲得自旋鎖。為了避免這種鎖陷阱,需要在擁有自旋鎖時禁止中斷),不能放棄CPU(如休眠,休眠可發生在許多無法預期的地方)。否則CPU將有可能永遠自旋下去(死機)。
(2)擁有自旋鎖的時間越短越好。
需 要強調的是,自旋鎖別設計用於多處理器的同步機制,對於單處理器(對於單處理器並且不可搶占的內核來說,自旋鎖什麼也不作),內核在編譯時不會引入自旋鎖 機制,對於可搶占的內核,它僅僅被用於設置內核的搶占機制是否開啟的一個開關,也就是說加鎖和解鎖實際變成了禁止或開啟內核搶占功能。如果內核不支持搶 占,那麼自旋鎖根本就不會編譯到內核中。
內核中使用spinlock_t類型來表示自旋鎖,它定義在:
typedef struct {
raw_spinlock_t raw_lock;
#if defined(CONFIG_PREEMPT) && defined(CONFIG_SMP)
unsigned int break_lock;
#endif
} spinlock_t;
對於不支持SMP的內核來說,struct raw_spinlock_t什麼也沒有,是一個空結構。對於支持多處理器的內核來說,struct raw_spinlock_t定義為
typedef struct {
unsigned int slock;
} raw_spinlock_t;
slock表示了自旋鎖的狀態,「1」表示自旋鎖處於解鎖狀態(UNLOCK),「0」表示自旋鎖處於上鎖狀態(LOCKED)。
break_lock表示當前是否由進程在等待自旋鎖,顯然,它只有在支持搶占的SMP內核上才起作用。
自旋鎖的實現是一個復雜的過程,說它復雜不是因為需要多少代碼或邏輯來實現它,其實它的實現代碼很少。自旋鎖的實現跟體系結構關系密切,核心代碼基本也是由匯編語言寫成,與體協結構相關的核心代碼都放在相關的目錄下,比如。對於我們驅動程序開發人員來說,我們沒有必要了解這么spinlock的內部細節,如果你對它感興趣,請參考閱讀Linux內核源代碼。對於我們驅動的spinlock介面,我們只需包括頭文件。在我們詳細的介紹spinlock的API之前,我們先來看看自旋鎖的一個基本使用格式:
#include
spinlock_t lock = SPIN_LOCK_UNLOCKED;
spin_lock(&lock);
....
spin_unlock(&lock);
從使用上來說,spinlock的API還很簡單的,一般我們會用的的API如下表,其實它們都是定義在中的宏介面,真正的實現在中
#include
SPIN_LOCK_UNLOCKED
DEFINE_SPINLOCK
spin_lock_init( spinlock_t *)
spin_lock(spinlock_t *)
spin_unlock(spinlock_t *)
spin_lock_irq(spinlock_t *)
spin_unlock_irq(spinlock_t *)
spin_lock_irqsace(spinlock_t *,unsigned long flags)
spin_unlock_irqsace(spinlock_t *, unsigned long flags)
spin_trylock(spinlock_t *)
spin_is_locked(spinlock_t *)
• 初始化
spinlock有兩種初始化形式,一種是靜態初始化,一種是動態初始化。對於靜態的spinlock對象,我們用 SPIN_LOCK_UNLOCKED來初始化,它是一個宏。當然,我們也可以把聲明spinlock和初始化它放在一起做,這就是 DEFINE_SPINLOCK宏的工作,因此,下面的兩行代碼是等價的。
DEFINE_SPINLOCK (lock);
spinlock_t lock = SPIN_LOCK_UNLOCKED;
spin_lock_init 函數一般用來初始化動態創建的spinlock_t對象,它的參數是一個指向spinlock_t對象的指針。當然,它也可以初始化一個靜態的沒有初始化的spinlock_t對象。
spinlock_t *lock
......
spin_lock_init(lock);
• 獲取鎖
內核提供了三個函數用於獲取一個自旋鎖。
spin_lock:獲取指定的自旋鎖。
spin_lock_irq:禁止本地中斷並獲取自旋鎖。
spin_lock_irqsace:保存本地中斷狀態,禁止本地中斷並獲取自旋鎖,返回本地中斷狀態。
自旋鎖是可以使用在中斷處理程序中的,這時需要使用具有關閉本地中斷功能的函數,我們推薦使用 spin_lock_irqsave,因為它會保存加鎖前的中斷標志,這樣就會正確恢復解鎖時的中斷標志。如果spin_lock_irq在加鎖時中斷是關閉的,那麼在解鎖時就會錯誤的開啟中斷。
另外兩個同自旋鎖獲取相關的函數是:
spin_trylock():嘗試獲取自旋鎖,如果獲取失敗則立即返回非0值,否則返回0。
spin_is_locked():判斷指定的自旋鎖是否已經被獲取了。如果是則返回非0,否則,返回0。
• 釋放鎖
同獲取鎖相對應,內核提供了三個相對的函數來釋放自旋鎖。
spin_unlock:釋放指定的自旋鎖。
spin_unlock_irq:釋放自旋鎖並激活本地中斷。
spin_unlock_irqsave:釋放自旋鎖,並恢復保存的本地中斷狀態。
五、讀寫自旋鎖
如 果臨界區保護的數據是可讀可寫的,那麼只要沒有寫操作,對於讀是可以支持並發操作的。對於這種只要求寫操作是互斥的需求,如果還是使用自旋鎖顯然是無法滿 足這個要求(對於讀操作實在是太浪費了)。為此內核提供了另一種鎖-讀寫自旋鎖,讀自旋鎖也叫共享自旋鎖,寫自旋鎖也叫排他自旋鎖。
讀寫自旋鎖是一種比自旋鎖粒度更小的鎖機制,它保留了「自旋」的概念,但是在寫操作方面,只能最多有一個寫進程,在讀操作方面,同時可以有多個讀執行單元,當然,讀和寫也不能同時進行。
讀寫自旋鎖的使用也普通自旋鎖的使用很類似,首先要初始化讀寫自旋鎖對象:
// 靜態初始化
rwlock_t rwlock = RW_LOCK_UNLOCKED;
//動態初始化
rwlock_t *rwlock;
...
rw_lock_init(rwlock);
在讀操作代碼里對共享數據獲取讀自旋鎖:
read_lock(&rwlock);
...
read_unlock(&rwlock);
在寫操作代碼里為共享數據獲取寫自旋鎖:
write_lock(&rwlock);
...
write_unlock(&rwlock);
需要注意的是,如果有大量的寫操作,會使寫操作自旋在寫自旋鎖上而處於寫飢餓狀態(等待讀自旋鎖的全部釋放),因為讀自旋鎖會自由的獲取讀自旋鎖。
讀寫自旋鎖的函數類似於普通自旋鎖,這里就不一一介紹了,我們把它列在下面的表中。
RW_LOCK_UNLOCKED
rw_lock_init(rwlock_t *)
read_lock(rwlock_t *)
read_unlock(rwlock_t *)
read_lock_irq(rwlock_t *)
read_unlock_irq(rwlock_t *)
read_lock_irqsave(rwlock_t *, unsigned long)
read_unlock_irqsave(rwlock_t *, unsigned long)
write_lock(rwlock_t *)
write_unlock(rwlock_t *)
write_lock_irq(rwlock_t *)
write_unlock_irq(rwlock_t *)
write_lock_irqsave(rwlock_t *, unsigned long)
write_unlock_irqsave(rwlock_t *, unsigned long)
rw_is_locked(rwlock_t *)
六、順序瑣
順序瑣(seqlock)是對讀寫鎖的一種優化,若使用順序瑣,讀執行單元絕不會被寫執行單元阻塞,也就是說,讀執行單元可以在寫執行單元對被順序瑣保護的共享資源進行寫操作時仍然可以繼續讀,而不必等待寫執行單元完成寫操作,寫執行單元也不需要等待所有讀執行單元完成讀操作才去進行寫操作。
但是,寫執行單元與寫執行單元之間仍然是互斥的,即如果有寫執行單元在進行寫操作,其它寫執行單元必須自旋在哪裡,直到寫執行單元釋放了順序瑣。
如果讀執行單元在讀操作期間,寫執行單元已經發生了寫操作,那麼,讀執行單元必須重新讀取數據,以便確保得到的數據是完整的,這種鎖在讀寫同時進行的概率比較小時,性能是非常好的,而且它允許讀寫同時進行,因而更大的提高了並發性,
注意,順序瑣由一個限制,就是它必須被保護的共享資源不含有指針,因為寫執行單元可能使得指針失效,但讀執行單元如果正要訪問該指針,將導致Oops。
七、信號量
Linux中的信號量是一種睡眠鎖,如果有一個任務試圖獲得一個已經被佔用的信號量時,信號量會將其推進一個等待隊列,然後讓其睡眠,這時處理器能重獲自由,從而去執行其它代碼,當持有信號量的進程將信號量釋放後,處於等待隊列中的哪個任務被喚醒,並獲得該信號量。
信號量,或旗標,就是我們在操作系統里學習的經典的P/V原語操作。
P:如果信號量值大於0,則遞減信號量的值,程序繼續執行,否則,睡眠等待信號量大於0。
V:遞增信號量的值,如果遞增的信號量的值大於0,則喚醒等待的進程。
信號量的值確定了同時可以有多少個進程可以同時進入臨界區,如果信號量的初始值始1,這信號量就是互斥信號量(MUTEX)。對於大於1的非0值信號量,也可稱為計數信號量(counting semaphore)。對於一般的驅動程序使用的信號量都是互斥信號量。
類似於自旋鎖,信號量的實現也與體系結構密切相關,具體的實現定義在頭文件中,對於x86_32系統來說,它的定義如下:
struct semaphore {
atomic_t count;
int sleepers;
wait_queue_head_t wait;
};
信號量的初始值count是atomic_t類型的,這是一個原子操作類型,它也是一個內核同步技術,可見信號量是基於原子操作的。我們會在後面原子操作部分對原子操作做詳細介紹。
信號量的使用類似於自旋鎖,包括創建、獲取和釋放。我們還是來先展示信號量的基本使用形式:
static DECLARE_MUTEX(my_sem);
......
if (down_interruptible(&my_sem))
{
return -ERESTARTSYS;
}
......
up(&my_sem)
Linux內核中的信號量函數介面如下:
static DECLARE_SEMAPHORE_GENERIC(name, count);
static DECLARE_MUTEX(name);
seam_init(struct semaphore *, int);
init_MUTEX(struct semaphore *);
init_MUTEX_LOCKED(struct semaphore *)
down_interruptible(struct semaphore *);
down(struct semaphore *)
down_trylock(struct semaphore *)
up(struct semaphore *)
• 初始化信號量
信號量的初始化包括靜態初始化和動態初始化。靜態初始化用於靜態的聲明並初始化信號量。
static DECLARE_SEMAPHORE_GENERIC(name, count);
static DECLARE_MUTEX(name);
對於動態聲明或創建的信號量,可以使用如下函數進行初始化:
seam_init(sem, count);
init_MUTEX(sem);
init_MUTEX_LOCKED(struct semaphore *)
顯然,帶有MUTEX的函數始初始化互斥信號量。LOCKED則初始化信號量為鎖狀態。
• 使用信號量
信號量初始化完成後我們就可以使用它了
down_interruptible(struct semaphore *);
down(struct semaphore *)
down_trylock(struct semaphore *)
up(struct semaphore *)
down函數會嘗試獲取指定的信號量,如果信號量已經被使用了,則進程進入不可中斷的睡眠狀態。down_interruptible則會使進程進入可中斷的睡眠狀態。關於進程狀態的詳細細節,我們在內核的進程管理里在做詳細介紹。
down_trylock嘗試獲取信號量, 如果獲取成功則返回0,失敗則會立即返回非0。
當退出臨界區時使用up函數釋放信號量,如果信號量上的睡眠隊列不為空,則喚醒其中一個等待進程。
八、讀寫信號量
類似於自旋鎖,信號量也有讀寫信號量。讀寫信號量API定義在頭文件中,它的定義其實也是體系結構相關的,因此具體實現定義在頭文件中,以下是x86的例子:
struct rw_semaphore {
signed long count;
spinlock_t wait_lock;
struct list_head wait_list;
};
6. 在linux環境下進程進行同步和互斥提供什麼系統函數
pthread 里提供了互斥鎖 mutex,自旋鎖 spinlock, 讀寫鎖 rwlock 等線程鎖
另外semaphore.h里提供了信號量相關的鎖操作
7. linux下epoll如何實現高效處理
使用起來很清晰,首先要調用epoll_create建立一個epoll對象。參數size是內核保證能夠正確處理的最大句柄數,多於這個最大數時內核可不保證效果。 epoll_ctl可以操作上面建立的epoll,例如,將剛建立的socket加入到epoll中讓其監控,或者把 epoll正在監控的某個socket句柄移出epoll,不再監控它等等。 epoll_wait在調用時,在給定的timeout時間內,當在監控的所有句柄中有事件發生時,就返回用戶態的進程。 從上面的調用方式就可以看到epoll比select/poll的優越之處:因為後者每次調用時都要傳遞你所要監控的所有socket給select/poll系統調用,這意味著需要將用戶態的socket列表到內核態,如果以萬計的句柄會導致每次都要幾十幾百KB的內存到內核態,非常低效。而我們調用epoll_wait時就相當於以往調用select/poll,但是這時卻不用傳遞socket句柄給內核,因為內核已經在epoll_ctl中拿到了要監控的句柄列表。 所以,實際上在你調用epoll_create後,內核就已經在內核態開始准備幫你存儲要監控的句柄了,每次調用epoll_ctl只是在往內核的數據結構里塞入新的socket句柄。 在內核里,一切皆文件。所以,epoll向內核注冊了一個文件系統,用於存儲上述的被監控socket。當你調用epoll_create時,就會在這個虛擬的epoll文件系統里創建一個file結點。當然這個file不是普通文件,它只服務於epoll。 epoll在被內核初始化時(操作系統啟動),同時會開辟出epoll自己的內核高速cache區,用於安置每一個我們想監控的socket,這些socket會以紅黑樹的形式保存在內核cache里,以支持快速的查找、插入、刪除。這個內核高速cache區,就是建立連續的物理內存頁,然後在之上建立slab層,簡單的說,就是物理上分配好你想要的size的內存對象,每次使用時都是使用空閑的已分配好的對象。1staticint__init eventpoll_init(void)2{3 ... ...45/*Allocates slab cache used to allocate "struct epitem" items*/6epi_cache = kmem_cache_create("eventpoll_epi", sizeof(structepitem),70 , SLAB_HWCACHE_ALIGN|EPI_SLAB_DEBUG|SLAB_PANIC,8NULL, NULL);910/*Allocates slab cache used to allocate "struct eppoll_entry"*/11pwq_cache = kmem_cache_create("eventpoll_pwq",12sizeof(structeppoll_entry),0,13EPI_SLAB_DEBUG|SLAB_PANIC, NULL, NULL);1415 epoll的高效就在於,當我們調用epoll_ctl往裡塞入百萬個句柄時,epoll_wait仍然可以飛快的返回,並有效的將發生事件的句柄給我們用戶。這是由於我們在調用epoll_create時,內核除了幫我們在epoll文件系統里建了個file結點,在內核cache里建了個紅黑樹用於存儲以後epoll_ctl傳來的socket外,還會再建立一個list鏈表,用於存儲准備就緒的事件,當epoll_wait調用時,僅僅觀察這個list鏈表裡有沒有數據即可。有數據就返回,沒有數據就sleep,等到timeout時間到後即使鏈表沒數據也返回。所以,epoll_wait非常高效。 那麼,這個准備就緒list鏈表是怎麼維護的呢?當我們執行epoll_ctl時,除了把socket放到epoll文件系統里file對象對應的紅黑樹上之外,還會給內核中斷處理程序注冊一個回調函數,告訴內核,如果這個句柄的中斷到了,就把它放到准備就緒list鏈表裡。所以,當一個socket上有數據到了,內核在把網卡上的數據到內核中後就來把socket插入到准備就緒鏈表裡了。 如此,一顆紅黑樹,一張准備就緒句柄鏈表,少量的內核cache,就幫我們解決了大並發下的socket處理問題。執行epoll_create時,創建了紅黑樹和就緒鏈表,執行epoll_ctl時,如果增加socket句柄,則檢查在紅黑樹中是否存在,存在立即返回,不存在則添加到樹幹上,然後向內核注冊回調函數,用於當中斷事件來臨時向准備就緒鏈表中插入數據。執行epoll_wait時立刻返回准備就緒鏈表裡的數據即可。 最後看看epoll獨有的兩種模式LT和ET。無論是LT和ET模式,都適用於以上所說的流程。區別是,LT模式下,只要一個句柄上的事件一次沒有處理完,會在以後調用epoll_wait時次次返回這個句柄,而ET模式僅在第一次返回。 這件事怎麼做到的呢?當一個socket句柄上有事件時,內核會把該句柄插入上面所說的准備就緒list鏈表,這時我們調用epoll_wait,會把准備就緒的socket拷貝到用戶態內存,然後清空准備就緒list鏈表,最後,epoll_wait幹了件事,就是檢查這些socket,如果不是ET模式(就是LT模式的句柄了),並且這些socket上確實有未處理的事件時,又把該句柄放回到剛剛清空的准備就緒鏈表了。所以,非ET的句柄,只要它上面還有事件,epoll_wait每次都會返回。而ET模式的句柄,除非有新中斷到,即使socket上的事件沒有處理完,也是不會次次從epoll_wait返回的。1/*2* Each file descriptor added to the eventpoll interface will3* have an entry of this type linked to the hash.4*/5structepitem {6/*RB-Tree node used to link this structure to the eventpoll rb-tree*/7structrb_node rbn;8//紅黑樹,用來保存eventpoll910/*List header used to link this structure to the eventpoll ready list*/11structlist_head rdllink;12//雙向鏈表,用來保存已經完成的eventpoll1314/*The file descriptor information this item refers to*/15structepoll_filefd ffd;16//這個結構體對應的被監聽的文件描述符信息1718/*Number of active wait queue attached to poll operations*/19intnwait;20//poll操作中事件的個數2122/*List containing poll wait queues*/23structlist_head pwqlist;24//雙向鏈表,保存著被監視文件的等待隊列,功能類似於select/poll中的poll_table2526/*The "container" of this item*/27structeventpoll *ep;28//指向eventpoll,多個epitem對應一個eventpoll2930/*The structure that describe the interested events and the source fd*/31structepoll_eventevent;32//記錄發生的事件和對應的fd3334/*35* Used to keep track of the usage count of the structure. This avoids36* that the structure will desappear from underneath our processing.37*/38atomic_t usecnt;39//引用計數4041/*List header used to link this item to the "struct file" items list*/42structlist_head fllink;43雙向鏈表,用來鏈接被監視的文件描述符對應的struct file。因為file里有f_ep_link,用來保存所有監視這個文件的epoll節點4445/*List header used to link the item to the transfer list*/46structlist_head txlink;47雙向鏈表,用來保存傳輸隊列4849/*50* This is used ring the collection/transfer of events to userspace51* to pin items empty events set.52*/53unsignedintrevents;54//文件描述符的狀態,在收集和傳輸時用來鎖住空的事件集合55};5657//該結構體用來保存與epoll節點關聯的多個文件描述符,保存的方式是使用紅黑樹實現的hash表.58//至於為什麼要保存,下文有詳細解釋。它與被監聽的文件描述符一一對應.59structeventpoll {60/*Protect the this structure access*/61rwlock_tlock;62//讀寫鎖6364/*65* This semaphore is used to ensure that files are not removed66* while epoll is using them. This is read-held ring the event67* collection loop and it is write-held ring the file cleanup68* path, the epoll file exit code and the ctl operations.69*/70structrw_semaphore sem;71//讀寫信號量7273/*Wait queue used by sys_epoll_wait()*/74wait_queue_head_t wq;75/*Wait queue used by file->poll()*/7677wait_queue_head_t poll_wait;78/*List of ready file descriptors*/7980structlist_head rdllist;81//已經完成的操作事件的隊列。8283/*RB-Tree root used to store monitored fd structs*/84structrb_root rbr;85//保存epoll監視的文件描述符86};8788//這個結構體保存了epoll文件描述符的擴展信息,它被保存在file結構體的private_data89//中。它與epoll文件節點一一對應。通常一個epoll文件節點對應多個被監視的文件描述符。90//所以一個eventpoll結構體會對應多個epitem結構體。那麼,epoll中的等待事件放在哪裡呢?見下面91/*Wait structure used by the poll hooks*/92structeppoll_entry {93/*List header used to link this structure to the "struct epitem"*/94structlist_head llink;95/*The "base" pointer is set to the container "struct epitem"*/96void*base;97/*98* Wait queue item that will be linked to the target file wait99* queue head. 100*/101wait_queue_t wait;102/*The wait queue head that linked the "wait" wait queue item*/103wait_queue_head_t *whead;104};105106//與select/poll的struct poll_table_entry相比,epoll的表示等待隊列節點的結107//構體只是稍有不同,與struct poll_table_entry比較一下。 108structpoll_table_entry {109structfile * filp;110wait_queue_t wait;111wait_queue_head_t * wait_address;112};
8. linux 互斥鎖和讀寫鎖的區別與聯系
信號量與互斥鎖之間的區別:
1. 互斥量用於線程的互斥,信號量用於線程的同步。
這是互斥量和信號量的根本區別,也就是互斥和同步之間的區別。
互斥:是指某一資源同時只允許一個訪問者對其進行訪問,具有唯一性和排它性。但互斥無法限制訪問者對資源的訪問順序,即訪問是無序的。
同步:是指在互斥的基礎上(大多數情況),通過其它機制實現訪問者對資源的有序訪問。在大多數情況下,同步已經實現了互斥,特別是所有寫入資源的情況必定是互斥的。少數情況是指可以允許多個訪問者同時訪問資源
2. 互斥量值只能為0/1,信號量值可以為非負整數。
也就是說,一個互斥量只能用於一個資源的互斥訪問,它不能實現多個資源的多線程互斥問題。信號量可以實現多個同類資源的多線程互斥和同步。當信號量為單值信號量是,也可以完成一個資源的互斥訪問。
3. 互斥量的加鎖和解鎖必須由同一線程分別對應使用,信號量可以由一個線程釋放,另一個線程得到。
9. linux怎麼把文件同時進行讀寫鎖
讀寫鎖與互斥量類似,不過讀寫鎖的並行性更高。
讀寫鎖可以有三種狀態:(1)讀模式加鎖;(2)寫模式加鎖;(3)不加鎖。
在寫加鎖狀態時,在解鎖之前,所有試圖對這個鎖加鎖的線程都會被阻塞。在讀加鎖狀態時,所有試圖以讀模式對它進行加鎖的線程都可以得到訪問許可權。但是如果線程希望以寫模式加鎖,它必須阻塞,直至所有的線程釋放讀鎖。
讀寫鎖很適合於對數據結構讀的次數遠大於寫的情況。
相關函數:
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock) // 成功則返回0,失敗則返回錯誤代碼
int pthread_rwlock_rdlock(pthread_rwlock_t *restrict rwlock) ;//讀模式加鎖
int pthread_rwlock_wrlock(pthread_rwlock_t *restrict rwlock);//寫模式加鎖
int pthread_rwlock_unlock(pthread_rwlock_t *restrick rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *restrict rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *restrict rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *restrict rwlock);
相關示例:讀者寫者問題,這也是一個很經典的多線程題目,題目大意:有一個寫者多個讀者,多個讀者可以同時讀文件,但寫者在寫文件時不允許有讀者在讀取文件,同樣有讀者讀文件時
#include <stdio.h>
#include <pthread.h>
#define Read_Num 2
pthread_rwlock_t lock;
class Data
{
public:
Data(int i, float f): I(i),F(f)
{}
private:
int I;
float F;
};
Data *pdata = NULL;
void *read(void * arg)
{
int id = (int)arg;
while(true)
{
pthread_rwlock_rdlock(&lock);
printf(" reader %d is reading data!\n", id);
if(data == NULL)
{
printf("data is NULL\n");
}
else
{
printf("data: I = %d, F = %f \n", pdata->I, pdata->F);
}
pthread_rwlock_unlock(&lock);
}
pthread_exit(0);
}
void *write()
{
while(true)
{
pthread_rwlock_wrlock(&lock);
printf(" writer is writind data!\n");
if(pdata == NULL)
{
pdata = new Data(1, 1.1);
printf("Writer is writing data: %d, %f\n", pdata->I, pdata->F);
}
else
{
delete pdata;
pdata = NULL;
printf("writer free the data!");
}
pthread_rwlock_unlock(&lock);
}
pthread_exit(0);
}
void main()
{
pthread_t reader[Read_Num];
pthread_t writer;
for(int i = 0;i<Read_Num;i++)
{
pthread_create(&read[i],NULL,read,(void *)i);
}
pthread_create(writer, NULL, write, NULL);
sleep(1);
return 0;
}
10. linux下,所有進程間通信手段是否都可以用在線程間
除了共享內存(只進程間,線程地址空間都是共享的不需要),和條件變數(線程間,這個是pthread庫自己實現的),其他的進程線程都可用