michile

导航

映射内存

映射内存提供了一种使多个进程通过一个共享文件进行通信的机制。尽管可以将映射内
存想象为一个有名字的共享内存,你始终应当记住两者之间有技术层面的区别。映射内存既
可以用于进程间通信,也可以作为一种访问文件内容的简单方法
映射内存在一个文件和一块进程地址空间之间建立了联系。Linux 将文件分割成内存分
页大小的块并复制到虚拟内存中,因此进程可以在自己的地址空间中直接访问文件内容。
样,进程就可以以读取普通内存空间的方法来访问文件的内容,也可以通过写入内存地址来
修改文件的内容。这是一种方便的访问文件的方法。
你可以将映射内存想象成这样的操作:分配一个足够容纳整个文件内容的缓存,将全部
文件内容读入缓存,并且(当缓存内容被修改过后)最后将缓存写回文件。Linux 替你完成
文件读写的操作。
除了用于进程间通信,还有其它一些情况会使用映射内存。其中一些用途在 5.3.5 节
“mmap 的其它用途”中进行了讨论。
5.3.1 映射一个普通文件
要将一个普通文件映射到内存空间,应使用 mmap(映射内存,“Memory MAPped”,读
作“em-map”)。函数 mmap 的第一个参数指明了你希望Linux 将文件映射在进程地址空间中
的位置;传递 NULL 指针允许Linux 系统自动选择起始地址。第二个参数是映射内存块的长
度,以字节为单位。第三个参数指定了对被映射内存区域的保护,由 PROT_READ、
PROT_WRITE 和PROT_EXEC三个标志位按位与操作得到。三个值分别标识读、写和执
行权限。第四个参数是一个用于指明额外选项的标志值。第五个参数应传递一个已经打开的、
指向被映射文件的句柄。最后一个参数指明了文件中被映射区域相对于文件开始位置的偏移
量。通过选择适当的开始位置和偏移量,你可以选择将文件的全部内容或某个特定部分映射
到内存中。
标志值可以由以下常量进行按位或操作进行组合得到:
· MAP_FIXED——如果你指定这个标志,Linux 会强制使用你提供的地址进行映射,
而不只是将该地址作为一个对映射地址的参考进行选择。该地址必须按内存分页边
界对齐。
· MAP_PRIVATE——对映射区域内存的写操作不会直接导致对被绑定文件的修改,

而是修改该进程持有的一份该文件的私有副本。其它进程不会发现这些写操作。这
个模式不能与MAP_SHARED 同时使用。
· MAP_SHARED——对内存的写入操作会立刻反应在被映射的文件中,而不会被系
统缓冲。将映射内存作为一种 IPC 手段时应使用这个标志。这个模式不能与
MAP_PRIVATE同时使用。
如果调用成功,mmap 会返回一个指向被映射内存区域的起点的指针。如果调用失败则
返回常量MAP_FAILED。
当你不再使用一块映射内存的时候应调用munmap 进行释放。将被映射内存区域的开始
地址和内存块的长度作为参数调用这个函数。Linux 会在进程结束的时候自动释放进程中映
射的内存区域。
5.3.2 示例程序
让我们看两个利用映射内存对文件进行读写的程序。列表 5.5中的第一个程序会产生一
个随机数并写入一个映射到内存的文件中。列表 5.6中的第二个程序则会从文件中读取并输
出这个值,然后将该值的两倍写回到文件中。两个程序均接受一个指明被映射文件的参数。

代码列表 5.5 (mmap-write.c)将一个随即数写入内存映射文件

#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#define FILE_LENGTH 0x100

/* 在从 low 到 high 的范围中返回一个平均分布的随机数 */

int random_range (unsigned const low, unsigned const high)
{
unsigned const range = high - low + 1;
return low + (int) (((double) range) * rand() / (RAND_MAX + 1.0));
}

