目录

  • 1. 前言
  • 2. SYSCALL_DEFINE1(brk, unsigned long, brk)
    • |- -__do_munmap
    • |- -do_brk_flags
      • |- - -get_unmapped_area
      • |- - -munmap_vma_range
    • |- -mm_populate
      • |- - -populate_vma_page_range
        • |- - - -__get_user_pages
          • |- - - - - follow_page_mask
          • |- - - - - faultin_page
  • 参考文档

1. 前言

本专题我们开始学习内存管理部分,本文为进程地址空间的学习笔记。本文主要参考了《奔跑吧, Linux内核》、ULA、ULK的相关内容。

malloc函数是C函数库封装的一个核心函数,C函数库会做一些处理后调用系统调用brk,如果把malloc想想成零售,brk就是代理商。malloc函数的实现为用户进程维护一个本地小仓库,当进程需要使用更多的内存时就向这个小仓库要货,小仓库存量不足时就通过代理商brk向内核批发。

下面是在学习过程中记录的几个问题:
Q: malloc函数返回的内存是否马上分配物理内存?
A: 在真正使用这段内存时才通过缺页中断来分配
Q: 实际使用时,malloc分配100字节,实际上内核分配多少?
A: 按页对齐,最少分配一个页
Q:如果两个进程的地址相同,会有冲突吗?
A:不会冲突,每个进程维护自己的页表,两个相同的地址分别属于不同的VMA
Q:vm_normal_page函数返回什么样的页面?
只返回normal mapping页面的page,它们可以被回收或合并,其它特定页面不返回
Q:get_user_page函数的作用?
A: 为用户空间的地址创建页表,分配页面
Q:follow_page函数的作用?
A:根据虚拟地址返回物理页面

kernel版本:5.10
平台:arm64

2. SYSCALL_DEFINE1(brk, unsigned long, brk)

如下为进程地址空间的灵活布局方式的一个示例, 本文以此种布局进行介绍(区别于传统布局):

brk系统调用主要实现在mm/mmap.c中

SYSCALL_DEFINE1(brk, unsigned long, brk)|  //动态堆分配区的新边界地址|--newbrk = PAGE_ALIGN(brk)|  //动态堆分配区当前边界地址 |--oldbrk = PAGE_ALIGN(mm->brk) |-------//>>>>>>情形1:释放堆内存|  //如果动态堆分配区新的边界地址小于当前边界地址,表示进程请求释放堆空间|--if (brk <= mm->brk) |      //更新动态堆分配区当前边界地址为新边界地址|      mm->brk = brk |      //unmap [newbrk,oldbrk]空间|      __do_munmap(mm, newbrk, oldbrk-newbrk, &uf, true) |      goto success |------//>>>>>>情形2:申请堆内存|--next = find_vma(mm, oldbrk)|--if (next && newbrk + PAGE_SIZE > vm_start_gap(next))|      goto out;|   //以老边界oldbrk开始,以(newbrk-oldbrk)大小分配vma|-- if (do_brk_flags(oldbrk, newbrk-oldbrk, 0, &uf) < 0)|      goto out;|  //分配成功后更新动态分配区当前边界|--mm->brk = brk|--populate = newbrk > oldbrk && (mm->def_flags & VM_LOCKED) != 0\--if (populate)mm_populate(oldbrk, newbrk - oldbrk)
  1. newbrk 为动态分配区的新边界地址,是用户进程要求分配内存大小与当前动态分配区底部边界地址的和。而mm_struct有个数据成员brk用于记录动态分配区当前的底部边界地址,它是动态分配区起始地址与当前使用的动态分配区大小的和

  2. oldbrk记录了动态分配区当前底部边界地址

  3. 如果动态分配区新的边界地址小于当前边界地址,表示进程请求释放堆内存,则调用__do_munmap执行空间释放

  4. 如果动态分配区新的边界地址大于当前边界地址,就是申请堆内存,以动态分配区的当前边界地址oldbrk开始查找是否有一块vma与[oldbrk,newbrk]的区域有重叠(find_vma加上下面的代码判断等价于find_vma_intersection),如果有重叠说明以老边界oldbrk开始的地址空间已经在使用了,直接将老边界返回给用户空间,退出函数?

  5. do_brk_flags以老边界oldbrk开始分配vma,覆盖newbrk-oldbrk大小的空间

  6. mm_populate:应用可能会使用mlokall()系统调用把进程中全部的进程虚拟空间加锁,防止内存被交换,此时mm->def_flags会置位VM_LOCKED, 此时会调用mm_populate立刻分配物理内存并建立映射关系

