Kernel Exploring
  • 前言
  • 支持
  • 老司机带你探索内核编译系统
    • 编译出你的第一个内核
    • 内核编译中的小目标
    • 可能是kbuild中最直接的小目标 – help
    • 使用了一个kbuild函数的目标 – cscope
    • 内核中单个.o文件的编译过程
    • 根目录vmlinux的编译过程
    • 启动镜像bzImage的前世今生
    • setup.bin的诞生记
    • 真假vmlinux–由vmlinux.bin揭开的秘密
    • bzImage的全貌
    • kbuild系统浅析
  • 启动时的小秘密
    • INIT_CALLS的秘密
    • 内核参数
  • 内核加载全流程
    • bootloader如何加载bzImage
    • 内核压缩与解压
    • 内核加载的几个阶段
    • 保护模式内核代码赏析
  • 内存管理
    • 内核页表成长记
      • 未解压时的内核页表
      • 内核早期的页表
      • cleanup_highmap之后的页表
      • 映射完整物理地址
      • 启用init_level4_pgt
    • 自底而上话内存
      • e820从硬件获取内存分布
      • 原始内存分配器--memblock
      • 页分配器
        • 寻找页结构体的位置
        • 眼花的页结构体
        • Node-Zone-Page
        • 传说的伙伴系统
        • Compound Page
        • GFP的功效
        • 页分配器的用户们
      • slub分配器
        • slub的理念
        • 图解slub
      • 内存管理的不同粒度
      • 挑战和进化
        • 扩展性的设计和实现
        • 减少竞争 per_cpu_pageset
        • 海量内存
        • 延迟初始化
        • 内存热插拔
        • 连续内存分配器
    • 虚拟内存空间
      • 页表和缺页中断
      • 虚拟地址空间的管家--vma
      • 匿名反向映射的前世今生
      • 图解匿名反向映射
      • THP和mapcount之间的恩恩怨怨
      • 透明大页的玄机
      • NUMA策略
      • numa balance
      • 老版vma
    • 内存的回收再利用
      • 水线
      • Big Picture
      • 手动触发回收
      • Page Fram Reclaim Algorithm
      • swapfile原理使用和演进
    • 内存隔离
      • memcg初始化
      • 限制memcg大小
      • 对memcg记账
    • 通用
      • 常用全局变量
      • 常用转换
    • 测试
      • 功能测试
      • 性能测试
  • 中断和异常
    • 从IDT开始
    • 中断?异常?有什么区别
    • 系统调用的实现
    • 异常向量表的设置
    • 中断向量和中断函数
    • APIC
    • 时钟中断
    • 软中断
    • 中断、软中断、抢占和多处理器
  • 设备模型
    • 总线
    • 驱动
    • 设备
    • 绑定
  • nvdimm初探
    • 使用手册
    • 上帝视角
    • nvdimm_bus
    • nvdimm
    • nd_region
    • nd_namespace_X
    • nd_dax
      • dev_dax
  • KVM
    • 内存虚拟化
      • Qemu内存模型
      • KVM内存管理
  • cgroup
    • 使用cgroup控制进程cpu和内存
    • cgroup文件系统
    • cgroup层次结构
    • cgroup和进程的关联
    • cgroup数据统计
  • 同步机制
    • 内存屏障
    • RCU
  • Trace/Profie/Debug
    • ftrace的使用
    • 探秘ftrace
    • 内核热补丁的黑科技
    • eBPF初探
    • TraceEvent
    • Drgn
  • 内核中的数据结构
    • 双链表
    • 优先级队列
    • 哈希表
    • xarray
    • B树
    • Maple Tree
    • Interval Tree
  • Tools
  • Good To Read
    • 内核自带文档
    • 内存相关
    • 下载社区邮件
Powered by GitBook
On this page
  • 从设计理念开始
  • 分类管理
  • 预取

Was this helpful?

  1. 内存管理
  2. 自底而上话内存
  3. slub分配器

slub的理念

slub分配器对一个内核开发者来讲是既熟悉又陌生的。

熟悉是因为在开发的过程中大家总会使用到它,什么kmem_cache_alloc(), kmalloc()都是slub分配器的接口。而陌生是因为大部分开发者都不了解slub分配器的工作机制。像页分配器大家至少还听说过伙伴系统,而slub分配器好像真的一点绯闻都没有。

那今天我就尝试用我这粗陋的认知给大家揭开一点点盖头来。

从设计理念开始

假装自己懂得很多的样子,给大家讲讲我对内存模块的理解。

内存模块就是一个大管家,管理着系统中内存的使用。有人要用内存了,它分配一点。有人释放内存了,它小心翼翼收好。

在整个模块中,我们能看到其运用了三个设计思想。

第一个思想就是分层。通过之前的文章分析我们也能看到在x86架构上分成了:e820, memblock, page, slub这么几层。分层设计的思想在计算机业十分普遍,其优势也自然用不着我说。

第二个思想我认为是分类管理。

这个思想从页分配器这一层就开始了。为了更好的管理内存,在页分配器这一层,页就依照NUMA和ZONE的属性分类,在不同需求时分配不同的页。而且页还依照他连续空闲的大小分类,这样对不同大小的内存请求也可以快速找到空闲页。slub中也是这种思想,

