linux程式間的通訊(C): 共享記憶體

劍西樓發表於2017-02-28
一、共享記憶體介紹
共享記憶體是三個IPC(Inter-Process Communication)機制中的一個。
它允許兩個不相關的程式訪問同一個邏輯記憶體。
共享記憶體是在兩個正在進行的程式之間傳遞資料的一種非常有效的方式。
大多數的共享記憶體的實現,
都把由不同程式之間共享的記憶體安排為同一段實體記憶體

共享記憶體是由IPC為程式建立一個特殊的地址範圍,
它將出現在該程式的地址空間中。
其他程式可以將同一段共享記憶體連線它們自己的地址空間中。
所有程式都可以訪問共享記憶體中的地址,
就好像它們是由malloc分配的一樣。

如果某個程式向共享記憶體寫入了資料,
所做的改動將立刻被可以訪問同一段共享記憶體的任何其他程式看到。

二、共享記憶體的同步
共享記憶體為在多個程式之間共享和傳遞資料提供了一種有效的方式。
但是它並未提供同步機制
所以我們通常需要用其他的機制來同步對共享記憶體的訪問。
我們通常是用共享記憶體來提供對大塊記憶體區域的有效訪問,
同時通過傳遞小訊息來同步對該記憶體的訪問。

在第一個程式結束對共享記憶體的寫操作之前,
並無自動的機制可以阻止第二個程式開始對它進行讀取。
對共享記憶體訪問的同步控制必須由程式設計師來負責。

下圖顯示了共享記憶體是如何共存的:

圖中的箭頭顯示了每個程式的邏輯地址空間到可用實體記憶體的對映關係。


三、共享記憶體使用的函式
  1. #include <sys/shm.h>

  2. int shmget(key_t key, size_t size, int shmflg);
  3. void *shmat(int shm_id, const void *shm_addr, int shmflg);
  4. int shmdt(const void *shm_addr);
  5. int shmctl(int shm_id, int cmd, struct shmid_ds *buf);

1. shmget函式
該函式用來建立共享記憶體:
  1. int shmget(key_t key, size_t size, int shmflg);
引數:
key : 和訊號量一樣,程式需要提供一個引數key,
      它有效地為共享記憶體段命名。
      
      有一個特殊的鍵值IPC_PRIVATE, 
      它用於建立一個只屬於建立程式的共享記憶體,
      通常不會用到。
size: 以位元組為單位指定需要共享的記憶體容量。
shmflag: 包含9個位元的許可權標誌,
         它們的作用與建立檔案時使用的mode標誌是一樣。
         由IPC_CREAT定義的一個特殊位元必須和許可權標誌按位或
         才能建立一個新的共享記憶體段。

NOTE:
許可權標誌對共享記憶體非常有用,
因為它允許一個程式建立的共享記憶體可以被共享記憶體的建立者所擁有的程式寫入,
同時其它使用者建立的程式只能讀取共享記憶體。

我們可以利用這個功能來提供一種有效的對資料進行只讀訪問的方法,
通過將資料放共享記憶體並設定它的許可權,
就可以避免資料被其他使用者修改。

返回值:
建立成功,則返回一個非負整數,即共享記憶體標識;
如果失敗,則返回-1.

2. shmat函式
第一次建立共享記憶體段時,它不能被任何程式訪問。
要想啟動對該記憶體的訪問,
必須將其連線到一個程式的地址空間
這個工作由shmat函式完成:
  1. void *shmat(int shm_id, const void *shm_addr, int shmflg);
引數:
shm_id : 由shmget返回的共享記憶體標識。
shm_add: 指定共享記憶體連線到當前程式中的地址位置。
         它通常是一個空指標, 
         表示讓系統來選擇共享記憶體出現的地址。
shmflg : 是一組標誌。
         它的兩個可能取值是:
         SHM_RND, 和shm_add聯合使用,
                  用來控制共享記憶體連線的地址。
         SHM_RDONLY, 它使連線的記憶體只讀
         
返回值:
如果呼叫成功, 返回一個指向共享記憶體第一個位元組的指標;
如果失敗,返回-1.

