开发一个RISC-V上的操作系统(四)—— 内存管理

news2024/9/29 9:38:01

目录

往期文章传送门

一、内存管理简介

二、Linker Script 链接脚本

三、动态分配内存

四、测试


往期文章传送门

开发一个RISC-V上的操作系统(一)—— 环境搭建_riscv开发环境_Patarw_Li的博客-CSDN博客

开发一个RISC-V上的操作系统(二)—— 系统引导程序(Bootloader)_Patarw_Li的博客-CSDN博客

开发一个RISC-V上的操作系统(三)—— 串口驱动程序(UART)_Patarw_Li的博客-CSDN博客

本节的代码在仓库的02_MEM_M目录下,仓库链接:riscv_os: 一个RISC-V上的简易操作系统

本文代码的运行调试会在前面开发的RISC-V处理器上进行,仓库链接:cpu_prj: 一个基于RISC-V指令集的CPU实现

一、内存管理简介

操作系统将一整块内存划分了几个区域,每个区域用来做不同的事情: 

其中:

  • 栈区(stack):存放函数形参和局部变量,由编译器自动分配和释放。
  • 堆区(heap):动态分配区域,由程序员申请后使用(如使用malloc函数),需要手动释放否则会造成内存泄漏。
  • 全局/静态存储区:存放全局变量和静态变量(包括静态全局变量和静态局部变量),初始化后的全局变量和静态局部变量放在.data段,未初始化的放在.bss段。
  • 常量区:存放常量,如一些const修饰的符号,字符串等等,并且常量区的内存是只读的,位于.rodata段。
  • 代码区:存放程序二进制代码,存在于.text段。

而我们所做的操作系统同样也要将系统内存划分为这些区域:

那么要如何管理划分这些区域呢,这就要用到Linker Script连接脚本了,下面介绍一下Linker Script连接脚本的使用方法。

二、Linker Script 链接脚本

官方文档链接:Scripts (LD) 

一般来说,程序从.c到可执行文件会经过三个步骤:编译、汇编、链接。编译步骤会将.c文件编译成.s汇编文件,.s汇编文件再由汇编操作生成.o目标文件,最后再由链接器将所有的.o文件链接起来,生成可执行文件:

