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

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

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



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



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