• 深入理解Linux2.6内核中sys_mmap系统调用


    在csdn逛了一圈发现并没有博主把sys_mmap讲的特别仔细,所以笔者觉得很有必要写一篇关于sys_mmap系统调用的文章。内核版本选自Linux2.6.0,CPU选自i386,32位机。

    sys_mmap的作用

    一言以蔽之,在虚拟内存指定的一段空间中开辟一段空间,此空间可以用来正常映射使用,也可以用来映射文件,所以如果是文件映射的话,文件数据就会映射在用户态空间。

     

    sys_mmap源码讲解

    流程图先放在这里。

     

    用户态根据0x80中断向量走idt表映射到SYSTEM_CALL然后查系统调用表执行sys_mmap2最终陷入到内核态,这里的过程不过细讲直接看到sys_mmap2源码。

    1. // addr 内核态想要的开始地址,实际会根据页对齐
    2. // len 长度
    3. // prot 拥有的操作
    4. // flags 标志位
    5. // fd 文件的下标
    6. // pgoff 页全局表的偏移量
    7. asmlinkage long sys_mmap2(unsigned long addr, unsigned long len,
    8. unsigned long prot, unsigned long flags,
    9. unsigned long fd, unsigned long pgoff)
    10. {
    11. return do_mmap2(addr, len, prot, flags, fd, pgoff);
    12. }

     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方法中以下的代码中。

    1. // 之前创建出vma
    2. // 这里是vma的属性赋值。
    3. vma->vm_mm = mm;
    4. vma->vm_start = addr;
    5. vma->vm_end = addr + len;
    6. vma->vm_flags = vm_flags;
    7. vma->vm_page_prot = protection_map[vm_flags & 0x0f];
    8. // 这是公共部分,也就是除了文件映射,正常映射的vma->vm_ops是空的
    9. // 而文件映射的vma->vm_ops在后续代码中会做设置回调操作。
    10. vma->vm_ops = NULL;
    11. vma->vm_pgoff = pgoff;
    12. vma->vm_file = NULL;
    13. vma->vm_private_data = NULL;
    14. vma->vm_next = NULL;
    15. INIT_LIST_HEAD(&vma->shared);
    16. if (file) {
    17. error = -EINVAL;
    18. if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
    19. goto free_vma;
    20. if (vm_flags & VM_DENYWRITE) {
    21. error = deny_write_access(file);
    22. if (error)
    23. goto free_vma;
    24. correct_wcount = 1;
    25. }
    26. vma->vm_file = file;
    27. get_file(file);
    28. // 函数指针,调用当前文件系统具体的实现。
    29. error = file->f_op->mmap(file, vma);
    30. if (error)
    31. goto unmap_and_free_vma;
    32. } else if (vm_flags & VM_SHARED) {
    33. error = shmem_zero_setup(vma);
    34. if (error)
    35. goto free_vma;
    36. }

    对于当前mmap是文件映射来说,执行file->f_op->mmap(file, vma);所以看到ext2文件系统的实现

    ext2文件系统file的operations函数指针的实现

     

    1. int generic_file_mmap(struct file * file, struct vm_area_struct * vma)
    2. {
    3. struct address_space *mapping = file->f_dentry->d_inode->i_mapping;
    4. struct inode *inode = mapping->host;
    5. if (!mapping->a_ops->readpage)
    6. return -ENOEXEC;
    7. update_atime(inode);
    8. // 把当前开辟的vma中的operations的函数指针给实现
    9. // 也就是说文件映射的mmap中开辟的vma对应的操作由文件系统实现
    10. // 也就是说挂了钩子函数。
    11. vma->vm_ops = &generic_file_vm_ops;
    12. return 0;
    13. }
    14. static struct vm_operations_struct generic_file_vm_ops = {
    15. // 缺页异常中回调函数
    16. .nopage = filemap_nopage,
    17. // 这个是vma文件映射大小改变回调的函数。
    18. .populate = filemap_populate,
    19. };

    对于文件映射来说会给创建的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具体代码。 

    1. int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct * vma,
    2.     unsigned long address, int write_access)
    3. {
    4.     pgd_t *pgd;
    5.     pmd_t *pmd;
    6.     __set_current_state(TASK_RUNNING);
    7. // 找到全局页目录表
    8.     pgd = pgd_offset(mm, address);
    9.     inc_page_state(pgfault);
    10.     if (is_vm_hugetlb_page(vma))
    11.         return VM_FAULT_SIGBUS;    /* mapping truncation does this. */
    12.     /*
    13.      * We need the page table lock to synchronize with kswapd
    14.      * and the SMP-safe atomic PTE updates.
    15.      */
    16.     spin_lock(&mm->page_table_lock);
    17. // 找到页中目录表
    18. // 不存在页中目录就会去创建
    19.     pmd = pmd_alloc(mm, pgd, address);
    20. // 通过页中目录表找到页表
    21.     if (pmd) {
    22. // 通过页中目录找到页表。
    23. // 如果页表不存在就会去创建
    24.         pte_t * pte = pte_alloc_map(mm, pmd, address);
    25.         if (pte)
    26. // 根据页表来完成新创建的页的映射
    27.             return handle_pte_fault(mm, vma, address, write_access, pte, pmd);
    28.     }
    29.     spin_unlock(&mm->page_table_lock);
    30.     return VM_FAULT_OOM;
    31. }

    当缺页异常时,CPU会把错误码自动压栈,并且会把导致缺页异常的线性地址(高版本的内核中就可以理解为虚拟地址,因为没分段)给放入cr2寄存器中。而线性地址要参与到寻找页表的过程,因为把线性地址查分为几段,每一段表示不同页的具体偏移量(全局页、页中目录、页表、页帧)。然后再通过CR3寄存器找到页全局表的基址。

    而上述代码通过CR2+CR3,从全局页表一层一层往下找,找到具体的页表描述符,再通过handler_pte_fault做具体的操作。

    1. static inline int handle_pte_fault(struct mm_struct *mm,
    2.     struct vm_area_struct * vma, unsigned long address,
    3.     int write_access, pte_t *pte, pmd_t *pmd)
    4. {
    5.     pte_t entry;
    6.     // 把地址转换为描述符数据。
    7.     entry = *pte;
    8.     // 这里能进来就代表当前的pte描述符的第1位和第8位为0
    9.     // 第1位为0也就是没映射。
    10.     if (!pte_present(entry)) {
    11.         /*
    12.          * If it truly wasn't present, we know that kswapd
    13.          * and the PTE updates will not touch it later. So
    14.          * drop the lock.
    15.          */
    16.         // pte描述符都为0。
    17. // 也就代表当前pte描述符没被页帧映射到。
    18.         if (pte_none(entry))
    19.             return do_no_page(mm, vma, address, write_access, pte, pmd);
    20.         if (pte_file(entry))
    21.             return do_file_page(mm, vma, address, write_access, pte, pmd);
    22.         return do_swap_page(mm, vma, address, pte, pmd, entry, write_access);
    23.     }
    24.     if (write_access) {
    25.         if (!pte_write(entry))
    26.             return do_wp_page(mm, vma, address, pte, pmd, entry);
    27.         entry = pte_mkdirty(entry);
    28.     }
    29.     entry = pte_mkyoung(entry);
    30.     establish_pte(vma, address, pte, entry);
    31.     pte_unmap(pte);
    32.     spin_unlock(&mm->page_table_lock);
    33.     return VM_FAULT_MINOR;
    34. }

    这里判断是不是没被映射过的pte页表项,没被映射就走到do_no_page。

    1. static int
    2. do_no_page(struct mm_struct *mm, struct vm_area_struct *vma,
    3. unsigned long address, int write_access, pte_t *page_table, pmd_t *pmd)
    4. {
    5. struct page * new_page;
    6. struct address_space *mapping = NULL;
    7. pte_t entry;
    8. struct pte_chain *pte_chain;
    9. int sequence = 0;
    10. int ret;
    11. if (!vma->vm_ops || !vma->vm_ops->nopage)
    12. return do_anonymous_page(mm, vma, page_table,
    13. pmd, write_access, address);
    14. pte_unmap(page_table);
    15. spin_unlock(&mm->page_table_lock);
    16. if (vma->vm_file) {
    17. mapping = vma->vm_file->f_dentry->d_inode->i_mapping;
    18. sequence = atomic_read(&mapping->truncate_count);
    19. }
    20. smp_rmb(); /* Prevent CPU from reordering lock-free ->nopage() */
    21. retry:
    22. new_page = vma->vm_ops->nopage(vma, address & PAGE_MASK, 0);
    23. /* no page was available -- either SIGBUS or OOM */
    24. if (new_page == NOPAGE_SIGBUS)
    25. return VM_FAULT_SIGBUS;
    26. if (new_page == NOPAGE_OOM)
    27. return VM_FAULT_OOM;
    28. pte_chain = pte_chain_alloc(GFP_KERNEL);
    29. if (!pte_chain)
    30. goto oom;
    31. /*
    32. * Should we do an early C-O-W break?
    33. */
    34. if (write_access && !(vma->vm_flags & VM_SHARED)) {
    35. struct page * page = alloc_page(GFP_HIGHUSER);
    36. if (!page) {
    37. page_cache_release(new_page);
    38. goto oom;
    39. }
    40. copy_user_highpage(page, new_page, address);
    41. page_cache_release(new_page);
    42. lru_cache_add_active(page);
    43. new_page = page;
    44. }
    45. spin_lock(&mm->page_table_lock);
    46. /*
    47. * For a file-backed vma, someone could have truncated or otherwise
    48. * invalidated this page. If invalidate_mmap_range got called,
    49. * retry getting the page.
    50. */
    51. if (mapping &&
    52. (unlikely(sequence != atomic_read(&mapping->truncate_count)))) {
    53. sequence = atomic_read(&mapping->truncate_count);
    54. spin_unlock(&mm->page_table_lock);
    55. page_cache_release(new_page);
    56. pte_chain_free(pte_chain);
    57. goto retry;
    58. }
    59. page_table = pte_offset_map(pmd, address);
    60. /*
    61. * This silly early PAGE_DIRTY setting removes a race
    62. * due to the bad i386 page protection. But it's valid
    63. * for other architectures too.
    64. *
    65. * Note that if write_access is true, we either now have
    66. * an exclusive copy of the page, or this is a shared mapping,
    67. * so we can make it writable and dirty to avoid having to
    68. * handle that later.
    69. */
    70. /* Only go through if we didn't race with anybody else... */
    71. if (pte_none(*page_table)) {
    72. if (!PageReserved(new_page))
    73. ++mm->rss;
    74. flush_icache_page(vma, new_page);
    75. entry = mk_pte(new_page, vma->vm_page_prot);
    76. if (write_access)
    77. entry = pte_mkwrite(pte_mkdirty(entry));
    78. set_pte(page_table, entry);
    79. pte_chain = page_add_rmap(new_page, page_table, pte_chain);
    80. pte_unmap(page_table);
    81. } else {
    82. /* One of our sibling threads was faster, back out. */
    83. pte_unmap(page_table);
    84. page_cache_release(new_page);
    85. spin_unlock(&mm->page_table_lock);
    86. ret = VM_FAULT_MINOR;
    87. goto out;
    88. }
    89. /* no need to invalidate: a not-present page shouldn't be cached */
    90. update_mmu_cache(vma, address, entry);
    91. spin_unlock(&mm->page_table_lock);
    92. ret = VM_FAULT_MAJOR;
    93. goto out;
    94. oom:
    95. ret = VM_FAULT_OOM;
    96. out:
    97. pte_chain_free(pte_chain);
    98. return ret;
    99. }

    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挺轻松的。

    最后,如果本帖对您有一定的帮助,希望能点赞+关注+收藏!您的支持是给我最大的动力,后续会一直更新各种框架的使用和框架的源码解读~!

  • 相关阅读:
    2022年4月4日:使用 C# 迈出第一步--编写第一个C#代码
    MYSQL海量数据存储与优化
    Nat. Biomed. Eng.| 综述:医学和医疗保健中的自监督学习
    多线程&并发篇---第十四篇
    uniapp实现图片上传——支持APP、微信小程序
    thinkphp withJoin 模式下field 无效
    滚珠螺杆应如何存放避免受损
    企业信息化建设该如何评估自己ERP、MES、APS需求
    Linux下IPv6地址的配置
    【机器学习】实验4布置:AAAI会议论文聚类分析
  • 原文地址:https://blog.csdn.net/qq_43799161/article/details/126149128