【IPC通信--共享内存mmap】

共享内存是一种高效的进程间通信方式,可以在多个进程之间共享数据,提高程序的效率。mmap是一种常用的实现共享内存的机制,它可以将一个文件或者设备映射到内存中,使得多个进程可以通过访问这块内存来实现数据共享。

一、共享内存的概念

共享内存是一种特殊的内存区域,可以被多个进程同时访问。在传统的进程间通信方式中,如管道、消息队列、信号量等,数据需要在进程间进行复制和传递,造成了额外的开销和延迟。而共享内存机制通过将数据映射到共享区域,多个进程可以直接访问这个区域,无需通过复制来传递数据,从而提高了通信的效率。

二、mmap的原理

mmap是一种将文件或者设备映射到内存的机制,它利用了虚拟内存的特性。在Linux系统中,每个进程都有自己的虚拟内存空间,而不同进程的虚拟内存空间可以映射到相同的物理内存页面。mmap函数可以将一个文件或者设备映射到这些共享的物理内存页面上,从而实现多个进程之间的数据共享。

1、page cache及swap cache中页面的区分:一个被访问文件的物理页面都驻留在page cache或swap cache中,一个页面的所有信息由struct page来描述。struct page中有一个域为指针mapping ,它指向一个struct address_space类型结构。page cache或swap cache中的所有页面就是根据address_space结构以及一个偏移量来区分的。

2、文件与address_space结构的对应:一个具体的文件在打开后,内核会在内存中为之建立一个struct inode结构,其中的i_mapping域指向一个address_space结构。这样,一个文件就对应一个address_space结构,一个address_space与一个偏移量能够确定一个page cache 或swap cache中的一个页面。因此,当要寻址某个数据时,很容易根据给定的文件及数据在文件内的偏移量而找到相应的页面。

3、进程调用mmap()时,只是在进程空间内新增了一块相应大小的缓冲区,并设置了相应的访问标识,但并没有建立进程空间到物理页面的映射。因此,第一次访问该空间时,会引发一个缺页异常。

4、对于共享内存映射情况,缺页异常处理程序首先在swap cache中寻找目标页(符合address_space以及偏移量的物理页),如果找到,则直接返回地址;如果没有找到,则判断该页是否在交换区(swap area),如果在,则执行一个换入操作;如果上述两种情况都不满足,处理程序将分配新的物理页面,并把它插入到page cache中。进程最终将更新进程页表。

注:对于映射普通文件情况(非共享映射),缺页异常处理程序首先会在page cache中根据address_space以及数据偏移量寻找相应的页面。如果没有找到,则说明文件数据还没有读入内存,处理程序会从磁盘读入相应的页面,并返回相应地址,同时,进程页表也会更新。

5、所有进程在映射同一个共享内存区域时,情况都一样,在建立线性地址与物理地址之间的映射之后,不论进程各自的返回地址如何,实际访问的必然是同一个共享内存区域对应的物理页面。

注:一个共享内存区域可以看作是特殊文件系统shm中的一个文件,shm的安装点在交换区上。

上面涉及到了一些数据结构,围绕数据结构理解问题会容易一些。

三、mmap的使用方法

在使用mmap函数进行共享内存操作时,需要先打开一个文件或者设备,然后使用mmap函数将其映射到内存中。具体的步骤如下:

1. 打开文件或者设备:使用open函数打开一个文件或者设备,并获取文件描述符。

2. 获取文件大小:使用lseek函数将文件指针定位到文件末尾,并使用ftell函数获取文件大小。

3. 映射到内存:使用mmap函数将文件或者设备映射到内存中,并指定映射的起始地址和映射的大小。

4. 进行读写操作:多个进程可以通过访问映射的内存区域来进行数据的读写操作。

5. 解除映射:使用munmap函数解除内存的映射关系。

6. 关闭文件:使用close函数关闭文件。

mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。

注:实际上,mmap()系统调用并不是完全为了用于共享内存而设计的。它本身提供了不同于一般对普通文件的访问方式,进程可以像读写内存一样对普通文件的操作。而Posix或系统V的共享内存IPC则纯粹用于共享目的,当然mmap()实现共享内存也是其主要应用之一。

1、mmap()系统调用形式如下:

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

