閱讀945 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Linux 共享內存 詳解


一、什麼是共享內存區

共享內存區是最快的可用IPC形式。它允許多個不相關的進程去訪問同一部分邏輯內存。如果需要在兩個運行中的進程之間傳輸數據,共享內存將是一種效率極高的解決方案。一旦這樣的內存區映射到共享它的進程的地址空間,這些進程間數據的傳輸就不再涉及內核。這樣就可以減少係統調用時間,提高程序效率。

共享內存是由IPC為一個進程創建的一個特殊的地址範圍,它將出現在進程的地址空間中。其他進程可以把同一段共享內存段“連接到”它們自己的地址空間裏去。所有進程都可以訪問共享內存中的地址。如果一個進程向這段共享內存寫了數據,所做的改動會立刻被有訪問同一段共享內存的其他進程看到。

要注意的是共享內存本身沒有提供任何同步功能。也就是說,在第一個進程結束對共享內存的寫操作之前,並沒有什麼自動功能能夠預防第二個進程開始對它進行讀操作。共享內存的訪問同步問題必須由程序員負責。可選的同步方式有互斥鎖、條件變量、讀寫鎖、紀錄鎖、信號燈。

           在將共享內存前我們要先來介紹下麵幾個函數。

二、mmap

mmap函數把一個文件或一個Posix共享內存區對象映射到調用進程的地址空間。使用該函數有三個目的:

            1.使用普通文件以提供內存映射I/O

            2.使用特殊文件以提供匿名內存映射。

            3.使用shm_open以提供無親緣關係進程間的Posix共享內存區。

名稱::

mmap

功能:

把I/O文件映射到一個存儲區域中

頭文件:

#include <sys/mman.h>

函數原形:

void *mmap(void *addr,size_t len,int prot,int flag,int filedes,off_t off);

參數:

addr      指向映射存儲區的起始地址

len       映射的字節

prot      對映射存儲區的保護要求

flag      flag標誌位

filedes    要被映射文件的描述符

off       要映射字節在文件中的起始偏移量

返回值:

若成功則返回映射區的起始地址,若出錯則返回MAP_FAILED

addr參數用於指定映射存儲區的起始地址。通常將其設置為NULL,這表示由係統選擇該映射區的起始地址。

            filedes指要被映射文件的描述符。在映射該文件到一個地址空間之前,先要打開該文件。len是映射的字節數。

            off是要映射字節在文件中的起始偏移量。通常將其設置為0。

            prot參數說明對映射存儲區的保護要求。可將prot參數指定為PROT_NONE,或者是PROT_READ(映射區可讀),PROT_WRITE(映射區可寫),PROT_EXEC(映射區可執行)任意組合的按位或,也可以是PROT_NONE(映射區不可訪問)。對指定映射存儲區的保護要求不能超過文件open模式訪問權限。

            flag參數影響映射區的多種屬性:

MAP_FIXED返回值必須等於addr.因為這不利於可移植性,所以不鼓勵使用此標誌。

MAP_SHARED這一標誌說明了本進程對映射區所進行的存儲操作的配置。此標誌指定存儲操作修改映射文件。

MAP_PRIVATE本標誌導致對映射區建立一個該映射文件的一個私有副本。所有後來對該映射區的引用都是引用該副本,而不是原始文件。

要注意的是必須指定MAP_FIXED或MAP_PRIVATE標誌其中的一個,指定前者是對存儲映射文件本身的一個操作,而後者是對其副本進行操作。

 

mmap成功返回後,fd參數可以關閉。該操作對於由mmap建立的映射關係沒有影響。為從某個進程的地址空間刪除一個映射關係,我們調用munmap.

名稱::

munmap

功能:

解除存儲映射

頭文件:

#include <sys/mman.h>

函數原形:

int munmap(caddr_t addr,size_t len);

參數:

addr      指向映射存儲區的起始地址

len       映射的字節

返回值:

