6.4.3 内存映射

     当某个程序的映象开始执行时,可执行映象必须装入到进程的虚拟地址空间。如果该进程用到了任何一个共享库,则共享库也必须装入到进程的虚拟地址空间。由此可看出,Linux并不将映象装入到物理内存,相反,可执行文件只是被连接到进程的虚拟地址空间中。随着程序的运行,被引用的程序部分会由操作系统装入到物理内存,这种将映象链接到进程地址空间的方法被称为“内存映射”。

当可执行映象映射到进程的虚拟地址空间时,将产生一组 vm_area_struct 结构来描述虚拟内存区间的起始点和终止点,每个 vm_area_struct 结构代表可执行映象的一部分,可能是可执行代码,也可能是初始化的变量或未初始化的数据,这些都是在函数do_mmap()中来实现的。随着 vm_area_struct 结构的生成,这些结构所描述的虚拟内存区间上的标准操作函数也由 Linux 初始化。但要明确在这一步还没有建立从虚拟内存到物理内存的影射,也就是说还没有建立页表页目录。

 

为了对上面的原理进行具体的说明,我们来看一下do_mmap()的实现机制。

函数do_mmap()为当前进程创建并初始化一个新的虚拟区,如果分配成功,就把这个新的虚拟区与进程已有的其他虚拟区进行合并,do_mmap()include/linux/mm.h 中定义如下:

 

static inline unsigned long do_mmap(struct file *file, unsigned long addr,

         unsigned long len, unsigned long prot,

         unsigned long flag, unsigned long offset)

{

         unsigned long ret = -EINVAL;

         if ((offset + PAGE_ALIGN(len)) < offset)

                 goto out;

         if (!(offset & ~PAGE_MASK))

              ret = do_mmap_pgoff(file, addr, len, prot, flag, offset >> PAGE_SHIFT);

out:

         return ret;

}

 

函数中参数的含义如下:

file:表示要映射的文件,file结构将在第八章文件系统中进行介绍;

off:文件内的偏移量,因为我们并不是一下子全部映射一个文件,可能只是映射文件的一部分,off就表示那部分的起始位置;

len:要映射的文件部分的长度

addr:虚拟空间中的一个地址,表示从这个地址开始查找一个空闲的虚拟区;

prot: 这个参数指定对这个虚拟区所包含页的存取权限。可能的标志有PROT_READPROT_WRITEPROT_EXECPROT_NONE。前三个标志与标志VM_READVM_WRITE VM_EXEC的意义一样。PROT_NONE表示进程没有以上三个存取权限中的任意一个。

     Flag:这个参数指定虚拟区的其它标志:

 

MAP_GROWSDOWNMAP_LOCKEDMAP_DENYWRITEMAP_EXECUTABLE

它们的含义与表6.2中所列出标志的含义相同。

MAP_SHARED MAP_PRIVATE

前一个标志指定虚拟区中的页可以被许多进程共享;后一个标志作用相反。这两个标志都涉及vm_area_struct中的VM_SHARED标志。

MAP_ANONYMOUS

表示这个虚拟区是匿名的,与任何文件无关。

MAP_FIXED

这个区间的起始地址必须是由参数addr所指定的。

MAP_NORESERVE

函数不必预先检查空闲页面的数目。

do_mmap()函数对参数offset的合法性检查后,就调用do_mmap_pgoff()函数,该函数才是内存映射的主要函数,do_mmap_pgoff()的代码在mm/mmap.c中,代码比较长,我们分段来介绍:

 

unsigned long do_mmap_pgoff(struct file * file, unsigned long addr, unsigned long len,

         unsigned long prot, unsigned long flags, unsigned long pgoff)

