《操作系统-真象还原》12. 进一步完善内核

Linux 的系统调用

Linux 系统调用是通过中断门来实现的,通过软中断指令 int 来主动发送中断信号。但由于系统调用的功能不止一个两个,所以不可能一个系统调用对应一个中断门描述符,即不可能一个系统调用对应一个中断向量。

因此 Linux 使用了一个中断向量号 0x80,通过子功能号的形式去调用不同的功能。

查看系统调用帮助信息:

$ man syscall

函数 syscall 并不是由操作系统提供的,它由运行库 glibc 提供的,因此实际上 syscall 是库函数。

查看直接调用帮助信息:

$ man _syscall

Linux 中的系统调用是用寄存器来传递参数,这些参数从左到右的顺序依次存入不同的通用寄存器(除了 ESP)。

  • eax:保存子功能号
  • ebx:保存第 1 个参数
  • ecx:保存第 2 个参数
  • edx:保存第 3 个参数
  • esi:保存第 4 个参数
  • edi:保存第 5 个参数

还有个传入 6 个参数的,略过了。

系统调用的实现 —— 图解

image-20221119203653627

系统调用的实现 —— 代码

触发中断

lib/user/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(void);
uint32_t write(char* str);
void* malloc(uint32_t size);
void free(void* ptr);

#endif

lib/user/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; \
})

// 两个参数的系统调用
#define _syscall2(NUMBER, ARG1, ARG2) ({ \
    int retval; \
    asm volatile ("int $0x80" : "=a"(retval) : "a"(NUMBER), "b"(ARG1), "c"(ARG2) : "memory"); \
    retval; \
})

// 三个参数的系统调用
#define _syscall3(NUMBER, ARG1, ARG2, ARG3) ({ \
    int retval; \
    asm volatile ("int $0x80" : "=a"(retval) : "a"(NUMBER), "b"(ARG1), "c"(ARG2), "d"(ARG3) : "memory"); \
    retval; \
})

// 返回当前任务的 PID
uint32_t getpid() {
    return _syscall0(SYS_GETPID);
}

// 打印字符串
uint32_t write(char* str) {
    return _syscall1(SYS_WRITE, str);
}

// 申请 size 字节大小的内存,并返回结果
void* malloc(uint32_t size) {
    return (void*) _syscall1(SYS_MALLOC, size);
}

// 释放 ptr 指向的内存
void free(void* ptr) {
    _syscall1(SYS_FREE, ptr);
}

寻找 IDT 中断描述符

kernel/interrupt.c:

// 初始化中断描述符表
static void idt_desc_init(void) {
    int i, lastindex = IDT_DESC_CNT - 1;
    for(i = 0; i < IDT_DESC_CNT; i++) make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]);

    // 单独处理系统调用,系统调用对应的中断门 DPL 为 3
    // 注:此描述符的 DPL 必须为 3,若为 0,则在 3 特权级环境下执行 int 指令会出现 GP 异常
    make_idt_desc(&idt[lastindex], IDT_DESC_ATTR_DPL3, syscall_handler);
    put_str("   idt_desc_init done\n");
}

执行对应的中断例程

kernel/kernel.S:

;;;;;;;;;;;;;;;;   0x80号中断   ;;;;;;;;;;;;;;;;
[bits 32]
extern syscall_table
section .text
global syscall_handler
syscall_handler:
; 保护上下文环境 BEGIN
    push 0 ; 压入 0 假装一下中断错误码,使其格式统一

    push ds
    push es
    push fs
    push gs
    pushad

    push 0x80

; 压入参数
    push edx ; 第三个参数
    push ecx ; 第二个参数
    push ebx ; 第一个参数

; 调用子功能处理函数
    call [syscall_table + eax * 4] ; 编译器会在栈中传入的参数去找到正确的重载函数
    add esp, 12 ; 跨过上面传入的三个参数

; 将 call 调用后的返回值存入到 intr_exit 准备要恢复现场的对应 EAX 位置
; 这是为了避免 intr_exit 后 EAX 的值被旧的 EAX 值所还原、覆盖
    mov [esp + 8 * 4], eax
    jmp intr_exit ; 执行 intr_exit 恢复上下文环境

中断例程中通过用户传入的功能号去执行对应的功能函数

userprog/syscall-init.h:

