Linux brk(),mmap()系统调用源码分析 brk()的内存释放流程

荣涛 2021年4月30日

  • 内核版本:linux-5.10.13
  • 注释版代码:https://github.com/Rtoax/linux-5.10.13

1. 基础部分

在上篇文章中已经介绍了基础部分 《Linux内存管理 brk(),mmap()系统调用源码分析1:基础部分》,本文介绍brk的释放部分。

下面开始介绍brk释放流程。

brk会提高或者降低堆顶位置,从而达到分配和释放用户地址空间的效果。

首先获取brk开始的地方,如果新的brk小于最小的brk,直接退出:

min_brk = mm->start_brk;
if (brk < min_brk)goto out;

接着,检测进程允许的数据大小,如果超限,直接退出:

if (check_data_rlimit(rlimit(RLIMIT_DATA), brk, mm->start_brk,mm->end_data, mm->start_data))goto out;

上面的结构就像上节提高过的地址空间,其中数据结构如下:

+-------+ brk
|       |
|       |   堆
|  heap |
+-------+ mm->start_brk
|       |
|  ...  |
|       |
+-------+ mm->end_data
|       |
|  data |   数据段
|       |
+-------+ mm->start_data

接着将brk页对齐newbrk = PAGE_ALIGN(brk);,这也是为什么申请几个字节的数据,越界使用也不会出错,但是超出页大小就会段错误的原因。然后获取上次brk的也对齐位置,当这两个数值对齐后相等,那么就可以直接推出了:

 newbrk = PAGE_ALIGN(brk);       /* 新的 brk :页对齐,申请大小对齐 page */oldbrk = PAGE_ALIGN(mm->brk);   /* 旧的 brk */if (oldbrk == newbrk) { /* brk 位置没有发生变化 */mm->brk = brk;goto success;}

如果新的brk小于上次的brk呢?很好理解,就是对应free/release操作呗,brk <= mm->brk

2. 释放

如果brk <= mm->brk,首先更新brk位置mm->brk = brk;,然后调用__do_munmap函数,

__do_munmap(mm, newbrk, oldbrk-newbrk, &uf, true)

参数列表为:

  • mm:进程地址空间mm_struct结构;
  • newbrk:新的页对齐的brk位置;
  • oldbrk-newbrk:长度(也是页对齐的);
  • uf:链表头,在上面初始化LIST_HEAD(uf);
  • true:代表downgrade;

此时的关系为:

+-------+ oldbrk
|       |
|       |
|       | newbrk ~ mm->brk 约等于,页对齐
|       |
|       |
+-------+ mm->start_brk

下面详细看下__do_munmap函数实现。

3. __do_munmap

函数原型为:

int __do_munmap(struct mm_struct *mm, unsigned long start, size_t len,struct list_head *uf, bool downgrade)

也就是说,他是这样的:

+-------+---
|       |
|       | len
|       |--- start = newbrk(页对齐位置)
|       |
|       |
+-------+ mm->start_brk

接收,首先是否超出判断:

 if ((offset_in_page(start)) || start > TASK_SIZE || len > TASK_SIZE-start)return -EINVAL;

因为已经进行了页对齐,start在页内偏移一定为0,所以offset_in_page(start)为真时,返回错误,另外两个判断是对大小的判断。

接着,获取有几个页的长度len = PAGE_ALIGN(len);,这里如果没问题的话,len应该等于0,4096,8192这些数值,然后计算结束点位置end = start + len,即:

+-------+--- end
|       |
|       | len
|       |--- start
|       |
|       |
+-------+ mm->start_brk

接着就调用架构相关的unmap函数arch_unmap,在x86下这个函数为空:

static inline void arch_unmap(struct mm_struct *mm, unsigned long start,unsigned long end)/*  */
{}

然后,将start转化为vma(搜索),使用find_vma,为了加速,里面会首先看cache中是不是有vmacache_find,为了加速查找,vma是保存在mm的红黑树中,可从数据结构中查阅。关于find_vma的详细介绍不在过多赘述。

至此,就获取到了start地址所属的VMA结构vma

    +-------+--- end|       |                   +-------+|       | len               |  VMA  ||       |--- start -------->|       ||       |                   |       ||       |                   |       |+-------+ mm->start_brk     |       |+-------+

下一步获取上一个vma结构(双向链表)prev = vma->vm_prev

    +-------+--- end|       |                   +-------+ vma->vm_end|       | len               |  vma  ||       |--- start -------->|       ||       |                   |       ||       |                   |       |+-------+ mm->start_brk     |       ||       |+-------+ vma->vm_start+-------+ | prev  ||       ||       ||       ||       ||       |+-------+