addr: 要映射的起始地址,通常指定为NULL,让内核自动选择
len:映射到进程地址空间的字节数
prot:映射区保护方式
flags:标志
fd:文件描述符
offset:从文件头开始的偏移量,必须是页大小的整数倍(在32位体系统结构上通常是4K)
返回值:成功返回映射到的内存区的起始地址;失败返回-1

参数fd为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd可以指定为-1,此时须指定flags参数中的MAP_ANON,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的进程间通信)。

参数addr指定文件应被映射到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成。

len是映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起。

prot 参数指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读) , PROT_WRITE (可写), PROT_EXEC (可执行), PROT_NONE(不可访问)。

flags由以下几个常值指定:MAP_SHARED , MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED , MAP_PRIVATE必选其一,而MAP_FIXED则不推荐使用。
offset参数一般设为0,表示从文件头开始映射。

MAP_SHARED 多个进程对同一个文件的映射是共享的,一个进程对映射的内存做了修改,另一个进程也会看到这种变化。

MAP_PRIVATE 多个进程对同一个文件的映射不是共享的,一个进程对映射的内存做了修改,另一个进程并不会看到这种变化,也不会真的写到文件中去。

函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。这里不再详细介绍mmap()的参数,读者可参考mmap()手册页获得进一步的信息。

内存映射文件示意图:

如果mmap成功则返回映射首地址,如果出错则返回常数MAP_FAILED。当进程终止时,该进程的映射内存会自动解除,也可以调用munmap解除映射。 

2、系统调用mmap()用于共享内存的两种方式:

(1)使用普通文件提供的内存映射:适用于任何进程之间; 此时,需要打开或创建一个文件,然后再调用mmap();典型调用代码如下:

fd=open(name, flag, mode);
if(fd<0)
...

ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0); 

通过mmap()实现共享内存的通信方式有许多特点和要注意的地方,我们将在范例中进行具体说明。

(2)使用特殊文件提供匿名内存映射:适用于具有亲缘关系的进程之间; 

由于父子进程特殊的亲缘关系,在父进程中先调用mmap(),然后调用fork()。那么在调用fork()之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap()返回的地址,这样,父子进程
就可以通过映射区域进行通信了。注意,这里不是一般的继承关系。一般来说,子进程单独维护从父进程继承下来的一些变量。而mmap()返回的地址,却由父子进程共同维护。

对于具有亲缘关系的进程实现共享内存最好的方式应该是采用匿名内存映射的方式。此时,不必指定具体的文件,只要设置相应的标志即可,参见范例2。

系统调用munmap()

int munmap( void * addr, size_t len )

该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小。当映射关系解除后,对原来映射地址的访问将导致段错误发生。

系统调用msync()

int msync ( void * addr , size_t len, int flags)

一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致。

四、mmap的优点

1. 高效:mmap共享内存可以直接在内存中进行数据访问,无需复制和传递数据,提高了通信的效率。

2. 灵活:可以将文件或者设备映射到内存中,实现不同进程之间的数据共享。

3. 方便:通过访问共享内存区域,进程可以实时地共享数据,无需等待和同步。

五、mmap的适用场景

mmap共享内存机制适用于需要高效、实时地进行大量数据交换的场景,例如多进程的服务器程序、多线程的应用程序等。它可以提高程序的性能和响应速度,减少了数据复制和传递的开销。

mmap(内存映射)机制在多种场景中都非常适用,尤其是在需要高效处理大型文件或频繁进行文件访问的情况下。以下是 mmap 机制适用的一些典型场景的详细描述:

处理大型文件:
当文件非常大,以至于无法或不方便完全加载到内存中时,mmap 显示出其优势。你可以映射整个文件,并像访问内存数组一样访问文件的任何部分,而无需加载整个文件。

高效的文件随机访问:
对于需要频繁执行随机读写操作的应用,如数据库和索引系统,mmap 提供了一种高效的访问方式。它允许直接跳到文件的任意位置进行读写,而无需使用诸如 lseek 的文件指针操作。

共享内存和进程间通信(IPC):
mmap 可以用于创建多个进程间的共享内存区域。这在需要在进程间快速共享大量数据时特别有用,例如,在某些并行计算或服务架构中。

文件映射的数据库和缓存系统:
一些数据库管理系统(DBMS)使用 mmap 来映射索引和数据文件,从而提高数据检索和写入的效率。同样,某些缓存系统也用 mmap 来存储和访问缓存数据。

