linux中mmap系统调用原理分析与实现

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/u013467442/article/details/49722197

参考文章:http://blog.csdn.net/shaoguangleo/article/details/5822110

linux中mmap系统调用原理分析与实现 

1、mmap系统调用(功能)
      void* mmap ( void * addr , size_t len , int prot , int flags ,int fd , off_t offset )
      内存映射函数mmap, 负责把文件内容映射到进程的虚拟内存空间, 通过对这段内存的读取和修改,来实现对文件的读取和修改,而不需要再调用read,write等操作。

2、mmap系统调用(参数)
      1)addr: 指定映射的起始地址, 通常设为NULL, 由系统指定。
      2)length: 映射到内存的文件长度。
      3) prot:   映射区的保护方式, 可以是:
             PROT_EXEC: 映射区可被执行
             PROT_READ: 映射区可被读取
             PROT_WRITE: 映射区可被写入

      4)flags: 映射区的特性, 可以是:
            MAP_SHARED:写入映射区的数据会复制回文件, 且允许其他映射该文件的进程共享。
            MAP_PRIVATE:对映射区的写入操作会产生一个映射区的复制(copy-on-write), 对此区域所做的修改不会写回原文件。

      5)fd: 由open返回的文件描述符, 代表要映射的文件。
      6)offset: 以文件开始处的偏移量, 必须是分页大小的整数倍, 通常为0, 表示从文件头开始映射。

3、解除映射
      int munmap(void *start,size_t length)
     功能:取消参数start所指向的映射内存,参数length表示欲取消的内存大小。
    返回值:解除成功返回0,否则返回-1,错误原因存于errno中。

实例分析
mmap系统调用

4、虚拟内存区域
      虚拟内存区域是进程的虚拟地址空间中的一个同质区间,即具有同样特性的连续地址范围。一个进程的内存映象由下面几部分组成:程序代码、数据、BSS
和栈区域,以及内存映射的区域。

 一个进程的内存区域可以通过查看:/proc/pid/maps
08048000-0804f000 r-xp 00000000 08:01 573748 /sbin/rpc.statd #text
0804f000-08050000 rw-p 00007000 08:01 573748 /sbin/rpc.statd #data
08050000-08055000 rwxp 00000000 00:00 0 #bss
040000000-40015000 r-xp 00000000 08:01 933965 /lib/ld2.3.2.so #text
40015000-40016000 rw-p 00014000 08:01 933965 /lib/ld-2.3.2.so #data

       每一行的域为:start_end perm offset major:minor inode
       1) Start: 该区域起始虚拟地址
       2) End: 该区域结束虚拟地址
       3) Perm: 读、写和执行权限;表示对这个区域,允许进程做什么。这个域的最后一个字符要么是p表示私有的,要么是s表示共享的。
       4) Offset: 被映射部分在文件中的起始地址
       5) Major、minor:主次设备号
       6) Inode:索引结点

5、vm_area_struct
      Linux内核使用结构vm_area_struct()来描述虚拟内存区域,其中几个主要成员如下:
     1)unsigned long vm_start   虚拟内存区域起始地址
     2)unsigned long vm_end    虚拟内存区域结束地址

     3)unsigned long vm_flags  该区域的标记。如:VM_IO和VM_RESERVED。VM_IO将该VMA标记为内存映射的IO区域,VM_IO会阻止系统将该区域包含在进程的存放转
存(core dump )中,VM_RESERVED标志内存区域不能被换出。

6、mmap设备操作
      映射一个设备是指把用户空间的一段地址关联到设备内存上。当程序读写这段用户空间的地址时,它实际上是在访问设备。

      mmap设备方法需要完成什么功能?
      mmap方法是file_oprations结构的成员,在mmap系统调用发出时被调用。在此之前,内核已经完成了很多工作。mmap设备方法所需要做的就是建立
虚拟地址到物理地址的页表。
      int (*mmap) (struct file *, struct vm_area_struct *)

      mmap如何完成页表的建立?
     方法有二:
     1)使用remap_pfn_range一次建立所有页表;
     2)使用nopage VMA方法每次建立一个页表。

     构造页表的工作可由remap_pfn_range函数完成,原型如下:
     int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,unsigned long pfn, unsigned long size, pgprot_t prot)

     vma:           虚拟内存区域指针
     virt_addr:   虚拟地址的起始值

     pfn:             要映射的物理地址所在的物理页帧号,可将物理地址>>PAGE_SHIFT得到。
     size:           要映射的区域的大小。
     prot:            VMA的保护属性。