接下来检测vma->vm_start >= end(内核真的是比较鲁棒,各种安全检测)。紧接着,又是检测start > vma->vm_start,这种情况是啥呢?

    +-------+--- end|       |                   +-------+ vma->vm_end|       | len               |  vma  ||       |--- start -------->|       | <-- end|       |                   |       ||       |                   |       | <-- start+-------+ mm->start_brk     |       ||       |+-------+ vma->vm_start

这里简单介绍一个变量sysctl_max_map_count,它是内核sysctl参数,默认值为65530

如果是上图情况,将直接返回

     if (end < vma->vm_end && mm->map_count >= sysctl_max_map_count) return -ENOMEM;

否则如果是这种情况:

    +-------+--- end|       |                   +-------+ vma->vm_end |       | len               |  vma  ||       |--- start -------->|       | <-- end|       |                   |       ||       |                   |       | <-- start+-------+ mm->start_brk     |       ||       |+-------+ vma->vm_start

进行vma的分割,使用__split_vma函数实现。下面详细介绍。

3.1. __split_vma

函数原型为:

int __split_vma(struct mm_struct *mm, struct vm_area_struct *vma,unsigned long addr, int new_below)

在上面的情况中参数对应关系为:

    +-------+--- end|       |                   +-------+ vma->vm_end|       | len               |  vma  ||       |--- start -------->|       | <-- end|       |                   |       ||       |                   |       | <-- addr ****+-------+ mm->start_brk     |       ||       |+-------+ vma->vm_start
  • new_below=0

首先使用vm_area_dup为新的vma分配内存(kmem_alloc),并且这个结构没有加入链表中new->vm_next = new->vm_prev = NULL。当new_below=0时,new->vm_start = addr,并计算其在页中的偏移new->vm_pgoff += ((addr - vma->vm_start) >> PAGE_SHIFT)。接着调用vma_dup_policy赋值内存策略,这是和NUMA相关的。然后进行匿名vma克隆anon_vma_clone,这和反向映射相关,本文不做过多解释。

紧接着,判断是否为文件映射,如果是,增加file结构的引用计数,如果vm_ops存在,调用open方法:

 if (new->vm_file)get_file(new->vm_file);if (new->vm_ops && new->vm_ops->open)new->vm_ops->open(new);

上面的两步,不是本文的重点,下面调用vma_adjust函数。需要说明的是,在调用vma_adjust之前,new的结构是这样的:

                                          <-- end+-------+--- end|       |                   +-------+ vma->vm_end   +-------+|       | len               |  vma  |               |       ||       |--- start -------->|       |               |  new  ||       |                   |       |               |       ||       |                   |       | <-- addr **** +-------+ <-- new->vm_start+-------+ mm->start_brk     |       ||       |+-------+ vma->vm_start

vma_adjust会调用__vma_adjust

static inline int vma_adjust(struct vm_area_struct *vma, unsigned long start,unsigned long end, pgoff_t pgoff, struct vm_area_struct *insert)
{return __vma_adjust(vma, start, end, pgoff, insert, NULL);
}

__vma_adjust函数的注释是:

如果不调整树,则无法调整i_mmap树中已经存在的vma的vm_start,vm_end,vm_pgoff字段。 当需要进行此类调整时,应使用以下帮助器功能。 在插入必要的锁之前,将插入“insert” vma(如果有)。

3.2. __vma_adjust

这个函数比较复杂。

函数原型为:

int __vma_adjust(struct vm_area_struct *vma, unsigned long start,unsigned long end, pgoff_t pgoff, struct vm_area_struct *insert,struct vm_area_struct *expand)

对应参参数变量分别为:

  • vma:当前操作传递vma结构;
  • start:vma->vm_start;
  • end:addr,也就是new的vm_start;
  • pgoff:vma->vm_pgoff;
  • insert:new;
  • expand:NULL;

获取vma的next vma结构next = vma->vm_next,如下图:

                                +-------+|       ||       ||       || next  ||       ||       |+-------++-------+--- end            orig_vma|       |                   +-------+               +-------+|       | len               |  vma  |               |       ||       |--- start -------->|       |               | insert||       |                   |       |               |       ||       |                   |       | <-- end ----> +-------++-------+ mm->start_brk     |       ||       |+-------+ start