int man (int argc, char* const argv[])
{
int fd;
void* file_memory;

/* 为随机数发生器提供种子 */
srand (time (NULL));

/* 准备一个文件使之长度足以容纳一个无符号整数 */

fd = open (argv[1], O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
lseek (fd, FILE_LENGTH+1, SEEK_SET);
write (fd, "", 1);
lseek (fd, 0, SEEK_SET);

/* 创建映射内存 */
file_memory = mmap (0, FILE_LENGTH, PROT_WRITE, MAP_SHARED, fd,
0);
close (fd);
/* 将一个随机整数写入映射的内存区域 */
sprintf((char*) file_memory, "%d\n", random_range (-100, 100));
/* 释放内存块(不是必需,因为程序即将退出而映射内存将被自动释放) */
munmap (file_memory, FILE_LENGTH);

return 0;
}


上面的mmap-write 程序打开了一个指定的文件(如果不存在则创建它)。传递给 open
的第二个参数表明以读写模式创建文件。(译者注:原文为第三个参数,疑为笔误。)因为我
们不知道文件的长度,我们利用lseek 确保文件具有足够容纳一个整数的长度,然后将游
标移动到文件的开始位置。
程序在将文件映射到内存之后随即关闭了文件描述符,因为我们不再需要通过这个描述
符操作文件。随后程序将一个随机整数写入映射内存,从而也写入了文件内容本身;之后程
序取消了内存映射。对 munmap 的调用不是必须的,因为 Linux 会在程序结束的时候自动取
消全部内存映射。

代码列表 5.6 (mmap-read.c)从文件映射内存中读取一个整数,然后将其倍增

#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#define FILE_LENGTH 0x100

int main (int argc, char* const argv[])
{
int fd;
void* file_memory;
int integer;

/* 打开文件 */

fd = open (argv[1], O_RDWR, S_IRUSR | S_IWUSR);
/* 创建映射内存 */
file_memory = mmap (0, FILE_LENGTH, PROT_READ | PROT_WRITE,MAP_SHARED, fd, 0); 

close (fd);

/* 读取整数,输出,然后将其倍增 */
sscanf (file_memory, "%d", %integer);
printf ("value: %d\n", integer);
sprintf ((char*) file_memory, "%d\n", 2 * integer);
/* 释放内存(非必须,因为程序就此结束)*/
munmap (file_memory, FILE_LENGTH);

return 0;
}

上面的mmap-read 程序从文件中读取一个整数值,将其倍增并写回到文件中。首先它
以读写模式打开文件。因为我们确信文件足以容纳一个整数,我们不必像前面的程序那样使
用lseek 。程序从内存中用sscanf 读取这个值,然后用sprintf将值写回内存中。
这里是某次运行这个程序的结果。它将文件/tmp/integer-file映射到内存。

************************************
% ./mmap-write  /tmp/integer-file
% cat /tmp/integer-file
42
% ./mmap-read /tmp/integer/file
value: 42
% cat /tmp/integer-file
84
*****************************************
我们可以看到,程序并没有调用write 就将数字写入了文件,同样也没有用read 就
将数字读回。注意,仅出于演示的考虑,我们将数字转换为字符串进行读写(通过使用
sprintf和sscanf)——一个内存映射文件的内容并不要求为文本格式。你可以在其中存
取任意二进制数据。
5.3.3 对文件的共享访问
不同进程可以将同一个文件映射到内存中,并借此进行通信。通过指定MAP_SHARED
标志,所有对映射内存的写操作都会直接作用于底层文件并且对其它进程可见。如果不指定
这个标志,Linux 可能在将修改写入文件之前进行缓存。
除了使用MAP_SHARED 标志,你也可以通过调用 msync 强制要求Linux 将缓存的内
容写入文件。它的前两个参数与 munmap 相同,用于指明一个映射内存块。第三个参数可以
接受如下标志位:
· MS_ASYNC——计划一次更新,但是这次更新未必在调用返回之前完成。
· MS_SYNC——立刻执行更新;msync 调用会导致进程阻塞直到更新完成。
MS_SYNC 和 MS_ASYNC 不能同时使用。
· MS_INVALIDATE——其它所有进程对这个文件的映射都会失效,但是??它们可以(原文为因此   不懂这关系。。

看到被修改过的值。

例如,要更新一块从mem_addr 开始的、长度为mem_length 的共享内存块需要使用
如下调用:
msync (mem_addr, mem_length, MS_SYNC | MS_INVALIDATE);

与使用共享内存一样,使用文件映射内存的程序之间必须遵循一定的协议以避免竞争状
态的发生。例如,可以通过一个信号量协调多个进程一块内存映射文件的并发访问。除此之
外你还可以使用第八章8.3 节“fcntl :锁定与其它文件操作”中介绍的方法对文件进行读
写锁定。
5.3.4 私有映射
在mmap 中指定MAP_PRIVATE 可以创建一个写时复制(copy-on-write )区域。所有对
映射区域内存内容的修改都仅反映在当前程序的地址空间中;其它进程即使映射了同一个文
件也不会看到这些变化。与普通情况下直接写入所有进程共享的页面中的行为不同,指定
MAP_PRIVATE 进行映射的进程只将改变写入一份私有副本中。该进程随后执行的所有读写
操作都针对这个副本进行。
5.3.5 mmap 的其它用途。
系统调用mmap 还可以用于除进程间通信之外的其它用途。一个常见的用途就是取代
read 和write 。例如,要读取一个文件的内容,程序可以不再显式地读取文件并复制到内
存中,而是将文件映射到地址空间然后通过内存读写操作来操作文件内容。对于一些程序而
言这样更方便,也可能具有更高的效率。
许多程序都使用了这样一个非常强大的高级技巧:将某种数据结构(例如各种 struct
结构体的实例)直接建立在映射内存区域中。在下次调用过程中,程序将这个文件映射回内
存中,此时这些数据结构都会恢复到之前的状态。不过需要注意的是,这些数据结构中的指
针都会失效,除非这些指针都指向这个内存区域内部并且这个内存区域被特意映射到与之前
一次映射位置完全相同的地址。
另一个相当有用的技巧是将设备文件/dev/zero 映射到内存中。这个文件,将在第六
章“设备” 6.5.2 节“/dev/zero”中介绍到的,将自己表现为一个无限长且内容全部为 0
字节的文件。对/dev/zero 执行的写入操作将被丢弃,因此由它映射的内存区域可以用作
任何用途。自定义的内存分配过程经常通过映射/dev/zero 以获取整块经过初始化的内存。

 

 

posted on 2013-02-07 10:09  michile  阅读(1257)  评论(0编辑  收藏  举报