《操作系统真象还原》第十二章 进一步完善内核

第十二章 进一步完善内核

本文是对《操作系统真象还原》第十二章学习的笔记,欢迎大家一起交流,目前所有代码已托管至 fdx-xdf/MyTinyOS 。

实现系统调用 getpid

前置知识

系统调用(System Call)是操作系统提供给应用程序访问硬件资源和操作系统服务的接口。通过系统调用,程序可以向操作系统请求服务,如文件操作、内存管理、进程控制等。系统调用位于用户态和内核态之间,通常被用来执行用户程序无法直接完成的任务。

linux 系统调用是通过软中断实现的,并且 linux 系统调用产生的中断向量号只有一个,即 0x80​,也就是说,当处理器执行指令 int 0x80 ​时就触发了系统调用。

为了让用户程序可以通过这一个中断门调用多种系统功能,在系统调用之前,Linux 在寄存器 eax ​中写入子功能号,例如系统调用 open​ 和 close​ 都是不同的子功能号,当用户程序通过 int 0x80 ​进行系统调用时,对应的中断处理例程会根据 eax ​的值来判断用户进程申请哪种系统调用。

下面借助(《操作系统真象还原》第十二章(一) —— 系统调用 (jl-sky.github.io))里面的图来展示整个流程

image

image

完善系统调用框架

框架如上图所示,我们要做的任务如下:

  1. 构建系统调用所需的中断描述符
  2. 构建触发系统调用中断的转接口,该转接口的作用是将 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);
}

编译运行结果如下:

image

实现系统调用 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 中,流程图如下:

image

image

image

测试

#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);
}

编译测试如下:

image

完善堆内存管理

我们之前实现的内存管理都是很粗糙的,以 4kb 的页框为单位进行分配,我们现在要实现更细致的内存分配。

本节的主要任务有:

  1. 实现 sys_malloc
  2. 实现 sys_free
  3. 实现 malloc
  4. 实现 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 博客):

image

内核内存池

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;}
}
  1. 4-25 行先判断是内核线程还是用户进程,然后分别赋予不同的 pf,mem_poll,poll_size

  2. 35-54 行是申请内存块大于 1024 的情况,此时我们按页框分配

  3. 58 行开始处理不同的内存块

    1. 61-67 行先找出最合适的内存块
    2. 69-92 行处理 mem_block_desc 的 free_list 中已经没有可用的 mem_block 的情况,新建 arena 提供 mem_block,首先先申请新的内存页来当 arena,然后给该结构体附上对应的值,再处理 mem_block,都加入到描述符指向的链表中
    3. 然后从链表中取出来进行分配即可

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

image

实现 sys_free

内存页释放

内存块的释放是基于页面的——假如所有内存块都空闲,则直接将该页面释放,否则就只是将该内存块插入到空闲链表中

因此我们首先需要构建内存页的释放,内存页的释放是内存页分配的逆过程

  1. 在物理内存池中释放物理内存页(只需将位图置为 0 即可)
  2. 清除页表中的页表项,即清除掉虚拟内存和物理内存的映射关系
  3. 在虚拟内存池中释放虚拟内存页(只需将位图置为 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);
}

image

没有问题

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/878265.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深度学习(RNN,LSTM,GRU)

