linux mmap内存文件映射

一、传统文件访问

unix访问文件的传统方法使用open打开他们,如果有多个进程访问一个文件,则每一个进程在自己的地址空间都包含有该文件的副本,这浪费了存储空间。下面说明了两个进程同时读一个文件的同一页的情形,系统将该页从磁盘读到高速缓冲区中,每个进程再执行一个复制操作将数据从高速缓冲区读到自己的地址空间。

二、共享内存映射

现在考虑林一种处理方法:进程A和进程B都将该页映射到自己的地址空间,当进程A第一次访问该页中的数据时,它生成一个缺页终端,内核此时读入这一页到内存并更新页表使之指向它,以后,当进程B访问同一页面而出现缺页中断时,该页已经在内存,内核只需要将进程B的页表登记项指向此页即可。

三、mmap及其相关系统调用

mmap()系统调用使得进城之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用read,write等操作。

mmap()系统调用形式如下:

  1. #include<sys/mman.h>

  2.  
  3. void mmap(void *addr, size_t len, int prot,int flags, int fildes, off_t off)

  4. int msync(void *addr, size_t len, int flags);

  5. int munmap(void *addr, size_t len);

mmap的作用是映射文件描述符和指定文件的(off_t off)区域至调用进程的(addr,addr *len)的内存区域,如下图所示:

参数:

  fd:为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd可以指定为-1,此时须指定flags参数中的MAP_ANON,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的进程间进行通信)。

  len:是映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起。

  prot:指定空想内存的访问权限。可取如下几个值的或:PROT_READ(可读)、PROT_WRITE(可写)、PROT_EXEC(可执行)、PROT_NONE(不可访问)。

  flag:由以下几个常值指定:MAP_SHARED、MAP_PRIVATE、MAP_FIXED,其中,MAP_SHARED,MAP_PRIVATE必选其一,而MAP_FIXED则不推荐使用。

  offset:一般设为0,表示从文件头开始映射。

  addr:指定文件应被映射到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成

函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。

四、mmap基础用例

  1. //测试文件 data.txt 后面的程序也要用到

aaaaaaaaa
bbbbbbbbb
cccccccccccc
ddddddddd

1、通过共享内存映射的方式修改文件

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

int main(int argc, char * argv[])
{
    int fd, nread;
    struct stat sb;
    char *mapped;

    //打开文件
    if((fd = open("hao.log", O_RDWR)) < 0){
        perror("open") ;
    }

    //获取文件的属性
    if((fstat(fd, &sb)) == -1 ){
        perror("fstat") ;
    }

    //将文件映射至进程的地址空间
    if((mapped = (char*)mmap(NULL, sb.st_size, PROT_READ|
                      PROT_WRITE, MAP_SHARED, fd, 0)) ==(void*) -1){
        perror("mmap") ;
    }

    //修改一个字符,同步到磁盘文件
    mapped[20] = '9';
    if((msync((void *)mapped, sb.st_size, MS_SYNC)) == -1){
        perror("msync") ;

        //释放存储映射区
        if((munmap((void *)mapped, sb.st_size)) == -1){
            perror("munmap");
        }

        return 0;
    }
}

不能改变文件大小:

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

int main(int argc, char * argv[])
{
    int fd, nread;
    struct stat sb;
    char *mapped;

    //打开文件
    if((fd = open("hao.log", O_RDWR)) < 0){
        perror("open") ;
    }

    //获取文件的属性
    if((fstat(fd, &sb)) == -1 ){
        perror("fstat") ;
    }

    //将文件映射至进程的地址空间
    if((mapped = (char*)mmap(NULL, sb.st_size + 50, PROT_READ|
                      PROT_WRITE, MAP_SHARED, fd, 0)) ==(void*) -1){
        perror("mmap") ;
    }

    //修改一个字符,同步到磁盘文件
    mapped[20] = '9';
    int i=0;
    for(i=sb.st_size+10; i>21; i--)
    {
        mapped[i] = mapped[i-10];
    }

    for(i=10; i<15; i++)
    {
        mapped[i] = 'k';
    }

    if((msync((void *)mapped, sb.st_size + 50, MS_SYNC)) == -1){
        perror("msync") ;
    }

    //释放存储映射区
    if((munmap((void *)mapped, sb.st_size + 50)) == -1){
        perror("munmap");
    }

    return 0;
}

结果:

aaaaaaaaa
kkkkkbbbb
9cbbbbbbb
9ccccccccccc

