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
报告一个页故障及其对应的故障原因,于是就用到了请页机制
。