天天看點

unix/linux共享記憶體應用與陷阱

共享記憶體是系統出于多個程序之間通訊的考慮,而預留的的一塊記憶體區。在/proc/sys/kernel/目錄下,記錄着共享記憶體的一些限制,如一個共享記憶體區的最大位元組數shmmax,系統範圍内最大共享記憶體區辨別符數shmmni等,可以手工對其調整,但不推薦這樣做。

一、應用

共享記憶體的使用,主要有以下幾個api:ftok()、shmget()、shmat()、shmdt()及shmctl()。

1)用ftok()函數獲得一個id号.

應用說明:

在ipc中,我們經常用用key_t的值來建立或者打開信号量,共享記憶體和消息隊列。

函數原型:

key_t ftok(const char *pathname, int proj_id);

keys:

1)pathname一定要在系統中存在并且程序能夠通路的

3)proj_id是一個1-255之間的一個整數值,典型的值是一個ascii值。

當成功執行的時候,一個key_t值将會被傳回,否則-1被傳回。我們可以使用strerror(errno)來确定具體的錯誤資訊。

考慮到應用系統可能在不同的主機上應用,可以直接定義一個key,而不用ftok獲得:

#define ipckey 0x344378

2)shmget()用來開辟/指向一塊共享記憶體的函數

shmget()用來獲得共享記憶體區域的id,如果不存在指定的共享區域就建立相應的區域。

int shmget(key_t key, size_t size, int shmflg);

key_t key 是這塊共享記憶體的辨別符。如果是父子關系的程序間通信的話,這個辨別符用ipc_private來代替。如果兩個程序沒有任何關系,是以就用ftok()算出來一個辨別符(或者自己定義一個)使用了。

int size 是這塊記憶體的大小.

int flag 是這塊記憶體的模式(mode)以及權限辨別。

模式可取如下值:

ipc_creat 建立(如果已建立則傳回目前共享記憶體的id)

ipc_excl 與ipc_creat結合使用,如果已建立則則傳回錯誤

然後将“模式” 和“權限辨別”進行“或”運算,做為第三個參數。

如: ipc_creat | ipc_excl | 0640

例子中的0666為權限辨別,4/2/1 分别表示讀/寫/執行3種權限,第一個0是uid,第一個6(4+2)表示擁有者的權限,第二個4表示同組權限,第3個0表示他人的權限。

這個函數成功時傳回共享記憶體的id,失敗時傳回-1。

關于這個函數,要多說兩句。

建立共享記憶體時,shmflg參數至少需要 ipc_creat | 權限辨別,如果隻有ipc_creat 則申請的位址都是k=0xffffffff,不能使用;

擷取已建立的共享記憶體時,shmflg不要用ipc_creat(隻能用建立共享記憶體時的權限辨別,如0640),否則在某些情況下,比如用ipcrm删除共享記憶體後,用該函數并用ipc_creat參數擷取一次共享記憶體(當然,擷取失敗),則即使再次建立共享記憶體也不能成功,此時必須更改key來重建共享記憶體。

3) shmat()将這個記憶體區映射到本程序的虛拟位址空間。

void *shmat( int shmid , char *shmaddr , int shmflag );

shmat()是用來允許本程序通路一塊共享記憶體的函數。

int shmid是那塊共享記憶體的id。

char *shmaddr是共享記憶體的起始位址,如果shmaddr為0,核心會把共享記憶體映像到調用程序的位址空間中標明位置;如果shmaddr不為0,核心會把共享記憶體映像到shmaddr指定的位置。是以一般把shmaddr設為0。

int shmflag是本程序對該記憶體的操作模式。如果是shm_rdonly的話,就是隻讀模式。其它的是讀寫模式

成功時,這個函數傳回共享記憶體的起始位址。失敗時傳回-1。

4) shmdt()函數删除本程序對這塊記憶體的使用,shmdt()與shmat()相反,是用來禁止本程序通路一塊共享記憶體的函數。

int shmdt( char *shmaddr );

參數char *shmaddr是那塊共享記憶體的起始位址。

成功時傳回0。失敗時傳回-1。

5) shmctl() 控制對這塊共享記憶體的使用

