Linux程式間通訊——使用共享記憶體

後開啟撒打發了發表於2018-01-10
下面將講解程式間通訊的另一種方式,使用共享記憶體。

一、什麼是共享記憶體
顧名思義,共享記憶體就是允許兩個不相關的程式訪問同一個邏輯記憶體。共享記憶體是在兩個正在執行的程式之間共享和傳遞資料的一種非常有效的方式。不同程式之間共享的記憶體通常安排為同一段實體記憶體。程式可以將同一段共享記憶體連線到它們自己的地址空間中,所有程式都可以訪問共享記憶體中的地址,就好像它們是由用C語言函式malloc分配的記憶體一樣。而如果某個程式向共享記憶體寫入資料,所做的改動將立即影響到可以訪問同一段共享記憶體的任何其他程式。

特別提醒:共享記憶體並未提供同步機制,也就是說,在第一個程式結束對共享記憶體的寫操作之前,並無自動機制可以阻止第二個程式開始對它進行讀取。所以我們通常需要用其他的機制來同步對共享記憶體的訪問,例如前面說到的訊號量。有關訊號量的更多內容,可以查閱我的另一篇文章:Linux程式間通訊——使用訊號量

二、共享記憶體的使得
與訊號量一樣,在Linux中也提供了一組函式介面用於使用共享記憶體,而且使用共享共存的介面還與訊號量的非常相似,而且比使用訊號量的介面來得簡單。它們宣告在標頭檔案 sys/shm.h中。

1、shmget函式
該函式用來建立共享記憶體,它的原型為:
[cpp] view plain copy
  1. int shmget(key_t key, size_t size, int shmflg);  
第一個引數,與訊號量的semget函式一樣,程式需要提供一個引數key(非0整數),它有效地為共享記憶體段命名,shmget函式成功時返回一個與key相關的共享記憶體識別符號(非負整數),用於後續的共享記憶體函式。呼叫失敗返回-1.

不相關的程式可以通過該函式的返回值訪問同一共享記憶體,它代表程式可能要使用的某個資源,程式對所有共享記憶體的訪問都是間接的,程式先通過呼叫shmget函式並提供一個鍵,再由系統生成一個相應的共享記憶體識別符號(shmget函式的返回值),只有shmget函式才直接使用訊號量鍵,所有其他的訊號量函式使用由semget函式返回的訊號量識別符號。

第二個引數,size以位元組為單位指定需要共享的記憶體容量

第三個引數,shmflg是許可權標誌,它的作用與open函式的mode引數一樣,如果要想在key標識的共享記憶體不存在時,建立它的話,可以與IPC_CREAT做或操作。共享記憶體的許可權標誌與檔案的讀寫許可權一樣,舉例來說,0644,它表示允許一個程式建立的共享記憶體被記憶體建立者所擁有的程式向共享記憶體讀取和寫入資料,同時其他使用者建立的程式只能讀取共享記憶體。

2、shmat函式
第一次建立完共享記憶體時,它還不能被任何程式訪問,shmat函式的作用就是用來啟動對該共享記憶體的訪問,並把共享記憶體連線到當前程式的地址空間。它的原型如下:
[cpp] view plain copy
  1. void *shmat(int shm_id, const void *shm_addr, int shmflg);  
第一個引數,shm_id是由shmget函式返回的共享記憶體標識。
第二個引數,shm_addr指定共享記憶體連線到當前程式中的地址位置,通常為空,表示讓系統來選擇共享記憶體的地址。
第三個引數,shm_flg是一組標誌位,通常為0。

呼叫成功時返回一個指向共享記憶體第一個位元組的指標,如果呼叫失敗返回-1.

3、shmdt函式
該函式用於將共享記憶體從當前程式中分離。注意,將共享記憶體分離並不是刪除它,只是使該共享記憶體對當前程式不再可用。它的原型如下:
[cpp] view plain copy
  1. int shmdt(const void *shmaddr);  
引數shmaddr是shmat函式返回的地址指標,呼叫成功時返回0,失敗時返回-1.

4、shmctl函式
與訊號量的semctl函式一樣,用來控制共享記憶體,它的原型如下:
[cpp] view plain copy
  1. int shmctl(int shm_id, int command, struct shmid_ds *buf);  
第一個引數,shm_id是shmget函式返回的共享記憶體識別符號。

第二個引數,command是要採取的操作,它可以取下面的三個值 :
    IPC_STAT:把shmid_ds結構中的資料設定為共享記憶體的當前關聯值,即用共享記憶體的當前關聯值覆蓋shmid_ds的值。
    IPC_SET:如果程式有足夠的許可權,就把共享記憶體的當前關聯值設定為shmid_ds結構中給出的值
    IPC_RMID:刪除共享記憶體段

第三個引數,buf是一個結構指標,它指向共享記憶體模式和訪問許可權的結構。
shmid_ds結構至少包括以下成員:
[cpp] view plain copy
  1. struct shmid_ds  
  2. {  
  3.     uid_t shm_perm.uid;  
  4.     uid_t shm_perm.gid;  
  5.     mode_t shm_perm.mode;  
  6. };  

三、使用共享記憶體進行程式間通訊
說了這麼多,又到了實戰的時候了。下面就以兩個不相關的程式來說明程式間如何通過共享記憶體來進行通訊。其中一個檔案shmread.c建立共享記憶體,並讀取其中的資訊,另一個檔案shmwrite.c向共享記憶體中寫入資料。為了方便操作和資料結構的統一,為這兩個檔案定義了相同的資料結構,定義在檔案shmdata.c中。結構shared_use_st中的written作為一個可讀或可寫的標誌,非0:表示可讀,0表示可寫,text則是記憶體中的檔案。

shmdata.h的原始碼如下:
[cpp] view plain copy
  1. #ifndef _SHMDATA_H_HEADER  
  2. #define _SHMDATA_H_HEADER  
  3.   
  4. #define TEXT_SZ 2048  
  5.   
  6. struct shared_use_st  
  7. {  
  8.     int written;//作為一個標誌,非0:表示可讀,0表示可寫  
  9.     char text[TEXT_SZ];//記錄寫入和讀取的文字  
  10. };  
  11.   
  12. #endif  
原始檔shmread.c的原始碼如下:
[cpp] view plain copy
  1. #include <unistd.h>  
  2. #include <stdlib.h>  
  3. #include <stdio.h>  
  4. #include <sys/shm.h>  
  5. #include "shmdata.h"  
  6.   
  7. int main()  
  8. {  
  9.     int running = 1;//程式是否繼續執行的標誌  
  10.     void *shm = NULL;//分配的共享記憶體的原始首地址  
  11.     struct shared_use_st *shared;//指向shm  
  12.     int shmid;//共享記憶體識別符號  
  13.     //建立共享記憶體  
  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.     //將共享記憶體連線到當前程式的地址空間  
  21.     shm = shmat(shmid, 0, 0);  
  22.     if(shm == (void*)-1)  
  23.     {  
  24.         fprintf(stderr, "shmat failed\n");  
  25.         exit(EXIT_FAILURE);  
  26.     }  
  27.     printf("\nMemory attached at %X\n", (int)shm);  
  28.     //設定共享記憶體  
  29.     shared = (struct shared_use_st*)shm;  
  30.     shared->written = 0;  
  31.     while(running)//讀取共享記憶體中的資料  
  32.     {  
  33.         //沒有程式向共享記憶體定資料有資料可讀取  
  34.         if(shared->written != 0)  
  35.         {  
  36.             printf("You wrote: %s", shared->text);  
  37.             sleep(rand() % 3);  
  38.             //讀取完資料,設定written使共享記憶體段可寫  
  39.             shared->written = 0;  
  40.             //輸入了end,退出迴圈(程式)  
  41.             if(strncmp(shared->text, "end", 3) == 0)  
  42.                 running = 0;  
  43.         }  
  44.         else//有其他程式在寫資料,不能讀取資料  
  45.             sleep(1);  
  46.     }  
  47.     //把共享記憶體從當前程式中分離  
  48.     if(shmdt(shm) == -1)  
  49.     {  
  50.         fprintf(stderr, "shmdt failed\n");  
  51.         exit(EXIT_FAILURE);  
  52.     }  
  53.     //刪除共享記憶體  
  54.     if(shmctl(shmid, IPC_RMID, 0) == -1)  
  55.     {  
  56.         fprintf(stderr, "shmctl(IPC_RMID) failed\n");  
  57.         exit(EXIT_FAILURE);  
  58.     }  
  59.     exit(EXIT_SUCCESS);  
  60. }  
