首先了解一下,信號量機概念是由荷蘭科學家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)。此操作可以隨時進行---它永遠不會強制等待的過程。調用進程必須有改變信號量集的權限。
實驗代碼:
pv.h pv.c文件介紹:可以封裝成一個塊
pv.h
#ifndef __PV_H__
#define __PV_H__
#include <sys/types.h>
int I(int semnums,int value);
int I_2(key_t key,int semnums,int value);
void P(int semid,int semnum,int value);
void V(int semid,int semnum,int value);
void D(int semid);
#endif
pv.c
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include "pv.h"
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
};
int I(int semnums,int value)
{
int semid;
if((semid=semget(IPC_PRIVATE,semnums,IPC_CREAT|IPC_EXCL|0777))<0)//創建一個信號量
{
perror("semget error");
return -1;
}
union semun un;
unsigned short *array=(unsigned short*)calloc(semnums,sizeof(unsigned short));
int i=0;
for(i=0;i<semnums;i++)
{
array[i]=value;
}
un.array=array;
if(semctl(semid,0,SETALL,un)<0)//SETALL 用ctl_arg.array中的值設置所有信號量的semvals
{
perror("semctl error");
return -1;
}
free(array);
return semid;
}
int I_2(key_t key,int semnums,int value)
{
int semid;
if((semid=semget(key,semnums,IPC_CREAT|IPC_EXCL))<0)
{
perror("semget error");
return -1;
}
union semun un;
unsigned short *array=(unsigned short*)calloc(semnums,sizeof(unsigned short));
int i=0;
for(i=0;i<semnums;i++)
{
array[i]=value;
}
un.array=array;
if(semctl(semid,0,SETALL,un)<0)
{
perror("semctl error");
return -1;
}
free(array);
return semid;
}
void P(int semid,int semnum,int value)
{
assert(value>=0);
struct sembuf buf[]={semnum,-value,SEM_UNDO};
if(semop(semid,buf,sizeof(buf)/sizeof(struct sembuf))<0)
{
perror("P:semop error");
}
else
{
printf("semop P operation success\n");
}
}
void V(int semid,int semnum,int value)
{
assert(value>=0);
struct sembuf buf[]={semnum,value,SEM_UNDO};
if(semop(semid,buf,sizeof(buf)/sizeof(struct sembuf))<0)
{
perror("V:semop error");
}
else
{
printf("semop P operation success\n");
}
}
void D(int semid)
{
if(semctl(semid,0,IPC_RMID,NULL)<0)
{
perror("D operation error");
}
else
{
printf("D operation success\n");
}
}
可以調用命令:gcc -o pv.o -c pv.c 鏈接成一個.o文件
測試文件:test_sem.c
#include "pv.h"
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
void handlesem(key_t skey,int semid);
int main(void)
{
key_t semkey=0x200;
int i;
int semid;
semid=I_2(semkey,1,1);
for (i=0;i<3;i++)
{
if (fork()==0) //父進程負責產生3個子進程
handlesem(semkey,semid); //子進程中才執行handlesem,做完後就exit。
}
D(semid);
exit(0);
}
void handlesem(key_t skey,int semid)
{
pid_t pid=getpid();
printf("進程 %d 在臨界資源區之前 \n",pid);
P(semid,0,1); //進程進入臨界資源區,信號量減少1
printf("進程 %d 在使用臨界資源時,停止10s \n",pid);
/*in real life do something interesting */
sleep(10);
printf("進程 %d 退出臨界區後 \n",pid);
V(semid,0,1); //進程退出臨界資源區,信號量加1
printf("進程 %d 完全退出\n",pid);
exit(0);
}
執行結果:
進程 30562 在臨界資源區之前
semop P operation success
進程 30562 在使用臨界資源時,停止10s
進程 30561 在臨界資源區之前
進程 30560 在臨界資源區之前
進程 30562 退出臨界區後
semop V operation success
semop P operation success
進程 30561 在使用臨界資源時,停止10s
進程 30562 完全退出
進程 30561 退出臨界區後
semop V operation success
semop P operation success
進程 30560 在使用臨界資源時,停止10s
進程 30561 完全退出
進程 30560 退出臨界區後
semop V operation success
進程 30560 完全退出
D operation success
線程信號量和進程信號量的區別:
信號量分爲有名與無名
信號量在進程是以有名信號量進行通信的,在線程是以無名信號進行通信的,因爲線程LINUX還沒有實現進程間的通信,所以在sem_init的第二個參數要爲0,而且在多線程間的同步是可以通過有名信號量也可通過無名信號,但是一般情況線程的同步是無名信號量,無名信號量比較簡單,有名信號量必須由LINUX 內核管理,由內核結構struct ipc_ids 存儲,是隨內核持續,系統關閉,信號量則刪除,當然也可以顯示刪除,通過系統調用刪除。