#ifndef __USERPROG_SYSCALLINIT_H
#define __USERPROG_SYSCALLINIT_H
#include "stdint.h"
void syscall_init(void);
uint32_t sys_getpid(void);
#endif

userprog/syscall-init.c:

#define syscall_nr 32
typedef void* syscall;
syscall syscall_table[syscall_nr]; // 系统调用表

// 返回当前任务的 PID
uint32_t sys_getpid(void) {
    return running_thread() -> pid;
}

// 打印字符串(未实现文件系统前的版本)
uint32_t sys_write(char* str) {
    console_put_str(str);
    return strlen(str);
}

// 初始化系统调用
void syscall_init(void) {
    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");
}

关于 printf

你需要知道

  • 静态内存: 程序本身占用的内存,这通常是编译时数据结构长度就确定了的。
  • 动态内存: 程序运行过程中额外需求的内存,这通常是编译过后数据结构的长度还会变长。
  • 堆本来就是用于程序运行时的动态内存分配。
  • 函数占用也是静态内存,因此也得提前告知编译器自己所占用的内存大小。
  • 编译器不关心函数名是什么,只关心参数个数和类型,编译器用这两个信息才能确定函数在栈中所需要的内存大小。

可变参数的原理

上面说了堆用于动态内存分配,而函数用的是栈空间,因此编译器必须要要提前知道函数所需要占用的内存大小,但可变参数的参数却可以有很多个,看上去很动态的样子。

可变参数本质上是静态的,这得益于编译器采用 C 调用约定来处理函数的传参方式。

C 调用约定:由调用者把参数从右向左的顺序压入栈中,并且由调用者清理堆栈中的参数。

既然参数由调用者压入,那调用者自然知道有几个参数,以及所占用多少空间,因此采用 C 调用约定,无论有多少参数,调用者都可以确定个数以及所占用空间大小,从而完好的回收栈空间。

也就是说,传入的参数的个数是编译器在编译阶段就以及确定下来的了。

Linux 中的可变参数原理

查看 stdarg.h:

man stdarg

image-20221119212402843

va_start、va_end、va_arg 这三个表示可变参数。

查看 stdarg.h 文件:

...
#define va_start(v,l)   __builtin_va_start(v,l)
#define va_end(v)       __builtin_va_end(v)
#define va_arg(v,l)     __builtin_va_arg(v,l)
...

我的路径:/usr/lib/gcc/x86_64-linux-gnu/9/include/stdarg.h

__builtin_va_* 表示内建符号,内建 builtins 表示这个是 gcc 内部实现的功能。

参数:

  • ap(argument pointer):是个指针变量,它指向 format 在栈中的地址。

三个宏:

#define va_start(ap, v) ap = (va_list)&v  // 把ap指向第一个固定参数v
#define va_arg(ap, t) *((t*)(ap += 4))    // ap指向下一个参数并返回其值
#define va_end(ap) ap = NULL              // 清除a

Linux 中的可变参数实现

printf 只是个壳

printf 输出靠内部的 write 系统调用,其内容的格式化又靠 vsprintf 函数,因此 printf 只是个壳子。

实现 write

lib/user/syscall.h:

enum SYSCALL_NR {
   SYS_WRITE,
};

uint32_t write(char* str);

lib/user/syscall.c:

// 打印字符串
uint32_t write(char* str) {
    return _syscall1(SYS_WRITE, str);
}

userprog/syscall-init.c:

// 初始化系统调用
void syscall_init(void) {
    syscall_table[SYS_WRITE] = sys_write;
}
实现 vsprintf 与 printf

lib/stdio.c:

#define va_start(ap, v) ap = (va_list)&v  // 把ap指向第一个固定参数v
#define va_arg(ap, t) *((t*)(ap += 4))	  // ap指向下一个参数并返回其值
#define va_end(ap) ap = NULL		      // 清除ap

// 将整形转换成字符
static void itoa(uint32_t value, char** buf_ptr_addr, uint8_t base) {
    uint32_t m = value % base; // 取模
    uint32_t i = value / base; // 取整
    if(i) itoa(i, buf_ptr_addr, base);          // 若为0表示已经除到头了、转换完了
    if(m < 10) *((*buf_ptr_addr)++) = m + '0';  // 若取模得到的余数 < 10,则转换为 '0'~'9' 字符
    else *((*buf_ptr_addr)++) = m - 10 + 'A';   // 若取模得到的余数 >= 10,则转换为 'A'~'F' 字符
}

