基于Linux2.4.0源码分析。
mmap:一个进程可以通过系统调用mmap(),将一个已打开的文件内容映射到它的用户空间。比之常规的文件操作,如read()、write()、lseek()等等,将文件映射到用户空间后像访问内存一样的访问文件显然要方便得多。
mmap系统调用原型为:mmap(void *start, size_t length,int prot ,int flags , int fd ,off_t offset)
参数fd:代表着一个已打开文件
参数start:为映射到用户空间中的起始位置
参数length:为长度
参数prot:用于对所映射区间的访问模式,如可写、可执行等等。
参数flags:其他控制目的。
1、在2.4.0版的内核中实现这个调用的函数为sys_mmap2()
asmlinkage long sys_mmap2(unsigned long addr, unsigned long len,
unsigned long prot, unsigned long flags,
unsigned long fd, unsigned long pgoff)
{
return do_mmap2(addr, len, prot, flags, fd, pgoff);
}
2、接着调用了do_mmap2
一般来说,系统调用mmap()将已打开文件映射到用户空间。但是有个例外,那就是可以在调用参数flags中把标志位MAP_ANONYMOUS设成1,表示没有文件,实际上只是用来”圈地“,即在指定的位置上分配空间。除此之外,操作的主体就是do_mmap_pgoff()
/* common code for old and new mmaps */
static inline long do_mmap2(
unsigned long addr, unsigned long len,
unsigned long prot, unsigned long flags,
unsigned long fd, unsigned long pgoff)
{
int error = -EBADF;
struct file * file = NULL;
flags &= ~(MAP_EXECUTABLE | MAP_DENYWRITE); //排除MAP_EXECUTABLE 和 MAP_DENYWRITE的影响
if (!(flags & MAP_ANONYMOUS)) { //MAP_ANONYMOUS设为0
file = fget(fd);
if (!file)
goto out;
}
//down() up() 操作信号量进入临界区
down(¤t->mm->mmap_sem);
error = do_mmap_pgoff(file, addr, len, prot, flags, pgoff);
up(¤t->mm->mmap_sem);
if (file)
fput(file);
out:
return error;
}
3、接着调用do_mmap_pgoof()
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;
int correct_wcount = 0;
int error;
//指针file非0表示映射的是具体的文件(而不是MAP_ANONYMOUS),所以相应file结构中的指针f_op必须指向一个file_operations数据结构,其中的
//函数指针mmap又必须指向具体文件系统所提供的mmap操作
if (file && (!file->f_op || !file->f_op->mmap))
return -ENODEV;
//使用的长度是否为0
if ((len = PAGE_ALIGN(len)) == 0)
return addr;
//使用的长度是否超出了栈大小
if (len > TASK_SIZE || addr > TASK_SIZE-len)
return -EINVAL;
/* offset overflow? */
if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)
return -EINVAL;
//超过最大映射数
/* Too many mappings? */
if (mm->map_count > MAX_MAP_COUNT)
return -ENOMEM;
/* mlock MCL_FUTURE? */
if (mm->def_flags & VM_LOCKED) {
unsigned long locked = mm->locked_vm << PAGE_SHIFT;
locked += len;
if (locked > current->rlim[RLIMIT_MEMLOCK].rlim_cur)
return -EAGAIN;
}
/* 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.
*/
if (file != NULL) {
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;
/* fall through */
case MAP_PRIVATE:
if (!(file->f_mode & FMODE_READ))
return -EACCES;
break;
default:
return -EINVAL;
}
}
/* Obtain the address to map to. we verify (or select) it and ensure
* that it represents a valid section of the address space.
*/
if (flags & MAP_FIXED) {
if (addr & ~PAGE_MASK)
return -EINVAL;
} else { //如果参数flags中的标志位MAP_FIXED为0,就表示指定的映射地址只是个参考值,不能满足时可以由内核给分配一个
//所以,就通过get_unmapped_area()在当前进程的用户空间中分配一个起始地址
addr = get_unmapped_area(addr, len);
if (!addr)
return -ENOMEM;
}
/* 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.
*/
//每个逻辑区间都要有个vm_area_struct数据结构,所以通过kmem_cache_alloc()为待映射的区间分配一个
//以前我们提到过,属性不同的区段不能区段不能共存于同一逻辑区间中,而映射到一个特定的文件也是一种属性,所以总要为之单独建立一个逻辑
//区间
vma = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
if (!vma)
return -ENOMEM;
//为vma设置各项属性
vma->vm_mm = mm;
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_flags = vm_flags(prot,flags) | mm->def_flags;
if (file) { //把为文件设置的访问权限考虑进去
VM_ClearReadHint(vma);
vma->vm_raend = 0;
if (file->f_mode & FMODE_READ)
vma->vm_flags |= VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
if (flags & MAP_SHARED) {
vma->vm_flags |= VM_SHARED | VM_MAYSHARE;
/* This looks strange, but when we don't have the file open
* for writing, we can demote the shared mapping to a simpler
* private mapping. That also takes care of a security hole
* with ptrace() writing to a shared mapping without write
* permissions.
*
* We leave the VM_MAYSHARE bit on, just to get correct output
* from /proc/xxx/maps..
*/
if (!(file->f_mode & FMODE_WRITE))
vma->vm_flags &= ~(VM_MAYWRITE | VM_SHARED);
}
} else {
vma->vm_flags |= VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
if (flags & MAP_SHARED)
vma->vm_flags |= VM_SHARED | VM_MAYSHARE;
}
vma->vm_page_prot = protection_map[vma->vm_flags & 0x0f];
vma->vm_ops = NULL;
//将参数pgoff设置到vm_eara_struct数据结构中的vm_pgoff字段
//这个参数代表者所映射内容在文件中的起点。有了这个起点,发生缺页异常时就可以根据虚存地址计算出
//相应页面在文件中的位置。所以当断开映射时,对于文件映射页面不需要像普通换入\换出页面那样在页面表项中
//指明其去向,另一方面,这也说明了为什么这样的区间必须是独立的
vma->vm_pgoff = pgoff;
vma->vm_file = NULL;
vma->vm_private_data = NULL;
/* Clear old maps */
error = -ENOMEM;
//do_munmap,检查目标地址在当前在当前进程的虚存空间是否已经在使用,如果已经在使用就要将老的映射撤销
//要是这个操作失败,要是这个操作失败,那当然不能重复映射同一个目标地址,所以就得转移到free_vma,
//把已经分配的vm_area_struct数据结构撤销
if (do_munmap(mm, addr, len))
goto free_vma;
/* Check against address space limit. */
if ((mm->total_vm << PAGE_SHIFT) + len
> current->rlim[RLIMIT_AS].rlim_cur)
goto free_vma;
/* Private writable mapping? Check memory availability.. */
if ((vma->vm_flags & (VM_SHARED | VM_WRITE)) == VM_WRITE &&
!(flags & MAP_NORESERVE) &&
!vm_enough_memory(len >> PAGE_SHIFT))
goto free_vma;
if (file) {
if (vma->vm_flags & VM_DENYWRITE) {
error = deny_write_access(file);
if (error)
goto free_vma;
correct_wcount = 1;
}
vma->vm_file = file;
get_file(file);
//每种文件都有个file_operations数据结构,其中的函数指针mmap提供了用来建立从该类文件到虚存区间的映射操作
//file->f_op->mmap就指向generic_file_mmap
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;
}
/* Can addr have changed??
*
* Answer: Yes, several device drivers can do it in their
* f_op->mmap method. -DaveM
*/
flags = vma->vm_flags;
addr = vma->vm_start;
//完成了这些检查和处理,把新建立的vm_area_struct结构插入到当前进程的mm_struct结构中
//就基本完成了do_mmap_pgoff()的操作
insert_vm_struct(mm, vma);
if (correct_wcount)
atomic_inc(&file->f_dentry->d_inode->i_writecount);
mm->total_vm += len >> PAGE_SHIFT;
if (flags & VM_LOCKED) {
mm->locked_vm += len >> PAGE_SHIFT;
//仅在要求对区间加锁时才调用make_pages_present()建立起初始的文件映射
make_pages_present(addr, addr + len);
}
return addr;
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. */
flush_cache_range(mm, vma->vm_start, vma->vm_end);
zap_page_range(mm, vma->vm_start, vma->vm_end - vma->vm_start);
flush_tlb_range(mm, vma->vm_start, vma->vm_end);
free_vma:
kmem_cache_free(vm_area_cachep, vma);
return error;
}
4、get_unmapped_area()在当前的进程用户空间中分配一个起始地址
/* Get an address range which is currently unmapped.
* For mmap() without MAP_FIXED and shmat() with addr=0.
* Return value 0 means ENOMEM.
*/
#ifndef HAVE_ARCH_UNMAPPED_AREA
unsigned long get_unmapped_area(unsigned long addr, unsigned long len)
{
struct vm_area_struct * vmm;
if (len > TASK_SIZE)
return 0;
if (!addr)
addr = TASK_UNMAPPED_BASE; //常数TASK_UNMAPPED_BAsE在include/asm-i386/processor.h中定义
//#define TASK_UNMAPPED_BASE (TASK_SIZE / 3)
addr = PAGE_ALIGN(addr);
for (vmm = find_vma(current->mm, addr); ; vmm = vmm->vm_next) {
/* At this point: (!vmm || addr < vmm->vm_end). */
//超出用户栈
if (TASK_SIZE - len < addr)
return 0;
//第一次进入vmm不为NULL时 addr + len <= vmm->vm_start一定为false
//之后是addr(上一vmm->end addr + len <= vmm->vm_start 即addr+len小于下一vmm的start 即在两个vmm之间)
//或者vmm为null遍历到最后一个vmm 还有可能是find_vma()在当前进程已经映射的虚存空间中到不到满足vma->vm_end大于给定地址的区间
if (!vmm || addr + len <= vmm->vm_start)
return addr;
addr = vmm->vm_end;
}
}
当给定的目标地址为0时,内核从(TASk_SiZE/3)即1GB处开始向上在当前进程的虚存空间空间中寻找一块足以容纳给定长度的区间,当给定的地址不为0时,则从给定的地址开始向上寻找。函数find_vma()在当前进程已经映射的虚存空间中找到第一个满足vma->vm_end大于给定地址的区间。如果找不到这么一个区间,那就说明给定的地址尚未映射,因而可以使用。
5、generic_file_mmap()函数
/* This is used for a general mmap of a disk file */
int generic_file_mmap(struct file * file, struct vm_area_struct * vma)
{
struct vm_operations_struct * ops;
struct inode *inode = file->f_dentry->d_inode;
//根据映射为专有或共享而分别指向数据结构file_private_mmap或file_shared_mmap
ops = &file_private_mmap;
if ((vma->vm_flags & VM_SHARED) && (vma->vm_flags & VM_MAYWRITE)) {
//检验用于页面写的函数是否存在
if (!inode->i_mapping->a_ops->writepage)
return -EINVAL;
ops = &file_shared_mmap;
}
if (!inode->i_sb || !S_ISREG(inode->i_mode))
return -EACCES;
//检验用于页面读的函数是否存在
if (!inode->i_mapping->a_ops->readpage)
return -ENOEXEC;
UPDATE_ATIME(inode);
//将虚存区间控制结构中的指针vm_ops设置成ops
vma->vm_ops = ops;
return 0;
}
用于检验用于页面读/写的函数writepage和readpage因该由文件的inode数据结构间接提供。在inode结构中有个指针i_mapping,它指向一个address_space数据结构,addres_space结构中的指针a_ops,它指向一个address_space_operations数据结构。不同的文件系统(页面交换设备可以看作是一种特殊的文件系统)有不同的address_space_operations结构。对于Ext2文件系统是ext2_aops
struct address_space_operations ext2_aops = {
readpage: ext2_readpage,
writepage: ext2_writepage,
sync_page: block_sync_page,
prepare_write: ext2_prepare_write,
commit_write: generic_commit_write,
bmap: ext2_bmap
};
这个数据结构提供了用来读/写ext2文件页面的函数ext2_readpage()和ext2_writepage()。这些有关的数据结构和指针也是在打开文件时设置好的。
为映射的建立,物理页面的换入和换出(以及映射的拆除)分别准备一些函数,这就是filemap_nopage()、ext2_readpage()以及ext2_writepage()。
那么,什么时候,由谁来调用这些函数呢?
(1)首先,当这个区间的一个页面受到首次访问时,会由于页面五映射发生缺页异常,相应的异常处理程序为do_no_page()。对于Ext2文件系统,do_no_page()会通过ext2_readpage()分配一个空闲内存页面并从文件读入相应的页面,然后建立起映射。
(2)建立起映射以后,对页面的写操作使页面变“脏”,但是页面的内容并不立即写回文件中,而是由内核线程bdflush()周期性的运行时通过page_launder()间接地调用ext2_writepage(),将页面内容写入文件。如果页面很长时间没有受到访问,则页面会耗尽它的寿命,从而在一次try_to_swap_out()中被解除映射而转入不活跃状态。如果页面是“脏”的,则也会在page_launder()中调用ext2_writepage()。我们在try_to_swap_out()的代码中,对用于文件映射的页面于普通的换入/换出页面有不同的处理。对于前者是解除页面映射,把页面表项设置成0;对于后者是断开页面映射,使页面表项指向盘上页面。
(3)解除了映射的页面在再次受到访问时又会发生缺页异常,仍旧因页面五映射而进入do_no_page(),而不像换入/换出页面那样进入do_swap_page(0。