内存映射的文件编辑:
在文本编辑器或类似应用中,使用 mmap 可以有效处理大文件。用户对文件所做的更改直接在内存中进行,而不必进行频繁的文件读写操作。

多媒体处理和大型数据集分析:
对于视频编辑、图像处理或科学计算等需要处理大型数据集的应用,mmap 可以减少内存占用和提高数据处理效率。

文件系统操作的优化:
在需要频繁读写大量小文件的情况下,使用 mmap 可以减少因打开和关闭文件而产生的开销,特别是在文件系统性能对应用性能有显著影响的场景中。

动态链接库(DLLs)和可执行文件的加载:
操作系统常使用 mmap 来加载共享库(如 DLLs)和可执行文件,因为这种方式能更高效地利用内存,只有实际被访问的部分才会被加载。

六、针对进程间通讯的场景权衡

在用于进程间通信(IPC)的场景中,mmap(内存映射)和共享内存各有其优势。选择使用哪一种技术取决于具体的应用需求和环境。
以下是它们各自的优势和权衡考虑:

mmap 优势:
文件持久化:

mmap 允许将文件直接映射到内存中,这意味着数据可以被持久化存储。即使进程结束,数据仍然保存在文件中。

高效的大文件处理:
对于需要处理大型文件的应用,mmap 特别有效。它允许进程仅加载需要访问的文件部分到内存,减少内存占用。

简化的数据访问:
使用 mmap 后,文件内容可以像普通的内存数组一样被访问,简化了编程模型。

系统优化:
操作系统会自动优化对映射文件的访问,如利用页面缓存等机制。

共享内存优势:
效率:

共享内存是最快的进程间通信方法之一,因为它提供了对同一内存区域的直接访问,减少了数据复制的需要。

实时性:
对于需要快速、实时交换数据的应用,共享内存提供了极低的延迟。

灵活的数据共享:
共享内存允许多个进程共享任意数据结构,而不仅限于文件内容。

无需文件系统:
共享内存不依赖于文件系统,这对于没有文件系统或磁盘访问限制的环境(如嵌入式系统)来说是一个优势。

权衡考虑
数据持久化需求:

如果需要数据持久化到磁盘,mmap 更有优势。共享内存主要用于临时数据交换。

文件大小和访问模式:
大文件或需要随机访问的文件更适合使用 mmap。共享内存适合于共享固定大小或结构化的数据。

同步机制:
在共享内存中,进程需要额外的同步机制(如信号量)来协调访问。mmap 也需要同步,但通常简单,尤其是在文件被映射为只读时。

环境和平台:
在资源有限或不支持文件系统的环境中,共享内存可能是更好的选择。相反,如果环境支持高效的文件系统操作,mmap 可能更优。

数据安全性和一致性:
考虑数据在系统崩溃或意外中断时的安全性和一致性。mmap 通过文件系统提供一定程度的数据恢复能力。

关于共享内存和文件映射(如 mmap 使用的文件映射)在实时性方面的比较,需要一些澄清。

共享内存和文件映射都提供了对同一内存区域的直接访问,但它们在底层实现和适用场景上有所不同:

共享内存:
共享内存是一种特定的内存区域,由操作系统管理,可以被多个进程共同访问。它不依赖于文件系统,且访问这块内存就像访问进程本地内存一样快速。
共享内存的实时性主要体现在它提供了一种非常直接和快速的方式来在进程之间交换数据。没有磁盘 I/O 操作的参与,数据交换几乎可以实时进行。

文件映射(mmap):
文件映射是将文件内容映射到进程的地址空间。这意味着文件的一部分(或全部)被当作内存区域来处理,从而允许程序像访问内存一样直接访问文件数据。
文件映射的实时性可能略逊于共享内存,原因是文件映射依赖于文件系统和磁盘 I/O。虽然 mmap 减少了传统的文件 I/O 需求,并且能够利用操作系统的页面缓存来提高效率,但在处理磁盘 I/O 或页面缓存时仍然存在一定的延迟。

七、mmap()范例

下面将给出使用mmap()的两个范例:范例1给出两个进程通过映射普通文件实现共享内存通信;范例2给出父子进程通过匿名映射实现共享内存。系统调用mmap()有许多有趣的地方,下面是通过mmap()映射普通文件实现进程间的通信的范例,我们通过该范例来说明mmap()实现共享内存的特点及注意事项。

简单示例代码

#include <sys/mman.h>
#include <fcntl.h>

