RISC-V 32架构实践专题七(从零开始写操作系统-内存管理)

本文介绍了如何在嵌入式系统中实现一个简单的内存分配器rv_memory_allocator,包括获取堆内存大小、堆栈信息的存储、堆内存的页数量计算、内存分配和释放机制,以及通过测试代码验证其功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

        完成串口初始化后,接下来将实现一个简单的内存分配器以页大小为单位来进行内存的分配管理。我们将此内存分配器命名为rv内存分配器。

一、获取堆内存大小

        堆内存的起始地址、结束地址以及大小是由链接文件决定的。在链接文件中,我们将划分内存空间,将哪一部分作为数据空间、栈空间或堆空间都划分的一清二楚。

1.1 增加只读数据段

        在链接脚本中新增只读数据段专用于存放代码段、数据段与堆栈信息等,并声明页大小为1KB;修改链接文件如下所示:

/* Simple Linker Script */

/* 分配2kB内存作为栈空间使用 */
PROVIDE(_stack_size = 2048);
/* 设置页大小为1KB */
PROVIDE(_page_size = 1024);

··········

/* Sections */
SECTIONS 
{
    ··········

    /* 开辟只读数据段,将此段存放在flash */
    .rodata : 
    {
        . = ALIGN(4);
        /* Initialized data */
        *(.rodata)
        . = ALIGN(4);
    } >FLASH AT>FLASH

    /* Section for uninitialized data */
    .bss : 
    {
        . = ALIGN(4);
        /* 获取bss段在ram中的开始位置 */
        PROVIDE(_sbss = .);
        /* Uninitialized data */
        *(.bss)
        . = ALIGN(4);
        /* 获取bss段在ram中的结束位置 */
        PROVIDE(_ebss = .);
    } >RAM AT>FLASH

    /* 设置堆空间起始地址,且地址必须页大小对齐 */
    . = ALIGN(_page_size);
    PROVIDE(_heap_start = .);

    /* Section for stack */
    /* 
        输出段格式为:SECTION [ADDRESS] [(TYPE)] : [AT(LMA)] 
        其中SECTION为输出文件的段名,后面的都为可选项,其中ADDRESS为输出段的内存运行地址
    */
    /* 
        输入段格式为,举代码段为例:*(.text)
        其中*号表示所有的输入文件,即.o文件;.text表示输入文件的代码段。
     */
    .stack ORIGIN(RAM) + LENGTH(RAM) - _stack_size : 
    {
        /* stack */
        . = ALIGN(_page_size);
        PROVIDE(_heap_end = .);
        PROVIDE(_stack_start = .);
        . += _stack_size;
        PROVIDE(_stack_end = .);
    } >RAM
}

1.2 将堆栈信息存入只读数据段

        然后在代码中获取堆空间的地址信息与栈空间的地址信息,首先编写一个汇编文件,然后将链接脚本中的定位地址信息赋值给汇编文件中定义声明的全局变量。并将这些全局变量都链接到只读数据段中去:

# 声明下面的数据存放在只读代码段中;通过链接文件可知,这些数据将存放在flash中,并只能读取
.section .rodata

.global	DATA_START
DATA_START : .word _vma_data_start

.global	DATA_END
DATA_END : .word _vma_data_end

.global	BSS_START
BSS_START : .word _sbss

.global	BSS_END
BSS_END : .word _ebss

.global	HEAP_START
HEAP_START : .word _heap_start

.global	HEAP_END
HEAP_END : .word _heap_end

.global	STACK_START
STACK_START : .word _stack_start

.global	STACK_END
STACK_END : .word _stack_end

1.3 获取并输出堆栈内存地址信息

        然后再在c代码中使用汇编文件中定义的只读全局变量来获取到各个段的位置信息,并通过串口打印输出相关段信息:

void page_init(void)
{
	printf("DATA:   0x%x -> 0x%x\n\r", DATA_START, DATA_END);
	printf("BSS:    0x%x -> 0x%x\n\r", BSS_START, BSS_END);
	printf("HEAP:   0x%x -> 0x%x\n\r", HEAP_START, HEAP_END);
    printf("STACK:   0x%x -> 0x%x\n\r", STACK_START, STACK_END);
}