int memdev_mmap(struct file*filp, struct vm_area_struct *vma)
{
Vma->vm_flags |= VM_IO;
Vma->vm_flags |= VM_RESERVED;
if (remap_pfn_range(vma, vma->vm_start,
virt_to_phys(dev- >data)>> PAGE_SHIFT,
size,
vma->vm_page_prot))
return -EAGAIN;
return 0;
}

7、mmap设备方法实例
       1)memdev.源码

#ifndef _MEMDEV_H_
#define _MEMDEV_H_

#ifndef MEMDEV_MAJOR
#define MEMDEV_MAJOR 0   /*预设的mem的主设备号*/
#endif

#ifndef MEMDEV_NR_DEVS
#define MEMDEV_NR_DEVS 2    /*设备数*/
#endif

#ifndef MEMDEV_SIZE
#define MEMDEV_SIZE 4096
#endif

/*mem设备描述结构体*/
struct mem_dev                                     
{                                                        
  char *data;                      
  unsigned long size;       
};

#endif /* _MEMDEV_H_ */

       2)memdev.c源码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include

#include 
#include "memdev.h"

static int mem_major = MEMDEV_MAJOR;

module_param(mem_major, int, S_IRUGO);

struct mem_dev *mem_devp; /*设备结构体指针*/

struct cdev cdev;

/*文件打开函数*/
int mem_open(struct inode *inode, struct file *filp)
{
    struct mem_dev *dev;
    
    /*获取次设备号*/
    int num = MINOR(inode->i_rdev);

    if (num >= MEMDEV_NR_DEVS) 
            return -ENODEV;
    dev = &mem_devp[num];
    
    /*将设备描述结构指针赋值给文件私有数据指针*/
    filp->private_data = dev;
    
    return 0; 
}

/*文件释放函数*/
int mem_release(struct inode *inode, struct file *filp)
{
  return 0;
}
static int memdev_mmap(struct file*filp, struct vm_area_struct *vma)
{
      struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/
      
      vma->vm_flags |= VM_IO;
      vma->vm_flags |= VM_RESERVED;

     
      if (remap_pfn_range(vma,vma->vm_start,virt_to_phys(dev->data)>>PAGE_SHIFT, vma->vm_end - vma->vm_start, vma->vm_page_prot))
          return  -EAGAIN;
                
      return 0;
}

/*文件操作结构体*/
static const struct file_operations mem_fops =
{
  .owner = THIS_MODULE,
  .open = mem_open,
  .release = mem_release,
  .mmap = memdev_mmap,
};

/*设备驱动模块加载函数*/
static int memdev_init(void)
{
  int result;
  int i;

  dev_t devno = MKDEV(mem_major, 0);

  /* 静态申请设备号*/
  if (mem_major)
    result = register_chrdev_region(devno, 2, "memdev");
  else  /* 动态分配设备号 */
  {
    result = alloc_chrdev_region(&devno, 0, 2, "memdev");
    mem_major = MAJOR(devno);
  }  
  
  if (result < 0)
    return result;

  /*初始化cdev结构*/
  cdev_init(&cdev, &mem_fops);
  cdev.owner = THIS_MODULE;
  cdev.ops = &mem_fops;
  
  /* 注册字符设备 */
  cdev_add(&cdev, MKDEV(mem_major, 0), MEMDEV_NR_DEVS);
   
  /* 为设备描述结构分配内存*/
  mem_devp = kmalloc(MEMDEV_NR_DEVS * sizeof(struct mem_dev), GFP_KERNEL);
  if (!mem_devp)    /*申请失败*/
  {
    result =  - ENOMEM;
    goto fail_malloc;
  }
  memset(mem_devp, 0, sizeof(struct mem_dev));
  
  /*为设备分配内存*/
  for (i=0; i < MEMDEV_NR_DEVS; i++) 
  {
        mem_devp[i].size = MEMDEV_SIZE;
        mem_devp[i].data = kmalloc(MEMDEV_SIZE, GFP_KERNEL);
        memset(mem_devp[i].data, 0, MEMDEV_SIZE);
  }
    
  return 0;

  fail_malloc: 
  unregister_chrdev_region(devno, 1);
  
  return result;
}

