概念
Linux內(nèi)核的信號量在概念和原理上和用戶態(tài)的System V的IPC機制信號量是相同的,不過他絕不可能在內(nèi)核之外使用,因此他和System V的IPC機制信號量毫不相干。
如果有一個任務想要獲得已經(jīng)被占用的信號量時,信號量會將其放入一個等待隊列(它不是站在外面癡癡地等待而是將自己的名字寫在任務隊列中)然后讓其睡眠。
當持有信號量的進程將信號釋放后,處于等待隊列中的一個任務將被喚醒(因為隊列中可能不止一個任務),并讓其獲得信號量。這一點與自旋鎖不同,處理器可以去執(zhí)行其它代碼。
應用場景
由于爭用信號量的進程在等待鎖重新變?yōu)榭捎脮r會睡眠,所以信號量適用于鎖會被長時間持有的情況;相反,鎖被短時間持有時,使用信號量就不太適宜了,因為睡眠、維護等待隊列以及喚醒所花費的開銷可能比鎖占用的全部時間表還要長。
舉2個生活中的例子:
-
我們坐火車從南京到新疆需要2天的時間,這個'任務'特別的耗時,只能坐在車上等著車到站,但是我們沒有必要一直睜著眼睛等,理想的情況就是我們上車就直接睡覺,醒來就到站(看過《異形》的讀者會深有體會),這樣從人(用戶)的角度來說,體驗是最好的,對比于進程,程序在等待一個耗時事件的時候,沒有必須要一直占用CPU,可以暫停當前任務使其進入休眠狀態(tài),當?shù)却氖录l(fā)生之后再由其他任務喚醒,類似于這種場景采用信號量比較合適。
-
我們有時候會等待電梯、洗手間,這種場景需要等待的時間并不是很多,如果我們還要找個地方睡一覺,然后等電梯到了或者洗手間可以用了再醒來,那很顯然這也沒有必要,我們只需要排好隊,刷一刷抖音就可以了,對比于計算機程序,比如驅動在進入中斷例程,在等待某個寄存器被置位,這種場景需要等待的時間往往很短暫,系統(tǒng)開銷甚至遠小于進入休眠的開銷,所以這種場景采用自旋鎖比較合適。
關于信號量和自旋鎖,以及死鎖問題,我們后面會再詳細討論。
使用方法
一個任務要想訪問共享資源,首先必須得到信號量,獲取信號量的操作將把信號量的值減1,若當前信號量的值為負數(shù),表明無法獲得信號量,該任務必須掛起在 該信號量的等待隊列等待該信號量可用;若當前信號量的值為非負數(shù),表示能獲得信號量,因而能即時訪問被該信號量保護的共享資源。
當任務訪問完被信號量保護的共享資源后,必須釋放信號量,釋放信號量通過把信號量的值加1實現(xiàn),如果信號量的值為非正數(shù),表明有任務等待當前信號量,因此他也喚醒所有等待該信號量的任務。
內(nèi)核信號量的構成
內(nèi)核信號量類似于自旋鎖,因為當鎖關閉著時,它不允許內(nèi)核控制路徑繼續(xù)進行。然而,當內(nèi)核控制路徑試圖獲取內(nèi)核信號量鎖保護的忙資源時,相應的進程就被掛起。只有在資源被釋放時,進程才再次變?yōu)榭蛇\行。
只有可以睡眠的函數(shù)才能獲取內(nèi)核信號量;中斷處理程序和可延遲函數(shù)都不能使用內(nèi)核信號量。
內(nèi)核信號量是struct semaphore類型的對象,在內(nèi)核源碼中位于include\linux\semaphore.h文件
struct?semaphore{
????raw_spinlock_t????????lock;
????unsigned?int????????count;
????struct?list_head????wait_list;
}
成員 | 描述 |
---|---|
lock | 在2.6.33之后的版本,內(nèi)核加入了raw_spin_lock系列,使用方法和spin_lock系列一模一樣,只是參數(shù)spinlock_t變?yōu)榱藃aw_spinlock_t |
count | 相當于信號量的值,大于0,資源空閑;等于0,資源忙,但沒有進程等待這個保護的資源;小于0,資源不可用,并至少有一個進程等待資源 |
wait_list | 內(nèi)核鏈表,當前獲得信號量的任務會與該成員一起注冊到等待的鏈表中 |
信號量的API
初始化
DECLARE_MUTEX(name)
該宏聲明一個信號量name并初始化他的值為1,即聲明一個互斥鎖。
DECLARE_MUTEX_LOCKED(name)
該宏聲明一個互斥鎖name,但把他的初始值設置為0,即鎖在創(chuàng)建時就處在已鎖狀態(tài)。因此對于這種鎖,一般是先釋放后獲得。
void?sema_init?(struct?semaphore?*sem,?int?val);
該函用于數(shù)初始化設置信號量的初值,他設置信號量sem的值為val。
注意:
val設置為1說明只有一個持有者,這種信號量叫二值信號量或者叫互斥信號量。
我們還允許信號量可以有多個持有者,這種信號量叫計數(shù)信號量,在初始化時要說明最多允許有多少個持有者也可以把信號量中的val初始化為任意的正數(shù)值n,在這種情況下,最多有n個進程可以并發(fā)地訪問這個資源。
void?init_MUTEX?(struct?semaphore?*sem);
該函數(shù)用于初始化一個互斥鎖,即他把信號量sem的值設置為1。
void?init_MUTEX_LOCKED?(struct?semaphore?*sem);
該函數(shù)也用于初始化一個互斥鎖,但他把信號量sem的值設置為0,即一開始就處在已鎖狀態(tài)。
PV操作
獲取信號量(P)
void?down(struct?semaphore?*?sem);
該函數(shù)用于獲得信號量sem,他會導致調(diào)用該函數(shù)的進程睡眠,因此不能在中斷上下文(包括IRQ上下文和softirq上下文)使用該函數(shù)。該函數(shù)將把sem的值減1,如果信號量sem的值非負,就直接返回,否則調(diào)用者將被掛起,直到別的任務釋放該信號量才能繼續(xù)運行。
int?down_interruptible(struct?semaphore?*?sem);
該函數(shù)功能和down類似,不同之處為,down不會被信號(signal)打斷,但down_interruptible能被信號打斷,因此該函數(shù)有返回值來區(qū)分是正常返回還是被信號中斷,如果返回0,表示獲得信號量正常返回,如果被信號打斷,返回-EINTR。
int?down_trylock(struct?semaphore?*?sem);
該函數(shù)試著獲得信號量sem,如果能夠即時獲得,他就獲得該信號量并返回0,否則,表示不能獲得信號量sem,返回值為非0值。因此,他不會導致調(diào)用者睡眠,能在中斷上下文使用。
int?down_killable(struct?semaphore?*sem);
int?down_timeout(struct?semaphore?*sem,?long?jiffies);
int?down_timeout_interruptible(struct?semaphore?*sem,?long?jiffies);
釋放內(nèi)核信號量(V)
void?up(struct?semaphore?*?sem);
該函數(shù)釋放信號量sem,即把sem的值加1,如果sem的值為非正數(shù),表明有任務等待該信號量,因此喚醒這些等待者。
補充
int?down_interruptible(struct?semaphore?*sem)
這個函數(shù)的功能就是獲得信號量,如果得不到信號量就睡眠,此時沒有信號打斷,那么進入睡眠。但是在睡眠過程中可能被信號打斷,打斷之后返回-EINTR,主要用來進程間的互斥同步。
下面是該函數(shù)的注釋:
/**
*?down_interruptible?-?acquire?the?semaphore?unless?interrupted
*?@sem:?the?semaphore?to?be?acquired
*
*?Attempts?to?acquire?the?semaphore.?If?no?more?tasks?are?allowed?to
*?acquire?the?semaphore,?calling?this?function?will?put?the?task?to?sleep.
*?If?the?sleep?is?interrupted?by?a?signal,?this?function?will?return?-EINTR.
*?If?the?semaphore?is?successfully?acquired,?this?function?returns?0.
*/
一個進程在調(diào)用down_interruptible()之后,如果sem<0,那么就進入到可中斷的睡眠狀態(tài)并調(diào)度其它進程運行, 但是一旦該進程收到信號,那么就會從down_interruptible函數(shù)中返回。并標記錯誤號為:-EINTR。
一個形象的比喻:傳入的信號量為1好比天亮,如果當前信號量為0,進程睡眠,直到(信號量為1)天亮才醒,但是可能中途有個鬧鈴(信號)把你鬧醒。
又如:小強下午放學回家,回家了就要開始吃飯嘛,這時就會有兩種情況:情況一:飯做好了,可以開始吃;情況二:當他到廚房去的時候發(fā)現(xiàn)媽媽還在做, 媽媽就對他說:“你先去睡會,待會做好了叫你?!?小強就答應去睡會,不過又說了一句:“睡的這段時間要是小紅來找我玩,你可以叫醒我?!?小強就是down_interruptible,想吃飯就是獲取信號量,睡覺對應這里的休眠,而小紅來找我玩就是中斷休眠。
使用可被中斷的信號量版本的意思是,萬一出現(xiàn)了semaphore的死鎖,還有機會用ctrl+c發(fā)出軟中斷,讓等待這個內(nèi)核驅動返回的用戶態(tài)進程退出。而不是把整個系統(tǒng)都鎖住了。在休眠時,能被中斷信號終止,這個進程是可以接受中斷信號的!
比如你在命令行中輸入# sleep 10000,按下ctrl + c,就給上面的進程發(fā)送了進程終止信號。信號發(fā)送給用戶空間,然后通過系統(tǒng)調(diào)用,會把這個信號傳給遞給驅動。信號只能發(fā)送給用戶空間,無權直接發(fā)送給內(nèi)核的,那1G的內(nèi)核空間,我們是無法直接去操作的。
內(nèi)核信號量的使用例程
場景1
在驅動程序中,當多個線程同時訪問相同的資源時(驅動中的全局變量時一種典型的共享資源),可能會引發(fā)“競態(tài)“,因此我們必須對共享資源進行并發(fā)控制。Linux內(nèi)核中
解決并發(fā)控制的最常用方法是自旋鎖與信號量(絕大多數(shù)時候作為互斥鎖使用)。
場景2
有時候我們希望設備只能被一個進程打開,當設備被占用的時候,其他設備必須進入休眠。
信號處理示意圖
如上圖:
-
進程A首先通過open()打開設備文件,調(diào)用到內(nèi)核的hello_open(),并調(diào)用down_interruptible(),因為此時信號量沒有被占用,所以進程A可以獲得信號量;
-
進程A獲得信號量之后繼續(xù)處理原有任務,此時進程B也要通過open()打開設備文件,同樣調(diào)用內(nèi)核函數(shù)hello_open(),但此時信號量獲取不到,于是進程B被阻塞;
-
進程A任務執(zhí)行完畢,關閉設備文件,并通過up()釋放信號量,于是進程B被喚醒,并得以繼續(xù)執(zhí)行剩下的任務,
-
進程B執(zhí)行完任務,釋放設備文件,通過up()釋放信號量
代碼如下:
#include?
#include?
#include?
#include?
#include?
#include?
#include?
static?int?major?=?250;
static?int?minor?=?0;
static?dev_t?devno;
static?struct?cdev?cdev;
static?struct?class?*cls;
static?struct?device?*test_device;
static?struct?semaphore?sem;
static?int?hello_open?(struct?inode?*inode,?struct?file?*filep)
{
????
????if(down_interruptible(&sem))//p
????{
????????return?-ERESTARTSYS;
????}
??????return?0;
}
static?int?hello_release?(struct?inode?*inode,?struct?file?*filep)
{
????up(&sem);//v
????return?0;
}
static?struct?file_operations?hello_ops?=
{
????.open?=?hello_open,
????.release?=?hello_release,
};
static?int?hello_init(void)
{
????int?result;
????int?error;????
????printk("hello_init?\n");
????result?=?register_chrdev(?major,?"hello",?&hello_ops);
????if(result?0)
????{
????????printk("register_chrdev?fail?\n");
????????return?result;
????}
????devno?=?MKDEV(major,minor);
????cls?=?class_create(THIS_MODULE,"helloclass");
????if(IS_ERR(cls))
????{
????????unregister_chrdev(major,"hello");
????????return?result;
????}
????test_device?=?device_create(cls,NULL,devno,NULL,"test");
????if(IS_ERR(test_device?))
????{
????????class_destroy(cls);
????????unregister_chrdev(major,"hello");
????????return?result;
????}
????sem_init(&sem,1);
????return?0;
}
static?void?hello_exit(void)
{
????printk("hello_exit?\n");
????device_destroy(cls,devno);????
????class_destroy(cls);
????unregister_chrdev(major,"hello");
????return;
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("daniel.peng");
測試程序 test.c
#include?
#include?
#include?
#include?
main()
{
????int?fd;
????
????printf("before?open\n?");????
????fd?=?open("/dev/test",O_RDWR);??//原子變量??0
????if(fd<0)
????{
????????perror("open?fail?\n");
????????return;
????}
????printf("open?ok?,sleep......\n?");????
????sleep(20);
????printf("wake?up?from?sleep!\n?");????????
????close(fd);???//加為1
}
編譯步驟
1 make 生成 hello.ko
2 gcc test.c -o a
3 gcc test.c -o b
測試步驟
-
安裝驅動
insmod?hello.ko
-
先運行進程A,在運行進程B
可見進程A成功打開設備,在進程A sleep期間會一直占有該字符設備,進程B由于無法獲得信號量,進入休閑,結合代碼可知,進程B被阻塞在函數(shù)open()中。
-
進程A 結束了sleep,并釋放字符設備以及信號量,進程B被喚醒獲得信號量,并成功打開了字符設備。
-
進程B執(zhí)行完sleep函數(shù)后退出,并釋放字符設備和信號量。
讀-寫信號量
跟自旋鎖一樣,信號量也有區(qū)分讀-寫信號量之分。
如果一個讀寫信號量當前沒有被寫者擁有并且也沒有寫者等待讀者釋放信號量,那么任何讀者都可以成功獲得該讀寫信號量;否則,讀者必須被掛起直到寫者釋放該信號量。如果一個讀寫信號量當前沒有被讀者或寫者擁有并且也沒有寫者等待該信號量,那么一個寫者可以成功獲得該讀寫信號量,否則寫者將被掛起,直到?jīng)]有任何訪問者。因此,寫者是排他性的,獨占性的。
讀寫信號量有兩種實現(xiàn),一種是通用的,不依賴于硬件架構,因此,增加新的架構不需要重新實現(xiàn)它,但缺點是性能低,獲得和釋放讀寫信號量的開銷大;另一種是架構相關的,因此性能高,獲取和釋放讀寫信號量的開銷小,但增加新的架構需要重新實現(xiàn)。在內(nèi)核配置時,可以通過選項去控制使用哪一種實現(xiàn)。
讀寫信號量的相關API:
DECLARE_RWSEM(name)
該宏聲明一個讀寫信號量name并對其進行初始化。
void?init_rwsem(struct?rw_semaphore?*sem);
該函數(shù)對讀寫信號量sem進行初始化。
void?down_read(struct?rw_semaphore?*sem);
讀者調(diào)用該函數(shù)來得到讀寫信號量sem。該函數(shù)會導致調(diào)用者睡眠,因此只能在進程上下文使用。
int?down_read_trylock(struct?rw_semaphore?*sem);
該函數(shù)類似于down_read,只是它不會導致調(diào)用者睡眠。它盡力得到讀寫信號量sem,如果能夠立即得到,它就得到該讀寫信號量,并且返回1,否則表示不能立刻得到該信號量,返回0。因此,它也可以在中斷上下文使用。
void?down_write(struct?rw_semaphore?*sem);
寫者使用該函數(shù)來得到讀寫信號量sem,它也會導致調(diào)用者睡眠,因此只能在進程上下文使用。
int?down_write_trylock(struct?rw_semaphore?*sem);
該函數(shù)類似于down_write,只是它不會導致調(diào)用者睡眠。該函數(shù)盡力得到讀寫信號量,如果能夠立刻獲得,就獲得該讀寫信號量并且返回1,否則表示無法立刻獲得,返回0。它可以在中斷上下文使用。
void?up_read(struct?rw_semaphore?*sem);
讀者使用該函數(shù)釋放讀寫信號量sem。它與down_read或down_read_trylock配對使用。
如果down_read_trylock返回0,不需要調(diào)用up_read來釋放讀寫信號量,因為根本就沒有獲得信號量。
void?up_write(struct?rw_semaphore?*sem);
寫者調(diào)用該函數(shù)釋放信號量sem。它與down_write或down_write_trylock配對使用。如果down_write_trylock返回0,不需要調(diào)用up_write,因為返回0表示沒有獲得該讀寫信號量。
void?downgrade_write(struct?rw_semaphore?*sem);
該函數(shù)用于把寫者降級為讀者,這有時是必要的。因為寫者是排他性的,因此在寫者保持讀寫信號量期間,任何讀者或寫者都將無法訪問該讀寫信號量保護的共享資源,對于那些當前條件下不需要寫訪問的寫者,降級為讀者將,使得等待訪問的讀者能夠立刻訪問,從而增加了并發(fā)性,提高了效率。
讀寫信號量適于在讀多寫少的情況下使用,在linux內(nèi)核中對進程的內(nèi)存映像描述結構的訪問就使用了讀寫信號量進行保護。
-THE END-
推薦閱讀
【3】搞懂進程組、會話、控制終端關系,才能明白守護進程干嘛的?
【4】快速掌握TCP協(xié)議
本公眾號全部原創(chuàng)干貨已整理成一個目錄,點擊「干貨」即可獲得。
后臺回復「進群」,即可加入技術交流群,進群福利:免費贈送Linux學習資料。
免責聲明:本文內(nèi)容由21ic獲得授權后發(fā)布,版權歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!