C/C++ 使用mmap/munmap函数分配内存

在C/C++ 中常用的内存分配和管理的方式有很多,如智能指针, STL容器, new/delete, malloc/free, brk, sbrk等,linux有一种比较底层的内存管理方式mmap/munmap,需要完全自己来维护分配的虚拟内存,没有任何其他辅助的数据结构来帮助维护内存空间。mmap系统调用可以分配一段匿名的虚拟内存区域,也可以映射一个文件到内存,这个映射让文件操作像直接操作内存一样,这种方式称之为内存映射。

 mmap()必须以内存页(PAGE_SIZE)为单位进行映射,而内存也只能以页为单位进行映射,若要映射非PAGE_SIZE整数倍的地址范围,要先进行内存对齐,强行以PAGE_SIZE的倍数大小进行映射。
 mmap操作提供了一种机制,让用户程序直接访问设备内存,这种机制,相比较在用户空间和内核空间互相拷贝数据,效率更高。在要求高性能的应用中比较常用,但在面向流的设备不能进行mmap,mmap的实现和硬件有关。

一、在终端里输入 man mmap 可以查看此函数的API文档,此函数的具体描述如下:

#include <sys/mman.h>

 void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
 int munmap(void *start, size_t length);
函数形式参数:
start :  指向欲映射的内存起始地址,通常设为 NULL,代表让系统自动选定地址,映射成功后返回该地址。
length:  代表将文件中多大的部分映射到内存,即映射区的长度。
prot  :  映射区域的保护方式,不能与文件的打开模式冲突。可以为以下几种方式:
                    PROT_EXEC 映射区域可被执行
                    PROT_READ 映射区域可被读取
                    PROT_WRITE 映射区域可被写入
                    PROT_NONE 映射区域不能存取

以上的方式可以通过or运算(“|”)合理地组合在一起
flags :  影响映射区域的各种特性,指定映射对象的类型,映射选项和映射页是否可以共享。在调用mmap()时必须要指定MAP_SHARED 或MAP_PRIVATE,flags的值可以是一个或者多个以下位的组合体:
                    MAP_FIXED 如果参数start所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。通常不鼓励用此旗标。
                    MAP_SHARED 对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。
                    MAP_PRIVATE 对映射区域的写入操作会产生一个映射文件的复制,即私人的“写入时复制”(copy on write)对此区域作的任何修改都不会写回原来的文件内容,写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。
                    MAP_ANONYMOUS建立匿名映射。此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享。
                    MAP_DENYWRITE只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝。
                    MAP_LOCKED 将映射区域锁定住,这表示该区域不会被置换(swap),从而防止页面被交换出内存。

                   MAP_NORESERVE //不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。
                  MAP_LOCKED //锁定映射区的页面,从而防止页面被交换出内存。
                  MAP_GROWSDOWN //用于堆栈,告诉内核VM系统,映射区可以向下扩展。

                  MAP_ANON //MAP_ANONYMOUS的别称,不再被使用。
                  MAP_FILE //兼容标志,被忽略。
                  MAP_32BIT //将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。
                  MAP_POPULATE //为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。
                  MAP_NONBLOCK //仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。
fd    :  要映射到内存中的文件描述符。如果使用匿名内存映射时,即flags中设置了MAP_ANONYMOUS,fd设为-1。有些系统不支持匿名内存映射,则可以使用fopen打开/dev/zero文件,
          然后对该文件进行映射,可以同样达到匿名内存映射的效果。
offset:文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是PAGE_SIZE的整数倍。

返回值:

       成功执行时,mmap() 若映射成功则返回映射区的内存起始地址,munmap()返回0。失败时,mmap()返回MAP_FAILED[其值为(void *)-1],错误原因存于errno 中,munmap返回-1。

错误代码:
            EBADF  参数fd 不是有效的文件描述词
            EACCES 存取权限有误。如果是MAP_PRIVATE 情况下文件必须可读,使用MAP_SHARED则要有PROT_WRITE以及该文件要能写入。
            EINVAL 参数start、length 或offset有一个不合法。
            EAGAIN 文件被锁住,或是有太多内存被锁住。
            ENOMEM 内存不足。

errno返回值:
 errno被设为以下的某个值:
            EACCES:访问出错
            EAGAIN:文件已被锁定,或者太多的内存已被锁定
            EBADF:fd不是有效的文件描述词
            EINVAL:一个或者多个参数无效
            ENFILE:已达到系统对打开文件的限制
            ENODEV:指定文件所在的文件系统不支持内存映射
            ENOMEM:内存不足,或者进程已超出最大内存映射数量
            EPERM:权能不足,操作不允许
            ETXTBSY:已写的方式打开文件,同时指定MAP_DENYWRITE标志
            SIGSEGV:试着向只读区写入
            SIGBUS:试着访问不属于进程的内存区
用户层的调用很简单,其具体功能就是直接将物理内存直接映射到用户虚拟内存,使用户空间可以直接对物理空间操作。但是对于内核层而言,其具体实现比较复杂。