而每一个链接操作都是由链接脚本(Linker Script所控制的,按照官方的话来说,链接脚本用来描述 input file 中的每个section应该如何被映射到 output file 中,并且控制 output file中的内存布局。我们可以自己编写链接脚本,也可以使用默认的链接脚本,如果要使用自己编写的链接脚本,则需要在编译时使用 -T 参数来指定。

下面是链接脚本的一些基础语法(更详细的描述建议去看官方文档):

下面看看本项目中的链接脚本是如何编写的,位置在02_MEM_M下的os.ld文件:

OUTPUT_ARCH( "riscv" )

ENTRY( _start )

MEMORY
{
	rom   (wxari) : ORIGIN = 0x00000000, LENGTH = 16K
	ram   (wxa!ri) : ORIGIN = 0x00004000, LENGTH = 8K
}

SECTIONS
{
    .text : {
        PROVIDE(_text_start = .);
        *(.text .text.*)
        PROVIDE(_text_end = .);
		. = ALIGN(4);
    } >rom

	.rodata : {
		PROVIDE(_rodata_start = .);
		*(.rodata .rodata.*)
		PROVIDE(_rodata_end = .);
		. = ALIGN(4);
	} >rom

	.data : {
		PROVIDE(_data_start = .);
        *(.sdata .sdata.*)
        *(.data .data.*)
		PROVIDE(_data_end = .);
		. = ALIGN(4);
	} >ram

	.bss :{
        PROVIDE(_bss_start = .);
        *(.sbss .sbss.*)
        *(.bss .bss.*)
        *(COMMON)
        PROVIDE(_bss_end = .);
		. = ALIGN(4);
    } >ram
	
	/* stack */
	PROVIDE(_stack_start = _bss_end);
    PROVIDE(_stack_end = _stack_start + 512);

	/* heap */
	PROVIDE(_heap_start = _stack_end);
    PROVIDE(_heap_size = _memory_end - _heap_start);

    PROVIDE(_memory_start = ORIGIN(ram));
    PROVIDE(_memory_end = ORIGIN(ram) + LENGTH(ram));
}

其中:

  • OUTPUT_ARCH( "riscv" ) 指出输出文件所适用的体系架构。
  • ENTRY( _start ) 设置入口点为我们在start.S中定义的_start符号。
  • MEMORY 指定内存各个部分的起始地址和大小,这里和前面设计的RISC-V处理器上的rom和ram大小对应。
  • SECTIONS 指定分区的布局,.text和.rodata段放在rom中,因为都是只读数据;.data、.bss段放在ram中;接下来是栈区,大小为512B,然后是堆区。

添加了各个区块的管理后,在启动文件start.S里面的内容也需要作出一些更新:

#include "inc/platform.h"
	
    .global _start

	.text
_start:
	    # Set all bytes in the BSS section to zero.
        la      a0, _bss_start
        la      a1, _bss_end
        bgeu    a0, a1, 2f
1:
        sw      zero, (a0)
        addi    a0, a0, 4
        bltu    a0, a1, 1b
2:
    	la      sp, _stack_end    # set the initial stack pointer
					 
    	j   	start_kernel      # jump to kernel

其中 1: 下方的代码是用来初始化.bss段的数据,因为.bss段都是未经过初始化的符号,所以我们需要将.bss段内容清0。

有了链接脚本,我们开发的操作系统程序在编译链接后的各个分区都会被分配到指定的位置上,极大地方便了之后的开发。

三、动态分配内存

既然我们已经完成了静态内存的分配,接下来我们要实现内存的动态分配,也就是如何去动态地分配堆区的空间,下面会基于page来实现动态内存分配,和C语言里面的malloc和free函数一样,这里也实现了page_alloc和page_free函数,用来实现page的分配和释放:

并且采用数组的方式来管理页内存,前面红蓝区域为页索引,用来标明对应页是否已经被分配、是否是最后一页,_alloc_start为页内存开始分配的起始地址:

页索引的数据结构定义如下(在page.c中),使用flags的两个位作为标志,一个是页是否被使用标志PAGE_TAKEN,一个是是否为最后一页标志PAGE_LAST:

下面是page.c的全部代码:

#include "inc/os.h"

/*
 * Following global vars are defined in mem.S
 */
extern uint32_t TEXT_START;
extern uint32_t TEXT_END;
extern uint32_t RODATA_START;
extern uint32_t RODATA_END;
extern uint32_t DATA_START;
extern uint32_t DATA_END;
extern uint32_t BSS_START;
extern uint32_t BSS_END;
extern uint32_t HEAP_START;
extern uint32_t HEAP_SIZE;

/*
 * _alloc_start points to the actual start address of heap pool
 * _alloc_end points to the actual end address of heap pool
 * _num_pages holds the actual max number of pages we can allocate.
 */
static uint32_t _alloc_start = 0;
static uint32_t _alloc_end = 0;
static uint32_t _num_pages = 0;

#define PAGE_SIZE 128
#define PAGE_ORDER 7

#define PAGE_TAKEN (uint8_t)(1 << 0)
#define PAGE_LAST  (uint8_t)(1 << 1)

/*
 * Page Descriptor
 * flags:
 * - bit 0: flag if this page is taken(allocated)
 * - bit 1: flag if this page is the last page of the memory block allocated
 */
typedef struct Page {
	uint8_t flags;
} Page;

static inline void _clear(Page *page)
{
	page->flags = 0;
}

static inline uint8_t _is_free(Page *page)
{
	if (page->flags & PAGE_TAKEN) {
		return 0;
	} else {
		return 1;
	}
}

static inline void _set_flag(Page *page, uint8_t flags)
{
	page->flags |= flags;
}

static inline uint8_t _is_last(Page *page)
{
	if (page->flags & PAGE_LAST) {
		return 1;
	} else {
		return 0;
	}
}

/*
 * align the address to the border of page(128)
 */
static inline uint32_t _align_page(uint32_t address)
{
	uint32_t order = (1 << PAGE_ORDER) - 1;
	return (address + order) & (~order);
}

void page_init()
{	
	/*
	 * one page for page struct, max can index 128 page
	 */
	_num_pages = (HEAP_SIZE / PAGE_SIZE) - 1;
	printf("HEAP_START=%x, HEAP_SIZE=%x, num of pages = %d\n", HEAP_START, HEAP_SIZE, _num_pages);

	Page *page = (Page *)HEAP_START;
	for (int i = 0; i < _num_pages; i++) {
		_clear(page);
		page++;
	}

	_alloc_start = _align_page(HEAP_START + 1 * PAGE_SIZE);
	_alloc_end = _alloc_start + (_num_pages * PAGE_SIZE);

	printf("TEXT:   0x%x -> 0x%x\n", TEXT_START, TEXT_END);
    printf("RODATA: 0x%x -> 0x%x\n", RODATA_START, RODATA_END);
    printf("DATA:   0x%x -> 0x%x\n", DATA_START, DATA_END);
    printf("BSS:    0x%x -> 0x%x\n", BSS_START, BSS_END);
    printf("HEAP:   0x%x -> 0x%x\n", _alloc_start, _alloc_end);
}

/*
 * Allocate a memory block which is composed of contiguous physical pages
 * - npages: the number of PAGE_SIZE pages to allocate
 */
void *page_alloc(int npages)
{
	/* Note we are searching the page descriptor bitmaps. */
	uint8_t found = 0;
	Page *page_i = (Page *)HEAP_START;
	for (int i = 0; i < (_num_pages - npages); i++) {
		if (_is_free(page_i)) {
			found = 1;
			/* 
             * meet a free page, continue to check if following
             * (npages - 1) pages are also unallocated.
             */
			Page *page_j = page_i + 1;
			for (int j = i + 1; j < (i + npages); j++) {
				if (!_is_free(page_j)) {
					found = 0;
					break;
				}
				page_j++;
			}
			/*
			 * found equal 1 means get a memory block which is enough for us,
			 * then return the address of the first page of this memory block.
			 */
			if (found) {
				Page *page_k = page_i;
				for (int k = i; k < (i + npages); k++) {
					_set_flag(page_k, PAGE_TAKEN);
					page_k++;
				}
				_set_flag(--page_k, PAGE_LAST);
				return (void *)(_alloc_start + i * PAGE_SIZE);
			}
		}
		page_i++;
	}
	return NULL;
}

/*
 * Free the memory block
 * - p: start address of the memory block
 */
void page_free(void *p) 
{
	/*
     * Assert (TBD) if p is invalid
     */
	if (!p || (uint32_t)p >= _alloc_end) {
		return;
	}
	/* get the first page descriptor of this memory block */
	Page *page = (Page *)HEAP_START;
	page += ((uint32_t)p - _alloc_start) / PAGE_SIZE;
	/* loop and clear all the page descriptors of the memory block */
	while (!_is_free(page)) {
		if (_is_last(page)) {
			_clear(page);
			break;
		} else {
			_clear(page);
			page++;
		}
	}
}

四、测试

还是和之前一样,我们在kernal.c文件里面调用页分配和释放函数来测试:

void start_kernel(void){

        printf("Hello World!\n");
        page_init();

        void *p = page_alloc(2);
        printf("p = 0x%x\n", p);
        //page_free(p);

        void *p2 = page_alloc(7);
        printf("p2 = 0x%x\n", p2);
        page_free(p2);

        void *p3 = page_alloc(4);
        printf("p3 = 0x%x\n", p3);

        while(1){}; // stop here!
}

打印的信息会通过串口工具来接收,所以要准备好串口调试工具,在RISC-V处理器仓库的serial_utils目录下:

怎么编译和烧录操作系统程序可以参考我的这篇文章:开发一个RISC-V上的操作系统(一)—— 环境搭建_riscv开发环境_Patarw_Li的博客-CSDN博客

烧录完成后,打开串口调试工具,开启串口,然后按下复位键即可看到如下现象:

可以看到每个段的地址起始都被打印出来了,并且最下面打印了start_kernal函数里面的测试结果,结果和理想的结果一样,至此,RISC-V上的操作系统的内存管理部分结束!

遇到问题欢迎加群 892873718 交流~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/808316.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

springboot创建并配置环境(三) - 配置扩展属性(上集)

文章目录 一、介绍二、配置文件application.yml 一、介绍 在上一篇文章&#xff1a;springboot创建并配置环境(二) - 配置基础环境中&#xff0c;我们介绍了springboot如何配置基础环境变量。本篇文章讨论如何处理配置文件。即来自不同位置的配置属性&#xff0c;如&#xff1…

chatglm2外挂知识库问答的简单实现

一、背景 大语言模型应用未来一定是开发热点&#xff0c;现在一个比较成功的应用是外挂知识库。相比chatgpt这个知识库比较庞大&#xff0c;效果比较好的接口。外挂知识库大模型的方式可以在不损失太多效果的条件下获得数据安全。 二、原理 现在比较流行的一个方案是langcha…

OpenLayers入门,OpenLayers使用瓦片加载事件实现瓦片加载进度条,进度条根据瓦片加载数量自动更新进度,加载完毕后隐藏进度条

专栏目录: OpenLayers入门教程汇总目录 前言 本章主要讲解OpenLayers如何使用瓦片加载事件(tileloadstart)、瓦片加载完成事件(tileloadend)以及瓦片加载错误事件(tileloadend)。 并通过OpenLayers使用瓦片加载事件通过实现瓦片加载进度条的案例,实现进度条根据瓦片加…

vue3 vant上传图片

在 Vue 3 中使用 Vant 组件库进行图片上传&#xff0c;您可以使用 Vant 的 ImageUploader 组件。ImageUploader 是 Vant 提供的图片上传组件&#xff0c;可以方便地实现图片上传功能。 以下是一个简单的示例&#xff0c;演示如何在 Vue 3 中使用 Vant 的 ImageUploader 组件进行…

解决Font family [‘sans-serif’] not found问题

序言 以下测试环境都是在 anaconda3 虚拟环境下执行。 激活虚拟环境 conda activate test_python_env 或 source activate test_python_env工具&#xff1a; WinSCP Visual Studio Code 这里笔者使用 WinSCP 工具连接&#xff0c;编辑工具是 Visual Studio Code 一、字体…

【Python数据分析】Python基本数据类型

&#x1f389;欢迎来到Python专栏~Python基本数据类型 ☆* o(≧▽≦)o *☆嗨~我是小夏与酒&#x1f379; ✨博客主页&#xff1a;小夏与酒的博客 &#x1f388;该系列文章专栏&#xff1a;Python学习专栏 文章作者技术和水平有限&#xff0c;如果文中出现错误&#xff0c;希望…

C\C++内存管理

目录 1.C/C内存分布2.C语言中动态内存管理方式3.C中动态内存管理3.1new/delete内置类型3.2new和delete操作自定义类型 4.operator new与operator delete函数4.2重载operator new与operator delete&#xff08;了解&#xff09; 5.new和delete的实现原理5.1内置类型5.2 自定义类…

Vue 3:玩一下web前端技术(六)

前言 本章内容为VUE请求后端技术与相关技术讨论。 上一篇文章地址&#xff1a; Vue 3&#xff1a;玩一下web前端技术&#xff08;五&#xff09;_Lion King的博客-CSDN博客 下一篇文章地址&#xff1a; &#xff08;暂无&#xff09; 一、请求后端技术 1、使用Mock.js模…

【业务功能篇60】Springboot + Spring Security 权限管理 【终篇】

4.4.7 权限校验扩展 4.4.7.1 PreAuthorize注解中的其他方法 hasAuthority&#xff1a;检查调用者是否具有指定的权限&#xff1b; RequestMapping("/hello")PreAuthorize("hasAuthority(system:user:list)")public String hello(){return "hello Sp…

基于BSV的高性能并行CRC硬件电路生成器

01、引 言 循环冗余校验码&#xff0c;即Cyclic Redundancy Check (CRC), 是一种在各种通信系统中广泛应用的检错机制。CRC算法的工作原理和哈希函数类似&#xff0c;具体来说&#xff0c;其对任意长度的数据计算出一段唯一的标识&#xff08;校验和&#xff09;, 然后根据这个…

#typescript 使用file-saver模块#

场景&#xff1a;前端使用file-saver模块做导出文档的时候&#xff0c;出现两个错误 1&#xff1a;npm run build 提示找不到模块&#xff0c;如图 解决方法&#xff1a; 先卸载&#xff0c;不管是否安装都先要卸载 ,然后安装&#xff1a; npm uninstall file-saver npm…

AD21原理图的高级应用(二)层次原理图设计

&#xff08;二&#xff09;层次原理图设计 1.层次原理图概述2.层次化原理图的应用2.1 自上而下的层次化原理图2.2 自下而上的层次化原理图 3.生成层次设计表 对于大规模的电路系统,需要将其按功能分解为若干个电路模块,用户可以单独绘制好各个功能模块,再将它们组合起来继续处…

DevOps-Jenkins

Jenkins Jenkins是一个可扩展的持续集成引擎&#xff0c;是一个开源软件项目&#xff0c;旨在提供一个开放易用的软件平台&#xff0c;使软件的持续集成变成可能。 官网 应用场景 场景一 研发人员上传开发好的代码到github代码仓库需要将代码下载nginx服务器部署手动下载再…

数据结构:快速的Redis有哪些慢操作?

redis 为什么要这莫快&#xff1f;一个就是他是基于内存的&#xff0c;另外一个就是他是他的数据结构 说到这儿&#xff0c;你肯定会说&#xff1a;“这个我知道&#xff0c;不就是 String&#xff08;字符串&#xff09;、List&#xff08;列表&#xff09;、 Hash&#xff08…

【雕爷学编程】MicroPython动手做(13)——掌控板之RGB三色灯2

知识点&#xff1a;什么是掌控板&#xff1f; 掌控板是一块普及STEAM创客教育、人工智能教育、机器人编程教育的开源智能硬件。它集成ESP-32高性能双核芯片&#xff0c;支持WiFi和蓝牙双模通信&#xff0c;可作为物联网节点&#xff0c;实现物联网应用。同时掌控板上集成了OLED…

Spring使用注解进行对象装配(DI)

文章目录 一. 什么是对象装配二. 三种注入方式1. 属性注入2. 构造方法注入3. Setter注入 三. 三种注入方式的优缺点四. 综合练习 通过五大类注解可以更便捷的将对象存储到 Spring 中&#xff0c;同样也可以使用注解将已经储存的对象取出来&#xff0c;直接赋值到注解所在类的一…

守护进程——后台服务进程

文章目录 什么是终端进程组会话关系相关函数守护进程创建步骤应用 什么是终端 echo $$:可以查看当前进程的进程号 进程组 会话》进程组》首进程 会话 关系 >&#xff1a;重定向 |&#xff1a;管道 wc -l&#xff1a;查找 &&#xff1a;在后台去运行 SID&#xff1a;会…

小学期笔记——天天酷跑3

画笔的载体是图层 图层的载体是窗体 效果&#xff1a; ------------------- 效果&#xff1a; ---------------------- 实现一个接口可以理解成添加一个能力 接口可以理解为能力的集合 对于abstract&#xff08;判断&#xff1a;没有方法体&#xff09;&#xff0c;尽量使用…

linux系统上安装kail

1.虚拟机安装 加入kail镜像 kail系统的安装 2.更新kail的源 注释原本的源&#xff0c;加入阿里云的源 #阿里云 #deb http://mirrors.aliyun.com/kali kali-rolling main non-free contrib #deb-src http://mirrors.aliyun.com/kali kali-rolling main non-free contrib 参考&…

【计算机网络】11、网桥(bridge)、集线器(hub)、交换机(switch)、路由器(router)、网关(gateway)

文章目录 一、网桥&#xff08;bridge)二、集线器&#xff08;hub&#xff09;三、交换机&#xff08;switch)四、路由器&#xff08;router&#xff09;五、网关&#xff08;gateway&#xff09; 对于hub&#xff0c;一个包过来后&#xff0c;直接将包转发到其他口。 对于桥&…