函數原型:

int shmctl( int shmid , int cmd , struct shmid_ds *buf );

int shmid是共享記憶體的id。

int cmd是控制指令,可取值如下:

ipc_stat 得到共享記憶體的狀态

ipc_set 改變共享記憶體的狀态

ipc_rmid 删除共享記憶體

struct shmid_ds *buf是一個結構體指針。ipc_stat的時候,取得的狀态放在這個結構體中。如果要改變共享記憶體的狀态,用這個結構體指定。

傳回值: 成功:0

失敗:-1

示例程式:

#include <sys/ipc.h>

#include <sys/shm.h>

#include <string.h>

#include <stdio.h>

#include <stdlib.h>

#define ipckey 0x366378

typedef struct{

char agen[10];

unsigned char file_no;

} st_setting;

int main(int argc, char** argv)

{

int shm_id;

key_t key;

st_setting *p_setting;

//首先檢查共享記憶體是否存在,存在則先删除

shm_id = shmget(ipckey ,1028,0640);

if(shm_id != -1)

p_setting = (st_setting*)shmat(shm_id,null,0);

if ( p_setting != (void *)-1)

shmdt(p_setting);

shmctl(shm_id,ipc_rmid,0) ;

}

shm_id=shmget(ipckey,1028,0640|ipc_creat|ipc_excl);

if(shm_id==-1)

printf("shmget error\n");

return -1;

//将這塊共享記憶體區附加到自己的記憶體段

p_setting=(st_setting*)shmat(shm_id,null,0);

strncpy(p_setting->agen,"jinyh",10);

printf( "agen:%s\n",p_setting->agen );

p_setting->file_no = 1;

printf( "file_no:%d\n",p_setting->file_no );

system("ipcs -m");//此時可看到有程序關聯到共享記憶體的資訊,nattch為1

//将這塊共享記憶體區從自己的記憶體段删除出去

if(shmdt(p_setting) == -1)

perror(" detach error ");

system("ipcs -m");//此時可看到有程序關聯到共享記憶體的資訊,nattch為0

//删除共享記憶體

if (shmctl( shm_id , ipc_rmid , null ) == -1)

perror(" delete error ");

//exit(0);

注意:在使用共享記憶體,結束程式退出後。如果你沒在程式中用shmctl()删除共享記憶體的話,一定要在指令行下用ipcrm指令删除這塊共享記憶體。你要是不管的話,它就一直在那兒放着了。

簡單解釋一下ipcs指令和ipcrm指令。

取得ipc資訊:

ipcs [-m|-q|-s]

-m 輸出有關共享記憶體(shared memory)的資訊

-q 輸出有關資訊隊列(message queue)的資訊

-s 輸出有關“遮斷器”(semaphore)的資訊

%ipcs -m

删除ipc

ipcrm -m|-q|-s shm_id

%ipcrm -m 105

1)ftok陷阱

采用ftok來生成key的情況下,如果ftok的參數pathname指定檔案被删除後重建,則檔案系統會賦予這個同名檔案(或目錄)新的i節點資訊,于是這些程序所調用的ftok雖然都能正常傳回,但得到的鍵值卻并不能保證相同。

2)3. aix中shmat的問題

aix系統中,system v各類程序間通信機制在使用中均存在限制。差別于其它unix作業系統對ipc機制的資源配置方式,aix使用了不同的方法;在aix中定義了 ipc 機制的上限, 且是不可配置的。就共享記憶體機制而言,在4.2.1及以上版本的aix系統上,存在下列限制:

對于64位程序,同一程序可連接配接最多268435456個共享記憶體段;

對于32位程序,同一程序可連接配接最多11個共享記憶體段,除非使用擴充的shmat;

上述限制對于64位應用不會帶來麻煩,因為可供連接配接的數量已經足夠大了;但對于32位應用,卻很容易帶來意外的問題,因為最大的連接配接數量隻有11個。

下面的例程test02.c示範了這個問題,為了精簡代碼,它反複連接配接的是同一個共享記憶體對象;實際上,無論所連接配接的共享記憶體對象是否相同,該限制制約的是連接配接次數:

#include <errno.h>

#include <sys/types.h>

