isware

Linux的mmap文件内存映射机制

  在讲述文件映射的概念时, 不可避免的要牵涉到虚存(SVR 4的VM). 实际上, 文件映射是虚存的中心概念, 文件映射一方面给用户提供了一组措施, 好似用户将文件映射到自己地址空间的某个部分, 使用简单的内存访问指令读写文件;另一方面, 它也可以用于内核的基本组织模式, 在这种模式种, 内核将整个地址空间视为诸如文件之类的一组不同对象的映射. 中的传统文件访问方式是, 用 open系统调用打开文件, 然后使用read, write以及lseek等调用进行顺序或者随即的I/O. 这种方式是非常低效的, 每一次I/O操作都需要一次系统调用. 另外, 如果若干个进程访问同一个文件, 每个进程都要在自己的地址空间维护一个副本, 浪费了内存空间. 而如果能够通过一定的机制将页面映射到进程的地址空间中, 也就是说通过简单的产生某些内存管理数据结构完成映射的创建. 当进程访问页面时产生一个缺页中断, 内核将页面读入内存并且更新页表指向该页面. 这种方式非常方便于同一副本的共享.

  VM是面向对象的方法设计的, 这里的对象是指内存对象: 内存对象是一个软件抽象的概念, 它描述内存区与后备存储之间的映射. 系统可以使用多种类型的后备存储, 比如交换空间, 本地或者远程文件以及帧缓存等等. VM系统对它们统一处理, 采用同一操作集操作, 比如读取页面或者回写页面等. 每种不同的后备存储都可以用不同的方法实现这些操作. 这样, 系统定义了一套统一的接口, 每种后备存储给出自己的实现方法. 这样, 进程的地址空间就被视为一组映射到不同数据对象上的的映射组成. 所有的有效地址就是那些映射到数据对象上的地址. 这些对象为映射它的页面提供了持久性的后备存储. 映射用户可以直接寻址这些对象.

  值得提出的是, VM体系结构独立于Unix系统, 所有的Unix系统语义, 如正文, 数据及堆栈区都可以建构在基本VM系统之上. 同时, VM体系结构也是独立于存储管理的, 存储管理是由操作系统实施的, 如: 究竟采取什么样的对换和请求调页算法, 究竟是采取分段还是分页机制进行存储管理, 究竟是如何将虚拟地址转换成为物理地址等等(Linux中是一种叫Three Level Page Table的机制), 这些都与内存对象的概念无关.

  下面介绍Linux中 VM的实现.

  一个进程应该包括一个mm_struct(memory manage struct), 该结构是进程虚拟地址空间的抽象描述, 里面包括了进程虚拟空间的一些管理信息: start_code, end_code, start_data, end_data, start_brk, end_brk等等信息. 另外, 也有一个指向进程虚存区表(vm_area_struct: virtual memory area)的指针, 该链是按照虚拟地址的增长顺序排列的. 在Linux进程的地址空间被分作许多区(vma), 每个区(vma)都对应虚拟地址空间上一段连续的区域, vma是可以被共享和保护的独立实体, 这里的vma就是前面提到的内存对象. 下面是vm_area_struct的结构, 其中, 前半部分是公共的, 与类型无关的一些数据成员, 如: 指向mm_struct的指针, 地址范围等等, 后半部分则是与类型相关的成员, 其中最重要的是一个指向vm_operation_struct向量表的指针vm_ops, vm_pos向量表是一组虚函数, 定义了与vma类型无关的接口. 每一个特定的子类, 即每种vma类型都在向量表中实现这些操作. 这里包括了: open, close, unmap, protect, sync, nopage, wppage, swapout这些操作.

  struct vm_area_struct {

  /*公共的, 与vma类型无关的 */

  struct mm_struct * vm_mm;

  unsigned long vm_start;

  unsigned long vm_end;

  struct vm_area_struct *vm_next;

  pgprot_t vm_page_prot;

  unsigned long vm_flags;

  short vm_avl_height;

  struct vm_area_struct * vm_avl_left;

  struct vm_area_struct * vm_avl_right;

  struct vm_area_struct *vm_next_share;

  struct vm_area_struct **vm_pprev_share;

  /* 与类型相关的 */

  struct vm_operations_struct * vm_ops;

  unsigned long vm_pgoff;

  struct file * vm_file;

  unsigned long vm_raend;

  void * vm_private_data;

  };

  vm_ops: open, close, no_page, swapin, swapout……

  介绍完VM的基本概念后, 我们可以讲述mmap和munmap系统调用了. mmap调用实际上就是一个内存对象vma的创建过程, mmap的调用格式是:

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

  其中start是映射地址, length是映射长度, 如果flags的MAP_FIXED不被置位, 则该参数通常被忽略, 而查找进程地址空间中第一个长度符合的空闲区域;Fd是映射文件的文件句柄, offset是映射文件中的偏移地址;prot是映射保护权限, 可以是PROT_EXEC, PROT_READ, PROT_WRITE, PROT_NONE, flags则是指映射类型, 可以是MAP_FIXED, MAP_PRIVATE, MAP_SHARED, 该参数被 指定为MAP_PRIVATE和MAP_SHARED其中之一, MAP_PRIVATE 是创建一个写时拷贝映射(copy-on-write), 也就是说如果有多个进程同时映射到一个文件上, 映射建立时只是共享同样的存储页面, 但是某进程企图修改页面内容, 则复制一个副本给该进程私用, 它的任何修改对其它进程都不可见. 而MAP_SHARED则无论修改与否都使用同一副本, 任何进程对页面的修改对其它进程都是可见的.

  mmap系统调用的实现过程是:

  1.先通过文件系统定位要映射的文件;

  2.权限检查, 映射的权限不会超过文件打开的方式, 也就是说如果文件是以只读方式打开, 那么则不允许建立一个可写映射;

  3.创建一个vma对象, 并对之进行初始化;

  4.调用映射文件的mmap函数, 其主要工作是给vm_ops向量表赋值;

  5.把该vma链入该进程的vma链表中, 如果可以和前后的vma合并则合并;

  6.如果是要求VM_LOCKED(映射区不被换出)方式映射, 则发出缺页请求, 把映射页面读入内存中.

  munmap(void * start, size_t length):

  该调用可以看作是 mmap的一个逆过程. 它将进程中从start开始length长度的一段区域的映射关闭, 如果该区域不是恰好对应一个vma, 则有可能会分割几个或几个vma.

  msync(void * start, size_t length, int flags):

  把映射区域的修改回写到后备存储中. munmap 时并不保证页面回写, 如果不调用msync, 那么有可能在munmap后丢失对映射区的修改. 其中flags可以是MS_SYNC, MS_ASYNC, MS_INVALIDATE, MS_SYNC要求回写完成后才返回, MS_ASYNC发出回写请求后立即返回, MS_INVALIDATE使用回写的内容更新该文件的其它映射. 该系统调用是通过调用映射文件的sync函数来完成工作的.

  brk(void * end_data_segement):

  将进程的数据段扩展到 end_data_segement指定的地址, 该系统调用和mmap的实现方式十分相似, 同样是产生一个vma, 然后指定其属性. 不过在此之前需要做一些合法性检查, 比如该地址是否大于mm->end_code, end_data_segement和mm->brk之间是否还存在其它vma等等. 通过brk产生的vma映射的文件为空, 这和匿名映射产生的vma相似, 关于匿名映射不做进一步介绍. 库函数malloc就是通过brk实现的.

  Linux提供了内存映射函数mmap, 它把文件内容映射到一段内存上(准确说是虚拟内存上), 通过对这段内存的读取和修改, 实现对文件的读取和修改, 先来看一下mmap的函数声明:

  头文件:

  <unistd.h>

  <sys/mman.h>

  原型: void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offsize);

  返回值: 成功则返回映射区起始地址, 失败则返回MAP_FAILED(-1).

  参数:

  addr: 指定映射的起始地址, 通常设为NULL, 由系统指定.

  length: 将文件的多大长度映射到内存.

  prot: 映射区的保护方式, 可以是:

  PROT_EXEC: 映射区可被执行.

  PROT_READ: 映射区可被读取.

  PROT_WRITE: 映射区可被写入.

  PROT_NONE: 映射区不能存取.

  flags: 映射区的特性, 可以是:

  MAP_SHARED: 对映射区域的写入数据会复制回文件, 且允许其他映射该文件的进程共享.

  MAP_PRIVATE: 对映射区域的写入操作会产生一个映射的复制(copy-on-write), 对此区域所做的修改不会写回原文件.

  此外还有其他几个flags不很常用, 具体查看linux C函数说明.

  fd: 由open返回的文件描述符, 代表要映射的文件.

  offset: 以文件开始处的偏移量, 是分页大小的整数倍, 通常为0, 表示从文件头开始映射.

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

  用open系统调用打开文件, 并返回描述符fd.

  用mmap建立内存映射, 并返回映射首地址指针start.

  对映射(文件)进行各种操作, 显示(printf), 修改(sprintf).

  用munmap(void *start, size_t lenght)关闭内存映射.

  用close系统调用关闭文件fd.

  注意事项:

  在修改映射的文件时, 只能在原长度上修改, 不能增加文件长度, 内存是已经分配好的.

  Linux-mmap函数介绍

  mmap函数是unix/linux下的系统调用,来看《Unix Netword programming》卷二12.2节对mmap的介绍:

  The mmap function maps either a file or a Posix shared memory object into the address space of a process.We use this function for three purposes:

  1. with a regular file to provide memory-mapped I/O

  2. with special files to provide anonymous memory mappings

  3. with shm_open to provide Posix shared memory between unrelated processes

  mmap系统调用并不是完全为了用于共享内存而设计的.它本身提供了不同于一般对普通文件的访问方式,进程可以像读写内存一样对普通文件的操作.而 Posix或系统V的共享内存IPC则纯粹用于共享目的,当然mmap()实现共享内存也是其主要应用之一.

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

  我们的程序中大量运用了mmap,用到的正是mmap的这种“像访问普通内存一样对文件进行访问”的功能.实践证明,当要对一个文件频繁的进行访问,并且指针来回移动时,调用mmap比用常规的方法快很多.

  来看看mmap的定义:

  void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);

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

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

  prot参数指定共享内存的访问权限.可取如下几个值的或:PROT_READ(可读),PROT_WRITE(可写),PROT_EXEC(可执行),PROT_NONE(不可访问).

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

  如果指定为MAP_SHARED,则对映射的内存所做的修改同样影响到文件.如果是MAP_PRIVATE,则对映射的内存所做的修改仅对该进程可见,对文件没有影响.

  offset参数一般设为0,表示从文件头开始映射.

  参数addr指定文件应被映射到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成.函数的返回值为文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址.

  ,举个例子来结束本节.4.2节说过,Fileinformation数组是以二进制的形式写进一个叫inforindex的文件中.那么,当要访问 Fileinformation数组时,代码类似这样:

  struct stat st;

  char buffer=” inforindex”;

  Fileinformation *_fileinfoIndexptr = NULL;

  if(stat(buffer,&st)<0)

 

  fprintf(stderr,"error to stat %sn",buffer);

  exit(-1);

  }

  // mmap the inforindex to _fileinfoIndexptr

  int fd=open(buffer, O_RDONLY);

  if(fd<0)

  {

  printf("error to open %sn",buffer);

  exit(-1);

  }

  _fileinfoIndexptr = (Fileinformation*)mmap(NULL,st.st_size, PROT_READ,MAP_SHARED,fd,0);

  if(MAP_FAILED == _fileinfoIndexptr)

  {

  printf("error to mmap %sn",buffer);

  close(fd);

  exit(-1);

  }

  close(fd);

  下面这个例子显示了把文件映射到内存的方法

  源代码是:

  /************关于本文档********************************************

  *filename: mmap.c

  *purpose: 说明调用mmap把文件映射到内存的方法

  *wrote by: zhoulifa(zhoulifa@163.com) 周立发(http://zhoulifa.bokee.com)

  Linux爱好者 Linux知识传播者 SOHO族 开发者 最擅长C语言

  *date time:2008-01-27 18:59 上海大雪天,据说是多年不遇

  *Note: 任何人可以任意复制代码并运用这些文档,当然包括你的商业用途

  * 但请遵循GPL

  *Thanks to:

  * Ubuntu 本程序在Ubuntu 7.10系统上测试完全正常

  * Google.com 我通常通过google搜索发现许多有用的资料

  *Hope:希望越来越多的人贡献自己的力量,为科学技术发展出力

  * 科技站在巨人的肩膀上进步更快!感谢有开源前辈的贡献!

  *********************************************************************/

  #include <sys/mman.h> /* for mmap and munmap */

  #include <sys/types.h> /* for open */

  #include <sys/stat.h> /* for open */

  #include <fcntl.h> /* for open */

  #include <unistd.h> /* for lseek and write */

  #include <stdio.h>

  int main(int argc, char **argv)

  {

  int fd;

  char *mapped_mem, * p;

  int flength = 1024;

  void * start_addr = 0;

  fd = open(argv[1], O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);

  flength = lseek(fd, 1, SEEK_END);

  write(fd, "", 1); /* 在文件添加一个空字符,以便下面printf正常工作 */

  lseek(fd, 0, SEEK_SET);

  mapped_mem = mmap(start_addr, flength, PROT_READ, //允许读

  MAP_PRIVATE, //不允许其它进程访问此内存区域

  fd, 0);

  /* 使用映射区域. */

  printf("%sn", mapped_mem); /* 为了保证这里工作正常,参数传递的文件名最好是一个文本文件 */

  close(fd);

  munmap(mapped_mem, flength);

  return 0;

  }

  编译运行此程序:

  gcc -Wall mmap.c

  ./a.out text_filename

  上面的方法用了PROT_READ,只能读取文件里的内容,不能修改,如果换成PROT_WRITE就可以修改文件的内容了.又 用了MAAP_PRIVATE只能此进程使用此内存区域,如果换成MAP_SHARED,则可以被其它进程访问,比如下面的:

  #include <sys/mman.h> /* for mmap and munmap */

  #include <sys/types.h> /* for open */

  #include <sys/stat.h> /* for open */

  #include <fcntl.h> /* for open */

  #include <unistd.h> /* for lseek and write */

  #include <stdio.h>

  #include <string.h> /* for memcpy */

  int main(int argc, char **argv)

  {

  int fd;

  char *mapped_mem, * p;

  int flength = 1024;

  void * start_addr = 0;

  fd = open(argv[1], O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);

  flength = lseek(fd, 1, SEEK_END);

  write(fd, "", 1); /* 在文件添加一个空字符,以便下面printf正常工作 */

  lseek(fd, 0, SEEK_SET);

  start_addr = 0x80000;

  mapped_mem = mmap(start_addr, flength, PROT_READ|PROT_WRITE, //允许写入

  MAP_SHARED, //允许其它进程访问此内存区域

  fd, 0);

  /* 使用映射区域. */

  printf("%sn", mapped_mem); /* 为了保证这里工作正常,参数传递的文件名最好是一个文本文 */

  while((p = strstr(mapped_mem, "Hello"))) { /* 此处来修改文件 内容 */

  memcpy(p, "Linux", 5);

  p = 5;

  }

  close(fd);

  munmap(mapped_mem, flength);

  return 0;

  }

  man -a mmap 看更详细的信息

posted on 2011-07-20 15:51 艾斯维亚 阅读(1022) 评论(0)  编辑 收藏 引用


只有注册用户登录后才能发表评论。
【推荐】超50万行VC++源码: 大型组态工控、电力仿真CAD与GIS源码库
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理