Linux内核内存映射包括创建虚拟区间地址和改变映射区域的边界地址两部分。首先先来看创建虚拟区间地址部分,本部分代码主要在linux-2.6.33.2/mm下的mmap.c中,通过系统调用mmap可以访问来创建虚拟区间地址,下面来看该系统调用的具体实现:
先从do_mmap()函数看起:
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;
/*对offset进行合法性检查*/
if ((offset + PAGE_ALIGN(len)) < offset)
goto out;
/*调用内存映射主要函数do_mmap_pgoff */
if (!(offset & ~PAGE_MASK))
ret = do_mmap_pgoff(file, addr, len, prot, flag, offset >> PAGE_SHIFT);
out:
return ret;
}
接下来进入do_mmap_pgoff()函数,前面提过,这是内存映射 的主要函数:
unsigned long do_mmap_pgoff(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flags, unsigned long pgoff)
{
/*=============省略掉若干非重要代码==================*/
/*检查是否PROT_READ隐含着EXEC*/
if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
if (!(file && (file->f_path.mnt->mnt_flags & MNT_NOEXEC)))
prot |= PROT_EXEC;
/*=============省略掉若干非重要代码==================*/
/*该虚拟区间的地址是否必须有参数addr指定,若不是,重新获取addr地址*/
if (!(flags & MAP_FIXED))
addr = do_mmap_pgoff()(addr);
/* 检测要映射的文件部分的长度是否是页对齐 */
len = PAGE_ALIGN(len);
if (!len)
return -ENOMEM;
/* offset是否越界*/
if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)
return -EOVERFLOW;
/*映射数量限制 */
if (mm->map_count > sysctl_max_map_count)
return -ENOMEM;
/* 获得一个一个未映射区间的起始地址,并检查是否是页大小对齐的,即低12位必须*为0*/
addr = get_unmapped_area(file, addr, len, pgoff, flags);
if (addr & ~PAGE_MASK)
return addr;
/*=============省略掉若干非重要代码==================*/
/*获取文件相关节点inode*/
inode = file ? file->f_path.dentry->d_inode : NULL;
/*如果file结构指针为0,则目的仅在于创建虚拟区间,或者说,并没有真正的映射*发生;如果file结构指针不为0,则目的在于建立从文件到虚拟区间的映射,那就要*根据标志指定的映射种类,把为文件设置的访问权考虑进去*/
if (file) {
switch (flags & MAP_TYPE) {
case MAP_SHARED:
if ((prot&PROT_WRITE) && !(file->f_mode&FMODE_WRITE))
return -EACCES;
/*==========略掉若干在MAP_SHARED情形下的访问权限检查代码===========*/
/*具体检查为:检查要映射的文件是为写入而打开的,而不是以追加模式打开的,*还要检查文件上没有强制锁*/
case MAP_PRIVATE:
/*==========略掉若干在MAP_PRIVATE情形下的访问权限检查代码===========*/
/*具体检查为:检查要映射的文件是否为写入而打开,然后再检查是否是想要在*NOEXEC上执行EXEC,最后检查f_op*/
default:
return -EINVAL;
}
/*file指针为0时*/
} else {
/*==========略掉若干在不进行文件映射情形下的访问权限设置===========*/
}
/*检查给定的地址是否能够进行安全的地址映射,如果是,则返回0,然后进入mmap_region()函数*/
error = security_file_mmap(file, reqprot, prot, flags, addr, 0);
if (error)
return error;
/*创建虚拟区间,并进行映射*/
return mmap_region(file, addr, len, flags, vm_flags, pgoff);
}
下面进入mmap_region()函数:
unsigned long mmap_region(struct file *file, unsigned long addr,
unsigned long len, unsigned long flags,
unsigned int vm_flags, unsigned long pgoff)
{
/*==========略掉若干变量声明代码===========*/
/ *find_vma_prepare函数扫描当前进程地址空间的vm_area_struct结构所形成的红黑树,试图找到结束地址高于addr的第一个区间;如果找到了一个虚拟区,说明addr所在的虚拟区已经在使用,也就是已经有映射存在,因此要调用do_munmap()把这个老的虚拟区从进程地址空间中撤销,如果撤销不成功,就返回一个负数;如果撤销成功,就继续查找,直到在红黑树中找不到addr所在的虚拟区,并继续下面的检查*/
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;
}
/*检查是否要对此虚拟区间进行扩充*/
if (!may_expand_vm(mm, len >> PAGE_SHIFT))
return -ENOMEM;
/*=================省略掉若干非重要标志检查或设置代码===============*/
/*如果是匿名映射(file为空),并且这个虚拟区是非共享的,则可以把这个虚拟区和*与它紧挨的前一个虚拟区进行合并;虚拟区的合并是由vma_merge()函数实现的。如*果合并成功,则转out处,请看后面out处的代码*/
vma = vma_merge(mm, prev, addr, addr + len, vm_flags, NULL, file, pgoff, NULL);
if (vma)
goto out;
/*分配映射虚拟区空间*/
vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
if (!vma) {
error = -ENOMEM;
goto unacct_error;
}
/*=================省略掉若干struct vm_area_struct初始化代码===============*/
/*若file不为空,则是建立文件到虚拟区间的映射*/
if (file) {
/*=========省略掉若干文件访问权限的检查及文件使用计数的增减代码=========*/
/*addr有可能已被驱动程序改变,因此,把新虚拟区的起始地址赋给addr*/
addr = vma->vm_start;
pgoff = vma->vm_pgoff;
vm_flags = vma->vm_flags;
}
/*file为空时,如果flags参数中的MAP_SHARED标志位为1,则调用shmem_zero_setup()进行共享内存的映射*/
else if (vm_flags & VM_SHARED) {
error = shmem_zero_setup(vma);
if (error)
goto free_vma;
}
/*=====================省略掉若干非重要代码===========================*/
/*把新建的虚拟区插入到进程的地址空间*/
vma_link(mm, vma, prev, rb_link, rb_parent);
file = vma->vm_file;
out:
perf_event_mmap(vma);
/*增加进程地址空间长度*/
mm->total_vm += len >> PAGE_SHIFT;
vm_stat_account(mm, vm_flags, file, len >> PAGE_SHIFT);
if (vm_flags & VM_LOCKED) {
/*虚拟区间加锁状态下完成虚拟页面到物理页面的映射并完成文件到物理内存的真正调入 */
long nr_pages = mlock_vma_pages_range(vma, addr, addr + len);
if (nr_pages < 0)
return nr_pages; /* vma gone! */
mm->locked_vm += (len >> PAGE_SHIFT) - nr_pages;
} else if ((flags & MAP_POPULATE) && !(flags & MAP_NONBLOCK))
/*完成虚拟页面到物理页面的映射和文件到物理内存的真正调入*/
make_pages_present(addr, addr + len);
return addr;
unmap_and_free_vma:
if (correct_wcount)
atomic_inc(&inode->i_writecount);
vma->vm_file = NULL;
fput(file);
/*在指定区域撤销映射*/
unmap_region(mm, vma, prev, vma->vm_start, vma->vm_end);
charged = 0;
free_vma:
kmem_cache_free(vm_area_cachep, vma);
unacct_error:
if (charged)
vm_unacct_memory(charged);
return error;
}
下面来看系统调用mremap的实现,其实现代码主要在linux-2.6.33.2/mm下的mremap.c中,系统调用mremap可以用来完成改变虚拟区域边界地址的功能,下面来看具体实现:
unsigned long do_mremap(unsigned long addr,
unsigned long old_len, unsigned long new_len,
unsigned long flags, unsigned long new_addr)
{
/*=====================省略掉若干变量声明代码=========================*/
/*首先查看重映射标志是否满足*/
if (flags & ~(MREMAP_FIXED | MREMAP_MAYMOVE))
goto out;
/*检查地址是否是页对齐,即低12位为0*/
if (addr & ~PAGE_MASK)
goto out;
/*之前的要映射的长度*/
old_len = PAGE_ALIGN(old_len);
/*现在要映射的长度*/
new_len = PAGE_ALIGN(new_len);
/*检查此次要映射的长度是否为0*/
if (!new_len)
goto out;
/*首先判断是否是既MREMAP_FIXED 又要MREMAP_MAYMOVE ,则要移动虚拟区域,由mremap_to实现*/
if (flags & MREMAP_FIXED) {
if (flags & MREMAP_MAYMOVE)
ret = mremap_to(addr, old_len, new_addr, new_len);
goto out;
}
/*如果是减少区域的大小,则用do_munmap函数取消不用的映射区域*/
if (old_len >= new_len) {
ret = do_munmap(mm, addr+new_len, old_len - new_len);
if (ret && old_len != new_len)
goto out;
ret = addr;
goto out;
}
/*至此,所要进行的动作应该是要对映射区域进行扩展*/
vma = vma_to_resize(addr, old_len, new_len, &charged);
if (IS_ERR(vma)) {
ret = PTR_ERR(vma);
goto out;
}
/* 检查old_len即目前的映射长度是否正好等于当前映射区域的长度.*/
if (old_len == vma->vm_end - addr) {
/* 如果是,说明没有多余的映射区间,需要进行扩展,先检查能否进行扩展*/
if (vma_expandable(vma, new_len - old_len)) {
/*需要多少页*/
int pages = (new_len - old_len) >> PAGE_SHIFT;
/*调整vma的边界,并将新的vma重新插入进程vma链中*/
vma_adjust(vma, vma->vm_start,
addr + new_len, vma->vm_pgoff, NULL);
/*vma数量增加*/
mm->total_vm += pages;
vm_stat_account(mm, vma->vm_flags, vma->vm_file, pages);
/*如果上锁,则表示即将要用到,所以调用函数mlock_vma_pages_range将页面调入*/
if (vma->vm_flags & VM_LOCKED) {
mm->locked_vm += pages;
mlock_vma_pages_range(vma, addr + old_len,
addr + new_len);
}
ret = addr;
goto out;
}
}
/*如果既不是要扩展vma,也不是要缩小vma,而是要重新创建一个新的映射区,并移动它,则进行下面的动作,这些与do_mmap代码基本一致,就是用来创建新的映射区域,唯一的不同只是:此处创建完成以后需要将其移动,由move_vma函数完成*/
ret = -ENOMEM;
if (flags & MREMAP_MAYMOVE) {
unsigned long map_flags = 0;
if (vma->vm_flags & VM_MAYSHARE)
map_flags |= MAP_SHARED;
new_addr = get_unmapped_area(vma->vm_file, 0, new_len,
vma->vm_pgoff +
((addr - vma->vm_start) >> PAGE_SHIFT),
map_flags);
if (new_addr & ~PAGE_MASK) {
ret = new_addr;
goto out;
}
ret = security_file_mmap(NULL, 0, 0, 0, new_addr, 1);
if (ret)
goto out;
ret = move_vma(vma, addr, old_len, new_len, new_addr);
}
out:
if (ret & ~PAGE_MASK)
vm_unacct_memory(charged);
return ret;
}