[rCore学习笔记 029] 动态内存分配器实现-以buddy_system_allocator源码为例
在上一部分,我们讲了动态内存分配器的原理是维护一个堆,而且是实现各种连续内存分配方法.
但是上一部分是直接通过引用了buddy_system_allocator
来解决的问题.
那么对于内存分配算法有兴趣的我,还是决定看一下源码,总之人是咸鱼但是还是需要有梦想.
人生这么不顺,若是连梦想都没有了,可能当即就找不到活着的意义了吧.
获取buddy_system_allocator的源码
buddy_system_allocator
也是rCore
这个社区的项目.
cd ~/workspace
git clone https://github.com/rcore-os/buddy_system_allocator.git
从实用的角度开始看源码
为了起一个好头,还是从比较熟悉的部分看代码,思考代码是怎么组织的:
buddy_system_allocator
是怎么作为一个外部包被引用的?- 上一部分我们调用了
LockedHeap
,那么这个类是怎么实现的,它依赖于什么?
LockedHeap
我们在源码中搜索LockedHeap
,我们可以在lib.rs
里找到它的实现.
pub struct LockedHeap<const ORDER: usize>(Mutex<Heap<ORDER>>);
在看到这个定义的时候有一种似懂非懂的感觉,只能猜到LockedHeap
是一个加了线程锁的大小为ORDER
的Heap
:
- 因为
ORDER
放在了<>
中间,应该是和泛型有关系,但是这里又明确标注了usize
说明ORDER
是一个变量.- 因为在结构体的实现中出现了
()
有点不知所云
元组结构体
查看Rust圣经,发现确实存在这种字段可以没名称的结构体.
这里又产生了一个新的疑问,如果字段可以没名称,那么怎么去访问结构体内容呢?
查阅Rust语言官方参考手册,可以看到:
Tuple structs are similar to regular structs, but its fields have no names. They are used like tuples, with deconstruction possible via
let TupleStruct(x, y) = foo;
syntax. For accessing individual variables, the same syntax is used as with regular tuples, namelyfoo.0
,foo.1
, etc, starting at zero.
通过数字来访问这些结构体内容.
// 假如存在TupleStruct这个结构体
let foo = TupleStruct(1,2);
// 可以通过这种方法来进行析构
let TupleStruct(x, y) = foo;
// 可以用数字访问
let x = foo.0;
let y = foo.1;
值泛型
那么这里就需要查看参考书目-值泛型的内容尤其是它的示例.
最终得到结论:Rust是允许使用值的泛型的,这代表LockedHeap
有一个和值相关的泛型参数.
在某些时候是很像
C
里边的#define ORDER 0x30000
的.
但是事实上在Rust里是灵活了非常多的.
这和LockedHeap
提供的两种获取示例的方法是相对应的:
impl<const ORDER: usize> LockedHeap<ORDER> {
/// Creates an empty heap
pub const fn new() -> Self {
LockedHeap(Mutex::new(Heap::<ORDER>::new()))
}
/// Creates an empty heap
pub const fn empty() -> Self {
LockedHeap(Mutex::new(Heap::<ORDER>::new()))
}
}
单看这里还看不出来,因为还套了一层Heap
,要看Heap
的获取实例的方法.
加互斥锁
理解了上边的语法,只需要理解GlobalAlloc
这个trait
对于LockedHeap
的实现:
#[cfg(feature = "use_spin")]
unsafe impl<const ORDER: usize> GlobalAlloc for LockedHeap<ORDER> {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
self.0
.lock()
.alloc(layout)
.ok()
.map_or(core::ptr::null_mut(), |allocation| allocation.as_ptr())
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
self.0.lock().dealloc(NonNull::new_unchecked(ptr), layout)
}
}
实际上就是在Heap
外边加了一个Mutex
互斥锁,那么对于alloc
和dealloc
的实现,只需要经过互斥锁访问里边的Heap
,然后访问Heap
的alloc
和dealloc
方法.
Heap
定义
Heap
实际上由一个长度为ORDER
的list
和user
,allocated
和total
几个值组成.
pub struct Heap<const ORDER: usize> {
// buddy system with max order of `ORDER - 1`
free_list: [linked_list::LinkedList; ORDER],
// statistics
user: usize,
allocated: usize,
total: usize,
}
那么ORDER
实际上就是const 值泛型
了.
为什么在代码里不需要指定ORDER的值?
因为我们设置的包的版本为0.6
,这个版本的包没用加入泛型参数,而是固定链表长度为32
.
获取实例
查看Heap
的new
和empty
方法:
impl<const ORDER: usize> Heap<ORDER> {
/// Create an empty heap
pub const fn new() -> Self {
Heap {
free_list: [linked_list::LinkedList::new(); ORDER],
user: 0,
allocated: 0,
total: 0,
}
}
/// Create an empty heap
pub const fn empty() -> Self {
Self::new()
}
}
这里注意list
是一个LinkedList
类型,是一个链表.
设置堆范围
记得上一篇博客内容,我们是使用如下代码初始化的:
/// Initialize heap allocator
pub fn init_heap() {
unsafe {
HEAP_ALLOCATOR
.lock()
.init(HEAP_SPACE.as_ptr() as usize, KERNEL_HEAP_SIZE);
}
}
这里且不说HEAP_ALLOCATOR.lock()
是怎么获取到Heap
实例的.这里这句init
确实是调用的Heap
的init
.
接下来我们看它的实现.
impl<const ORDER: usize> Heap<ORDER> {
... ...
/// Add a range of memory [start, end) to the heap
pub unsafe fn add_to_heap(&mut self, mut start: usize, mut end: usize) {
// avoid unaligned access on some platforms
start = (start + size_of::<usize>() - 1) & (!size_of::<usize>() + 1);
end &= !size_of::<usize>() + 1;
assert!(start <= end);
let mut total = 0;
let mut current_start = start;
while current_start + size_of::<usize>() <= end {
let lowbit = current_start & (!current_start + 1);
let mut size = min(lowbit, prev_power_of_two(end - current_start));
// If the order of size is larger than the max order,
// split it into smaller blocks.
let mut order = size.trailing_zeros() as usize;
if order > ORDER - 1 {
order = ORDER - 1;
size = 1 << order;
}
total += size;
self.free_list[order].push(current_start as *mut usize);
current_start += size;
}
self.total += total;
}
/// Add a range of memory [start, start+size) to the heap
pub unsafe fn init(&mut self, start: usize, size: usize) {
self.add_to_heap(start, start + size);
}
}
init
是调用的add_to_heap
,输入的是堆需要管理内存的初始地址和空间大小.
主要是add_to_heap
中精妙的算法.
地址对齐算法
对于首地址,要保证start
的值是与usize
的大小对齐的.
这里首先要声明,所有的变量大小都是\(2^n\).那么它的二进制实际上是某一位是1其余位都是0的.
start = (start + size_of::<usize>() - 1) & (!size_of::<usize>() + 1);
Rust里!
是按位取反,和C里边!
是逻辑非~
才是按位取反不同.
这里用的公式实际上是$$aligned_addr = (addr + align - 1) & (!align + 1)$$
这里直接举例说明.
本身不对齐的addr
:
\(addr=15,align=2\)
\(addr=b'0\_1111\)
\(addr+align-1=b'1\_0000\)
\(align=b'0\_0010\)
\(!align=b'1\_1101\)
\(!align+1=b'1\_1110\)
\(aligned\_addr=b'1\_0000\)
最终得到的结果aligned_addr
是16
本身已经对齐的addr
:
\(addr=16,align=2\)
\(addr=b'1\_0000\)
\(addr+align-1=b'1\_0001\)
\(align=b'0\_0010\)
\(!align=b'1\_1101\)
\(!align+1=b'1\_1110\)
\(aligned\_addr=b'1\_0000\)
最终得到的结果aligned_addr
是16
设align
为\(2^n\),addr + align - 1
保证了如果低n
位只要不是全0
就都会向n + 1
位进1
,而右边!(align-1)
,减1
后按位取反,再做与运算保证低n
位为0
,这样就完成了对齐,且如果不是对齐的向上取整.
同样地,对于尾地址:
end &= !size_of::<usize>() + 1;
也写成公式表达:$$addr_aligned=addr&(!align+1)$$
这样就很好理解,保证低n
位是0
,这样也是一个对齐的地址,但是向下取整.
这样首地址向上取整,尾地址向下取整,就可以保证操作的地址是原地址的子集,不会出现越界.
#todo
这里可能需要画张图.
最后通过:
assert!(start <= end);
保证地址有效.
地址录入堆的算法
计算地址的对齐要求
根据起始地址计算地址要求是几字节对齐的,就是计算地址的最低有效位.
计算地址最低一位的1
对应的值:
公式:$$lowbit=num&(!num+1)$$
例子:
\(num=10\)
\(num=b'1010\)
\(!num=b'0101\)
\(!num+1=b'0110\)
\(num\&(!num+1)=b'0010\)
\(lowbit=b'0010=2\)
对num
取反,那么最低位的1
变成0
,其余的0
都变成1
,那么!num+1
一定会使得最低位1
变成1
,其余位变回0
,这样在与num
自身求与,最终得到的就是只有最低位1的一个数.
计算剩余空间中能容纳的2的幂的大小
先说计算小于或等于给定数 num
的最大 2 的幂:
pub(crate) fn prev_power_of_two(num: usize) -> usize {
1 << (usize::BITS as usize - num.leading_zeros() as usize - 1)
}
usize::BITS
是usize
的位数,num.leading_zeros()
是最高一位1
之前的0
的个数.
那么求usize::BITS as usize - num.leading_zeros() as usize - 1
就是第一个1
以后的位数.
那么很容易明白最后求出来的就是小于或等于给定数 num
的最大 2 的幂.
计算块大小
比较地址最低一位的1
对应的值和小于或等于地址区间长度的最大2的幂的大小,选择比较小的那一个.
这样理解,
- 正常情况下,最小的块大小应该是符合地址对齐的.
- 但是可能剩下的空间不足以存下这样的块,这时候就按照剩余空间中能容纳的最小\(2^n\)的大小决定块的大小.
判断块大小和最大阶
计算当前阶数,size
后有几个0
就是几阶.
如果阶数大于最大阶,那么就把块分半,降一阶.
GPT:
Buddy System 算法有一个最大阶的概念。最大阶限制了单个块的最大大小。
- 内存碎片管理:通过限制块的大小,可以更好地管理内存碎片。如果块太大,可能会导致内存碎片问题,因为大块可能无法被较小的请求完全利用。
- 性能优化:较小的块更容易管理和分配,可以提高内存分配和释放的效率。
累积当前分好的块的大小
使用total
计算此时使用的块的大小.
将块起始地址根据阶数存储在对应阶数的可用空间列表中
每个可用空间列表的每个元素是一个链表,链表保存当前阶数的起始地址.
也就是同样大小的块的指针存在一个链表中.
self.free_list[order].push(current_start as *mut usize);
移动起始地址指针
移动起始地址指针,使得下一轮继续分配.
current_start += size;
总结
可以看到是先将可分配内存的地址对齐,从start
到end
,尽量把空间分为更大的\(2^n\)的块,而不浪费空间,并且用链表存储起来.
具体怎么回事呢.
这里以最小对齐单元为8=b1000=0x0008
为例.
例子一
比如你的地址是(0x0100,0x0120)
,那么经过对齐之后还是(0x0100,0x0120)
:
这里注意0x0120-0x0100=32
,因此直接分配一个大小为32
的块.
![[Pasted image 20241004223938.png|1200]]
例子二
比如你的地址是(0x0001,0x0021)
,那么经过对齐之后是(0x0008,0x0021)
:
![[Pasted image 20241004231630.png|1200]]
为了物尽其用,每次去对齐最低位.
到了最后,可能剩余的内存不足以满足对齐最低位了,这时候因为我们的地址是对齐过的,因此剩余的内存大小也是满足\(2^n\)的,直接把剩余内存存成一个块.
如果可分配的内存超过可用空间列表存储阶数,那么就分解,一直到能分配的最大块储存.
分配内存
分配内存的代码如下:
pub fn alloc(&mut self, layout: Layout) -> Result<NonNull<u8>, ()> {
let size = max(
layout.size().next_power_of_two(),
max(layout.align(), size_of::<usize>()),
);
let class = size.trailing_zeros() as usize;
for i in class..self.free_list.len() {
// Find the first non-empty size class
if !self.free_list[i].is_empty() {
// Split buffers
for j in (class + 1..i + 1).rev() {
if let Some(block) = self.free_list[j].pop() {
unsafe {
self.free_list[j - 1]
.push((block as usize + (1 << (j - 1))) as *mut usize);
self.free_list[j - 1].push(block);
}
} else {
return Err(());
}
}
let result = NonNull::new(
self.free_list[class]
.pop()
.expect("current block should have free space now")
as *mut u8,
);
if let Some(result) = result {
self.user += layout.size();
self.allocated += size;
return Ok(result);
} else {
return Err(());
}
}
}
Err(())
}
首先,传入的参数layout
是一个结构体或者一个基本数据类型.
- 计算大于这个基本数据类型大小的\(2^n\).
- 计算这个基本数据类型的对齐大小.
- 计算
usize
的大小.
比较这三个大小,选择其中最大的作为size
.
最后取size
的order
阶数为class
,也就是实际上只找比class
大的order
对应链表中的未分配的块.
从最小---也就是最符合size
大小的对应链表找起,如果是非空的就调出来.
此时匹配的order
为i
.
(class + 1..i + 1).rev()
是非常巧妙的设计,从class+1
到i+1
,并且翻转.
每次pop
一个块,并且把这个块分成两个块,计算两个块的首地址,然后存进下一级的块.
一直到符合大小块的class
.
最后只需要把当前class
对应链表的第一个块pop
出来即可,这就是答案.
销毁内存
销毁内存的方法为:
pub fn dealloc(&mut self, ptr: NonNull<u8>, layout: Layout) {
let size = max(
layout.size().next_power_of_two(),
max(layout.align(), size_of::<usize>()),
);
let class = size.trailing_zeros() as usize;
unsafe {
// Put back into free list
self.free_list[class].push(ptr.as_ptr() as *mut usize);
// Merge free buddy lists
let mut current_ptr = ptr.as_ptr() as usize;
let mut current_class = class;
while current_class < self.free_list.len() - 1 {
let buddy = current_ptr ^ (1 << current_class);
let mut flag = false;
for block in self.free_list[current_class].iter_mut() {
if block.value() as usize == buddy {
block.pop();
flag = true;
break;
}
}
// Free buddy found
if flag {
self.free_list[current_class].pop();
current_ptr = min(current_ptr, buddy);
current_class += 1;
self.free_list[current_class].push(current_ptr as *mut usize);
} else {
break;
}
}
}
self.user -= layout.size();
self.allocated -= size;
}
首先,传入的参数ptr
是一个结构体或者一个基本数据类型的指针.
- 计算大于这个基本数据类型大小的\(2^n\).
- 计算这个基本数据类型的对齐大小.
- 计算
usize
的大小.
比较这三个大小,选择其中最大的作为size
.
最后取size
的order
阶数为class
,也就是实际上只找比class
大的order
对应链表中的未分配的块.
把ptr
存入可用空间列表free_list
里边.
但是只是简单地存入,会导致空间越来越碎片化,这样后续申请大的内存块就无法提供.
这里有个非常核心的算法,也就是为啥这个算法叫buddy system
.
let buddy = current_ptr ^ (1 << current_class);
是通过这种方法计算当前内存块的buddy
.
1<<current_class
是求出一个二进制只有一个位是1
的值.
随后current_ptr
与它求异或,那么最后实际上求出的是对current_ptr
在class
那一位的翻转的结果.
假如是current_ptr
是000110100100
:
000110100100
(current_ptr
)000000000100
(掩码)000110100000
(异或结果)
那么,实际上这两个地址是相邻的两个同大小的块.
如果在这个class
对应的链表中找到这个地址开始的块,那么合并这两个块,然后找两个地址较小的,实际上是地址在前半边的,然后存入order
大一级的链表中.
Buddy System内存分配算法
看完代码感觉心里有底了,但是还是乱糟糟的,还是需要系统性地捋清一下算法.
实际上理论部分就是躲不过嘛,不好好搞要吃大亏!
这里通过使用指定filetype的方法找到了很好的资料.
链表
Rust刚接触的时候就听说链表难写,我看了仓库中链表相关的算法确实可以看懂,但是看懂和能够自己写出来是两码事.
要弄清楚三件事:
- 使用了rust的那些特性
- 为什么要用到这些特性
- 为什么要用
unsafe
#TODO
后续可能出一个自写rust链表的练习帖子.
总结
做事不要太工程化,尤其是自学的过程中,要注重基础注重能力的培养,自我培养的过程中要注意基础,要把能跑就行这种思想赶出脑子.
如果自学的时候还是能跑就行,那为什么还要自学呢?又没人给我发工资.
[rCore学习笔记 029] 动态内存分配器实现-以buddy_system_allocator源码为例的更多相关文章
- OpenCV学习笔记(27)KAZE 算法原理与源码分析(一)非线性扩散滤波
http://blog.csdn.net/chenyusiyuan/article/details/8710462 OpenCV学习笔记(27)KAZE 算法原理与源码分析(一)非线性扩散滤波 201 ...
- <c和指针>学习笔记5动态内存分配和预处理器
1 动态内存 比如声明数组得时候,我们需要提前预估数组长度,分配大了浪费,少了就更不好操作了.从而引入动态分配,需要的时候再分配. (1)malloc和free void *malloc(size_t ...
- Hadoop源码学习笔记之NameNode启动场景流程一:源码环境搭建和项目模块及NameNode结构简单介绍
最近在跟着一个大佬学习Hadoop底层源码及架构等知识点,觉得有必要记录下来这个学习过程.想到了这个废弃已久的blog账号,决定重新开始更新. 主要分以下几步来进行源码学习: 一.搭建源码阅读环境二. ...
- 基于Netty的RPC架构学习笔记(五):netty线程模型源码分析(二)
文章目录 小技巧(如何看开源框架的源码) 源码解析 阅读源码技巧 打印查看 通过打断点调试 查看调用栈 小技巧(如何看开源框架的源码) 一断点 二打印 三看调用栈 四搜索 源码解析 //设置nioso ...
- 基于Netty的RPC架构学习笔记(四):netty线程模型源码分析(一)
文章目录 如何提高NIO的工作效率 举个
- Web Service学习笔记:动态调用WebService
原文:Web Service学习笔记:动态调用WebService 多数时候我们通过 "添加 Web 引用..." 创建客户端代理类的方式调用WebService,但在某些情况下我 ...
- iOS学习笔记之ARC内存管理
iOS学习笔记之ARC内存管理 写在前面 ARC(Automatic Reference Counting),自动引用计数,是iOS中采用的一种内存管理方式. 指针变量与对象所有权 指针变量暗含了对其 ...
- MyBatis:学习笔记(4)——动态SQL
MyBatis:学习笔记(4)——动态SQL 如果使用JDBC或者其他框架,很多时候需要你根据需求手动拼装SQL语句,这是一件非常麻烦的事情.MyBatis提供了对SQL语句动态的组装能力,而且他只有 ...
- MyBatis:学习笔记(4)——动态SQL
MyBatis:学习笔记(4)——动态SQL
- [C++学习笔记14]动态创建对象(定义静态方法实现在map查找具体类名对应的创建函数,并返回函数指针,map真是一个万能类)good
[C++学习笔记14]动态创建对象 C#/Java中的反射机制 动态获取类型信息(方法与属性) 动态创建对象 动态调用对象的方法 动态操作对象的属性 前提:需要给每个类添加元数据 动态创建对象 实 ...
随机推荐
- 【Spring-Security】Re10 Oauth2协议 P1 授权码模式 & 密码模式
一.Oauth2协议: 第三方登录,即忘记本站密码,但是登录界面中提供了一些第三方登录,例如微信.支付宝.QQ.等等,通过第三方授权实现登录 第三方认证技术主要解决的时认证标准,各个平台的登录要遵循统 ...
- 阿里提供的免费DNS服务器
阿里提供的免费DNS服务器的介绍网页: https://developer.aliyun.com/mirror/DNS nameserver 223.5.5.5 nameserver 223.6.6. ...
- 如何使用深度学习技术探测代码逻辑死循环 —— 浪潮集团的“公开号CN117271314A”专利
专利公开号: CN117271314A 新闻链接: https://mbd.baidu.com/newspage/data/landingsuper?context={"nid"% ...
- 10-canva绘制数据点
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="U ...
- 推荐5款免费、开箱即用的Vue后台管理系统模板
前言 在现今的软件开发领域,Vue凭借其高效.灵活和易于上手的特性,成为了前端开发的热门选择.对于需要快速搭建企业级后台管理系统的开发者而言,使用现成的Vue后台管理系统模板无疑是一个明智之举.本文大 ...
- zabbix 4.0 监控 mysql
zabbix官方支持监控MySQL,但直接使用默认的模板是不可用的,需要经过额外的设置才可以使用.如果只需要对mysql数据库做简单的监控,zabbix自带的模板完全能够满足要求 下面是用zabbix ...
- 再探se
对象 没有分配内存空间的对象是一个特殊的对象 null null是引用类型的,但是没有指向任何位置,所以是不能被访问的,强制访问会空指针异常 针对具体对象的属性称之为对象属性,成员属性,实例属性 针对 ...
- 淘宝订单信息获取接口,淘宝开放平台R2权限,淘宝开放平台订单获取接口
目前淘宝开放平台是关闭了订单权限申请的,有这方便的需求的人,除非是天猫用户才能申请(天猫用户申请到只能给自己天猫店授权),否则是申请不到这个订单接口了,如果有这方面需要的人可以联系我,站内信留下QQ或 ...
- 【YashanDB知识库】yasdb jdbc驱动集成druid连接池,业务(java)日志中有token IDENTIFIER start异常
问题现象 客户的java日志中有如下异常信息: 问题的风险及影响 对正常的业务流程无影响,但是影响druid的merge sql功能(此功能会将sql语句中的字面量替换为绑定变量,然后将替换以后的sq ...
- 消息队列为什么选用redis?聊聊如何做技术方案选型?
消息队列为什么选用redis?聊聊如何做技术方案选型? 老生常谈,消息队列主要有几大用途: 解耦:下单完成之后,需要订单服务去调用库存服务减库存,调用营销服务加营销数据. 引入消息队列,可以把订单完成 ...