之前写了(一)(二)其实就梳理到了get_unmapped_area的内容,而且有一点混乱,这里进行第三篇的讲解,讲解在do_mmap_pgoff中除了get_unmapped_area的内容,来了解mmap的具体实现。通过(一)(二)(三)来将mmap内核源码进行一次梳理。可能过程有一点乱,所以最后准备写一篇总结来总结mmap这样一个流程。
(一)https://blog.csdn.net/SweeNeil/article/details/83685812
(二)https://blog.csdn.net/SweeNeil/article/details/83897094
在(三)中我准备从do_mmap_pgoff中的内容出发,分析(一)、(二)中没有分析的内容。
直接上do_mmap_pgoff函数的内容,/mm/mmap.c文件中的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,
unsigned long *populate)
{
struct mm_struct * mm = current->mm;
struct inode *inode;
vm_flags_t vm_flags;
*populate = 0;
/*
* Does the application expect PROT_READ to imply PROT_EXEC?
*
* (the exception is when the underlying filesystem is noexec
* mounted, in which case we dont add PROT_EXEC.)
应用程序是否期望PROT_READ影射PROT_EXEC?
(例外情况是底层文件系统是noexec挂载的,
在这种情况下我们不添加PROT_EXEC)
*/
if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
if (!(file && (file->f_path.mnt->mnt_flags & MNT_NOEXEC)))
prot |= PROT_EXEC;
if (!len)
return -EINVAL;
if (!(flags & MAP_FIXED))
addr = round_hint_to_min(addr);
/* 小心溢出 */
len = PAGE_ALIGN(len);
if (!len)
return -ENOMEM;
/* 溢出偏移? */
if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)
return -EOVERFLOW;
/* 影射是否过多? */
if (mm->map_count > sysctl_max_map_count)
return -ENOMEM;
/* 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);
//如果现在都不是页对齐的,那么返回的肯定是错误码,返回之return addr
if (addr & ~PAGE_MASK)
return addr;
/* 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.
在这里进行简单的检查,以便下级例程不必。
我们假设访问权限已由内存对象的打开处理,
因此我们不在此处执行任何操作。
*/
//calc_vm_prot_bits 将mmap“prot”参数合并到内部使用的“vm_flags”中。
vm_flags = calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags) |
mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
if (flags & MAP_LOCKED)
if (!can_do_mlock())
return -EPERM;
/* mlock MCL_FUTURE? */
if (vm_flags & VM_LOCKED) {
unsigned long locked, lock_limit;
locked = len >> PAGE_SHIFT;
locked += mm->locked_vm;
lock_limit = rlimit(RLIMIT_MEMLOCK);
lock_limit >>= PAGE_SHIFT;
if (locked > lock_limit && !capable(CAP_IPC_LOCK))
return -EAGAIN;
}
inode = file ? file_inode(file) : NULL;
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(inode) && (file->f_mode & FMODE_WRITE))
return -EACCES;
/*
* Make sure there are no mandatory locks on the file.
*确保文件没有强制锁定
*/
if (locks_verify_locked(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;
if (file->f_path.mnt->mnt_flags & MNT_NOEXEC) {
if (vm_flags & VM_EXEC)
return -EPERM;
vm_flags &= ~VM_MAYEXEC;
}
if (!file->f_op || !file->f_op->mmap)
return -ENODEV;
break;
default:
return -EINVAL;
}
} else {
switch (flags & MAP_TYPE) {
case MAP_SHARED:
/*
* Ignore pgoff.
* 忽略pgoff
*/
pgoff = 0;
vm_flags |= VM_SHARED | VM_MAYSHARE;
break;
case MAP_PRIVATE:
/*
* Set pgoff according to addr for anon_vma.
* 根据addon为anon_vma设置pgoff
*/
pgoff = addr >> PAGE_SHIFT;
break;
default:
return -EINVAL;
}
}
/*
* Set 'VM_NORESERVE' if we should not account for the
* memory use of this mapping.
* 如果我们不考虑此映射的内存使用,请设置'VM_NORESERVE'
*/
if (flags & MAP_NORESERVE) {
/* We honor MAP_NORESERVE if allowed to overcommit
如果允许过度使用,我们会尊重MAP_NORESERVE */
if (sysctl_overcommit_memory != OVERCOMMIT_NEVER)
vm_flags |= VM_NORESERVE;
/* hugetlb applies strict overcommit unless MAP_NORESERVE
除非MAP_NORESERVE,否则hugetlb会应用严格的overcommit */
if (file && is_file_hugepages(file))
vm_flags |= VM_NORESERVE;
}
addr = mmap_region(file, addr, len, vm_flags, pgoff);
if (!IS_ERR_VALUE(addr) &&
((vm_flags & VM_LOCKED) ||
(flags & (MAP_POPULATE | MAP_NONBLOCK)) == MAP_POPULATE))
*populate = len;
return addr;
}
从do_mmap_pgoff中可以看到,其实get_unmapped_area函数只是返回了新线性区的地址,除此之外真正要做的工作还有很多,现在开始一步一步进行分析:
在do_mmap_pgoff的最开始做了一些标志的判断,然后调用了get_unmapped_area函数,返回了一个addr(这个过程在(一)、(二)中有详细讲解)。对get_unmapped_area返回的addr进行校验,如果addr不满足页对齐,那么说明get_unmapped_area函数返回的是一个错误码,直接将这个错误码返回即可。
如果addr正常,继续往下分析,调用了calc_vm_prot_bits函数将mmap prot 参数合并到内部使用的 vm_flags 中,只有在prot中设置了相应的PROT_READ、PROT_WRITE、PROT_EXEC等标志,calc_vm_prot_bits函数才会在vm_flags中设置VM_READ、VM_WRITE、VM_EXEC标志。同样的只有在flags设置了相应的MAP_GROWSDOWN、MAP_EXECUTABLE等标志,calc_vm_prot_bits函数才在vm_flags中设置VM_GROWSDOWN、VM_EXECUTABLE标志。
继续往下看又是flags的判断,然后根据是否为文件来获取inode,如果是文件就获取这个文件的inode。if(file)……else中进行了一些判断,保证mmap的顺利进行,if(file)……else之后调用了mmap_region函数
addr = mmap_region(file, addr, len, vm_flags, pgoff);
我们进入到mmap_region函数中查看其中的具体内容:
unsigned long mmap_region(struct file *file, unsigned long addr,
unsigned long len, vm_flags_t vm_flags, unsigned long pgoff)
{
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma, *prev;
int correct_wcount = 0;
int error;
struct rb_node **rb_link, *rb_parent;
unsigned long charged = 0;
struct inode *inode = file ? file_inode(file) : NULL;
/* 检查地址空间限制 */
if (!may_expand_vm(mm, len >> PAGE_SHIFT)) {
unsigned long nr_pages;
/*
* MAP_FIXED may remove pages of mappings that intersects with
* requested mapping. Account for the pages it would unmap.
* MAP_FIXED可能会删除与请求的映射相交的映射页面。
* 说明要取消映射的页面。
*/
if (!(vm_flags & MAP_FIXED))
return -ENOMEM;
nr_pages = count_vma_pages_range(mm, addr, addr + len);
if (!may_expand_vm(mm, (len >> PAGE_SHIFT) - nr_pages))
return -ENOMEM;
}
/* Clear old maps */
error = -ENOMEM;
munmap_back:
if (find_vma_links(mm, addr, addr + len, &prev, &rb_link, &rb_parent)) {
if (do_munmap(mm, addr, len))
return -ENOMEM;
goto munmap_back;
}
/*
* Private writable mapping: check memory availability
* 专用可写映射:检查内存可用性
*/
if (accountable_mapping(file, vm_flags)) {
charged = len >> PAGE_SHIFT;
if (security_vm_enough_memory_mm(mm, charged))
return -ENOMEM;
vm_flags |= VM_ACCOUNT;
}
/*
* Can we just expand an old mapping?
* 我们可以扩展一个旧的映射吗?
*/
vma = vma_merge(mm, prev, addr, addr + len, vm_flags, NULL, file, pgoff, NULL);
if (vma)
goto 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_zalloc(vm_area_cachep, GFP_KERNEL);
if (!vma) {
error = -ENOMEM;
goto unacct_error;
}
vma->vm_mm = mm;
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_flags = vm_flags;
vma->vm_page_prot = vm_get_page_prot(vm_flags);
vma->vm_pgoff = pgoff;
INIT_LIST_HEAD(&vma->anon_vma_chain);
error = -EINVAL; /* 拒绝VM_GROWSDOWN | VM_GROWSUP时 */
if (file) {
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 = get_file(file);
error = file->f_op->mmap(file, vma);
if (error)
goto unmap_and_free_vma;
/* Can addr have changed??
*
* Answer: Yes, several device drivers can do it in their
* f_op->mmap method. -DaveM
* Bug: If addr is changed, prev, rb_link, rb_parent should
* be updated for vma_link()
* addr可以改变??
* 答:可以,有几个设备驱动程序可以在f_op-> mmap方法中执行此操作。
* -DaveM Bug:如果更改了addr,则应为vma_link()更新prev,rb_link,rb_parent
*/
WARN_ON_ONCE(addr != vma->vm_start);
addr = vma->vm_start;
pgoff = vma->vm_pgoff;
vm_flags = vma->vm_flags;
} else if (vm_flags & VM_SHARED) {
if (unlikely(vm_flags & (VM_GROWSDOWN|VM_GROWSUP)))
goto free_vma;
error = shmem_zero_setup(vma);
if (error)
goto free_vma;
}
if (vma_wants_writenotify(vma)) {
pgprot_t pprot = vma->vm_page_prot;
/* Can vma->vm_page_prot have changed??
*
* Answer: Yes, drivers may have changed it in their
* f_op->mmap method.
*
* Ensures that vmas marked as uncached stay that way.
* vma-> vm_page_prot可以改变吗?
* 答:可以,驱动程序可能已在其f_op-> mmap方法中更改了它。
* 确保标记为未缓存的vma保持这种方式。
*/
vma->vm_page_prot = vm_get_page_prot(vm_flags & ~VM_SHARED);
if (pgprot_val(pprot) == pgprot_val(pgprot_noncached(pprot)))
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
}
vma_link(mm, vma, prev, rb_link, rb_parent);
file = vma->vm_file;
/* Once vma denies write, undo our temporary denial count
一旦vma拒绝写入,撤消我们的临时拒绝计数 */
if (correct_wcount)
atomic_inc(&inode->i_writecount);
out:
perf_event_mmap(vma);
vm_stat_account(mm, vm_flags, file, len >> PAGE_SHIFT);
if (vm_flags & VM_LOCKED) {
if (!((vm_flags & VM_SPECIAL) || is_vm_hugetlb_page(vma) ||
vma == get_gate_vma(current->mm)))
mm->locked_vm += (len >> PAGE_SHIFT);
else
vma->vm_flags &= ~VM_LOCKED;
}
if (file)
uprobe_mmap(vma);
return addr;
unmap_and_free_vma:
if (correct_wcount)
atomic_inc(&inode->i_writecount);
vma->vm_file = NULL;
fput(file);
/* Undo any partial mapping done by a device driver.
撤消设备驱动程序完成的任何部分映射 */
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;
}
在mmap_region中,可以看到这样的一条语句:
if (find_vma_links(mm, addr, addr + len, &prev, &rb_link, &rb_parent)) {
if (do_munmap(mm, addr, len))
return -ENOMEM;
goto munmap_back;
}
mmap_region调用find_vma_links函数确定处于新区间之前的线性区对象的位置,以及在红-黑树中新线性区的位置。同时find_vma_link函数也检查是否还存在与新区建重叠的线性区。如果这样就调用do_munmap函数删除新的区间,然后重复判断。
检查无误后,再检查内存的可用性,可用就继续往下通过vma_merge函数返回了一个vma,语句如下:
vma = vma_merge(mm, prev, addr, addr + len, vm_flags, NULL, file, pgoff, NULL);
调用vma_merge检查前一个线性区是否可以以这样的方式进行扩展来包含新的区间。同时需要保证,前一个线性区必须与在vm_flags局部变量中存放的那些线性区具有完全相同的标志。如果前一个线性区可以扩展,那么vm_merge函数就会试图把它与随后的线性区进行合并,如果合并成功就直接跳转到out
如果合并不成功,就继续往下执行,调用kmem_cache_zalloc函数,为新的线性区分配一个vm_area_struct结构。这里的这两个函数vma_merge和kmem_zcache_alloc函数都比较重要。
vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
if (!vma) {
error = -ENOMEM;
goto unacct_error;
}
进入到kmem_cache_zalloc函数中查看其内容:
static inline void *kmem_cache_zalloc(struct kmem_cache *k, gfp_t flags)
{
return kmem_cache_alloc(k, flags | __GFP_ZERO);
}
它实际调用了kmem_cache_alloc函数,继续进入到kmem_cache_alloc函数中查看究竟
void *kmem_cache_alloc(struct kmem_cache *s, gfp_t gfpflags)
{
void *ret = slab_alloc(s, gfpflags, _RET_IP_);
trace_kmem_cache_alloc(_RET_IP_, ret, s->object_size, s->size, gfpflags);
return ret;
}
可以发现,在kmem_cache_alloc中调用了slab分配函数,slab_alloc函数。
回到mmap_region函数中,kmem_cache_zalloc返回了这个vma之后,就开始对vma进行赋值,初始化了新的线性区对象。
接下来判断如果是文件,则通过
vma->vm_file = get_file(file);
对vm_file进行赋值等。
如果MAP_SHARED标志被设置,同时新的线性区不映射磁盘上的文件,则这个线性区是一个共享匿名区,因此又调用了shmem_zero_setup函数对vma进行了初始化。
else if (vm_flags & VM_SHARED) {
if (unlikely(vm_flags & (VM_GROWSDOWN|VM_GROWSUP)))
goto free_vma;
error = shmem_zero_setup(vma);
if (error)
goto free_vma;
}
进入到shmem_zero_setup函数中查看其内容:
int shmem_zero_setup(struct vm_area_struct *vma)
{
struct file *file;
loff_t size = vma->vm_end - vma->vm_start;
file = shmem_file_setup("dev/zero", size, vma->vm_flags);
if (IS_ERR(file))
return PTR_ERR(file);
if (vma->vm_file)
fput(vma->vm_file);
vma->vm_file = file;
vma->vm_ops = &shmem_vm_ops;
return 0;
}
发现该函数和dev/zero有关,这与匿名内存映射是相关的,然后将vma->vm_file赋值给了这个dev/zero。
回到mmap_region函数中,继续往下看
if (vma_wants_writenotify(vma))
这里调用了vma_wants_writenotify函数的判断, 对于某些共享映射会希望标记为只读的页面跟踪写入事件。 如果是这样,会将vm_page_prot降级为私有版本(使用protection_map []而不使用VM_SHARED位)。
继续往下
vma_link(mm, vma, prev, rb_link, rb_parent);
file = vma->vm_file;
这里调用了vma_link函数,进入到vma_link函数中,查看里面做了什么事情:
static void vma_link(struct mm_struct *mm, struct vm_area_struct *vma,
struct vm_area_struct *prev, struct rb_node **rb_link,
struct rb_node *rb_parent)
{
struct address_space *mapping = NULL;
if (vma->vm_file)
mapping = vma->vm_file->f_mapping;
if (mapping)
mutex_lock(&mapping->i_mmap_mutex);
__vma_link(mm, vma, prev, rb_link, rb_parent);
__vma_link_file(vma);
if (mapping)
mutex_unlock(&mapping->i_mmap_mutex);
mm->map_count++;
validate_mm(mm);
}
我们发现里面又调用了__vma_link函数,进入里面继续往下看发现其实vma_link函数把新的线性区插入到了线性区链表的红-黑树中。
继续往下到了out这里
perf_event_mmap(vma);
调用了这样一个函数,进入到里面,发现在里面对结构体perf_mmap_event进行了赋值,添加了这样一个mmap_event事件。
继续往下:
vm_stat_account(mm, vm_flags, file, len >> PAGE_SHIFT);
调用了vm_stat_account,我们进入到里面

