一. 传统文件访问
UNIX访问文件的传统方法是用open打开它们,如果有多个进程访问同一个文件,则每一个进程在自己的地址空间都包含有该
文件的副本,这不必要地浪费了存储空间. 下图说明了两个进程同时读一个文件的同一页的情形. 系统要将该页从磁盘读到高
速缓冲区中,每个进程再执行一个存储器内的复制操作将数据从高速缓冲区读到自己的地址空间.
二. 共享存储映射
现在考虑另一种处理方法: 进程A和进程B都将该页映射到自己的地址空间,当进程A第一次访问该页中的数据时,它生成一
个缺页中断. 内核此时读入这一页到内存并更新页表使之指向它.以后,当进程B访问同一页面而出现缺页中断时,该页已经在
内存,内核只需要将进程B的页表登记项指向次页即可. 如下图所示:
三、mmap()及其相关系统调用
mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访
问普通内存一样对文件进行访问,不必再调用read(),write()等操作。
mmap函数把一个文件或一个Posix共享内存区对象映射到调用进程的地址空间。使用该函数有三个目的:
(1)使用普通文件以提供内存映射I/O;
(2)使用特殊文件以提供匿名内存映射;
(3)使用shm_open以提供无亲缘关系进程间的Posix共享内存区。
其中addr可以指定描述符fd应被映射到的进程内空间的起始地址。它通常被指定为一个空指针,这样告诉内核自己去选择起始地址。无论哪种情况下,该函数的返回值都是描述符fd所映射到内存区的起始地址。
注意:fd指定要被映射文件的描述符,在映射该文件到一个地址空间之前,先要打开该文件。
同时,fd可以指定为-1,此时须指定flags参数中的MAP_ANON,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的进程间通信)。
len是映射到调用进程地址空间中的字节数,它从被映射文件开头起第off个字节处开始算。off通常设置为0.
内存映射区得保护由prot参数指定,它使用如下的常值。该参数的常见值是代表读写访问的PROT_READ | PROT_WRITE。
对指定映射区的prot参数指定,不能超过文件open模式访问权限。例如:若该文件是只读打开的,那么对映射存储区就不能指定PROT_WRITE。
flags使用如下的常值指定。MAP_SHARED或MAP_PRIVATE这两个标志必须指定一个,并可有选择的或上MAP_FIXED。如果指定了MAP_PRIVATE,那么调用进程被映射数据所作的修改只对该进程可见,而不改变其底层支撑对象(或者是一个文件独享,或者是一个共享内存区对象)。如果指定了MAP_SHARED,那么调用进程对被映射数据所作的修改对于共享该对象的所有进程都可见,而且确实改变了其底层支撑对象。
mmap成功返回后,fd参数可以关闭。该操作对于由mmap建立的映射关系没有影响。
为从某个进程的地址空间删除一个映射关系,我们调用munmap。
其中addr参数由mmap返回的地址,len是映射区的大小。再次访问这些地址将导致向调用进程产生一个SIGSEGV信号(当然这里假设以后的mmap调用并不重用这部分地址空间)。
如果被映射区是使用MAP_PRIVATE标志映射的,那么调用进程对它所作的变动都会被丢弃掉,即不会同步到文件中。
注意:进程终止时,或调用munmap之后,存储映射区就被自动解除映射。关闭文件描述符fd并不解除,munmap不会影响被映射的对象,在解除了映射之后,对于MAP_PRIVATE存储区的修改被丢弃。
内核的虚拟内存算法保持内存映射文件(一般在硬盘上)与内存映射区(在内存中)的同步,前提是它是一个MAP_SHARED内存区。这就是说,如果我们修改了处于内存映射到某个文件的内存区中某个位置的内容,那么内核将在稍后的某个时刻相应的更新文件。然而有时候我们希望确信硬盘上的文件内容与内存映射区中的内容一致,于是调用msync来执行这种同步。
其中addr和len参数通常指代内存中的整个内存映射区,不过也可以指定该内存区的一个子集。flags参数如下所示的各常值的组合。
MS_ASYNC和MS_SYNC这两个常值中必须指定一个,但不能都指定。他们的差别是,一旦写操作已由内核排入队列,MS_ASYNC即返回,而MS_SYNC则要等到写操作完成后才返回。如果指定了MS_INVALIDATE,那么与其最终副本不一致的文件数据的所有内存中副本都失效。后续的引用将从文件中取得数据。
为何使用mmap
到此为止就mmap的描述符间接说明了内存映射文件:我们open它之后调用mmap把它映射到调用进程地址空间的某个文件。使用内存映射文件得到的奇妙特性是,所有的I/O都在内核的掩盖下完成,我们只需编写存取内存映射区中各个值得代码。我们决不调用read,write或lseek。这么一来往往可以简化我们的代码。
然而需要了解以防误解的说明是,不是所有文件都能进行内存映射。例如,试图把一个访问终端或套接字的描述符映射到内存将导致mmap返回一个错误。这些类型的描述符必须使用read和write(或者他们的变体)来访问。
mmap的另一个用途是在无亲缘关系的进程间提供共享内存区。这种情形下,所映射文件的实际内容成了被共享内存区的初始内容,而且这些进程对该共享内存区所作的任何变动都复制回所映射的文件(以提供随文件系统的持续性)。这里假设指定了MAP_SHARED标志,它是进程间共享内存所需求的。
示例代码:
1 通过共享映射的方式修改文件
copy
2 私有映射无法修改文件
运行结果:
copy
3.两个进程中通信
进程B的代码:
运行结果:
如果进程B中的映射设置为私有映射,运行结果:
4. 匿名映射实现父子进程通信
四、对mmap地址的访问
命令行参数
16-19 命令行参数有三个,分别指定即将创建并映射到内存的文件的路径名,该文件将被设置成的大小以及内存映射区得大小。
创建,打开并截断文件;设置文件大小
22-24 待打开的文件若不存在则创建之,若已存在则把它的大小截短成0.接着把该文件的大小设置成由命令行参数指定的大小,办法是把文件读写指针移动到这个大小减去1的字节位置,然后写1个字节。
内存映射文件
25-26 使用作为最后一个命令行参数指定的大小对该文件进行内存映射。其描述符随后被关闭。
输出页面大小
28-29 使用sysconf获取系统实现的页面大小并将其输出。
读出和存入内存映射区
31-38 读出内存映射区中每个页面的首字节和尾字节,并输出他们的值。我们预期这些值全为0.同时把每个页面的这两个字节设置为1,。我们预期某个引用会最终引发一个信号,它将终止程序。当for循环结束时,我们输出下一页的首字节,并预期这会失败。
我们仍然能访问内存映射区以远部分,不过只能在边界所在的那个内存页面内(下标为5000-8191)。访问ptr[8192]将引发SIGSEGV信号,这是我们预期的。
现在我们把内存映射区大小(15000字节)指定成大于文件大小(5000字节)。
其结果与先前那个文件大小等于内存映射区大小(都是5000字节)的例子类似。本例子引发SIGBUS信号(其shell输出为"Bus Error(总线出错)"),前一个例子则引发SIGSEGV信号。两者的差别是,SIGBUS意味着我们是在内存映射区访问的,但是已超出了底层支撑对象的大小。上一个例子中的SIGSEGV则意味着我们已在内存映射区以远访问。可以看出,内核知道被映射的底层支撑对象(本例子中为文件test)的大小,即使我们访问不了该对象以远的部分(最后一页上该对象以远的那些字节除外,他们的下标为5000-8191)。
注意:
打开文件
15-17 打开一个文件,若不存在则创建之,若已存在则把它截短成大小为0.以32768字节的大小对该文件进行内存映射,尽管它当前的大小为0.
增长文件大小
19-24 通过调用ftruncate函数把文件的大小每次增长4096字节,然后取出现在是该文件最后一个字节的那个字节。
现在运行这个持续,我们看到随着文件的大小的增长,我们能通过所建立的内存映射区访问新的数据。
本例子表明,内核跟踪着被内存映射的底层支撑对象(本例子中为文件test.data)的大小,而且我们总是能访问在当前文件大小以内又在内存映射区以内的那些字节。