共用記憶體
共用記憶體是第二種IPC工具。他允許兩個無關的進程訪問相同的邏輯記憶體。共用記憶體是在兩個啟動並執行程式之間傳遞資料的有效手段。儘管X/Open標準並沒有要求,很可能絕大數的共用記憶體實現都是會將不同進程之間正在共用的記憶體安排在相同的實體記憶體中。
共用記憶體為在多個進程之間共用與傳遞資料提供一個有效手段。因為他並沒有提供同步的方法,所以通常我們需要使用其他的機制來同步對共用記憶體的訪問。通常,我們也許會使用共用記憶體來提供對大塊記憶體區的有效訪問,並且傳遞少量的訊息來同步對此記憶體的訪問。
共用記憶體是由IPC為一個進程所建立並且出現在這個進程的地址空間中的一段特殊的地址序列。其他的進程可以將同樣的共用記憶體段關聯到他們自己的地址空間中。所有的進程都可以訪問這段記憶體位址,就如同這段記憶體是由malloc所分配的。如果一個進程寫入共用記憶體,這些改變立即就可以為訪問相同共用記憶體的其他進程所見。
就其自身而言,共用記憶體並沒有提供任何共用方法。並沒有自動的方法來阻止在第一個進程完成寫入共用記憶體之前第二個進程開始讀取共用記憶體。同步訪問是程式員的責任。圖14-2顯示共用記憶體是如何工作的。
每一個箭頭顯示的是每一個進程的邏輯地址空間到可用的實體記憶體的映射。實際的情形要更為複雜,因為可用的記憶體是由實體記憶體與交換到磁碟上的記憶體混合構成的。
用於共用記憶體的函數如下:
#include <sys/shm.h>
void *shmat(int shm_id, const void *shm_addr, int shmflg);
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
int shmdt(const void *shm_addr);
int shmget(key_t key, size_t size, int shmflg);
與訊號量相類似,通常需要在包含shm.h檔案之前包含sys/types.h與sys/ipc.h這兩個標頭檔。
shmget
我們使用shmget函數建立共用記憶體:
int shmget(key_t key, size_t size, int shmflg);
與訊號量相類似,這個函數也提供了key,這可以有效命名共用記憶體段,而且shmget函數會返回一個共用記憶體標識符,這個標識符可以用於後續的共用記憶體函數中。還有一個特殊的關索引值,IPC_PRIVATE,這可以建立進程私人的共用記憶體。我們通常並不會使用這個值,而且與訊號量相類似,我們會發現私人的共用記憶體在許多Linux系統上實際上並不是私人的。
第二個參數,size,以位元組形式指定了所需要的記憶體數量。
第三個參數,shmflg,是由9個許可權標記所組成的,這些標記的使用與用於建立檔案的模型參數相同。IPC_CREAT定義了一個特殊位,必須與許可權標記進行位或操作來建立一個新的共用記憶體段。設定IPC_CREAT標記並且傳遞一個已經存在的共用記憶體段並不是錯誤。如果不需要,IPC_CREAT只是簡單的被忽略掉。
許可權標記是十分有用的,因為這些許可權標記可以允許建立共用記憶體所有者進程可以寫入而其他使用者所建立的進程只能讀取的共用記憶體。我們可以應用這個特點通過將資料放入共用記憶體中,從而提供對於唯讀資料的有效訪問,而不必擔心資料被其他使用者修改的風險。
如果共用記憶體成功建立,shmget會返回一個非負整數,共用記憶體標識符。如果失敗,則會返回-1。
shmat
當我們第一次建立一個共用記憶體段時,他並不能為任何進程所訪問。為了能夠訪問共用記憶體,我們必須將其與一個進程地址空間關聯到一起。我們可以使用shmat函數來達到這一目的:
void *shmat(int shm_id, const void *shm_addr, int shmflg);
第一個參數,shm_id,是由shmget函數所返回的共用記憶體標識符。
第二個參數,shm_addr,是將要關聯到當前進程的共用記憶體所在的位置。這個參數應總是一個null 指標,從而可以允許系統來選擇記憶體出現的地址。
第三個參數,shmflg,是一個位標記集合。兩個可能的值為SHM_RND與SHM_RDONLY。前者與shm_addr聯合,控制將被關聯的共用記憶體所在的地址;而後者使得關聯的記憶體唯讀。通常很少需要來控制被關聯的記憶體所在的地址;我們通常應允許系統來為我們選擇一個地址,否則就會使得程式變得高度硬體相關。
如果shmat調用成功,他會返回一個指向共用記憶體第一位元組的指標。如果失敗,則會返回-1。
共用記憶體將會依據所有者(共用記憶體的建立者),許可權與當前進程的所有者而具有讀或寫入權限。共用記憶體上的許可權與檔案上的許可權相類似。
shmfgl & SHM_RDONLY為真的情況是這個規則的一個例外。此時共用記憶體並不可寫,儘管許可權已經允許了寫訪問。
shmdt
shmdt函數將共用記憶體與當前進程相分離。他傳遞一個指向由shmat所返回的地址的指標。如果成功,則會返回0;如果失敗,則會返回-1。注意,分離共用記憶體並不會刪除他;他只是使得記憶體對於當前進程不可用。
shmctl
共用記憶體的控制函數要比複雜的訊號量控制函數簡單得多:
int shmctl(int shm_id, int command, struct shmid_ds *buf);
shmid_ds結構至少具有下列成員:
struct shmid_ds {
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
}
第一個參數,shm_id,是由shmget所返回的標記符。
第二個參數,command,是要執行的動作。他可以有三個值:
命令 描述
IPC_STAT 設定shmid_ds結構中的資料反射與共用記憶體相關聯的值。
IPC_SET 如果進程有相應的許可權,將與共用記憶體相關聯的值設定為shmid_ds資料結構中所提供的值。
IPC_RMID 刪除共用記憶體段。
第三個參數,buf,是一個指向包含共用記憶體模式與許可權的結構的指標。
如果成功,則返回0,如果失敗,則會返回-1。X/Open並沒有說明如果我們嘗試刪除一個已經關聯的共用記憶體時會發生什麼。通常,一個已經關聯但是被刪除的共用記憶體通常會繼續發揮作用,直到他與最後一個進程相分離。然而,因為這個行為並沒有被規範,所以最好不要依賴於他。
實驗--共用記憶體
現在我們已經瞭解了共用記憶體函數,我們可以編寫一些代碼來使用這些函數。我們將會編寫一對程式,shm1.c與shm2.c。第一個程式(消費者)將會建立一個共用記憶體段並且顯示寫入共用記憶體中的資料。第二個程式(生產者)將會關聯已經存在的共用記憶體段並且允許我們進入記憶體段中的資料。
1 首先,我們建立一個通用標頭檔來描述我們希望傳遞的共用記憶體。我們將其命名為shm_com.h。
#ifndef _SHM_COM_H
#define _SHM_COM_H 1
#define TEXT_SZ 2048
struct shared_use_at
{
int written_by_you;
char some_text[TEXT_SZ];
};
#endif
這個檔案定義了在消費者程式與生產者程式中都會用到的結構。當資料已經寫入結構的其他部分並且認為我們需要傳送2k文本時,我們使用一個int標記written_by_you來通知消費者。
2 我們的第一個程式用於消費者。在包含標頭檔之後,我們通過調用shmget函數,指定IPC_CREAT位來建立一個共用記憶體段(我們共用記憶體結構的大小):
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/irc.h>
#include <sys/shm.h>
#include "shm_com.h"
int main()
{
int running = 1;
void *shared_memory = (void *)0;
struct shared_use_st *shared_stuff;
int shmid;
srand((unsigned int) getpid());
shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);
if(shmid == -1)
{
fprintf(stderr, "shmget failed/n");
exit(EXIT_FAILURE);
}
3 我們現在使用共用記憶體可以為程式所訪問:
shared_memory = shmat(shmid, (void *)0, 0);
if(shared_memory == (void *)-1)
{
fprintf(stderr, "shmat failed/n");
exit(EXIT_FAILURE);
}
printf("Memory attached at %X/n", (int)shared_memroy);
4 程式的接下來部分將shared_memroy段賦給shared_stuff,後者會輸出written_by_you中的任何文本。程式繼續迴圈直到written_by_you中的文本為end。sleep調用會強制消費者停留在其臨界區中,這會使得生產者程式等待。
shared_stuff = (struct_shared_use_st *)shared_memory;
shared_stuff->written_by_you = 0;
while(running)
{
if(shared_stuff->written_by_you)
{
printf("You wrote: %s", shared_stuff->some_text);
sleep(rand() % 4);
shared_stuff->written_by_you = 0;
if(strncmp(shared_stuff->some_text, "end", 3)==0)
{
running = 0;
}
}
}
5 最後共用記憶體被分離並被刪除:
if(shmdt(shared_memory)==-1)
{
fprintf(stderr, "shmdt failed/n");
exit(EXIT_FAILURE);
}
if(shmctl(shmid, IPC_RMID, 0)==-1)
{
fprintf(stderr, "shmctl(IPC_RMID) failed/n");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
6 我們的第二個程式,shm2.c,是生產者程式;他允許我們進入消費者的資料。這個程式與shm1.c程式十分相似:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "shm_com.h"
int main()
{
int runnint = 1;
void *shared_memory = (void *)0;
struct shared_use_st *shared_stuff;
char buffer[BUFSIZ];
int shmid;
shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666 | IPC_CREAT);
if(shmid == -1)
{
fprintf(stderr, "shmget failed/n");
exit(EXIT_FAILURE);
}
shared_memory = shmat(shmid, (void *)0, 0);
if(shared_memory == (void *)-1)
{
fprintf(stderr, "shmat failed/n");
exit(EXIT_FAILURE);
}
printf("Memory attached at %X/n", (int)shared_memory);
shared_stuff = (struct shared_use_st *)shared_memory;
while(running)
{
while(shared_stuff->written_by_you == 1)
{
sleep(1);
printf("waiting for client.../n");
}
printf("Enter some text: ");
fgets(buffer, BUFSIZ, stdin);
strncpy(shared_stuff->some_text, buffer, TEXT_SZ);
shared_stuff->written_by_you = 1;
if(strncmp(buffer, "end", 3) == 0)
{
running = 0;
}
}
if(shmdt(shared_memory) == -1)
{
fprintf(stderr, "shmdt failed/n");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
當我們運行這些程式,我們會得到下面的輸出:
$ ./shm1 &
[1] 294
Memory attached at 40017000
$ ./shm2
Memory attached at 40017000
Enter some text: hello
You wrote: hello
waiting for client...
waiting for client...
Enter some text: Linux!
You wrote: Linux!
waiting for client...
waiting for client...
waiting for client...
Enter some text: end
You wrote: end
$
工作原理
第一個程式,shm1,建立共用記憶體段並其關聯到他的地址空間。我們在共用記憶體的第一部分揭示了shared_use_st結構。這個結構有一個標記,written_by_you,當資料可用時會設定這個標記。當設定了這個標記時,程式會讀取文本,輸出文本,並且清除標記來表示程式已經讀取資料了。我們使用一個特殊的字串,end,來進行由迴圈中的退出。程式然後分離共用記憶體並且刪除他。
第二個程式,shm2,獲得並關聯共用記憶體段,因為他使用相同的索引值,1234。然後他提示使用者輸入一些文本。如果設定了written_by_you標記,shm2就會知道用戶端程式還沒有讀取前面輸入的資料並且進行等待。當其他進程清除了這個標記,shm2會寫入新的資料並且設定標記。他也使用字串end來結束並分離共用記憶體段。
注意,我們必須提供我們自己的,相當粗糙的同步標記,written_by_you,這會導致一個低效的忙等待。在實際的程式中,我們會傳遞一個訊息,或者使用管道,或者使用IPC訊息(我們會在稍後討論),產生資訊,或是使用訊號量來在程式的讀取與寫入部分提供同步。