【Linux文件映射 -- mmap基本介绍】
文件映射的引入
在传统的文件读写中,例如说我们使用read/write操作文件时,它们会进行如下操作
- 用户态切换为内核态
- 将文件内容拷贝至自己的内核缓冲区
- 将内核缓冲区的内容拷贝至用户层缓冲区
- 内核态切换为用户态
在上述操作中,主要涉及到的时间成本是系统调用导致的状态变化以及拷贝所带来的消耗
为了解决上述问题,Linux中提供了mmap系统调用。如下图
mmap可以实现的功能是,将打开的文件对应的内容之间映射到自己的进程地址空间中。如下图
解析:由于直接将文件的内容映射到了自己的进程地址空间中,那么也就意味着我们不再需要进行状态切换,因为进程地址空间中的除内核空间外的是能够用户态下进行访问的!我们只需要知道文件映射的地址空间的起始位置,及其文件的大小。那么我们就可以直接访问文件的内容,从而免去了拷贝即可读取/写入文件,进一步提高效率!
文件映射接口
mmap系统调用所在的头文件是:#include<sys/mman.h>
功能:构建文件和进程地址空间之间的映射
如下是它的接口:
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
- addr:表示用户需要将文件的内容映射到进程地址空间的起始地址,一般设置为null表示由系统自动分配
- length:表示需要映射的字节数
- prot:表示该映射空间的权限,常用选项:PROT_READ(读)、PROT_WRITE(写)、PROT_EXEC(可执行)
- flags:表示映射类型和其他选项,常用选项为:MAP_PRIVATE(私有映射)、MAP_SHARED(共享映射)、MAP_ANONYMOUS(匿名映射)
- fd:需要映射的文件描述符
- offset:映射文件的起始偏移量,即映射区域的开始位置
- 返回值为MAP_FAILED表示失败,否则返回成功映射空间的起始地址
注意:length长度字段必须设置为页面大小的整数倍,若不是页面大小的整数倍,那么mmap会自动向上取整,例如我当前系统的页面大小是4096字节,那么我传递1000时,mmap会自动映射出4096的大小
页面大小:由操作系统内存管理单元(MMU)定义,是虚拟内存分页的最小单位(如4KB、2MB等)。其核心作用是优化物理内存的使用,支持虚拟地址到物理地址的映射,减少内存碎片。
munmap系统调用所在的头文件是:#include<sys/mman.h>
功能:将一个映射文件进行去映射
如下是它的接口:
int munmap(void *addr, size_t length);
- addr:即mmap返回的映射空间的起始地址
- length:即映射空间的字节数
- 若返回值为-1则表示去映射失败,返回值为0表示成功
注意:munmap传递length时内部处理也是按照页面大小来处理的,换句话来说会向上取整到4096字节的整数倍
扩展:
ftruncate系统调用所在的头文件是:<sys/types.h>和<unistd.h>
功能是调整一个文件的大小(若调整的大小比未调整之前大,则多出来的用0填充)
如下是它的接口介绍:
int ftruncate(int fd, off_t length);
- fd:需要调整大小的文件的文件描述符
- length:调整后的大小
- 返回值为-1表示调整失败,等于0表示成功
fstat系统调用所在的头文件是:<sys/types.h>和<sys/stat.h>和<unistd.h>
功能是获取一个文件的属性
如下是它的接口介绍
int fstat(int fd, struct stat *statbuf);
- fd:对应文件的文件描述符
- statbuf:输出型参数,该结构体中包含了文件的属性
- 返回值为0表示获取成功,否则表示失败
struct stat结构体中的属性如下:
dev_t st_dev; /* ID of device containing file */ino_t st_ino; /* Inode number */mode_t st_mode; /* File type and mode */nlink_t st_nlink; /* Number of hard links */uid_t st_uid; /* User ID of owner */gid_t st_gid; /* Group ID of owner */dev_t st_rdev; /* Device ID (if special file) */off_t st_size; /* Total size, in bytes */blksize_t st_blksize; /* Block size for filesystem I/O */blkcnt_t st_blocks; /* Number of 512B blocks allocated */
文件写入映射的步骤与demo
编程一个文件写入映射代码如下步骤:
- 打开目标文件(mmap需要你自己先打开文件)
- 手动调整文件大小,否则可能造成非法映射(例如你文件的大小只有1000,而映射时只能是页面整数倍,造成非法映射)
- 文件映射
- 进行文件操作
- 关闭映射
- 关闭文件
假设我们现在需要通过命令行参数传入一个文件名,之后对该文件写入a-z字符,则可编写出如下demo
#include <iostream>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#define PAGE_SIZE 4096
//命令行参数中输入文件名,通过mmap对文件内容进行操作
int main(int argc , char* argv[])
{if(argc != 2){std::cout << "Usage: " << argv[0] << " filename" << std::endl;return 1;}//1.打开目标文件.mmap需要你自己先打开文件int fd = ::open(argv[1] , O_RDWR | O_CREAT | O_TRUNC , 0666);if(fd < 0){std::cerr << "open file error!" << std::endl;return 2;}//2.手动调整文件的大小为PAGE_SIZE,方便我们进行合法映射//ftruncate调整大小后,默认是用0填充的if(::ftruncate(fd,PAGE_SIZE) < 0){std::cerr << "Failed to ftruncate file !" << std::endl;return 3;}//3. 文件映射char* shmaddr = (char*)::mmap(nullptr , PAGE_SIZE , PROT_READ | PROT_WRITE , MAP_SHARED , fd , 0);if(shmaddr == MAP_FAILED){std::cerr << "Failed to mmap file" << std::endl;return 4;}//4.进行文件操作for(char c = 'a' ; c <= 'z' ; ++c){*(shmaddr + c - 'a') = c;}//5.关闭映射if(::munmap(shmaddr,PAGE_SIZE) < 0){std::cerr << "Failed to munmap file" << std::endl;return 5;}//关闭文件close(fd);return 0;
}
文件读取映射的步骤与demo
编程一个文件写入映射代码如下步骤:
- 打开目标文件
- 获取文件大小
- 文件映射
- 进行文件操作
- 关闭映射
- 关闭文件
假设我们现在需要通过命令行参数传入一个文件名,之后读取该文件内容,则可编写出如下demo
#include <iostream>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
//命令行参数中输入文件名,通过mmap映射到文件后,读取文件内容
int main(int argc , char* argv[])
{if(argc != 2){std::cout << "Usage: " << argv[0] << " filename" << std::endl;return 1;}//1.打开目标文件.mmap需要你自己先打开文件int fd = ::open(argv[1] , O_RDONLY);if(fd < 0){std::cerr << "open file error!" << std::endl;return 2;}//2.获取文件大小struct stat st;::fstat(fd,&st);//3. 文件映射char* shmaddr = (char*)::mmap(nullptr , st.st_size , PROT_READ , MAP_SHARED , fd , 0);if(shmaddr == MAP_FAILED){std::cerr << "Failed to mmap file" << std::endl;return 4;}//4.进行文件操作std::cout << "File Content : " << shmaddr << std::endl;//5.关闭映射if(::munmap(shmaddr,st.st_size) < 0){std::cerr << "Failed to munmap file" << std::endl;return 5;}//关闭文件close(fd);return 0;
}
mmap扩展 -- 申请内存空间
mmap的flags参数我们在进行传递时一般都是传递MAP_SHARED
接下来看看对他flags参数其他选项的介绍:
- MAP_PRIVATE:创建一个私有的内存映射,进程对映射内存的修改不会同步到文件或其他进程,且对其他映射同一文件的进程不可见。
- MAP_ANONYMOUS:创建一块不与任何文件关联的匿名内存区域,内容初始化为零,通常用于动态分配大块内存。
通过这两个选项"|"的方式,我们可以实现映射一块不与任何文件关联的私有的内存映射,实际上就是说的开辟内存。
实际上malloc底层很大一部分的实现都是基于mmap的,除此之外还有brk(感兴趣了解下)
我们可以使用mmap实现一个精简版的malloc和free
注意:对于匿名映射并不会关联到文件,设置为-1即可
上述这种申请内存方式,可以用于一次申请大量内存,不适用于小内存的申请,因为mmap强制分配以页大小倍数为单位!