新聞中心
Linux 是一種開源的操作系統,它具有高度的靈活性和可定制性,同時也支持多線程編程。由于多線程可以提高程序的運行效率,因此在今天的軟件開發(fā)中,它已經成為了離不開的一部分。深入探秘 linux 多線程通訊方式,不僅能夠幫助開發(fā)者更好地理解多線程在程序中所起的作用,而且可以幫助他們更有效地管理多線程通信,并提高程序的效率。

成都創(chuàng)新互聯公司主要從事成都網站制作、網站設計、網頁設計、企業(yè)做網站、公司建網站等業(yè)務。立足成都服務陵水黎族,10余年網站建設經驗,價格優(yōu)惠、服務專業(yè),歡迎來電咨詢建站服務:028-86922220
多線程通信是指在多線程編程中,不同線程之間相互交互信息的過程。在 Linux 下,多線程之間可以使用以下的通信方式:
1.管道通信
管道通信是指通過一個進程向另一個進程傳輸數據。在 Linux 下,管道通信可以使用 pipe 函數來創(chuàng)建一個管道, A 進程將數據寫入管道中,而 B 進程則可以通過讀取管道中的數據來獲取這些信息。管道通信可以有效地實現多線程之間的通信,但是其缺點是只能在具有親緣關系的進程之間進行。
2.共享內存通信
共享內存通信是指多個線程之間共享內存段的信息。在 Linux 下,共享內存通信可以使用 shmget 函數來創(chuàng)建一個共享內存段,多個線程可以通過對該共享內存段進行讀寫來實現信息交互。共享內存通信具有速度快、效率高等特點,但是需要注意的是,在多個線程訪問同一個共享內存段時,需要進行加鎖等安全措施,防止資源競爭。
3.消息隊列通信
消息隊列通信是指多個線程之間通過一個消息隊列來進行信息傳遞。在 Linux 下,消息隊列通信可以使用 msgget 函數來創(chuàng)建消息隊列,線程 A 可以通過向該消息隊列中寫入信息,而線程 B 則可以通過讀取該消息隊列中的信息來獲取這些數據。因為消息隊列通信具有異步性和可靠性,因此其被廣泛應用于 Linux 多線程編程中。
4.信號量通信
信號量通信是指多個線程之間通過信號量來進行同步和互斥。在 Linux 下,信號量通信可以使用 semget 函數來創(chuàng)建一個信號量,線程之間可以使用 semop 函數對該信號量進行加鎖和解鎖。信號量通信可以有效地實現不同線程之間的同步和互斥,但是需要注意的是,在使用信號量通信時,如果加鎖的線程出現異常,可能會造成死鎖等嚴重問題。
綜上所述,多線程通信在 Linux 下可以通過管道通信、共享內存通信、消息隊列通信和信號量通信來實現。不同的通信方式具有不同的特點,開發(fā)者需要根據具體應用場景來選擇合適的通信方式,以便提高程序的效率和性能。同時,在進行多線程編程時,需要注意進行線程之間的同步和互斥,以防止資源競爭等問題。在實際應用中,多線程通信已經被廣泛應用于各種互聯網軟件、計算機游戲以及數據處理等領域,可以幫助開發(fā)者更好地管理多線程編程,提高程序的效率和性能。
相關問題拓展閱讀:
- 如何實現linux下多線程之間的互斥與同步
- Linux 多線程編程(二)
如何實現linux下多線程之間的互斥與同步
Linux設備驅動中必須解決的一個問題是多個進程對共享資源的并發(fā)訪問,并發(fā)訪問會導致競態(tài),linux提供了多種解決競態(tài)問題的方式,這些方式適合不同的應用場景。
Linux內核是多進程、多線程的操作系統,它提供了相當完整的內核同步方法。內核同步方法列表如下:
中斷屏蔽
原子操作
自旋鎖
讀寫自旋鎖
順序鎖
信號量
讀寫信號量
BKL(大內核鎖)
Seq鎖
一、并發(fā)與競態(tài):
定義:
并發(fā)(concurrency)指的是多個執(zhí)行單元同時、并行被執(zhí)行,而并發(fā)的執(zhí)行單元對共享資源(硬件資源和軟件上的全局變量、靜態(tài)變量等)的訪問則很容易導致競態(tài)(race conditions)。
在linux中,主要的競態(tài)發(fā)生在如下幾種情況:
1、對稱多處理器歷辯埋(P)多個CPU
特點是多個CPU使用共同的系統總線,因此可訪問共同的外設和存儲器。
2、單CPU內進程與搶占它的進程
3、中斷(硬中斷、軟中斷、Tasklet、底半部)與進程之間
只要并發(fā)的多個執(zhí)行單元存在對共享資源的訪問,競態(tài)就有可能發(fā)生。
如果中斷處理程序訪問進程正在訪問的資源,則競態(tài)也會會發(fā)生。
多個中斷之間本身也可能引起并發(fā)而導致競態(tài)(中斷被更高優(yōu)先級的中斷打斷)。
解決競態(tài)問題的途徑是保證對共享資源的互斥訪問,所謂互斥訪問就是指一個執(zhí)行單元在訪問共享資源的時候,其他的執(zhí)行單元都被禁止訪問。
訪問共享資源的代碼區(qū)域被稱為臨界區(qū),臨界區(qū)需要以某種互斥機制加以保護,中斷屏蔽,原子操作,自旋鎖,和信號量都是linux設備驅動中可采用的互斥途徑。
臨界區(qū)和競爭條件:
所謂臨界區(qū)(critical regions)就是訪問和操作共享數據的代碼段,為了避免在臨界區(qū)中并發(fā)訪問,編程者必須保證這些代碼原子地執(zhí)行——也就是說,代碼在執(zhí)行結束前不可被打斷,就如同整個臨界區(qū)是一個不可分割的指令一樣,如果兩個執(zhí)行線程有可能處于同一個臨界區(qū)中,那么就是程序包含一個bug,如果這種情況發(fā)生了,我們就稱之為競爭條件(race conditions),避免并發(fā)和防止競爭條件被稱為同步。
死鎖:
死鎖的產生需要一定條件:要有一個或多個執(zhí)行線程和一個或多個資源,每個線程都在等待其中的一個資源,但所有的資源都已經被占用了,所有線程都在相互等待,但它們永遠不會釋放已經占有的資源,于是任何線程都無法繼續(xù),這便意味著死鎖的發(fā)生。
二、中斷屏蔽
在單CPU范圍內避免競態(tài)的一種簡單方法是在進入臨界區(qū)之前屏蔽系統的中斷。
由于linux內核的進程調度等操作都依賴中斷來實現,內核搶占進程之間的并發(fā)也就得以避免了。
中斷屏蔽的使用方法:
local_irq_disable()//屏蔽中斷
//臨界區(qū)
local_irq_enable()//灶念開中斷
特點:
由于linux系統的異步IO,進程調度等很多重要操作都依賴于中斷,在屏蔽中斷期間所有的中斷都無法得到處理,因此長時間的屏蔽是很危險的,有可能造成數據丟失甚至系統崩潰,這就要求在屏蔽中斷之后,當前的內核執(zhí)行路徑應當盡快地執(zhí)行完臨界區(qū)的代碼。
中斷屏蔽只能禁止本CPU內的中斷,因此,并不能解決多CPU引發(fā)的競態(tài),所以單獨使用中斷屏蔽并不是一個值得推薦的避免競態(tài)的方法,它一般和自旋鎖配合使用。
三、原子操作
定義:原子操作指的是在執(zhí)行過程中不會被別的代碼路徑所中斷的操作。
(原子原本指的是不可分割的微粒,所以原子操作也就是不能夠被分割的指令)
(它保證指令以“原子”的方式執(zhí)行而不能被打斷)
原子操作是不可分割的,在執(zhí)行完畢不會被任何其它任務或事件中斷。在單處理器系統(UniProcessor)中,能夠在單條指令中完成的操作都可以認為是” 原子操作”,因為中斷只能發(fā)生于指令之間。這也是某些CPU指令系統中引入了test_and_set、test_and_clear等指令用于臨界資源互斥的原因。但是,在對稱多處理器(Symmetric Multi-Processor)結構中就不同了,由于系統中有多個處理器在獨立地運行,即使能在單條指令中完成的操作也有可能受到干擾。我們以decl (遞減指令)為例,這是一個典型的”讀-改-肢螞寫”過程,涉及兩次內存訪問。
通俗理解:
原子操作,顧名思義,就是說像原子一樣不可再細分。一個操作是原子操作,意思就是說這個操作是以原子的方式被執(zhí)行,要一口氣執(zhí)行完,執(zhí)行過程不能夠被OS的其他行為打斷,是一個整體的過程,在其執(zhí)行過程中,OS的其它行為是插不進來的。
分類:linux內核提供了一系列函數來實現內核中的原子操作,分為整型原子操作和位原子操作,共同點是:在任何情況下操作都是原子的,內核代碼可以安全的調用它們而不被打斷。
原子整數操作:
針對整數的原子操作只能對atomic_t類型的數據進行處理,在這里之所以引入了一個特殊的數據類型,而沒有直接使用C語言的int型,主要是出于兩個原因:
之一、讓原子函數只接受atomic_t類型的操作數,可以確保原子操作只與這種特殊類型數據一起使用,同時,這也確保了該類型的數據不會被傳遞給其它任何非原子函數;
第二、使用atomic_t類型確保編譯器不對相應的值進行訪問優(yōu)化——這點使得原子操作最終接收到正確的內存地址,而不是一個別名,最后就是在不同體系結構上實現原子操作的時候,使用atomic_t可以屏蔽其間的差異。
原子整數操作最常見的用途就是實現計數器。
另一點需要說明原子操作只能保證操作是原子的,要么完成,要么不完成,不會有操作一半的可能,但原子操作并不能保證操作的順序性,即它不能保證兩個操作是按某個順序完成的。如果要保證原子操作的順序性,請使用內存屏障指令。
atomic_t和ATOMIC_INIT(i)定義
typedef struct { volatile int counter; } atomic_t;
#define ATOMIC_INIT(i) { (i) }
在你編寫代碼的時候,能使用原子操作的時候,就盡量不要使用復雜的加鎖機制,對多數體系結構來講,原子操作與更復雜的同步方法相比較,給系統帶來的開銷小,對高速緩存行的影響也小,但是,對于那些有高性能要求的代碼,對多種同步方法進行測試比較,不失為一種明智的作法。
原子位操作:
針對位這一級數據進行操作的函數,是對普通的內存地址進行操作的。它的參數是一個指針和一個位號。
為方便其間,內核還提供了一組與上述操作對應的非原子位函數,非原子位函數與原子位函數的操作完全相同,但是,前者不保證原子性,且其名字前綴多兩個下劃線。例如,與test_bit()對應的非原子形式是_test_bit(),如果你不需要原子性操作(比如,如果你已經用鎖保護了自己的數據),那么這些非原子的位函數相比原子的位函數可能會執(zhí)行得更快些。
四、自旋鎖
自旋鎖的引入:
如 果每個臨界區(qū)都能像增加變量這樣簡單就好了,可惜現實不是這樣,而是臨界區(qū)可以跨越多個函數,例如:先得從一個數據結果中移出數據,對其進行格式轉換和解 析,最后再把它加入到另一個數據結構中,整個執(zhí)行過程必須是原子的,在數據被更新完畢之前,不能有其他代碼讀取這些數據,顯然,簡單的原子操作是無能為力 的(在單處理器系統(UniProcessor)中,能夠在單條指令中完成的操作都可以認為是” 原子操作”,因為中斷只能發(fā)生于指令之間),這就需要使用更為復雜的同步方法——鎖來提供保護。
自旋鎖的介紹:
Linux內核中最常見的鎖是自旋鎖(spin lock),自旋鎖最多只能被一個可執(zhí)行線程持有,如果一個執(zhí)行線程試圖獲得一個被爭用(已經被持有)的自旋鎖,那么該線程就會一直進行忙循環(huán)—旋轉—等待鎖重新可用,要是鎖未被爭用,請求鎖的執(zhí)行線程便能立刻得到它,繼續(xù)執(zhí)行,在任意時間,自旋鎖都可以防止多于一個的執(zhí)行線程同時進入理解區(qū),注意同一個鎖可以用在多個位置—例如,對于給定數據的所有訪問都可以得到保護和同步。
一個被爭用的自旋鎖使得請求它的線程在等待鎖重新可用時自旋(特別浪費處理器時間),所以自旋鎖不應該被長時間持有,事實上,這點正是使用自旋鎖的初衷,在短期間內進行輕量級加鎖,還可以采取另外的方式來處理對鎖的爭用:讓請求線程睡眠,直到鎖重新可用時再喚醒它,這樣處理器就不必循環(huán)等待,可以去執(zhí)行其他代碼,這也會帶來一定的開銷——這里有兩次明顯的上下文切換, 被阻塞的線程要換出和換入。因此,持有自旋鎖的時間更好小于完成兩次上下文切換的耗時,當然我們大多數人不會無聊到去測量上下文切換的耗時,所以我們讓持 有自旋鎖的時間應盡可能的短就可以了,信號量可以提供上述第二種機制,它使得在發(fā)生爭用時,等待的線程能投入睡眠,而不是旋轉。
自旋鎖可以使用在中斷處理程序中(此處不能使用信號量,因為它們會導致睡眠),在中斷處理程序中使用自旋鎖時,一定要在獲取鎖之前,首先禁止本地中斷(在 當前處理器上的中斷請求),否則,中斷處理程序就會打斷正持有鎖的內核代碼,有可能會試圖去爭用這個已經持有的自旋鎖,這樣以來,中斷處理程序就會自旋, 等待該鎖重新可用,但是鎖的持有者在這個中斷處理程序執(zhí)行完畢前不可能運行,這正是我們在前一章節(jié)中提到的雙重請求死鎖,注意,需要關閉的只是當前處理器上的中斷,如果中斷發(fā)生在不同的處理器上,即使中斷處理程序在同一鎖上自旋,也不會妨礙鎖的持有者(在不同處理器上)最終釋放鎖。
自旋鎖的簡單理解:
理解自旋鎖最簡單的方法是把它作為一個變量看待,該變量把一個臨界區(qū)或者標記為“我當前正在運行,請稍等一會”或者標記為“我當前不在運行,可以被使用”。如果A執(zhí)行單元首先進入例程,它將持有自旋鎖,當B執(zhí)行單元試圖進入同一個例程時,將獲知自旋鎖已被持有,需等到A執(zhí)行單元釋放后才能進入。
自旋鎖的API函數:
其實介紹的幾種信號量和互斥機制,其底層源碼都是使用自旋鎖,可以理解為自旋鎖的再包裝。所以從這里就可以理解為什么自旋鎖通常可以提供比信號量更高的性能。
自旋鎖是一個互斥設備,他只能會兩個值:“鎖定”和“解鎖”。它通常實現為某個整數之中的單個位。
“測試并設置”的操作必須以原子方式完成。
任何時候,只要內核代碼擁有自旋鎖,在相關CPU上的搶占就會被禁止。
適用于自旋鎖的核心規(guī)則:
(1)任何擁有自旋鎖的代碼都必須使原子的,除服務中斷外(某些情況下也不能放棄CPU,如中斷服務也要獲得自旋鎖。為了避免這種鎖陷阱,需要在擁有自旋鎖時禁止中斷),不能放棄CPU(如休眠,休眠可發(fā)生在許多無法預期的地方)。否則CPU將有可能永遠自旋下去(死機)。
(2)擁有自旋鎖的時間越短越好。
需 要強調的是,自旋鎖別設計用于多處理器的同步機制,對于單處理器(對于單處理器并且不可搶占的內核來說,自旋鎖什么也不作),內核在編譯時不會引入自旋鎖 機制,對于可搶占的內核,它僅僅被用于設置內核的搶占機制是否開啟的一個開關,也就是說加鎖和解鎖實際變成了禁止或開啟內核搶占功能。如果內核不支持搶 占,那么自旋鎖根本就不會編譯到內核中。
內核中使用spinlock_t類型來表示自旋鎖,它定義在:
typedef struct {
raw_spinlock_t raw_lock;
#if defined(CONFIG_PREEMPT) && defined(CONFIG_P)
unsigned int break_lock;
#endif
} spinlock_t;
對于不支持P的內核來說,struct raw_spinlock_t什么也沒有,是一個空結構。對于支持多處理器的內核來說,struct raw_spinlock_t定義為
typedef struct {
unsigned int slock;
} raw_spinlock_t;
slock表示了自旋鎖的狀態(tài),“1”表示自旋鎖處于解鎖狀態(tài)(UNLOCK),“0”表示自旋鎖處于上鎖狀態(tài)(LOCKED)。
break_lock表示當前是否由進程在等待自旋鎖,顯然,它只有在支持搶占的P內核上才起作用。
自旋鎖的實現是一個復雜的過程,說它復雜不是因為需要多少代碼或邏輯來實現它,其實它的實現代碼很少。自旋鎖的實現跟體系結構關系密切,核心代碼基本也是由匯編語言寫成,與體協結構相關的核心代碼都放在相關的目錄下,比如。對于我們驅動程序開發(fā)人員來說,我們沒有必要了解這么spinlock的內部細節(jié),如果你對它感興趣,請參考閱讀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有兩種初始化形式,一種是靜態(tài)初始化,一種是動態(tài)初始化。對于靜態(tài)的spinlock對象,我們用 SPIN_LOCK_UNLOCKED來初始化,它是一個宏。當然,我們也可以把聲明spinlock和初始化它放在一起做,這就是 DEFINE_SPINLOCK宏的工作,因此,下面的兩行代碼是等價的。
DEFINE_SPINLOCK (lock);
spinlock_t lock = SPIN_LOCK_UNLOCKED;
spin_lock_init 函數一般用來初始化動態(tài)創(chuàng)建的spinlock_t對象,它的參數是一個指向spinlock_t對象的指針。當然,它也可以初始化一個靜態(tài)的沒有初始化的spinlock_t對象。
spinlock_t *lock
……
spin_lock_init(lock);
? 獲取鎖
內核提供了三個函數用于獲取一個自旋鎖。
spin_lock:獲取指定的自旋鎖。
spin_lock_irq:禁止本地中斷并獲取自旋鎖。
spin_lock_irqsace:保存本地中斷狀態(tài),禁止本地中斷并獲取自旋鎖,返回本地中斷狀態(tài)。
自旋鎖是可以使用在中斷處理程序中的,這時需要使用具有關閉本地中斷功能的函數,我們推薦使用 spin_lock_irqsave,因為它會保存加鎖前的中斷標志,這樣就會正確恢復解鎖時的中斷標志。如果spin_lock_irq在加鎖時中斷是關閉的,那么在解鎖時就會錯誤的開啟中斷。
另外兩個同自旋鎖獲取相關的函數是:
spin_trylock():嘗試獲取自旋鎖,如果獲取失敗則立即返回非0值,否則返回0。
spin_is_locked():判斷指定的自旋鎖是否已經被獲取了。如果是則返回非0,否則,返回0。
? 釋放鎖
同獲取鎖相對應,內核提供了三個相對的函數來釋放自旋鎖。
spin_unlock:釋放指定的自旋鎖。
spin_unlock_irq:釋放自旋鎖并激活本地中斷。
spin_unlock_irqsave:釋放自旋鎖,并恢復保存的本地中斷狀態(tài)。
五、讀寫自旋鎖
如 果臨界區(qū)保護的數據是可讀可寫的,那么只要沒有寫操作,對于讀是可以支持并發(fā)操作的。對于這種只要求寫操作是互斥的需求,如果還是使用自旋鎖顯然是無法滿 足這個要求(對于讀操作實在是太浪費了)。為此內核提供了另一種鎖-讀寫自旋鎖,讀自旋鎖也叫共享自旋鎖,寫自旋鎖也叫排他自旋鎖。
讀寫自旋鎖是一種比自旋鎖粒度更小的鎖機制,它保留了“自旋”的概念,但是在寫操作方面,只能最多有一個寫進程,在讀操作方面,同時可以有多個讀執(zhí)行單元,當然,讀和寫也不能同時進行。
讀寫自旋鎖的使用也普通自旋鎖的使用很類似,首先要初始化讀寫自旋鎖對象:
// 靜態(tài)初始化
rwlock_t rwlock = RW_LOCK_UNLOCKED;
//動態(tài)初始化
rwlock_t *rwlock;
…
rw_lock_init(rwlock);
在讀操作代碼里對共享數據獲取讀自旋鎖:
read_lock(&rwlock);
…
read_unlock(&rwlock);
在寫操作代碼里為共享數據獲取寫自旋鎖:
write_lock(&rwlock);
…
write_unlock(&rwlock);
需要注意的是,如果有大量的寫操作,會使寫操作自旋在寫自旋鎖上而處于寫?zhàn)囸I狀態(tài)(等待讀自旋鎖的全部釋放),因為讀自旋鎖會自由的獲取讀自旋鎖。
讀寫自旋鎖的函數類似于普通自旋鎖,這里就不一一介紹了,我們把它列在下面的表中。
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)是對讀寫鎖的一種優(yōu)化,若使用順序瑣,讀執(zhí)行單元絕不會被寫執(zhí)行單元阻塞,也就是說,讀執(zhí)行單元可以在寫執(zhí)行單元對被順序瑣保護的共享資源進行寫操作時仍然可以繼續(xù)讀,而不必等待寫執(zhí)行單元完成寫操作,寫執(zhí)行單元也不需要等待所有讀執(zhí)行單元完成讀操作才去進行寫操作。
但是,寫執(zhí)行單元與寫執(zhí)行單元之間仍然是互斥的,即如果有寫執(zhí)行單元在進行寫操作,其它寫執(zhí)行單元必須自旋在哪里,直到寫執(zhí)行單元釋放了順序瑣。
如果讀執(zhí)行單元在讀操作期間,寫執(zhí)行單元已經發(fā)生了寫操作,那么,讀執(zhí)行單元必須重新讀取數據,以便確保得到的數據是完整的,這種鎖在讀寫同時進行的概率比較小時,性能是非常好的,而且它允許讀寫同時進行,因而更大的提高了并發(fā)性,
注意,順序瑣由一個限制,就是它必須被保護的共享資源不含有指針,因為寫執(zhí)行單元可能使得指針失效,但讀執(zhí)行單元如果正要訪問該指針,將導致Oops。
七、信號量
Linux中的信號量是一種睡眠鎖,如果有一個任務試圖獲得一個已經被占用的信號量時,信號量會將其推進一個等待隊列,然后讓其睡眠,這時處理器能重獲自由,從而去執(zhí)行其它代碼,當持有信號量的進程將信號量釋放后,處于等待隊列中的哪個任務被喚醒,并獲得該信號量。
信號量,或旗標,就是我們在操作系統里學習的經典的P/V原語操作。
P:如果信號量值大于0,則遞減信號量的值,程序繼續(xù)執(zhí)行,否則,睡眠等待信號量大于0。
V:遞增信號量的值,如果遞增的信號量的值大于0,則喚醒等待的進程。
信號量的值確定了同時可以有多少個進程可以同時進入臨界區(qū),如果信號量的初始值始1,這信號量就是互斥信號量(MUTEX)。對于大于1的非0值信號量,也可稱為計數信號量(counting semaphore)。對于一般的驅動程序使用的信號量都是互斥信號量。
類似于自旋鎖,信號量的實現也與體系結構密切相關,具體的實現定義在頭文件中,對于x86_32系統來說,它的定義如下:
struct semaphore {
atomic_t count;
int sleepers;
wait_queue_head_t wait;
};
信號量的初始值count是atomic_t類型的,這是一個原子操作類型,它也是一個內核同步技術,可見信號量是基于原子操作的。我們會在后面原子操作部分對原子操作做詳細介紹。
信號量的使用類似于自旋鎖,包括創(chuàng)建、獲取和釋放。我們還是來先展示信號量的基本使用形式:
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 *)
? 初始化信號量
信號量的初始化包括靜態(tài)初始化和動態(tài)初始化。靜態(tài)初始化用于靜態(tài)的聲明并初始化信號量。
static DECLARE_SEMAPHORE_GENERIC(name, count);
static DECLARE_MUTEX(name);
對于動態(tài)聲明或創(chuàng)建的信號量,可以使用如下函數進行初始化:
seam_init(sem, count);
init_MUTEX(sem);
init_MUTEX_LOCKED(struct semaphore *)
顯然,帶有MUTEX的函數始初始化互斥信號量。LOCKED則初始化信號量為鎖狀態(tài)。
? 使用信號量
信號量初始化完成后我們就可以使用它了
down_interruptible(struct semaphore *);
down(struct semaphore *)
down_trylock(struct semaphore *)
up(struct semaphore *)
down函數會嘗試獲取指定的信號量,如果信號量已經被使用了,則進程進入不可中斷的睡眠狀態(tài)。down_interruptible則會使進程進入可中斷的睡眠狀態(tài)。關于進程狀態(tài)的詳細細節(jié),我們在內核的進程管理里在做詳細介紹。
down_trylock嘗試獲取信號量, 如果獲取成功則返回0,失敗則會立即返回非0。
當退出臨界區(qū)時使用up函數釋放信號量,如果信號量上的睡眠隊列不為空,則喚醒其中一個等待進程。
八、讀寫信號量
類似于自旋鎖,信號量也有讀寫信號量。讀寫信號量API定義在頭文件中,它的定義其實也是體系結構相關的,因此具體實現定義在頭文件中,以下是x86的例子:
struct rw_semaphore {
signed long count;
spinlock_t wait_lock;
struct list_head wait_list;
};
Linux 多線程編程(二)
三種專門用于線程同步的機制:POSIX信號量,互斥量和條件變量.
在Linux上信號量API有兩組,一組是System V IPC信號量,即PV操作,另外就是POSIX信號量,POSIX信號量的名字都是以sem_開頭.
phshared參數指定信號量的類型,若其值為0,就表示這個信號量是當前進程的局部信號量,否則該信號量可以在多個進程之間共享.value值指定信號量的初始值,一般與下面的sem_wait函數相對應.
其中比較重要的函數sem_wait函數會以原子操作的方式將信號量的值減一,如果信號量的值為零,則sem_wait將會阻塞,信號量的值可以在sem_init函數中的value初始化;sem_trywait函數是sem_wait的非阻塞版本;sem_post函數將以原子的操作對信號量加一,當信號量的值大于0時,其他正在調用sem_wait等待信號量的線程將被喚醒.
這些函數成功時返回0,失敗則返回-1并設置errno.
生產者消費者模型:
生產者對應一個信號量:sem_t producer;
消費者對應一個信號量:sem_t customer;
sem_init(&producer,2)—-生產者擁有資源,可以工悶亂作;
sem_init(&customer,0)—-消費者沒有資源,阻塞;
在訪問公共資源前對互滾耐斥量設置(加鎖),確保同一時間只有一個線程訪問數據,在訪問完成后再釋放(解鎖)互斥量.
互斥鎖的運行方式:串行訪問共享資源;
信號量的運行方式:并行訪問共享資源;
互斥量用pthread_mutex_t數據類型表示,在使用互斥量之前,必須使用pthread_mutex_init函數對它進行初始化,注意,使用完畢后需調用pthread_mutex_destroy.
pthread_mutex_init用于初始化互斥鎖,mutexattr用于指定互斥鎖的屬性,若為NULL,則表示默認屬性。除了用這個函數初始化互斥所外,還可以用如下方式初始化:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER。
pthread_mutex_destroy用于銷毀互斥鎖,以釋放占用的內核資源,銷毀一個已經加鎖的互斥鎖將導致不可預期的后果。
pthread_mutex_lock以原子操作給一個互斥鎖加鎖。如果目標互斥鎖已經被加鎖,則pthread_mutex_lock則被阻塞,直到該互斥鎖占有者把它給解鎖.
pthread_mutex_trylock和pthread_mutex_lock類似,不過它始終立即返回,而不論作的互斥鎖是否加鎖,是pthread_mutex_lock的非阻塞版本.當目標互斥鎖未被加鎖時,pthread_mutex_trylock進行加鎖操作;否則將返回EBUSY錯誤碼。注意:這里討論的pthread_mutex_lock和pthread_mutex_trylock是針對普通鎖而言的,對于其他類型的鎖,這兩個加鎖函數會有不同的行為.
pthread_mutex_unlock以原子操作方式給一個互斥鎖進行解鎖操作。如果此時有其他線程正在等待這個互斥鎖,則這些線程中的一個將獲得它.
三個打印機輪流打印:
輸出結螞備檔果:
如果說互斥鎖是用于同步線程對共享數據的訪問的話,那么條件變量就是用于在線程之間同步共享數據的值.條件變量提供了一種線程之間通信的機制:當某個共享數據達到某個值時,喚醒等待這個共享數據的線程.
條件變量會在條件不滿足的情況下阻塞線程.且條件變量和互斥量一起使用,允許線程以無競爭的方式等待特定的條件發(fā)生.
其中pthread_cond_broadcast函數以廣播的形式喚醒所有等待目標條件變量的線程,pthread_cond_signal函數用于喚醒一個等待目標條件變量線程.但有時候我們可能需要喚醒一個固定的線程,可以通過間接的方法實現:定義一個能夠唯一標識目標線程的全局變量,在喚醒等待條件變量的線程前先設置該變量為目標線程,然后采用廣播的方式喚醒所有等待的線程,這些線程被喚醒之后都檢查該變量以判斷是否是自己.
采用條件變量+互斥鎖實現生產者消費者模型:
運行結果:
阻塞隊列+生產者消費者
運行結果:
關于linux 多線程通訊方式的介紹到此就結束了,不知道你從中找到你需要的信息了嗎 ?如果你還想了解更多這方面的信息,記得收藏關注本站。
香港服務器選創(chuàng)新互聯,2H2G首月10元開通。
創(chuàng)新互聯(www.cdcxhl.com)互聯網服務提供商,擁有超過10年的服務器租用、服務器托管、云服務器、虛擬主機、網站系統開發(fā)經驗。專業(yè)提供云主機、虛擬主機、域名注冊、VPS主機、云服務器、香港云服務器、免備案服務器等。
標題名稱:深入探秘Linux多線程通訊方式,提高程序效率(linux多線程通訊方式)
轉載源于:http://www.dlmjj.cn/article/coocooc.html


咨詢
建站咨詢