这个分支if (next && !insert)我们先不用看。来到again:标签处,vma_adjust_trans_huge函数被调用,入参分别为vma_adjust_trans_huge(orig_vma, start, end, adjust_next=0);,这是和大页内存相关的,本文先略过。紧接着是文件映射if (file),在后面就是anon_vma,这里给出一个简图:

这是反向映射的基石,本文不讨论。接下来迎接来了代码:

 if (start != vma->vm_start) {vma->vm_start = start;start_changed = true;}if (end != vma->vm_end) {vma->vm_end = end;end_changed = true;}vma->vm_pgoff = pgoff;

这个操作很简单,直接看图就行了:

     +-------+|  vma  ||       | |       ||       | <-- end|       ||       |+-------+ start>>>> 变为+-------+ <-- vma->vm_end|  vma  ||       |+-------+ <-- vma->vm_start

接下来的remove_next不执行。转而执行else if (insert)分支,这个分支很简单,执行__insert_vm_struct函数,该函数原型是:

static void __insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vma)
{struct vm_area_struct *prev;struct rb_node **rb_link, *rb_parent;if (find_vma_links(mm, vma->vm_start, vma->vm_end,&prev, &rb_link, &rb_parent))BUG();__vma_link(mm, vma, prev, rb_link, rb_parent);mm->map_count++;
}

将vma结构添加至mm结构的红黑树和双向链表。后面执行的validate_mm是打开CONFIG_DEBUG_VM_RB功能的操作,不做讨论。

至此,vma_adjust就返回了,接着__split_vma也返回了。

     err = vma_adjust(vma, vma->vm_start, addr, vma->vm_pgoff, new);/* Success. */if (!err)return 0;

现在回到__do_munmap。在__split_vma成功放回后,执行prev = vma;操作。

 error = __split_vma(mm, vma, start, 0); /* 分离一个 vma 结构 */if (error)return error;prev = vma;

要知道,此时的vma比原来要小了,并且它的下一个vma是自己被切出来的,查找下一个vma结构:

last = find_vma(mm, end);

如果是下面这种情况,需要继续拆分vma结构:

                                +-------+|       | |       | +-------+--- end            |       | <-- end|       |                   | last  ||       | len               |       | |       |                   +-------+ last->vm_start|       |                   |       |                   +-------+ vma->vm_end+-------+ mm->start_brk     | prev  ||       |+-------+ vma->vm_start

这与上面的情况正好相反,上面是需要拆分的部分在vma之上,现在是需要查分的部分在vma之下,所以在调用__split_vma时候的标志位new_below这次等于1

接入函数后,还是申请vma新的结构,然后进行vm_end赋值,此时的结构为:

    +-------+|       | |       | |       | <-- addr  +-------+ <-- new->vm_end|  vma  |           |       ||       |           |  new  |+-------+           +-------+

然后进行vma_adjust操作,可以不做过多解释了,直接给出一段我对他的代码注释吧。

if (new_below)/* +-------+|       | |       | |       | <-- addr      +-------+ <-- new->vm_end|  vma  |               |       ||       |               |  new  |+-------+               +-------+ <-- new->vm_start*/err = vma_adjust(vma, addr, vma->vm_end, vma->vm_pgoff +((addr - new->vm_start) >> PAGE_SHIFT), new);
else/*+-------+ vma->vm_end   +-------+|       |               |       ||       |               |  new  ||  vma  |               |       ||       | <-- addr **** +-------+ <-- new->vm_start|       ||       |+-------+ vma->vm_start*/err = vma_adjust(vma, vma->vm_start, addr, vma->vm_pgoff, new);

整体上面对vma的拆分工作可以认为是将用户地址空间需要释放的区域单独组建vma结构,从其他的vma中隔离出来。

3.3. detach_vmas_to_be_unmapped

上面拆分完vma后,需要将这些vma从红黑树中擦除,擦除的范围呢?从函数的调用中可以看:

detach_vmas_to_be_unmapped(mm, vma, prev, end)
  • mm:当前的进程地址空间;
  • vma:是prev的下一个vma,vma = vma_next(mm, prev);
  • prev:不在free空间的最后一个vma;
  • end:需要free的最大地址;

那就好理解了,从vma开始遍历红黑树,并对其进行重新连接,代码如下:

 insertion_point = (prev ? &prev->vm_next : &mm->mmap);vma->vm_prev = NULL;do {vma_rb_erase(vma, &mm->mm_rb);mm->map_count--;tail_vma = vma;vma = vma->vm_next;} while (vma && vma->vm_start < end);*insertion_point = vma;

