在csdn逛了一圈发现并没有博主把sys_mmap讲的特别仔细,所以笔者觉得很有必要写一篇关于sys_mmap系统调用的文章。内核版本选自Linux2.6.0,CPU选自i386,32位机。
一言以蔽之,在虚拟内存指定的一段空间中开辟一段空间,此空间可以用来正常映射使用,也可以用来映射文件,所以如果是文件映射的话,文件数据就会映射在用户态空间。
流程图先放在这里。
用户态根据0x80中断向量走idt表映射到SYSTEM_CALL然后查系统调用表执行sys_mmap2最终陷入到内核态,这里的过程不过细讲直接看到sys_mmap2源码。
- // addr 内核态想要的开始地址,实际会根据页对齐
- // len 长度
- // prot 拥有的操作
- // flags 标志位
- // fd 文件的下标
- // pgoff 页全局表的偏移量
- 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);
- }

do_mmap_pgoff代码量特别得多,笔者挑选比较重要的部分讲解。
因为mmap有2种选择,一种是正常映射,一种是文件映射。而在do_mmap2代码中就已经判断了是不是文件映射,如果是文件映射,就会通过fd找到对应file结构体。所以当我们看到在do_mmap_pgoff代码中看到有判断file结构体的if指令段都是在处理文件映射。
而我们需要理解一件事,不管是文件映射还是正常映射,对于mmap来说通用的部分就是会在虚拟地址中开辟一段空间,所以do_mmap_pgoff方法中会去映射一段虚拟地址抽象成一个vma_struct结构体来表示。具体逻辑在get_unmapped_area -> arch_get_unmapped_area方法中。

这里创建出一个vma后,重点看到do_mmap_pgoff方法中以下的代码中。
- // 之前创建出vma
- // 这里是vma的属性赋值。
- 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是空的
- // 而文件映射的vma->vm_ops在后续代码中会做设置回调操作。
- vma->vm_ops = NULL;
- vma->vm_pgoff = pgoff;
- vma->vm_file = NULL;
- vma->vm_private_data = NULL;
- vma->vm_next = NULL;
- INIT_LIST_HEAD(&vma->shared);
-
- 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 (vm_flags & VM_SHARED) {
- error = shmem_zero_setup(vma);
- if (error)
- goto free_vma;
- }
对于当前mmap是文件映射来说,执行file->f_op->mmap(file, vma);所以看到ext2文件系统的实现
- int generic_file_mmap(struct file * file, struct vm_area_struct * vma)
- {
- struct address_space *mapping = file->f_dentry->d_inode->i_mapping;
- struct inode *inode = mapping->host;
-
- if (!mapping->a_ops->readpage)
- return -ENOEXEC;
- update_atime(inode);
-
- // 把当前开辟的vma中的operations的函数指针给实现
- // 也就是说文件映射的mmap中开辟的vma对应的操作由文件系统实现
- // 也就是说挂了钩子函数。
- vma->vm_ops = &generic_file_vm_ops;
- return 0;
- }
-
-
- static struct vm_operations_struct generic_file_vm_ops = {
- // 缺页异常中回调函数
- .nopage = filemap_nopage,
-
- // 这个是vma文件映射大小改变回调的函数。
- .populate = filemap_populate,
- };
对于文件映射来说会给创建的vma挂上一个文件的回调梯子。而正常使用映射的vma的vm_ops是null,这点在后续的缺页异常中区分。
而我们要知道,mmap系统调用仅仅是在虚拟内存中创建一段空间的映射,所以以上代码就介绍完mmap系统调用了。
mmap仅仅是在虚拟内存中创建一片区域的映射,而用户态程序使用这片映射区域时,当MMU+OS去完成虚拟内存映射到物理内存的过程中,会发现当前虚拟内存并没有映射到物理页中。所以MMU就会给CPU发送一个PAGE FAULT(缺页异常),然后CPU查询idt表最终走到do_page_fault方法。
由于do_page_fault的代码量特别特别的大,还是老规矩看重点部分。但是看之前我们需要知道page fault的目的是什么,当虚拟地址映射到物理地址时,发现没有对应的物理页就会发生page fault,所以这个中断异常的目的是找到一个空闲的页,并且完成物理页跟虚拟地址的映射。

