文章目錄:
一、相關基礎概念
(一)進程關係
1.進程同步:進程之間相互合作,協同工作的關係稱爲進程的同步。簡單來說就是多個相關進程在執行次序上的協調,誰先執行後執行都有順序。是一種直接制約關係
。
2.進程互斥:多個進程因爲爭奪臨界資源而互斥執行稱爲進程的互斥。是一種間接制約關係。
如多個人打籃球,籃球是臨界資源,形成互斥,對他們形成了一個間接制約關係。工廠上流水線工作,一道工序接着下一道,有明確的次序,形成同步,上一道的工序對下一道工序有直接影響,是一種直接制約關係。
(二)臨界資源&臨界區
1.臨界資源:在操作系統中,把那些可以被進程共享的資源(文件,打印機等),但是在同一時刻只允許一個進程/線程訪問的資源
統稱爲臨界資源或共享變量。
2.臨界區:訪問臨界資源的那段代碼。
(三)原子操作
原子操作是指不會被進程/線程調度機制打斷的操作,這種操作一旦開始,就一直運行到結束,中間不會有任何進程切換。故原子操作要不然不做,要不然一直做到結束。
(四)PV操作
這兩個字母來源於荷蘭語單詞:passeren傳遞,就好像進入臨界區;vrijgeven釋放,退出臨界區。假設信號量爲SV,那麼P、V操作含義如下:
- P(SV):如果SV的值大於1,進行減一操作,表示獲得資源;如果SV的值爲0,則掛起進程的執行,即阻塞,因爲目前已經沒有資源了。
- V(SV):如果當前有掛起的進程在等待資源,那麼執行V就會將它喚醒;如果沒有,SV加一,即釋放資源。
P操作會出現阻塞,V操作永遠不會阻塞。
二、信號量概念
(一)定義
信號量和前面介紹的IPC(管道,消息隊列)不同,它是一個計數器,用於多線程對共享數據對象的訪問。和前面的有本質的不同,前面學的管道,消息隊列是爲了傳送數據,而信號量是以保護共享資源(臨界資源)或保證進程同步爲目標的,不存儲進程間的通信數據。
故信號量機制一般用在進程或線程之間的同步與互斥。
信號量是一個特殊的計數器,一般取正數值。它的值代表允許訪問的資源數目。一般有兩種常用取值:
- 信號量的值如果只取0,1,將其稱爲
二元信號量
,控制單個資源,初始值爲1。 - 如果信號量的值大於1,則稱之爲
計數信號量
,說明有多個臨界資源單位可共享。
(二)使用
使用信號量獲得共享資源,進程需要執行下列操作:
- 測試控制臨界資源的信號量。
- 若此信號量的值爲正,則進程可以使用該資源,進程P操作將信號量減一,表示它使用了一個資源單位。
- 若此信號量的值爲0,則進程進入休眠狀態,因爲此時沒有資源可以獲取。如果進程被喚醒後,它返回至第(1)步。
- 當進程不再使用由一個信號量控制的臨界資源時,V操作將信號量值加1。如果有進程正在阻塞等待此信號量,則喚醒它。
上述操作均爲原子操作,所以信號量通常在內核中實現。
(三)特點
- 本質是一個計數器,內存中有多少個臨界資源,信號量的數字就是多少。
- 信號量基於操作系統的 PV 操作,程序對信號量的操作都是原子操作
- 信號量用於進程間同步,若要在進程間傳遞數據需要結合共享內存。
- 信號量不能傳遞複雜消息,只能用來同步。
三、信號量函數
內核爲每個信號量集合維護了一個semid_ds結構體,具體成員如下:
strcut semid_ds{
struct ipc_perm sem_perm; //權限結構體
time_t sem_otime; //最後一次操作時間
time_t sem_ctime; //最後一次改變時間
unsigned long sem_nsems; //在集合中信號量
};
我們要操作的信號量就包含在這個信號量集合中,信號量的操作大多數也是通過這個信號量集合來操作的。
(一)創建/獲取信號量集合semget函數
對信號量進行操作,我們先創建或獲得一個信號量ID,那麼調用semget函數,函數原型如下:
#include<sys/sem.h>
int semget(key_t key,int nsems,int flag);
成功返回信號量ID,失敗出錯返回-1
參數:
- key: 有兩種辦法可以獲得,在消息隊列講解中我們已詳細寫出兩種辦法,
兩個進程使用相同key值,就可以使用同一個信號量。
- nsems:如果是創建新信號量集合,那麼nsems代表新信號量集合中的信號量的數目。如果是獲取當前存在的信號量集合,那麼此參數爲0。
- flag: 和消息隊列的設置一樣,標識函數的行爲信號量集合的權限,取值如下:
取值 | 含義 |
---|---|
IPC_CREATE | 創建信號量集合 |
IPC_EXCL | 檢測信號量集合是否存在 |
位或權限位 | 可以設置信號量集合的訪問權限,和其他兩個參數可以或表示,取值和open函數的open_t一樣,一般爲0664 |
如果semget函數用來創建一個新集合,那麼內核會自動把新信號量集合的strut semid_ds結構體做以下初始化:
- 初始化ipc_perm結構體,該結構體中的mode成員按semget函數的flag參數中的相應權限位設置。
- sem_otime設置爲0。
- sem_ctime設置爲當前時間。
- sem_qnsems設置爲semget函數的nsems參數的值。
(二)設置信號量集合semctl函數
semctl函數可以對信號量集合/信號量進行不同的操作,如初始化,刪除等,函數原型爲:
# include<sys/sem.h>
int semctl(int semid,int semnum,int cmd,……/*union semun arg*/);
cmd參數爲SETAVL,IPC_RMID參數,函數返回值成功爲0,失敗返回-1
參數:
-
semid: 信號量集合ID
-
semunm: 用來
指定
該信號量集合中的某一特定信號量成員
,也就是信號量對應的ID。 -
cmd參數: 用來指定對信號量集合的操作,通常用下面兩個值:
(1) SETVAL: 在信號量第一次使用之前,把信號量初始一個已知的值,通過自定義union semun中的val成員設置。
(2)IPC_RMID: 刪除信號量集合,立即發生。 -
arg參數: 此參數可選的,取決於請求的命令,一般初始化時使用,如果使用該參數,必須要自己定義聯合體,表示信號量的相關信息。
union semun{ int val; //信號量的值 struct semid_ds* buf; // ipc_stat,ipc_set的緩衝區 unsigned short* array; //SETALL,GETALL數組 struct seminfo* _buf; //ipc_info緩衝區(Linux專用) };
注意:arg指向的聯合體名稱,成員均不固定,可以自行選擇需要的。一般聯合體成員包含信號量值即可。
(三)操作信號量semop函數
利用semop函數可以改變信號量的值,其函數原型爲:
# include<sys/sem.h>
int semop(int semid,struct sembuf* sops,unsigned nsops);
成功返回0,出錯返回-1
semop函數具有原子性,要不然執行所有操作,要不然一個也不執行。
參數:
-
semid: 信號量集合ID。
-
sops參數: 參數爲一個指針,指向一個由struct sembuf結構表示的信號量操作數組,數組中的每個sembuf結構體對應一個信號量ID,以及對該信號量ID進行操作的標誌:
struct sembuf{ unsigned short sem_num;//信號量ID short sem_op; //信號量操作 short sem_flg;//操作標誌 };
其中結構體中成員的取值有不同的使用:
(1)sem_op:1.如果sem_op爲正值
,如1,表示釋放sem_num對應的信號量的資源(信號量的值增加),如同V操作。
2.如果sem_op爲負值,
如-1,表示獲取sem_num對應的信號量資源(信號量的值減少),如同P操作。若此時已經沒有資源了進行-1操作:- 如果指定了IPC_NOWIT,會出錯返回。
- 未指定,進程會被掛起,知道有資源或捕捉到信號結束掛起。
3.如果sem_op爲0,
表示調用進程希望該信號量值變爲0。如果當前信號量是0,則此函數立即返回。(2)sem_flag:
- 默認填0。
- SEM_UNDO:進程退出後,該進程對sem進行的操作都被撤銷,例如對信號量值進行加1或減1操作,則進程退出後這些操作都被撤銷。
-
nsops參數:
對應參數sops數據的元素個數。
(四)封裝系統調用函數實現一系列功能
在進行進程同步控制時,經常說到P、V操作,那麼如何用上述的系統調用封裝一系列的函數,讓我們使用起來方便。根據需求我們將其分爲4類函數:創建/獲取信號量;P操作;V操作;刪除信號集合。我們將表示信號量信息的union semun聯合體(因爲業務簡單,所以聯合體中只包含信號量的值val即可)和函數聲明寫在自己創建的.h文件中。
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<assert.h>
# include<sys/sem.h>
//保存信號量值的聯合體
union semval
{
int val;
};
//創建/獲得信號量集合
int CreateSem(int key,int init_val[],int len);
//減1操作
void SemP(int semid,int index);
//加1操作
void SemV(int semid,int index);
//刪除信號量集合
void DeleteSem(int semid);
我們獲取key值不通過ftok函數獲取,而是手動強轉。
(key_t)k;//k爲正整數,如1234,強轉爲key值
1. 創建/獲取信號量集合函數: 主要使用系統調用函數semget獲取/創建信號量集合,如果創建信號量集合需要用semctl對信號量集合進行初始化。
我們可以實現函數的流程進行說明:
int CreateSem(int key,int init_val[],int len);
//key爲信號量集合標識符,強轉;init_val[]爲信號量集合,len爲長度,即信號量個數。
- 首先嚐試獲取信號量集合ID: 用semget函數,key強轉爲key_t類型,傳參數nsems爲0,不指定信號量集合方式,只指定獲取權限爲0664。如果返回-1,表示當前沒有信號量集合,否則我們將返回的信號量集合ID返回即可。
- 進行信號量集合創建: 利用semget函數,參數nsems爲len,即信號量個數,指定方式爲IPC_CREATE,打開權限爲0664。
- 進行信號量集合初始化: 循環信號量數組,將信號量的值給semun聯合體中的val,通過函數semctl,用semun聯合體對創建出來的信號量集合進行初始化,cmd參數爲SETVAL。
- 初始化完成,返回信號量集合的標識符。
2. P操作:主要利用semop函數,對參數sops進行設置實現:
void SemP(int semid,int index);
//semid表示信號量集合標識符ID,index表示進行P操作的信號量ID
- 創建sembuf類型的結構體buf,對其參數進行設置,井sem_op設置爲-1,表示執行semop函數就會進行資源獲取,-1操作。
- 調用函數semop對信號量進行-1操作。
3. V操作:主要利用semop函數,對參數sops進行設置實現:
void SemV(int semid,int index);
//semid表示信號量集合標識符ID,index表示進行V操作的信號量ID
- 創建sembuf類型的結構體buf,對其參數進行設置,井sem_op設置爲1,表示執行semop函數就會進行資源釋放,+1操作。
- 調用函數semop對信號量進行+1操作。
4. 刪除整個信號量集:利用函數semctl,對cmd參數設置實現:
void DeleteSem(int semid);
//semid表示信號量集合標識符ID
- 直接調用semctl函數,將semunm參數設置爲0,表示刪除所有信號,cmd參數爲IPC_RMID。
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<assert.h>
# include<sys/sem.h>
# include<string.h>
# include "./sem.h"
//創建/獲得信號量集合
int CreateSem(int key,int init_val[],int len)
{
//
int semid=semget((key_t)key,0,0664);
if(semid!=-1)
{
return semid;
}
//
semid=semget((key_t)key,len,IPC_CREAT|0664);
if(semid==-1)
{
perror("Create Sem Error\n");
return -1;
}
//
int i=0;
for(;i<len;i++)
{
union semval data;
data.val=init_val[i];
if(semctl(semid,i,SETVAL,data)==-1)
{
perror("Init Sem Value Fail\n");
return -1;
}
}
return semid;
}
//減1操作
void SemP(int semid,int index)
{
struct sembuf buf;
buf.sem_num=index;
buf.sem_op=-1;//
buf.sem_flg=SEM_UNDO;
if(semop(semid,&buf,1)==-1)
{
perror("Sem P Error\n");
}
}
//加1操作
void SemV(int semid,int index)
{
struct sembuf buf;
buf.sem_num=index;
buf.sem_op=1;
buf.sem_flg=SEM_UNDO;
if(semop(semid,&buf,1)==-1)
{
perror("Sem V Error\n");
}
}
//刪除信號量集合
void DeleteSem(int semid)
{
if(semctl(semid,0,IPC_RMID)==-1)
{
perror("Delete Error\n");
}
}
四、實例
例題: 進程a和進程b模擬訪問打印機,進程a輸出第一個字符’a’表示開始使用打印機,輸出第二個字符‘a’表示結束打印機;b進程的操作和a進程相同,每個進程循環使用打印機5次。現在打印機爲臨界資源,所以在某一時刻它只能被一個進程使用,所以輸出的結果不應該出現:abab,abba等情況。
1.我們不對這兩個進程採取信號量進行同步控制,
代碼如下:b.c打印b,a.c代碼一樣,只不過打印a。
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<assert.h>
# include<sys/sem.h>
# include<string.h>
# include "./sem.h"
int main()
{
while(1)
{
printf("b\n");
sleep(5);
printf("b\n");
sleep(2);
}
exit(0);
}
輸出了在我們預料之中的混亂數據,這就是沒有加同步控制出現的結構,並不是我們想要的,那麼我們現在進行一個同步控制。
2.採取信號量進行同步控制:
打印機輸入臨界資源,那麼設定一個信號量來管理打印機:
- 它只有一臺,所以初始值爲1,在a進程使用時,我們對其進行一個P操作,即讓a獲取打印機,此時信號量爲0,如果b來訪問就會被掛起。
- 在a使用完打印機後,我們釋放打印機資源,進行V操作,這是信號量爲1,b進程這時就可以訪問打印機,b也是一樣的,訪問前進行一次P操作獲取,打印完畢進行一次V操作釋放。
代碼如下:
a.c:
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<assert.h>
# include<sys/sem.h>
# include<string.h>
# include "./sem.h"
int main()
{
int val=1;//信號量初始值
int semid=CreateSem(1234,&val,1);//a先運行,創建信號集合IDkey爲1234,值爲1,1個信號量的信號集合
assert(semid!=-1);
int count=0;//使用打印機次數
while(1)
{
SemP(semid,0);//P,佔有打印機
printf("a\n");
sleep(5);
printf("a\n");
SemV(semid,0);//V,釋放打印機
sleep(2);
count++;
if(count==5)
{
break;
}
}
exit(0);
}
b.c:
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<assert.h>
# include<sys/sem.h>
# include<string.h>
# include "./sem.h"
int main()
{
int val=1;//信號量初始值
int semid=CreateSem(1234,&val,1);//b後運行,直接獲取到信號量
assert(semid!=-1);
int count=0;//使用打印機次數
while(1)
{
SemP(semid,0);//P,佔有打印機
printf("b\n");
sleep(5);
printf("b\n");
SemV(semid,0);//V,釋放打印機
sleep(2);
count++;
if(count==5)
{
break;
}
}
exit(0);
}
進行編譯時爲:
gcc -o a a.c sem.c //需要加上自己寫的頭文件
我們觀察結果,發現沒有出現abab這種a,b進程混亂使用打印機的現象,都是a/b進程打印完了,釋放資源,a/b進程再進行獲取打印。
加油哦!💪。