// 将参数 ap 按照格式 format 输出到字符串 str,并返回替换后的 str 长度
uint32_t vsprintf(char* str, const char* format, va_list ap) {
    char* buf_ptr = str;
    const char* index_ptr = format;
    char index_char = *index_ptr; // 指向 format 字符串某个字符
    int32_t arg_int;
    char* arg_str;
    while(index_char) {
        if(index_char != '%') { // 处理当前字符不是 % 的情况
            *(buf_ptr++) = index_char;  // 先把字符输出到 str,然后++
            index_char = *(++index_ptr);// 得到 format 下一个字符,赋值给 index_char
            continue;
        }
        index_char = *(++index_ptr); // 得到 % 后的字符,它表示数据类型
        switch(index_char) {
            case 's':
                arg_str = va_arg(ap, char*);
                strcpy(buf_ptr, arg_str);
                buf_ptr += strlen(arg_str);
                index_char = *(++index_ptr);
                break;
            case 'c':
                *(buf_ptr++) = va_arg(ap, char);
                index_char = *(++index_ptr);
                break;
            case 'd':
                arg_int = va_arg(ap, int);
                if(arg_int < 0) { // 若为负数,将其转为正数后,在前面添加一个负号'-'
                    arg_int = 0 - arg_int;
                    *buf_ptr++ = '-';
                }
                itoa(arg_int, &buf_ptr, 10);
                index_char = *(++index_ptr);
                break;
            case 'x':
                arg_int = va_arg(ap, int);
                itoa(arg_int, &buf_ptr, 16);
                index_char = *(++index_ptr); // 指向 format 的下一个字符
                break;
        }
    }
    return strlen(str);
}

/* 同printf不同的地方就是字符串不是写到终端,而是写到buf中 */
uint32_t sprintf(char* buf, const char* format, ...) {
    va_list args;
    uint32_t retval;
    va_start(args, format);
    retval = vsprintf(buf, format, args);
    va_end(args);
    return retval;
}

// 格式化输出字符串 format
uint32_t printf(const char* format, ...) {
    va_list args;
    va_start(args, format);     // 将 args 指向 format
    char buf[1024] = {0};       // 用于存储拼接后的字符串
    vsprintf(buf, format, args);
    va_end(args);
    return write(buf);
}

lib/stdio.h:

#ifndef __LIB_STDIO_H
#define __LIB_STDIO_H
#include "stdint.h"
typedef char* va_list;
uint32_t printf(const char* str, ...);
uint32_t vsprintf(char* str, const char* format, va_list ap);
#endif

完善内存管理

相关概念

arena 是很多开源项目中都会用到的内存管理概念,将内存划分为多个大内存块,每个大内存块之间互不干涉,可以分别管理,这个大内存块称为 arena。
而大内存块,即 arena 中包含两部分,其中一部分用来划分小内存块,这个小内存块我们称为 mem_block

本书中一个大内存块为 4KB 粒度,即为一页,当然,大内存块也可以是一页或多页,根据大内存块的容量平均划分小内存块的大小。

arena 是个提供内存分配的数据结构,它分为两部分,一部分是元信息,用来描述自己内存池中空闲内存块的数量,该部分占用 12 字节,另一部分就是内存池区域,里面有很多被划分好的小内存块。每个内存块命名为 mem_block,它们是内存分配粒度更细的资源。

图示:

image-20221120015853806

当一个 arena 中的小内存块,即 mem_block 都分配完了后,若还需要申请空间,则此时需要再次创建一个同一规格的 arena,若同一规格的 arena 越来也多,为了跟踪这些 arena,也就是为了跟踪这些 arena 中的 mem_block,我们需要为每个 arena 的元信息中建立一个内存块描述符,即 mem_block_desc,在其中记录内存块规格大小,以及所有同一规格 arena 中的空闲块(是个链表)。

image-20221120015923932

本书支持了 7 种规格,分别是:16、32、64、128、256、512、1024 字节。超过 1024 字节的直接分配一页空间。

arena 中 mem_block 数量的计算方式:(内存粒度 - 12) / 规格。
例如:(4096 - 12) / 128

arena 是个内存仓库,并不直接对外提供内存分配,只有内存块描述符才对外提供内存块,内存块描述符将同类 arena 中的空闲块汇聚到一起,作为某个同一规格内存块的分配入口。

