Linux内存管理 brk(),mmap()系统调用源码分析2:brk()的内存释放流程
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()的内存释放流程相关推荐
- 【Linux 内核 内存管理】mmap 系统调用源码分析 ④ ( do_mmap 函数执行流程 | do_mmap 函数源码 )
文章目录 一.do_mmap 函数执行流程 二.do_mmap 函数源码 调用 mmap 系统调用 , 先检查 " 偏移 " 是否是 " 内存页大小 " 的 & ...
- Linux brk(),mmap()系统调用源码分析3:brk()的内存申请流程
Linux brk(),mmap()系统调用源码分析 brk()的内存申请流程 荣涛 2021年4月30日 内核版本:linux-5.10.13 注释版代码:https://github.com/Rt ...
- Linux内存管理 brk(),mmap()系统调用源码分析1:基础部分
Linux内存管理 brk(),mmap(),munmap()系统调用源码分析 基础部分 荣涛 2021年4月30日 内核版本:linux-5.10.13 注释版代码:https://github.c ...
- 【Linux 内核】进程管理 ( 进程相关系统调用源码分析 | fork() 源码 | vfork() 源码 | clone() 源码 | _do_fork() 源码 | do_fork() 源码 )
文章目录 一.fork 系统调用源码 二.vfork 系统调用源码 三.clone 系统调用源码 四._do_fork 函数源码 五.do_fork 函数源码 Linux 进程相关 " 系统 ...
- 【C++内存管理】loki::allocator 源码分析
loki 是书籍 <Modern C++ Design>配套发行的一个 C++ 代码库,里面对模板的使用发挥到了极致,对设计模式进行了代码实现.这里是 loki 库的源码. ps. 有空是 ...
- 【SemiDrive源码分析】【X9芯片启动流程】30 - AP1 Android Kernel 启动流程 start_kernel 函数详细分析(一)
[SemiDrive源码分析][X9芯片启动流程]30 - AP1 Android Kernel 启动流程 start_kernel 函数详细分析(一) 一.Android Kernel 启动流程分析 ...
- 掌握鸿蒙轻内核静态内存的使用,从源码分析开始
摘要:静态内存实质上是一个静态数组,静态内存池内的块大小在初始化时设定,初始化后块大小不可变更.静态内存池由一个控制块和若干相同大小的内存块构成.控制块位于内存池头部,用于内存块管理.内存块的申请和释 ...
- (转)Linux设备驱动之HID驱动 源码分析
//Linux设备驱动之HID驱动 源码分析 http://blog.chinaunix.net/uid-20543183-id-1930836.html HID是Human Interface De ...
- 【SemiDrive源码分析】【X9芯片启动流程】21 - MailBox 核间通信机制介绍(代码分析篇)之 Mailbox for Linux 篇
[SemiDrive源码分析][X9芯片启动流程]21 - MailBox 核间通信机制介绍(代码分析篇)之 Mailbox for Linux 篇 一.Mailbox for Linux 驱动框架分 ...
最新文章
- 【C++】C/C++ 中default/delete特性
- SAP MM 如何看一个Inbound Delivery单据相关的IDoc?
- csdn将文章添加到专栏
- 对一次短路故障的分析与总结
- 【论文解读】CVPR 2021 妆容迁移 论文+ 代码 汇总,美得很美得很!
- ubuntu 创建桌面快捷方式
- 数据库性能优化—分库分表
- BeetleX服务网关授权配置
- NYOJ 2 括号配对问题
- 2016.08.30~2017.07.20
- mikumikudance
- doodoo.js快速入门教程
- 孙溟㠭绘画篆刻——《梦》
- 设计模式之模板模式和工厂模式
- [K8S]error execution phase preflight: couldn‘t validate the identity of the API Server
- 用CSS实现HTML图文混排
- FPGA之DDR4驱动
- Wireless Communication学习笔记-路径损耗,阴影和多径效应
- [转载] mig (Multi-Instance GPUs) 多实例GPU 是什么
- C# WINFORM窗口 装载控件的工具栏不小心隐藏了