其做法就是事先分配好指定大小的内存板,当有内存请求时,直接在指定的内存板中分配和释放。

而这个思想在我们生活中也有用武之地,当遇到比较多的物品时,就可以分类存放方便使用。比如忘了在哪个节目中听说有女明星有上百双鞋放满了一个屋子,那这个时候就可以按照运动鞋,高跟鞋,休闲鞋等分类。否则找鞋所花费的检索时间还不如直接网购一双来的快。

第三个思想是预取,也就是事先准备好一些资源而不是每次等有请求时再去分配。

这个思想也很常见,比如说cpu上的cache。当每次读取一段数据的时候,把其后的一段内容也加载进cache。根据概率统计,在读取一部分内容后有很大概率会访问之后的部分内容,所以这样的设计可以提升性能。(PS:这也导致了各种性能优化的小窍门)

slub分配器也使用了这样的思想。在每一个分类的内存板中,都预先保存了空闲的内存以便于快速响应。

好了,吹牛的部分讲完了,接下来就看看slub是究竟如何做到分类管理和预取的。

分类管理

在slub分配器中,用来做分类管理的就是这个kmem_cache结构体了。

kmem_cache                      
+------------------------------+
|name                          |
|    (char *)                  |
+------------------------------+
|object_size                   |   = original object size
|inuse                         |   = ALIGN(object_size, sizeof(void *))
|size                          |   = ALIGN(inuse + padding + debug space, s->align)
|align                         |
|offset                        |
|    (int)                     |
|reserved                      |
+------------------------------+
|oo                            |
|min                           |
|max                           |
|   (kmem_cache_order_objects) |
|   +--------------------------+
|   |order                     |
|   |order_objects             |
+---+--------------------------+

说到分类,我们总是按照某些属性分的,比如说鞋子的用途。那在slub中按照什么属性分类呢?

首先就是按照名字了。所以我们看到在调用kmem_cache_create()的第一个参数就是name,而这个名字就保存在了kmem_cache中的name字段。我们可以通过

cat /proc/slabinfo

来查看系统中slub的分类。比如会有常见的task_struct, inode_cache等。

另一种是按照大小分类。我们可以在上面命令的输出中看到kmalloc-512, kmalloc-256等字样。这就是那些没有特定名字,按照大小来分配时选用的内存板。

其实按照名字分类时隐含了按大小分类的意思,这里单独列出是为了引出slub中对大小的一个计算,也就是按照什么样的标准进行预取。

预取

预取的目的是为了能够提高系统的性能,那它是如何做到这一点的呢?我们来看看生活中的例子。

比如在超市中,我们看到货架上琳琅满目的商品,这都可以算是预取。超市按照估计预先把商品放在货架上,等待顾客的购买。好像没有见过哪个超市是每个商品只摆放一个的吧?如果每种商品货架上只有一个,那么如果有几个人要购买,还不要等很久?

预取首先节省了顾客的平均购买时间。

接下来我们再来看看另一头,服务员的工作。顾客在购买的过程中,服务员也会不断地补充货架。但是从仓库取出商品,到摆放到货架需要一定的时间。如果每当一个商品被拿走就去仓库取出一个商品补充,那是不是太琐碎了呢?如果有哪个超市是这么做的,那么这个超市肯定要倒闭。所以超市中改进的方案是,当货架上的货物基本卖完了后,才去仓库取出一批货物补充货架。

预取其次降低了服务员的工作负担。

道理都是懂的,但是落到操作上就有一个实际问题需要解决--一批是多少?拿少了不能最大化每次取货的劳动,拿多了货架上放不下。那如果是你,你会如何定义“一批”的数量呢?

如果是我的话,我可能会选择补满一货架

生活中如此,内核中也是如此。

内存管理模块是层次化的,slub分配器建立在页分配器上,所以可以牵强的理解为页是slub的货架。

超市中货物上架前需要做好两个计算:

  • 货架的大小

  • 货架上能放多少货物

在slub中同样要计算相应的两个值:

  • 用多大的页来作为货架

  • 每个页中可以放多少object

这两个数据都保存在上图kmem_cache结构体中的oo字段。整个计算过程在calculate_sizes()函数中,图表中其余字段在计算过程中各有用途。

PS:上面说到的页不是单个的物理页,而是内核struct page对应页的概念。

来举两个例子说明一下问题:

  • 假如想要申请的结构体(货物)大小是512字节,那么页(货架)可以选择为4K字节大小,每个页(货架)上就可以存放8个结构体(货物)。

  • 假如想要申请的结构体(货物)大小是2050字节,那么页(货架)可以选择为8K字节大小,每个页(货架)上就可以存放3个结构体(货物)。

当然在实际计算的时候第二种情况的值可能不是这样,因为大家可以看到这么选择其实会有较大的浪费,内核很有可能选择更大的页来减少内存浪费。

好了,希望本文对大家理解slub有一点点的帮助。slub依然博大精深,还有很多非常巧(nue)妙(xin)的设计。有兴趣的童鞋做好心理准备~

Previousslub分配器Next图解slub

Last updated 3 years ago

Was this helpful?