发现其实该函数主要讲内存描述符的total_vm字段中的进程地址空间大小进行了增加。
if (vm_flags & VM_LOCKED) {
if (!((vm_flags & VM_SPECIAL) || is_vm_hugetlb_page(vma) ||
vma == get_gate_vma(current->mm)))
mm->locked_vm += (len >> PAGE_SHIFT);
else
vma->vm_flags &= ~VM_LOCKED;
}
然后继续判断是否设置了VM_LOCKED标志,如果设置了就对mm->locked_vm进行赋值。
最后
if (file)
uprobe_mmap(vma);
如果是file就调用了一个uprobe_mmap的函数,如果此时没有活动的uprobe事件,我们可以跳过uprobe_mmap。
在uprobe_mmap函数中验证指定的vma是否为可执行文件vma。(这里有点模糊,欢迎讨论)
最后返回addr,至此mmap_region函数解析完毕,然后回到do_mmap_pgoff函数中,同样也返回addr。
至此整个过程到此结束!!
本文详细分析了Linux内核3.10版本中的mmap实现,特别是do_mmap_pgoff函数的流程。从get_unmapped_area返回线性区地址开始,接着讨论了calc_vm_prot_bits函数如何处理mmap参数,以及mmap_region函数中如何处理线性区对象,包括内存可用性检查、vma_merge与kmem_cache_zalloc的作用。文章还介绍了在文件映射和匿名映射时的不同处理,如shmem_zero_setup函数在共享匿名映射中的角色。通过对vma_link和其他相关函数的解析,完整展示了mmap操作如何整合到内核的数据结构中。
&spm=1001.2101.3001.5002&articleId=83927862&d=1&t=3&u=252abfb6940d46cfa7dfb56c095b75d8)
2428

被折叠的 条评论
为什么被折叠?