若成功則返回0,若出錯則返回-1

      其中addr參數是由mmap返回的地址,len是映射區的大小。再次訪問這些地址導致向調用進程產生一個SIGSEGV信號。

      如果被映射區是使用MAP_PRIVATE標誌映射的,那麼調用進程對它所作的變動都被丟棄掉。

 

內核的虛存算法保持內存映射文件(一般在硬盤上)與內存映射區(在內存中)的同步(前提它是MAP_SHARED內存區)。這就是說,如果我們修改了內存映射到某個文件的內存區中某個位置的內容,那麼內核將在稍後某個時刻相應地更新文件。然而有時候我們希望確信硬盤上的文件內容與內存映射區中的文件內容一致,於是調用msync來執行這種同步。

名稱::

msync

功能:

同步文件到存儲器

頭文件:

#include <sys/mman.h>

函數原形:

int msync(void *addr,size_t len,int flags);

參數:

addr      指向映射存儲區的起始地址

len       映射的字節

prot      flags

返回值:

若成功則返回0,若出錯則返回-1

            其中addr和len參數通常指代內存中的整個內存映射區,不過也可以指定該內存區的一個子集。flags參數為MS_ASYNC(執行異步寫),MS_SYNC(執行同步寫),MS_INVALIDATE(使高速緩存的數據實效)。其中MS_ASYNC和MS_SYNC這兩個常值中必須指定一個,但不能都指定。它們的差別是,一旦寫操作已由內核排入隊列,MS_ASYNC即返回,而MS_SYNC則要等到寫操作完成後才返回。如果還指定了MS_INVALIDATE,那麼與其最終拷貝不一致的文件數據的所有內存中拷貝都失效。後續的引用將從文件取得數據。

名稱::

memcpy

功能:

複製映射存儲區

頭文件:

#include <string.h>

函數原形:

void *memcpy(void *dest,const void *src,size_t n);

參數:

dest       待複製的映射存儲區

src        複製後的映射存儲區

n          待複製的映射存儲區的大小

返回值:

返回dest的首地址

memcpy拷貝n個字節從dest到src。

 

下麵就是利用mmap函數影射I/O實現的cp命令。

#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
 
int main(int argc,char *argv[])
{
int fdin,fdout;
void *arc,dst;
struct stat statbuf;
 
if(argc!=3)
{
    printf(“please input two file!\n”);
    exit(1);
}
if((fdin=open(argv[1],O_RDONLY))<0) /*打開原文件*/
    perror(argv[1]);
if((fdout=open(argv[2],O_RDWR|O_CREAT|O_TRUNC))<0)/*創建並打開目標文件*/
    perror(argv[2]);
 
if(fstat(fdin,&statbuf)<0) /*獲得文件大小信息*/
    printf(“fstat error”);
 
if(lseek(fdout,statbuf.st_size-1,SEEK_SET)==-1)/*初始化輸出映射存儲區*/
    printf(“lseek error”);
if(write(fdout,”1”)!=1)
    printf(“write error”);
 
if((src=mmap(0,statbuf.st_size,PROT_READ,MAP_SHARED,fdin,0))==MAP_FAILED)
    /*映射原文件到輸入的映射存儲區*/
    printf(“mmap error);
if((dst=mmap(0,statbuf.st_size,PROT_READ|PROT_WRITE,MAP_SHARED,fdout,0)) ==MAP_FAILED) /*映射目標文件到輸出的映射存儲區*/
    printf(“mmap error);
memcpy(dst,src,statbuf.st_size);/*複製映射存儲區*/
munmap(src,statbuf.st_size); /*解除輸入映射*/
munmap(dst,statbuf.st_size); /*解除輸出映射*/
close(fdin);
close(fdout);
}

下麵是運行結果:

#cc –o mycp mycp.c

#./mycp test1 test2

三、posix共享內存函數

posix共享內存區涉及兩個步驟:

1、指定一個名字參數調用shm_open,以創建一個新的共享內存區對象或打開一個以存在的共享內存區對象。