{

         struct mm_struct * mm = current->mm;

         struct vm_area_struct * vma, * prev;

        unsigned int vm_flags;

        int correct_wcount = 0;

        int error;

         rb_node_t ** rb_link, * rb_parent;

  

        if (file && (!file->f_op || !file->f_op->mmap))

                 return -ENODEV;

    

         if ((len = PAGE_ALIGN(len)) == 0)

                return addr;

 

         if (len > TASK_SIZE)

                 return -EINVAL;

 

        /* offset overflow? */

         if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)

                 return -EINVAL;

 

        /* Too many mappings? */

         if (mm->map_count > MAX_MAP_COUNT)

                 return -ENOMEM;

   函数首先检查参数的值是否正确,所提的请求是否能够被满足,如果发生以上情况中的任何一种,do_mmap()函数都终止并返回一个负值。

 

  /* Obtain the address to map to. we verify (or select) it and ensure

    * that it represents a valid section of the address space.

   */

        addr = get_unmapped_area(file, addr, len, pgoff, flags);

         if (addr & ~PAGE_MASK)

                return addr;

     调用get_unmapped_area()函数在当前进程的用户空间中获得一个未映射区间的起始地址。PAGE_MASK的值为0xFFFFF000,因此,如果“addr & ~PAGE_MASK”为非0,说明addr最低12位非0addr就不是一个有效的地址,就以这个地址作为返回值;否则,addr就是一个有效的地址(最低12位为0),继续向下看:

 

/* Do simple checking here so the lower-level routines won't have

* to. we assume access permissions have been handled by the open

* of the memory object, so we don't do any here.

*/

vm_flags = calc_vm_flags(prot,flags) | mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;

 

    /* mlock MCL_FUTURE? */

         if (vm_flags & VM_LOCKED) {

                 unsigned long locked = mm->locked_vm << PAGE_SHIFT;

                locked += len;

                 if (locked > current->rlim[RLIMIT_MEMLOCK].rlim_cur)

                        return -EAGAIN;

        }

   如果flag参数指定的新虚拟区中的页必须锁在内存,且进程加锁页的总数超过了保存在进程的task_struct结构rlim[RLIMIT_MEMLOCK].rlim_cur域中的上限值,则返回一个负值,继续:

 

   if (file) {

                 switch (flags & MAP_TYPE) {

                 case MAP_SHARED:

                        if ((prot & PROT_WRITE) && !(file->f_mode & FMODE_WRITE))

                                return -EACCES;

 

                         /* Make sure we don't allow writing to an append-only file.. */

if (IS_APPEND(file->f_dentry->d_inode) && (file->f_mode &   FMODE_WRITE))

                                return -EACCES;

 

                        /* make sure there are no mandatory locks on the file. */

                        if (locks_verify_locked(file->f_dentry->d_inode))

                                return -EAGAIN;

 

                         vm_flags |= VM_SHARED | VM_MAYSHARE;

                        if (!(file->f_mode & FMODE_WRITE))

                                vm_flags &= ~(VM_MAYWRITE | VM_SHARED);

 

                         /* fall through */

                case MAP_PRIVATE:

                        if (!(file->f_mode & FMODE_READ))

                                return -EACCES;

                         break;

 

                default:

                         return -EINVAL;

                 }

        } else {

                vm_flags |= VM_SHARED | VM_MAYSHARE;

                switch (flags & MAP_TYPE) {

                default:

                         return -EINVAL;

                case MAP_PRIVATE:

                        vm_flags &= ~(VM_SHARED | VM_MAYSHARE);

                        /* fall through */

                 case MAP_SHARED:

                         break;

               }

        }

如果file结构指针为0,则目的仅在于创建虚拟区间,或者说,并没有真正的映射发生;如果file结构指针不为0,则目的在于建立从文件到虚拟区间的映射,那就要根据标志指定的映射种类,把为文件设置的访问权考虑进去:

·      如果所请求的内存映射是共享可写的,就要检查要映射的文件是为写入而打开的,而不是以追加模式打开的,还要检查文件上没有强制锁。

·      对于任何种类的内存映射,都要检查文件是为读操作而打开的。

 如果以上条件都不满足,就返回一个错误码。

 

/* Clear old maps */

         error = -ENOMEM;