void start_kernel(void)
{
	// 设置sysclk系统时钟为96MHz
	clock_hse_96Mhz();
	// 设置usart1,并初始化配置为115200,8,none,1
	usart1_init();

	//usart1_puts("hello RVOS!\n");
	//usart1_puts("\r======== RVOS ========\r\n");
	printf("\r======== hello RVOS ========\n\r");

	page_init();

	led2_ctrl(1);

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

DATA数据段:

        因为在代码中我们目前还没有定义过已经初始化的全局变量与静态变量,所以可以看到数据段的大小为0(因为并没有数据在此数据段中);

BSS段:

        在实现printf()函数时,我们定义过1000字节未初始化的全局静态数组static char out_buf[1000]; // buffer for _vprintf(),所以由于其未初始化,其应该存放在bss段;因此可以看到bss段的大小为0x3e8,也就是1000字节大小;

HEAP堆:

        因为在链接脚本中,bss段之后就紧跟着heap堆空间,且堆空间的起始地址与page页大小(1KB)对齐,所以可以看到堆空间的起始地址是0x2000_0400,而不是0x2000_03e8;

STACK栈:

        栈的起始地址通过链接文件就可以算出,它为64KB - 2KB + ram基地址;并且栈的起始地址就是堆的结束地址。

下图就是通过串口输出的数据段、bss段、堆和栈的相关位置信息:

二、rv内存分配器的实现

2.1 获取堆空间的页数量

        在得到堆内存的起始地址与结束地址后,我们就可以计算出堆内存的大小以及堆内存可以分配出的页数量。同时我们还需要预留一个页面进行对堆内存空间的页面管理。

        需要注意的是,在计算堆空间大小时,堆地址需要先进行对齐操作,如下代码所示:

/*********************************************************************
 * @fn      page_init
 *
 * @brief   初始化page,获取堆栈信息.
 *
 * @param   none
 *
 * @return  none
 */
void page_init(void)
{
	uint32_t heap_size = 0, i = 0;
	page_t *page = 0;

	// 重新设置堆起始地址,使其与PAGE_SIZE对齐
	heap_start = (HEAP_START + (PAGE_SIZE - 1)) & (~(PAGE_SIZE - 1));
	// 重新设置堆结束地址,使其与PAGE_SIZE对齐
	heap_end = (HEAP_END) & (~(PAGE_SIZE - 1));
	// 计数出堆空间大小
	heap_size = heap_end - heap_start;
	// 计数出堆空间能划分多少个页(由于此MCU的ram为64KB,所以最多不超过64个页)
	page_num = heap_size / PAGE_SIZE;
	// 预留1个页面空间,此页面专用于堆空间管理
	page_num -= res_page_num;

	// 清除页面管理存储区
	page = (page_t *)heap_start;
	for(i = 0; i < page_num; i++, page++)
	{
		clear_flag(page);
	}

	printf("DATA:   0x%x -> 0x%x\n\r", DATA_START, DATA_END);
	printf("BSS:    0x%x -> 0x%x\n\r", BSS_START, BSS_END);
	printf("HEAP:   0x%x -> 0x%x\n\r", heap_start, heap_end);
    printf("STACK:   0x%x -> 0x%x\n\r", STACK_START, STACK_END);
}

        在进行堆起始地址对齐时,需要向后对齐;在进行堆结束地址对齐时,需要向前对齐。这样做的原因是因为不能使对齐后的地址超出原本的堆空间范围。

 2.2 内存分配器的实现

        我们将堆内存分为两部分:

        第一部分是预留堆内存,用于存放内存页信息,使用内存页信息结构体来描述一个页内存;

        第二部分是实际提供给用户使用的内存页,以页大小为单位提供给用户。

        每一个内存页信息结构体唯一对应一个页内存空间,它们是一一对应的关系。如下图所示:

         内存页信息结构体如下所示,使用一个flag来表示内存页是否已被分配以及是否是分配空间中的最后一个页:

typedef struct page
{
    uint8_t flag;   // bit.0,用于表示page是否空闲;bit.1,用于表示是否为分配内存的最后一个页面
} page_t;

        页分配器代码的实现如下所示:

/*********************************************************************
 * @fn      alloc_pages
 *
 * @brief   分配内存页.
 *
 * @param   num,需要分配的页空间数量
 *
 * @return  起始页内存地址
 */
void *alloc_pages(uint32_t num)
{
	uint32_t i = 0, j = 0;
	void *page_addr = 0;
	page_t *page = (page_t *)heap_start;

	// 查找符合要求的连续页内存空间
	for(i = 0; i < page_num - num + 1; i++, page++)
	{
		// 判断是否有满足要求的连续内存页空间
		for(j = 0; j < num; j++)
		{
			if(!is_free(page))
			{
				i += j;
				page += j;
				break;
			}
		}

		// 如果j == num时,找到了满足要求的连续页空间
		if(j == num)
		{
			//将找到符合要求的page转换为地址
			page_addr = (void *)page_to_addr(page);
			// 将找到的页内存分配出去
			for(j = 0; j < num - 1; j++, page++)
			{
				set_flag(page, PAGE_ISALLOC);
			}
			// 将此连续空间中的最后一页进行标识
			set_flag(page, PAGE_ISALLOC | PAGE_ISLAST);
			//page_addr = (void *)(heap_start + res_page_num * PAGE_SIZE + i * PAGE_SIZE);

			return page_addr;
		}
	}

	return 0;
}

实现原理:

  1. 从第一个内存页信息结构体开始遍历,找到第一个空闲的内存页;
  2. 开始查询第一个空闲内存页后面是否有num个空闲的连续内存页;
  3. 如果有,则将这几个空闲内存页进行分配,设置对应的内存页信息结构体,并返回第一个空闲内存页的内存地址;
  4. 如果没有,则加上不合格的内存页个数,再从第1步开始循环遍历;

2.3 内存释放器的实现

        释放器的实现则比分配器简单多了,直接将传入的内存地址转换为对应的内存页信息结构体,然后再循环判断对应的内存页是否被分配,如果被分配就释放对应的内存页,直到释放到此段内存的最后一个内存页为止。

        相关代码如下所示:

/*********************************************************************
 * @fn      free_pages
 *
 * @brief   释放内存页.
 *
 * @param   num,需要释放的内存地址空间
 *
 * @return  none
 */
void free_pages(void *addr)
{
	uint32_t num = 0, align_addr = (uint32_t)addr & ~(PAGE_SIZE - 1);
	page_t *page = (page_t *)heap_start;

	// 判断想要释放的地址空间是否合法
	if((!addr) || (align_addr >= heap_end) || (align_addr < (heap_start + PAGE_SIZE)))
		return;

	// 计算出addr对应的哪一个page
	num = (align_addr - heap_start - PAGE_SIZE) / PAGE_SIZE;
	page += num;

	// 如果page已经被分配,则释放它
	while(!is_free(page))
	{
		if(is_last(page))
		{
			clear_flag(page);
			break;
		}else
		{
			clear_flag(page);
			page++;
		}
	}
}

三、分配器验证

        通过编写测试代码来验证分配器是否工作正常,代码如下所示:

void page_test()
{
	void *p = alloc_pages(2);
	printf("p = 0x%x\n\r", p);
	//page_free(p);

	void *p2 = alloc_pages(8);
	printf("p2 = 0x%x\n\r", p2);
	free_pages(p2);

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

        通过上述代码可知,如果分配了2个page内存,但是不释放,则p2的地址应该在p地址的后面2个内存页地址上;并且当p2被释放后,再进行p3的分配时,p3的地址就应该是原来p2的地址。

        测试结果如下所示:

        由上图可知,p地址为0x20000800,因为预留了1个page内存给页信息管理使用,所以p的地址应该是堆内存地址后偏移1个page内存页,也就是0x20000800 = 0x20000400 + 0x400;

        p2则等于0x20000800 + 0x400 * 2 = 0x20001000;

        p3则是因为p2被释放后再分配的,所以地址应该和之前的p2一样为0x20001000。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值