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

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_SHIFT
       mm->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_SIZE
                ret = 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
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值