mmap是linux中用处非常广泛的一个系统调用。

mmap将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零

mmap 必须以PAGE_SIZE为单位进行映射,而内存也只能以页为单位进行映射,若要映射非PAGE_SIZE整数倍的地址范围,要先进行内存对齐,强行以PAGE_SIZE的倍数大小进行映射

函数原型:

void mmap(void start,size_t length,int prot,int flags,int fd,off_t offset);

int munmap(void* start,size_t length);

下面说一下内存映射的步骤:

  1. 用open系统调用打开文件, 并返回描述符fd.
  2. 用mmap建立内存映射, 并返回映射首地址指针start.
  3. 对映射(文件)进行各种操作, 显示(printf), 修改(sprintf)
  4. 用munmap(void *start, size_t length)关闭内存映射.
  5. 用close系统调用关闭文件fd.

 

这里简单总结一下其几个用法,以及一些注意事项:

用法一:当要map的文件是/dev/mem的时候,就可以实现把物理地址映射到虚拟空间中【比如在SV验证的时候在linux应用层访问设备寄存器】

#define MAP_SIZE 4096
#define MAP_MASK (MAP_SIZE - 1)

int dev_mem_fd = -1;
void* g_map_base = NULL;

void initMmap()
{
    dev_mem_fd = open("/dev/mem", O_RDWR | O_SYNC);
}

void* MapPaddr2Vaddr(void* paddr)
{
   void* vaddr = NULL;
   void* vaddr_map_base = NULL;
   void* map_base = (long)paddr & ~MAP_MASK;
   vaddr_map_base = mmap(NULL, MAP_SIZE, PORT_READ | 
   PORT_WRITE, MAP_SHARED, dev_mem_fd, (long)map_base);
   if(vaddr_map_base != MAP_FAILED) {
       vaddr = vaddr_map_base + ((long)paddr & MAP_MASK);
       g_map_base = vaddr_map_base;
   }
   return vaddr;
}

void UnmapAddr()
{
    if(g_map_base ) {
        munmap(g_map_base, MAP_SIZE);
    }
}

void WriteReg(void* paddr, uint32_t val)
{
  void* vaddr = MapPaddr2Vaddr(paddr);
  *(volatile uint32_t*)vaddr = val; 
  __asm("DC CIVAC, %0\n", :: "r" ((uint64_t)vaddr));
  UnmapAddr(); // makesure update sync to file
}

补充:系统虚拟存储页的大小获取方式

printf("%ld\n", getpagesize());
printf("%ld\n", sysconf(_SC_PAGESIZE));

参数说明

  1. 参数fd为即将映射到内存空间的文件描述符,一般由open返回。同时fd也可以指定为-1,此时须指定flags参数中的MAP_ANON,表名进行的是匿名映射。
  2. len是映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起。
  3. prot参数指定共享内存的访问权限,可指定为
    PROT_NONE:映射区不可访问
    或者以下几个值的或:
    PROT_READ:可读
    PROT_WRITE:可写
    PROT_EXEC:可执行
  4. flags参数影响映射存储区的多种属性:
    MAP_FIXED: 返回值必须等于addr。因为这不利于可移植性,所以不建议使用此标志。如果未指定此标志,而且addr非0,则内核只把addr视为在何处设置映射区的一种建议,但是不保证会使用所要求的地址。将addr指定为0可获得最大可移植性。
    MAP_SHARED: 这一标志说明了本进程对映射区所进行的存储操作的配置。此标志指定存储操作修改映射文件,也就是说,存储操作相当于对该文件的write。
    MAP_PRIVATE: 本标志说明,对映射区的存储操作导致创建该映射文件的一个私有副本。所有后来对该映射区的引用都是引用该副本,而不是原始文件。(此标志的一种用途是用于调试程序,它将一程序文件的正文部分映射至一存储区,但允许用户修改其中的指令。任何修改只影响程序文件的副本,而不影响原文件)。
  5. offset参数一般设置为0,表示从文件头开始映射。
  6. 参数addr指定文件应被映射到进程空间的起始地址,一般被指定为一个空指针,此时选择起始地址的任务留给内核来完成。返回值为最后文件映射到进程空间的起始地址,进程可以直接操作该地址。

用法二:将一个普通文件映射到内存中,通常在需要对文件进行频繁读写时使用,这样用内存读写取代I/O读写,以获得较高的性能

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <errno.h>

