探索 Linux 下的内存映射(mmap):原理、应用与实践

简介

mmap 是一个系统调用,用于在内存中创建映射区域,将文件或者设备映射到进程的地址空间,从而允许对这些映射区域进行读写操作。通过 mmap 函数,程序可以直接将文件内容映射到内存中,从而避免了频繁的文件 I/O 操作,提高了访问文件数据的效率。此外,mmap 还可以用于创建匿名内存映射,用于进程间通信或者共享内存。

mmap函数

  • addr:指定被映射到进程空间内的起始地址,通常设为 NULL,代表让系统自动选定地址,映射成功后返回该地址。
  • len:映射到调用进程地址空间中的字节数。
  • prot:内存映射区域的保护方式,也即是获取映射地址指针进程的读写执行权限设置。
    • PROT_EXEC 映射区域可被执行;
    • PROT_READ 映射区域可被读取;
    • PROT_WRITE 映射区域可被写入;
    • PROT_NONE 映射区域不能存取;
  • flagsMAP_SHAREDMAP_PRIVATE 必须指定一个,其他可选:
    • MAP_SHARED 共享映射,多个进程可以共享同一份映射区域,对映射区的修改会反映到文件中,并且对其他映射该文件的进程也可见。
    • MAP_PRIVATE 私有映射,映射区的修改不会反映到文件中,而是在进程私有的副本中进行。
    • MAP_ANONYMOUS(或 MAP_ANON): 匿名映射,不与任何文件关联,用于创建匿名共享内存或者用作进程间通信的方式之一。
    • MAP_FIXED 试图将映射区域放置在指定的地址上,如果不能在指定地址上映射,则调用会失败。
    • MAP_NORESERVE 保留内核空间,但不分配任何物理页面。这允许使用大于实际物理内存的映射空间,但可能导致后续内存分配失败。
    • MAP_LOCKED 将映射区的所有页锁定到物理内存中,防止被交换出去。
    • MAP_POPULATE 在映射时预先填充映射区域的内存页,可以减少访问映射区域时的延迟,但可能会导致较长的映射时间。
    • MAP_NONBLOCKMAP_POPULATE 模式下,不会阻塞等待内存页的填充完成。
    • MAP_HUGETLB 使用大页来映射区域。这可以提高性能,特别是对于大型内存映射。
    • MAP_EXECUTABLE 映射区域可执行。用于将文件映射为可执行代码区域。
  • fd:要映射到内存中的文件描述符。如果使用匿名内存映射时,即flags中设置了MAP_ANON,fd设为-1。
  • offset:文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页大小的整数倍(一般是4096的整数倍)
  • 返回值:成功返回映射的内存地址指针,可以用这个地址指针对映射的文件内容进行读写操作,读写文件数据如同操作内存一样;如果 失败则返回NULL。
#include 
void *mmap(void *addr, size_t len, int prot, int flags,int fd, off_t offset);

取消内存映射,addr是由mmap成功返回的地址,length是要取消的内存长度,munmap 只是将映射的内存从进程的地址空间撤销,如果不调用这个函数,则在进程终止前,该片区域将得不到释放。

#include 
int munmap(void *addr, size_t len);

文件存储映射(零拷贝)

#include 
int main()
{  
   int fd, zero = 0;
   fd = open("1.txt", O_RDWR | O_CREAT, 0644);
   write(fd, &zero, sizeof(int));
   ptr = mmap(NULL, sizeof(int), 
              PROT_READ | PROT_WRITE | MAP_SHARED,
              fd, 0);
    /*
     这里父子进程同步(信号量)地使用ptr进行数据交换
     且退出exit(0)
    */
   munmap(ptr,sizeof(int));
   close(fd); //至此就可以摆脱文件描述符参与的工作啦
}

匿名内存映射(共享内存)

1.使用 /dev/zero 文件进行匿名映射

优点:可以得到干净的内存

缺点:只能用于父子进程

通过将 /dev/zero 文件映射到内存中,可以获得一个全是零的内存区域,这可以看作是一种匿名映射。

