加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 综合聚焦 > 服务器 > 安全 > 正文

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

发布时间:2020-12-15 16:57:06 所属栏目:安全 来源:网络整理
导读:http://blog.csdn.net/ctthuangcheng/article/details/9278107 共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式,因为进程可以直接读写内存,而不需要任何 数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷
http://blog.csdn.net/ctthuangcheng/article/details/9278107
共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据: 一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。

一. 传统文件访问

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

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

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


二. 共享存储映射

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

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

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


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

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

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

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

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

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

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

[cpp] view plain copy
print ?
  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);

其中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。

copy
intmunmap(void*addr,szie_tlen);

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

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

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


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

copy
intmsync(void*addr,87); background-color:inherit; font-weight:bold">intflags);
其中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 通过共享映射的方式修改文件

copy
#include<sys/stat.h>
  • #include<fcntl.h>
  • #include<stdio.h>
  • #include<stdlib.h>
  • #include<unistd.h>
  • #include<error.h>
  • #defineBUF_SIZE100
  • intmain(intargc,87); background-color:inherit; font-weight:bold">char**argv)
  • {
  • structstatsb;
  • char*mapped,buf[BUF_SIZE];
  • for(i=0;i<BUF_SIZE;i++){
  • buf[i]='#';
  • }
  • /*打开文件*/
  • if((fd=open(argv[1],O_RDWR))<0){
  • perror("open");
  • }
  • /*获取文件的属性*/
  • if((fstat(fd,&sb))==-1){
  • perror("fstat");
  • /*将文件映射至进程的地址空间*/
  • if((mapped=(char*)mmap(NULL,sb.st_size,PROT_READ|
  • PROT_WRITE,MAP_SHARED,fd,0))==(void*)-1){
  • perror("mmap");
  • /*映射完后,关闭文件也可以操纵内存*/
  • close(fd);
  • printf("%s",mapped);
  • /*修改一个字符,同步到磁盘文件*/
  • mapped[20]='9';
  • if((msync((void*)mapped,MS_SYNC))==-1){
  • perror("msync");
  • /*释放存储映射区*/
  • if((munmap((void*)mapped,sb.st_size))==-1){
  • perror("munmap");
  • return0;
  • }
  • 注释掉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
    /*将文件映射至进程的地址空间,这里是私有映射*/
  • }

  • 运行结果:

    copy

    ffffffffffff

    3.两个进程中通信
    两个程序映射同一个文件到自己的地址空间,进程A先运行,每隔两秒读取映射区域,看是否发生变化.进程B后运行,它修改映射区域,然后推出,此时进程A能够观察到存储映射区的变化。
    进程A的代码:
    copy
    /*文件已在内存,0); background-color:inherit">/*每隔两秒查看存储映射区是否被修改*/
  • while(1){
  • printf("%sn",226); color:inherit; line-height:18px; list-style-position:outside!important"> sleep(2);
  • return0;
  • }

  • 进程B的代码:
    copy
    /*修改一个字符*/
  • }

  • 运行结果:
    copy
    huangcheng@ubuntu:~$./adata.txt
  • bbbbbbb9bbbb
  • ............

  • 如果进程B中的映射设置为私有映射,运行结果:
    copy
    ............

    4. 匿名映射实现父子进程通信

    copy
    #defineBUF_SIZE100
  • char**argv)
  • {
  • char*p_map;
  • /*匿名映射,创建一块内存供父子进程通信*/
  • p_map=( MAP_SHARED|MAP_ANONYMOUS,-1,0);
  • if(fork()==0){
  • sleep(1);
  • printf("childgotamessage:%sn",p_map);
  • sprintf(p_map,"%s","hi,dad,thisisson");
  • munmap(p_map,BUF_SIZE);//实际上,进程终止时,会自动解除映射。
  • exit(0);
  • sprintf(p_map,thisisfather");
  • printf("parentgotamessage:%sn",226); color:inherit; line-height:18px; list-style-position:outside!important"> }


  • 四、对mmap地址的访问
    copy
    void*mmap(void*start,87); background-color:inherit; font-weight:bold">size_tlength,off_toffset);
  • intmunmap(void*start,87); background-color:inherit; font-weight:bold">size_tlength);

  • mmap将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。
    mmap()必须以PAGE_SIZE()为单位进行映射,而内存也只能以页为单位进行映射, 若要映射非PAGE_SIZE整数倍的地址范围,要先进行内存对齐,强行以PAGE_SIZE的倍数大小进行映射
    mmap操作提供了一种机制,让用户程序直接访问设备内存,这种机制,相比较在用户空间和内核空间互相拷贝数据,效率更高。在要求高性能的应用中比较常用。mmap映射内存必须是页面大小的整数倍,面向流的设备不能进行mmap,mmap的实现和硬件有关。
    内存映射一个普通文件时,内存中映射区的大小(mmap的第二个参数)通常等于该文件的大小,然而文件的大小和内存的映射区大小可以不同。
    我们要展示的第一种情形的前提是:文件大小等于内存映射区大小,但这个大小不是页面大小的倍数。
    [cpp] view plain copy
    print ?
    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=%ldn",(long)pagesize);
    25. for(i=0;i<max(filesize,mmapsize);i+=pagesize)
    26. {
    27. printf("ptr[%d]=%dn",i,ptr[i]);
    28. ptr[i]=1;
    29. ptr[i+pagesize-1]=1;
    30. }
    31. printf("ptr[%d]=%dn",ptr[i]);
    32. exit(0);
    33. }

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

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

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

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

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


    运行结果:
    copy
    huangcheng@ubuntu:~$ls-ltest
  • ls:无法访问test:没有那个文件或目录
  • huangcheng@ubuntu:~$./a.outtest50005000
  • PAGESIZE=4096
  • ptr[0]=0
  • ptr[4095]=0
  • ptr[4096]=0
  • ptr[8191]=0
  • 段错误
  • huangcheng@ubuntu:~$ls-ltest
  • -rwxr-xr-x1huangchenghuangcheng50002013-07-0915:48test
  • huangcheng@ubuntu:~$od-b-Adtest
  • 0000000001000000000000000000000000000000000000000000000
  • 0000016000000000000000000000000000000000000000000000000
  • *
  • 0004080000000000000000000000000000000000000000000000001
  • 0004096001000000000000000000000000000000000000000000000
  • 0004112000000000000000000000000000000000000000000000000
  • 0004992000000000000000000000040
  • 0005000
  • 页面大小为4096字节,我们能够读完整的第2页(下标为4096-8191),但是访问第3页时(下标为8192)引发SIGSEGV信号,shell将它输出成"Segmentation Fault(分段障)"。尽管我们把ptr[8191]设置成1,它也不写到test文件中,因而该文件的大小仍然是5000.内核允许我们读写最后一页中映射区以远部分(内核的内存保护是以页面为单位的)。但是我们写向这部分扩展区的任何内容都不会写到test文件中。设置成1的其他3个字节(下标分别为0,4905和4906)复制回test文件,这一点可使用od命令来验证。(-b选项指定以八进制数输出各个字节,-A d选项指定以十进制数输出地址。)

    我们仍然能访问内存映射区以远部分,不过只能在边界所在的那个内存页面内(下标为5000-8191)。访问ptr[8192]将引发SIGSEGV信号,这是我们预期的。

    现在我们把内存映射区大小(15000字节)指定成大于文件大小(5000字节)。

    copy
    huangcheng@ubuntu:~$./a.outtest500015000
  • 总线错误
  • -rwxr-xr-x1huangchenghuangcheng50002013-07-0916:00test

  • 其结果与先前那个文件大小等于内存映射区大小(都是5000字节)的例子类似。本例子引发SIGBUS信号(其shell输出为"Bus Error(总线出错)"),前一个例子则引发SIGSEGV信号。两者的差别是,SIGBUS意味着我们是在内存映射区访问的,但是已超出了底层支撑对象的大小。上一个例子中的SIGSEGV则意味着我们已在内存映射区以远访问。可以看出,内核知道被映射的底层支撑对象(本例子中为文件test)的大小,即使我们访问不了该对象以远的部分(最后一页上该对象以远的那些字节除外,他们的下标为5000-8191)。

    注意:
    copy
    huangcheng@ubuntu:~$./a.outtest50001000
  • 0004080000000000000000000000000000000000000000000000001//修改为1了
  • 0004096000000000000000000000000000000000000000000000000
  • *
  • 0004992000000000000000000000040
  • 0005000
  • 当文件长度为5000字节,映射1000字节时,但是1000字节不是PAGE_SIZE(4096)的整数倍,这样会强制映射为PAGE_SIZE的整数倍,现在映射的是4096字节。所以对映射区里面的第4096字节进行修改为1,会同步到文件中。

    下面的程序展示了处理一个持续增长的文件的一种常用技巧:指定一个大于该文件大小的内存映射区大小,跟踪该文件的当前大小(以确保不访问当前文件尾以远的部分),然后就让该文件的大小随着往其中每次写入数据而增长。
    copy
    #defineFILE"test.data"
  • #defineSIZE32768
  • /*open:createortruncate;thenmmapfile*/
  • fd=open(FILE,0777);
  • for(i=4096;i<=SIZE;i+=4096)
  • printf("settingfilesizeto%dn",i);
  • ftruncate(fd,i);
  • }
  • }

  • 打开文件
    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)的大小,而且我们总是能访问在当前文件大小以内又在内存映射区以内的那些字节。

    (编辑:李大同)

    【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

      推荐文章
        热点阅读