munmap_back:

         vma = find_vma_prepare(mm, addr, &prev, &rb_link, &rb_parent);

         if (vma && vma->vm_start < addr + len) {

                 if (do_munmap(mm, addr, len))

                        return -ENOMEM;

                 goto munmap_back;

         }

   函数find_vma_prepare()与find_vma()基本相同,它扫描当前进程地址空间的vm_area_struct结构所形成的红黑树,试图找到结束地址高于addr的第一个区间;如果找到了一个虚拟区,说明addr所在的虚拟区已经在使用,也就是已经有映射存在,因此要调用do_munmap()把这个老的虚拟区从进程地址空间中撤销,如果撤销不成功,就返回一个负数;如果撤销成功,就继续查找,直到在红黑树中找不到addr所在的虚拟区,并继续下面的检查;

 

     /* Check against address space limit. */

        if ((mm->total_vm << PAGE_SHIFT) + len

            > current->rlim[RLIMIT_AS].rlim_cur)

                return -ENOMEM;

total_vm是表示进程地址空间的页面数,如果把文件映射到进程地址空间后,其长度超过了保存在当前进程rlim[RLIMIT_AS].rlim_cur中的上限值,则返回一个负数。

   

   /* Private writable mapping? Check memory availability.. */

         if ((vm_flags & (VM_SHARED | VM_WRITE)) == VM_WRITE &&

             !(flags & MAP_NORESERVE) &&!vm_enough_memory(len >> PAGE_SHIFT))

                 return -ENOMEM;

     如果flags参数中没有设置MAP_NORESERVE标志,新的虚拟区含有私有的可写页,空闲页面数小于要映射的虚拟区的大小;则函数终止并返回一个负数;其中函数vm_enough_memory()用来检查一个进程的地址空间中是否有足够的内存来进行一个新的映射。

 

           /* Can we just expand an old anonymous mapping? */

        if (!file && !(vm_flags & VM_SHARED) && rb_parent)

                 if (vma_merge(mm, prev, rb_parent, addr, addr + len, vm_flags))

                        goto out;

   如果是匿名映射(file为空),并且这个虚拟区是非共享的,则可以把这个虚拟区和与它紧挨的前一个虚拟区进行合并;虚拟区的合并是由vma_merge()函数实现的。如果合并成功,则转out处,请看后面out处的代码。

 

       /* Determine the object being mapped and call the appropriate

          * specific mapper. the address has already been validated, but

          * not unmapped, but the maps are removed from the list.

         */

         vma = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);

         if (!vma)

                 return -ENOMEM;

         vma->vm_mm = mm;

         vma->vm_start = addr;

         vma->vm_end = addr + len;

         vma->vm_flags = vm_flags;

         vma->vm_page_prot = protection_map[vm_flags & 0x0f];

        vma->vm_ops = NULL;

        vma->vm_pgoff = pgoff;

        vma->vm_file = NULL;

        vma->vm_private_data = NULL;

        vma->vm_raend = 0;

 

   经过以上各种检查后现在必须为新的虚拟区分配一个vm_area_struct结构。这是通过调用Slab分配函数kmem_cache_alloc()来实现的,然后就对这个结构的各个域进行了初始化。

 

            if (file) {

                error = -EINVAL;

                if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))

                         goto free_vma;

                if (vm_flags & VM_DENYWRITE) {

                        error = deny_write_access(file);

                         if (error)

                                goto free_vma;

                         correct_wcount = 1;

                }

                vma->vm_file = file;

                get_file(file);

                error = file->f_op->mmap(file, vma);

                if (error)

                         goto unmap_and_free_vma;

        } else if (flags & MAP_SHARED) {

                 error = shmem_zero_setup(vma);

                 if (error)

                         goto free_vma;

         }

free_vma:

        kmem_cache_free(vm_area_cachep, vma);

        return error;

}

   如果建立的是从文件到虚存区间的映射,则:

·      当参数flags中的VM_GROWSDOWNVM_GROWSUP标志位为1时,说明这个区间可以向低地址或高地址扩展,但从文件映射的区间不能进行扩展,因此转到free_vma,释放给vm_area_struct分配的Slab,并返回一个错误;

·      flags中的VM_DENYWRITE标志位为1时,就表示不允许通过常规的文件操作访问该文件,所以要调用deny_write_access()排斥常规的文件操作(参见第八章)。

