Linux之進程間通信(共享內存【mmap實現+系統V】)

共享內存

  • 共享內存可以說是最有用的進程間通信方式,也是最快的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

總結

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: