6. 4 地址映射机制
顾名思义地址映射就是建立几种存储媒介(内存,辅存,虚存)间的关联,完成地址间的相互转换,它既包括磁盘文件到虚拟内存的映射,也包括虚拟内存到物理内存的映射,如图6.13 所示。本节主要讨论磁盘文件到虚拟内存的映射,虚拟内存到物理内存的映射实际上是请页机制 完成的( 请看下节) 。
图6.13 存储介质间的映射关系
6.4.1 描述虚拟空间的数据结构
前几节介绍的数据结构如存储节点(node )、管理区(zone )、页面(page )及空闲区(free_area )都用于物理空间的管理。这一节主要关注虚拟空间的管理。虚拟空间的管理是以进程 为基础的,每个进程都有各自的虚存空间(或叫用户空间,地址空间),除此之外,每个进程的“内核空间”是为所有的进程所共享的。
一个进程的虚拟地址空间主要由两个数据结来描述。一个是最高层次的:mm_struct ,一个是较高层次的:vm_area_structs 。最高层次的mm_struct 结构描述了一个进程的整个虚拟地址空间。较高层次的结构vm_area_truct 描述了虚拟地址空间的一个区间(简称虚拟区)。
1. MM_STRUCT 结构
mm_strcut 用来描述一个进程的虚拟地址空间,在/include/linux/sched.h 中描述如下:
struct mm_struct {
struct vm_area_struct * mmap; /* 指向虚拟区间(VMA )链表 */
rb_root_t mm_rb; /* 指向red_black 树*/
struct vm_area_struct * mmap_cache; /* 指向最近找到的虚拟区间*/
pgd_t * pgd; /* 指向进程的页目录*/
atomic_t mm_users; /* 用户空间中的有多少用户*/
atomic_t mm_count; /* 对"struct mm_struct" 有多少引用*/
int map_count; /* 虚拟区间的个数*/
struct rw_semaphore mmap_sem;
spinlock_t page_table_lock; /* 保护任务页表和 mm->rss */
struct list_head mmlist; /* 所有活动(active )mm 的链表 */
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
unsigned long rss, total_vm, locked_vm;
unsigned long def_flags;
unsigned long cpu_vm_mask;
unsigned long swap_address;
unsigned dumpable:1;
/* Architecture-specific MM context */
mm_context_t context;
};
对该结构进一步说明如下:
· 在内核代码中,指向这个数据结构的变量常常是mm 。
· 每个进程只有一个mm_struct 结构,在每个进程的task_struct 结构中,有一个指向该进程的结构。可以说,mm_struct 结构是对整个用户空间的描述。
· 一个进程的虚拟空间中可能有多个虚拟区间(参见下面对vm_area_struct 描述),对这些虚拟区间的组织方式有两种,当虚拟区较少时采用单链表,由mmap 指针指向这个链表,当虚拟区间多时采用“红黑树(red_black tree )”结构,由mm_rb 指向这颗树。在2.4.10 以前的版本中,采用的是AVL 树,因为与AVL 树相比,对红黑树进行操作的效率更高。
· 因为程序中用到的地址常常具有局部性,因此,最近一次用到的虚拟区间很可能下一次还要用到,因此,把最近用到的虚拟区间结构应当放入高速缓存,这个虚拟区间就由mmap_cache 指向。
· 指针pgt 指向该进程的页目录(每个进程都有自己的页目录,注意同内核页目录的区别), 当调度程序调度一个程序运行时,就将这个地址转成物理地址,并写入控制寄存器(CR3 )。
· 由于进程的虚拟空间及其下属的虚拟区间有可能在不同的上下文中受到访问,而这些访问又必须互斥,所以在该结构中设置了用于P 、V 操作的信号量mmap_sem 。此外,page_table_lock 也是为类似的目的而设置。
· 虽然每个进程只有一个虚拟地址空间,但这个地址空间可以被别的进程来共享,如,子进程共享父进程的地址空间(也即共享mm_struct 结构)。所以,用mm_user 和mm_count 进行计数。类型atomic_t 实际上就是整数,但对这种整数的操作必须是“原子”的。
· 另外,还描述了代码段、数据段、堆栈段、参数段以及环境段的起始地址和结束地址。这里的段是对程序的逻辑划分,与我们前面所描述的段机制 是不同的。
· mm_context_t 是与平台相关的一个结构,对i386 几乎用处不大。
在后面对代码的分析中对有些域给予 进一步说明。
2. VM_AREA_STRUCT 结构
vm_area_struct 描述进程的一个虚拟地址区间,在/include/linux/mm.h 中描述如下:
struct vm_area_struct
struct mm_struct * vm_mm; /* 虚拟区间所在的地址空间*/
unsigned long vm_start; /* 在vm_mm 中的起始地址*/
unsigned long vm_end; /* 在vm_mm 中的结束地址 */
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next;
pgprot_t vm_page_prot; /* 对这个虚拟区间的存取权限 */
unsigned long vm_flags; /* 虚拟区间的标志. */
rb_node_t vm_rb;
/*
* For areas with an address space and backing store,
* one of the address_space->i_mmap{,shared} lists,
* for shm areas, the list of attaches, otherwise unused.
*/
struct vm_area_struct *vm_next_share;
struct vm_area_struct **vm_pprev_share;
/* 对这个区间进行操作的函数 */
struct vm_operations_struct * vm_ops;
/* Information about our backing store: */
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
units , *not* PAGE_CACHE_SIZE */
struct file * vm_file; /* File we map to (can be NULL). */
unsigned long vm_raend; /* XXX: put full readahead info here. */
void * vm_private_data; /* was vm_pte (shared mem) */
};
vm_flag 是描述对虚拟区间的操作的标志,其定义和描述如下:
表6.1 虚拟区间的标志
标志名 描述
VM_DENYWRITE 在这个区间映射一个打开后不能用来写的文件。
VM_EXEC 页可以 被执行。
VM_EXECUTABLE 页含有可执行代码。
VM_GROWSDOWN 这个区间可以向低地址扩展。
VM_GROWSUP 这个区间可以向高地址扩展。
VM_IO 这个区间映射一个设备的I/O 地址空间。
VM_LOCKED 页被锁住不能被交换出去。
VM_MAYEXEC VM_EXEC 标志可以被设置。
VM_MAYREAD VM_READ 标志可以被设置。
VM_MAYSHARE VM_SHARE 标志可以被设置。
VM_MAYWRITE VM_WRITE 标志可以被设置。
VM_READ 页是可读的。
VM_SHARED 页可以 被多个进程共享。
VM_SHM 页用于 IPC 共享内存。
VM_WRITE 页是可写的。
较高层次的结构vm_area_structs 是由双向链表连接起来的,它们是按虚地址的降顺序来排列的,每个这样的结构都对应描述一个相邻的地址空间范围。之所以这样分割,是因为每个虚拟区间可能来源不同,有的可能来自可执行映象 ,有的可能来自共享库,而有的则可能是动态分配的内存区,所以对每一个由vm_area_structs 结构所描述的区间的处理操作和它前后范围的处理操作不同。因此Linux 把虚拟内存分割管理,并利用了虚拟内存处理例程(vm_ops )来抽象对不同来源虚拟内存的处理方法。不同的虚拟区间其处理操作可能不同,Linux 在这里利用 了面向对象的思想,即把一个虚拟区间看成一个对象,用vm_area_structs 描述了这个对象的属性,其中的vm_operation 结构描述了在这个对象上的操作,其定义在/include /linux /mm.h 中:
/*
* These are the virtual MM functions - opening of an area, closing and
* unmapping it (needed to keep files on disk up-to-date etc), pointer
* to the functions called when a no-page or a wp-page exception occurs.
*/
struct vm_operations_struct {
void (*open)(struct vm_area_struct * area);
void (*close)(struct vm_area_struct * area);
struct page * (*nopage)(struct vm_area_struct * area, unsigned long address, int unused);
};
vm_operations 结构中包含的是函数指针;其中,open 、close 分别用于虚拟区间的打开、关闭,而nopage 用于当虚存页面不在物理内存而引起的“缺页异常”时所应该调用的函数。如图6.14 给出虚拟区间的操作集。
图6.14 虚拟地址区间的操作集
3 .红黑树结构
Linux 内核从2.4.10 开始,对虚拟区的组织不再采用AVL 树,而是采用红黑树,这也是出于效率的考虑,虽然AVL 树和红黑树很类似,但在插入和删除节点方面,采用红黑树的性能更好一些,下面对红黑树给予 简单介绍。
一颗红黑树是具有以下特点的二叉树:
(1) 每个节点着 有颜色,或者为红,或者为黑
(2) 根节点为黑色
(3) 如果一个节点为红色,那么它的子节点必须为黑色
(4) 从一个节点到叶子节点上的所有路径都包含有相同的黑色节点数
图6.15 就是一颗红黑树。
图6.15 一颗红黑树
红黑树的结构在include/linux/rbtree.h 中定义如下:
typedef struct rb_node_s
{
struct rb_node_s * rb_parent;
int rb_color;
#define RB_RED 0
#define RB_BLACK 1
struct rb_node_s * rb_right;
struct rb_node_s * rb_left;
} rb_node_t;
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, struct vm_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 中的线性队列或红黑树中。
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_READ 、PROT_WRITE 、PROT_EXEC 和PROT_NONE 。前三个标志与标志VM_READ 、VM_WRITE 及VM_EXEC 的意义一样。PROT_NONE 表示进程没有以上三个存取权限中的任意一个。
Flag :这个参数指定虚拟区的其它标志:
MAP_GROWSDOWN ,MAP_LOCKED ,MAP_DENYWRITE 和MAP_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 位非0 ,addr 就不是一个有效的地址,就以这个地址作为返回值;否则,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_GROWSDOWN 或 VM_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_inc ( x )给 *x 加 1 ,这是一个原子操作。在内核代码中,有很多地方调用了以 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 报告一个页故障及其对应的故障原因,于是就用到了请页机制 。