原始檔shmwrite.c的原始碼如下:
[cpp] view plain copy
  1. #include <unistd.h>  
  2. #include <stdlib.h>  
  3. #include <stdio.h>  
  4. #include <string.h>  
  5. #include <sys/shm.h>  
  6. #include "shmdata.h"  
  7.   
  8. int main()  
  9. {  
  10.     int running = 1;  
  11.     void *shm = NULL;  
  12.     struct shared_use_st *shared = NULL;  
  13.     char buffer[BUFSIZ + 1];//用於儲存輸入的文字  
  14.     int shmid;  
  15.     //建立共享記憶體  
  16.     shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);  
  17.     if(shmid == -1)  
  18.     {  
  19.         fprintf(stderr, "shmget failed\n");  
  20.         exit(EXIT_FAILURE);  
  21.     }  
  22.     //將共享記憶體連線到當前程式的地址空間  
  23.     shm = shmat(shmid, (void*)0, 0);  
  24.     if(shm == (void*)-1)  
  25.     {  
  26.         fprintf(stderr, "shmat failed\n");  
  27.         exit(EXIT_FAILURE);  
  28.     }  
  29.     printf("Memory attached at %X\n", (int)shm);  
  30.     //設定共享記憶體  
  31.     shared = (struct shared_use_st*)shm;  
  32.     while(running)//向共享記憶體中寫資料  
  33.     {  
  34.         //資料還沒有被讀取,則等待資料被讀取,不能向共享記憶體中寫入文字  
  35.         while(shared->written == 1)  
  36.         {  
  37.             sleep(1);  
  38.             printf("Waiting...\n");  
  39.         }  
  40.         //向共享記憶體中寫入資料  
  41.         printf("Enter some text: ");  
  42.         fgets(buffer, BUFSIZ, stdin);  
  43.         strncpy(shared->text, buffer, TEXT_SZ);  
  44.         //寫完資料,設定written使共享記憶體段可讀  
  45.         shared->written = 1;  
  46.         //輸入了end,退出迴圈(程式)  
  47.         if(strncmp(buffer, "end", 3) == 0)  
  48.             running = 0;  
  49.     }  
  50.     //把共享記憶體從當前程式中分離  
  51.     if(shmdt(shm) == -1)  
  52.     {  
  53.         fprintf(stderr, "shmdt failed\n");  
  54.         exit(EXIT_FAILURE);  
  55.     }  
  56.     sleep(2);  
  57.     exit(EXIT_SUCCESS);  
  58. }  
再來看看執行的結果:



分析:
1、程式shmread建立共享記憶體,然後將它連線到自己的地址空間。在共享記憶體的開始處使用了一個結構struct_use_st。該結構中有個標誌written,當共享記憶體中有其他程式向它寫入資料時,共享記憶體中的written被設定為0,程式等待。當它不為0時,表示沒有程式對共享記憶體寫入資料,程式就從共享記憶體中讀取資料並輸出,然後重置設定共享記憶體中的written為0,即讓其可被shmwrite程式寫入資料。

2、程式shmwrite取得共享記憶體並連線到自己的地址空間中。檢查共享記憶體中的written,是否為0,若不是,表示共享記憶體中的資料還沒有被完,則等待其他程式讀取完成,並提示使用者等待。若共享記憶體的written為0,表示沒有其他程式對共享記憶體進行讀取,則提示使用者輸入文字,並再次設定共享記憶體中的written為1,表示寫完成,其他程式可對共享記憶體進行讀操作。

四、關於前面的例子的安全性討論
這個程式是不安全的,當有多個程式同時向共享記憶體中讀寫資料時,問題就會出現。可能你會認為,可以改變一下written的使用方式,例如,只有當written為0時程式才可以向共享記憶體寫入資料,而當一個程式只有在written不為0時才能對其進行讀取,同時把written進行加1操作,讀取完後進行減1操作。這就有點像檔案鎖中的讀寫鎖的功能。咋看之下,它似乎能行得通。但是這都不是原子操作,所以這種做法是行不能的。試想當written為0時,如果有兩個程式同時訪問共享記憶體,它們就會發現written為0,於是兩個程式都對其進行寫操作,顯然不行。當written為1時,有兩個程式同時對共享記憶體進行讀操作時也是如些,當這兩個程式都讀取完是,written就變成了-1.

要想讓程式安全地執行,就要有一種程式同步的進位制,保證在進入臨界區的操作是原子操作。例如,可以使用前面所講的訊號量來進行程式的同步。因為訊號量的操作都是原子性的。

五、使用共享記憶體的優缺點
1、優點:我們可以看到使用共享記憶體進行程式間的通訊真的是非常方便,而且函式的介面也簡單,資料的共享還使程式間的資料不用傳送,而是直接訪問記憶體,也加快了程式的效率。同時,它也不像匿名管道那樣要求通訊的程式有一定的父子關係。

2、缺點:共享記憶體沒有提供同步的機制,這使得我們在使用共享記憶體進行程式間通訊時,往往要藉助其他的手段來進行程式間的同步工作。

相關文章