这段代码不用解释了吗,很简单。如果说需要释放的空间以上(next)还有有效的vma怎么办呢,更简单:

if (vma) {vma->vm_prev = prev;vma_gap_update(vma);
}

然后将最后一个vma的next置空tail_vma->vm_next = NULL;。接下来的判断是:

 /** Do not downgrade mmap_lock if we are next to VM_GROWSDOWN or* VM_GROWSUP VMA. Such VMAs can change their size under* down_read(mmap_lock) and collide with the VMA we are about to unmap.*/if (vma && (vma->vm_flags & VM_GROWSDOWN))return false;if (prev && (prev->vm_flags & VM_GROWSUP))return false;

上面这两个判断会在后续的文章中讲解,detach_vmas_to_be_unmapped到此结束。

如果detach_vmas_to_be_unmapped执行失败,将执行下面的代码,本文也不做讲解。

 if (downgrade)mmap_write_downgrade(mm);

3.4. unmap_region

接下来迎接的就是unmap_region函数了,在该函数的定义如下:

/** Get rid of page table information in the indicated region.** Called with the mm semaphore held.*/ /*  */
static void unmap_region(struct mm_struct *mm,struct vm_area_struct *vma, struct vm_area_struct *prev,unsigned long start, unsigned long end)
{struct vm_area_struct *next = vma_next(mm, prev);struct mmu_gather tlb;lru_add_drain();tlb_gather_mmu(&tlb, mm, start, end);update_hiwater_rss(mm);unmap_vmas(&tlb, vma, start, end);free_pgtables(&tlb, vma, prev ? prev->vm_end : FIRST_USER_ADDRESS,next ? next->vm_start : USER_PGTABLES_CEILING);tlb_finish_mmu(&tlb, start, end);
}

简言之,这是一些列的free和flush操作,同时也会更新水位,将物理内存归还给操作系统。由于篇幅限制,这些函数功能不一一讲解,可以单独作为一篇或者更多篇幅。

3.5. remove_vma_list

__do_munmap中的最后一个函数。在上面的操作中,已经将vma结构从红黑树中擦除了,下面将遍历vma链表,进行vma结构告诉缓存的释放,先看下函数定义:

/** Ok - we have the memory areas we should free on the vma list,* so release them, and do the vma updates.** Called with the mm semaphore held.*/
static void remove_vma_list(struct mm_struct *mm, struct vm_area_struct *vma)
{unsigned long nr_accounted = 0;/* Update high watermark before we lower total_vm */update_hiwater_vm(mm);do {long nrpages = vma_pages(vma);/*  */if (vma->vm_flags & VM_ACCOUNT)nr_accounted += nrpages;vm_stat_account(mm, vma->vm_flags, -nrpages);vma = remove_vma(vma);  /* 释放内存 */} while (vma);vm_unacct_memory(nr_accounted);validate_mm(mm);    /*  */
}

这将遍历整个需要free的vma链表,通过使用remove_vma对slab object进行释放,并返回下一个vma结构。

3.6. remove_vma

/** Close a vm structure and free it, returning the next.*/
static struct vm_area_struct *remove_vma(struct vm_area_struct *vma)    /*  */
{struct vm_area_struct *next = vma->vm_next;might_sleep();if (vma->vm_ops && vma->vm_ops->close)vma->vm_ops->close(vma);if (vma->vm_file)fput(vma->vm_file);mpol_put(vma_policy(vma));vm_area_free(vma);return next;
}

其中的关键函数是vm_area_free,这个函数很简单,

void vm_area_free(struct vm_area_struct *vma)
{kmem_cache_free(vm_area_cachep, vma);
}

至此,关于__do_munmap结束,他在brk系统调用中返回:

 ret = __do_munmap(mm, newbrk, oldbrk-newbrk, &uf, true);    /* do munmap */if (ret < 0) {mm->brk = origbrk;  /* unmap 失败使用原来的brk 位置 */goto out;} else if (ret == 1) {downgraded = true;}goto success;

4. 申请

上面释放的篇幅过长,申请流程单独介绍。

5. 相关链接

  • https://www.cs.unc.edu/~porter/courses/cse506/f12/slides/address-spaces.pdf
  • https://stackoverflow.com/questions/14943990/overlapping-pages-with-mmap-map-fixed