·      get_file()函数的主要作用是递增file结构中的共享计数;

·      每个文件系统都有个fiel_operation数据结构,其中的函数指针mmap提供了用来建立从该类文件到虚存区间进行映射的操作,这是最具有实质意义的函数;对于大部分文件系统,这个函数为generic_file_mmap( )函数实现的,该函数执行以下操作:

(1)   初始化vm_area_struct结构中的vm_ops域。如果VM_SHARED标志为1,就把该域设置成file_shared_mmap,否则就把该域设置成file_private_mmap。从某种意义上说,这个步骤所做的事情类似于打开一个文件并初始化文件对象的方法。

(2)   从索引节点的i_mode域(参见第八章)检查要映射的文件是否是一个常规文件。如果是其他类型的文件(例如目录或套接字),就返回一个错误代码。

(3)   从索引节点的i_op域中检查是否定义了readpage( )的索引节点操作。如果没有定义,就返回一个错误代码。

(4)   调用update_atime( )函数把当前时间存放在该文件索引节点的i_atime域中,并将这个索引节点标记成脏。

·      如果flags参数中的MAP_SHARED标志位为1,则调用shmem_zero_setup()进行共享内存的映射。

    继续看do_mmap()中的代码;

 

         /* Can addr have changed??

          *

          * Answer: Yes, several device drivers can do it in their

         *         f_op->mmap method. -DaveM

          */

         addr = vma->vm_start;

     源码作者给出了解释,意思是说,addr有可能已被驱动程序改变,因此,把新虚拟区的起始地址赋给addr

 

         vma_link(mm, vma, prev, rb_link, rb_parent);

         if (correct_wcount)

                 atomic_inc(&file->f_dentry->d_inode->i_writecount);

   此时,应该把新建的虚拟区插入到进程的地址空间,这是由函数vma_link()完成的,该函数具有三方面的功能:

(1)   vma 插入到虚拟区链表中

(2)   vma插入到虚拟区形成的红黑树中

(3)   vam插入到索引节点(inode)共享链表中

   函数atomic_incx)给*x1,这是一个原子操作。在内核代码中,有很多地方调用了以atomic为前缀的函数。所谓原子操作,就是在操作过程中不会被中断。

 

out:   

         mm->total_vm += len >> PAGE_SHIFT;

         if (vm_flags & VM_LOCKED) {

                 mm->locked_vm += len >> PAGE_SHIFT;

                 make_pages_present(addr, addr + len);

         }

         return addr;

   do_mmap()函数准备从这里退出,首先增加进程地址空间的长度,然后看一下对这个区间是否加锁,如果加锁,说明准备访问这个区间,就要调用make_pages_present()函数,建立虚拟页面到物理页面的映射,也就是完成文件到物理内存的真正调入。返回一个正数,说明这次映射成功。

unmap_and_free_vma:

         if (correct_wcount)

                 atomic_inc(&file->f_dentry->d_inode->i_writecount);

         vma->vm_file = NULL;

         fput(file);

 

        /* Undo any partial mapping done by a device driver. */

         zap_page_range(mm, vma->vm_start, vma->vm_end - vma->vm_start);

如果对文件的操作不成功,则解除对该虚拟区间的页面映射,这是由zap_page_range()函数完成的。

   当你读到这里时可能感到困惑,页面的映射到底在何时建立?实际上,generic_file_mmap( )就是真正进行映射的函数。因为这个函数的实现涉及很多文件系统的内容,我们在此不进行深入的分析,当读者了解了文件系统的有关内容后,可自己进行分析。

    这里要说明的是,文件到虚存的映射仅仅是建立了一种映射关系,也就是说,虚存页面到物理页面之间的映射还没有建立。当某个可执行映象映射到进程虚拟内存中并开始执行时,因为只有很少一部分虚拟内存区间装入到了物理内存,可能会遇到所访问的数据不在物理内存。这时,处理器将向 Linux 报告一个页故障及其对应的故障原因,于是就用到了请页机制