2、調用mmap把這個共享內存區映射到調用進程的地址空間。傳遞給shm_open的名字參數隨後由希望共享該內存區的任何其他進程使用。

名稱::

shm_open

功能:

打開或創建一個共享內存區

頭文件:

#include <sys/mman.h>

函數原形:

int shm_open(const char *name,int oflag,mode_t mode);

參數:

name    共享內存區的名字

cflag    標誌位

mode    權限位

返回值:

成功返回0,出錯返回-1

            oflag參數必須含有O_RDONLY和O_RDWR標誌,還可以指定如下標誌:O_CREAT,O_EXCL或O_TRUNC.

            mode參數指定權限位,它指定O_CREAT標誌的前提下使用。

shm_open的返回值是一個整數描述字,它隨後用作mmap的第五個參數。

名稱::

shm_unlink

功能:

刪除一個共享內存區

頭文件:

#include <sys/mman.h>

函數原形:

int shm_unlink(const char *name);

參數:

name    共享內存區的名字

返回值:

成功返回0,出錯返回-1

                        

  shm_unlink函數刪除一個共享內存區對象的名字,刪除一個名字僅僅防止後續的open,mq_open或sem_open調用取得成功。

 下麵是創建一個共享內存區的例子:

#include <sys/mman.h>
#include <stdio.h>
#include <fcntl.h>
 
int main(int argc,char **argv)
{
int shm_id;
 
if(argc!=2)
{
    printf(“usage:shm_open <pathname>\n”);
    exit(1);
}
shm_id=shm_open(argv[1],O_RDWR|O_CREAT,0644);
printf(“shmid:%d\n”,shm_id);
shm_unlink(argv[1]);
}

下麵是運行結果,注意編譯程序我們要加上“-lrt”參數。

#cc –lrt –o shm_open shm_open.c

#./shm_open test

shm_id:3


四、ftruncate和fstat函數

普通文件或共享內存區對象的大小都可以通過調用ftruncate修改。

名稱::

ftruncate

功能:

調整文件或共享內存區大小

頭文件:

#include <unistd.h>

函數原形:

int ftruncate(int fd,off_t length);

參數:

fd          描述符

length       大小

返回值:

成功返回0,出錯返回-1

 當打開一個已存在的共享內存區對象時,我們可調用fstat來獲取有關該對象的信息。

名稱::

fstat

功能:

獲得文件或共享內存區的信息

頭文件:

#include <unistd.h>

#include <sys/types.h>

#include <sys/stat.h>

函數原形:

int stat(const char *file_name,struct stat *buf);

參數:

file_name          文件名

buf               stat結構

返回值:

成功返回0,出錯返回-1

      對於普通文件stat結構可以獲得12個以上的成員信息,然而當fd指代一個共享內存區對象時,隻有四個成員含有信息。

struct stat{

mode_t st_mode;

uid_t st_uid;

gid_t st_gid;

off_t st_size;

};

 

#include <unistd.h>
#include <sys/type.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/mman.h>
 
int main(int argc,char **argv)
{
    int shm_id;
struct stat buf;
 
if(argc!=2)
{
    printf(“usage:shm_open <pathname>\n”);
    exit(1);
}
shm_id=shm_open(argv[1],O_RDWR|O_CREAT,0644);/*創建共享內存*/
ftruncate(shm_id,100);/*修改共享內存的打開*/
fstat(shm_id,&buf); /*把共享內存的信息記錄到buf中*/
printf(“uid_t:%d\n”,buf.st_uid); /*共享內存區所有者ID*/
printf(“git_t:%d\n”,buf.st_gid); /*共享內存區所有者組ID*/
printf(“size :%d\n”,buf.st_size); /*共享內存區大小*/
}

下麵是運行結果:

#cc –lrt –o shm_show shm_show.c

#./shm_show test

uid_t:0

git_t:0

size:100

五、共享內存區的寫入和讀出

      上麵我們介紹了mmap函數,下麵我們就可以通過這些函數,把進程映射到共享內存區。