int main()
{  
   int fd;
   ptr = mmap(NULL, sizeof(int), 
              PROT_READ | PROT_WRITE | MAP_SHARED | MAP_ANON,
              -1, 0);
   /*
     这里父子进程同步(信号量)的使用ptr进行数据交换
     且退出exit(0)
    */
}


int main(int argc, char **argv)
{  
   /*忽略命令行参数处理步骤*/
   int fd;
   fd = open("/dev/zero", O_RDWR);
   ptr = mmap(NULL, sizeof(int), 
              PROT_READ | PROT_WRITE | MAP_SHARED,
              fd, 0);
   close(fd);
   /*
     这里父子进程同步(信号量)地使用ptr进行数据交换
     且退出exit(0)
    */
}
/*/dev/zero 是一个特殊的文件,当你读它的时候,
它会提供无限的空字符(NULL, ASCII NUL, 0x00)
该文件一个作用是用它作为源,产生一个特定大小的空白文件。*/

2.不指定文件描述符进行匿名映射

mmap 函数中,将文件描述符参数设置为 -1,并指定 MAP_ANONYMOUS 标志,可以实现匿名映射。在这种情况下,mmap 函数不会使用文件,而是直接在内存中创建一个新的匿名映射区域。

优点:写法简单

缺点:只能用于父子进程

void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

3.通过shm_open函数打开共享内存文件

优点:可以在没有血缘的进程中通信

缺点:代码相比于之前的微多

shm_open函数介绍

功能说明:shm_open 用于创建或者打开共享内存文件。笔者认为shm_open 也许仅仅是系统函数open的一个包装,不同之处就是shm_open操作的文件一定是位于tmpfs文件系统里的,常见的Linux发布版的tmpfs文件系统的存放目录就是/dev/shm。

返回值:成功返回fd>0, 失败返回fd<0

参数说明:

  • name:要打开或创建的共享内存文件名,由于shm_open 打开或操作的文件都是位于/dev/shm目录的,因此name不能带路径,例如:/var/myshare 这样的名称是错误的,

  • oflag:打开的文件操作属性:O_CREATO_RDWRO_EXCL的按位或运算组合

  • mode:文件共享模式,例如 0777

#include 
#include         /* For mode constants */
#include            /* For O_* constants */

int shm_open(const char *name, int oflag, mode_t mode);

int shm_unlink(const char *name);
/*
功能说明:删除/dev/shm目录的文件,shm_unlink 删除的文件是由shm_open函数创建于/dev/shm目录的。可以用系统函数unlink来达到同样的效果,用/dev/shm + name 组成完整的路径即可,但一般不要这么做,因为系统的tmpfs的位置也许不是/dev/shm。用shm_open 创建的文件,如果不调用此函数删除,会一直存在于/dev/shm目录里,直到操作系统重启或者调用linux命令rm来删除为止。
*/

示例代码

  1. 通过shm_open创建文件
  2. 通过ftruncate函数设置文件大小
  3. 使用mmap函数进行映射
  4. 写入/读取问价内容
  5. 关闭/释放相关资源
//write端
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    int fd = shm_open("mmap", O_CREAT | O_RDWR, 0777);
    ftruncate(fd, 4);
    int* ptr = (int*)mmap(NULL, sizeof(int),
               PROT_READ | PROT_WRITE , MAP_SHARED,
               fd, 0);
    *ptr = 666;
    getchar();
    close(fd);
    munmap(ptr, sizeof(int));
    shm_unlink(mmap);
    return 0;
}

//read端
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    int fd = shm_open("mmap", O_CREAT | O_RDWR, 0777);
    ftruncate(fd, 4);
    int *ptr = (int *)mmap(NULL, sizeof(int),
                           PROT_READ | PROT_WRITE, MAP_SHARED,
                           fd, 0);
    printf("*ptr==%d", *ptr);
    getchar();
    close(fd);
    munmap(ptr, sizeof(int));
    return 0;
}

输出,成功读取写端写入的内容

root@VM-16-3-ubuntu:~/projects/test# ./read.out 
*ptr==666

你可能感兴趣的:(Linux,服务器开发,Linux,系统编程,linux,服务器,内存映射,零拷贝)