linux共享內存機制(二)

實現進程間通信最簡單也是最直接的方法就是共享內存——爲參與通信的多個進程在內存中開闢一個共享區。由於進程可以直接對共享內存進行讀寫操作,因此這種通信方式效率特別高,但其弱點是,它沒有互斥機制,需要信號量之類的手段來配合。

共享內存原理與shm系統

共享內存,顧名思義,就是兩個或多個進程都可以訪問的同一塊內存空間,一個進程對這塊空間內容的修改可爲其他參與通信的進程所看到的。

顯然,爲了達到這個目的,就需要做兩件事:一件是在內存劃出一塊區域來作爲共享區;另一件是把這個區域映射到參與通信的各個進程空間。

通常在內存劃出一個區域的方法是,在內存中打開一個文件,若通過系統調用mmap()把這個文件所佔用的內存空間映射到參與通信的各個進程地址空間,則這些進程就都可以看到這個共享區域,進而實現進程間的通信。

爲了方便,再把mmap()的原理簡述如下:

mmap()原型如下:

void * mmap(void *start, size_t len, int prot, int flags, int fd, off_t offset);

其中,參數fd用來指定被映射的文件;offset指定映射的起始位置偏移量(通常爲0);len指定文件被映射部分的長度;start用來指定映射到虛地址空間的起始位置(通常爲NULL,即由系統確定)。

mmap是一種內存映射文件的方法,即將一個文件或者其它對象映射到進程的地址空間,實現文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對映關係。實現這樣的映射關係後,進程就可以採用指針的方式讀寫操作這一段內存,而系統會自動回寫髒頁面到對應的文件磁盤上,即完成了對文件的操作而不必再調用read,write等系統調用函數。相反,內核空間對這段區域的修改也直接反映用戶空間,從而可以實現不同進程間的文件共享。

mmap()映射過程示意圖如下所示:
在這裏插入圖片描述
那麼mmap是怎麼形成這個文件映射過程呢?

mmap本身其實是一個很簡單的操作,在進程頁表中添加一個頁表項,該頁表項是物理內存的地址。調用mmap的時候,內核會在該進程的地址空間的映射區域查找一塊滿足需求的空間用於映射該文件,然後生成該虛擬地址的頁表項,改頁表項此時的有效位(標誌是否已經在物理內存中)爲0,頁表項的內容是文件的磁盤地址,此時mmap的任務已經完成。

簡而言之,就是在進程對應的虛存段添加一個段,也就是創建一個新的vm_area_struct結構,並將其與文件的物理磁盤地址相連。在創建虛擬區間並完成地址映射,但是並沒有將任何文件數據的拷貝至主存。真正的文件讀取是當進程發起讀或寫操作時。進程的讀或寫操作訪問虛擬地址空間這一段映射地址,通過查詢頁表,引發缺頁異常,內核進行請頁。

IPC的共享內存通信方式與上面的mmap()方式極爲相似,但因爲建立一個文件的目的僅是爲了通信,於是這種文件沒有永久保存的意義,因此IPC並沒有使用正規的文件系統,而是在系統初始化時在磁盤交換區建立了一個專門用來實現共享內存的特殊臨時文件系統shm,當系統斷電後,其中的文件會全部自行銷燬。

Linux共享內存結構

Linux的一個共享內存區由多個共享段組成。用來描述共享內存段的內核數據結構shmid_kernel如下:

struct shmid_kernel /* private to the kernel */
{	
	struct kern_ipc_perm	shm_perm;        //描述進程間通信許可的結構
	struct file *		shm_file;            //指向共享內存文件的指針
	unsigned long		shm_nattch;            //掛接到本段共享內存的進程數
	unsigned long		shm_segsz;            //段大小
	time_t			shm_atim;            //最後掛接時間
	time_t			shm_dtim;            //最後解除掛接時間
	time_t			shm_ctim;            //最後變化時間
	pid_t			shm_cprid;            //創建進程的PID
	pid_t			shm_lprid;            //最後使用進程的PID
	struct user_struct	*mlock_user;
};

shmid_kernel中最重要的域是指針shm_file,它指向臨時文件file對象。當進程需要使用這個文件進行通信時,由內核負責將其映射到用戶地址空間。

爲了便於管理,內核把共享內存區的所有描述結構shmid_kernel都存放在結構ipc_id_ary中的一個數組中。結構ipc_id_ary的定義如下:

struct ipc_id_ary
{
        int size;
        struct kern_ipc_perm *p[0];            //存放段描述結構的數組
};

同樣,爲了描述一個共享內存區的概貌,內核使用了數據結構ipc_ids。該結構的定義如下:

struct ipc_ids {
	int in_use;
	unsigned short seq;
	unsigned short seq_max;
	struct rw_semaphore rw_mutex;
	struct idr ipcs_idr;
        struct ipc_id_ary *entries;        //指向struct ipc_id_ary的指針
};

由多個共享段組成的共享區的結構如下所示:
在這裏插入圖片描述

共享內存的使用

頭文件:

#include <sys/shm.h>

共享內存的打開或創建

進程可以通過調用函數shmget()來打開或創建一個共享內存區。函數shmget()內部由系統調用sys_shmget來實現。函數shmget()的原型如下:

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

其中,參數key爲用戶給定的鍵值。

所謂的鍵值,是在IPC的通信模式下每個IPC對象的名字。進程通過鍵值識別所有的對象。如果不使用鍵,進程將無法獲取IPC對象,因此IPC對象並不存在於進程本身所使用的的內存中。

因此任何進程都無法爲一塊共享內存定義一個鍵值。因此,在調用函數shmget()時,需要key設爲IPC_PRIVATE,這樣,操作系統將忽略鍵,建立一個新的共享內存,指定一個鍵值並返回這塊共享內存的IPC標識符ID,然後再設法將這個新的共享內存的標識符ID告訴其他需要使用這個共享內存區的進程。

函數中的參數size爲所申請的共享存儲段的長度(以頁爲單位)。

函數中的參數flag爲標誌,常用的有效標誌有IPC_CREAT和IPC_EXCL,它們的功能與文件打開函數open()的O_CREAT和O_EXCL相當。如果用戶希望所創建的共享內存區可讀,則需要使用標誌S_IRUSR;若可讀,則需要使用標誌S_IWUSR。

函數shmget()調用成功後,返回共享內存區的ID,否則返回-1。

Linux用shmid_ds數據結構表示每個新建的共享內存。當shmget()創建一塊新的共享內存後,返回一個可以引用該共享內存的shmid_ds數據結構的標識符。定義在include/linux/shm.h文件中的shmid_ds如下:

struct shmid_ds {
	struct ipc_perm		shm_perm;	/* operation perms */
	int			shm_segsz;	/* size of segment (bytes) */
	__kernel_time_t		shm_atime;	/* last attach time */
	__kernel_time_t		shm_dtime;	/* last detach time */
	__kernel_time_t		shm_ctime;	/* last change time */
	__kernel_ipc_pid_t	shm_cpid;	/* pid of creator */
	__kernel_ipc_pid_t	shm_lpid;	/* pid of last operator */
	unsigned short		shm_nattch;	/* no. of current attaches */
	unsigned short 		shm_unused;	/* compatibility */
	void 			*shm_unused2;	/* ditto - used by DIPC */
	void			*shm_unused3;	/* unused */
};

例如:調用函數shmget()爲當前進程創建一個共享內存區。

代碼如下:

int main(void)
{
        int shmid;
        if((shmid = shmget(IPC_PRIVATE, 10, IPC_CREAT)) < 0)
        {
                perror("shmget error!");
                exit(1);
        }else
                printf("shmget success!");
 
        return 0;
}

共享內存與進程的連接

如果一個進程已創建或打開一個共享內存,則在需要使用它時,要調用函數shmat()把該共享內存連接到進程上,即要把待使用的共享內存映射到進程空間。函數shmat()通過系統調用sys_shmat()實現。函數shmat()的原型如下:

void * shmat(int shmid, char __user * shmaddr, int shmflg);

其中,參數shmid爲共享內存的標識;參數shmaddr爲映射地址,如果該值爲0,則由內核決定;參數shmflg爲共享內存的標誌,如果shmflg的值爲SHM_RDONLY,則進程以只讀的方式訪問共享內存,否則以讀寫方式訪問共享內存。

若函數調用成功,則返回共享存儲段地址;若出錯,則返回-1。

斷開共享內存與進程的連接

調用函數shmdt()可以斷開共享內存與進程的連接,其原型如下:

int shmdt(coid * addr);

其中,參數addr爲共享存儲段的地址,即調用shmat時的返回值。shmdt將使相關shmid_ds結構中的shm_nattch計數器值減1。

共享內存的控制

調用函數shmctl()可以對共享內存進行一些控制,其原型如下:

int shmctl(int shmid, int cmd, struct shmid_ds * buf);

其中,參數shmid爲共享存儲段的ID;參數cmd爲控制命令,常用的值有IPC_STAT(賦值)、IPC_SET(賦值)、IPC_RMID(刪除)、SHM_LOCK(上鎖)、SHM_UNLOCK(解鎖)等等;參數buf爲struct shmid_ds類型指針,由buf返回的數值與命令參數cmd表示的操作相關。

共享內存不會隨着程序的結束而自動消除,要麼調用shmctl()刪除,要麼手動使用命令ipcrm -m shmid去刪除,否則一直保留在系統中,直至系統掉電。

例子:調用函數shmget()爲當前進程創建一個共享內存區並使用它。

代碼如下:

#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <sys/stat.h>
 
int main(void)
{
	int shm_id;							//定義共享內存鍵
	char* shared_memory;				//定義共享內存指針
	struct shmid_ds shmbuffer;			//定義共享內存緩衝
	int shm_size;						//定義共享內存大小
 
	shm_id = shmget(IPC_PRIVATE, 0x6400, IPC_CREAT | IPC_EXCL | S_IRUSE | S_IWUSE);		//創建一個共享內存區
	shared_memory = (char*)shmat(shm_id, 0, 0);					//綁定到共享內存
	printf("shared memory attached at address %p\n", shared_memory);
 
	shmctl(shm_id, IPC_STAT, &shmbuffer);				//讀共享內存結構struct shmid_ds
	shm_size = shmbuffer.shm_segsz;						//自結構struct shmid_ds獲取內存大小
	printf("segment size:%d\n", shm_size);
 
	sprintf(shared_memory, "Hello,world.");				//向共享內存中寫入一個字符串
	shmdt(shared_memory);							//脫離該共享內存
 
	shared_memory = (char*)shmat(shm_id, (void *)0x500000, 0);			//重新綁定共享內存
	printf("shared memory reattched at address %p\n", shared_memory);
	printf("%s\n", shared_memory);
	shmdt(shared_memory);						//脫離該共享內存
	shmctl(shm_id, IPC_RMID, 0);				//釋放共享內存
 
	return 0;
}

共享內存的互斥

從上面的敘述中可以看到,共享內存是一種低級的通信機制,它沒有提供進程間同步和互斥的功能。所以,共享內存通常是要與信號量結合使用。

原文鏈接:https://blog.csdn.net/qq_38410730/article/details/81488145?utm_medium=distribute.pc_relevant_right.none-task-blog-BlogCommendFromMachineLearnPai2-2.nonecase&depth_1-utm_source=distribute.pc_relevant_right.none-task-blog-BlogCommendFromMachineLearnPai2-2.nonecase

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章