第十二章 进一步完善内核
本文是对《操作系统真象还原》第十二章学习的笔记,欢迎大家一起交流,目前所有代码已托管至 fdx-xdf/MyTinyOS 。
实现系统调用 getpid
前置知识
系统调用(System Call)是操作系统提供给应用程序访问硬件资源和操作系统服务的接口。通过系统调用,程序可以向操作系统请求服务,如文件操作、内存管理、进程控制等。系统调用位于用户态和内核态之间,通常被用来执行用户程序无法直接完成的任务。
linux 系统调用是通过软中断实现的,并且 linux 系统调用产生的中断向量号只有一个,即 0x80
,也就是说,当处理器执行指令 int 0x80
时就触发了系统调用。
为了让用户程序可以通过这一个中断门调用多种系统功能,在系统调用之前,Linux 在寄存器 eax
中写入子功能号,例如系统调用 open
和 close
都是不同的子功能号,当用户程序通过 int 0x80
进行系统调用时,对应的中断处理例程会根据 eax
的值来判断用户进程申请哪种系统调用。
下面借助(《操作系统真象还原》第十二章(一) —— 系统调用 (jl-sky.github.io))里面的图来展示整个流程
完善系统调用框架
框架如上图所示,我们要做的任务如下:
- 构建系统调用所需的中断描述符
- 构建触发系统调用中断的转接口,该转接口的作用是将
eax
中的系统调用号作为索引,然后按照索引寻找syscall_table
中对应的系统调用例程
首先,我们在 /kernel/interrupt.c
中先支持 80 中断号,注意系统调用对应的中断门为 dpl3,修改如下:
#define IDT_DESC_CNT 0x81 //目前支持的中断数
extern uint32_t syscall_handler(void);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,* 中断处理程序为单独的syscall_handler */make_idt_desc(&idt[lastindex],IDT_DESC_ATTR_DPL3,syscall_handler);put_str(" idt_desc_init done\n");
}
syscall_handler
表示系统调用的中断触发时所调用的函数,也就是我们的转接口,其实现在 kernel.s 中,如下:
;;;;;;;;;;;;;;;; 0x80号中断 ;;;;;;;;;;;;;;;;
[bits 32]
extern syscall_table
section .text
global syscall_handler
syscall_handler:
;1 保存上下文环境push 0 ; 压入0, 使栈中格式统一push dspush espush fspush gspushad ; PUSHAD指令压入32位寄存器,其入栈顺序是:; EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI push 0x80 ; 此位置压入0x80也是为了保持统一的栈格式;2 为系统调用子功能传入参数push edx ; 系统调用中第3个参数push ecx ; 系统调用中第2个参数push ebx ; 系统调用中第1个参数;3 调用子功能处理函数call [syscall_table + eax*4] ; 编译器会在栈中根据C函数声明匹配正确数量的参数add esp, 12 ; 跨过上面的三个参数;4 将call调用后的返回值存入待当前内核栈中eax的位置mov [esp + 8*4], eaxjmp intr_exit ; intr_exit返回,恢复上下文
该函数和上面 intr%1entry
有很多类似的的地方,毕竟都是用来中断处理的,首先先 push 一些寄存器用来保护现场,还 push 了一些常数来统一栈上的格式,方便最后调用 intr_exit
中断返回,然后 call [syscall_table + eax*4]
来调用系统调用处理函数,最后平栈,处理返回结果,然后跳到上面的 intr_exit
即可。
实现 getpid
我们在上面已经实现了大致框架,接下来只需要将实现的函数加入到 syscall_table
中即可
为用户进程和内核线程分配 pid
getpid()的作用是返回给用户当前任务的 pid,为了实现此功能,我们首先需要给进程或者线程分配 pid,首先在 pcb 中加入 pid 参数
/* 进程或线程的pcb,程序控制块, 此结构体用于存储线程的管理信息*/
struct task_struct
{uint32_t *self_kstack; // 用于存储线程的栈顶位置,栈顶放着线程要用到的运行信息pid_t pid;enum task_status status;uint8_t priority; // 线程优先级char name[16]; // 用于存储自己的线程的名字uint8_t ticks; // 线程允许上处理器运行还剩下的滴答值,因为priority不能改变,所以要在其之外另行定义一个值来倒计时uint32_t elapsed_ticks; // 此任务自上cpu运行后至今占用了多少cpu嘀嗒数, 也就是此任务执行了多久*/struct list_elem general_tag; // general_tag的作用是用于线程在一般的队列(如就绪队列或者等待队列)中的结点struct list_elem all_list_tag; // all_list_tag的作用是用于线程队列thread_all_list(这个队列用于管理所有线程)中的结点uint32_t *pgdir; // 进程自己页表的虚拟地址struct virtual_addr userprog_vaddr; // 用户进程的虚拟内存池uint32_t stack_magic; // 如果线程的栈无限生长,总会覆盖地pcb的信息,那么需要定义个边界数来检测是否栈已经到了PCB的边界
};
然后在进程/线程初始化的时候调用分配 pid 的函数,该函数如下:
struct lock pid_lock; // 分配pid锁
// 分配pid
static pid_t allocate_pid(void)
{static pid_t next_pid = 0;lock_acquire(&pid_lock);next_pid++;lock_release(&pid_lock);return next_pid;
}
在初始化线程的时候要先初始化 pid 锁
/* 初始化线程环境 */
void thread_init(void)
{put_str("thread_init start\n");list_init(&thread_ready_list);list_init(&thread_all_list);lock_init(&pid_lock);/* 将当前main函数创建为线程 */make_main_thread();put_str("thread_init done\n");
}
然后在 init_tinit_hread
里面初始化 pid
/* 初始化线程基本信息 , pcb中存储的是线程的管理信息,此函数用于根据传入的pcb的地址,线程的名字等来初始化线程的管理信息*/
void init_tinit_hread(struct task_struct *pthread, char *name, int prio)
{memset(pthread, 0, sizeof(*pthread)); // 把pcb初始化为0pthread->pid = allocate_pid();if (pthread == main_thread)pthread->status = TASK_RUNNING;elsepthread->status = TASK_READY;strcpy(pthread->name, name);pthread->priority = prio;/* self_kstack是线程自己在内核态下使用的栈顶地址 */pthread->self_kstack = (uint32_t *)((uint32_t)pthread + PG_SIZE); // 本操作系统比较简单,线程不会太大,就将线程栈顶定义为pcb地址//+4096的地方,这样就留了一页给线程的信息(包含管理信息与运行信息)空间pthread->ticks = prio;pthread->elapsed_ticks = 0;pthread->pgdir = NULL;pthread->stack_magic = 0x19870916; // /定义的边界数字,随便选的数字来判断线程的栈是否已经生长到覆盖pcb信息了
}
getpid
接下来实现 getpid
/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
#include "syscall-init.h"
#include "syscall.h"
#include "stdint.h"
#include "print.h"
#include "thread.h"#define syscall_nr 32
typedef void* syscall;
syscall syscall_table[syscall_nr];/* 返回当前任务的pid */
uint32_t sys_getpid(void) {return running_thread()->pid;
}/* 初始化系统调用 */
void syscall_init(void) {put_str("syscall_init start\n");syscall_table[SYS_GETPID] = sys_getpid;put_str("syscall_init done\n");
}
syscall_init 函数也要加入到 init_all 里面,/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
然后将上面实现的 sys_getpid 加入到系统调用表中,/lib/user/syscall.h
如下:
#ifndef __LIB_USER_SYSCALL_H
#define __LIB_USER_SYSCALL_H
#include "stdint.h"
enum SYSCALL_NR {SYS_GETPID
};
uint32_t getpid(void);
#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);
}
getpid
就是我们提供给用户使用的库函数
main.c 修改如下:
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall-init.h"
#include "syscall.h"void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);
int prog_a_pid = 0, prog_b_pid = 0;int main(void) {put_str("I am kernel\n");init_all();process_execute(u_prog_a, "user_prog_a");process_execute(u_prog_b, "user_prog_b");intr_enable();console_put_str(" main_pid:0x");console_put_int(sys_getpid());console_put_char('\n');thread_start("k_thread_a", 31, k_thread_a, "argA ");thread_start("k_thread_b", 31, k_thread_b, "argB ");while(1);return 0;
}/* 在线程中运行的函数 */
void k_thread_a(void* arg) { char* para = arg;console_put_str(" thread_a_pid:0x");console_put_int(sys_getpid());console_put_char('\n');console_put_str(" prog_a_pid:0x");console_put_int(prog_a_pid);console_put_char('\n');while(1);
}/* 在线程中运行的函数 */
void k_thread_b(void* arg) { char* para = arg;console_put_str(" thread_b_pid:0x");console_put_int(sys_getpid());console_put_char('\n');console_put_str(" prog_b_pid:0x");console_put_int(prog_b_pid);console_put_char('\n');while(1);
}/* 测试用户进程 */
void u_prog_a(void) {prog_a_pid = (int)sys_getpid();while(1);
}/* 测试用户进程 */
void u_prog_b(void) {prog_b_pid = getpid();while(1);
}
编译运行结果如下:
实现系统调用 printf
即实现 wirte 系统调用,先封装函数
/userprog/syscall-init.h
#ifndef __USERPROG_SYSCALLINIT_H
#define __USERPROG_SYSCALLINIT_H
#include "stdint.h"
void syscall_init(void);
uint32_t sys_getpid(void);
uint32_t sys_write(char* str);
#endif
/userprog/syscall-init.c
#include "syscall-init.h"
#include "syscall.h"
#include "stdint.h"
#include "print.h"
#include "thread.h"
#include "console.h"
#include "string.h"#define syscall_nr 32
typedef void *syscall;
syscall syscall_table[syscall_nr];/* 返回当前任务的pid */
uint32_t sys_getpid(void)
{return running_thread()->pid;
}
/* 打印字符串str(未实现文件系统前的版本) */
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;put_str("syscall_init done\n");
}
/lib/user/syscall.h
#ifndef __LIB_USER_SYSCALL_H
#define __LIB_USER_SYSCALL_H
#include "stdint.h"
enum SYSCALL_NR {SYS_GETPID,SYS_WRITE
};
uint32_t getpid(void);
uint32_t write(char* str);
#endif
/lib/user/syscall.c
/* 打印字符串str */
uint32_t write(char* str) {return _syscall1(SYS_WRITE, str);
}
接下来就是 printf 实现,我们要实现格式化字符串的功能
printf
/lib/user/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);
uint32_t sprintf(char* buf, const char* format, ...);
#endif
可以看到我们封装了三个函数 printf、vsprintf、sprintf,其中 print 是用户调用的函数,然后会调用 vsprintf,在 vsprintf 里面会照 format 格式解析字符串,最后调用 write 打印,sprintf 就是按照 format 格式化指定的字符串
/lib/user/stdio.c
#include "stdio.h"
#include "stdint.h"
#include "string.h"
#include "global.h"
#include "syscall.h"#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/* 将整型转换成字符(integer to ascii) */
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) // 如果倍数不为0则递归调用。itoa(i, buf_ptr_addr, base);if (m < 10)*((*buf_ptr_addr)++) = m + '0'; // 将数字0~9转换为字符'0'~'9'else*((*buf_ptr_addr)++) = m - 10 + 'A'; // 将数字A~F转换为字符'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;int32_t arg_int;char *arg_str;while (index_char){if (index_char != '%'){*(buf_ptr++) = index_char;index_char = *(++index_ptr);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); // 跳过格式字符并更新index_charbreak;}}return strlen(str);
}/* 格式化输出字符串format */
uint32_t printf(const char* format, ...) {va_list args;va_start(args, format); // 使args指向formatchar buf[1024] = {0}; // 用于存储拼接后的字符串vsprintf(buf, format, args);va_end(args);return write(buf);
}/* 同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;
}
首先先定义了三个宏,用于处理可变参数。va_start(ap, v)初始化可变参数指针 ap
,使其指向第一个固定参数 v
,va_arg(ap, t)获取当前参数的值,并将 ap
移动到下一个参数的位置,va_end(ap)清理 ap
,标记可变参数处理结束。
核心就是 vsprintf,不断遍历格式化字符串,如果没遇到 %,就继续遍历,遇到 % 就根据后面的字母判断是什么类型,然后去栈上找复制到 buf 中,流程图如下:
测试
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall-init.h"
#include "syscall.h"
#include "stdio.h"void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);int main(void) {put_str("I am kernel\n");init_all();process_execute(u_prog_a, "u_prog_a");process_execute(u_prog_b, "u_prog_b");console_put_str(" I am main, my pid:0x");console_put_int(sys_getpid());console_put_char('\n');intr_enable();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) { char* para = arg;console_put_str(" I am thread_a, my pid:0x");console_put_int(sys_getpid());console_put_char('\n');while(1);
}/* 在线程中运行的函数 */
void k_thread_b(void* arg) { char* para = arg;console_put_str(" I am thread_b, my pid:0x");console_put_int(sys_getpid());console_put_char('\n');while(1);
}/* 测试用户进程 */
void u_prog_a(void) {char* name = "prog_a";printf(" I am %s, my pid:%d%c", name, getpid(),'\n');while(1);
}/* 测试用户进程 */
void u_prog_b(void) {char* name = "prog_b";printf(" I am %s, my pid:%d%c", name, getpid(), '\n');while(1);
}
编译测试如下:
完善堆内存管理
我们之前实现的内存管理都是很粗糙的,以 4kb 的页框为单位进行分配,我们现在要实现更细致的内存分配。
本节的主要任务有:
- 实现 sys_malloc
- 实现 sys_free
- 实现 malloc
- 实现 free
数据结构准备
首先,我们来进行底层数据结构的建立:
空闲内存块的定义,对内存块我们使用双向链表进行定义:
/* 内存块 */
struct mem_block {struct list_elem free_elem;
};
为了对内存块进行管理,我们还需要定义内存块的信息管理数据结构
/* 内存块描述符 */
struct mem_block_desc {uint32_t block_size; // 内存块大小uint32_t blocks_per_arena; // 本arena中可容纳此mem_block的数量.struct list free_list; // 目前可用的mem_block链表
};
同时为了分配内存,我们需要记录空闲内存块的数量和位置:
/* 内存仓库arena元信息 */
struct arena
{struct mem_block_desc *desc; // 此arena关联的mem_block_descuint32_t cnt;bool large; /* large为ture时,cnt表示的是页框数。否则cnt表示空闲mem_block数量 */
};
struct mem_block_desc、struct mem_block、struct arena 的关系:
struct mem_block_desc 描述了不同类型的小块,比如:4KB 页面划分成不同的小块,如 256 个 16B 小块、8 个 512B 的小块。512B 的小块对应一个 mem_block_desc,而 16B 的小块对应另一个。block_size 就是记录这个 mem_block_desc 用于描述哪种大小的小内存块,比如 512 或者 16。blocks_per_arena 用于记录一个页面拆分成了多少个小块,比如 8 个或者 256 个。free_list 用于管理可以分配的小块,也就是用于将可以分配的小块形成链表。
struct mem_block 其实本意是用来描述这个由 4KB 页面二次划分而成的固定小块,但是作者为了实现更通用的管理逻辑,所以这个结构体里面只包含了一个用于管理这个空闲小块的链表节点。
struct arena 用于描述这个 arena,desc 用于指向这个管理这种 arena 的 mem_block_desc 结构体,cnt 的值意义取决于 large 的值,如果 large = true,那么表示本 arena 占用的页框数目,否则表示本 arena 中还有多少空闲小内存块可用。需要注意的是,一个 mem_block_desc 对应的 arena 数量可不止一个,其实很好理解,当一个 arena 的小内存块分配完毕,我们就要再分配一个新的页充当 arena 然后划分成固定大小的小块。
这三个个结构体关系如图(来自《操作系统真象还原》 第十二章 进一步完善内核-CSDN 博客):
内核内存池
struct mem_block_desc k_block_descs[DESC_CNT]; // 内核内存块描述符数组
用户内存池
在 pcb 中增加字段
/* 进程或线程的pcb,程序控制块, 此结构体用于存储线程的管理信息*/
struct task_struct
{uint32_t *self_kstack; // 用于存储线程的栈顶位置,栈顶放着线程要用到的运行信息pid_t pid;enum task_status status;uint8_t priority; // 线程优先级char name[16]; // 用于存储自己的线程的名字uint8_t ticks; // 线程允许上处理器运行还剩下的滴答值,因为priority不能改变,所以要在其之外另行定义一个值来倒计时uint32_t elapsed_ticks; // 此任务自上cpu运行后至今占用了多少cpu嘀嗒数, 也就是此任务执行了多久*/struct list_elem general_tag; // general_tag的作用是用于线程在一般的队列(如就绪队列或者等待队列)中的结点struct list_elem all_list_tag; // all_list_tag的作用是用于线程队列thread_all_list(这个队列用于管理所有线程)中的结点uint32_t *pgdir; // 进程自己页表的虚拟地址struct virtual_addr userprog_vaddr; // 用户进程的虚拟内存池struct mem_block_desc u_block_desc[DESC_CNT]; // 用户进程内存块描述符uint32_t stack_magic; // 如果线程的栈无限生长,总会覆盖地pcb的信息,那么需要定义个边界数来检测是否栈已经到了PCB的边界
};
初始化
对于函数如下,初始化不同描述符
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;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; // 来到下一个}
}
内核直接 mem_init
中调用初始化,进程在 process_execute
中调用初始化
实现 sys_malloc
接下来实现 sys_malloc,首先是两个辅助函数,分别用来返回 arena 中第 x 个内存块地址,以及内存块对应 arena 的地址
/* 返回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; // 用于存储mem_block_desc数组地址struct task_struct *cur_thread = running_thread();/* 判断用哪个内存池*/if (cur_thread->pgdir == NULL){// 内核线程PF = PF_KERNEL;mem_pool = &kernel_pool;pool_size = mem_pool->pool_size;descs = k_block_descs;}else{// 用户进程PF = PF_USER;mem_pool = &user_pool;pool_size = mem_pool->pool_size;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);if (size > PG_SIZE){// 如果大小大于页框,单独考虑uint32_t page_cnt = DIV_ROUND_UP(size + sizeof(struct arena), PG_SIZE);a = malloc_page(PF, page_cnt);if (a != NULL){/* 对于分配的大块页框,将desc置为NULL, cnt置为页框数,large置为true */a->desc = NULL;a->large = true;a->cnt = page_cnt;memset(a, 0, page_cnt * PG_SIZE);lock_release(&mem_pool->lock);return (void *)(a + 1);}else{lock_release(&mem_pool->lock);return NULL;}}else {// 若申请的内存小于等于1024,可在各种规格的mem_block_desc中去适配uint8_t desc_idx;// 找到最合适的大小for (desc_idx = 0; desc_idx < DESC_CNT; desc_idx++){if (size <= descs[desc_idx].block_size){break;}}// 若mem_block_desc的free_list中已经没有可用的mem_block,就创建新的arena提供mem_block 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->large = false;a->cnt = descs[desc_idx].blocks_per_arena;uint32_t block_idx;enum intr_status old_status = intr_disable();/* 开始将arena拆分成内存块,并添加到内存块描述符的free_list中 */for (block_idx = 0; block_idx < a->cnt; block_idx++){b = arena2block(a, block_idx);ASSERT(!elem_find(&a->desc->free_list, &b->free_elem));list_append(&descs[desc_idx].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所在的arenaa->cnt--;lock_release(&mem_pool->lock);return (void *)b;}
}
-
4-25 行先判断是内核线程还是用户进程,然后分别赋予不同的 pf,mem_poll,poll_size
-
35-54 行是申请内存块大于 1024 的情况,此时我们按页框分配
-
58 行开始处理不同的内存块
- 61-67 行先找出最合适的内存块
- 69-92 行处理 mem_block_desc 的 free_list 中已经没有可用的 mem_block 的情况,新建 arena 提供 mem_block,首先先申请新的内存页来当 arena,然后给该结构体附上对应的值,再处理 mem_block,都加入到描述符指向的链表中
- 然后从链表中取出来进行分配即可
main.c 测试代码如下:
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall-init.h"
#include "syscall.h"
#include "stdio.h"
#include "memory.h"void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);int main(void) {put_str("I am kernel\n");init_all();intr_enable();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) { char* para = arg;void* addr = sys_malloc(33);console_put_str(" I am thread_a, sys_malloc(33), addr is 0x");console_put_int((int)addr);console_put_char('\n');while(1);
}/* 在线程中运行的函数 */
void k_thread_b(void* arg) { char* para = arg;void* addr = sys_malloc(63);console_put_str(" I am thread_b, sys_malloc(63), addr is 0x");console_put_int((int)addr);console_put_char('\n');while(1);
}/* 测试用户进程 */
void u_prog_a(void) {char* name = "prog_a";printf(" I am %s, my pid:%d%c", name, getpid(),'\n');while(1);
}/* 测试用户进程 */
void u_prog_b(void) {char* name = "prog_b";printf(" I am %s, my pid:%d%c", name, getpid(), '\n');while(1);
}
成功申请到内存,并且计算一下和预期内存是一样的,申请的是 33 和 63,所以按照 64(0x40)给,最后的 c 是 arena 的大小,详见书 P554
实现 sys_free
内存页释放
内存块的释放是基于页面的——假如所有内存块都空闲,则直接将该页面释放,否则就只是将该内存块插入到空闲链表中
因此我们首先需要构建内存页的释放,内存页的释放是内存页分配的逆过程
- 在物理内存池中释放物理内存页(只需将位图置为 0 即可)
- 清除页表中的页表项,即清除掉虚拟内存和物理内存的映射关系
- 在虚拟内存池中释放虚拟内存页(只需将位图置为 0 即可)
释放物理内存页
其实就是将物理位置对应的位图项置为 0
// 将物理地址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);
}
删除映射关系
其实就是将 pte 的 p 位 置 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位置0asm volatile("invlpg %0" ::"m"(vaddr) : "memory"); // 更新tlb
}
释放虚拟内存页
在虚拟内存页对应的位图中将对应位 置 0 即可。
// 在虚拟地址池中释放以_vaddr起始的连续pg_cnt个虚拟页地址
static void vaddr_remove(enum pool_flags pf, void *_vaddr, uint32_t pg_cnt)
{uint32_t vaddr_start = (uint32_t)_vaddr;uint32_t bit_idx_start = 0, cnt = 0;if (pf == PF_USER){struct task_struct *cur_thread = running_thread();bit_idx_start = (vaddr_start - 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);}else{bit_idx_start = (vaddr_start - kernel_vaddr.vaddr_start) / PG_SIZE;while (cnt < pg_cnt)bitmap_set(&kernel_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); // 获取虚拟地址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) { // 位于user_pool内存池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);page_cnt++;}/* 清空虚拟地址的位图中的相应位 */vaddr_remove(pf, _vaddr, pg_cnt);} else { // 位于kernel_pool内存池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
在释放内存块时,如果释放的内存块大小超过 1024,则直接释放内存页,否则释放内存块,再看该 arena 的内存块是不是都属于空闲状态,是的话就将整个内存页释放
/* 回收内存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;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_listlist_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); } } lock_release(&mem_pool->lock); }
}
测试代码如下:
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall-init.h"
#include "syscall.h"
#include "stdio.h"
#include "memory.h"void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);int main(void) {put_str("I am kernel\n");init_all();intr_enable();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) { char* para = arg;void* addr1;void* addr2;void* addr3;void* addr4;void* addr5;void* addr6;void* addr7;console_put_str(" thread_a start\n");int max = 1000;while (max-- > 0) {int size = 128;addr1 = sys_malloc(size); size *= 2; addr2 = sys_malloc(size); size *= 2; addr3 = sys_malloc(size);sys_free(addr1);addr4 = sys_malloc(size);size *= 2; size *= 2; size *= 2; size *= 2; size *= 2; size *= 2; size *= 2; addr5 = sys_malloc(size);addr6 = sys_malloc(size);sys_free(addr5);size *= 2; addr7 = sys_malloc(size);sys_free(addr6);sys_free(addr7);sys_free(addr2);sys_free(addr3);sys_free(addr4);}console_put_str(" thread_a end\n");while(1);
}/* 在线程中运行的函数 */
void k_thread_b(void* arg) { char* para = arg;void* addr1;void* addr2;void* addr3;void* addr4;void* addr5;void* addr6;void* addr7;void* addr8;void* addr9;int max = 1000;console_put_str(" thread_b start\n");while (max-- > 0) {int size = 9;addr1 = sys_malloc(size);size *= 2; addr2 = sys_malloc(size);size *= 2; sys_free(addr2);addr3 = sys_malloc(size);sys_free(addr1);addr4 = sys_malloc(size);addr5 = sys_malloc(size);addr6 = sys_malloc(size);sys_free(addr5);size *= 2; addr7 = sys_malloc(size);sys_free(addr6);sys_free(addr7);sys_free(addr3);sys_free(addr4);size *= 2; size *= 2; size *= 2; addr1 = sys_malloc(size);addr2 = sys_malloc(size);addr3 = sys_malloc(size);addr4 = sys_malloc(size);addr5 = sys_malloc(size);addr6 = sys_malloc(size);addr7 = sys_malloc(size);addr8 = sys_malloc(size);addr9 = sys_malloc(size);sys_free(addr1);sys_free(addr2);sys_free(addr3);sys_free(addr4);sys_free(addr5);sys_free(addr6);sys_free(addr7);sys_free(addr8);sys_free(addr9);}console_put_str(" thread_b end\n");while(1);
}/* 测试用户进程 */
void u_prog_a(void) {char* name = "prog_a";printf(" I am %s, my pid:%d%c", name, getpid(),'\n');while(1);
}/* 测试用户进程 */
void u_prog_b(void) {char* name = "prog_b";printf(" I am %s, my pid:%d%c", name, getpid(), '\n');while(1);
}
详情见书 P561
封装用户接口
这部分很简单,封装接口即可
/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");
}
/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
/* 申请size字节大小的内存,并返回结果 */
void *malloc(uint32_t size)
{return (void *)_syscall1(SYS_MALLOC, size);
}/* 释放ptr指向的内存 */
void free(void *ptr)
{_syscall1(SYS_FREE, ptr);
}
测试代码 main.c 如下:
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall-init.h"
#include "syscall.h"
#include "stdio.h"
#include "memory.h"void k_thread_a(void*);
void k_thread_b(void*);
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 = 9999999;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 = 999999;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);
}
没有问题