信號量又稱爲信號燈,它是用來協調不一樣進程間的數據對象的,而最主要的應用是共享內存方式的進程間通訊。本質上,信號量是一個計數器,它用來記錄對某個資源(如共享內存)的存取情況,信號量是一個特殊的變量,而且只有兩個操做能夠改變其值:等待(wait)與信號(signal)。html
由於在Linux與UNIX編程中,"wait"與"signal"已經具備特殊的意義了(暫不知這特殊意義是啥),因此原始概念:
用於等待(wait)的P(信號量變量) ;
用於信號(signal)的V(信號量變量) ;
這兩字母來自等待(passeren:經過,如同臨界區前的檢測點)與信號(vrjgeven:指定或釋放,如同釋放臨界區的控制權)的荷蘭語。linux
P操做 負責把當前進程由運行狀態轉換爲阻塞狀態,直到另一個進程喚醒它。算法
操做爲:申請一個空閒資源(把信號量減1),若成功,則退出;若失敗,則該進程被阻塞;編程
V操做 負責把一個被阻塞的進程喚醒,它有一個參數表,存放着等待被喚醒的進程信息。數組
操做爲:釋放一個被佔用的資源(把信號量加1),若是發現有被阻塞的進程,則選擇一個喚醒之。 數據結構
補充:查看共享信息的內存的命令是ipcs [-m|-s|-q] (所有的話是ipcs -a) ;查看共享信息的內存的命令是ipcs [-m|-s|-q]。ide
函數原型:int semget(key_t key,int nsems,int semflg); 函數
功能描述: 建立一個新的信號量集,或者存取一個已經存在的信號量集。測試
當調用semget建立一個信號量時,他的相應的semid_ds結構被初始化。ipc_perm中各個量被設置爲相應
值:
sem_nsems被設置爲nsems所示的值;
sem_otime被設置爲0;
sem_ctime被設置爲當前時間 ui
參數介紹:
key:所建立或打開信號量集的鍵值,鍵值是IPC_PRIVATE,該值一般爲0,建立一個僅能被進程進程給個人信號量, 鍵值不是IPC_PRIVATE,咱們能夠指定鍵值,例如1234;也能夠一個ftok()函數來取得一個惟一的鍵值。
nsems:建立的信號量集中的信號量的個數,該參數只在建立信號量集時有效。
semflg:調用函數的操做類型,也可用於設置信號量集的訪問權限,二者經過or表示:
有IPC_CREAT,IPC_EXCL兩種:
IPC_CREAT若是信號量不存在,則建立一個信號量,不然獲取。
IPC_EXCL只有信號量不存在的時候,新的信號量才創建,不然就產生錯誤。
返回值說明:
若是成功,則返回信號量集的IPC標識符,其做用與信息隊列識符同樣。
若是失敗,則返回-1,errno被設定成如下的某個值
EACCES:沒有訪問該信號量集的權限
EEXIST:信號量集已經存在,沒法建立
EINVAL:參數nsems的值小於0或者大於該信號量集的限制;或者是該key關聯的信號量集已存在,而且nsems
大於該信號量集的信號量數
ENOENT:信號量集不存在,同時沒有使用IPC_CREAT
ENOMEM :沒有足夠的內存建立新的信號量集
ENOSPC:超出系統限制
圖解:
每一個信號量都有一些相關值: semval 信號量的值,通常是一個正整數,它只能經過信號量系統調用semctl函數設置,程序沒法直接對它進行修改。 sempid 最後一個對信號量進行操做的進程的pid. semcnt 等待信號量的值大於其當前值的進程數。 semzcnt 等待信號量的值歸零的進程數。 |
原型:int semctl(int semid,int semnum,int cmd,union semun ctl_arg);
參數介紹: semid爲信號量集引用標誌符,即semget 的返回值。
semnum第二個參數是信號量數目;
cmd表示調用該函數執行的操做,其取值和對應操做以下:
標準的IPC函數 (注意在頭文件<sys/sem.h>中包含semid_ds結構的定義) |
IPC_STAT 把狀態信息放入ctl_arg.stat中 IPC_SET 用ctl_arg.stat中的值設置全部權/許可權 IPC_RMID 從系統中刪除信號量集合 |
單信號量操做 (下面這些宏與sem_num指定的信號量合semctl返回值相關) |
GETVAL 返回信號量的值(也就是semval) SETVAL 把信號量的值寫入ctl_arg.val中 GETPID 返回sempid值 GETNCNT 返回semncnt(參考上面內容) GETZCNT 返回semzcnt(參考上面內容) |
全信號量操做 |
GETALL 把全部信號量的semvals值寫入ctl_arg.array SETALL 用ctl_arg.array中的值設置全部信號量的semvals |
參數arg表明一個union的semun的實例。semun是在linux/sem.h中定義的:
union semun {
int val; //執行SETVAL命令時使用
struct semid_ds *buf; //在IPC_STAT/IPC_SET命令中使用
unsigned short *array; //使用GETALL/SETALL命令時使用的指針
}
聯合體中每一個成員都有各自不一樣的類型,分別對應三種不一樣的semctl 功能,若是semval 是SETVAL.則使用的將是ctl_arg.val.
。
功能:smctl函數依據command參數會返回不一樣的值。它的一個重要用途是爲信號量賦初值,由於進程沒法直接對信號量的值進行修改。
在 Linux 下,PV 操做經過調用semop函數來實現,也只有它能對PV進行操做
調用原型:int semop(int semid,struct sembuf*sops,unsign ednsops);
返回值:0,若是成功。-1,若是失敗:errno=E2BIG(nsops大於最大的ops數目)
EACCESS(權限不夠)
EAGAIN(使用了IPC_NOWAIT,但操做不能繼續進行)
EFAULT(sops指向的地址無效)
EIDRM(信號量集已經刪除)
EINTR(當睡眠時接收到其餘信號)
EINVAL(信號量集不存在,或者semid無效)
ENOMEM(使用了SEM_UNDO,但無足夠的內存建立所需的數據結構)
ERANGE(信號量值超出範圍)
參數介紹:
第一個參數semid 是信號量集合標識符,它多是從前一次的semget調用中得到的。
第二個參數是一個sembuf結構的數組,每一個 sembuf 結構體對應一個特定信號的操做,sembuf結構在,<sys/sem.h>中定義
struct sembuf{
usign short sem_num;/*信號量索引*/
short sem_op;/*要執行的操做*/
short sem_flg;/*操做標誌*/
}
sem_num 存放集合中某一信號量的索引,若是集合中只包含一個元素,則sem_num的值只能爲0。
----------------------------------------------------------------------------------------------
Sem_op取得值爲一個有符號整數,該整數實際給定了semop函數將完成的功能。包括三種狀況:
若是sem_op是負數,那麼信號量將減去它的值,對應於p()操做。這和信號量控制的資源有關。若是沒有使用IPC_NOWAIT,那麼調用進程將進入睡眠狀態,直到信號量控制的資源可使用爲止。
若是sem_op是正數,則信號量加上它的值。對應於v()操做。這也就是進程釋放信號量控制的資源。
最後,若是sem_op是0,那麼調用進程將調用sleep(),直到信號量的值爲0。這在一個進程等待徹底空閒的資源時使用。
----------------------------------------------------------------------------------------------
sem_flag是用來告訴系統當進程退出時自動還原操做,它維護着一個整型變量semadj(信號燈的計數器),可設置爲 IPC_NOWAIT 或 SEM_UNDO 兩種狀態。只有將 sem_flg 指定爲 SEM_UNDO 標誌後,semadj (所指定信號量針對調用進程的調整值)纔會更新,即減去減去sem_num的值。 此外,若是此操做指定SEM_UNDO,系統更新過程當中會撤消此信號燈的計數(semadj)。此操做能夠隨時進行---它永遠不會強制等待的過程。調用進程必須有改變信號量集的權限。
第三個參數是sembuf組成的數組中索引。參數sops指向由sembuf組成的數組,結構數組中的一員。
實驗所需頭文件:放在/usr/include目錄下
//pv.h頭文件 #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> #include <errno.h> #define SEMPERM 0600 #define TRUE 1 #define FALSE 0 typedef union _semun { int val; struct semid_ds *buf; ushort *array; } semun;
信號量賦初值以及獲取信號量標識符函數:
//initsem.c 對信號量賦初值,初值固定爲1 #include "pv.h" int initsem(key_t semkey) { int status=0,semid; //信號量標識符semid if ((semid=semget(semkey,1,SEMPERM|IPC_CREAT|IPC_EXCL))==-1) { if (errno==EEXIST) //EEXIST:信號量集已經存在,沒法建立 semid=semget(semkey,1,0); //建立一個信號量 } else { semun arg; arg.val=1; //信號量的初值 status=semctl(semid,0,SETVAL,arg); //設置信號量集中的一個單獨的信號量的值。 } if (semid==-1||status==-1) { perror("initsem failed"); return(-1); } /*all ok*/ return(semid); }
v操做
//v.c V操做 #include "pv.h" int v(int semid) { struct sembuf v_buf; v_buf.sem_num=0; v_buf.sem_op=1; //信號量加1 v_buf.sem_flg=SEM_UNDO; if (semop(semid, &v_buf, 1)==-1) { perror("v(semid)failed"); exit(1); } return(0); }
p操做
//p.c P操做 #include "pv.h" int p(int semid) { struct sembuf p_buf; p_buf.sem_num=0; p_buf.sem_op=-1; //信號量減1,注意這一行的1前面有個負號 p_buf.sem_flg=SEM_UNDO; //p_buf = {0,-1,SEM_UNDO}; if (semop(semid, &p_buf, 1)==-1) { perror("p(semid)failed"); exit(1); } return(0); }
測試函數一(使用PV操做實現三個進程的互斥)
//testsem.c 主程序,使用PV操做實現三個進程的互斥 #include "pv.h" void handlesem(key_t skey); main() { key_t semkey=0x200; int i; for (i=0;i<3;i++) { if (fork()==0) //父進程負責產生3個子進程 handlesem(semkey); //子進程中才執行handlesem,作完後就exit。 } } void handlesem(key_t skey) { int semid; pid_t pid=getpid(); if ((semid=initsem(skey))<0) exit(1); printf("進程 %d 在臨界資源區以前 \n",pid); p(semid); //進程進入臨界資源區,信號量減小1 printf("進程 %d 在使用臨界資源時,中止10s \n",pid); /*in real life do something interesting */ sleep(10); printf("進程 %d 退出臨界區後 \n",pid); v(semid); //進程退出臨界資源區,信號量加1 printf("進程 %d 徹底退出\n",pid); exit(0); }
測試結果截圖:
測試函數二(實現兩個進程交替輸出A和B,並在程序中查看信號量的值)
//ab.c 主程序,使用PV操做,兩個進程交替輸出A和B,實現臨界區的互斥訪問的基本模型 #include "pv.h" main() { key_t semkey_A=0x200; key_t semkey_B=0x220; int semid_A,semid_B; if ((semid_A=initsem(semkey_A,1))<0) exit(1); if ((semid_B=initsem(semkey_B,0))<0) exit(1); printf("A 進程A的信號量標識符%d,它的初始值爲%d\n", semid_A,semctl(semid_A, 0, GETVAL)); printf("B 進程B的信號量標識符%d,它的初始值爲%d\n", semid_B,semctl(semid_B, 0, GETVAL)); if (fork()!=0) //父進程先執行 { int i; for (i=0;i<10;i++) { p(semid_A); printf("A 進程A的信號量值爲%d\n",semctl(semid_A, 0, GETVAL)); v(semid_B); } } else { int j; for (j=0;j<10;j++) { p(semid_B); printf("B 進程B的信號量值爲%d\n",semctl(semid_B, 0, GETVAL)); v(semid_A); } } }
測試結果
實驗思考:
(1)信號量一經建立就存在在內存中,這會影響到其餘用戶及其程序。所以妥善的作法是在程序結束時,若再也不須要該信號量,則能夠將其從內存中刪除,要求實現刪除信號量以及輸出信號量的值,使用semctl的刪除命令就能夠了,代碼以下:
#include "pv.h" main() { key_t semkey_A=0x200; key_t semkey_B=0x220; int semid_A,semid_B; if ((semid_A=initsem(semkey_A,1))<0) exit(1); if ((semid_B=initsem(semkey_B,0))<0) exit(1); printf("A 進程A的信號量標識符%d,它的初始值爲%d\n", semid_A,semctl(semid_A, 0, GETVAL)); printf("B 進程B的信號量標識符%d,它的初始值爲%d\n", semid_B,semctl(semid_B, 0, GETVAL)); if (fork()!=0) //父進程先執行 { int i; for (i=0;i<10;i++) { p(semid_A); printf("A 進程A的信號量值爲%d\n",semctl(semid_A, 0, GETVAL)); v(semid_B); } } else { int j; for (j=0;j<10;j++) { p(semid_B); printf("B 進程B的信號量值爲%d\n",semctl(semid_B, 0, GETVAL)); v(semid_A); } } if((semctl(semid_A,0,IPC_RMID))<0) //刪除進程Ad的信號量值,IPC_RMID是刪除命令 { perror("semctl error"); exit(1); } if((semctl(semid_B,0,IPC_RMID))<0) { perror("semctl error"); exit(1); } }
結果截圖:
(實驗先後信號量的值與標識符都不在。)
實驗測試三:用信號量機制解決實際的進程同步問題。有三個進程分別用P一、P二、P3表示,其中P1輸出字符A,P2輸出字符B,P3輸出字符C;現要求三個進程協做完成以下的輸出序列:
ABABABCABABABCABABABC…
本身寫的代碼:
//abc.c 主程序,使用PV操,在實驗二的基礎上輸出ABABABCABABABCABABABC… #include "pv.h" main() { key_t semkey_A=0x200; key_t semkey_B=0x220; key_t semkey_C=0x240; int semid_A,semid_B,semid_C; if ((semid_A=initsem(semkey_A))<0) exit(1); if ((semid_B=initsem(semkey_B))<0) exit(1); if ((semid_C=initsem(semkey_C))<0) exit(1); printf("A 進程A的信號量%d,它的初始值爲%d\n", semid_A,semctl(semid_A, 0, GETVAL)); printf("B 進程B的信號量%d,它的初始值爲%d\n", semid_B,semctl(semid_B, 0, GETVAL)); printf("C 進程B的信號量%d,它的初始值爲%d\n", semid_C,semctl(semid_C, 0, GETVAL)); int count=0; if (fork()!=0) //父進程先執行 { int i; for (i=0;i<10;i++) { p(semid_B); printf("A 進程A的信號量值爲%d\n",semctl(semid_A, 0, GETVAL)); v(semid_A); } } else { int j; for (j=0;j<10;j++) { p(semid_A); printf("B 進程B的信號量值爲%d\n",semctl(semid_B, 0, GETVAL)); count++; if (count==3) { v(semid_C); printf("C 進程C的信號量值爲%d,couont=%d\n",semctl(semid_C, 0, GETVAL),count) ; v(semid_B); count=0;} else v(semid_B); } } if((semctl(semid_A,0,IPC_RMID))<0) //刪除進程A的信號量值,IPC_RMID是刪除命令 { perror("semctl error"); exit(1); } if((semctl(semid_B,0,IPC_RMID))<0) { perror("semctl error"); exit(1); } if((semctl(semid_C,0,IPC_RMID))<0) { perror("semctl error"); exit(1); } }
實驗結果截圖:
實驗分析:
觀察到C是出如今第3個B後面的,就在輸出B的控制語句里加一個判斷就能夠了。
本身寫出代碼後,發覺實驗指導書後面給了答案,坑:
//abc.c 主程序,使用PV操做,三個進程分別輸出A和B和C //同步輸出格式爲:ABABABC-ABABABC-ABABABC-ABABABC- #include "pv.h" main() { key_t semkey_A=0x200; key_t semkey_B=0x220; key_t semkey_C=0x260; int semid_A,semid_B,semid_C; if ((semid_A=initsem(semkey_A,1))<0) exit(1); if ((semid_B=initsem(semkey_B,0))<0) exit(1); if ((semid_C=initsem(semkey_C,0))<0) exit(1); if (fork()>0)//父進程 { if (fork()>0) {//父進程 int i; for (i=0;i<90;i++) { p(semid_A); printf("A\n"); v(semid_B); } } else {//第二次fork的子進程 int j; int count=0; for (j=0;j<90;j++) { p(semid_B); printf("B\n"); count++; if (count==3) { v(semid_C); count=0; } else { v(semid_A); } } } } else//第一次fork的子進程 { int k; for (k=0;k<30;k++) { p(semid_C); printf("C-\n"); v(semid_A); } } }
實驗思考:若將輸出語句中的「\n」去掉,程序執行會有什麼不一樣,你推測多是什麼緣由形成的?
若是去掉\n等相關輸出,代碼以下:
//abc.c 主程序,使用PV操,在實驗二的基礎上輸出ABABABCABABABCABABABC… #include "pv.h" main() { key_t semkey_A=0x200; key_t semkey_B=0x220; key_t semkey_C=0x240; int semid_A,semid_B,semid_C; if ((semid_A=initsem(semkey_A))<0) exit(1); if ((semid_B=initsem(semkey_B))<0) exit(1); if ((semid_C=initsem(semkey_C))<0) exit(1); printf("A 進程A的信號量%d,它的初始值爲%d\n", semid_A,semctl(semid_A, 0, GETVAL)); printf("B 進程B的信號量%d,它的初始值爲%d\n", semid_B,semctl(semid_B, 0, GETVAL)); printf("C 進程B的信號量%d,它的初始值爲%d\n", semid_C,semctl(semid_C, 0, GETVAL)); int count=0; if (fork()!=0) //父進程先執行 { int i; for (i=0;i<10;i++) { p(semid_B); printf("A"); v(semid_A); } } else { int j; for (j=0;j<10;j++) { p(semid_A); printf("B"); count++; if (count==3) { v(semid_C); printf("C") ; v(semid_B); count=0;} else v(semid_B); } } if((semctl(semid_A,0,IPC_RMID))<0) //刪除進程A的信號量值,IPC_RMID是刪除命令 { perror("semctl error"); exit(1); } if((semctl(semid_B,0,IPC_RMID))<0) { perror("semctl error"); exit(1); } if((semctl(semid_C,0,IPC_RMID))<0) { perror("semctl error"); exit(1); } }
結果截圖:
緣由分析:
這和緩衝機制有關(參考:這個寫的很不錯http://www.myexception.cn/linux-unix/1442125.html):
緩衝機制通常分爲:全緩衝、行緩衝、無緩衝。
顯然這裏printf採用的是標準IO,只有當遇到換行符號後,纔會輸出,如若沒有,則父子進程只能一次性輸出緩衝區裏內容,就會有上面的結果。
參考:
http://www.cnblogs.com/lixiaofei1987/p/3208414.html semop函數詳解
http://www.cnblogs.com/hjslovewcl/archive/2011/03/03/2314341.html 信號量介紹
http://blog.chinaunix.net/uid-23193900-id-3221978.html 三個函數的介紹