image-20221120021230210

数据结构

kernel/memory.c:

// 内存仓库 arena 的元信息
struct arena {
    struct mem_block_desc* desc; // 此 arena 锁关联的 mem_block_desc
    uint32_t cnt;                // 若 large = true,则 cnt 表示页宽数
                                 // 若 large = false,则 cnt 表示空闲 mem_block 的数量
    bool large; // 标识该 arena 到底是管理 1024 字节内的内存(true),如何超过 1024 字节的内存(false)
};

struct mem_block_desc k_block_descs[DESC_CNT]; // 内核内存块描述符数组

kernel/memory.h:

// 内存块
struct mem_block {
   struct list_elem free_elem;
};

// 内存块描述符
struct mem_block_desc {
   uint32_t block_size;       // 内存块大小
   uint32_t blocks_per_arena; // 该 arena 中可容纳多少 block_size 大小的块
   struct list free_list;     // 目前可用的 mem_block 链表
};

#define DESC_CNT 7 // 内存块描述符的个数

kernel/menory.c:

// 为 malloc 做准备
void block_desc_init(struct mem_block_desc* desc_array) {
    uint16_t desc_idx, block_size = 16;

    for(desc_idx = 0; desc_idx < DESC_CNT; desc_idx++) {
        desc_array[desc_idx].block_size = block_size;
        // 初始化 arena 中的内存块数量
        desc_array[desc_idx].blocks_per_arena = (PG_SIZE - sizeof(struct arena)) / block_size;
        
        list_init(&desc_array[desc_idx].free_list);

        block_size *= 2; // 更新为下一个规格的内存块
    }

}

// 内存管理部分的初始化入口
void mem_init() {
    put_str("mem_init start\n");

    // 0xb00 是 loader.S 中定义的 mem_bytes_total 存储总内存容量
    uint32_t mem_bytes_total = (*(uint32_t*)(0xb00)); 

    mem_pool_init(mem_bytes_total);	  // 初始化内存池
    block_desc_init(k_block_descs);
    put_str("mem_init done\n");
}

内存分配 —— sys_malloc

thread/thread.h:

// 进程或线程的 PCB,即程序控制块
struct task_struct {
    ...
    struct mem_block_desc u_block_desc[DESC_CNT]; // 用户进程内存块的描述符
    ...
};

userprog/process.c:

void process_execute(void* filename, char* name) {
    ...
    thread -> pgdir = create_page_dir();
    block_desc_init(thread -> u_block_desc);
    ...
}

kernel/memory.c:

// 返回 arena 中第 idx 个内存块的地址
static struct mem_block* arena2block(struct arena* a, uint32_t idx) {
    return (struct mem_block*) ((uint32_t)a + sizeof(struct arena) + idx * a -> desc -> block_size);
}

// 返回内存块 b 所在的 arena 地址
static struct arena* block2arena(struct mem_block* b) {
    return (struct arena*)((uint32_t)b & 0xFFFFF000);
}