|- -__do_munmap

__do_munmap(mm, newbrk, oldbrk-newbrk, &uf, true)//start为newbrk,len为oldbrk-newbrk|--vma = find_vma(mm, start)|--prev = vma->vm_prev|--if (start > vma->vm_start)//start地址位于vma线性区|      __split_vma(mm, vma, start, 0) //如下图,拆分成vma和new1两个线性区|--last = find_vma(mm, end)|--if (last && end > last->vm_start)//end位于last线性区|      __split_vma(mm, last, end, 1)//如下图,拆分成last和new2两个线性区|--unlock any mlock()ed ranges before detaching vmas|--detach_vmas_to_be_unmapped(mm, vma, prev, end) |--unmap_region(mm, vma, prev, start, end)//删除与线性区对应的页表项并释放相应的页框\--remove_vma_list(mm, vma) //vma上挂载者所有的待删除线性区


__do_munmap就是删除用户空间地址newbrk到newbrk~oldbrk的vma线性区,并删除vma线性区对应的页表项,释放相应的页

  1. detach_vmas_to_be_unmapped:vma上挂载着所有的待删除线性区,从内存描述符的mm_rb删除从vma开始(图示new1)的线性区,一直删除到end地址(图示end)所在的线性区

  2. unmap_region:删除vma开始的所有的待删除线性区对应的页表项并释放相应的页框

  3. remove_vma_list: 删除vma开始的所有的待删除线性区,此处根据线性区回调执行一些预处理,然后释放其占用的slab缓存

unmap_region(mm, vma, prev, start, end) //删除vma开始的所有的待删除线性区对应的页表项并释放相应的页框|--lru_add_drain()|--tlb_gather_mmu(&tlb, mm, start, end)|--update_hiwater_rss(mm)|--unmap_vmas(&tlb, vma, start, end)|      |--for ( ; vma && vma->vm_start < end_addr; vma = vma->vm_next)|             unmap_single_vma(tlb, vma, start_addr, end_addr, NULL)|--free_pgtables(&tlb, vma,...)|--tlb_finish_mmu(&tlb, start, end)|--mm_tlb_flush_nested(tlb->mm) //刷新TLB|--tlb_flush_mmu(tlb) // 释放page|--tlb_flush_mmu_tlbonly(tlb)|--tlb_flush_mmu_free(tlb)

unmap_region删除vma开始的所有的待删除线性区对应的页表项并释放相应的页框

  1. unmap_vmas: 参数vma为要unmap的线性区链表的首个线性区,此处unmap线性区链表所覆盖的page,地址范围为start~end,主要是清空对应的pte页表项

  2. free_pgtables: 因为删除了一些映射,会造成一个pte页表空闲的情况,回收这些页表所占的空间

  3. tlb_finish_mmu: 结束unmap_region的工作,主要刷新了mmu, 释放了页框,tlb_flush_mmu_free的实现较为复杂(TODO)

unmap_single_vma(tlb, vma, start_addr, end_addr, NULL)//unmap一个线性区vma的所覆盖的所有page|--unmap_page_range(tlb, vma, start, end, details) //unmap地址范围start~end的page|--pgd = pgd_offset(vma->vm_mm, addr)|--do {next = pgd_addr_end(addr, end)next = zap_p4d_range(tlb, vma, pgd, addr, next, details)} while (pgd++, addr = next, addr != end)

unmap一个线性区vma的所覆盖的所有page

