内核杂谈——vmalloc分配器的进化故事

2023-11-01 14:40

本文主要是介绍内核杂谈——vmalloc分配器的进化故事,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

从懵逼到不懵逼

vmalloc机制在最近的内核中做了大改,很有意思。通过对比的角度来看看最近一次的进化史。对比版本5.0和5.4

patch: commitID 68ad4a3304335358f95a417f2a2b0c909e5119c4

    mm/vmalloc.c: keep track of free blocks for vmap allocation
    
    Patch series "improve vmap allocation", v3.

5.1中提出的,5.2中合入。本文侧重点是空洞的处理,笔者第一次读vmalloc分配器的时候是比较懵逼的,因为早期vmalloc记录的是已分配出去的对象,而空闲块是隐蔽描述的,正常理解的思维像伙伴系统一样,查找空闲对象然后获取,但vmlloac不是。

直到上述patch出现,理解不再晦涩。
   

vmalloc的建立

在vmalloc_init之前使用vm_struct来记录分配出去的vmalloc区域,然后会链接在vmlist全局链表中。早期的vmalloc区域有per cpu使用的(见pcpu_page_first_chunk),还有内核使用的代码段数据段等(见map_kernel)。这些区域比较零散,在vmalloc_init中会将这些区域从vmlist组织到vmap_area红黑树中。讲到树,肯定离不开查找 删除 插入这些让人产生条件反射的想法。

上述过程没啥变化,变化主要在vmalloc_init中,该函数建立了vmalloc机制。在vmalloc_init中引入了一个新的小内存cache——vmap_area_cachep。不用多想,看到kmem_cache也要产生条件反射——vmap_area是一个频繁的操作对象,建立对应的小内存可以加快内存获取的速度。

void __init vmalloc_init(void)
{struct vmap_area *va;struct vm_struct *tmp;int ivmap_area_cachep = KMEM_CACHE(vmap_area, SLAB_PANIC);for (tmp = vmlist; tmp; tmp = tmp->next) {va = kmem_cache_zalloc(vmap_area_cachep, GFP_NOWAIT);va->va_start = (unsigned long)tmp->addr;va->va_end = va->va_start + tmp->size;va->vm = tmp;insert_vmap_area(va, &vmap_area_root, &vmap_area_list);}
}

va的作用就是接管vmlist里的已被使用的vmalloc区域,然后存放到红黑树中。将已用的va使用vmap_area红黑树管理,同时使用链表将这些va按顺序排序,红黑树方便查找区间,链表方便获取下一个对象。 这里使用两种结构,使得查找操作快一点,这个是保留的,改进的是区间的更新及查找。

进化一:空洞实体化

在vmalloc_init中添加了vmap_init_free_space。函数中出现了一个新的对象描述空洞。也就是free va。vmalloc_init之前链入vmlist的vmalloc区域不一定是连续的,如代码下的KVA space描述,B(usy)代表已被分配出去的vmalloc区域,F(ree)代表未被分配出去的vmalloc区域。

5.0代码中没有这个函数,也就是并未将free区域单独拎出来。

static void vmap_init_free_space(void)
{unsigned long vmap_start = 1;const unsigned long vmap_end = ULONG_MAX;struct vmap_area *busy, *free;/**     B     F     B     B     B     F* -|-----|.....|-----|-----|-----|.....|-*  |           The KVA space           |*  |<--------------------------------->|*/
//拎出vmap_area_list中的空洞区域。list_for_each_entry(busy, &vmap_area_list, list) {if (busy->va_start - vmap_start > 0) {free = kmem_cache_zalloc(vmap_area_cachep, GFP_NOWAIT);if (!WARN_ON_ONCE(!free)) {free->va_start = vmap_start;free->va_end = busy->va_start;insert_vmap_area_augment(free, NULL,&free_vmap_area_root,&free_vmap_area_list);}}vmap_start = busy->va_end;}
//拎出剩余的区域,这块区域是最大的。if (vmap_end - vmap_start > 0) {free = kmem_cache_zalloc(vmap_area_cachep, GFP_NOWAIT);if (!WARN_ON_ONCE(!free)) {free->va_start = vmap_start;free->va_end = vmap_end;insert_vmap_area_augment(free, NULL,&free_vmap_area_root,&free_vmap_area_list);}}
}