#define max_attach_num 15

void main(int argc, char* argv[])

key_t mem_key;

long mem_id;

void* mem_addr[max_attach_num];

int i;

if ( ( mem_key = ftok("/tmp/mykeyfile", 1) ) == (key_t)(-1) ) {

printf("failed to generate shared memory access key, errno=%d\n",

errno);

goto mod_exit;

if ( ( mem_id = shmget(mem_key, 256, ipc_creat) ) == (-1) ) {

printf("failed to obtain shared memory id, errno=%d\n", errno);

for ( i=1; i<=max_attach_num; i++ ) {

if ( ( mem_addr[i] = (void *)shmat(mem_id, 0, 0) ) == (void *)(-1) )

printf("failed to attach shared memory, times [%02d], errno:%d\n", i,

else

printf("successfully attached shared memory, times [%02d]\n", i);

mod_exit:

shmctl(mem_id, ipc_rmid, null);

在aix系統上,我們将其編譯為test02,并運作,可以看到如下輸出:

successfully attached shared memory, times [01]

successfully attached shared memory, times [02]

successfully attached shared memory, times [03]

successfully attached shared memory, times [04]

successfully attached shared memory, times [05]

successfully attached shared memory, times [06]

successfully attached shared memory, times [07]

successfully attached shared memory, times [08]

successfully attached shared memory, times [09]

successfully attached shared memory, times [10]

successfully attached shared memory, times [11]

failed to attach shared memory, times [12], errno:24

failed to attach shared memory, times [13], errno:24

failed to attach shared memory, times [14], errno:24

failed to attach shared memory, times [15], errno:24

說明超出11個連接配接之後,所有後續的共享記憶體連接配接都将無法建立。錯誤碼24的定義是emfile,aix給予的解釋是:

the number of shared memory segments attached to the calling process exceeds the system-imposed limit。

解決這個問題的方法是,使用擴充的shmat;具體而言就是,在運作相關應用之前(确切地說,是在共享記憶體被建立之前),首先在shell中設定extshm環境變量,通過它擴充shmat,對于源代碼本身無需作任何修改:

export extshm=on

值得注意的是,雖然設定環境變量,在程式中也可通過setenv函數來做到,比如在程式的開始,加入下列代碼:

setenv("extshm", "on", 1);

但實踐證明這樣的方法在解決這個問題上是無效的;也就是說唯一可行的辦法,就是在shell中設定extshm環境變量,而非在程式中。

在aix上配置32位db2執行個體時,也要求確定将環境變量 extshm 設為 on,這是運作 warehouse manager 和 query patroller 之前必需的操作:

db2set db2envlist=extshm

db2start

其原因即來自我們剛剛介紹的aix中32位應用連接配接共享記憶體時,存在最大連接配接數限制。這個問題同樣普遍存在于aix平台上oracle等軟體産品中。

3)hp-ux中shmget和shmat的問題

3.1 32位和64位應用相容問題

在hp-ux平台上,如果同時運作32位應用和64位應用,而且它們通路的是一個相同的共享記憶體區,則會遇到相容性問題。

在hp-ux中,應用程式設定ipc_creat标志調用shmget,所建立的共享記憶體區,隻可被同類型的應用所通路;即32位應用程式所建立的共享記憶體區隻可被其它的32位應用程式通路,同樣地,64位應用程式所建立的共享記憶體區隻可被其它的64位應用程式通路。

如果,32位應用企圖通路一個由64位應用建立的共享記憶體區,則會在調用shmget時失敗,得到einval錯誤碼,其解釋是:

a shared memory identifier exists for key but is in 64-bit address space and the process performing the request has been compiled as a 32-bit executable.

解決這一問題的方法是,當64位應用建立共享記憶體時,合并ipc_creat标志,同時給定ipc_share32标志:

shmget(mem_key, size, 0666 | ipc_creat | ipc_share32)

對于32位應用,沒有設定ipc_share32标志的要求,但設定該标志并不會帶來任何問題,也就是說無論應用程式将被編譯為32位還是64位模式,都可采用如上相同的代碼;并且由此解決32位應用和64位應用在共享記憶體通路上的相容性問題。

3.2 對同一共享記憶體的連接配接數限制

在hp-ux上,應用程序對同一個共享記憶體區的連接配接次數被限制為最多1次;差別于上面第3節所介紹的aix上的連接配接數限制,hp-ux并未對指向不同共享記憶體區的連接配接數設定上限,也就是說,運作在hp-ux上的應用程序可以同時連接配接很多個不同的共享記憶體區,但對于同一個共享記憶體區,最多隻允許連接配接1次;否則,shmat調用将失敗,傳回錯誤碼einval,在shmat的man幫助中,對該錯誤碼有下列解釋:

shmid is not a valid shared memory identifier, (possibly because the shared memory segment was already removed using shmctl(2) with ipc_rmid), or the calling process is already attached to shmid.

這個限制會對多線程應用帶來無法避免的問題,隻要一個應用程序中有超過1個以上的線程企圖連接配接同一個共享記憶體區,則都将以失敗而告終。

解決這個問題,需要修改應用程式設計,使應用程序具備對同一共享記憶體的多線程通路能力。相對于前述問題的解決方法,解決這個問題的方法要複雜一些。

作為可供參考的方法之一,以下介紹的邏輯可以很好地解決這個問題:

基本思路是,對于每一個共享記憶體區,應用程序首次連接配接上之後,将其鍵值(ftok的傳回值)、系統辨別符(shmid,shmget調用的傳回值)和通路位址(即shmat調用的傳回值)儲存下來,以這個程序的全局數組或者連結清單的形式留下記錄。在任何對共享記憶體的連接配接操作之前,程式都将先行檢索這個記錄清單,根據鍵值和标志符去比對希望通路的共享記憶體,如果找到比對記錄,則從記錄中直接讀取通路位址,而無需再次調用shmat函數,進而解決這一問題;如果沒有找到比對目标,則調用shmat建立連接配接,并且為新連接配接上來的共享記憶體添加一個新記錄。

記錄條目的資料結構,可定義為如下形式:

typedef struct _shared_memory_record

key_t mem_key; // key generated by ftok()

int mem_id; // id returned by shmget()

void* mem_addr; // access address returned by shmat()

int nattach; // times of attachment

} shared_

4)solaris中的shmdt函數原型問題

solaris系統中的shmdt調用,在原型上與system v标準有所不同,

default

int shmdt(char *shmaddr);

即形參shmaddr的資料類型在solaris上是char *,而system v定義的是void * 類型;實際上solaris上shmdt調用遵循的函數原型規範是svid-v4之前的标準;以linux系統為例,libc4和libc5 采用的是char * 類型的形參,而遵循svid-v4及後續标準的glibc2及其更新版本,均改為采用void * 類型的形參。

如果仍在代碼中采用system v的标準原型,就會在solaris上編譯代碼時造成編譯錯誤;比如:

error: formal argument 1 of type char* in call to shmdt(char*)

is being passed void*.

解決方法是,引入一個條件編譯宏,在編譯平台是solaris時,采用char * 類型的形參,而對其它平台,均仍采用system v标準的void * 類型形參,比如:

#ifdef _solaris_shared_memory

shmdt((char *)mem_addr);

#else

shmdt((void *)mem_addr);

#endif

5)通過shmctl删除共享記憶體的風險

如果共享記憶體已經與所有通路它的程序斷開了連接配接,則調用ipc_rmid子指令後,系統将立即删除共享記憶體的辨別符,并删除該共享記憶體區,以及所有相關的資料結構;

如果仍有别的程序與該共享記憶體保持連接配接,則調用ipc_rmid子指令後,該共享記憶體并不會被立即從系統中删除,而是被設定為ipc_private狀态,并被标記為"已被删除";直到已有連接配接全部斷開,該共享記憶體才會最終從系統中消失。

需要說明的是:一旦通過shmctl對共享記憶體進行了删除操作,則該共享記憶體将不能再接受任何新的連接配接,即使它依然存在于系統中!是以,可以确知,在對共享記憶體删除之後不可能再有新的連接配接,則執行删除操作是安全的;否則,在删除操作之後如仍有新的連接配接發生,則這些連接配接都将失敗!

繼續閱讀