// 在堆中申请 size 字节内存
void* sys_malloc(uint32_t size) {
    enum pool_flags PF;
    struct pool* mem_pool;
    uint32_t pool_size;
    struct mem_block_desc* descs;
    struct task_struct* cur_thread = running_thread();

    // 判断使用哪个内存池
    if(cur_thread -> pgdir == NULL) { // 内核线程
        PF = PF_KERNEL;
        pool_size = kernel_pool.pool_size;
        mem_pool = &kernel_pool;
        descs = k_block_descs;
    } else { // 用户线程
        PF = PF_USER;
        pool_size = user_pool.pool_size;
        mem_pool = &user_pool;
        descs = cur_thread -> u_block_desc;
    }

    // 若申请的内存不在内存池容量范围内,则直接返回 NULL
    if(!(size > 0 && size < pool_size)) return NULL;

    struct arena* a;     // 这就是划分的大内存块
    struct mem_block* b; // 这就是在大内存块内又划分的小内存块
    lock_acquire(&mem_pool -> lock);

    // 超过最大内存块 1024 字节,就分配页框
    if(size > 1024) {
        uint32_t page_cnt = DIV_ROUND_UP(size + sizeof(struct arena), PG_SIZE);

        a = malloc_page(PF, page_cnt);

        if(a == NULL) {
            lock_release(&mem_pool -> lock);
            return NULL;
        }

        memset(a, 0, page_cnt * PG_SIZE);

        // 对于分配的大块页框,要初始化如下:
        a -> desc = NULL;    // 没有关联的内存块描述符
        a -> cnt = page_cnt; // 页框的数量
        a -> large = true;   // 该 arena 表示为页框

        lock_release(&mem_pool -> lock);
        return (void*) (a + 1); // 跨过 arena 大小,把剩下的内存将其首地址返回
    } else {
        uint8_t desc_idx;

        // 从内存块描述符中匹配合适的内存块规格
        for(desc_idx = 0; desc_idx < DESC_CNT; desc_idx++) {
            if(size <= descs[desc_idx].block_size) break;
        }

        // 判断该规格的空闲块是否还有剩余,若无,则进行生产
        if(list_empty(&descs[desc_idx].free_list)) {
            a = malloc_page(PF, 1);
            if(a == NULL) {
                lock_release(&mem_pool -> lock);
                return NULL;
            }
            memset(a, 0, PG_SIZE);

            a -> desc = &descs[desc_idx];
            a -> cnt = descs[desc_idx].blocks_per_arena;
            a -> large = false;

            uint32_t block_idx;

            enum intr_status old_status = intr_disable();

            // 开始将 arena 拆分成内存块,并添加到内存块描述符的 free_list 中
            for(block_idx = 0; block_idx < descs[desc_idx].blocks_per_arena; block_idx++) {
                b = arena2block(a, block_idx);
                ASSERT(!elem_find(&a -> desc -> free_list, &b -> free_elem));
                list_append(&a -> desc -> free_list, &b -> free_elem);
            }

            intr_set_status(old_status);
        }

        // 开始分配内存块
        b = elem2entry(struct mem_block, free_elem, list_pop(&(descs[desc_idx].free_list)));
        memset(b, 0, descs[desc_idx].block_size);

        a = block2arena(b); // 获取内存块 b 所在的 arena
        a -> cnt--;         // 将此 arena 中的空闲内存块减一
        lock_release(&mem_pool -> lock);
        return (void*) b;
    }
}

内存释放

步骤概述

内存的使用情况是通过位图来管理的,因此,无论内存的分配或释放,本质上都是在设置相关位图中的相应位。

分配内存:

  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。

以上步骤封装到了 malloc_page 中。

释放内存:

  1. 在物理地址内存池中释放物理页地址,相关函数是 pfree。
  2. 在页表中去掉虚拟地址的映射,原理是将虚拟地址对应的 PTE 的 P 位置为 0,相关函数是 page_table_pte_remove。
  3. 在虚拟地址池中释放虚拟地址,相关函数是 vaddr_remove。
释放虚拟地址
// 将物理地址 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 的映射,只去掉 vaddr 对应的 PTE
static void page_table_pte_remove(uint32_t vaddr) {
    uint32_t* pte = pte_ptr(vaddr);
    *pte &= ~PG_P_1; // 将页表项 PTE 的 P 位置为 0
    asm volatile("invlpg %0" : : "m"(vaddr) : "memory"); // 更新 TLB
}