void *map_file(const char *filepath, size_t size) {
    int fd = open(filepath, O_RDONLY);
    if (fd == -1) {
        // 处理打开文件的错误
        return NULL;
    }

    void *mapped = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
    if (mapped == MAP_FAILED) {
        // 处理映射失败的错误
        close(fd);
        return NULL;
    }

    close(fd);
    return mapped;
}

在这段代码中,我们打开了一个文件并使用 mmap 将其映射到内存。这样,我们就可以直接通过返回的指针来访问文件内容,而不需要进行传统的文件读写操作。

范例1:两个进程通过映射普通文件实现共享内存通信

范例1包含两个子程序:map_normalfile1.c及map_normalfile2.c。编译两个程序,可执行文件分别为map_normalfile1及map_normalfile2。两个程序通过命令行参数指定同一个文件来实现共享内存方式的进程间通信。map_normalfile2试图打开命令行参数指定的一个普通文件,把该文件映射到进程的地址空间,并对映射后的地址空间进行写操作。map_normalfile1把命令行参数指定
的文件映射到进程地址空间,然后对映射后的地址空间执行读操作。这样,两个进程通过命令行参数指定同一个文件来实现共享内存方式的进程间通信。

下面是两个程序代码:

/*-------------map_normalfile1.c-----------*/
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
typedef struct{
	char name[4];
	int age;
}people;
main(int argc, char** argv) // map a normal file as shared mem:
{
	int fd,i;
	people *p_map;
	char temp;

	fd=open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777);
	lseek(fd,sizeof(people)*5-1,SEEK_SET);
	write(fd,"",1);

	p_map = (people*) mmap( NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0 );close( fd );
	temp = 'a';
	for(i=0; i<10; i++)
	{
		temp += 1;
		memcpy( ( *(p_map+i) ).name, &temp,2 );
		( *(p_map+i) ).age = 20+i;
	}
	printf(" initialize over \n ");
	sleep(10);
	munmap( p_map, sizeof(people)*10 );
	printf( "umap ok \n" );
}