/*模块卸载函数*/
static void memdev_exit(void)
{
  cdev_del(&cdev);   /*注销设备*/
  kfree(mem_devp);     /*释放设备结构体内存*/
  unregister_chrdev_region(MKDEV(mem_major, 0), 2); /*释放设备号*/
}

MODULE_AUTHOR("yinjiabin);
MODULE_LICENSE("GPL");

module_init(memdev_init);
module_exit(memdev_exit);

 

3)测试程序源码

#include 
#include
#include
#include
#include
#include

int main()
{
 int fd;
 char *start;
 //char buf[100];
 char *buf;
 
 /*打开文件*/
 fd = open("/dev/memdev0",O_RDWR);
        
 buf = (char *)malloc(100);
 memset(buf, 0, 100);
 start=mmap(NULL,100,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
 
 /* 读出数据 */
 strcpy(buf,start);
 sleep (1);
 printf("buf 1 = %s\n",buf); 

 /* 写入数据 */
 strcpy(start,"Buf Is Not Null!");
 
 memset(buf, 0, 100);
 strcpy(buf,start);
 sleep (1);
 printf("buf 2 = %s\n",buf);

       
 munmap(start,100); /*解除映射*/
 free(buf);
 close(fd);  
 return 0; 
}




Linux内核中,关于虚存管理的最基本的管理单元应该是struct vm_area_struct了,它描述的是一段连续的、具有相同访问属性的虚存空间,该虚存空间的大小为物理内存页面的整数倍

  下面是struct vm_area_struct结构体的定义:

  1. <span style="font-family:Microsoft YaHei;">/* 
  2. * This struct defines a memory VMM memory area. There is color: black; background-color: #a0ffff;">vm_area_struct { 
  3. struct mm_struct * vm_mm; /* VM area parameters */  
  4. unsigned long vm_start;  
  5. unsigned long vm_end;  
  6.   
  7. /* linked list of VM areas per task, sorted by address */  
  8. struct vm_area_struct *vm_next;  
  9.   
  10. pgprot_t vm_page_prot;  
  11. unsigned long vm_flags;  
  12.   
  13. /* AVL tree of VM areas per task, sorted by address */  
  14. short vm_avl_height;  
  15. struct vm_area_struct * vm_avl_left;  
  16. struct vm_area_struct * vm_avl_right;  
  17.   
  18. /* For areas with an address space and backing store, 
  19. * font-size: 10px;">vm_area_struct *vm_next_share; 
  20. struct vm_area_struct **vm_pprev_share; 
  21.  
  22. struct vm_operations_struct * vm_ops; 
  23. unsigned long vm_pgoff; /* offset in PAGE_SIZE units, *not* PAGE_CACHE_SIZE */  
  24. struct file * vm_file;  
  25. unsigned long vm_raend;  
  26. void * vm_private_data; /* was vm_pte (shared mem) */  
  27. }; </span>  
  1. <span style="font-family:Microsoft YaHei;">/* 
  2. * This struct defines a memory VMM memory area. There is color: black; background-color: #a0ffff;">vm_area_struct { 
  3. struct mm_struct * vm_mm; /* VM area parameters */  
  4. unsigned long vm_start;  
  5. unsigned long vm_end;  
  6.   
  7. /* linked list of VM areas per task, sorted by address */  
  8. struct vm_area_struct *vm_next;  
  9.   
  10. pgprot_t vm_page_prot;  
  11. unsigned long vm_flags;  
  12.   
  13. /* AVL tree of VM areas per task, sorted by address */  
  14. short vm_avl_height;  
  15. struct vm_area_struct * vm_avl_left;  
  16. struct vm_area_struct * vm_avl_right;  
  17.   
  18. /* For areas with an address space and backing store, 
  19. * font-size: 10px;">vm_area_struct *vm_next_share; 
  20. struct vm_area_struct **vm_pprev_share; 
  21.  
  22. struct vm_operations_struct * vm_ops; 
  23. unsigned long vm_pgoff; /* offset in PAGE_SIZE units, *not* PAGE_CACHE_SIZE */  
  24. struct file * vm_file;  
  25. unsigned long vm_raend;  
  26. void * vm_private_data; /* was vm_pte (shared mem) */  
  27. }; </span>  


