多程序程式設計之程序間通訊-共享記憶體,訊號量和套接字
1. 背景
本文將介紹程序通訊中的訊號量,共享記憶體和套接字方法。
2. 訊號量
2.1 訊號量的定義
為了防止出現因多個程式同時訪問一個共享資源而引發的一系列問題,我們需要一種方法,它可以通過生成並使用令牌來授權,在任一時刻只能有一個執行執行緒訪問程式碼的臨界區域。臨界區域是指執行資料更新的程式碼需要獨佔式地執行。而訊號量就可以提供這樣的一種訪問機制,讓一個臨界區同一時間只有一個執行緒在訪問它,也就是說訊號量是用來調協程序對共享資源的訪問的。
訊號量是一個特殊的變數,程式對其訪問都是原子操作,且只允許對它進行等待(即P(訊號變數))和傳送(即V(訊號變數))資訊操作。最簡單的訊號量是隻能取0和1的變數,這也是訊號量最常見的一種形式,叫做二進位制訊號量。而可以取多個正整數的訊號量被稱為通用訊號量。這裡主要討論二進位制訊號量。
由於訊號量只能進行兩種操作等待和傳送訊號,即P(sv)和V(sv),他們的行為是這樣的,假設有一個訊號量變數sv:
P(sv):如果sv的值大於零,就給它減1;如果它的值為0,就掛起該程序的執行。
V(sv):如果有其他程序因等待sv而被掛起,就讓它恢復執行,如果沒有程序因等待sv而掛起,就給它加1。
舉個例子,就是兩個程序共享訊號量sv,一旦其中一個程序執行了P(sv)操作,它將得到訊號量,並可以進入臨界區,使sv減1。而第二個程序將被阻止進入臨界區,因為當它試圖執行P(sv)時,sv為0,它會被掛起以等待第一個程序離開臨界區域並執行V(sv)釋放訊號量,這時第二個程序就可以恢復執行。
1、semget函式
它的作用是建立一個新訊號量或取得一個已有訊號量,原型為:
int semget(key_t key, int num_sems, int sem_flags);
第一個引數key是整數值(唯一非零),不相關的程序可以通過它訪問一個訊號量,它代表程式可能要使用的某個資源,程式對所有訊號量的訪問都是間接的,程式先通過呼叫semget函式並提供一個鍵,再由系統生成一個相應的訊號識別符號(semget函式的返回值),只有semget函式才直接使用訊號量鍵,所有其他的訊號量函式使用由semget函式返回的訊號量識別符號。如果多個程式使用相同的key值,key將負責協調工作。
第二個引數num_sems指定需要的訊號量數目,它的值幾乎總是1。
第三個引數sem_flags是一組標誌,當想要當訊號量不存在時建立一個新的訊號量,可以和值IPC_CREAT做按位或操作。設定了IPC_CREAT標誌後,即使給出的鍵是一個已有訊號量的鍵,也不會產生錯誤。而IPC_CREAT | IPC_EXCL則可以建立一個新的,唯一
semget函式成功返回一個相應訊號識別符號(正數非零),失敗返回-1。
2、semop函式
它的作用是改變訊號量的值,原型為:
int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);
第一個引數,sem_id是由semget返回的訊號量識別符號。第二個引數sem_ops是指向一個sembuf結構陣列的指標,每個陣列元素至少包含以下幾個成員:
struct sembuf{
short sem_num;//該成員是訊號編號,除非使用一組訊號量,否則它為0
short sem_op;//該成員的值是訊號量在一次操作中需要改變的。
//訊號量在一次操作中需要改變的資料,通常是兩個數,一個是-1,即P(等待)操作,它等待訊號量變為可用;一個是+1,即V(傳送訊號)操作,它傳送訊號表示訊號量現在已可用。
short sem_flg;//通常為SEM_UNDO,使作業系統跟蹤當前程序對這個訊號量的修改情況,
//並在程序沒有釋放該訊號量而終止時,作業系統釋放訊號量
};
semop成員的值數值
3、semctl函式
該函式用來直接控制訊號量資訊,它的原型為:
int semctl(int sem_id, int sem_num, int command, ...);
前兩個引數與前面一個函式中的一樣,分別是訊號量識別符號和訊號量編號。command表示將要採取的動作,通常是下面兩個值中的其中一個:
SETVAL:用來把訊號量初始化為一個已知的值。p 這個值通過union semun中的val成員設定,其作用是在訊號量第一次使用前對它進行設定。
IPC_RMID:用於刪除一個已經無需繼續使用的訊號量識別符號。
如果有第四個引數,它通常是一個union semum結構,定義如下:
union semun{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
semctl函式將根據command引數的不同而返回不同的值,對於SETVAL和IPC_RMID如果成功時,返回0,失敗時返回-1。
2.2 訊號量的使用
將兩個不同字元的輸出來表示進入和離開臨界區域,如果程式啟動時帶有一個引數,它將在進入和退出臨界區域時候列印字元X;而程式的其他執行例項將在進入和退出臨界區域的時候列印字元O,因為在任一給定時刻,只能有一個程序可以進入到臨界區域,所以字元X和字元O應該是成對出現的。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/sem.h>
static int set_semvalue(void);
static void del_semvalue(void);
static int semaphore_p(void);
static int semaphore_v(void);
static int sem_id;
union semun
{
int val; /* value for SETVAL */
struct semid_ds *buf; /* buffer for IPC_STAT, IPC_SET */
unsigned short int *array; /* array for GETALL, SETALL */
struct seminfo *__buf; /* buffer for IPC_INFO */
};
int main(int argc, char *argv[])
{
int i;
int pause_time;
char op_char = 'O';
srand((unsigned int)getpid());
sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);
if (argc > 1)
{
if (!set_semvalue())//程式第一次被呼叫,初始化訊號量
{
fprintf(stderr, "Failed to initialize semaphore\n");
exit(EXIT_FAILURE);
}
op_char = 'X';
sleep(2);
}
/* Then we have a loop which enters and leaves the critical section ten times.
There, we first make a call to semaphore_p which sets the semaphore to wait, as
this program is about to enter the critical section. */
for(i = 0; i < 10; i++)
{
//進入臨界區
if (!semaphore_p()) exit(EXIT_FAILURE);
printf("%c", op_char);fflush(stdout); //清理緩衝區,然後休眠隨機時間
pause_time = rand() % 3;
sleep(pause_time);
printf("%c", op_char);fflush(stdout);
//離開臨界區前再一次向螢幕輸出資料
/* After the critical section, we call semaphore_v, setting the semaphore available,
before going through the for loop again after a random wait. After the loop, the call
to del_semvalue is made to clean up the code. */
//離開臨界區,休眠隨機時間後繼續迴圈
if (!semaphore_v()) exit(EXIT_FAILURE);
pause_time = rand() % 2;
sleep(pause_time);
}
printf("\n%d - finished\n", getpid());
if (argc > 1)
{//如果程式是第一次被呼叫,則在退出前刪除訊號量
sleep(10);
del_semvalue();
}
exit(EXIT_SUCCESS);
}
/* The function set_semvalue initializes the semaphore using the SETVAL command in a
semctl call. We need to do this before we can use the semaphore. */
static int set_semvalue(void)
{
//用於初始化訊號量,在使用訊號量前必須這樣做
union semun sem_union;
sem_union.val = 1;
if (semctl(sem_id, 0, SETVAL, sem_union) == -1) return(0);
return(1);
}
/* The del_semvalue function has almost the same form, except the call to semctl uses
the command IPC_RMID to remove the semaphore's ID. */
static void del_semvalue(void)
{
//刪除訊號量
union semun sem_union;
if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
fprintf(stderr, "Failed to delete semaphore\n");
}
/* semaphore_p changes the semaphore by -1 (waiting). */
static int semaphore_p(void)
{
//對訊號量做減1操作,即等待P(sv)
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1; /* P() */
sem_b.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_b, 1) == -1) {
fprintf(stderr, "semaphore_p failed\n");
return(0);
}
return(1);
}
/* semaphore_v is similar except for setting the sem_op part of the sembuf structure to 1,
so that the semaphore becomes available. */
static int semaphore_v(void)
{
//這是一個釋放操作,它使訊號量變為可用,即傳送訊號V(sv)
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1; /* V() */
sem_b.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_b, 1) == -1) {
fprintf(stderr, "semaphore_v failed\n");
return(0);
}
return(1);
}
執行結果如下:
同時執行一個程式的兩個例項,注意第一次執行時,要加上一個字元作為引數,例如本例中的字元 1,它用於區分是否為第一次呼叫。因為每個程式都在其進入臨界區後和離開臨界區前列印一個字元,所以每個字元都應該成對出現,正如你看到的上圖的輸出那樣。在main函式中迴圈中我們可以看到,每次程序要訪問stdout(標準輸出),即要輸出字元時,每次都要檢查訊號量是否可用(即stdout有沒有正在被其他程序使用)。所以,當一個程序A在呼叫函式semaphore_p進入了臨界區,輸出字元後,呼叫sleep時,另一個程序B可能想訪問stdout,但是訊號量的P請求操作失敗,只能掛起自己的執行,當程序A呼叫函式semaphore_v離開了臨界區,程序B馬上被恢復執行。然後程序A和程序B就這樣一直迴圈了10次。
另一個例子來說明一下,它實現的功能與前面的例子一樣,執行方式也一樣,都是兩個相同的程序,同時向stdout中輸出字元,只是沒有使用訊號量,兩個程序在互相競爭stdout。它的程式碼如下:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
char message = 'X';
int i = 0;
if(argc > 1)
message = argv[1][0];
for(i = 0; i < 10; ++i)
{
printf("%c", message);
fflush(stdout);
sleep(rand() % 3);
printf("%c", message);
fflush(stdout);
sleep(rand() % 2);
}
sleep(10);
printf("\n%d - finished\n", getpid());
exit(EXIT_SUCCESS);
}
執行結果如下所示:
從上面的輸出結果,我們可以看到字元‘X’和‘O’並不像前面的例子那樣,總是成對出現,因為當第一個程序A輸出了字元後,呼叫sleep休眠時,另一個程序B立即輸出並休眠,而程序A醒來時,再繼續執行輸出,同樣的程序B也是如此。所以輸出的字元就是不成對的出現。這兩個程序在競爭stdout這一共同的資源。
2.3 訊號量小結:
訊號量是一個特殊的變數,程式對其訪問都是原子操作,且只允許對它進行等待(即P(訊號變數))和傳送(即V(訊號變數))資訊操作。我們通常通過訊號來解決多個程序對同一資源的訪問競爭的問題,使在任一時刻只能有一個執行執行緒訪問程式碼的臨界區域,也可以說它是協調程序間的對同一資源的訪問權,也就是用於同步程序的。
3. 共享記憶體
共享記憶體是最快的IPC(程序間通訊)方式,它允許兩個不相關程序訪問同一個邏輯記憶體。共享記憶體是一個程式向記憶體寫資料,另一個程式讀資料,共享記憶體牽扯到同步的問題,一般有三種方案可以實現共享資源的同步。它們分別是訊號量,記錄鎖和互斥量。
使用訊號量,首先服務端建立一個只含一個訊號的訊號量集合,並初始化為1。佔據資源,則以sem_op=-1呼叫semop函式。釋放資源,則則以sem_op=1呼叫semop函式。
使用記錄鎖,需要建立一個檔案,並寫入一個位元組。分配資源,對檔案獲得寫鎖,釋放資源,解鎖。
使用互斥量,需要所有的程序將相同的檔案對映到他們的地址空間,並使用PTHREAD_PROCESS_SHARED互斥量屬性在檔案中初始化互斥量。分配資源,對互斥量加鎖,釋放資源,解鎖互斥量。
共享記憶體使用的函式類似訊號量的函式:
#include <sys/shm.h>
int shmget(key_t key, int size, int flag);
void* shmat(int shmid, const void *addr, int flag);
int shmdt(char *shmaddr);
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
3-1 shmget函式:
用於開闢或指向一塊共享記憶體,返回獲得共享記憶體區域的ID(共享記憶體識別符號),該識別符號用於後續的共享記憶體函式。如果不存在指定的共享區域就建立相應的區域。 如果建立失敗,則返回-1。
keyt key: 共享記憶體的識別符號。如果是父子關係的程序間通訊的話,這個識別符號用IPC_PRIVATE來代替。
如果兩個程序沒有任何關係,所以就用ftok()算出來一個識別符號(或者自己定義一個)使用了。
int size: 以位元組為單位指定需要共享的記憶體容量。
int flag: 包含9個位元的許可權標誌,它是這塊記憶體的模式(mode)以及許可權標識。由IPC_CREAT定義的一個特殊位元必須和許可權標誌按位或才能建立一個新的共享記憶體段。設定IPC_CREAT標誌的同時,給shmget函式傳遞一個已有共享記憶體段的鍵並不是一個錯誤,如果無需用到IPC_CREAT標誌,該標誌會被悄悄地忽略掉。
許可權標誌的存在,使得允許一個程序建立的共享記憶體可以被共享記憶體的建立者所擁有的程序寫入,同時其他使用者建立的程序只能讀取該共享記憶體。可以利用這個功能提高一種有效的對資料進行只讀訪問的方法,通過將資料放入共享記憶體並設定它的許可權,即可避免資料被其他使用者修改。
模式可取如下值:
IPC_CREAT 新建(如果已建立則返回目前共享記憶體的id)
IPC_EXCL 與 IPC_CREAT結合使用,如果已建立則返回錯誤
將“模式” 和“許可權標識”進行或運算,做為第三個引數。如:IPC_CREAT | IPC_EXCL | 0640
其中0640為許可權標識,4/2/1 分別表示讀/寫/執行3種許可權,第一個0是UID,第一個6(4+2)表示擁有者的許可權,第二個4表示同組許可權,第3個0表示他人的許可權。
函式呼叫成功時返回共享記憶體的ID,失敗時返回-1。
注:建立共享記憶體時,shmflg引數至少需要 IPC_CREAT | 許可權標識,如果只有IPC_CREAT 則申請的地址都是k=0xffffffff,不能使用;
3-2 shmat函式:
用來允許本程序訪問一塊共享記憶體的函式。
第一次建立共享記憶體時,它不能任何程序訪問,要想啟用對該共享記憶體的訪問,必須將其連線到一個程序的地址空間中。
shmat函式就是用來完成此工作的。
int shmid : 共享記憶體的ID,即共享記憶體的標識。
char *shmaddr: 共享記憶體連線到程序中的起始地址,如果shmaddr為NULL,核心會把共享記憶體對映到系統選定的地址空間中;如果shmaddr不為NULL,核心會把共享記憶體對映到shmaddr指定的位置。
注:一般情況下我們很少需要控制共享記憶體連線的地址,通常都是讓系統來選擇一個地址,否則就會使應用程式對硬體的依賴性過高。所以一般把shmaddr設為NULL。
int shmflag : 本程序對該記憶體的操作模式,可以由兩個取值:SHM_RND和SHM_RDONLY。SHM_RND為讀寫模式,
SHM_RDONLY是隻讀模式。需要注意的是,共享記憶體的讀寫許可權由它的屬主、它的訪問許可權和當前程序的屬主共同決定。如果當shmflg & SM_RDONLY為true時,即使該共享記憶體的訪問許可權允許寫操作,它也不能被寫入。該引數通常會被設為0。
函式呼叫成功時,返回共享記憶體的起始地址,失敗時返回-1。
3-3 shmdt函式:
用於函式刪除本程序對這塊記憶體的使用。
shmdt()與shmat()相反,是用來禁止本程序訪問一塊共享記憶體的函式。
char *shmaddr 是那塊共享記憶體的起始地址。
函式呼叫成功時返回0,失敗時返回-1。
3-4shmctl函式:
控制對這塊共享記憶體的使用。
int shmid: 共享記憶體的ID,即共享記憶體標識。
int cmd : 控制命令,表示要採取的動作,可取值如下:
IPC_STAT 得到共享記憶體的狀態:把shmid_ds結構中的資料設定為共享記憶體的當前關聯值
IPC_SET 改變共享記憶體的狀態:把共享記憶體的當前關聯值設定為shmid_ds結構中給出的值
IPC_RMID 刪除共享記憶體段
shmid_ds結構至少包含以下成員:
struct shmid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
uid_t shm_perm.mode;
}
struct shmid_ds *buf: 一個結構體指標。IPC_STAT的時候,取得的狀態放在這個結構體中。
如果要改變共享記憶體的狀態,用這個結構體指定。
函式呼叫成功時返回0,失敗時返回-1。
【未完待續!】
4. 套介面
【待補充】