内存管理系统不仅能分配内存,还应该能回收内存,这是最基本的内存管理机制,一直以来我们对内存的管理都是只借不还,本节就是结束这一现状。
本节的主要工作分为四大部分:
一、实现以块为单位(4KB)内存释放的三个基础函数pfree、page_table_pte_remove、vaddr_remove
二、将三个基础函数拼接实现以块为单位(4KB)的内存释放函数mfree_page
三、实现精细化(通过上篇提到的arena机制)的内存释放函数sys_free
四、将上篇的sys_malloc函数以及本篇实现的sys_free函数包装成为系统调用供用户态进程使用。
一、内存释放的基础函数
由于内存释放是内存分配的逆过程,所以我们需要先回忆分配内存的一般步骤:
(1)在虚拟地址池中分配虚拟地址,相关的函数是vaddr_get,此函数操作的是内核虚拟内存池位图kernel_vaddr.vaddr_bitmap或用户虚拟内存池位图pcb->userprog_vaddr.vaddr_bitmap
(2)在物理内存池中分配物理地址,相关的函数是palloc,此函数操作的是内核物理内存池位图kernel_pool->pool_bitmap或用户物理内存池位图user_pool->pool_bitmap
(3)在页表中完成虚拟内存到物理地址的映射,相关的函数是page_table_add
释放内存是于分配内存相反的过程,所以步骤如下:
(1)在物理地址池中释放物理页地址,函数起名pfree,操作的位图同palloc
(2)在页表中去掉虚拟地址的映射,原理是将虚拟地址对应pte(页表)的P位置0,函数起名page_table_pte_remove。
(3)在虚拟地址池中释放虚拟地址,函数起名vaddr_remove,操作的位图同vaddr_get
函数的具体实现如下:
/* 将物理地址pg_phy_addr回收到物理内存池 */
void pfree(uint32_t pg_phy_addr) {
struct pool* mem_pool;
uint32_t bit_idx = 0;
if(pg_phy_addr >= user_pool.phy_addr_start) {
mem_pool = &user_pool;
bit_idx = (pg_phy_addr - user_pool.phy_addr_start) / PG_SIZE;
} else {
mem_pool = &kernel_pool;
bit_idx = (pg_phy_addr - kernel_pool.phy_addr_start) / PG_SIZE;
}
bitmap_set(&mem_pool->pool_bitmap, bit_idx, 0);
}
/* 去掉页表中虚拟地址vaddr的映射,只处理pte不处理pde,处理pde的意义不大 */
static void page_table_pte_remove(uint32_t vaddr) {
uint32_t* pte = pte_ptr(vaddr);
*pte &= ~PG_P_1;
asm volatile("invlpg %0"::"m"(vaddr):"memory");
}
/* 在虚拟地址池中释放_vaddr起始的连续pg_cnt个虚拟页地址 */
static void vaddr_remove(enum pool_flags pf, void* addr, uint32_t pg_cnt) {
uint32_t bit_idx = 0;
struct virtual_addr* vaddr_pool;
if(pf == PF_KERNEL) {
bit_idx = ((uint32_t) addr - kernel_vaddr.vaddr_start) / PG_SIZE;
vaddr_pool = &kernel_vaddr;
} else {
struct task_struct* cur_thread = running_thread();
bit_idx = ((uint32_t) addr - cur_thread->userprog_vaddr.vaddr_start) / PG_SIZE;
vaddr_pool = &cur_thread->userprog_vaddr;
}
uint32_t cnt = 0;
while(cnt < pg_cnt) {
bitmap_set(&vaddr_pool->vaddr_bitmap, bit_idx++, 0);
cnt++;
}
}
二、以块为单位进行内存释放的函数mfree_page的实现
内存回收工作如上所述分为三大步骤,先调用pfree清空物理地址位图中的响应位,再调用page_table_pte_remove删除页表中此地址的pte,最后调用vaddr_remove清楚虚拟地址位图中的相应位(需要注意的是,物理内存申请时是不保证连续的,而虚拟内存是每个进程独享的所以我们保证了虚拟地址的连续性,所以每页虚拟地址对应的物理地址都需要通过函数addr_v2p找到后再进行释放,而清楚虚拟地址位图直接用vaddr_remove就可以将连续cnt页的虚拟地址在虚拟地址位图进行一次性释放)
mfree_page实现如下:
/* 释放以虚拟地址vaddr为起始的cnt个物理页框 */
void mfree_page(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt) {
/*
* 1、确保释放的内存在低端1MB+1KB大小的页目录+1KB大小的页表范围外
* 2、循环pg_cnt次将物理地址与页表映射释放
* 3、释放虚拟地址
* */
uint32_t vaddr = (uint32_t) _vaddr;
uint32_t paddr = addr_v2p(vaddr);
ASSERT(pg_cnt >= 1 && (vaddr % PG_SIZE) == 0);
ASSERT((paddr % PG_SIZE) == 0 && paddr >= 0x102000);
uint32_t cnt = 0;
while (cnt < pg_cnt) {
vaddr = _vaddr + cnt * PG_SIZE;
paddr = addr_v2p(vaddr);
if(pf == PF_KERNEL) {
ASSERT((paddr % PG_SIZE) == 0 && paddr >= kernel_pool.phy_addr_start && paddr < user_pool.phy_addr_start);
} else {
ASSERT((paddr % PG_SIZE) == 0 && paddr >= user_pool.phy_addr_start);
}
pfree(paddr);
page_table_pte_remove(vaddr);
cnt++;
}
vaddr_remove(pf, _vaddr, pg_cnt);
}
三、实现精细化(通过arena机制)的内存释放函数sys_free
思路如下:
1、首先判断是内核线程还是用户进程,该步的目的是如果后面要进行整页的内存释放需要知道释放的是内核内存还是用户内存,另外在释放内存时因为涉及到公共资源所以需要上锁,锁用到的是kernel_pool或user_pool中的锁
2、如果是>1024KB的内存块则直接释放内存页框
3、如果是<=1024KB的内存块则释放当前内存块到desc的free_list中,如果一个arena中所有块都没被使用将该arena所占用的一页内存进行释放
代码如下:
/* 精细化释放内存,只需要给内存块起始指针即可
* 如果是>1024KB的内存块则直接释放内存页框
* 如果是<=1024KB的内存块则释放当前内存块到desc的free_list中
* */
void sys_free(void* ptr) {
enum pool_flags PF;
struct pool* mem_pool;
if(running_thread()->pgdir == NULL) {
PF = PF_KERNEL;
mem_pool = &kernel_pool;
} else {
PF = PF_USER;
mem_pool = &user_pool;
}
struct mem_block* b = (struct mem_block*) ptr;
struct arena* a = block2arena(b);
ASSERT(a->large == 0 || a->large == 1);
lock_acquire(&mem_pool->lock);
if(a->desc == NULL || a->large == true) {
/* 大块内存直接释放整个内存页 */
mfree_page(PF, a, a->cnt);
} else {
/* 小块内存将当前内存块归还到free_list中 */
ASSERT(!elem_find(&a->desc->free_list, &b->free_elem));
list_append(&a->desc->free_list, &b->free_elem);
if(++a->cnt == a->desc->blocks_per_arena) {
uint32_t block_idx;
for(block_idx = 0; block_idx < a->desc->blocks_per_arena; block_idx++) {
ASSERT(elem_find(&a->desc->free_list, &(arena2block(a, block_idx)->free_elem)));
list_remove(&(arena2block(a, block_idx)->free_elem));
}
mfree_page(PF, a, 1);
}
}
lock_release(&mem_pool->lock);
}
四、将sys_malloc函数以及sys_free函数包装为系统调用
实现梳理一下0x80号系统调用的整体流程
用户态下:
(1)调用包装函数(如getpid、malloc、free),包装函数内部调用_syscallN进行宏展开
(2)宏会将调用号存入到eax,参数存入ebx,ecx,edx
(3)宏紧接着调用int 0x80号软中断指令,用来调用系统函数
(4)宏会从eax中拿到返回值
内核态下:
(1)进入0x80号中断处理程序,第一步先要保存中断前的上下文环境(ess、esp、eflags、cs、eip、错误号、四个段寄存器ds、es、fs、gs、pushad)
(2)接着push中断号,三个参数(edx、ecx、ebx),然后通过call [syscall_table + eax*4]调用对应的系统调用函数
(3)mov [esp+4+7*4],eax将函数返回结果覆盖到保存的中断前上下文环境的eax中接着跳到intr_exit还原中断前寄存器值然后通过iret返回用户态
根据以上0x80号系统调用中断过程的梳理,我们需要做的是分为以下几步:
(1)定义新增的系统调用的调用号(syscall.h中)
(2)创建包装函数(如sys_malloc的包装函数为malloc供用户态程序调用),包装函数内部通过_syscallN进行宏展开(syscall.c中)
(3)将对应的系统函数注册到syscall_table中(syscall-init.c中)
对应的代码如下,syscall.h中
#ifndef __LIB_USER_SYSCALL_H
#define __LIB_USER_SYSCALL_H
#include "stdint.h"
enum SYSCALL_NR {
SYS_GETPID,
SYS_WRITE,
SYS_MALLOC, //新增
SYS_FREE //新增
};
uint32_t getpid();
uint32_t write(char* str);
void* malloc(uint32_t size); //新增
void free(void* ptr); //新增
#endif
syscall.c中
#include "syscall.h"
#define _syscall0(NUMBER) ({ \
int retval; \
asm volatile ( \
"int $0x80" \
: "=a"(retval) \
: "a"(NUMBER) \
: "memory" \
); \
retval; \
})
#define _syscall1(NUMBER, ARG1) ({ \
int retval; \
asm volatile ( \
"int $0x80" \
: "=a"(retval) \
: "a"(NUMBER), "b"(ARG1) \
: "memory" \
);\
retval; \
})
uint32_t getpid() {
return _syscall0(SYS_GETPID);
}
uint32_t write(char* str) {
return _syscall1(SYS_WRITE, str);
}
//新增
void* malloc(uint32_t size) {
return (void*) _syscall1(SYS_MALLOC, size);
}
//新增
void free(void* ptr) {
_syscall1(SYS_FREE, ptr);
}
syscall-init.c中
#include "syscall-init.h"
#include "syscall.h"
#include "thread.h"
#include "console.h"
#include "string.h"
#include "memory.h"
#define syscall_nr 32
typedef void* syscall;
syscall syscall_table[syscall_nr];
uint32_t sys_getpid() {
struct task_struct* cur_therad = running_thread();
return cur_therad->pid;
}
uint32_t sys_write(char* str) {
console_put_str(str);
return strlen(str);
}
void syscall_init() {
put_str("syscall_init start\n");
syscall_table[SYS_GETPID] = sys_getpid;
syscall_table[SYS_WRITE] = sys_write;
syscall_table[SYS_MALLOC] = sys_malloc;
syscall_table[SYS_FREE] = sys_free;
put_str("syscall_init done\n");
}
至此,系统调用完善完成,接下来我们改动main.c进行系统调用的测试,main.c代码如下:
#include "print.h"
#include "init.h"
#include "debug.h"
#include "memory.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall.h"
#include "syscall-init.h"
#include "stdio.h"
void k_thread_a(void* arg);
void k_thread_b(void* arg);
void u_prog_a(void);
void u_prog_b(void);
int main(void) {
put_str("I am kernel\n");
init_all();
intr_enable();
process_execute(u_prog_a, "u_prog_a");
process_execute(u_prog_b, "u_prog_b");
thread_start("k_thread_a", 31, k_thread_a, "I am thread_a ");
thread_start("k_thread_b", 31, k_thread_b, "I am thread_b ");
while (1);
return 0;
}
void k_thread_a(void* arg) {
void* addr1 = sys_malloc(256);
void* addr2 = sys_malloc(255);
void* addr3 = sys_malloc(254);
console_put_str(" thread_a malloc addr:0x");
console_put_int((int) addr1);
console_put_char(',');
console_put_int((int) addr2);
console_put_char(',');
console_put_int((int) addr3);
console_put_char('\n');
int cpu_delay = 100000;
while(cpu_delay-- > 0);
sys_free(addr1);
sys_free(addr2);
sys_free(addr3);
while(1);
}
void k_thread_b(void* arg) {
void* addr1 = sys_malloc(256);
void* addr2 = sys_malloc(255);
void* addr3 = sys_malloc(254);
console_put_str(" thread_b malloc addr:0x");
console_put_int((int) addr1);
console_put_char(',');
console_put_int((int) addr2);
console_put_char(',');
console_put_int((int) addr3);
console_put_char('\n');
int cpu_delay = 100000;
while(cpu_delay-- > 0);
sys_free(addr1);
sys_free(addr2);
sys_free(addr3);
while(1);
}
void u_prog_a(void) {
void* addr1 = malloc(256);
void* addr2 = malloc(255);
void* addr3 = malloc(254);
printf(" prog_a malloc addr:0x%x,0x%x,0x%x\n", (int)addr1, (int)addr2, (int)addr3);
int cpu_delay = 100000;
while(cpu_delay-- > 0);
free(addr1);
free(addr2);
free(addr3);
while(1);
}
void u_prog_b(void) {
void* addr1 = malloc(256);
void* addr2 = malloc(255);
void* addr3 = malloc(254);
printf(" prog_b malloc addr:0x%x,0x%x,0x%x\n", (int)addr1, (int)addr2, (int)addr3);
int cpu_delay = 100000;
while(cpu_delay-- > 0);
free(addr1);
free(addr2);
free(addr3);
while(1);
}
接下来,就是最熟悉的make all环节,更新完kernel.bin后进入bochs运行得到结果如下:
大功告成,本节结束!