int main(int argc, char* argv[]) {
    int fd, offset;
    char* data;
    struct stat sbuf;

    if (argc != 2) {
        fprintf(stderr, "usage:mmapdemo offset\n");
        exit(1);
    }

    if ((fd = open("mmapdemo.c", O_RDONLY)) == -1) {//打开文件自身
        perror("open");
        exit(1);
    }

    if (stat("mmapdemo.c", &sbuf) == -1) {//文件大小,mmap的有效内存大小不超过该值
        perror("stat");
        exit(1);
    }

    offset = atoi(argv[1]);//文件偏移量
    if (offset < 0 || offset > sbuf.st_size - 1) {
        fprintf(stderr, "mmapdemo: offset must be in the range 0-%d\n",
                sbuf.st_size - 1);
        exit(1);
    }

    data = mmap((caddr_t)0, sbuf.st_size, PROT_READ, MAP_SHARED, fd, 0);

    if (data == (caddr_t)(-1)) {
        perror("mmap");
        exit(1);
    }

    printf("byte at offset %d is '%c'\n", offset, data[offset]);

    return 0;
}

用法三:进程间通信

//map_normalfile1.cpp
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>

struct People{
    char name[4];
    int age;
};

const int file_struct_cnt = 5;
const int mem_struct_cnt = 10;

int main(int argc, char* argv[]) {
    int fd = open(argv[1], O_CREAT | O_RDWR | O_TRUNC, 00777);
    lseek(fd, sizeof(People) * file_struct_cnt - 1, SEEK_SET);//文件大小为8*5
    write(fd, "", 1);

    //内存大小为8*10
    People* pmap = (People*)mmap(NULL, sizeof(People) * mem_struct_cnt, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);

    //内存赋值
    for (int i = 0; i < 10; ++i) {
        char c = 'a' + i;
        memcpy((pmap + i)->name, &c, 1);
        (pmap + i)->age = 20 + i;
    }

    printf("initialize over.\n");
    sleep(10);//等待map_normalfile2读取argv[1]
    if (munmap(pmap, sizeof(People) * 10) != 0) {
        printf("munmap error[%s]\n", strerror(errno));
        return -1;
    }

    return 0;
}
//map_normalfile2.cpp
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <sys/mman.h>

struct People{
    char name[4];
    int age;
};

const int mem_struct_cnt = 10;

int main(int argc, char* argv[]) {
    int fd = open(argv[1], O_CREAT | O_RDONLY, 00777);
    People* pmap = (People*)mmap(NULL, sizeof(People) * mem_struct_cnt, PROT_READ, MAP_SHARED, fd, 0);

    for (int i = 0; i < mem_struct_cnt; ++i) {
        printf("name:%s age:%d\n", (pmap + i)->name, (pmap + i)->age);
    }
    if (munmap(pmap, sizeof(People) * 10) != 0) {
        printf("munmap error[%s]\n", strerror(errno));
        return -1;
    }

    return 0;
}

其中: 两个文件都定义了struct People,映射相同的文件,分别编译为map_normalfile1 map_normalfile2。

  1. map_normalfile1写数据到文件,首先把文件长度调整为5个struct的大小,mmap映射到内存后,写入10个struct大小的数据,sleep 10秒,munmap程序退出。
  2. map_normalfile2读文件,通过mmap映射到内存,然后读取内存数据。

结论:

1、映射内存的实际长度不局限于文件大小,应该是虚存页大小的整数倍,超出部分填充’\0’。

2、对内存超出文件大小部分的修改不会对文件产生影响,我们执行stat mmapdata也可以验证这点(每个struct大小为8bytes,一共写了40bytes)。

 

用法四:进程间共享内存

我们知道信号量sem是支持跨进程的,这里写了一个简化的例子(同时出于篇幅的原因,省略了include)

进程间共享信号量,其中一个进程负责初始化/销毁信号量,并随机sleep一段时间后 sem_post该信号量,另一个进程sem_wait该信号量。代码如下:

const int mem_size = sizeof(sem_t);

int main() {
    int fd = open("mmap.data", O_CREAT | O_RDWR | O_TRUNC, 0777);
    lseek(fd, mem_size, SEEK_SET);
    write(fd, "", 1);

    sem_t *psem = (sem_t*)mmap(NULL, mem_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);

    int res = sem_init(psem, 1, 0);
    if (res != 0) {
        printf("sem_init error. %d %s\n", errno, strerror(errno));
        return -1;
    }

    while (true) {
        sleep(rand() % 10);
        printf("before post. ts:%ld\n", time(NULL));
        sem_post(psem);
    }

    res = sem_destroy(psem);
    assert(res != 0);

    munmap(psem, mem_size);
    printf("unmap.\n");

    return 0;
}
const int mem_size = sizeof(bin_sem);

int main() {
    int fd = open("mmap.data", O_CREAT | O_RDWR, 0777);
    sem_t *psem = (sem_t*)mmap(NULL, mem_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);

    while (true) {
        printf("before wait. ts:%ld\n", time(NULL));
        sem_wait(psem);
    }

    sem_destroy(psem);
    munmap(psem, mem_size);

    return 0;
}