/*-------------map_normalfile2.c-----------*/
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
typedef struct{
	char name[4];
	int age;
}people;
main(int argc, char** argv) // map a normal file as shared mem:
{
	int fd,i;
	people *p_map;
	fd=open( argv[1],O_CREAT|O_RDWR,00777 );
	p_map = (people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
	for(i = 0;i<10;i++)
	{
		printf( "name: %s age %d;\n",(*(p_map+i)).name, (*(p_map+i)).age );
	}
	munmap( p_map,sizeof(people)*10 );
}

map_normalfile1.c首先定义了一个people数据结构,(在这里采用数据结构的方式是因为,共享内存区的数据往往是有固定格式的,这由通信的各个进程决定,采用结构的方式有普遍代表性)。map_normfile1首先打开或创建一个文件,并把文件的长度设置为5个people结构大小。然后从mmap()的返回地址开始,设置了10个people结构。然后,进程睡眠10秒钟,等待其他进程映射同一个文件,最后解除映射。

map_normfile2.c只是简单的映射一个文件,并以people数据结构的格式从mmap()返回的地址处读取10个people结构,并输出读取的值,然后解除映射。

分别把两个程序编译成可执行文件map_normalfile1和map_normalfile2后,在一个终端上先运行./map_normalfile2 /tmp/test_shm,程序输出结果如下:

initialize over
umap ok

在map_normalfile1输出initialize over 之后,输出umap ok之前,在另一个终端上运行map_normalfile2 /tmp/test_shm,将会产生如下输出(为了节省空间,输出结果为稍作整理后的结果):

name: b age 20; name: c age 21; name: d age 22; name: e age 23; name: f age 24;
name: g age 25; name: h age 26; name: I age 27; name: j age 28; name: k age 29;

在map_normalfile1 输出umap ok后,运行map_normalfile2则输出如下结果:

name: b age 20; name: c age 21; name: d age 22; name: e age 23; name: f age 24;
name: age 0; name: age 0; name: age 0; name: age 0; name: age 0;

从程序的运行结果中可以得出的结论

1、 最终被映射文件的内容的长度不会超过文件本身的初始大小,即映射不能改变文件的大小;

2、可以用于进程通信的有效地址空间大小大体上受限于被映射文件的大小,但不完全受限于文件大小。打开文件被截短为5个people结构大小,而在map_normalfile1中初始化了10个people数据结构,在恰当时候(map_normalfile1输出initialize over 之后,输出umap ok之前)调用map_normalfile2会发现map_normalfile2将输出全部10个people结构的值,后面将给出详细讨论。

注:在linux中,内存的保护是以页为基本单位的,即使被映射文件只有一个字节大小,内核也会为映射分配一个页面大小的内存。当被映射文件小于一个页面大小时,进程可以对从mmap()返回地址开始的一个页面大小进行访问,而不会出错;但是,如果对一个页面以外的地址空间进行访问,则导致错误发生,后面将进一步描述。因此,可用于进程间通信的有效地址空间大小不会超过文件大小及一个页面大小的和。

3、 文件一旦被映射后,调用mmap()的进程对返回地址的访问是对某一内存区域的访问,暂时脱离了磁盘上文件的影响。所有对mmap()返回地址空间的操作只在内存中有意义,只有在调用了munmap()后或者msync()时,才把内存中的相应内容写回磁盘文件,所写内容仍然不能超过文件的大小。

范例2:父子进程通过匿名映射实现共享内存

#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
typedef struct{
	char name[4];
	int age;
}people;
main(int argc, char** argv)
{
	int i;
	people *p_map;
	char temp;
	p_map=(people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0);
	if(fork() == 0)
	{
		sleep(2);
		for(i = 0;i<5;i++)
		printf("child read: the %d people's age is %d\n",i+1,(*(p_map+i)).age);(*p_map).age = 100;
		munmap(p_map,sizeof(people)*10); //实际上,进程终止时,会自动解除映射。
		exit();
	}
	temp = 'a';
	for(i = 0;i<5;i++)
	{
		temp += 1;
		memcpy((*(p_map+i)).name, &temp,2);
		(*(p_map+i)).age=20+i;
	}
	sleep(5);
	printf( "parent read: the first people,s age is %d\n",(*p_map).age );
	printf("umap\n");
	munmap( p_map,sizeof(people)*10 );
	printf( "umap ok\n" );
}

考察程序的输出结果,体会父子进程匿名共享内存:

child read: the 1 people's age is 20
child read: the 2 people's age is 21
child read: the 3 people's age is 22
child read: the 4 people's age is 23
child read: the 5 people's age is 24
parent read: the first people,s age is 100
umap
umap ok

 对mmap()返回地址的访问

前面对范例运行结构的讨论中已经提到,linux采用的是页式管理机制。对于用mmap()映射普通文件来说,进程会在自己的地址空间新增一块空间,空间大小由mmap()的len参数指定,注意,进程并不一定能够对全部新增空间都能进行有效访问。进程能够访问的有效地址大小取决于文件被映射部分的大小。简单的说,能够容纳文件被映射部分大小的最少页面个数决定了进程从mmap()返回的地址开始,能够有效访问的地址空间大小。超过这个空间大小,内核会根据超过的严重程度返回发送不同的信号给进程。

注意:文件被映射部分而不是整个文件决定了进程能够访问的空间大小,另外,如果指定文件的偏移部分,一定要注意为页面大小的整数倍。下面是对进程映射地址空间的访问范例:

#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
typedef struct{
	char name[4];
	int age;
}people;
main(int argc, char** argv)
{
	int fd,i;
	int pagesize,offset;
	people *p_map;

	pagesize = sysconf(_SC_PAGESIZE);
	printf("pagesize is %d\n",pagesize);
	fd = open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777);
	lseek(fd,pagesize*2-100,SEEK_SET);
	write(fd,"",1);
	offset = 0; //此处offset = 0编译成版本1;offset = pagesize编译成版本2
	p_map = (people*)mmap(NULL,pagesize*3,PROT_READ|PROT_WRITE,MAP_SHARED,fd,offset);
	close(fd);

	for(i = 1; i<10; i++)
	{
		(*(p_map+pagesize/sizeof(people)*i-2)).age = 100;
		printf("access page %d over\n",i);
		(*(p_map+pagesize/sizeof(people)*i-1)).age = 100;
		printf("access page %d edge over, now begin to access page %d\n",i, i+1);
		(*(p_map+pagesize/sizeof(people)*i)).age = 100;
		printf("access page %d over\n",i+1);
	}
	munmap(p_map,sizeof(people)*10);
}

 如程序中所注释的那样,把程序编译成两个版本,两个版本主要体现在文件被映射部分的大小不同。文件的大小介于一个页面与两个页面之间(大小为:pagesize*2-99),版本1的被映射部分是整个文件,版本2的文件被映射部分是文件大小减去一个页面后的剩余部分,不到一个页面大小(大小为:pagesize-99)。程序中试图访问每一个页面边界,两个版本都试图在进程空间中映射pagesize*3的字节数。

