《Linux中C库mmap()函数详解》
介绍:
Linux的mmap系统调用(libc封装了同名函数)可以分配匿名虚拟内存区域,也可以将文件映射到内存。
mmap()必须以单元为单位进行映射,而内存只能以页为单位进行映射。 如果要映射不是整数倍的地址范围,必须首先执行内存对齐并强制映射到大小的倍数。
mmap操作提供了一种允许用户程序直接访问设备内存的机制。 这种机制比在用户空间和内核空间之间复制数据更有效。 它通常用于需要高性能的应用中。
面向流的设备无法执行 mmap。 mmap的实现与硬件有关。
函数定义:
#
void *mmap(void *start, , int prot, int flags, int fd, off_t );
int(无效*开始,);
函数形参:
start:映射区的起始地址。 当设置为0时,表示系统确定映射区域的起始地址。
:映射区域的长度。
prot:所需的内存保护标志,不能与文件的打开方式冲突。为以下值之一,可以通过或操作(“|”)合理组合
//可以执行页面内容
//可以读取页面内容
//页面可以写入
//页面不可访问
flags:指定映射对象的类型,映射选项和映射页是否可以共享。其值可以是以下一位或多位的组合
//使用指定的映射起始地址。 如果start和len参数指定的内存区域与现有的映射空间重叠,则重叠部分将被丢弃。 如果指定的起始地址不可用,则操作将失败。
//并且起始地址必须落在页边界上。
//与映射该对象的所有其他进程共享映射空间。 写入共享区域相当于输出到文件。 在调用 msync() 或 () 之前,文件实际上并未更新。
//创建一个写时复制的私有映射。 写入内存区域不会影响原文件。 该标志与上述标志是互斥的,只能使用其中之一。
//该标志被忽略。
//同上
//不为此映射保留交换空间。 当交换空间被保留时,保证了修改映射区域的可能性。 当未保留交换空间且内存不足时,对映射区域的修改将导致段违规信号。
//锁定映射区域的页面,防止页面被交换出内存。
// 用于堆栈告诉内核VM系统映射区域可以向下扩展。
//匿名映射,映射区域不与任何文件关联。
// // 的别名,不再使用。
//兼容性标志,忽略。
//将映射区域放置在进程地址空间的低2GB中。 指定后将被忽略。 目前此标志仅在 x86-64 平台上受支持。
//通过预读为文件映射准备页表。 对映射区域的后续访问不会因页面违规而被阻止。
//仅在与 一起使用时才有意义。 不执行预读,仅为内存中已存在的页面创建页表条目。
fd:有效的文件描述符。 一般由open()函数返回,其值也可以设置为-1。 此时需要指定flags参数,表示进行匿名映射。
:映射对象内容的起点。
函数返回值:
执行成功时,mmap()返回映射区域的指针,()返回0。失败时,mmap()返回[其值为(void *)-1]并返回-1。
errno 设置为以下值之一:
:访问错误
:文件被锁定,或者锁定的内存过多
EBADF:fd 不是有效的文件描述符
: 一个或多个参数无效
:已达到打开文件的系统限制
: 指定文件所在的文件系统不支持内存映射。
:内存不足,或者进程超出了内存映射的最大数量
EPERM:电量不足,不允许操作
:以写入模式打开文件并指定标志
:尝试写入只读区域
:尝试访问不属于该进程的内存区域
系统调用:
mmap() 系统调用允许进程通过映射相同的普通文件来共享内存。 将普通文件映射到进程地址空间后,进程就可以像访问普通内存一样访问该文件,而无需调用read()、write()等操作。
注意:事实上,mmap() 系统调用并不完全设计用于共享内存。 它本身提供了一种与普通文件不同的访问方法。 该进程可以像读写内存一样对普通文件进行操作。
Posix 或共享内存 IPC 纯粹用于共享目的。 当然,共享内存的mmap()实现也是它的主要应用之一。
系统调用 mmap() 有两种方式使用来共享内存:
(1)使用普通文件提供的内存映射:适合任何进程; 这时,需要打开或创建一个文件,然后调用mmap(); 典型的调用代码如下:
fd=open(名称、标志、模式);
如果(FD ...
ptr=mmap(NULL, len, |, , fd, 0);
通过mmap()进行共享内存的通信方式有很多特点和需要注意的地方,我们将在例子中详细解释。
(2)使用特殊文件提供匿名内存映射:适合有亲和关系的进程; 由于父子进程之间特殊的亲和关系,父进程中首先调用mmap(),然后再调用fork()。
那么子进程调用fork()后,就继承了父进程的匿名映射的地址空间,同时也继承了mmap()返回的地址。 这样父子进程就可以通过映射区域进行通信了。
注意,这不是一般的继承关系。 一般来说,子进程独立维护一些从父进程继承的变量。 mmap()返回的地址由父进程和子进程共同维护。
在相关进程之间实现共享内存的最佳方法是使用匿名内存映射。 此时,不需要指定特定的文件,只需设置适当的标志即可。
int(无效*地址,长度)
该调用释放进程地址空间中的映射关系。 addr是调用mmap()时返回的地址,len是映射区域的大小。 当映射关系被释放后,访问原来的映射地址会导致分段错误。
int msync ( void * addr , len, int 标志)
一般来说,进程对映射空间中的共享内容所做的更改不会直接写回磁盘文件,而往往是在调用()之后进行操作。 通过调用msync()可以使磁盘上的文件内容与共享内存区域的内容一致。
程序示例:
下面给出了使用 mmap() 的两个示例。 例1是两个进程通过映射普通文件实现共享内存通信,例2是一个父子进程通过匿名映射实现共享内存。
关于 mmap() 系统调用有很多有趣的事情。 下面是通过mmap()映射普通文件实现进程间通信的例子。 我们通过这个例子来讲解一下mmap()实现共享内存的特点和注意事项。
例1:两个进程通过映射普通文件实现共享内存通信
示例 1 包含两个子例程:.c 和 .c。 编译两个程序,可执行文件分别为 和 。
两个程序通过命令行参数指定同一个文件,实现共享内存中的进程间通信。 尝试打开命令行参数指定的普通文件,将文件映射到进程的地址空间,并对映射的地址空间执行写操作。
将命令行参数指定的文件映射到进程地址空间,然后对映射的地址空间执行读操作。 这样,两个进程通过命令行参数指定同一个文件,以共享内存的形式实现进程间通信。
下面是两个程序代码:
/* - - - - - - -。C - - - - - -*/
#
#
#
#
{
字符名称[4];
年龄;
};
main(int argc, char** argv) // 将文件映射为 mem:
int fd,i;
*p_map;
炭温度;
fd = open(argv[1],||,00777);
如果(fd<0)
(“打开错误\n”);
退出(1);
lseek(fd,()*5-1,);
写(fd,"",1);
p_map = (*) mmap( NULL,()*10,|,,fd,0 );
关闭(fd);
温度='a';
为(i = 0;我{
温度+=1;
( ( *(p_map+i) ).name, &temp,2 );
( *(p_map+i) ). 年龄 = 20+i;
(“超过\n”);
睡觉(10);
(p_map, ()*10);
(“umap 确定\n”);
/* - - - - - - -。C - - - - - -*/
#
#
#
#
{
字符名称[4];
年龄;
};
main(int argc, char** argv) // 将文件映射为 mem:
int fd,i;
*p_map;
fd=open(argv[1],|,00777);
p_map = (*)mmap(NULL,()*10,|,,fd,0);
为(i = 0;i {
( "姓名: %s 年龄 %d;\n",(*(p_map+i)).name, (*(p_map+i)).age );
(p_map,()*10);
.c首先定义了一个数据结构(这里使用该数据结构是因为共享内存区的数据往往有固定的格式,是由各个通信进程决定的,该结构具有普遍代表性)。
打开或创建一个文件并将文件的长度设置为 5 个结构大小。 然后从mmap()的返回地址开始,设置了10个结构体。
然后该进程休眠 10 秒,等待其他进程映射同一文件,最后取消映射。
.c 简单地映射一个文件,从 mmap() 返回的地址中读取 10 个数据结构格式的结构,输出读取的值,然后取消映射。
将两个程序编译成可执行文件后,在终端上运行./ /tmp/,程序输出如下:
超过
乌玛普 好的
在输出 over 之后和输出 umap ok 之前,在另一个终端上运行 /tmp/ 将产生以下输出(为了节省空间,输出结果略有整理):
姓名:b 年龄20; 姓名:c 年龄 21; 姓名:d 年龄 22; 姓名:e 年龄 23; 姓名:f 年龄 24;
姓名:g 年龄25; 姓名:h 年龄 26; 姓名:本人27岁; 姓名:j 年龄 28; 姓名:k 年龄 29;
输出umap ok后,运行时输出如下结果:
姓名:b 年龄20; 姓名:c 年龄 21; 姓名:d 年龄 22; 姓名:e 年龄 23; 姓名:f 年龄 24;
姓名:年龄0; 姓名:年龄0; 姓名:年龄0; 姓名:年龄0; 姓名:年龄0;
从程序结果中可以得出的结论:
1、最终映射的文件内容的长度不会超过文件本身的初始大小,即映射不能改变文件的大小;
2、可用于进程通信的有效地址空间大小一般受映射文件大小的限制,但并不完全受文件大小的限制。
打开的文件被截断为5个结构体大小,初始化了10个数据结构体,在适当的时候调用(输出over之后,输出umap之前ok)
你会发现10个结构体的值都会输出,后面会详细讨论。
注意:在Linux中,内存保护是基于页的。 即使映射文件只有一个字节大小,内核也会为映射分配一页大小的内存。
当映射文件小于一页大小时,进程可以从mmap()返回地址开始访问一页大小,不会出错;
然而,访问页面外部的地址空间会导致错误,如下文进一步描述。 因此,可用于进程间通信的有效地址空间大小不会超过文件大小加上一页大小。
3、文件一旦映射完毕,调用mmap()的进程对返回地址的访问就是对某块内存区域的访问,暂时脱离了磁盘上文件的影响。 mmap() 返回的地址空间上的所有操作仅在内存中有意义。
只有调用()之后或者调用msync()时,才会将内存中相应的内容写回磁盘文件,并且写入的内容仍然不能超过文件的大小。