// 在虚拟地址内存池中释放以 _vaddr 起始的连续 pg_cnt 个虚拟页地址
static void vaddr_remove(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt) {
    uint32_t bit_idx_start = 0, vaddr = (uint32_t) _vaddr, cnt = 0;
    if(pf == PF_KERNEL) { // 内核虚拟内存池
        bit_idx_start = (vaddr - kernel_vaddr.vaddr_start) / PG_SIZE; // 计算 vaddr 在虚拟内存池中的偏移量
        while(cnt < pg_cnt) 
            bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 0);
    } else { // 用户虚拟内存池
        struct task_struct* cur_thread = running_thread();
        bit_idx_start = (vaddr - cur_thread -> userprog_vaddr.vaddr_start) / PG_SIZE;
        while(cnt < pg_cnt)
            bitmap_set(&cur_thread -> userprog_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 0);
    }
}
释放物理地址
// 释放以虚拟地址 vaddr 为起始的 cnt 个物理页信息
void mfree_page(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt) {
    uint32_t pg_phy_addr;
    uint32_t vaddr = (int32_t) _vaddr, page_cnt = 0;
    ASSERT(pg_cnt >= 1 && vaddr % PG_SIZE == 0);
    pg_phy_addr = addr_v2p(vaddr);

    // 确保待释放的物理内存 存在低端 1M + 1k 大小的页目录 + 1k 大小的页表地址范围外
    ASSERT((pg_phy_addr % PG_SIZE) == 0 && pg_phy_addr >= 0x102000);

    // 判断 pg_phy_addr 属于哪个区域
    if(pg_phy_addr >= user_pool.phy_addr_start) { // 用户物理内存池
        vaddr -= PG_SIZE;
        while(page_cnt < pg_cnt) {
            vaddr += PG_SIZE;
            pg_phy_addr = addr_v2p(vaddr);

            // 确保物理地址属于用户内存池
            ASSERT((pg_phy_addr % PG_SIZE) == 0 && pg_phy_addr >= user_pool.phy_addr_start);

            // 先将对应的物理页框规划到内存池
            pfree(pg_phy_addr);

            // 再从页表中清除此虚拟地址所在页表项 PTE
            page_table_pte_remove(vaddr);

            //pg_cnt++;
            page_cnt++;
        }
        // 清空虚拟地址位图中相应的位
        vaddr_remove(pf, _vaddr, pg_cnt);
    } else { // 内核物理内存池
        vaddr -= PG_SIZE;
        while(page_cnt < pg_cnt) {
            vaddr += PG_SIZE;
            pg_phy_addr = addr_v2p(vaddr);
            // 确保待释放的物理内存只属于内核物理内存池
            ASSERT((pg_phy_addr % PG_SIZE) == 0 && pg_phy_addr >= kernel_pool.phy_addr_start && pg_phy_addr < user_pool.phy_addr_start);

            // 线将对应的物理页框归还到内存池
            pfree(pg_phy_addr);

            // 再从页表中清除此虚拟地址所在的页表项 PTE
            page_table_pte_remove(vaddr);

            page_cnt++;
        }

        // 清空虚拟地址位图中的相应位
        vaddr_remove(pf, _vaddr, pg_cnt);
    }
}
内存释放 sys_free
// 回收内存 ptr
void sys_free(void* ptr) {
    ASSERT(ptr != NULL);
    if(ptr != NULL) {
        enum pool_flags PF;
        struct pool* mem_pool;

        // 判断线程还是进程
        if(running_thread() -> pgdir == NULL) { // 线程
            ASSERT((uint32_t) ptr >= K_HEAP_START);
            PF = PF_KERNEL;
            mem_pool = &kernel_pool;
        } else { // 进程
            PF = PF_USER;
            mem_pool = &user_pool;
        }

        lock_acquire(&mem_pool -> lock);

        struct mem_block* b = ptr; // 转为 mem_block 内存块类型指针
        struct arena* a = block2arena(b); // mem_block 转换到 arena

        ASSERT(a -> large == 0 || a -> large == 1);

        if(a -> desc == NULL && a -> large == true) // 要释放的内存大于 1024 字节
            mfree_page(PF, a, a -> cnt);
        else { // 要释放的内存小于或等于 1024 字节
            // 先将内存块回收到 free_list
            list_append(&a -> desc -> free_list, &b -> free_elem);

            // 再判断此 arena 中的内存块是否都是空闲块,若是,则释放 arena
            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++) {
                    struct mem_block* b = arena2block(a, block_idx);
                    ASSERT(elem_find(&a -> desc -> free_list, &b -> free_elem));
                    list_remove(&b -> free_elem);
                }
                mfree_page(PF, a, 1); // 释放该 arena
            }
        }

        lock_release(&mem_pool -> lock);
    }
}

系统调用 malloc 和 free

lib/user/syscall.h:

enum SYSCALL_NR {
   SYS_GETPID,
   SYS_WRITE,
   SYS_MALLOC,
   SYS_FREE
};

uint32_t getpid(void);
uint32_t write(char* str);
void* malloc(uint32_t size);
void free(void* ptr);

lib/user/syscall.c:

// 申请 size 字节大小的内存,并返回结果
void* malloc(uint32_t size) {
    return (void*) _syscall1(SYS_MALLOC, size);
}

// 释放 ptr 指向的内存
void free(void* ptr) {
    _syscall1(SYS_FREE, ptr);
}

userprog/syscall-init.c:

// 初始化系统调用
void syscall_init(void) {
    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");
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值