三个网络的架构图: RNN:LSTM:GRU:特性对比列表:特性RNNLSTMGRU门控数量无3门(输入/遗忘/输出)2门(更新/重置)记忆机制 仅隐藏状态ht显式状态Ct + 隐藏状态ht隐式记忆(通过门控更新状态)核心操作 直接状态传递门控细胞状态更新 门控候选状态混合 计算复杂度O(d2)(1组权…

AI定制祝福视频,广州塔、动态彩灯、LED表白,直播互动新玩法(附下载链接)

在追剧的时候经常能看到一些浪漫的告白桥段,男主用圣诞彩灯表白、用城市标志性建筑的LED表白,或者在五光十色的烟花绽放后刻下女主角的名字,充满了仪式感和氛围感~现在,这样的表白效果用AI软件就能实现了,在社交平台上甚至还出现了类似的直播内容,观众送热气球或者其他礼…

VMware ARIA缺陷,黑客可获用户权限,哪些版本受影响?

VMware发布了安全更新,以修补影响VMware ARIA操作和日志操作的五个安全缺陷,并警告客户,黑客可以利用他们获得提升的访问或获得敏感信息。发行 安全 更新要补丁五安全 缺陷 影响VMware ARIA操作和日志操作,警告客户攻击者可以开发他们提高了使用权或者获得 敏感的信息。 列…

1. 2025年:致每一位在软件测试道路上奋斗的伙伴

亲爱的读者朋友们: 新年好!时光荏苒,转眼间我们已经迈入2025年。在这辞旧迎新的时刻,我怀着无比感恩的心情,向一路相伴的每一位软件测试从业者、爱好者以及关注者们致以最诚挚的祝福!愿大家在新的一年里,健康平安,事业有成,代码无Bug,需求皆清晰! 过去的一年,是软件测试行业蓬勃…

执行npm run dev时,报错10% building 2/5 modules 3 active node,如何解决?

错误信息如下:原因:版本问题,为了不替换node版本使用如下方法 在package.json文件下 将 "dev": " vue-cli-service serve", "build:prod": "vue-cli-service build", "build:stage": "vue-cli-service build --mode…

Make your ternimal more useful

目录引入Iterm2配置和Zshell配置TmuxVim配置基本使用插件配置Coc默认配置快捷键说明NerdTree快捷键分屏:Buffer, Windows和Tab 引入 本着好程序员要用好终端的信念,加之在使用mac过程中对快捷键依赖度增加,对鼠标的依赖逐渐减少,所以打算尝试配置终端的代码编写环境。 不曾…

龙哥量化:通达信技术指标编写技巧分享篇1-成交量和换手率

龙哥微信:Long622889代写通达信技术指标、选股公式(通达信,同花顺,东方财富,大智慧,文华,博易,飞狐)代写期货量化策略(TB交易开拓者,文华8,金字塔) 春节假期, 和朋友闲聊,发现在选股思路上很杂乱, 完全没有体系,但是大致可以分为两种,趋势策略和震荡策略,其…

昆明理工大学材料科学与工程学院 2025年硕士研究生招生预测调剂名额 (供考生提前规划)

亲爱的考生: 为助力各位考生提前规划考研调剂方向,昆明理工大学材料科学与工程学院结合近年招生趋势及学科发展需求,预测2025年材料工程相关专业将有部分调剂名额,具体信息如下。欢迎符合条件的考生持续关注! 一、预测调剂专业及名额注: 最终调剂名额以2025年研招网官方发…

hive-pig--pig安装

1.下载 curl https://dlcdn.apache.org/pig/pig-0.17.0/pig-0.17.0.tar.gz -o /opt/software/pig-0.17.0.tar.gz2.解压 tar -zxvf /opt/software/pig-0.17.0.tar.gz -C /usr/local/src/ mv /usr/local/src/pig-0.17.0/ /usr/local/src/pig 3.把二进制路径添加到命令行路径 echo…

PyTorch生态系统中的连续深度学习:使用Torchdyn实现连续时间神经网络

神经常微分方程(Neural ODEs)是深度学习领域的创新性模型架构,它将神经网络的离散变换扩展为连续时间动力系统。与传统神经网络将层表示为离散变换不同,Neural ODEs将变换过程视为深度(或时间)的连续函数。这种方法为机器学习开创了新的研究方向,尤其在生成模型、时间序…

[ArkUI] 记录一次 ArkUI 学习心得 (1) -- 基础概念

1.一个原生鸿蒙应用的源码目录其中:ets是项目的源码目录.ets/pages是页面目录, 用于渲染页面.resources是资源目录,下面会讲. 2.第一个原生鸿蒙应用 话不多说,直接上代码. @Entry @Component struct Index {@State message: string = My First Program!;@State num: number = 0…

互联网已经没法用了

图片:作者制作我们已经到了这样的地步——曾经能让我们随时随地获取全世界信息的互联网,现在已经完全没法用了。 罪魁祸首是广告,情况糟糕到一种极端的程度,以至于它被称为“广告末日”(adpocalypse)。 现在我打开的几乎每个网站都塞满了广告,整个页面都快撑爆了。在电脑…