共享記憶體的讀寫許可權由它的屬主(共享記憶體的建立者),
它的訪問許可權和當前程式的屬主決定。
共享記憶體的訪問許可權類似於檔案的訪問許可權。

3. shmdt
將共享記憶體從當前程式中分離
  1. int shmdt(const void *shm_addr);
shm_addr: shmat返回的地址指標。

成功時,返回0,
失敗時,返回-1.

NOTE:
共享記憶體分離並未刪除它,
只是使得該共享記憶體對當前程式不再可用。

4. shmctl
共享記憶體的控制函式
  1. int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
shmid_ds結構至少包含以下成員:
  1. struct shmid_ds {
  2.   uid_t shm_perm.uid;
  3.   uid_t shm_perm.gid;
  4.   mode_t shm_perm.mode;
  5. }

引數:
shm_id : 是shmget返回的共享記憶體識別符號。
command: 是要採取的動作,
         它可以取3個值:

IPC_STAT  把shmid_ds結構中的資料設定為共享記憶體的當前關聯值
IPC_SET   如果程式有足夠的許可權,
          就把共享記憶體的當前關聯值設定為shmid_ds結構中給出的值
IPC_RMID  刪除共享記憶體段

buf    : 是一個指標,
         包含共享記憶體模式和訪問許可權的結構。

返回值:
成功時,返回0,
失敗時,返回-1.

四、示例
典型的消費者-生產者程式,
第一個程式(消費者)將建立一個共享記憶體段,
然後把寫到它裡面的資料都顯示出來。
第二個程式(生產者)將連線一個已有的共享記憶體段,
並允許我們向其中輸入資料。

shm_com.h
  1. #define TEXT_SZ 2048

  2. struct shared_use_st {
  3.   int written_by_you;
  4.   char some_text[TEXT_SZ];
  5. };
當有資料寫入這個結構中時,
我們用結構中的written_by_you標誌來通知消費者。
需要傳輸的文字長度2K是隨意定的。

shm1.c 消費者程式
  1. #include <unistd.h>
  2. #include <stdlib.h>
  3. #include <stdio.h>
  4. #include <string.h>

  5. #include <sys/shm.h>

  6. #include "shm_com.h"
  7. int main()
  8. {
  9.   int running = 1;
  10.   void *shared_memory = (void *)0;
  11.   struct shared_use_st *shared_stuff;
  12.   int shmid;

  13.   srand((unsigned int)getpid());
  14.   shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666 | IPC_CREAT);

  15.   if (shmid == -1) {
  16.     fprintf(stderr, "shmget failed\n");
  17.     exit(EXIT_FAILURE);
  18.   }

現在,讓程式可以訪問這個共享記憶體:
  1.   shared_memory = shmat(shmid, (void *)0, 0);

  2.   if (shared_memory == (void *)-1) {
  3.     fprintf(stderr, "shmat failed\n");
  4.     exit(EXIT_FAILURE);
  5.   }

  6.   printf("Memory attached at %X\n", (int)shared_memory);

程式的下一部分將shared_memory分配給shared_stuff,
然後它輸出written_by_you中的文字。
迴圈將一直執行到在written_by_you中找到end字串為止。
sleep呼叫強迫消費者程式在臨界區域多待一會,
讓生產者程式等待:

  1.   shared_stuff = (struct shared_use_st *)shared_memory;
  2.   shared_stuff->written_by_you = 0;

  3.   while(running) 
  4.   {
  5.     if (shared_stuff->written_by_you) 
  6.     {
  7.       printf("You wrote: %s", shared_stuff->some_text);

  8.       sleep( rand() % 4 ); /* make the other process wait for us ! */
  9.       shared_stuff->written_by_you = 0;

  10.       if (strncmp(shared_stuff->some_text, “end”, 3) == 0) {
  11.         running = 0;
  12.       }
  13.     }
  14.   }

