UNIX环境高级编程—存储映射IO(mmap函数)

前端之家收集整理的这篇文章主要介绍了UNIX环境高级编程—存储映射IO(mmap函数)前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。
http://blog.csdn.net/ctthuangcheng/article/details/9278107
共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据: 一次从输入文件到共享内存区,另一次从共享内存区到输出文件实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。

一. 传统文件访问@H_404_47@

UNIX访问文件的传统方法是用open打开它们,如果有多个进程访问同一个文件,则每一个进程在自己的地址空间都包含有该@H_404_47@

文件的副本,这不必要地浪费了存储空间. 下图说明了两个进程同时读一个文件的同一页的情形. 系统要将该页从磁盘读到高@H_404_47@

速缓冲区中,每个进程再执行一个存储器内的复制操作将数据从高速缓冲区读到自己的地址空间.@H_404_47@


@H_404_47@

@H_404_47@

二. 共享存储映射@H_404_47@

现在考虑另一种处理方法: 进程A和进程B都将该页映射到自己的地址空间,当进程A第一次访问该页中的数据时,它生成@H_404_47@

个缺页中断. 内核此时读入这一页到内存并更新页表使之指向它.以后,当进程B访问同一页面而出现缺页中断时,该页已经在@H_404_47@

内存,内核只需要将进程B的页表登记项指向次页即可. 如下图所示:@H_404_47@

@H_404_47@

三、mmap()及其相关系统调用@H_404_47@

mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访@H_404_47@

问普通内存一样对文件进行访问,不必再调用read(),write()等操作。@H_404_47@

@H_404_47@

mmap函数把一个文件或一个Posix共享内存区对象映射到调用进程的地址空间。使用该函数有三个目的:@H_404_47@

(1)使用普通文件以提供内存映射I/O;@H_404_47@

(2)使用特殊文件以提供匿名内存映射;@H_404_47@

(3)使用shm_open以提供无亲缘关系进程间的Posix共享内存区。@H_404_47@

@H_404_47@

  1. #include<sys/mman.h>
  2. void*mmap(void*addr,size_tlen,87); background-color:inherit; font-weight:bold">intprot,87); background-color:inherit; font-weight:bold">intflags,87); background-color:inherit; font-weight:bold">intfd,off_toff);

@H_404_47@

其中addr可以指定描述符fd应被映射到的进程内空间的起始地址。它通常被指定为一个空指针,这样告诉内核自己去选择起始地址。无论哪种情况下,该函数的返回值都是描述符fd所映射到内存区的起始地址。@H_404_47@

注意:fd指定要被映射文件的描述符,在映射该文件到一个地址空间之前,先要打开该文件@H_404_47@

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

len是映射到调用进程地址空间中的字节数,它从被映射文件开头起第off个字节处开始算。off通常设置为0.@H_404_47@


@H_404_47@

内存映射区得保护由prot参数指定,它使用如下的常值。该参数的常见值是代表读写访问的PROT_READ | PROT_WRITE。@H_404_47@

对指定映射区的prot参数指定,不能超过文件open模式访问权限。例如:若该文件是只读打开的,那么对映射存储区就不能指定PROT_WRITE。@H_404_47@

@H_404_47@

@H_404_47@

flags使用如下的常值指定。MAP_SHARED或MAP_PRIVATE这两个标志必须指定一个,并可有选择的或上MAP_FIXED。如果指定了MAP_PRIVATE,那么调用进程被映射数据所作的修改只对该进程可见,而不改变其底层支撑对象(或者是一个文件独享,或者是一个共享内存区对象)。如果指定了MAP_SHARED,那么调用进程对被映射数据所作的修改对于共享该对象的所有进程都可见,而且确实改变了其底层支撑对象。@H_404_47@

mmap成功返回后,fd参数可以关闭。该操作对于由mmap建立的映射关系没有影响。@H_404_47@

@H_404_47@

为从某个进程的地址空间删除一个映射关系,我们调用munmap。@H_404_47@

@H_404_47@

其中addr参数由mmap返回的地址,len是映射区的大小。再次访问这些地址将导致向调用进程产生一个SIGSEGV信号(当然这里假设以后的mmap调用并不重用这部分地址空间)。

如果被映射区是使用MAP_PRIVATE标志映射的,那么调用进程对它所作的变动都会被丢弃掉,即不会同步到文件中

