信号量的使用主要是用來保護共享資源,使得資源在一個時刻隻有一個程序(線程)所擁有。
信号量的值為正的時候,說明它空閑。所測試的線程可以鎖定而使用它。若為0,說明它被占用,測試的線程要進入睡眠隊列中,等待被喚醒。
為了防止出現因多個程式同時通路一個共享資源而引發的一系列問題,我們需要一種方法,它可以通過生成并使用令牌來授權,在任一時刻隻能有一個執行線程通路代碼的臨界區域。
臨界區域是指執行資料更新的代碼需要獨占式地執行。而信号量就可以提供這樣的一種通路機制,讓一個臨界區同一時間隻有一個線程在通路它,也就是說信号量是用來調協程序對共享資源的通路的。
信号量是一個特殊的變量,程式對其通路都是原子操作,且隻允許對它進行等待(即p(信号變量))和發送(即v(信号變量))資訊操作。
最簡單的信号量是隻能取0和1的變量,這也是信号量最常見的一種形式,叫做二進制信号量。而可以取多個正整數的信号量被稱為通用信号量。這裡主要讨論二進制信号量。
由于信号量隻能進行兩種操作等待和發送信号,即p(sv)和v(sv),他們的行為是這樣的:
p(sv):如果sv的值大于零,就給它減1;如果它的值為零,就挂起該程序的執行
v(sv):如果有其他程序因等待sv而被挂起,就讓它恢複運作,如果沒有程序因等待sv而挂起,就給它加1.
舉個例子,就是兩個程序共享信号量sv,一旦其中一個程序執行了p(sv)操作,它将得到信号量,并可以進入臨界區,使sv減1。而第二個程序将被阻止進入臨界區,因為當它試圖執行p(sv)時,sv為0,它會被挂起以等待第一個程序離開臨界區域并執行v(sv)釋放信号量,這時第二個程序就可以恢複執行。
在學習信号量之前,我們必須先知道——linux提供兩種信号量:
核心信号量,由核心控制路徑使用
使用者态程序使用的信号量,這種信号量又分為posix信号量和system v信号量。
posix信号量又分為有名信号量和無名信号量
有名信号量,其值儲存在檔案中, 是以它可以用于線程也可以用于程序間的同步。無名信号量,其值儲存在記憶體中。
對posix來說,信号量是個非負整數。常用于線程間同步。
而system v信号量則是一個或多個信号量的集合,它對應的是一個信号量結構體,這個結構體是為system v ipc服務的,信号量隻不過是它的一部分。常用于程序間同步。
posix信号量的引用頭檔案是<code><semaphore.h></code>,而system v信号量的引用頭檔案是<code><sys/sem.h></code>
從使用的角度,system v信号量是複雜的,而posix信号量是簡單。比如,posix信号量的建立和初始化或pv操作就很非常友善。
linux核心的信号量在概念和原理上與使用者态的system v的ipc機制信号量是一樣的,但是它絕不可能在核心之外使用,它是一種睡眠鎖。
如果有一個任務想要獲得已經被占用的信号量時,信号量會将其放入一個等待隊列(它不是站在外面癡癡地等待而是将自己的名字寫在任務隊列中)然後讓其睡眠。
當持有信号量的程序将信号釋放後,處于等待隊列中的一個任務将被喚醒(因為隊列中可能不止一個任務),并讓其獲得信号量。
這一點與自旋鎖不同,處理器可以去執行其它代碼。
關于 他們的不同之處,請參見 <a href="http://blog.csdn.net/whycold/article/details/7554309">自旋鎖,mutex和信号量的使用</a> <a href="http://blog.csdn.net/xu_guo/article/details/6072823">linux 自旋鎖和信号量</a>
它與自旋鎖的差異:由于争用信号量的程序在等待鎖重新變為可用時會睡眠,是以信号量适用于鎖會被長時間持有的情況;
相反,鎖被短時間持有時,使用信号量就不太适宜了,因為睡眠、維護等待隊列以及喚醒所花費的開銷可能比鎖占用的全部時間表還要長;
由于執行線程在鎖被争用時會睡眠,是以隻能在程序上下文中才能獲得信号量鎖,因為在中斷上下文中是不能進行調試的;持有信号量的進行也可以去睡眠,當然也可以不睡眠,因為當其他程序争用信号量時不會是以而死鎖;不能同時占用信号量和自旋鎖,因為自旋鎖不可以睡眠而信号量鎖可以睡眠。相對而來說信号量比較簡單,它不會禁止核心搶占,持有信号量的代碼可以被搶占。
信号量還有一個特征,就是它允許多個持有者,而自旋鎖在任何時候隻能允許一個持有者。
當然我們經常遇到也是隻有一個持有者,這種信号量叫二值信号量或者叫互斥信号量。允許有多個持有者的信号量叫計數信号量,在初始化時要說明最多允許有多少個持有者(count值)
信号量在建立時需要設定一個初始值,表示同時可以有幾個任務可以通路該信号量保護的共享資源,初始值為1就變成互斥鎖(mutex),即同時隻能有一個任務可以通路信号量保護的共享資源。
當任務通路完被信号量保護的共享資源後,必須釋放信号量,釋放信号量通過把信号量的值加1實作,如果信号量的值為非正數,表明有任務等待目前信号量,是以它也喚醒所有等待該信号量的任務。
關于核心信号量的其他資訊 請參見 <a href="http://blog.sina.com.cn/s/blog_6d7fa49b01014q8y.html">大話linux核心中鎖機制之信号量、讀寫信号量</a>
核心信号量類似于自旋鎖,因為當鎖關閉着時,它不允許核心控制路徑繼續進行。然而,當核心控制路徑試圖擷取核心信号量鎖保護的忙資源時,相應的程序就被挂起。隻有在資源被釋放時,程序才再次變為可運作。
隻有可以睡眠的函數才能擷取核心信号量;中斷處理程式和可延遲函數都不能使用核心信号量。
核心信号量是<code>struct semaphore</code>類型的對象,在核心源碼中位于include\linux\semaphore.h檔案
1
2
3
4
5
6
成員
描述
count
相當于信号量的值,大于0,資源空閑;等于0,資源忙,但沒有程序等待這個保護的資源;小于0,資源不可用,并至少有一個程序等待資源
wait
存放等待隊列連結清單的位址,目前等待資源的所有睡眠程序都會放在這個連結清單中
sleepers
存放一個标志,表示是否有一些程序在信号量上睡眠
上面已經提到了核心信号量使用了等待隊列wait_queue來實作阻塞操作。
當某任務由于沒有某種條件沒有得到滿足時,它就被挂到等待隊列中睡眠。當條件得到滿足時,該任務就被移出等待隊列,此時并不意味着該任務就被馬上執行,因為它又被移進工作隊列中等待cpu資源,在适當的時機被排程。
核心信号量是在内部使用等待隊列的,也就是說該等待隊列對使用者是隐藏的,無須使用者幹涉。由使用者真正使用的等待隊列我們将在另外的篇章進行詳解。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI4MTM0UzN0AjM4IzMwYTMwIzLcRXZu5ibkN3Yuc2bsJmLn1Wavw1LcpDc0RHaiojIsJye.jpg)
7
該宏聲明一個信号量name是直接将結構體中count值設定成n,此時信号量可用于實作程序間的互斥量。
該宏聲明一個互斥鎖name,但把它的初始值設定為1
該函用于數初始化設定信号量的初值,它設定信号量sem的值為val。
該函數用于初始化一個互斥鎖,即它把信号量sem的值設定為1。
該函數也用于初始化一個互斥鎖,但它把信号量sem的值設定為0,即一開始就處在已鎖狀态。
注意:對于信号量的初始化函數linux最新版本存在變化,如init_mutex和init_mutex_locked等初始化函數目前新的核心中已經沒有或者更換了了名字等 是以建議以後在程式設計中遇到需要使用信号量的時候盡量采用sema_init(struct semaphore *sem, int val)函數,因為這個函數就目前為止從未發生變化。
該函數用于獲得信号量sem,它會導緻睡眠,是以不能在中斷上下文(包括irq上下文和softirq上下文)使用該函數。該函數将把sem的值減1,如果信号量sem的值非負,就直接傳回,否則調用者将被挂起,直到别的任務釋放該信号量才能繼續運作。
該函數功能與down類似,不同之處為,down不會被信号(signal)打斷,但down_interruptible能被信号(比如ctrl+c)打斷,是以該函數有傳回值來區分是正常傳回還是被信号中斷,如果傳回0,表示獲得信号量正常傳回,如果被信号打斷,傳回-eintr
該函數試着獲得信号量sem,如果能夠立刻獲得,它就獲得該信号量并傳回0,否則,表示不能獲得信号量sem,傳回值為非0值。是以,它不會導緻調用者睡眠,可以在中斷上下文使用。
該函數釋放信号量sem,即把sem的值加1,如果sem的值為非正數,表明有任務等待該信号量,是以喚醒這些等待者。
在驅動程式中,當多個線程同時通路相同的資源時(驅動中的全局變量時一種典型的
共享資源),可能會引發“競态“,是以我們必須對共享資源進行并發控制。linux核心中
解決并發控制的最常用方法是自旋鎖與信号量(絕大多數時候作為互斥鎖使用)。
8
9
10
11
12
13
14
15
16
17
跟自旋鎖一樣,信号量也有區分讀-寫信号量之分
如果一個讀寫信号量目前沒有被寫者擁有并且也沒有寫者等待讀者釋放信号量,那麼任何讀者都可以成功獲得該讀寫信号量;
否則,讀者必須被挂起直到寫者釋放該信号量。如果一個讀寫信号量目前沒有被讀者或寫者擁有并且也沒有寫者等待該信号量,那麼一個寫者可以成功獲得該讀寫信号量,否則寫者将被挂起,直到沒有任何通路者。是以,寫者是排他性的,獨占性的。
讀寫信号量的相關api有:
該宏聲明一個讀寫信号量name并對其進行初始化。
該函數對讀寫信号量sem進行初始化。
讀者調用該函數來得到讀寫信号量sem。該函數會導緻調用者睡眠,是以隻能在程序上下文使用。
該函數類似于down_read,隻是它不會導緻調用者睡眠。它盡力得到讀寫信号量sem,如果能夠立即得到,它就得到該讀寫信号量,并且傳回1,否則表示不能立刻得到該信号量,傳回0。是以,它也可以在中斷上下文使用。
寫者使用該函數來得到讀寫信号量sem,它也會導緻調用者睡眠,是以隻能在程序上下文使用。
該函數類似于down_write,隻是它不會導緻調用者睡眠。該函數盡力得到讀寫信号量,如果能夠立刻獲得,就獲得該讀寫信号量并且傳回1,否則表示無法立刻獲得,傳回0。它可以在中斷上下文使用。
讀者使用該函數釋放讀寫信号量sem。它與down_read或down_read_trylock配對使用。如果down_read_trylock傳回0,不需要調用up_read來釋放讀寫信号量,因為根本就沒有獲得信号量。
寫者調用該函數釋放信号量sem。它與down_write或down_write_trylock配對使用。如果down_write_trylock傳回0,不需要調用up_write,因為傳回0表示沒有獲得該讀寫信号量。
該函數用于把寫者降級為讀者,這有時是必要的。因為寫者是排他性的,是以在寫者保持讀寫信号量期間,任何讀者或寫者都将無法通路該讀寫信号量保護的共享資源,對于那些目前條件下不需要寫通路的寫者,降級為讀者将,使得等待通路的讀者能夠立刻通路,進而增加了并發性,提高了效率。
讀寫信号量适于在讀多寫少的情況下使用,在linux核心中對程序的記憶體映像描述結構的通路就使用了讀寫信号量進行保護。
究竟什麼時候使用自旋鎖什麼時候使用信号量,下面給出建議的方案
當對低開銷、短期、中斷上下文加鎖,優先考慮自旋鎖;當對長期、持有鎖需要休眠的任務,優先考慮信号量。
無名信号量的建立就像聲明一般的變量一樣簡單,例如:sem_t sem_id。然後再初始化該無名信号量,之後就可以放心使用了。
無名信号量常用于多線程間的同步,同時也用于相關程序間的同步。也就是說,無名信号量必須是多個程序(線程)的共享變量,無名信号量要保護的變量也必須是多個程序(線程)的共享變量,這兩個條件是缺一不可的。
pshared==0 用于同一多線程的同步;
若pshared>0 用于多個相關程序間的同步(即由fork産生的)
取回信号量sem的目前值,把該值儲存到sval中。
若有1個或更多的線程或程序調用sem_wait阻塞在該信号量上,該函數傳回兩種值:
傳回0
傳回阻塞在該信号量上的程序或線程數目
linux采用傳回的第一種政策。
sem_wait(或sem_trywait)相當于p操作,即申請資源。
測試所指定信号量的值,它的操作是原子的。
若sem>0,那麼它減1并立即傳回。
若sem==0,則睡眠直到sem>0,此時立即減1,然後傳回。
其他的行為和sem_wait一樣,除了:
若sem==0,不是睡眠,而是傳回一個錯誤eagain。
sem_post相當于v操作,釋放資源。
把指定的信号量sem的值加1;
呼醒正在等待該信号量的任意線程。
注意:在這些函數中,隻有sem_post是信号安全的函數,它是可重入函數
無名信号量的常見用法是将要保護的變量放在sem_wait和sem_post中間所形成的
臨界區内,這樣該變量就會被保護起來,例如:
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
上面的例程,到底哪個線程先申請到信号量資源,這是随機的。
程序1先執行,進城2後執行
程序2先執行,進城1後執行
如果想要某個特定的順序的話,可以用2個信号量來實作。例如下面的例程是線程1先執行完,然後線程2才繼續執行,直至結束。
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
說是相關程序,是因為本程式中共有2個程序,其中一個是另外一個的子程序(由fork産生)的。
本來對于fork來說,子程序隻繼承了父程序的代碼副本,mutex理應在父子程序中是互相獨立的兩個變量,但由于在初始化mutex的時候,由pshared = 1指定了mutex處于共享記憶體區域,是以此時mutex變成了父子程序共享的一個變量。此時,mutex就可以用來同步相關程序了。
有名信号量的特點是把信号量的值儲存在檔案中。
這決定了它的用途非常廣:既可以用于線程,也可以用于相關程序間,甚至是不相關程序。
由于有名信号量的值是儲存在檔案中的,是以對于相關程序來說,子程序是繼承了父程序的檔案描述符,那麼子程序所繼承的檔案描述符所指向的檔案是和父程序一樣的,當然檔案裡面儲存的有名信号量值就共享了。
有名信号量在使用的時候,和無名信号量共享sem_wait和sem_post函數。
差別是有名信号量使用sem_open代替sem_init,另外在結束的時候要像關閉檔案一樣去關閉這個有名信号量。
打開一個已存在的有名信号量,或建立并初始化一個有名信号量。一個單一的調用就完
成了信号量的建立、初始化和權限的設定。
參數
name
檔案的路徑名;
oflag
有o_creat或o_creat
mode_t
控制新的信号量的通路權限;
value
指定信号量的初始化值。
注意: 這裡的name不能寫成/tmp/aaa.sem這樣的格式,因為在linux下,sem都是建立在/dev/shm目錄下。你可以将name寫成“/mysem”或“mysem”,建立出來的檔案都是“/dev/shm/sem.mysem”,千萬不要寫路徑。也千萬不要寫“/tmp/mysem”之類的。 當oflag = o_creat時,若name指定的信号量不存在時,則會建立一個,而且後面的mode和value參數必須有效。若name指定的信号量已存在,則直接打開該信号量, 同時忽略mode和value參數。 當oflag = o_creat|o_excl時,若name指定的信号量已存在,該函數會直接傳回error。
一旦你使用了一信号量,銷毀它們就變得很重要。
在做這個之前,要确定所有對這個有名信号量的引用都已經通過sem_close()函數關閉了,然後隻需在退出或是退出處理函數中調用sem_unlink()去删除系統中的信号量,
注意如果有任何的處理器或是線程引用這個信号量,sem_unlink()函數不會起到任何的作用。
也就是說,必須是最後一個使用該信号量的程序來執行sem_unlick才有效。因為每個信号燈有一個引用計數器記錄目前的打開次數,sem_unlink必須等待這個數為0時才能把name所指的信号燈從檔案系統中删除。也就是要等待最後一個sem_close發生。
前面已經說過,有名信号量是位于共享記憶體區的,那麼它要保護的資源也必須是位于共享記憶體區,隻有這樣才能被無相關的程序所共享。
在下面這個例子中,服務程序和客戶程序都使用<code>shmget</code>和<code>shmat</code>來擷取得一塊共享記憶體資源。然後利用有名信号量來對這塊共享記憶體資源進行互斥保護。
伺服器程式
用戶端程式
這是信号量值的集合,而不是單個信号量。相關的信号量操作函數由<code><sys/ipc.h></code>引用。
ystem v 信号量在核心中維護,其中包括二值信号量 、計數信号量、計數信号量集。
二值信号量 : 其值隻有0、1 兩種選擇,0表示資源被鎖,1表示資源可用;
計數信号量:其值在0 和某個限定值之間,不限定資源數隻在0 1 之間;
計數信号量集 :多個信号量的集合組成信号量集
核心為每個信号量集維護一個信号量結構體,可在
其中ipc_perm 結構是核心給每個程序間通信對象維護的一個資訊結構,其成員包含所有者使用者id,所有者組id、建立者及其組id,以及通路模式等;semid_ds結構體中的sem結構是核心用于維護某個給定信号量的一組值的内部結構,其結構定義:
system v信号量是system v ipc(即system v程序間通信)的組成部分,其他的有system v消息隊列,system v共享記憶體。而關鍵字和ipc描述符無疑是它們的共同點,也使用它們,就不得不先對它們進行熟悉。這裡隻對system v信号量進行讨論。
ipc描述符相當于引用id号,要想使用system v信号量(或msg、shm),就必須用ipc描述符來調用信号量。而ipc描述符是核心動态提供的(通過semget來擷取),使用者無法讓伺服器和客戶事先認可共同使用哪個描述符,是以有時候就需要到關鍵字key來定位描述符。
某個key隻會固定對應一個描述符(這項轉換工作由核心完成),這樣假如伺服器和
客戶事先認可共同使用某個key,那麼大家就都能定位到同一個描述符,也就能定位到同一個信号量,這樣就達到了system v信号量在程序間共享的目的。
建立一個信号量或通路一個已經存在的信号量集。
該函數執行成功傳回信号量标示符,失敗傳回-1
key
通過調用ftok函數得到的鍵值
nsems
代表建立信号量的個數,如果隻是通路而不建立則可以指定該參數為0,我們一旦建立了該信号量,就不能更改其信号量個數,隻要你不删除該信号量,你就是重新調用該函數建立該鍵值的信号量,該函數隻是傳回以前建立的值,不會重新建立;
semflg
指定該信号量的讀寫權限,當建立信号量時不許加ipc_creat ,若指定ipc_creat
semget函數執行成功後,就産生了一個由核心維持的類型為semid_ds結構體的信号量集,傳回semid就是指向該信号量集的引索。
nsems>0 : 建立一個信的信号量集,指定集合中信号量的數量,一旦建立就不能更改。
nsems==0 : 通路一個已存在的集合
傳回的是一個稱為信号量辨別符的整數,semop和semctl函數将使用它。
建立成功後信号量結構被設定:
該集合中的每個信号量不初始化,這些結構是在semctl,用參數set_val,setall初始化的。
有多種方法使客戶機和伺服器在同一ipc結構上會合:
* 伺服器可以指定關鍵字ipc_private建立一個新ipc結構,将傳回的辨別符存放在某處(例如一個檔案)以便客戶機取用。關鍵字 ipc_private保證伺服器建立一個新ipc結構。這種技術的缺點是:伺服器要将整型辨別符寫到檔案中,然後客戶機在此後又要讀檔案取得此辨別符。
ipc_private關鍵字也可用于父、子關系程序。父程序指定 ipc_private建立一個新ipc結構,所傳回的辨別符在fork後可由子程序使用。子程序可将此辨別符作為exec函數的一個參數傳給一個新程式。
在一個公用頭檔案中定義一個客戶機和伺服器都認可的關鍵字。然後伺服器指定此關鍵字建立一個新的ipc結構。這種方法的問題是該關鍵字可能已與一個 ipc結構相結合,在此情況下,get函數(msgget、semget或shmget)出錯傳回。伺服器必須處理這一錯誤,删除已存在的ipc結構,然後試着再建立它。當然,這個關鍵字不能被别的程式所占用。
客戶機和伺服器認同一個路徑名和課題i d(課題i d是0 ~ 2 5 5之間的字元值) ,然後調用函數ftok将這兩個值變換為一個關鍵字。這樣就避免了使用一個已被占用的關鍵字的問題。
使用ftok并非高枕無憂。有這樣一種例外:伺服器使用ftok擷取得一個關鍵字後,該檔案就被删除了,然後重建。此時用戶端以此重建後的檔案來ftok所擷取的關鍵字就和伺服器的關鍵字不一樣了。是以一般商用的軟體都不怎麼用ftok。
一般來說,客戶機和伺服器至少共享一個頭檔案,是以一個比較簡單的方法是避免使用ftok,而隻是在該頭檔案中存放一個大家都知道的關鍵字。
semid
是semget傳回的semid信号量标示符
opsptr
指向信号量操作結構數組
nops
opsptr所指向的數組中的sembuf結構體的個數
該函數執行成功傳回0,失敗傳回-1;
第二個參數sops為一個結構體數組指針,結構體定義在sys/sem.h中,結構體如下
sem_num 操作信号的下标,其值可以為0 到nops
sem_flg為該信号操作的标志:其值可以為0、ipc_nowait 、 sem_undo
sem_flg辨別
在對信号量的操作不能執行的情況下,該操作阻塞到可以執行為止;
ipc_nowait
在對信号量的操作不能執行的情況下,該操作立即傳回;
sem_undo
當操作的程序推出後,該程序對sem進行的操作将被取消;
sem_op取值
>0
則信号量加上它的值,等價于程序釋放信号量控制的資源
=0=0
若沒有設定ipc_nowait, 那麼調用程序将進入睡眠狀态,直到信号量的值為0,否則程序直接傳回
<0<0
則信号量加上它的值,等價于程序申請信号量控制的資源,若程序設定ipc_nowait則程序再沒有可用資源情況下,程序阻塞,否則直接傳回。
例如,目前semval為2,而sem_op = -3,那麼怎麼辦? 注意:semval是指semid_ds中的信号量集中的某個信号量的值
是信号量集合;
semnum
是信号在集合中的序号;
semum
是一個必須由使用者自定義的結構體,在這裡我們務必弄清楚該結構體的組成:
值
ipc_stat
讀取一個信号量集的資料結構semid_ds,并将其存儲在semun中的buf參數中。
ipc_set
設定信号量集的資料結構semid_ds中的元素ipc_perm,其值取自semun中的buf參數。
ipc_rmid
将信号量集從系統中删除
getall
用于讀取信号量集中的所有信号量的值,存于semnu的array中
setall
設定所指定的信号量集的每個成員semval的值
getpid
傳回最後一個執行semop操作的程序的pid。
lsetval
把的val資料成員設定為目前資源數
getval
把semval中的目前值作為函數的傳回,即現有的資源數,傳回值為非負數。
val隻有cmd ==setval時才有用,此時指定的semval = arg.val。
注意:當cmd == getval時,semctl函數傳回的值就是我們想要的semval。千萬不要以為指定的semval被傳回到arg.val中。
array指向一個數組,
當cmd==setall時,就根據arg.array來将信号量集的所有值都指派;
當cmd ==getall時,就将信号量集的所有值傳回到arg.array指定的數組中。
buf 指針隻在cmd==ipc_stat 或ipc_set 時有用,作用是semid 所指向的信号量集
(semid_ds機構體)。一般情況下不常用,這裡不做談論。 另外,cmd == ipc_rmid還是比較有用的。
1.問題描述:
有一個長度為n的緩沖池為生産者和消費者所共有,隻要緩沖池未滿,生産者便可将
消息送入緩沖池;隻要緩沖池未空,消費者便可從緩沖池中取走一個消息。生産者往緩沖池
放資訊的時候,消費者不可操作緩沖池,反之亦然。
2.使用多線程和信号量解決該經典問題的互斥
轉載:http://blog.csdn.net/gatieme/article/details/50994533