最後,共享記憶體被分離,然後被刪除:
  1.   if (shmdt(shared_memory) == -1) 
  2.   {
  3.     fprintf(stderr, "shmdt failed\n");
  4.     exit(EXIT_FAILURE);
  5.   }

  6.   if (shmctl(shmid, IPC_RMID, 0) == -1) 
  7.   {
  8.     fprintf(stderr, "shmctl(IPC_RMID) failed\n");
  9.     exit(EXIT_FAILURE);
  10.   }

  11.   exit(EXIT_SUCCESS);
  12. }
shm2.c 生產者程式
通過它向消費者程式輸入資料。
  1. #include <unistd.h>
  2. #include <stdlib.h>
  3. #include <stdio.h>
  4. #include <string.h>

  5. #include <sys/shm.h>

  6. #include "shm_com.h"

  7. int main()
  8. {
  9.   int running = 1;
  10.   void *shared_memory = (void *)0;
  11.   struct shared_use_st *shared_stuff;
  12.   char buffer[BUFSIZ];
  13.   int shmid;

  14.   shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666 | IPC_CREAT);
  15.   if (shmid == -1) 
  16.   {
  17.     fprintf(stderr, "shmget failed\n");
  18.     exit(EXIT_FAILURE);
  19.   }

  20.   shared_memory = shmat(shmid, (void *)0, 0);
  21.   if (shared_memory == (void *)-1) 
  22.   {
  23.     fprintf(stderr, "shmat failed\n");
  24.     exit(EXIT_FAILURE);
  25.   }

  26.   printf("Memory attached at %X\n", (int)shared_memory);

  27.   shared_stuff = (struct shared_use_st *)shared_memory;
  28.   while(running) 
  29.   {
  30.     while(shared_stuff->written_by_you == 1) 
  31.     {
  32.       sleep(1);
  33.       printf("waiting for client...\n");
  34.     }
  35.     printf("Enter some text: ");
  36.     fgets(buffer, BUFSIZ, stdin);

  37.     strncpy(shared_stuff->some_text, buffer, TEXT_SZ);
  38.     shared_stuff->written_by_you = 1;

  39.     if (strncmp(buffer, "end", 3) == 0) {
  40.       running = 0;
  41.     }
  42.   }

  43.   if (shmdt(shared_memory) == -1) {
  44.     fprintf(stderr, "shmdt failed\n");
  45.     exit(EXIT_FAILURE);
  46.   }

  47.   exit(EXIT_SUCCESS);
  48. }

執行程式,
將看到如下所示的樣本輸出:
  1. ./shm1 &
  2. [1] 294
  3. Memory attached at 40017000
  4. ./shm2
  5. Memory attached at 40017000
  6. Enter some text: hello
  7. You wrote: hello
  8. waiting for client...
  9. waiting for client...
  10. Enter some text: 
  11. You wrote: 
  12. waiting for client...
  13. waiting for client...
  14. waiting for client...
  15. Enter some text: end
  16. You wrote: end
  17. $
程式解析:
消費者程式
建立共享記憶體段,
然後將它連線到它自己的地址空間中,
並且,
我們在共享記憶體的開始處使用了一個結構shared_use_st.
該結構中有個標誌written_by_you,
當共享記憶體中有資料寫入時,就設定這個標誌。

這個標誌被設定時,
程式就從共享記憶體中讀取文字,
將它列印出來,
然後清除這個標誌,表示已經讀完資料。
我們用一個特殊字串end來退出迴圈。

接下來,
程式分離共享記憶體段並刪除它。

生產者程式
使用相同的鍵值1234來取得並連線同一個共享記憶體段,
然後提示使用者輸入一些文字。
如果標誌written_by_you被設定,
生產者就知道消費都程式還未讀完上一次的資料,
因此就繼續等待。
當其它程式清除了這個標誌後,
生產者寫入新的資料並設定這個標誌。
它還使用字串end來終止並分離共享記憶體段。

這裡提供的同步標誌written_by_you,
它是一個非常缺乏效率的忙等待(不停地迴圈)。

但在實際程式設計中,
應該使用訊號量,
或通過傳遞訊息(使用管道或IPC訊息),
或生成訊號
的方法來提供讀寫之間的更有效的同步機制。

相關文章