vm_area_struct结构所描述的虚存空间以vm_start、vm_end成员表示,它们分别保存了该虚存空间的首地址和末地址后第一个字节的地址,以字节为单位,所以虚存空间范围可以用[vm_start, vm_end)表示。

  通常,进程所使用到的虚存空间不连续,且各部分虚存空间的访问属性也可能不同。所以一个进程的虚存空间需要多个vm_area_struct结构来描述。在vm_area_struct结构的数目较少的时候,各个vm_area_struct按照升序排序,以单链表的形式组织数据(通过vm_next指针指向下一个vm_area_struct结构)。但是当vm_area_struct结构的数据较多的时候,仍然采用链表组织的化,势必会影响到它的搜索速度。针对这个问题,vm_area_struct还添加了vm_avl_hight(树高)、vm_avl_left(左子节点)、vm_avl_right(右子节点)三个成员来实现AVL树,以提高vm_area_struct的搜索速度。

  假如该vm_area_struct描述的是一个文件映射的虚存空间,成员vm_file便指向被映射的文件的file结构,vm_pgoff是该虚存空间起始地址在vm_file文件里面的文件偏移,单位为物理页面。

  一个程序可以选择MAP_SHARED或MAP_PRIVATE共享模式将一个文件的某部分数据映射到自己的虚存空间里面。这两种映射方式的区别在于:MAP_SHARED映射后在内存中对该虚存空间的数据进行修改会影响到其他以同样方式映射该部分数据的进程,并且该修改还会被写回文件里面去,也就是这些进程实际上是在共用这些数据。而MAP_PRIVATE映射后对该虚存空间的数据进行修改不会影响到其他进程,也不会被写入文件中。

  来自不同进程,所有映射同一个文件的vm_area_struct结构都会根据其共享模式分别组织成两个链表。链表的链头分别是:vm_file->f_dentry->d_inode->i_mapping->i_mmap_shared,vm_file->f_dentry->d_inode->i_mapping->i_mmap。而vm_area_struct结构中的vm_next_share指向链表中的下一个节点;vm_pprev_share是一个指针的指针,它的值是链表中上一个节点(头节点)结构的vm_next_share(i_mmap_shared或i_mmap)的地址。

  进程建立vm_area_struct结构后,只是说明进程可以访问这个虚存空间,但有可能还没有分配相应的物理页面并建立好页面映射。在这种情况下,若是进程执行中有指令需要访问该虚存空间中的内存,便会产生一次缺页异常。这时候,就需要通过vm_area_struct结构里面的vm_ops->nopage所指向的函数来将产生缺页异常的地址对应的文件数据读取出来。

  vm_flags主要保存了进程对该虚存空间的访问权限,然后还有一些其他的属性。vm_page_prot是新映射的物理页面的页表项pgprot的默认值。


=======================================

原文:http://oss.org.cn/kernel-book/ch06/6.4.2.htm

6.4.2 进程的虚拟空间

如前所述,每个进程拥有3G字节的用户虚存空间。但是,这并不意味着用户进程在这3G的范围内可以任意使用,因为虚存空间最终得映射到某个物理存储空间(内存或磁盘空间),才真正可以使用。

那么,内核怎样管理每个进程3G的虚存空间呢?概括地说,用户进程经过编译、链接后形成的映象文件有一个代码段和数据段(包括data段和bss段),其中代码段在下,数据段在上。数据段中包括了所有静态分配的数据空间,即全局变量和所有申明为static的局部变量,这些空间是进程所必需的基本要求,这些空间是在建立一个进程的运行映像时就分配好的。除此之外,堆栈使用的空间也属于基本要求,所以也是在建立进程时就分配好的,如图6.16所示:

 









 

 

 

 


                                   

进程虚拟空间(3G)

 

 

 

       

 图6.16  进程虚拟空间的划分

由图可以看出,堆栈空间安排在虚存空间的顶部,运行时由顶向下延伸;代码段和数据段则在低部,运行时并不向上延伸。从数据段的顶部到堆栈段地址的下沿这个区间是一个巨大的空洞,这就是进程在运行时可以动态分配的空间(也叫动态内存)。