如上图描述了vmalloc区域,可以分为两块区域,一块支离破碎的,还一块待开发的。如何拎出来未开发的区域呢?看下图,其中的关键点:vmap_start的更新以及差值比较。

名字上是list,其实是一颗红黑树(便于理解画成了链表形式)。vmap_area_list树上挂了很多已被使用的vmalloc区域节点,各个节点间不连续的地方就是hole,然后找出来挂到free_vmap_area_list这颗树上。后面的获取空闲区域就从free树上获取。

进化二:空闲块的更新及查找

5.0:利用已分配区域找空闲区域

先看5.0中的。通过找空洞来获取空间,空洞代表没有使用的区域。

alloc_vmap_area:if (free_vmap_cache) {} else {addr = ALIGN(vstart, align);n = vmap_area_root.rb_node;first = NULL;while (n) {struct vmap_area *tmp;tmp = rb_entry(n, struct vmap_area, rb_node);
//rbtree中寻找tmp->va_start <= addr <= tmp->va_end的区域if (tmp->va_end >= addr) {first = tmp;if (tmp->va_start <= addr)break;n = n->rb_left;} elsen = n->rb_right;}}
//节点使用list成员组成的链表,按地址顺序排列while (addr + size > first->va_start && addr + size <= vend) {addr = ALIGN(first->va_end, align);
//如果找到的节点是最后一个节点,那就不用找了,直接拿后面没有开发的holeif (list_is_last(&first->list, &vmap_area_list))goto found;first = list_next_entry(first, list);}
found:va->va_start = addr;va->va_end = addr + size;va->flags = 0;__insert_vmap_area(va);

第一次找到addr最接近的区间位置

while条件满足说明找到的区域不是hole,更新addr和first位置

进入while判断,发现first前有一块空洞适合获取,退出循环,直接拿前一个first的后面一块区间使用,然后加入红黑树和list链表。

5.4:在空闲区域中查找

看5.4的。核心在free_vmap_area树中查找。

find_vmap_lowest_match:struct vmap_area *va;struct rb_node *node;unsigned long length;//从free树中查找node = free_vmap_area_root.rb_node;length = size + align - 1;while (node) {va = rb_entry(node, struct vmap_area, rb_node);
//首先判断左区间最大空闲区域是否满足申请需求,然后判断地址是否在左节点中if (get_subtree_max_size(node->rb_left) >= length &&vstart < va->va_start) {node = node->rb_left;} else {
//区间坐落在当前节点中if (is_within_this_va(va, size, align, vstart))return va;
//右区间最大空闲区域是否满足申请需求if (get_subtree_max_size(node->rb_right) >= length) {node = node->rb_right;continue;}return NULL;
}
__alloc_vmap_area:va = find_vmap_lowest_match(size, align, vstart); //free va中找到合适areaif (va->va_start > vstart)        //获得待分配的起始地址nva_start_addr = ALIGN(va->va_start, align);elsenva_start_addr = ALIGN(vstart, align);
//根据地址判断在当前free va中所处的匹配类型type = classify_va_fit_type(va, nva_start_addr, size);
//根据匹配类型,更新free vmap_area中节点的空闲区域ret = adjust_va_to_fit_type(va, nva_start_addr, size, type); 

找到合适的地址区间的思想是先判断是否有合适的长度去拿,满足长度后再决定从哪个区间去获取。其中的get_subtree_max_size是由argumemt_tree_propagate_from计算的,记录以某节点为头结点形成的树的最大空闲长度,保存在该节点的subtree_max_size中,这个变量是依据修改在vmap_area中新增的。

核心思想在type和subtree_max_size中。

不变的

想法都是不变的,都是为了提高vmalloc分配器的速度。在5.0之前有一笔patch

patch: commitID    89699605fe7cfd8611900346f61cb6cbf179b10a

mm: vmap area cache

引入了cache减少vmalloc分配器中对红黑树的查找操作。而最新的修改使用了type和subtree_max_size减少对红黑树的操作。可以去看作者描述。

这篇关于内核杂谈——vmalloc分配器的进化故事的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/323688

相关文章

内核启动时减少log的方式

内核引导选项 内核引导选项大体上可以分为两类:一类与设备无关、另一类与设备有关。与设备有关的引导选项多如牛毛,需要你自己阅读内核中的相应驱动程序源码以获取其能够接受的引导选项。比如,如果你想知道可以向 AHA1542 SCSI 驱动程序传递哪些引导选项,那么就查看 drivers/scsi/aha1542.c 文件,一般在前面 100 行注释里就可以找到所接受的引导选项说明。大多数选项是通过"_

认知杂谈52

今天分享 有人说的一段争议性的话 I I 1拓展人脉很重要** 咱们活在这世上啊,得明白一件事儿,知识、逻辑能力和实战经验虽然重要,但确实都不是最关键的。真正关键的是要懂得怎么和那些手里有资源的人打交道。人脉那可真是一笔无形的大财富呢。你想想看,有时候一个有影响力的人帮你一把,那效果可比你累死累活干一年都强得多。 I I 就比如说,你要是认识个行业里的大牛,他可能给你介绍个特别好的工

笔记整理—内核!启动!—kernel部分(2)从汇编阶段到start_kernel

kernel起始与ENTRY(stext),和uboot一样,都是从汇编阶段开始的,因为对于kernel而言,还没进行栈的维护,所以无法使用c语言。_HEAD定义了后面代码属于段名为.head .text的段。         内核起始部分代码被解压代码调用,前面关于uboot的文章中有提到过(eg:zImage)。uboot启动是无条件的,只要代码的位置对,上电就工作,kern

Ubuntu22.04回退系统内核

文章目录 起因回退操作卸载内核禁止内核升级 起因 最近因为系统内核自动升级,导致显卡驱动检测不到,炼丹环境被破坏。无奈只能重装驱动,于是跟着手册操作发现驱动要求的是内核版本是5.15.0-25-generic,而我通过uname -r发现这时候的内核版本是6.8.0-40-generic,看来只能回退了。 我搜索了网上很多的文章,没有一篇文章能够完全解决这个问题,所以在我多次尝

跟我一起玩《linux内核设计的艺术》第1章(四)——from setup.s to head.s,这回一定让main滚出来!(已解封)

看到书上1.3的大标题,以为马上就要见着main了,其实啊,还早着呢,光看setup.s和head.s的代码量就知道,跟bootsect.s没有可比性,真多……这确实需要包括我在内的大家多一些耐心,相信见着main后,大家的信心和干劲会上一个台阶,加油! 既然上篇已经玩转gdb,接下来的讲解肯定是边调试边分析书上的内容,纯理论讲解其实我并不在行。 setup.s: 目标:争取把setup.

编译linux内核出现 arm-eabi-gcc: error: : No such file or directory

external/e2fsprogs/lib/ext2fs/tdb.c:673:29: warning: comparison between : In function 'max2165_set_params': -。。。。。。。。。。。。。。。。。。 。。。。。。。。。。。。。 。。。。。。。。 host asm: libdvm <= dalvik/vm/mterp/out/Inte

linux 内核提权总结(demo+exp分析) -- 任意读写(四)

hijack_modprobe_path篇 本文转自网络文章,内容均为非盈利,版权归原作者所有。 转载此文章仅为个人收藏,分享知识,如有侵权,马上删除。 原文作者:jmpcall 专栏地址:https://zhuanlan.kanxue.com/user-815036.htm     原理同hijack_prctl, 当用户执行错误格式的elf文件时内核调用call_usermod

linux 内核提权总结(demo+exp分析) -- 任意读写(三)

hijack_prctl篇 本文转自网络文章,内容均为非盈利,版权归原作者所有。 转载此文章仅为个人收藏,分享知识,如有侵权,马上删除。 原文作者:jmpcall 专栏地址:https://zhuanlan.kanxue.com/user-815036.htm   prctl函数: 用户态函数,可用于定制进程参数,非常适合和内核进行交互 用户态执行prctl函数后触发prctl系统

linux 内核提权总结(demo+exp分析) -- 任意读写(二)

hijack_vdso篇 本文转自网络文章,内容均为非盈利,版权归原作者所有。 转载此文章仅为个人收藏,分享知识,如有侵权,马上删除。 原文作者:jmpcall 专栏地址:https://zhuanlan.kanxue.com/user-815036.htm     vdso: 内核实现的一个动态库,存在于内核,然后映射到用户态空间,可由用户态直接调用 内核中的vdso如果被修改