首頁 > 軟體

Linux之程序間通訊(共用記憶體【mmap實現+系統V】)

2023-03-23 22:02:28

共用記憶體

  • 共用記憶體可以說是最有用的程序間通訊方式,也是最快的IPC形式,兩個不同的程序A、B共用記憶體的意思就是:同一塊實體記憶體被對映到程序A、B各自的程序地址空間,程序A可以同時看到程序B對共用記憶體中資料的更新,反之亦然。
  • 由於個多個程序共用同一塊記憶體區域,必然需要某種同步機制、互斥鎖和號誌都可以。

好處: 效率高,程序可以直接讀寫記憶體,而不需要複製任何資料,而管道、訊息佇列等通訊方式,則需要在核心和使用者空間進行四次資料複製。

並且只有在解除對映時,共用記憶體的內容才會寫會文紀念

共用記憶體通過核心物件,使得不同的程序在自己的虛擬地址空間上分配一塊空間對映到相同的實體記憶體空間上,這塊實體記憶體空間對於對映到上面的每個程序而言都是可以存取的。(臨界資源)

共用記憶體就是允許兩個不相關的程序存取同一個邏輯記憶體

共用記憶體是在兩個正在執行的程序之間共用和傳遞資料的一種非常有效的方式。

不同程序之間共用的記憶體通常安排為同一段實體記憶體。

程序可以將同一段共用記憶體連線到它們自己的地址空間中,所有程序都可以存取共用記憶體中的地址,就好像它們是由用C語言函數malloc()分配的記憶體一樣。

而如果某個程序向共用記憶體寫入資料,所做的改動將立即影響到可以 存取同一段共用記憶體的任何其他程序。

mmap()及其相關的系統呼叫

mmap是linux作業系統提供給使用者空間呼叫的記憶體對映函數,很多人僅僅只是知道可以通過mmap完成程序間的記憶體共用和減少使用者態到核心態的資料拷貝次數,但是並沒有深入理解mmap在作業系統內部是如何實現的,原理是什麼

mmap()系統呼叫使得程序之間可以通過對映同一個普通檔案實現記憶體共用。普通檔案被對映到程序地址空間後,程序可以存取普通記憶體一樣對檔案進行存取,不必再呼叫read和write操作。 

注意: mmap並不是完全為了IPC而設計的,只是IPC的一種應用方式,它本身提供了一種像存取普通記憶體一樣的存取對普通檔案進行操作的方式。

通過使用帶有特殊許可權集的虛擬記憶體段來實現。對這類虛擬記憶體段的讀寫會使作業系統去讀寫磁碟檔案中與之對應的部分。

mmap 函數建立一個指向一段記憶體區域的指標,該記憶體區域與可以通過一個開啟的檔案描述符存取的檔案的內容相關聯

解釋如下:

mmap()

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

可以通過傳遞 offset 引數來改變經共用記憶體段存取的檔案中資料的起始偏移值。

開啟的檔案描述符由 fd 引數給出。

可以存取的資料量(即記憶體段的長度)由 length 引數設定。

可以通過 addr 引數來請求使用某個特定的記憶體地址。如果它的取值是零,結果指標就將自動分配。這是推薦的做法,否則會降低程式的可移植性,因為不同系統上的可用地址範圍是不一樣的。

prot 引數用於設定記憶體段的存取許可權。它是下列常數值的按位元或的結果

  • PROT_READ 記憶體段可讀。
  • PROT_WRITE 記憶體段可寫。
  • PROT_EXEC 記憶體段可執行。
  • PROT_NONE 記憶體段不能被存取。

flags 引數控制程式對該記憶體段的改變所造成的影響:

mmap()用於共用記憶體的量和兩種方式如下:

使用普通檔案提供的記憶體對映,適用於任何程序間,使用該方式需要先開啟或者建立一個檔案,再呼叫ngmmap,典型呼叫程式碼如下:

fd = open(name.falg.mode);
if(fd < 0)
ptr = mmap(NULL,len.PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);

使用特殊檔案提供的記憶體對映,適用於具有親緣關係的程序之間,由於父子程序特殊的親緣關係,在父程序中先呼叫mmap,呼叫fork,那麼在代用fork之後,子程序可以繼承父程序匿名對映後的地址空間,同樣也繼承mmap返回的地址,這樣父子程序就可以通過對映區域進行通訊了。(注意:一般來說,子程序單獨維護從父程序繼承而來的一些變數,而mmap()返回的地址由父子程序共同維護)【具體使用實現敬請期待博主整理】

munmap()

用於解除記憶體對映,取消引數start所指的對映記憶體的起始地址,引數length則是欲取消的記憶體大小,當程序結束或者利用exec相關函數來執行其他程式時,對映記憶體會自動解除,但關閉對應的檔案描述符時不會解除對映。

#include <sys/mman.h>

int munmap(void *addr, size_t length);

共用記憶體的使用

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