注意:进程终止时,或调用munmap之后,存储映射区就被自动解除映射。关闭文件描述符fd并不解除,munmap不会影响被映射的对象,在解除了映射之后,对于MAP_PRIVATE存储区的修改被丢弃。


内核的虚拟内存算法保持内存映射文件(一般在硬盘上)与内存映射区(在内存中)的同步,前提是它是一个MAP_SHARED内存区。这就是说,如果我们修改了处于内存映射到某个文件的内存区中某个位置的内容,那么内核将在稍后的某个时刻相应的更新文件。然而有时候我们希望确信硬盘上的文件内容与内存映射区中的内容一致,于是调用msync来执行这种同步。@H_404_47@

@H_404_47@

其中addr和len参数通常指代内存中的整个内存映射区,不过也可以指定该内存区的一个子集。flags参数如下所示的各常值的组合。



MS_ASYNCMS_SYNC这两个常值中必须指定一个,但不能都指定。他们的差别是,一旦写操作已由内核排入队列,MS_ASYNC即返回,而MS_SYNC则要等到写操作完成后才返回。如果指定了MS_INVALIDATE,那么与其最终副本不一致的文件数据的所有内存中副本都失效。后续的引用将从文件中取得数据。

为何使用mmap

到此为止就mmap的描述符间接说明了内存映射文件:我们open它之后调用mmap把它映射到调用进程地址空间的某个文件。使用内存映射文件得到的奇妙特性是,所有的I/O都在内核的掩盖下完成,我们只需编写存取内存映射区中各个值得代码。我们决不调用read,write或lseek。这么一来往往可以简化我们的代码。

然而需要了解以防误解的说明是,不是所有文件都能进行内存映射。例如,试图把一个访问终端或套接字的描述符映射到内存将导致mmap返回一个错误。这些类型的描述符必须使用read和write(或者他们的变体)来访问。

mmap的另一个用途是在无亲缘关系的进程间提供共享内存区。这种情形下,所映射文件的实际内容成了被共享内存区的初始内容,而且这些进程对该共享内存区所作的任何变动都复制回所映射的文件(以提供随文件系统的持续性)。这里假设指定了MAP_SHARED标志,它是进程间共享内存所需求的。


示例代码:

1 通过共享映射的方式修改文件

注释掉44-46与没有注释,运行结果都为:

copy