看到handler_mm_fault具体代码。
- int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct * vma,
- unsigned long address, int write_access)
- {
- pgd_t *pgd;
- pmd_t *pmd;
-
- __set_current_state(TASK_RUNNING);
-
- // 找到全局页目录表
- pgd = pgd_offset(mm, address);
-
- inc_page_state(pgfault);
-
- if (is_vm_hugetlb_page(vma))
- return VM_FAULT_SIGBUS; /* mapping truncation does this. */
-
- /*
- * We need the page table lock to synchronize with kswapd
- * and the SMP-safe atomic PTE updates.
- */
- spin_lock(&mm->page_table_lock);
-
- // 找到页中目录表
- // 不存在页中目录就会去创建
- pmd = pmd_alloc(mm, pgd, address);
-
- // 通过页中目录表找到页表
- if (pmd) {
-
- // 通过页中目录找到页表。
- // 如果页表不存在就会去创建
- pte_t * pte = pte_alloc_map(mm, pmd, address);
- if (pte)
-
- // 根据页表来完成新创建的页的映射
- return handle_pte_fault(mm, vma, address, write_access, pte, pmd);
- }
- spin_unlock(&mm->page_table_lock);
- return VM_FAULT_OOM;
- }
当缺页异常时,CPU会把错误码自动压栈,并且会把导致缺页异常的线性地址(高版本的内核中就可以理解为虚拟地址,因为没分段)给放入cr2寄存器中。而线性地址要参与到寻找页表的过程,因为把线性地址查分为几段,每一段表示不同页的具体偏移量(全局页、页中目录、页表、页帧)。然后再通过CR3寄存器找到页全局表的基址。
而上述代码通过CR2+CR3,从全局页表一层一层往下找,找到具体的页表描述符,再通过handler_pte_fault做具体的操作。
- static inline int handle_pte_fault(struct mm_struct *mm,
- struct vm_area_struct * vma, unsigned long address,
- int write_access, pte_t *pte, pmd_t *pmd)
- {
- pte_t entry;
-
- // 把地址转换为描述符数据。
- entry = *pte;
-
- // 这里能进来就代表当前的pte描述符的第1位和第8位为0
- // 第1位为0也就是没映射。
- if (!pte_present(entry)) {
- /*
- * If it truly wasn't present, we know that kswapd
- * and the PTE updates will not touch it later. So
- * drop the lock.
- */
-
- // pte描述符都为0。
- // 也就代表当前pte描述符没被页帧映射到。
- if (pte_none(entry))
- return do_no_page(mm, vma, address, write_access, pte, pmd);
- if (pte_file(entry))
- return do_file_page(mm, vma, address, write_access, pte, pmd);
- return do_swap_page(mm, vma, address, pte, pmd, entry, write_access);
- }
-
- if (write_access) {
- if (!pte_write(entry))
- return do_wp_page(mm, vma, address, pte, pmd, entry);
-
- entry = pte_mkdirty(entry);
- }
- entry = pte_mkyoung(entry);
- establish_pte(vma, address, pte, entry);
- pte_unmap(pte);
- spin_unlock(&mm->page_table_lock);
- return VM_FAULT_MINOR;
- }
这里判断是不是没被映射过的pte页表项,没被映射就走到do_no_page。
- static int
- do_no_page(struct mm_struct *mm, struct vm_area_struct *vma,
- unsigned long address, int write_access, pte_t *page_table, pmd_t *pmd)
- {
- struct page * new_page;
- struct address_space *mapping = NULL;
- pte_t entry;
- struct pte_chain *pte_chain;
- int sequence = 0;
- int ret;
-
- if (!vma->vm_ops || !vma->vm_ops->nopage)
- return do_anonymous_page(mm, vma, page_table,
- pmd, write_access, address);
-
- pte_unmap(page_table);
- spin_unlock(&mm->page_table_lock);
-
- if (vma->vm_file) {
- mapping = vma->vm_file->f_dentry->d_inode->i_mapping;
- sequence = atomic_read(&mapping->truncate_count);
- }
- smp_rmb(); /* Prevent CPU from reordering lock-free ->nopage() */
- retry:
- new_page = vma->vm_ops->nopage(vma, address & PAGE_MASK, 0);
-
- /* no page was available -- either SIGBUS or OOM */
- if (new_page == NOPAGE_SIGBUS)
- return VM_FAULT_SIGBUS;
- if (new_page == NOPAGE_OOM)
- return VM_FAULT_OOM;
-
- pte_chain = pte_chain_alloc(GFP_KERNEL);
- if (!pte_chain)
- goto oom;
-
- /*
- * Should we do an early C-O-W break?
- */
- if (write_access && !(vma->vm_flags & VM_SHARED)) {
- struct page * page = alloc_page(GFP_HIGHUSER);
- if (!page) {
- page_cache_release(new_page);
- goto oom;
- }
- copy_user_highpage(page, new_page, address);
- page_cache_release(new_page);
- lru_cache_add_active(page);
- new_page = page;
- }
-
- spin_lock(&mm->page_table_lock);
- /*
- * For a file-backed vma, someone could have truncated or otherwise
- * invalidated this page. If invalidate_mmap_range got called,
- * retry getting the page.
- */
- if (mapping &&
- (unlikely(sequence != atomic_read(&mapping->truncate_count)))) {
- sequence = atomic_read(&mapping->truncate_count);
- spin_unlock(&mm->page_table_lock);
- page_cache_release(new_page);
- pte_chain_free(pte_chain);
- goto retry;
- }
- page_table = pte_offset_map(pmd, address);
-
- /*
- * This silly early PAGE_DIRTY setting removes a race
- * due to the bad i386 page protection. But it's valid
- * for other architectures too.
- *
- * Note that if write_access is true, we either now have
- * an exclusive copy of the page, or this is a shared mapping,
- * so we can make it writable and dirty to avoid having to
- * handle that later.
- */
- /* Only go through if we didn't race with anybody else... */
- if (pte_none(*page_table)) {
- if (!PageReserved(new_page))
- ++mm->rss;
- flush_icache_page(vma, new_page);
- entry = mk_pte(new_page, vma->vm_page_prot);
- if (write_access)
- entry = pte_mkwrite(pte_mkdirty(entry));
- set_pte(page_table, entry);
- pte_chain = page_add_rmap(new_page, page_table, pte_chain);
- pte_unmap(page_table);
- } else {
- /* One of our sibling threads was faster, back out. */
- pte_unmap(page_table);
- page_cache_release(new_page);
- spin_unlock(&mm->page_table_lock);
- ret = VM_FAULT_MINOR;
- goto out;
- }
-
- /* no need to invalidate: a not-present page shouldn't be cached */
- update_mmu_cache(vma, address, entry);
- spin_unlock(&mm->page_table_lock);
- ret = VM_FAULT_MAJOR;
- goto out;
- oom:
- ret = VM_FAULT_OOM;
- out:
- pte_chain_free(pte_chain);
- return ret;
- }
if (!vma->vm_ops || !vma->vm_ops->nopage)
return do_anonymous_page(mm, vma, page_table,
pmd, write_access, address);看到这里,这是do_no_page最上层的一个if判断,回忆一下mmap系统调用中创建vma时候如果是文件就会走file->f_op->mmap(file, vma);,而此操作会把vma->vm_ops函数指针结构体给初始化。因为mmap非文件映射创建的vma->vm_ops = null,只有文件映射会赋值vm_ops。所以这里if也就是在判断当前创建页是文件映射还是普通正常映射。当这个if没过,下面的操作都是文件映射。
所以具体的创建页帧,页帧与页表项做映射的方法为:
正常映射:do_anonymous_page(mm, vma, page_table,pmd, write_access, address);
文件映射:new_page = vma->vm_ops->nopage(vma, address & PAGE_MASK, 0); 此函数指针对应的实现为filemap_nopage
总结
并不复杂,懂虚拟地址映射到物理地址的机制,分页的机制。并且懂中断机制其实看sys_mmap挺轻松的。
最后,如果本帖对您有一定的帮助,希望能点赞+关注+收藏!您的支持是给我最大的动力,后续会一直更新各种框架的使用和框架的源码解读~!