版本1的输出结果如下:

pagesize is 4096
access page 1 over
access page 1 edge over, now begin to access page 2
access page 2 over
access page 2 over
access page 2 edge over, now begin to access page 3
Bus error //被映射文件在进程空间中覆盖了两个页面,此时,进程试图访问第三个页面

版本2的输出结果如下:

pagesize is 4096
access page 1 over
access page 1 edge over, now begin to access page 2
Bus error //被映射文件在进程空间中覆盖了一个页面,此时,进程试图访问第二个页面

mmap_write.c

#include<string.h>
#include<stdio.h>
#include<stdlib.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<sys/types.h>
#include<unistd.h>
#include<errno.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/mman.h>

#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

typedef struct stu
{
    char name[4];
    int age;
} STU;

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        fprintf(stderr, "Usage: %s <file>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    int fd;
    fd = open(argv[1], O_CREAT | O_RDWR | O_TRUNC, 0666);
    if (fd == -1)
        ERR_EXIT("open");

    lseek(fd, sizeof(STU) * 5 - 1, SEEK_SET);
    write(fd, "", 1);

    STU *p;
    p = (STU *)mmap(NULL, sizeof(STU) * 5, PROT_READ | PROT_WRITE,
                    MAP_SHARED, fd, 0);

    if (p == -1)
        ERR_EXIT("mmap");

    char ch = 'a';
    int i;
    for (i = 0; i < 5; i++)
    {
        memcpy((p + i)->name, &ch, 1);
        (p + i)->age = 20 + i;
        ch++;
    }

    printf("initialize over\n");

    munmap(p, sizeof(STU) * 5);
    printf("exit...\n");
    return 0;
}

mmap_read.c

#include<string.h>
#include<stdio.h>
#include<stdlib.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<sys/types.h>
#include<unistd.h>
#include<errno.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/mman.h>

#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

typedef struct stu
{
    char name[4];
    int age;
} STU;

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        fprintf(stderr, "Usage: %s <file>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    int fd;
    fd = open(argv[1], O_RDWR);
    if (fd == -1)
        ERR_EXIT("open");


    STU *p;
    p = (STU *)mmap(NULL, sizeof(STU) * 5, PROT_READ | PROT_WRITE,
                    MAP_SHARED, fd, 0);

    if (p == -1)
        ERR_EXIT("mmap");

    int i;
    for (i = 0; i < 5; i++)
    {
        printf("name = %s age = %d\n", (p + i)->name, (p + i)->age);
    }
    munmap(p, sizeof(STU) * 5);
    printf("exit...\n");
    return 0;
}

 先运行mmap_write ,然后用od -c 查看文件内容:

simba@ubuntu:~/Documents/code/linux_programming/UNP/system_v$ ./mmap_write test 
initialize over
exit...
simba@ubuntu:~/Documents/code/linux_programming/UNP/system_v$ od -c test 
0000000   a  \0  \0  \0 024  \0  \0  \0   b  \0  \0  \0 025  \0  \0  \0
0000020   c  \0  \0  \0 026  \0  \0  \0   d  \0  \0  \0 027  \0  \0  \0
0000040   e  \0  \0  \0 030  \0  \0  \0
0000050

 注意od -c 输出的是八进制,024即20,即对内存的操作写入了文件。

再尝试运行mmap_read,输出如下:

simba@ubuntu:~/Documents/code/linux_programming/UNP/system_v$ ./mmap_read test 
name = a age = 20
name = b age = 21
name = c age = 22
name = d age = 23
name = e age = 24
exit...
simba@ubuntu:~/Documents/code/linux_programming/UNP/system_v$ 

再次将文件test 映射到内存,然后从内存读取到了文件的内容。

mmap 编程注意点:

1、映射不能改变文件的大小;
2、可用于进程间通信的有效地址空间不完全受限于被映射文件的大小;
3、文件一旦被映射后,所有对映射区域的访问实际上是对内存区域的访问。映射区域内容写回文件时,所写内容不能超过文件的大小;