huangcheng@ubuntu:~$catdata.txt
  • aaaaaaaaaaaa
  • bbbbbbbbbbbb
  • cccccccccccc
  • dddddddddddd
  • eeeeeeeeeeee
  • ffffffffffff
  • huangcheng@ubuntu:~$./a.outdata.txt
  • aaaaaaaaaaaa
  • bbbbbbbbbbbb
  • cccccccccccc
  • dddddddddddd
  • eeeeeeeeeeee
  • ffffffffffff
  • huangcheng@ubuntu:~$catdata.txt
  • bbbbbbb9bbbb
  • ffffffffffff

  • 2 私有映射无法修改文件

    运行结果:

    copy

    ffffffffffff

    3.两个进程中通信
    两个程序映射同一个文件到自己的地址空间,进程A先运行,每隔两秒读取映射区域,看是否发生变化.进程B后运行,它修改映射区域,然后推出,此时进程A能够观察到存储映射区的变化。
    进程A的代码:

    mmap将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。
    mmap()必须以PAGE_SIZE()为单位进行映射,而内存也只能以页为单位进行映射, 若要映射非PAGE_SIZE整数倍的地址范围,要先进行内存对齐,强行以PAGE_SIZE的倍数大小进行映射
    mmap操作提供了一种机制,让用户程序直接访问设备内存,这种机制,相比较在用户空间和内核空间互相拷贝数据,效率更高。在要求高性能的应用中比较常用。mmap映射内存必须是页面大小的整数倍,面向流的设备不能进行mmap,mmap的实现和硬件有关。
    内存映射一个普通文件时,内存中映射区的大小(mmap的第二个参数)通常等于该文件的大小,然而文件的大小和内存的映射区大小可以不同。
    我们要展示的第一种情形的前提是:文件大小等于内存映射区大小,但这个大小不是页面大小的倍数。
    1. #include<stdio.h>
    2. #include<stdlib.h>
    3. #include<sys/stat.h>
    4. #include<fcntl.h>
    5. #include<sys/types.h>
    6. #include<unistd.h>
    7. #include<sys/mman.h>
    8. #definemax(A,B)(((A)>(B))?(A):(B))
    9. char**argv)
    10. {
    11. char*ptr;
    12. size_tfilesize,mmapsize,pagesize;
    13. if(argc!=4)
    14. printf("usage:tes1<pathname><filesname><mmapsize>\n");
    15. filesize=atoi(argv[2]);
    16. mmapsize=atoi(argv[3]);
    17. /*openfile:createortruncate;setfilesize*/
    18. fd=open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0777);
    19. lseek(fd,filesize-1,SEEK_SET);
    20. write(fd,"",1);
    21. ptr=mmap(NULL,0);
    22. close(fd);
    23. pagesize=sysconf(_SC_PAGESIZE);
    24. printf("PAGESIZE=%ld\n",(long)pagesize);
    25. for(i=0;i<max(filesize,mmapsize);i+=pagesize)
    26. {
    27. printf("ptr[%d]=%d\n",i,ptr[i]);
    28. ptr[i]=1;
    29. ptr[i+pagesize-1]=1;
    30. }
    31. printf("ptr[%d]=%d\n",ptr[i]);
    32. exit(0);
    33. }

    命令行参数
    16-19 命令行参数有三个,分别指定即将创建并映射到内存的文件的路径名,该文件将被设置成的大小以及内存映射区得大小。@H_404_47@

    创建,打开并截断文件;设置文件大小
    22-24 待打开的文件若不存在则创建之,若已存在则把它的大小截短成0.接着把该文件的大小设置成由命令行参数指定的大小,办法是把文件读写指针移动到这个大小减去1的字节位置,然后写1个字节。@H_404_47@

    内存映射文件
    25-26 使用作为最后一个命令行参数指定的大小对该文件进行内存映射。其描述符随后被关闭。@H_404_47@

    输出页面大小
    28-29 使用sysconf获取系统实现的页面大小并将其输出。@H_404_47@

    读出和存入内存映射区
    31-38 读出内存映射区中每个页面的首字节和尾字节,并输出他们的值。我们预期这些值全为0.同时把每个页面的这两个字节设置为1,。我们预期某个引用会最终引发一个信号,它将终止程序。当for循环结束时,我们输出下一页的首字节,并预期这会失败。@H_404_47@

    运行结果:

    注意:
    当文件长度为5000字节,映射1000字节时,但是1000字节不是PAGE_SIZE(4096)的整数倍,这样会强制映射为PAGE_SIZE的整数倍,现在映射的是4096字节。所以对映射区里面的第4096字节进行修改为1,会同步到文件中。

    下面的程序展示了处理一个持续增长的文件的一种常用技巧:指定一个大于该文件大小的内存映射区大小,跟踪该文件的当前大小(以确保不访问当前文件尾以远的部分),然后就让该文件的大小随着往其中每次写入数据而增长。

    打开文件
    15-17 打开一个文件,若不存在则创建之,若已存在则把它截短成大小为0.以32768字节的大小对该文件进行内存映射,尽管它当前的大小为0.

    增长文件大小
    19-24 通过调用ftruncate函数把文件的大小每次增长4096字节,然后取出现在是该文件最后一个字节的那个字节。

    现在运行这个持续,我们看到随着文件的大小的增长,我们能通过所建立的内存映射区访问新的数据。

    copy
    huangcheng@ubuntu:~$ls-ltest.data
  • ls:无法访问test.data:没有那个文件或目录
  • huangcheng@ubuntu:~$./a.out
  • settingfilesizeto4096
  • ptr[4095]=0
  • settingfilesizeto8192
  • ptr[8191]=0
  • settingfilesizeto12288
  • ptr[12287]=0
  • settingfilesizeto16384
  • ptr[16383]=0
  • settingfilesizeto20480
  • ptr[20479]=0
  • settingfilesizeto24576
  • ptr[24575]=0
  • settingfilesizeto28672
  • ptr[28671]=0
  • settingfilesizeto32768
  • ptr[32767]=0
  • huangcheng@ubuntu:~$ls-ltest.data
  • -rwxr-xr-x1huangchenghuangcheng327682013-07-0916:47test.data

  • 本例子表明,内核跟踪着被内存映射的底层支撑对象(本例子中为文件test.data)的大小,而且我们总是能访问在当前文件大小以内又在内存映射区以内的那些字节。

    猜你在找的Bash相关文章