2 私有映射无法修改文件

  1. //将文件私有映射到进程的地址空间

  2. if((mapped = (char *)mmap(NULL,sb.st_size,PROT_READ|

  3. PROT_WRITE, MAP_PRIVATE, fd, 0))==(void *)-1){

  4. perror("mmap");

五、使用共享内存映射实现两个进程之间的通信

两个程序映射到同一个文件到自己的地址空间,进程A先运行,每两秒读取映射区域,看是否发生变化,进程B后运行,它修改映射区域,然后退出,此时进程A能够观察到存储映射区的变化

进程A的代码:

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

int main(int argc, char **argv)
{
    int fd, nread;
    struct stat sb;
    char *mapped;
    /* 打开文件 */
    if ((fd = open(argv[1], O_RDWR)) < 0) {
        perror("open");
    }

    /* 获取文件的属性 */
    if ((fstat(fd, &sb)) == -1) {
        perror("fstat");
    }

    /* 将文件映射至进程的地址空间 */
    if ((mapped = (char *)mmap(NULL, sb.st_size, PROT_READ 
               | PROT_WRITE, MAP_SHARED, fd, 0)) == (void *)-1) {
        perror("mmap");
    }

    /* 文件已在内存, 关闭文件也可以操纵内存 */
    close(fd);

    /* 每隔两秒查看存储映射区是否被修改 */
    while (1) {
        printf("%s\n", mapped);
        sleep(2);
    }

    return 0;
}

进程B的代码

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

int main(int argc, char **argv)
{
    int fd;
    struct stat sb;
    char *mapped;
    
    /* 打开文件 */
    if ((fd = open(argv[1], O_RDWR)) < 0) {
        perror("open");
    }
    
    /* 获取文件的属性 */
    if ((fstat(fd, &sb)) == -1) {
        perror("fstat");
    }
    
    /* 私有文件映射将无法修改文件 */
    if ((mapped = (char *)mmap(NULL, sb.st_size, PROT_READ
                               |PROT_WRITE,MAP_PRIVATE, fd, 0)) == (void*)-1) {
        perror("mmap");
    }
    
    /* 映射完后, 关闭文件也可以操纵内存 */
    close(fd);
    
    /* 修改一个字符 */
    mapped[20] = '9';
    return 0;
}

六、通过匿名映射实现父子进程通信

 

#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define BUF_SIZE 100
int main(int argc, char** argv)
{
    char *p_map;
    
    /* 匿名映射,创建一块内存供父子进程通信 */
    p_map = (char *)mmap(NULL, BUF_SIZE, PROT_READ 
                         | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    
    if(fork() == 0) {
        sleep(1);
        printf("child got a message: %s\n", p_map);
        sprintf(p_map, "%s", "hi, dad, this is son");
        
        munmap(p_map, BUF_SIZE); //实际上,进程终止时,会自动解除映射。
        exit(0);
    }
    
    sprintf(p_map, "%s", "hi, this is father");
    sleep(2);
    
    printf("parent got a message: %s\n", p_map);
    return 0;
}

七、对mmap()返回地址的访问

linux采用的是页式管理机制。对于用mmap()映射普通文件来说,进程会在自己的地址空间新增一块空间,空间大小由mmap()的len参数指定,注意,进程并不一定能够对全部新增空间都能进行有效访问。进程能够访问的有效地址大小取决于文件被映射部分的大小。简单的说,能够容纳文件被映射部分大小的最少页面个数决定了进程从mmap()返回的地址开始,能够有效访问的地址空间大小。超过这个空间大小,内核会根据超过的严重程度返回发送不同的信号给进程。可用如下图示说明:

总结一下就是,文件大小,mmap()的参数len都不能决定进程能访问的大小,而是容纳文件被映射部分的最小页面数决定进程能访问的大小,下面看一个实例:

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

int main(int argc, char** argv)
{
    int fd,i;
    int pagesize,offset;
    char *p_map;
    struct stat sb;

    /* 取得page size */
    pagesize = sysconf(_SC_PAGESIZE);
    printf("pagesize is %d\n",pagesize);
    /* 打开文件 */
    fd = open("hao.log", O_RDWR, 00777);

    fstat(fd, &sb);
    printf("file size is %zd\n", (size_t)sb.st_size);

    offset = 0;
    p_map = (char *)mmap(NULL, pagesize * 2, PROT_READ|PROT_WRITE, MAP_SHARED, fd, offset);

    close(fd);

    p_map[sb.st_size] = '9'; /* 导致总线错误 */
    p_map[pagesize] = '9'; /* 导致段错误 */

    munmap(p_map, pagesize * 2);

    return 0;
}

 

api说明:

void *mmap(void *start, size_t length, int prot, int flags,  int fd, off_t offset);
int munmap(void *start, size_t length); 
参数:   
start:映射区的开始地址。
length:映射区的长度。
prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起
PROT_EXEC //页内容可以被执行
PROT_READ  //页内容可以被读取
PROT_WRITE //页可以被写入
PROT_NONE  //页不可访问
flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体
MAP_FIXED //使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。
MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。
MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。
MAP_DENYWRITE //这个标志被忽略。
MAP_EXECUTABLE //同上
MAP_NORESERVE //不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。
MAP_LOCKED //锁定映射区的页面,从而防止页面被交换出内存。
MAP_GROWSDOWN //用于堆栈,告诉内核VM系统,映射区可以向下扩展。
MAP_ANONYMOUS //匿名映射,映射区不与任何文件关联。
MAP_ANON //MAP_ANONYMOUS的别称,不再被使用。
MAP_FILE //兼容标志,被忽略。
MAP_32BIT //将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。
MAP_POPULATE //为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。
MAP_NONBLOCK //仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。
fd:有效的文件描述词。如果MAP_ANONYMOUS被设定,为了兼容问题,其值应为-1。
offset:被映射对象内容的起点。
  
    
返回说明:   
成功执行时,mmap()返回被映射区的指针,munmap()返回0。失败时,mmap()返回MAP_FAILED[其值为(void *)-1],munmap返回-1。errno被设为以下的某个值   
EACCES:访问出错
EAGAIN:文件已被锁定,或者太多的内存已被锁定
EBADF:fd不是有效的文件描述词
EINVAL:一个或者多个参数无效
ENFILE:已达到系统对打开文件的限制
ENODEV:指定文件所在的文件系统不支持内存映射
ENOMEM:内存不足,或者进程已超出最大内存映射数量
EPERM:权能不足,操作不允许
ETXTBSY:已写的方式打开文件,同时指定MAP_DENYWRITE标志
SIGSEGV:试着向只读区写入
SIGBUS:试着访问不属于进程的内存区

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值