[rCore学习笔记 028] Rust 中的动态内存分配
引言
想起我们之前在学习C的时候,总是提到malloc,总是提起,使用malloc现场申请的内存是属于堆,而直接定义的变量内存属于栈.
还记得当初学习STM32的时候CubeIDE要设置stack 和heap的大小.
但是我们要记得,这么好用的功能,实际上是操作系统在负重前行.
那么为了实现动态内存分配功能,操作系统需要有如下功能:
- 初始时能提供一块大内存空间作为初始的“堆”。在没有分页机制情况下,这块空间是物理内存空间,否则就是虚拟内存空间。
- 提供在堆上分配和释放内存的函数接口。这样函数调用方通过分配内存函数接口得到地址连续的空闲内存块进行读写,也能通过释放内存函数接口回收内存,以备后续的内存分配请求。
- 提供空闲空间管理的连续内存分配算法。相关算法能动态地维护一系列空闲和已分配的内存块,从而有效地管理空闲块。
- (可选)提供建立在堆上的数据结构和操作。有了上述基本的内存分配与释放
动态内存分配
实现方法
应用另外放置了一个大小可以随着应用的运行动态增减的内存空间 – 堆(Heap)。同时,应用还要能够将这个堆管理起来,即支持在运行的时候从里面分配一块空间来存放变量,而在变量的生命周期结束之后,这块空间需要被回收以待后面的使用。如果堆的大小固定,那么这其实就是一个连续内存分配问题,同学们可以使用操作系统课上所介绍到的各种连续内存分配算法。
内存碎片
应用进行多次不同大小的内存分配和释放操作后,会产生内存空间的浪费,即存在无法被应用使用的空闲内存碎片。
内存碎片是指无法被分配和使用的空闲内存空间。可进一步细分为内碎片和外碎片:
- 内碎片:已被分配出去(属于某个在运行的应用)内存区域,占有这些区域的应用并不使用这块区域,操作系统也无法利用这块区域。
- 外碎片:还没被分配出去(不属于任何在运行的应用)内存空闲区域,由于太小而无法分配给提出申请内存空间的应用。
STD库中的动态内存分配
这里首先提到了在STD库中的堆相关的数据结构.可以自行阅读并且大概理解下图.