1.獲取或建立核心物件,並且制定共用記憶體的大小(系統分配物理空間是,按照頁進行分配)

int shmget(key_t key, int size, int flag);

只是建立核心物件,並申請物理空間

  • key_t key:與號誌的semget函數一樣,程式需要提供一個引數key(非0整數),它有效地為共用記憶體段命名,不同的程序通過相同的key值來存取同一塊共用記憶體
  • int size:size以位元組為單位指定需要共用的記憶體容量
  • int flag:falg是許可權標誌,它的作用與open函數的mode引數一樣,如果要想在key標識的共用記憶體不存在時,建立它的話,可以與IPC_CREAT做或操作。共用記憶體的許可權標誌與檔案的讀寫許可權一樣,舉例來說,0644,它表示允許一個程序建立的共用記憶體被記憶體建立者所擁有的程序向共用記憶體讀取和寫入資料,同時其他使用者建立的程序只能讀取共用記憶體。

返回值

  • shmget()函數成功時返回一個與key相關的共用記憶體識別符號(非負整數),用於後續的共用記憶體函數。
  • 呼叫失敗返回-1.

2.分配自己虛擬地址空間對映到共用記憶體的物理空間上

void *shmat(int shmid,const void *addr, int flag);
  • shmid:shmid是由shmget()函數返回的共用記憶體標識。
  • void *addr:addr指定共用記憶體連線到當前程序中的地址位置,通常為NULL,表示讓系統來選擇共用記憶體的地址。
  • int flag:flag是一組標誌位,通常為0。

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

3.斷開當前程序與共用記憶體的對映

不使用刪除而使用斷開的原因是因為:也許還存在其他的程序會繼續使用這塊共用記憶體

int shmdt(const void *addr);

4.操作共用記憶體的方法

int shmctl(int shmid, int cmd, struct shmid_t *buf);
  • int shmid:shmid是shmget()函數返回的共用記憶體識別符號。
  • int cmd:command是要採取的操作,它可以取下面的三個值 :

IPC_STAT:把shmid_ds結構中的資料設定為共用記憶體的當前關聯值,即用共用記憶體的當前關聯值覆蓋shmid_ds的值。

IPC_SET:如果程序有足夠的許可權,就把共用記憶體的當前關聯值設定為shmid_ds結構中給出的值

IPC_RMID:刪除共用記憶體段

  • struct shmid_t *buf:buf是一個結構指標,它指向共用記憶體模式和存取許可權的結構

因為有連線計數器,除非最後一個程序與該共用段斷開連線,則刪除該共用段。否則,並不會真正刪除該共用段,但是共用記憶體的核心物件會被立即刪除,不能使用shmat方法與該段連線。 

一個程序呼叫該方法刪除後,不會影響之前已經和該共用儲存段連線的程序

下面我們利用共用記憶體來進行一個簡單的測試:

完成下面的過程,

成功在共用記憶體中讀到了資料

 

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>

#include"sem.h"

#define READSEM 1
#define WRITESEM 0

int main()
{
	int shmid = shmget((key_t)1234,128,0664 | IPC_CREAT);
	assert(shmid != -1);

	char *ptr = (char*)shmat(shmid,NULL,0);
	assert(ptr != (char*)-1);
	
	int initVal[] = {1,0};
	int semid = SemGet(1234,intVal,2);
	assert(semid != -1);
	
	//A程序寫
	while(1)
	{
		SemP(semid,WRITESEM);
		printf("Input:");
		
		fgets(ptr,127,stdin);
		
		SemV(semid,READSEM);
		
		if(strncmp(ptr,"end",3) == 0)
		{
			break;
		}
	}
	
	shmdt(ptr);
	exit(0);
}
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>

#include"sem.h"

#define READSEM 1
#define WRITESEM 0

int main()
{
	int shmid = shmget((key_t)1234,128,0664 | IPC_CREAT);
	assert(shmid != -1);

	char *ptr = (char*)shmat(shmid,NULL,0);
	assert(ptr != (char*)-1);
	
	int initVal[] = {1,0};
	int semid = SemGet(1234,intVal,2);
	assert(semid != -1);
	
	//B程序讀
	while(1)
	{
		SemP(semid,READSEM);
		
		if(strncmp(ptr,"end",3) == 0)
		{
			break;
		}
		
		int i = 0;
		for(;i < strlen(ptr) - 1;i++)
		{
			printf("%c",toupper(ptr[i]));
			fflush(stdout);
			sleep(1);
		}
		printf("n");
		SemV(semid,WRITESEM);
	}
	
	shmdt(ptr);
	exit(0);
}

從上面的程式碼中我們可以看出: 

共用記憶體是最快的IPC,在通訊過程中少了兩次資料的拷貝。(相較於管道)

命令管理共用記憶體

  • 檢視 ipcs -m
  • 刪除 ipcrm -m shmid

總結

以上為個人經驗,希望能給大家一個參考,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com