zap_p4d_range(tlb, vma, pgd, addr, next, details)|--p4d = p4d_offset(pgd, addr)|--do {next = p4d_addr_end(addr, end)next = zap_pud_range(tlb, vma, p4d, addr, next, details)} while (p4d++, addr = next, addr != end)
zap_pud_range(tlb, vma, p4d, addr, next, details)|--pud = pud_offset(p4d, addr)|--do {next = pud_addr_end(addr, end)next = zap_pmd_range(tlb, vma, pud, addr, next, details)} while (pud++, addr = next, addr != end)
zap_pmd_range(tlb, vma, pud, addr, next, details)|--pmd = pmd_offset(pud, addr)|--do {next = pmd_addr_end(addr, end)next = zap_pte_range(tlb, vma, pmd, addr, next, details)} while (pmd++, addr = next, addr != end)
zap_pte_range(tlb, vma, pmd, addr, next, details)|--start_pte = pte_offset_map_lock(mm, pmd, addr, &ptl)|--pte = start_pte|--do {|      pte_t ptent = *pte|      if (pte_present(ptent)) {//相应的页在主存中(pte有效,页面未回收)|          page = vm_normal_page(vma, addr, ptent)|          ptent = ptep_get_and_clear_full(mm, addr, pte,tlb->fullmm)//清空addr对应的pte|          if (!PageAnon(page))|              if (pte_dirty(ptent))//页面被修改过|                  force_flush = 1|                  set_page_dirty(page)|              if (pte_young(ptent)//页面被刚刚访问过|                  mark_page_accessed(page)|           continue;|       }||       entry = pte_to_swp_entry(ptent)|       if (is_device_private_entry(entry)) {|           struct page *page = device_private_entry_to_page(entry);|           pte_clear_not_present_full(mm, addr, pte, tlb->fullmm)|           rss[mm_counter(page)]--|           page_remove_rmap(page, false)|           put_page(page)|           continue;|       }||       pte_clear_not_present_full(mm, addr, pte, tlb->fullmm)//清空addr对应的pte|  } while (pte++, addr += PAGE_SIZE, addr != end)|--add_mm_rss_vec(mm, rss)\--arch_leave_lazy_mmu_mode()

vm_normal_page根据pte来返回normal paging页面的struct page结构。

from:https://www.cnblogs.com/arnoldlu/p/8329283.html
一些特殊映射的页面是不会返回struct page结构的,这些页面不希望被参与到内存管理的一些活动中,如页面回收、页迁移和KSM等。
内核尝试用pte_mkspecial()宏来设置PTE_SPECIAL软件定义的比特位,主要用途有:
1.内核的零页面zero page
2.大量的驱动程序使用remap_pfn_range()函数来实现映射内核页面到用户空间。这些用户程序使用的VMA通常设置了(VM_IO|VM_PFNMAP|VM_DONTEXPAND|VM_DONTDUMP)
3.vm_insert_page()/vm_insert_pfn()映射内核页面到用户空间
vm_normal_page()函数把page页面分为两阵营,一个是normal page,另一个是special page。
normal page通常指正常mapping的页面,例如匿名页面、page cache和共享内存页面等。
special page通常指不正常mapping的页面,这些页面不希望参与内存管理的回收或者合并功能,比如:
1.VM_IO:为IO设备映射
2.VM_PFN_MAP:纯PFN映射
3.VM_MIXEDMAP:固定映射

|- -do_brk_flags

如果不是释放空间,而是申请堆空间则调用do_brk_flags

do_brk_flags(addr, len,  flags, uf)|--mapped_addr = get_unmapped_area(NULL, addr, len, 0, MAP_FIXED)|--munmap_vma_range(mm, addr, len, &prev, &rb_link, &rb_parent, uf)|--vma = vma_merge(mm, prev, addr, addr + len, flags,|       NULL, NULL, pgoff, NULL, NULL_VM_UFFD_CTX)|  //create a vma struct for an anonymous mapping|--vma = vm_area_alloc(mm)|--vma_set_anonymous(vma) //vma->vm_ops = NULL|--vma->vm_start = addr //初始化vma|  vma->vm_end = addr + len|  vma->vm_pgoff = pgoff|  vma->vm_flags = flags|  //vm_get_page_prot通过flags获取pte的相关属性|  vma->vm_page_prot = vm_get_page_prot(flags) |  vma_link(mm, vma, prev, rb_link, rb_parent)\--mm->total_vm += len >> PAGE_SHIFTmm->data_vm += len >> PAGE_SHIFT

do_brk_flags以addr查找或创建一个vma并插入到rb_tree

  1. get_unmapped_area:在进程地址空间中寻找一个可以使用的线性地址区间,它返回一段没有映射过的空间的起始地址

  2. munmap_vma_range:遍历用户红黑树中的VMA,然后根据addr来查找最合适插入红黑树的节点, rb_link指针指向最合适节点的rb_left或rb_right指针本身的地址

  3. vma_merge:检查有没有办法合并addr附近的vma , 如果有则直接合并,并goto out;

  4. vm_area_alloc: 如果不能合并则创建一个新的vma,新的vma的地址为[addr, addr+len]

|- - -get_unmapped_area

get_unmapped_area(NULL, addr, len, 0, MAP_FIXED)|--arch_get_unmapped_area_topdown(NULL, addr, len, 0, MAP_FIXED)|--mmap_end = arch_get_mmap_end(addr) //为TASK_SIZE

在load elf文件时会通过setup_new_exec->arch_pick_mmap_layout来初始化mm->get_unmapped_area,对于arm64就是arch_get_unmapped_area_topdown

|- - -munmap_vma_range

munmap_vma_range(mm, addr, len, &prev, &rb_link, &rb_parent, uf)//addr为start,rb_link为link,rb_parent为parent|--while (find_vma_links(mm, start, start + len, pprev, link, parent))do_munmap(mm, start, len, uf)

find_vma_links:遍历用户红黑树中的VMA,然后根据addr来查找最合适插入红黑树的节点, rb_link指针指向最合适节点的rb_left或rb_right指针本身的地址,如果返回0表示找到最合适插入的节点

如果find_vma_links返回-NOMEM,表示和现有的VMA重叠,会调用do_munmap释放这段重叠的空间

|- -mm_populate

 mm_populate(addr,  len)|--__mm_populate(addr, len, 1)|--for (nstart = start; nstart < end; nstart = nend) {/*查找vma*/vma = find_vma(mm, nstart) if (!vma || vma->vm_start >= end)break;nend = min(end, vma->vm_end)if (nstart < vma->vm_start)nstart = vma->vm_start/*制造缺页异常并完成地址映射*/ret = populate_vma_page_range(vma, nstart, nend, &locked)nend = nstart + ret * PAGE_SIZEret = 0}

__mm_populate通过一个for循环遍历[addr,addr+len]区间的所有vma,通过populate_vma_page_range对vma所描述的地址区间,通过人为制造缺页异常完成物理页面分配和地址映射。

|- - -populate_vma_page_range

populate_vma_page_range(vma,start, end, locked)|--gup_flags = FOLL_TOUCH | FOLL_POPULATE | FOLL_MLOCK |--if (vma->vm_flags & VM_LOCKONFAULT)|      gup_flags &= ~FOLL_POPULATE|--if ((vma->vm_flags & (VM_WRITE | VM_SHARED)) == VM_WRITE)|      gup_flags |= FOLL_WRITE;|--if (vma_is_accessible(vma))|      gup_flags |= FOLL_FORCE;|--__get_user_pages(mm, start, nr_pages, gup_flags,NULL, NULL, locked)

gup_flags:设置分配掩码标志位

__get_user_pages:为进程地址空间分配物理内存并建立映射关系

|- - - -__get_user_pages

__get_user_pages(mm, start, nr_pages, gup_flags,NULL, NULL, locked)|--do {vma = find_extend_vma(mm, start);page = follow_page_mask(vma, start, foll_flags, &ctx)if (!page) //vma没有被分配物理内存,也没有创建映射faultin_page(vma, start, &foll_flags, locked)} while (nr_pages);

__get_user_pages为用户空间分配内存并创建映射

  1. find_extend_vma: 查找新的vma的start相邻的vma,并将vma->start与新的vma的start比较,如果vma->start大于新的vma的 start,考虑将vma进行扩容到新的vma的start

  2. follow_page_mask:查看vma是否已经被分配了物理内存,如果已经映射,返回page

  3. faultin_page:如果vma没有映射,人为触发缺页中断,分配内存,建立映射

|- - - - - follow_page_mask
follow_page_mask(vma, address, flags, ctx)|  //处理巨页情况|--page = follow_huge_addr(mm, address, flags & FOLL_WRITE)|  //通过地址找到当前进程页表的pgd目录项|--pgd = pgd_offset(mm, address)|  //遍历p4d,Linux支持5级页表,arm64支持4级|--follow_p4d_mask(vma, address, pgd, flags, ctx) |  //通过地址找到当前进程页表的p4d目录项|--p4d = p4d_offset(pgdp, address) |  //遍历pud|--follow_pud_mask(vma, address, p4d, flags, ctx) |  //通过地址找到当前进程页表的pud目录项|--pud = pud_offset(p4dp, address) |  //遍历pmd|--follow_pmd_mask(vma, address, pud, flags, ctx) |  //通过地址找到当前进程页表的pmd目录项|--pmd = pmd_offset(pudp, address)|  //遍历pte|--follow_page_pte(vma, address, pmd, flags, &ctx->pgmap)

follow_page_mask最终根据address和内存描述符返回normal page物理页面的struct page结构

follow_page_pte(vma, address, pmd, flags, &ctx->pgmap)|  //通过pmd和address获取pte页表项|--ptep = pte_offset_map_lock(mm, pmd, address, &ptl)|--pte = *ptep;|  //根据pte来返回normal paging物理页面的struct page结构|--page = vm_normal_page(vma, address, pte)|--if (!pte_present(pte)) //页表项无效|      if (likely(!(flags & FOLL_MIGRATION)))|         goto no_page;|      if (pte_none(pte)) //页表项为空|          goto no_page|      entry = pte_to_swp_entry(pte)|      if (!is_migration_entry(entry))|         goto no_page|      migration_entry_wait(mm, pmd, address);|  //处理设备映射|--if (!page && pte_devmap(pte) && (flags & (FOLL_GET | FOLL_PIN)))|      *pgmap = get_dev_pagemap(pte_pfn(pte), *pgmap)|      if (*pgmap)|          page = pte_page(pte)|  //标记页面是活跃的,页面回收的核心辅助函数|--if (flags & FOLL_TOUCH)mark_page_accessed(page);

follow_page_pte根据pte返回normal paging物理页面的struct page结构,此page可能存在也可能不存在,如果不存在则通过下面的faultin_page来人为触发缺页中断

|- - - - - faultin_page
faultin_page(vma, start, &foll_flags, locked)|--if (*flags & FOLL_WRITE)|           fault_flags |= FAULT_FLAG_WRITE;|   if (*flags & FOLL_REMOTE)|           fault_flags |= FAULT_FLAG_REMOTE;|   if (locked)|           fault_flags |= FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE;|   if (*flags & FOLL_NOWAIT)|           fault_flags |= FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_RETRY_NOWAIT;|   if (*flags & FOLL_TRIED)|           fault_flags |= FAULT_FLAG_TRIED;|--handle_mm_fault(vma, address, fault_flags, NULL)

faultin_page通过handle_mm_fault人为触发缺页中断,分配内存,建立映射

参考文档

  1. https://www.pengrl.com/p/20032/
  2. http://blog.chinaunix.net/uid-20543183-id-1930786.html
  3. 奔跑吧,Linux内核
  4. ULK

内存管理基础学习笔记 - 3.2 进程地址空间 - brk系统调用相关推荐

  1. 内存管理基础学习笔记 - 3.1 进程地址空间 - VMA线性区

    目录 1. 前言 2. 进程地址空间 3. 用户空间与内核空间的隔离 4. 进程地址空间主要数据结构 struct mm_struct struct vm_area_struct 5. vma的标志属 ...

  2. 内存管理基础学习笔记 - 5.2 页面回收 - kswapd

    目录 1. 前言 2. kswapd_init 3.kswapd |- -balance_pgdat |- - -pgdat_balanced |- - -kswapd_shrink_node |- ...

  3. 内存管理基础学习笔记 - 2. 内核地址空间 - SLAB

    目录 1. 前言 2. slab总体说明 3. kmem_cache_create |- -__kmem_cache_create |- - -setup_cpu_cache 4. kmem_cach ...

  4. 《深入理解LINUX内存管理》学习笔记(一)

    引子 为什么要写这个笔记: 1,这本书的中文版翻译了太垃圾,没法阅读.阅读英文原版,可以很好的理解作者的思路.作此笔记备忘 2,一直以来学习LINUX kernel的知识缺乏系统化,借对这本书的学习, ...

  5. Arm V8内存管理架构.学习笔记

    目录 第1章 分级存储架构 1.1基础认识 1.1.1 从数据通路描述 1.1.2 从数据交换单位描述 1.1.3 Cache数据一致性拓扑结构 1.2 系统层内存模型 1.2.1 内存属性 1.2. ...

  6. RT-Thread 静态内存管理(学习笔记)

    本文参考自[野火EmbedFire]<RT-Thread内核实现与应用开发实战--基于STM32>,仅作为个人学习笔记.更详细的内容和步骤请查看原文(可到野火资料下载中心下载) 文章目录 ...

  7. RT-Thread 动态内存管理(学习笔记)

    本文参考自[野火EmbedFire]<RT-Thread内核实现与应用开发实战--基于STM32>,仅作为个人学习笔记.更详细的内容和步骤请查看原文(可到野火资料下载中心下载) 文章目录 ...

  8. arm64的ioremap_ARMv8 内存管理架构.学习笔记

    第1章分级存储架构 1.1基础认识 通常为了保证计算机的整体性能,内存和CPU之间的通信需保证很高的传输速率,然而这受限制于内存的大小和昂贵的硬件实现,传输速率和内存容量大小的关系遵循"Sm ...

  9. 8.Python基础学习笔记day8-正则表达式、网络编程、进程与线程

    8.Python基础学习笔记day8-正则表达式.网络编程.进程与线程 一.正则表达式 ''' 1. [1,2,3,4]中任意取3个元素排列: A43 = 4x3x2 = 24itertools.pe ...

最新文章

  1. shell脚本[] [[]] -n -z 的含义解析
  2. 在方法的形参位置使用@Qualifier注解||@Autowired 与@Resource的区别
  3. Linux新建用户图解
  4. Crawler:基于urllib库+实现爬虫有道翻译
  5. PIC单片机精通_A/D模数转换模块细节补充
  6. “大数据标准”征稿通知
  7. java 面试题分析
  8. java中阻止类的继承
  9. hp打印机驱动android,惠普打印机驱动
  10. UG8.5有限元仿真分析基础到高级视频教程
  11. 行政区划编码转换区域名工具类
  12. OSPF-1.ospf基础及工作流程
  13. 银行股的分红是不是比利率要高,投十万银行股一年分红有多少啊?
  14. 小程序新爆点,小程序能分享朋友圈了
  15. 服务器的回收站在哪个文件夹,Windows系统回收站的文件保存在哪个磁盘上
  16. C语言 文件操作 深度解析 #重点知识:文件操作函数的使用#
  17. java keyevent 组合键_JAVA中KeyEvent类键盘各键的代码
  18. OneDrive - “You‘re already syncing this account.“ 错误的解决方案
  19. 【JavaWeb】AJAX
  20. java毕业生设计医生咨询系统计算机源码+系统+mysql+调试部署+lw

热门文章

  1. 类似微信的联系人根据字母排列查询
  2. C++: 有限元法 (FEM) 应用于线性两点 一个空间维度上的边界值问题 (附完整源码)
  3. Linux压缩及解压缩命令
  4. 在Github中下载一个文件夹里的所有文件
  5. Jenkins 教程(安装和解锁)
  6. vue websocket 新消息提醒
  7. api接口 服务器是腾讯的lol服务器
  8. 基于SpringBoot的家政服务管理平台
  9. redis启动、获取密码及修改密码
  10. 文档处理教程:使用C#在PowerPoint演示文稿中创建图表