系统调用:
 mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。
 注:实际上,mmap()系统调用并不是完全为了用于共享内存而设计的。它本身提供了不同于一般对普通文件的访问方式,进程可以像读写内存一样对普通文件的操作。
     而Posix或SystemV的共享内存IPC则纯粹用于共享目的,当然mmap()实现共享内存也是其主要应用之一。
 
 系统调用mmap()用于共享内存的两种方式:
  (1)使用普通文件提供的内存映射:适用于任何进程之间;此时,需要打开或创建一个文件,然后再调用mmap();典型调用代码如下:
   fd=open(name, flag, mode);
   if(fd<0)
   ...
   ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0);
   通过mmap()实现共享内存的通信方式有许多特点和要注意的地方,我们将在范例中进行具体说明。
  (2)使用特殊文件提供匿名内存映射:适用于具有亲缘关系的进程之间;由于父子进程特殊的亲缘关系,在父进程中先调用mmap(),然后调用fork()。
       那么在调用fork()之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap()返回的地址,这样,父子进程就可以通过映射区域进行通信了。
       注意,这里不是一般的继承关系。一般来说,子进程单独维护从父进程继承下来的一些变量。而mmap()返回的地址,却由父子进程共同维护。
       对于具有亲缘关系的进程实现共享内存最好的方式应该是采用匿名内存映射的方式。此时,不必指定具体的文件,只要设置相应的标志即可。

 int munmap( void * addr, size_t len )
 该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小。当映射关系解除后,对原来映射地址的访问将导致段错误发生。

 int msync ( void * addr , size_t len, int flags)
 一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致。

程序例:
 下面将给出使用mmap()的两个范例。范例1给出两个进程通过映射普通文件实现共享内存通信,范例2给出父子进程通过匿名映射实现共享内存。
 系统调用 mmap()有许多有趣的地方,下面是通过mmap()映射普通文件实现进程间的通信的范例,我们通过该范例来说明mmap()实现共享内存的特点及注意事项。

 范例1:两个进程通过映射普通文件实现共享内存通信
 范例1包含两个子程序:map_normalfile1.c及map_normalfile2.c。编译两个程序,可执行文件分别为 map_normalfile1及map_normalfile2。
 两个程序通过命令行参数指定同一个文件来实现共享内存方式的进程间通信。 map_normalfile2试图打开命令行参数指定的一个普通文件,把该文件映射到进程的地址空间,并对映射后的地址空间进行写操作。
 map_normalfile1把命令行参数指定的文件映射到进程地址空间,然后对映射后的地址空间执行读操作。这样,两个进程通过命令行参数指定同一个文件来实现共享内存方式的进程间通信。
 下面是两个程序代码:

 /*-------------map_normalfile1.c-----------*/
  #include <sys/mman.h>
  #include <sys/types.h>
  #include <fcntl.h>
  #include <unistd.h>
  
  typedef struct{
   char name[4];
   int age;
  }people;
  
  main(int argc, char** argv) // map a normal file as shared mem:
  {
   int fd,i;
   people *p_map;
   char temp;
   
   fd = open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777);
   
   if(fd < 0)
   {
    printf("error open\n");
    exit(1);
   }
   
   lseek(fd,sizeof(people)*5-1,SEEK_SET);
   write(fd,"",1);
   p_map = (people*) mmap( NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0 );
   close( fd );
   temp = 'a';
   for(i=0; i<10; i++)
   {
    temp += 1;
    memcpy( ( *(p_map+i) ).name, &temp,2 );
    ( *(p_map+i) ).age = 20+i;
   }
   
   printf(" initialize over \n ");
   sleep(10);
   munmap( p_map, sizeof(people)*10 );
   printf( "umap ok \n" );
  }

  

示例2:

/*-------------map_normalfile2.c-----------*/
  #include <sys/mman.h>
  #include <sys/types.h>
  #include <fcntl.h>
  #include <unistd.h>
  typedef struct{
   char name[4];
   int age;
  }people;

  main(int argc, char** argv) // map a normal file as shared mem:
  {
   int fd,i;
   people *p_map;
   fd=open( argv[1],O_CREAT|O_RDWR,00777 );
   p_map = (people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
   for(i = 0;i<10;i++)
   {
    printf( "name: %s age %d;\n",(*(p_map+i)).name, (*(p_map+i)).age );
   }
   munmap( p_map,sizeof(people)*10 );
  }
  

map_normalfile1.c 首先定义了一个people数据结构,(在这里采用数据结构的方式是因为,共享内存区的数据往往是有固定格式的,这由通信的各个进程决定,采用结构的方式有普遍代表性)。
 map_normfile1打开或创建一个文件,并把文件的长度设置为5个people结构大小。然后从mmap()的返回地址开始,设置了10个people结构。
 然后,进程睡眠10秒钟,等待其他进程映射同一个文件,最后解除映射。
 map_normfile2.c只是简单的映射一个文件,并以people数据结构的格式从mmap()返回的地址处读取10个people结构,并输出读取的值,然后解除映射。
  
 分别把两个程序编译成可执行文件map_normalfile1和map_normalfile2后,在一个终端上先运行./map_normalfile2 /tmp/test_shm,程序输出结果如下:
  initialize over
  umap ok
 在map_normalfile1输出initialize over 之后,输出umap ok之前,在另一个终端上运行map_normalfile2 /tmp/test_shm,将会产生如下输出(为了节省空间,输出结果为稍作整理后的结果):
  name: b age 20; name: c age 21; name: d age 22; name: e age 23; name: f age 24;
  name: g age 25; name: h age 26; name: I age 27; name: j age 28; name: k age 29;
 在map_normalfile1 输出umap ok后,运行map_normalfile2则输出如下结果:
  name: b age 20; name: c age 21; name: d age 22; name: e age 23; name: f age 24;
  name: age 0; name: age 0; name: age 0; name: age 0; name: age 0;
 从程序的运行结果中可以得出的结论 :
  1、 最终被映射文件的内容的长度不会超过文件本身的初始大小,即映射不能改变文件的大小;
  2、可以用于进程通信的有效地址空间大小大体上受限于被映射文件的大小,但不完全受限于文件大小。
     打开文件被截短为5个people结构大小,而在 map_normalfile1中初始化了10个people数据结构,在恰当时候(map_normalfile1输出initialize over 之后,输出umap ok之前)调用map_normalfile2
     会发现map_normalfile2将输出全部10个people结构的值,后面将给出详细讨论。
     注:在linux中,内存的保护是以页为基本单位的,即使被映射文件只有一个字节大小,内核也会为映射分配一个页面大小的内存。
         当被映射文件小于一个页面大小时,进程可以对从mmap()返回地址开始的一个页面大小进行访问,而不会出错;
         但是,如果对一个页面以外的地址空间进行访问,则导致错误发生,后面将进一步描述。因此,可用于进程间通信的有效地址空间大小不会超过文件大小及一个页面大小的和。
  3、文件一旦被映射后,调用mmap()的进程对返回地址的访问是对某一内存区域的访问,暂时脱离了磁盘上文件的影响。所有对mmap()返回地址空间的操作只在内存中有意义,
     只有在调用了munmap()后或者msync()时,才把内存中的相应内容写回磁盘文件,所写内容仍然不能超过文件的大小。

程序例子:

例一:

//
//  main.cpp
//
//
//  Created by ChengChao on 14-9-27.
//  Copyright (c) 2014年 cc. All rights reserved.
//
#include <iostream>
#include <sys/mman.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main(int argc, const char * argv[]) {

    //申请内存
    int* arr = static_cast<int*>(mmap(
                    NULL,                   //分配的首地址
                    getpagesize(),          //分配内存大小(必须是页的整数倍, 32位1页=4k)
                    PROT_READ | PROT_WRITE, //映射区域保护权限:读|写
                    MAP_ANON | MAP_SHARED,  //匿名映射(不涉及文件io), 后面两个参数忽略
                    0,                      //要映射到内存中的文件描述符
                    0                       //文件映射的偏移量,通常设置为0,必须是页的整数倍
                ));
    printf("申请内存大小=%dk\n", sizeof(arr));
    
    *arr = 10;
    *(arr + 1) = 20;
    *(arr + 2) = 30;
    
    printf("arr[2]=%d\n", arr[2]);
    
    //释放指针arr指向的内存区域,并制定释放的内存大小
    munmap(arr, getpagesize());
    
    return 0;
}

例二:

#include <fstream>
#include <iostream>
#include <cstdlib>
#include <sys/mman.h> 
#include <unistd.h>
#include <fcntl.h>
using namespace std;
 
int main(int argc, char* argv[])
{
    int fd=open("/mnt/e/file",O_RDWR);
    FILE* f=fdopen(fd,"rw");
    fseek(f,0,SEEK_END);
    long ps=ftell(f);
    cout << "file size: " << ps << endl;
	
    //分配的大小必须为页的整数倍
    int* p = (int*)mmap(NULL, ps, PROT_WRITE|PROT_READ, MAP_SHARED, fd, 0);
    if (p == MAP_FAILED)
    {  
       perror("mmap error");  
       exit(1);  
    }
	
    //比如我用2个进程运行该程序,进程1 让*p=100 ,那么进程2 去读取*p也会变成100,但重要的是文件并没有更新,就是说没有IO操作,文件仅仅在msync()或mumap()调用时才会被更新,因为此时2个进程映射的该文件,都指向了相同的内存区域,也就说只有内存中的数据改变了
    int oper,data;
    while(true){
        cin>>oper;
        if(oper==1){
            cin>>data;
            *p=data;
        }
        else{
            cout<<*p<<endl;
        }
    }
	
//...
	
    munmap(p, ps);  
    close(fd);
    return 0;
}

https://www.xuebuyuan.com/1356242.html

http://www.bubuko.com/infodetail-384719.html

猜你喜欢

转载自blog.csdn.net/sunlin972913894/article/details/103963022
今日推荐