对于1,3点,将mmap_write.c 中40行以后的代码中的5改成10,即映射的内存大于文件的大小,这样写入是不会出错的,因为是向内存写入,但用od 查看时发现文件还是40 个字节,即只有前5个STU才被真正写入到了文件。

对于第2点,将mmap_write.c 和 mmap_read.c 都按上面说的更改成10,然后在mmap_write.c 中munmap 函数之前sleep(10); 先运行mmap_write,再在另一终端运行mmap_read,观察结果:

simba@ubuntu:~/Documents/code/linux_programming/UNP/system_v$ ./mmap_read test 
name = a age = 20
name = b age = 21
name = c age = 22
name = d age = 23
name = e age = 24
name = f age = 25
name = g age = 26
name = h age = 27
name = i age = 28
name = j age = 29
exit...

即在mmap_write 对映射内存区域写入之后尚未取消映射时,mmap_read 也映射了test 文件,两个虚拟进程地址空间的映射区域都指向了同一块物理内存,所以也能读到write 进程对内存的修改,但进程结束后查看test 文件,还是40个字节而已。内存的映射是以页面为单位的,一般为4k,所以才有第2条的说法,其实这才是真正体现共享内存可以进程间通信的所在。

最后一点,与write 类似,将文件映射到内存后对内存进行写入,不一定会马上写回文件,有可能内核也会产生一个缓冲区,找个适当的时间内核再写回设备文件,write 之后可以调用fsync 进行同步,同样地,mmap 可以调用msync 进行同步。

八、mmap 系统调用和直接使用IPC共享内存之间的差异

 mmap 系统调用用于将文件映射到进程的地址空间中,而共享内存是一种不同的机制,用于进程间通信。这两种方法都用于数据共享和高效的内存访问,但它们有一些关键区别:

数据源和持久化:
mmap: 通过 mmap 映射的数据通常来自文件系统中的文件。这意味着数据是持久化的——即使程序终止,文件中的数据依然存在。当你通过映射的内存区域修改数据时,这些更改最终会反映到磁盘上的文件中。

共享内存:共享内存是一块匿名的(或者有时与特定文件关联的)内存区域,它可以被多个进程访问。与 mmap 映射的文件不同,共享内存通常是非持久的,即数据仅在计算机运行时存在,一旦系统关闭或重启,存储在共享内存中的数据就会丢失。

使用场景:
mmap:mmap 特别适合于需要频繁读写大文件的场景,因为它可以减少磁盘 I/O 操作的次数。它也允许文件的一部分被映射到内存中,这对于处理大型文件尤为有用。

共享内存:共享内存通常用于进程间通信(IPC),允许多个进程访问相同的内存区域,这样可以非常高效地在进程之间交换数据。

性能和效率:
mmap:映射文件到内存可以提高文件访问的效率,尤其是对于随机访问或频繁读写的场景。系统可以利用虚拟内存管理和页面缓存机制来优化访问。

共享内存:共享内存提供了一种非常快速的数据交换方式,因为所有的通信都在内存中进行,没有文件 I/O 操作。

同步和一致性:
mmap:使用 mmap 时,必须考虑到文件内容的同步问题。例如,使用 msync 调用来确保内存中的更改被同步到磁盘文件中。

共享内存:在共享内存的环境中,进程需要使用某种形式的同步机制(如信号量、互斥锁)来避免竞争条件和数据不一致。

简而言之,mmap 主要用于将文件映射到内存以提高文件操作的效率,而共享内存主要用于进程间的高效数据交换。二者虽有相似之处,但各自适用于不同的应用场景。

总结:

mmap共享内存机制是一种高效的进程间通信方式,可以实现多个进程之间的数据共享。它利用虚拟内存的特性,将文件或者设备映射到内存中,实现了数据的实时共享。通过使用mmap,可以提高程序的效率和响应速度,适用于需要大量数据交换的场景。

使用共享内存的IPC通信github源码:

j8267643/shadesmar: Fast C++ IPC using shared memory (github.com)

j8267643/cpp-ipc: C++ IPC Library: A high-performance inter-process communication using shared memory on Linux/Windows. (github.com)

 参考来源:

共享内存映射之mmap()函数详解_51CTO博客_内存映射mmap

深入理解内存映射:mmap映射的背后原理以及和共享内存的差异 - 知乎 (zhihu.com)

共享内存简介和mmap 函数 - AlanTu - 博客园 (cnblogs.com)

  • 17
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值