但是这一部分向我们传达的信息是:
- rust编程可以很优雅地实现动态内存管理
- std库提供了动态内存管理的方法
- 但是我们的操作系统内核只能使用rust的core库来实现,因此需要重视和借用这些std库里的方法
在内核中支持动态内存分配
如上部分所说:
上述与堆相关的智能指针或容器都可以在 Rust 自带的
alloccrate 中找到。当我们使用 Rust 标准库std的时候可以不用关心这个 crate ,因为标准库内已经已经实现了一套堆管理算法,并将alloc的内容包含在std名字空间之下让开发者可以直接使用。然而操作系统内核运行在禁用标准库(即no_std)的裸机平台上,核心库core也并没有动态内存分配的功能,这个时候就要考虑利用alloc库定义的接口来实现基本的动态内存分配器。
具体实现这个动态内存分配器,是为自己实现的这个结构体,实现GlobalAlloc的Trait.
alloc库需要我们提供给它一个全局的动态内存分配器,它会利用该分配器来管理堆空间,从而使得与堆相关的智能指针或容器数据结构可以正常工作。具体而言,我们的动态内存分配器需要实现它提供的GlobalAllocTrait
GlobalAlloc的抽象接口:
// alloc::alloc::GlobalAlloc
pub unsafe fn alloc(&self, layout: Layout) -> *mut u8;
pub unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout);
可以看到,它们类似 C 语言中的
malloc/free,分别代表堆空间的分配和回收,也同样使用一个裸指针(也就是地址)作为分配的返回值和回收的参数。两个接口中都有一个alloc::alloc::Layout类型的参数, 它指出了分配的需求,分为两部分,分别是所需空间的大小size,以及返回地址的对齐要求align。这个对齐要求必须是一个 2 的幂次,单位为字节数,限制返回的地址必须是align的倍数。
具体编程实现
引入已有的内存分配器库
在os/Cargo.toml中引入:
buddy_system_allocator = "0.6"
引入alloc库
在os/src/main.rs中引入.
// os/src/main.rs
extern crate alloc;
实例化全局动态内存分配器
创建os/src/mm/heap_allocator.rs.
// os/src/mm/heap_allocator.rs
use buddy_system_allocator::LockedHeap;
use crate::config::KERNEL_HEAP_SIZE;
#[global_allocator]
static HEAP_ALLOCATOR: LockedHeap = LockedHeap::empty();
static mut HEAP_SPACE: [u8; KERNEL_HEAP_SIZE] = [0; KERNEL_HEAP_SIZE];
pub fn init_heap() {
unsafe {
HEAP_ALLOCATOR
.lock()
.init(HEAP_SPACE.as_ptr() as usize, KERNEL_HEAP_SIZE);
}
}
可以看到实例化了一个静态变量HEAP_ALLOCATOR.并且实例化了一个数组HEAP_SPACE来作为它的堆.
其中.HEAP_SPACE的大小为KERNEL_HEAP_SIZE.
那么这个KERNEL_HEAP_SIZE是取自config这个包的.
这里根据代码仓库里的代码来设置KERNEL_HEAP_SIZE的大小.
// os/src/config.rs
pub const KERNEL_HEAP_SIZE: usize = 0x30_0000;
大小为3145728.
标注全局动态内存分配器的语义项
注意上一段的代码,要标注#[global_allocator]这样这里的内存分配器才能被识别为全局动态内存分配器.
#[global_allocator]
处理动态内存分配失败的情形
需要开启条件编译,所以需要在main.rs里声明:
#![feature(alloc_error_handler)]
这时候就可以在os/src/mm/heap_allocator.rs里创建处理函数了:
// os/src/mm/heap_allocator.rs
#[alloc_error_handler]
pub fn handle_alloc_error(layout: core::alloc::Layout) -> ! {
panic!("Heap allocation error, layout = {:?}", layout);
}
测试实现效果
创建测试函数
在os/src/mm/heap_allocator.rs里创建测试函数.
#[allow(unused)]
pub fn heap_test() {
use alloc::boxed::Box;
use alloc::vec::Vec;
extern "C" {
fn sbss();
fn ebss();
}
let bss_range = sbss as usize..ebss as usize;
let a = Box::new(5);
assert_eq!(*a, 5);
assert!(bss_range.contains(&(a.as_ref() as *const _ as usize)));
drop(a);
let mut v: Vec<usize> = Vec::new();
for i in 0..500 {
v.push(i);
}
for i in 0..500 {
assert_eq!(v[i], i);
}
assert!(bss_range.contains(&(v.as_ptr() as usize)));
drop(v);
println!("heap_test passed!");
}
这里的#[allow(unused)]很有意思,可以阻止编译器对你因为调试暂时不调用的函数报错.
这里注意使用了println,在文件最上边加一句use crate::println.
这里的测试程序先获取了sbss的到ebss的范围.
这里回顾清零bss段的代码:
bss段本身是一个储存未初始化的全局变量的内存区域sbss是bss的开头ebss是bss的结尾
那么bss_range实际上是bss的范围.
根据这里,理解Box::new(5)是尝试在堆上储存a的值,且这个值为5.
那么下边的断言语句:
assert_eq!(*a, 5);
assert!(bss_range.contains(&(a.as_ref() as *const _ as usize)));
就很好理解了:
- 判断
a的值是否为5- 判断
a的指针是否在bss的范围内
随后的操作则是创建了一个Vec容器,然后储存了0..500的值进去,并且分别执行上述对a的断言判断.
如果断言没有报错,那么最后自然会输出heap_test passed!.
(最后注意drop是在堆(动态内存)里释放掉某个变量)
使mm包可调用
在os/src/mm下创建mod.rs使得mm可以被识别为一个包.
为了使用heap_allocator里的init_heap和heap_test,需要公开声明这个mod:
// os/src/mm/mod.rs
pub mod heap_allocator;
编辑main函数,实现测试
// os/src/main.rs
/// the rust entry-point of os
#[no_mangle]
pub fn rust_main() -> ! {
clear_bss();
println!("[kernel] Hello, world!");
logging::init();
println!("[kernel] logging init end");
mm::heap_allocator::init_heap();
println!("[kernel] heap init end");
mm::heap_allocator::heap_test();
println!("heap test passed");
trap::init();
println!("[kernel] trap init end");
loader::load_apps();
trap::enable_timer_interrupt();
timer::set_next_trigger();
task::run_first_task();
panic!("Unreachable in rust_main!");
}
运行测试
cd os
make run
得到运行结果:
[rustsbi] RustSBI version 0.3.1, adapting to RISC-V SBI v1.0.0
.______ __ __ _______.___________. _______..______ __
| _ \ | | | | / | | / || _ \ | |
| |_) | | | | | | (----`---| |----`| (----`| |_) || |
| / | | | | \ \ | | \ \ | _ < | |
| |\ \----.| `--' |.----) | | | .----) | | |_) || |
| _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|
[rustsbi] Implementation : RustSBI-QEMU Version 0.2.0-alpha.2
[rustsbi] Platform Name : riscv-virtio,qemu
[rustsbi] Platform SMP : 1
[rustsbi] Platform Memory : 0x80000000..0x88000000
[rustsbi] Boot HART : 0
[rustsbi] Device Tree Region : 0x87000000..0x87000f02
[rustsbi] Firmware Address : 0x80000000
[rustsbi] Supervisor Address : 0x80200000
[rustsbi] pmp01: 0x00000000..0x80000000 (-wr)
[rustsbi] pmp02: 0x80000000..0x80200000 (---)
[rustsbi] pmp03: 0x80200000..0x88000000 (xwr)
[rustsbi] pmp04: 0x88000000..0x00000000 (-wr)
[kernel] Hello, world!
heap_test passed!
[kernel] IllegalInstruction in application, kernel killed it.
All applications completed!
这里我为了log比较简短,把user里需要编译的app只保留了一个user/src/bin/00hello_world.rs.
这里看log,heap_test passed!,说明测试成功了.
[rCore学习笔记 028] Rust 中的动态内存分配的更多相关文章
- 第10课 C++中的动态内存分配
C++中的动态内存分配 C语言是通过库函数来完成动态内存分配的,而C++是通过关键字从语言层面支持的. C语言中的malloc是基于字节来进行内存申请的,C++中是基于类型来进行的. delete加上 ...
- 《C++ Primer Plus》读书笔记之十—类和动态内存分配
第12章 类和动态内存分配 1.不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存.可以在类声明之外使用单独的语句进行初始化,这是因为静态类成员是单独存储的,而不是对象的 ...
- 【c++ Prime 学习笔记】第12章 动态内存
对象的生存期: 全局对象:程序启动时创建,程序结束时销毁 局部static对象:第一次使用前创建,程序结束时销毁 局部自动对象:定义时创建,离开定义所在程序块时销毁 动态对象:生存期由程序控制,在显式 ...
- JVM学习笔记三:垃圾收集器与内存分配策略
内存回收与分配重点关注的是堆内存和方法区内存(程序计数器占用小,虚拟机栈和本地方法栈随线程有相同的生命周期). 一.判断对象是否存活? 1. 引用计数算法 优势:实现简单,效率高. 致命缺陷:无法解决 ...
- 【JVM学习笔记二】垃圾收集器与内存分配策略
1. 概述 1) GC的历史比Java久远 2) GC需要完成的三件事: | 哪些内存需要回收 | 什么时候回收 | 如何回收 3) Java内存运行时区域各个部分: | Java虚拟机栈.计数器.本 ...
- C++编程学习(八)new&delete动态内存分配
前段时间楼主忙着期末大作业,停更了一段,今天刚好在做机器人课程的大作业时,和同组的小伙伴利用python做了工业机器人的在线编程,突然想起来很久没有阅读大型工程了,马上补上- 接下来的几篇博客主要是博 ...
- c++中的动态内存分配
使用new和delete动态的分配和释放内存 使用new来分配新的内存块,通常情况下,如果成功,new将返回一个指针,指向分配的内存,否则将引发异常,使用new时,需要指定要为那种数据类型分配内存: ...
- C++ 学习笔记(四)类的内存分配及this指针
类,是使用C++的最主要的内容.如果将c++与C语言做比较,我感觉类更像是结构体的加强进化版.在刚接触C++不久的时候总是让类,对象,this指针弄得一脸懵逼,我对类有比较清楚的认识是从理解类在内存中 ...
- 动态内存分配(C++)
C++中的动态内存分配 C++中通过new关键字进行动态内存分配 C++中的动态内存申请是基于类型进行的 delet关键字用于内存释放 //变量申请 Type*pointer = new Type; ...
- 并发编程学习笔记(4)----jdk5中提供的原子类及Lock使用及原理
(1)jdk中原子类的使用: jdk5中提供了很多原子类,它会使变量的操作变成原子性的. 原子性:原子性指的是一个操作是不可中断的,即使是在多个线程一起操作的情况下,一个操作一旦开始,就不会被其他线程 ...
随机推荐
- Python threading实现多线程 基础篇
讲多线程前,先要了解什么是进程,什么是线程,已经知道的请略过. 一.进程与线程: 进程是资源分配的最小单位,一个程序至少有一个进程. 线程是程序执行的最小单位,一个进程至少有一个线程. 进程都有自己独 ...
- 如何对jar包修改并重新发布在本机
本人苦于jieba不能如何识别伊利丹·怒风,召唤者坎西恩这种名字,对jieba-analysis进行了解包和打包 步骤1:找到对应jar 步骤2:在cmd中输入jar -xvf xxx.jar解压包, ...
- 【C】Re08 内存
一.概述 程序运行之后,所有的数据加载到内存上 内存会被操作系统进行分区处理, 划分的区域主要分为4个: [1.代码文本区 text] 存放开发者编写的代码文本,二进制内容形式 [2.静态全局区 St ...
- 【Layui】08 时间线 Timeline
文档地址: https://www.layui.com/demo/timeline.html 常规时间线: <ul class="layui-timeline"> &l ...
- 神州笔记本 —— HASEE神州 —— 用户手册(使用功能键)—— 笔记本电脑功能键
功能键功能: FN+f1 启动/关闭 触摸板 FN+f2 启动/关闭 屏幕背光 FN+f3 启动/关闭 喇叭和外接耳机 FN+f5 减低音量 FN+f6 提高音量 FN+f7 切换屏幕 FN+f8 降 ...
- 国产CPU——兆芯(先开)KX-6640MA 使用感受
上半年买了个兆芯CPU的小mini电脑,一直没有换Windows系统,这两天想着就换了过来,具体配置如下: 1. 使用Python死循环代码烧机,性能和我14年买的i5-4200M的Intel CP ...
- Linux下文件及文件夹权限(学习笔记版)
本文遵循CC 4.0 BY-SA版权协议 注意:本文为学习笔记版,只记录个人觉得重要的部分,内容较为片面. Linux 下文件及文件夹的权限可以表示为rwx这三个字符,r代表read,w代表write ...
- 后端开发学习敏捷需求-->产品价值的定位
产品价值的定位 为什么要写这一系列文章 2023年网上报名学习了,敏捷软件需求的培训课程 ,一直都没有进行回顾,回顾学习,总结 业务分析的能力偏弱,学习和了解关于业务需求相关的方法和理论 每一年都有一 ...
- stm32 F103C8T6 4x4矩阵键盘使用
首先感谢 江科大 的stm32入门课程 受益匪浅.推荐有兴趣的朋友去看看. 先看看我用的矩阵键盘是啥样的(很常见的一种) 接线如图(其他型号根据自己需求接上GPIO口) 代码基于stm大善人的代码修改 ...
- 这就是为什么你学不会DDD
本文书接上回<为了给Javaer落地DDD,我们不得不写开源组件>,欢迎关注公众号(老肖想当外语大佬),获取最新文章更新和DDD框架源码,视频和直播在B站. https://mp.weix ...