信號量是是什麼?
信號量是進程間通信方式之一,用來實現進程間的同步與互斥。信號量的原理是一種數據操作鎖的概念,它本身不具備數據交換的功能,而是通過控制其他通信資源(如文本、外部設備等)來實現進程間通信。信號量本身不具備數據傳輸的功能,他只是一種外部資源的標識。
信號量的本質是:具有等待隊列的計數器。
相關概念介紹:
臨界資源:多個進程或線程可以共享的資源。
臨界區:對臨界資源進行操作的代碼。
同步:訪問臨界資源的時序可控性。
互斥:同一時間訪問臨界資源的唯一性。
爲了獲得共享資源,進程需要執行以下步驟:
1.測試控制該資源的信號量
2.若此信號爲正,當進程對信號量做減1操作時,表明它使用了一個單位的資源。
3.如此信號量的值爲0,則進程進入休眠狀態,直至信號量值大於0時。進程被喚醒後再執行第1步。
4.當進程不再使用該信號量控制的共享資源時,該信號值增加1.如果此時有等待此信號量的進程,則喚醒它。
注意:常用的信號量形式被稱爲二元信號量或雙態信號量。它控制單個資源,初值爲1。其實信號量初值可以爲任一正值,該值說明有多少個共享資源單位可供使用。信號量是對臨界資源的一種保護機制,所以對它操作是不能被打斷的即原子性,而這些都是內核幫我們做好的。
信號量的操作步驟:
- 信號量創建:
int semget(key_t key, int nsems, int semflg);
參數:
key:操作系統對該信號量的標識。
nsems:創建該信號量集合中的信號量數。
semflg:IPC_CREAT、IPC_EXCL等(創建標記)。
返回值:成功返回信號量操作句柄semid;失敗返回-1,並設置errno。
- 信號量的控制:
int semctl(int semid, int semnum, int cmd, ...);
參數:
semid:信號量ID
semnum:只要操作的第幾個信號量。
cmd:爲具體所要進行的操作,常用的操作:SETVAL設置單個信號量的初值。
SETALL:設置所有信號量的初值,此時的semnum會被忽略。
IPC_RMID:刪除信號量。
…:不定參數,可有可無。如:我們需要獲取信號量信息時就可通過這個參數獲取。
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO(Linux-specific) */
};
當我們要控制信號量的時候,必須手動定義這個聯合體。
val:是對信號量所要賦的初值。
buf:關於信號量的一些詳細信息存放在buf中
array:信號量集,用來設置信號量集中的信號量或獲取。
- 操作信號量:
int semop(int semid, struct sembuf *sops, unsigned nsops);
參數:
semid:所要操作信號量集的ID。
sops:對該信號量集的操作(下面詳細說明sembuf)。
nsops:要操作的信號數量。
返回值:成功返回0;失敗返回-1,並設置errno。
struct sembuf:這個結構體在我們PV操作時會用到。
struct sembuf
{
unsigned short int sem_num; /* semaphore number */
short int sem_op; /* semaphore operation */
short int sem_flg; /* operation flag */
};
數據成員:
sem_num:所要操作第幾個信號量
sem_op:所要做的操作,賦值爲-1表示P操作(資源申請),賦值爲1表示V操作(資源釋放)。
short_flag:通常設置爲SEM_UNDO,使得操作系統能夠跟蹤信號量,並在沒有釋放信號量時自動釋放。
下面我們可以通過一個小案例來實現一個同步互斥的例子:
案例1:加入現在我們有一個人是專門生產糖果的,還有一個人是專門吃糖果的,那麼這兩個人就必須遵守一個時序(吃糖果的人必須要在生糖果人生產出糖果後才能吃到糖果),這用專業術語就叫對同一資源訪問的時序性控制。程序說明:父進程(生產糖果的人)、子進程(吃糖果的人)
sync.c(同步):
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/sem.h>
#define KEY 0x01234567
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
};
//PV操作
void sem_p(int semid)
{
struct sembuf buf;
buf.sem_num = 0;
buf.sem_op = -1;
buf.sem_flg = SEM_UNDO;
semop(semid,&buf,1);
}
void sem_v(int semid)
{
struct sembuf buf;
buf.sem_num = 0;
buf.sem_op = 1;
buf.sem_flg = SEM_UNDO;
semop(semid,&buf,1);
}
int main()
{
//1.創建1個信號量
int semid = semget(KEY,1,IPC_CREAT|0664);
if(semid<0)
{
perror("semget error!\n");
return 1;
}
//同過父子進程完成同步操作
if(fork()==0)
{
while(1)
{
sem_p(semid);
printf("吃了一顆糖!!\n");
sem_v(semid);
}
}
else
{ //2.設置信號量初值,需要定義一個union semun
union semun sem;
sem.val = 1;
if(semctl(semid,0,SETVAL,sem)<0)
{
perror("semctl error!");
return 2;
}
while(1)
{
sem_p(semid);
printf("生產了一顆糖!!\n");
sleep(1);
sem_v(semid);
}
}
//3.最後要釋放信號量
semctl(semid,0,IPC_RMID);
return 0;
}
效果展示:
案例2:父子進程爭搶顯示器資源父進程打印B然後睡上1毫秒再打印個B、子進程打印A然後睡上1毫秒再打印個A。案例分析:如果我們不對顯示器資源進行保護,那麼可能在父子進程休眠的過程中,雙方都對顯示器資源操作,此時打印出來的將是 父子進程數據的交錯信息。
print.c:
#include<stdio.h>
#include<unistd.h>
int main()
{
pid_t pid = -1;
if((pid = fork())<0)
{
perror("fork!");
return 1;
}
if(pid == 0)
{
while(1)
{
printf("A");
fflush(stdout);
usleep(1000);
printf("A ");
fflush(stdout);
}
}
else if(pid>0)
{
while(1)
{
printf("B");
fflush(stdout);
usleep(1000);
printf("B ");
fflush(stdout);
}
//3.最後要釋放信號量
semctl(semid,0,IPC_RMID);
return 0;
}
}
不對顯示器資源進行保護打印的結果將會是這樣:
此時我們看到父子進程交叉進行,而沒有做到(同一時刻對共享資源的唯一訪問性即就是互斥)。
**下面通過互斥來保護同一時刻只能有一個進程訪問顯示器。
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/sem.h>
#define KEY 0x01234567
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
};
//PV操作
void sem_p(int semid)
{
struct sembuf buf;
buf.sem_num = 0;
buf.sem_op = -1;
buf.sem_flg = SEM_UNDO;
semop(semid,&buf,1);
}
void sem_v(int semid)
{
struct sembuf buf;
buf.sem_num = 0;
buf.sem_op = 1;
buf.sem_flg = SEM_UNDO;
semop(semid,&buf,1);
}
int main()
{
//1.創建1個信號量
int semid = semget(KEY,1,IPC_CREAT|0664);
if(semid<0)
{
perror("semget error!\n");
return 1;
}
//2.設置信號量初值,需要定義一個union semun
union semun sem;
sem.val = 1;
if(semctl(semid,0,SETVAL,sem)<0)
{
perror("semctl error!");
return 2;
}
//同過父子進程完成同步操作
if(fork()==0)
{
while(1)
{
sem_p(semid);
printf("A");
fflush(stdout);
usleep(1000);
printf("A ");
fflush(stdout);
sem_v(semid);
}
}
else
{
while(1)
{
sem_p(semid);
printf("B");
fflush(stdout);
usleep(1000);
printf("B ");
fflush(stdout);
sem_v(semid);
}
//3.最後要釋放信號量
semctl(semid,0,IPC_RMID);
return 0;
}
}
運行效果展示:
此時我們看到A、B是成雙成對出現的說明同一個時刻父子進程只有一個在操作顯示器(輸出設備)。
通過對信號量的學習,我們就可以對上次共享內存的server&client案例進行改進了,這裏就不在演示了,後面有機會在更新,這裏關於信號量重點已經講完。