前言
存储映射I/O使一个磁盘文件与存储空间中的一个缓存相映射。于是当从缓存中取数据,就相当于读文件中的相应字节。与其类似,将数据存入缓存,则相应字节就自动地写入文件。这样,就可以在不使用read和write的情况下执行I/O。为了使用这种功能,应首先告诉内核将一个给定的文件映射到一个存储区域中。这是由mmap函数实现的,接口如下:
#include
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
返回:若成功则为映射区的起始地址,若出错则为- 1
目录
1、mmap接口主要用途
2、mmap接口参数解析
3、参数取值以及相关信号量
4、相关配套接口
4.1、解除映射区域munmap()
4.2、同步映射区域msync()
5、文件拷贝性能测试
6、总结
UNIX网络编程第二卷进程间通信对mmap函数进行了说明。该函数主要用途有三个:
①将一个普通文件映射到内存中,通常在需要对文件进行频繁读写时使用,这样用内存读写取代I/O读写,以获得较高的性能。
②将特殊文件进行匿名内存映射,可以为关联进程提供共享内存空间。
③为无关联的进程提供共享内存空间,一般也是将一个普通文件映射到内存中。
addr:指向欲映射的内存起始地址,通常设为 NULL,代表让系统自动选定地址,映射成功后返回该地址。
length:代表将文件中多大的部分映射到内存。注意:若length > file_size,则超出部分的内存变动无法表现在文件fd中,而且 即便mmap能映射成功,对于超过部分内存的读和写,内核均可能会抛出SIGBUS,为什么是“可能”不是“一定”,因为 mmap的映射总是以系统的页为单位(ps:查看页大小指令 getconf PAGESIZE,默认值4096B)了,假设文件大小为 100B,映射大小为4K,对于超出部分的读写不会发生Bus error,因为本页已经映射成功,剩余内存可以继续访问,若 文件大小为100B,映射的大小为8K,那么对于超出的4K部分的访问则一定会产生Bus error)。解决方法:配合 ftruncate或者lseek对文件进行偏移,以保证文件大小和映射大小一致。测试代码如下:
//文件大小100B
p_map_dst = mmap(NULL,4096*2, (PROT_READ | PROT_WRITE), MAP_SHARED, fd_dst, 0);
if (-1 == (int)(unsigned long)p_map_dst)
{
ERR_DBG_PRINT("dst.txt mmap fail:");
return -1;
}
close(fd_dst);//映射成功即可关闭文件描述符
printf("mmap all ok\n");
memcpy(p_map_dst,p_map_src,4096+1);
prot:映射区域的保护方式。可以为以下几种方式的组合:
PROT_EXEC | 映射区域可被执行 |
PROT_READ | 映射区域可被读取 |
PROT_WRITE | 映射区域可被写入 |
PROT_NONE | 映射区域不能存取 |
flags:影响映射区域的各种特性。在调用mmap()时必须要指定MAP_SHARED 或MAP_PRIVATE。
MAP_FIXED:如果参数start所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。通常不鼓励用此旗标。
MAP_SHARED:对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。
MAP_PRIVATE:对映射区域的写入操作会产生一个映射文件的复制,即私人的“写入时复制”(copy on write)对此区域 作的任何修改都不会写回原来的文件内容。
MAP_ANONYMOUS:建立匿名映射。此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享。
MAP_DENYWRITE:只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝。
MAP_LOCKED:将映射区域锁定住,这表示该区域不会被置换(swap)。
fd:要映射到内存中的文件描述符。如果使用匿名内存映射时,即flags中设置了MAP_ANONYMOUS,fd设为-1。有些系统不支 持匿名内存映射,则可以使用fopen打开/dev/zero文件,然后对该文件进行映射,可以同样达到匿名内存映射的效果。
offset:文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页大小的整数倍。以一个把普遍文件映 射到用户空间的内存区域为例,具体示意图如下:
返回值:若映射成功则返回映射区的内存起始地址,否则返回MAP_FAILED(-1),错误原因存于errno 中。
错误代码:
EBADF:参数fd 不是有效的文件描述词
EACCES:存取权限有误。如果是MAP_PRIVATE 情况下文件必须可读,使用MAP_SHARED则要有PROT_WRITE 以及该文件要能写入。
EINVAL:参数start、length 或offset有一个不合法。
EAGAIN:文件被锁住,或是有太多内存被锁住。
ENOMEM:内存不足。
offset和addr的值(如果指定了MAPFIXED)通常应当是系统虚存页长度的倍数。通常offset和addr指定为0,所以这种要求一般并不是问题。因为映射文件的起动位移量受系统虚存页长度的限制,那么如果映射区的长度不是页长度的整数倍时,将如何呢?假定文件长12字节,系统页长为512字节,则系统通常提供512字节的映射区,其中后500字节被设为0。可以修改这500字节,但任何变动都不会在文件中反映出来。对于同一个文件fd,可以映射多次,得到多个虚拟地址,这些虚拟地址指向的是同一块物理内存,DPDK在初始化巨页执行映射rte_mapX,就是映射两次,第二次映射将第一次映射的虚拟地址都连续起来。
与映射存储区相关有两个信号: SIGSEGV和SIGBUS。信号SIGSEGV通常用于指示进程试图存取它不能存取的存储区,简单点说:读写的区域大于mmap映射长度了。如果进程企图存数据到用mmap指定为只读的映射存储区,那么也产生此信号。如果存取映射区的某个部分,而在存取时这一部分已不存在,则产生SIGBUS信号。ps:①用文件长度映射一个文件,但在存访该映射区之前,另一个进程已将该文件截短。此时,如果进程企图存取对应于该文件尾端部分的映射区,则接收到SIGBUS信号,简单点说:当mmap映射区长度大于文件实际长度,映射可以成功,但是后期的读写极有可能会产生SIGBUS。②mmap去扩展一个内容为空的新文件,因为大小为0,所有本没有与之对应的合法的物理页,不能扩展,内核也会抛出SIGBUS。
#include
int munmap(void *addr, size_t length);
成功返回0,失败返回-1.
addr:是待解除映射地址范围的起始地址,它必须与一个分页边界对齐。通常可取mmap的返回值
length:是一个非负整数,指定了待解除映射区域的大小(字节数)。范围为系统分页大小的下一个倍数的地址空间将会被解除 映射。通常可取mmap接口的参数length
int msync(void *addr, size_t length, int flags);
让应用程序能够显式地控制何时完成共享映射与映射文件之间的同步。这是非常有用的。如为确保数据完整性。
成功返回0,失败返回-1.
addr和length参数指定了需同步的内存区域的起始地址和大小。
addr中指定的地址必须是分页对齐的,通常为mmap的返回值,length会向上舍入到系统分页的下一个整数倍大小,通常可以去mmap中的映射内存大小参数length。
flags必选如下两个项之一:
MS_SYNC: 执行一个同步的文件写入。这个调用会阻塞直到内存区域中所有被修改过的分页被写入到底盘为止。
MS_ASYNC:执行一个异步的文件写入。内存区域中被修改过的分页会在后面某个时刻被写入磁盘并立即对在相应文件区域中 执行read()的其他进程可见。
flags参数中还可以加上下面这个值。
MS_INVALIDATE:使映射数据的缓存副本失效。当内存区域中所有被修改过的分页被同步到文件中之后,内存区域中所有与底 层文件不一致的分页会被标记为无效。当下次引用这些分页会从文件的相应位置处复制相应的分页内容。其 结果是其他进程对文件作出的所有更新将会在内存区域中可见。
通过mmap文件映射方式以及普通的read/write读写实现文件的拷贝,测试mmap接口对性能的提升,代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define PAGE_SIZE 4096
#define READ_WRITE_BUF_SIZE 4096
#define ERR_DBG_PRINT(fmt, args...) \
do \
{ \
printf("ERR_DBG:%s(%d)-%s:\n"fmt": %s\n", __FILE__,__LINE__,__FUNCTION__,##args, strerror(errno)); \
} while (0)
/* mmap映射文件 */
int main(int argc, char *argv[])
{
int fd_src = -1,fd_dst = -1,i = 0;
unsigned int file_src_size = 0,count;
char *p_map_src = NULL,*p_map_dst = NULL;
struct stat stat_src_buf;
char *rw_buff[READ_WRITE_BUF_SIZE];
fd_src = open("/sbc/wq/some_func/mmap_learn/src.txt", O_CREAT | O_RDWR, 0755);
if (fd_src < 0)
{
ERR_DBG_PRINT("open src.txt fail:");
return -1;
}
fd_dst = open("/sbc/wq/some_func/mmap_learn/dst.txt", O_CREAT | O_RDWR, 0755);
if (fd_dst < 0)
{
ERR_DBG_PRINT("open dst.txt fail:");
return -1;
}
/*
for(;;)
{
count = read(fd_src,rw_buff,READ_WRITE_BUF_SIZE);
if(write(fd_dst,rw_buff,count) == 0)
{
break;
}
}
return 0;
*/
if(fstat(fd_src,&stat_src_buf) < 0)
{
ERR_DBG_PRINT("get src.txt stat fail:");
return -1;
}
file_src_size = stat_src_buf.st_size;
printf("file_src_size =%d\n",file_src_size);
//set dst.txt size
if (lseek(fd_dst, file_src_size - 1, SEEK_SET) ==
-1)//如果输出文件为空或者文件长度小于映射长度,mmap可以映射成功,但对于超出内存的读和写均会发生Bus error
{
ERR_DBG_PRINT("dst.txt lseek error:");
return -1;
}
if (write(fd_dst, "", 1) != 1)
{
ERR_DBG_PRINT("dst.txt write error:");
return -1;
}
p_map_src = mmap(NULL,file_src_size, (PROT_READ | PROT_WRITE), MAP_SHARED, fd_src, 0);
if (-1 == (int)(unsigned long)p_map_src)
{
ERR_DBG_PRINT("src.txt mmap fail:");
return -1;
}
close(fd_src);//映射成功即可关闭文件描述符
p_map_dst = mmap(NULL,file_src_size, (PROT_READ | PROT_WRITE), MAP_SHARED, fd_dst, 0);
if (-1 == (int)(unsigned long)p_map_dst)
{
ERR_DBG_PRINT("dst.txt mmap fail:");
return -1;
}
close(fd_dst);//映射成功即可关闭文件描述符
printf("mmap all ok\n");
memcpy(p_map_dst,p_map_src,file_src_size);
return 0;
}
linux下time指令统计两种方式运行时间结果如下:
real | user | sys | |
read/write(缓存1k) | 0m0.085s | 0m0.001s | 0m0.084s |
read/write(缓存2k) | 0m0.054s | 0m0.002s | 0m0.052s |
read/write(缓存3k) | 0m0.049s | 0m0.001s | 0m0.048s |
read/write(缓存4k) | 0m0.036s | 0m0.003s | 0m0.032s |
read/write(缓存5k) | 0m0.058s | 0m0.001s | 0m0.039s |
read/write(缓存6k) | 0m0.029s | 0m0.000s | 0m0.028s |
read/write(缓存7k) | 0m0.029s | 0m0.000s | 0m0.028s |
read/write(缓存8k) | 0m0.026s | 0m0.000s | 0m0.026s |
mmap/memcpy |
0m0.035s | 0m0.018s | 0m0.016s |
其中src.txt的大小为22M,由上表可知,read/write读写性能和一次性读写的缓存大小有关。从表面上看,采用mmap文件映射方式和普通4K读写缓存消耗的总时间差不多,但是注意到采用mmap映射读写方式的sys时间却少的多,即进程在内核态时间少(mmap将内核态内存映射到用户态了,将在内核态的操作迁移至用户态上操作),随着系统上的进程增多,对于系统的调用就会增多(本例为文件IO的读写次数),如果系统调用对应的底层驱动有锁,那么进程被阻塞的时间就会更长,此时会出现(real > user +sys),因此我们实际编程时应该尽量减少不必要的系统调用以提高性能,如:可以减少调用次数,减少进程的内核态时间。
正如文章开头提到的mmap接口应用非常广泛,本文仅对存储映射方面做了研究,其实mmap在共享内存和巨页使用上mmap的必备的的。