进程在运行过程中,可能会通过系统调用mmap动态申请虚拟内存或释放已分配的内存,新分配的虚拟内存必须和进程已有的虚拟地址链接起来才能使用;Linux 进程可以使用共享的程序库代码或数据,这样,共享库的代码和数据也需要链接到进程已有的虚拟地址中。在后面我们还会看到,系统利用了请页机制来避免对物理内存的过分使用。因为进程可能会访问当前不在物理内存中的虚拟内存,这时,操作系统通过请页机制把数据从磁盘装入到物理内存。为此,系统需要修改进程的页表,以便标志虚拟页已经装入到物理内存中,同时,Linux 还需要知道进程虚拟空间中任何一个虚拟地址区间的来源和当前所在位置,以便能够装入物理内存。

由于上面这些原因,Linux 采用了比较复杂的数据结构跟踪进程的虚拟地址。在进程的 task_struct结构中包含一个指向 mm_struct 结构的指针。进程的mm_struct 则包含装入的可执行映象信息以及进程的页目录指针pgd。该结构还包含有指向 vm_area_struct 结构的几个指针,每个 vm_area_struct 代表进程的一个虚拟地址区间。

图6.17  进程虚拟地址示意图

图 6.17是某个进程的虚拟内存简化布局以及相应的几个数据结构之间的关系。从图中可以看出,系统以虚拟内存地址的降序排列 vm_area_struct。在进程的运行过程中,Linux 要经常为进程分配虚拟地址区间,或者因为从交换文件中装入内存而修改虚拟地址信息,因此,vm_area_struct结构的访问时间就成了性能的关键因素。为此,除链表结构外,Linux 还利用 红黑(Red_black)树来组织 vm_area_struct。通过这种树结构,Linux 可以快速定位某个虚拟内存地址。

当进程利用系统调用动态分配内存时,Linux 首先分配一个 vm_area_struct 结构,并链接到进程的虚拟内存链表中,当后续的指令访问这一内存区间时,因为 Linux 尚未分配相应的物理内存,因此处理器在进行虚拟地址到物理地址的映射时会产生缺页异常(请看请页机制),当 Linux 处理这一缺页异常时,就可以为新的虚拟内存区分配实际的物理内存。

在内核中,经常会用到这样的操作:给定一个属于某个进程的虚拟地址,要求找到其所属的区间以及vma_area_struct结构,这是由find_vma()来实现的,其实现代码在mm/mmap.c中:

 

   * Look up the first VMA which satisfies  addr< vm_end,  NULL if none. */

struct vm_area_struct * find_vma(struct mm_struct *mm, unsigned long addr)

{

        struct vm_area_struct *vma = NULL;

 

        if (mm) {

                /* Check the cache first. */

                /* (Cache hit rate is typically around 35%.) */

               vma = mm->mmap_cache;

                if (!(vma && vma->vm_end > addr&& vma->vm_start <= addr)) {

                       rb_node_t* rb_node;

 

                        rb_node = mm->mm_rb.rb_node;

                        vma = NULL;

 

                       while (rb_node) {

                              struct vm_area_struct * vma_tmp;

 

                             vma_tmp = rb_entry(rb_node, structvm_area_struct, vm_rb);

 

                              if (vma_tmp->vm_end > addr) {

                                        vma = vma_tmp;

                                       if (vma_tmp->vm_start <= addr)

                                               break;

                                      rb_node = rb_node->rb_left;

                                } else

                                        rb_node= rb_node->rb_right;

                       }

                        if (vma)

                                mm->mmap_cache = vma;

               }

        }

        return vma;

}

   这个函数比较简单,我们对其主要点给予解释:

·      参数的含义:函数有两个参数,一个是指向mm_struct结构的指针,这表示一个进程的虚拟地址空间;一个是地址,表示该进程虚拟地址空间中的一个地址。

·      条件检查:首先检查这个地址是否恰好落在上一次(最近一次)所访问的区间中。根据代码作者的注释,命中率一般达到35%,这也是mm_struct结构中设置mmap_cache指针的原因。如果没有命中,那就要在红黑树中进行搜索,红黑树与AVL树类似。

·      查找节点:如果已经建立了红黑树结构(rb_rode不为空),就在红黑树中搜索。

·      如果找到指定地址所在的区间,就把mmap_cache指针设置成指向所找到的vm_area_struct结构。

·      如果没有找到,说明该地址所在的区间还没有建立,此时,就得建立一个新的虚拟区间,再调用insert_vm_struct()函数将新建立的区间插入到vm_struct中的线性队列或红黑树中。



展开阅读全文

没有更多推荐了,返回首页