Linux内存管理 brk(),mmap()系统调用源码分析2:brk()的内存释放流程相关推荐

  1. 【Linux 内核 内存管理】mmap 系统调用源码分析 ④ ( do_mmap 函数执行流程 | do_mmap 函数源码 )

    文章目录 一.do_mmap 函数执行流程 二.do_mmap 函数源码 调用 mmap 系统调用 , 先检查 " 偏移 " 是否是 " 内存页大小 " 的 & ...

  2. Linux brk(),mmap()系统调用源码分析3:brk()的内存申请流程

    Linux brk(),mmap()系统调用源码分析 brk()的内存申请流程 荣涛 2021年4月30日 内核版本:linux-5.10.13 注释版代码:https://github.com/Rt ...

  3. Linux内存管理 brk(),mmap()系统调用源码分析1:基础部分

    Linux内存管理 brk(),mmap(),munmap()系统调用源码分析 基础部分 荣涛 2021年4月30日 内核版本:linux-5.10.13 注释版代码:https://github.c ...

  4. 【Linux 内核】进程管理 ( 进程相关系统调用源码分析 | fork() 源码 | vfork() 源码 | clone() 源码 | _do_fork() 源码 | do_fork() 源码 )

    文章目录 一.fork 系统调用源码 二.vfork 系统调用源码 三.clone 系统调用源码 四._do_fork 函数源码 五.do_fork 函数源码 Linux 进程相关 " 系统 ...

  5. 【C++内存管理】loki::allocator 源码分析

    loki 是书籍 <Modern C++ Design>配套发行的一个 C++ 代码库,里面对模板的使用发挥到了极致,对设计模式进行了代码实现.这里是 loki 库的源码. ps. 有空是 ...

  6. 【SemiDrive源码分析】【X9芯片启动流程】30 - AP1 Android Kernel 启动流程 start_kernel 函数详细分析(一)

    [SemiDrive源码分析][X9芯片启动流程]30 - AP1 Android Kernel 启动流程 start_kernel 函数详细分析(一) 一.Android Kernel 启动流程分析 ...

  7. 掌握鸿蒙轻内核静态内存的使用,从源码分析开始

    摘要:静态内存实质上是一个静态数组,静态内存池内的块大小在初始化时设定,初始化后块大小不可变更.静态内存池由一个控制块和若干相同大小的内存块构成.控制块位于内存池头部,用于内存块管理.内存块的申请和释 ...

  8. (转)Linux设备驱动之HID驱动 源码分析

    //Linux设备驱动之HID驱动 源码分析 http://blog.chinaunix.net/uid-20543183-id-1930836.html HID是Human Interface De ...

  9. 【SemiDrive源码分析】【X9芯片启动流程】21 - MailBox 核间通信机制介绍(代码分析篇)之 Mailbox for Linux 篇

    [SemiDrive源码分析][X9芯片启动流程]21 - MailBox 核间通信机制介绍(代码分析篇)之 Mailbox for Linux 篇 一.Mailbox for Linux 驱动框架分 ...

最新文章

  1. 【C++】C/C++ 中default/delete特性
  2. SAP MM 如何看一个Inbound Delivery单据相关的IDoc?
  3. csdn将文章添加到专栏
  4. 对一次短路故障的分析与总结
  5. 【论文解读】CVPR 2021 妆容迁移 论文+ 代码 汇总,美得很美得很!
  6. ubuntu 创建桌面快捷方式
  7. 数据库性能优化—分库分表
  8. BeetleX服务网关授权配置
  9. NYOJ 2 括号配对问题
  10. 2016.08.30~2017.07.20
  11. mikumikudance
  12. doodoo.js快速入门教程
  13. 孙溟㠭绘画篆刻——《梦》
  14. 设计模式之模板模式和工厂模式
  15. [K8S]error execution phase preflight: couldn‘t validate the identity of the API Server
  16. 用CSS实现HTML图文混排
  17. FPGA之DDR4驱动
  18. Wireless Communication学习笔记-路径损耗,阴影和多径效应
  19. [转载] mig (Multi-Instance GPUs) 多实例GPU 是什么
  20. C# WINFORM窗口 装载控件的工具栏不小心隐藏了

热门文章

  1. 我的JavaWeb学习2
  2. java-不用辅助变量,两变量直接交换
  3. Java-重定向输出流实现程序日志
  4. Mysql 用Not In 的问题:子查询的结果中有Null则查不出来
  5. python读文件指定行的数据
  6. docker学习-docker解决了什么问题
  7. Java学习笔记之log4j与commons-logging转
  8. 各数据库要使用保留字的处理办法
  9. 【转载更新】Linux工具之SED 2.应用实例
  10. Windows进程同步之事件内核对象(Event)