然後我們就可以通過共享內存區進行進程間通信了。

      下麵是共享內存區寫入的例子:

#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
 
int main(int argc,char **argv)
{
int shm_id;
struct stat buf;
char *ptr;
 
if(argc!=2)
{
    printf(“usage:shm_open <pathname>\n”);
    exit(1);
}
shm_id=shm_open(argv[1],O_RDWR|O_CREAT,0644);/*創建共享內存區*/
ftruncate(shm_id,100);/*修改共享區大小*/
fstat(shm_id,&buf);
ptr=mmap(NULL,buf.st_size,PROT_READ|PROT_WRITE,MAP_SHARED,shm_id,0);/*連接共享內存區*/
strcpy(ptr,”hello linux”);/*寫入共享內存區*/
printf(“%s\n”,ptr);/*讀出共享內存區*/
shm_unlink(argv[1]);/*刪除共享內存區*/
}

下麵是運行結果:

#cc –lrt –o shm_write shm_write.c

#./shm_write test

hello linux

 

六、程序例子

      下麵是利用pisix共享內存區實現進程間通信的例子:服務器進程讀出共享內存區內容,然後清空。客戶進程向共享內存區寫入數據。直到用戶輸入“q”程序結束。程序用posix信號量實現互斥。

/*server.c服務器程序*/
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <semaphore.h>
 
int main(int argc,char **argv)
{
int shm_id;
char *ptr;
sem_t *sem;
 
if(argc!=2)
{
    printf(“usage:shm_open <pathname>\n”);
    exit(1);
}
shm_id=shm_open(argv[1],O_RDWR|O_CREAT,0644);/*創建共享內存區*/
ftruncate(shm_id,100);/*調整共享內存區大小*/
sem=sem_open(argv[1],O_CREAD,0644,1);/*創建信號量*/
ptr=mmap(NULL,100,PROT_READ|PROT_WRITE,MAP_SHARED,shm_id,0);/*連接共享內存區*/
strcpy(ptr,”\0”);
while(1)
{
    if((strcmp(ptr,”\0”))==0)/*如果為空,則等待*/
        continue;
        else
        {
             if((strcmp(ptr,”q\n”))==0)/*如果內存為q\n退出循環*/
                 break;
             sem_wait(sem);/*申請信號量*/
             printf(“server:%s”,ptr);/*輸入共享內存區內容*/
             strcpy(ptr,”\0”);/*清空共享內存區*/
             sem_pose(sem);/*釋放信號量*/
         }
         sem_unlink(argv[1]);/*刪除信號量*/
         shm_unlink(argv[1]);/*刪除共享內存區*/
     }
}

 客戶端程序:

/*user.c 客戶端程序*/
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <semaphore.h>
#include <stdio.h>
 
int main(int argc,char **argv)
{
int shm_id;
char *ptr;
sem_t *sem;
 
if(argc!=2)
{
    printf(“usage:shm_open <pathname>\n”);
    exit(1);
}
shm_id=shm_open(argv[1],0);/*打開共享內存區
sem=sem_open(argv[1],0);/*打開信號量*/
ptr=mmap(NULL,100,PROT_READ|PROT_WRITE,MAP_SHARED,shm_id,0);/*連接共享內存區*/
while(1)
{
         sem_wait(sem);/*申請信號量*/
         fgets(ptr,10,stdin);/*從鍵盤讀入數據到共享內存區*/
         printf(“user:%s”,ptr);
         if((strcmp(ptr,”q\n”))==0)
             exit(0);
         sem_pose(sem);/*釋放信號量*/
         sleep(1);
     }
     exit(0);
}

#cc –lrt –o server server.c

#cc –lrt –o user user.c

#./server test&

#./user test

輸入:abc

user:abc

server:abc

輸入:123

user:123

server:123

輸入:q

user:q

 

最後更新:2017-04-03 12:54:31

  上一篇:go Java判斷字符串長度
  下一篇:go Oracle中“無監聽程序”和“協議適配器錯誤”的解決方法