首先了解一下,信號量機概念是由荷蘭科學家Dijkstr引入,值得一提的是,它提出的Dijksrtr算法解決了最短路徑問題。
信號量又稱為信號燈,它是用來協調不同進程間的數據對象的,而最主要的應用是共享內存方式的進程間通信。本質上,信號量是一個計數器,它用來記錄對某個資源(如共享內存)的存取狀況,信號量是一個特殊的變量,並且只有兩個操作可以改變其值:等待(wait)與信號(signal)。
因為在Linux與UNIX編程中,"wait"與"signal"已經具有特殊的意義了(暫不知這特殊意義是啥),所以原始概念:
用於等待(wait)的P(信號量變量) ;
用於信號(signal)的V(信號量變量) ;
這兩字母來自等待(passeren:通過,如同臨界區前的檢測點)與信號(vrjgeven:指定或釋放,如同釋放臨界區的控制權)的荷蘭語。
P操作 負責把當前進程由運行狀態轉換為阻塞狀態,直到另外一個進程喚醒它。
操作為:申請一個空閑資源(把信號量減1),若成功,則退出;若失敗,則該進程被阻塞;
V操作 負責把一個被阻塞的進程喚醒,它有一個參數表,存放着等待被喚醒的進程信息。
操作為:釋放一個被占用的資源(把信號量加1),如果發現有被阻塞的進程,則選擇一個喚醒之。
補充:查看共享信息的內存的命令是ipcs [-m|-s|-q] (全部的話是ipcs -a) ;查看共享信息的內存的命令是ipcs [-m|-s|-q]。
(一)系統調用函數semget()
函數原型:int semget(key_t key,int nsems,int semflg);
功能描述: 創建一個新的信號量集,或者存取一個已經存在的信號量集。
當調用semget創建一個信號量時,他的相應的semid_ds結構被初始化。ipc_perm中各個量被設置為相應
值:
sem_nsems被設置為nsems所示的值;
sem_otime被設置為0;
sem_ctime被設置為當前時間
參數介紹:
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 等待信號量的值歸零的進程數。 |
(二)信號量的控制 semctl()
原型: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參數會返回不同的值。它的一個重要用途是為信號量賦初值,因為進程無法直接對信號量的值進行修改。
(三)信號量操作semop函數
在 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); } }
結果截圖:
(實驗前后信號量的值與標識符都不在。)
實驗測試三:用信號量機制解決實際的進程同步問題。有三個進程分別用P1、P2、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):
緩沖機制一般分為:全緩沖、行緩沖、無緩沖。
- 全緩沖:緩沖區滿了以后,才發生真正的IO。我們通常用的磁盤文件IO就是這樣的。當然你可以調用flush類函數強制刷新緩沖。
- 行緩沖:緩沖區滿了以后或者緩沖區收到一個換行符(表示已輸入或輸出一行),后才發生真正的IO,比如標准輸出和標准輸入默認的緩沖機制就是行緩沖。(行緩沖還有一些規則,參考APUE)
- 無緩沖:立即發生IO,通常標准出錯是不帶緩沖的。所以建議用輸出信息來調試程序時,最后用標准出錯IO,以免調試信息延遲輸出。
顯然這里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 三個函數的介紹