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


    Linux 的系统调用

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

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

    查看系统调用帮助信息:

    $ man syscall
    
    • 1

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

    查看直接调用帮助信息:

    $ man _syscall
    
    • 1

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    寻找 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");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    执行对应的中断例程

    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 恢复上下文环境
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

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

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    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");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    关于 printf

    你需要知道

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

    可变参数的原理

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

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

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

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

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

    Linux 中的可变参数原理

    查看 stdarg.h:

    man stdarg
    
    • 1

    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)
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5

    我的路径:/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
    
    • 1
    • 2
    • 3

    Linux 中的可变参数实现

    printf 只是个壳

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

    实现 write

    lib/user/syscall.h:

    enum SYSCALL_NR {
       SYS_WRITE,
    };
    
    uint32_t write(char* str);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    lib/user/syscall.c:

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

    userprog/syscall-init.c:

    // 初始化系统调用
    void syscall_init(void) {
        syscall_table[SYS_WRITE] = sys_write;
    }
    
    • 1
    • 2
    • 3
    • 4
    实现 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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    完善内存管理

    相关概念

    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]; // 内核内存块描述符数组
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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 // 内存块描述符的个数
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    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");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    内存分配 —— sys_malloc

    thread/thread.h:

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

    userprog/process.c:

    void process_execute(void* filename, char* name) {
        ...
        thread -> pgdir = create_page_dir();
        block_desc_init(thread -> u_block_desc);
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103

    内存释放

    步骤概述

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

    分配内存:

    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    释放物理地址
    // 释放以虚拟地址 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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    内存释放 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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    系统调用 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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
  • 相关阅读:
    深度学习(Python)学习笔记2
    比memcpy还要快的内存拷贝,了解一下?
    Ladder Side-Tuning:预训练模型的“过墙梯”
    Grafana alert预警+钉钉通知
    quickapp_快应用_快应用组件
    3.Python-用Python实现MySQL数据库的增删改查
    echart3D地图
    LeetCode-623-在二叉树中增加一行
    JUC并发编程系列详解篇六(死锁的基本概念)
    大都会人寿线下培训第三天回顾(爱的书信)及线上课程笔记
  • 原文地址:https://blog.csdn.net/qq_43098197/article/details/127945949