操作系统 真象还原 学习笔记#12

第十二章 进一步完善内核

1.系统调用

系统调用就是让用户进程申请操作系统的帮助,让操作系统帮其完成某项工作,也就是相当于用户进程调用了操作系统的功能,因此“系统调用“准确地来说应该被称为“操作系统功能调用。

Linux 系统调用是用中断门来实现的,通过软中断指令int 来主动发起中断信号。由于要支持的系统功能很多,总不能一个系统功能调用就占用一个中断向量,真要是这样的话整个中断描述符表都不够用呢。Linux 只占用一个中断向量号,即0x80, 处理器执行指令int 0x80 时便触发了系统调用。为了让用户程序可以通过这一个中断门调用多种系统功能,在系统调用之前, Linux 在寄存器eax 中写入子功能号,例如系统调用open 和close 都是不同的子功能号,当用户程序通过int0x80 进行系统调用时,对应的中断处理例程会根据eax 的值来判断用户进程申请哪种系统调用。

宏_syscall 和库函数syscall 相比, syscall 实现更灵活,对用户来说任何参数个数的系统调用都统一用一种形式,用户只要记住syscall 就可以了,而宏_syscall 的实现比较死板,针对每种参数个数的系统调用都要有单独的形式,因此支持的参数数量必然有限,而且用户要记住7 种形式_syscall[0-6] ,调用时除了输入实参外,还要输入实参的类型,确实有些麻烦,此外这个宏会引发安全漏洞 ,故必然会被syscall 取代。

2.系统调用的实现

一个系统功能调用分为两部分, 一部分是暴露给用户进程的接口函数,它属于用户空间,此部分只是用户进程使用系统调用的途径,只负责发需求。另一部分是与之对应的内核具体实现,它属于内核空间,此部分完成的是功能需求,就是我们一直所说的系统调用子功能处理函数。

请添加图片描述

系统调用的实现思路:

实现系统调用的整体框架

准备用户程序的系统调用入口,也就是用于触发int 0x80的程序。(syscall)

准备0x80软中断对应的中断门描述符(interrupt)

编写汇编版用来处理系统调用的中断处理程序(kernel)

实现类似于中断机制中那种汇编代码跳入c中断处理程序的机制(syscall-init)

编写第一个系统调用函数getpid

首先编写内核中对应的处理函数sys_getpid和初始化系统调用(syscall-init)

在struct task_ struct 中添加了成员pid,并在线进程中为这个成员赋值(thread)

写一个用户程序进行系统调用的入口 (syscall)

由于系统调用机制是基于中断机制,所以系统调用的流程和中断流程非常相似

代码实现:

首先准备用户程序的系统调用接口,也就是触发int 0x80的程序

#include "syscall.h"

/* 无参数的系统调用 */   得到pid号就不需要传参
#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;						       \
})

系统调用号存在eax中,往外的返回值也放在eax中

然后准备0x80软中断对应的中断门描述符

#define IDT_DESC_CNT 0x81      // 目前总共支持的中断数,最后一个支持的中断号0x80 + 1

extern uint32_t syscall_handler(void);    //定义的汇编中断处理程序代码

//此函数用来循环调用make_idt_desc函数来完成中断门描述符与中断处理函数映射关系的建立,传入三个参数:中断描述符表某个中段描述符(一个结构体)的地址
//属性字段,中断处理函数的地址
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");
}

汇编版用来处理系统调用的中断处理程序

;;;;;;;;;;;;;;;;   0x80号中断   ;;;;;;;;;;;;;;;;
[bits 32]
extern syscall_table            ;如同之前我们中断处理机制中引入了C中定义的中断处理程序入口地址表一样,这里引入了C中定义的系统调用函数入口地址表
section .text
global syscall_handler
syscall_handler:
                                ;1 保存上下文环境,为了复用之前写好的intr_exit:,所以我们仿照中断处理机制压入的东西,构建系统调用压入的东西
   push 0			            ; 压入0, 使栈中格式统一
   push ds
   push es
   push fs
   push gs
   pushad			            ; PUSHAD指令压入32位寄存器,其入栈顺序是:EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI  
   push 0x80			        ; 此位置压入0x80也是为了保持统一的栈格式

                                ;2 为系统调用子功能传入参数,由于这个函数是3个参数的用户程序系统调用入口都会使用
                                ; 所以我们为了格式统一,直接按照最高参数数量压入3个参数

   push edx			            ; 系统调用中第3个参数
   push ecx			            ; 系统调用中第2个参数
   push ebx			            ; 系统调用中第1个参数

                                ;3 调用c中定义的功能处理函数

   call [syscall_table + eax*4]	    ; 编译器会在栈中根据C函数声明匹配正确数量的参数
   add esp, 12			        ; 跨过上面的三个参数

                                ;4 将call调用后的返回值存入待当前内核栈中eax的位置,c语言会自动把返回值放入eax中(c语言的ABI规定)

   mov [esp + 8*4], eax	
   jmp intr_exit		        ; intr_exit返回,恢复上下文

我们实现类似于中断机制中那种汇编代码跳入c中断处理程序的机制,这样我们就能用c管理系统调用(定义数组)

至此,我们的系统调用机制就已经构建完成,以后我们只需要将c写好的系统调用函数地址放入这个数组就行了

要实现的第一个系统调用是getpid, getpid 的功能是获取任务自己的getpid 是给用户进程使
用的接口函数, 它在内核中对应的处理函数是sys_getpid

typedef uint16_t pid_t;

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的边界
};

pcb有了这个pid这个成员,那么自然我们创建进程/线程的时候要去为这个成员赋值

#include "sync.h"

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

/* 初始化线程基本信息 , pcb中存储的是线程的管理信息,此函数用于根据传入的pcb的地址,线程的名字等来初始化线程的管理信息*/
void init_thread(struct task_struct* pthread, char* name, int prio) {
   memset(pthread, 0, sizeof(*pthread));                                //把pcb初始化为0
   pthread->pid = allocate_pid();
   strcpy(pthread->name, name);                                         //将传入的线程的名字填入线程的pcb中

   if(pthread == main_thread){
      pthread->status = TASK_RUNNING;     //由于把main函数也封装成一个线程,并且它一直是运行的,故将其直接设为TASK_RUNNING */  
   } 
   else{
      pthread->status = TASK_READY;
   }
   pthread->priority = prio;            
                                                                        /* self_kstack是线程自己在内核态下使用的栈顶地址 */
   pthread->ticks = prio;
   pthread->elapsed_ticks = 0;
   pthread->pgdir = NULL;	//线程没有自己的地址空间,进程的pcb这一项才有用,指向自己的页表虚拟地址	
   pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE);     //本操作系统比较简单,线程不会太大,就将线程栈顶定义为pcb地址
                                                                        //+4096的地方,这样就留了一页给线程的信息(包含管理信息与运行信息)空间
   pthread->stack_magic = 0x19870916;	                                // /定义的边界数字,随便选的数字来判断线程的栈是否已经生长到覆盖pcb信息了              
}

/* 初始化线程环境 */
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");
}

在系统中安装第一个系统调用——getpid

#include "syscall-init.h"
#include "syscall.h"
#include "stdint.h"
#include "print.h"
#include "thread.h"

/* 返回当前任务的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;   /* 相当于0号系统调用是getpid */
   put_str("syscall_init done\n");
}


(1) 在syscall.h 中的结构enum SYSCALL_NR 里添加新的子功能号。
(2) 在syscall.c 中增加系统调用的用户接口。
(3) 在syscall-init.c 中定义子功能处理函数并在syscall_ tabIe 中注册。

一定要区分作为实际系统调用处理函数的sys_getpid与作为用户程序入口的getpid,前者是运行在内核态的,后者是用户态程序的入口去执行int 0x80的。

编写main函数测试代码

#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 = getpid();
   while(1);
}

/* 测试用户进程 */
void u_prog_b(void) {
   prog_b_pid = getpid();
   while(1);
}

bin/bochs -f bochsrc.disk、

请添加图片描述

3.让用户进程“说话”

write 接受3 个参数, 其中的fd 是文件描述符, buf 是被输出数据所在的缓冲区, count 是输出的字符数, write 的功能是把buf 中count 个字符写到文件描述符fd 指向的文件中。

上一节系统调用的三步:

(1) 在syscall.h 中的结构enum SYSCALL_NR 里添加新的子功能号。
(2) 在syscall.c 中增加系统调用的用户接口。
(3) 在syscall-init.c 中定义子功能处理函数并在syscall_ tabIe 中注册。

首先增加write系统调用调用号

enum SYSCALL_NR {
   	SYS_GETPID,
   	SYS_WRITE
};

然后,我们实现write系统调用的用户程序入口。

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

声明write系统调用的用户程序入口

uint32_t write(char* str);

实现真正的系统调用执行函数,并将其添加到系统调用表中

#include "console.h"
#include "string.h"

/* 打印字符串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");
}

由于现在我们开启了页表机制,任何地址都将视作虚拟地址。我们之前编写print.S时,由于是给内核用的,所以用于与显存段打交道的地址有些是借助于内核页目录表0号项进行寻址的,现在我们将print共享给了用户进程,而用户进程无法去访问内核页目录表0号项。但是由于进程页目录表768号项与内核页目录表0号项指向同一张内核的页表(因为进程页目录表768号项就是拷贝的内核页目录表768号项)。所以我们能通过进程页目录表768号项访问原来通过内核页目录表0号项访问的地址。所以,我们需要修改print.S中的一些地址访问,将其升高3G,这样才能让原本通过内核页目录表0号项访问的地址,现在能通过进程页目录表768号项访问。

.roll_screen:				                                ; 若超出屏幕大小,开始滚屏
    cld                                                     
    mov ecx, 960				                            ; 一共有2000-80=1920个字符要搬运,共1920*2=3840字节.一次搬4字节,共3840/4=960次 
    ;mov esi, 0xb80a0			                           
	mov esi, 0xc00b80a0										; 第1行行首
    ;mov edi, 0xb8000			                            
	mov edi, 0xc00b8000										; 第0行行首
    rep movsd				                                ;rep movs word ptr es:[edi], word ptr ds:[esi] 简写为: rep movsw

  • 可变参数

    在格式化字符串中的字符%'便是在栈中寻找可变参数的依据,紧跟’%‘后面的是类型字符,类型字符表示数据类型和进制相关的内容。格式化字符串中有多少'%',就在栈中找多少次参数,总之正常情况下,用户传入可变参数的数量应与format 中字符%'的数量匹配,以format 中的%'作为参数的线索,每找到一个’%',就到栈中去找一次参数。

可变参数依靠的是编译器的特性,其原理的核心就是,调用者依据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) {     								// 如果余数是0~9
      	*((*buf_ptr_addr)++) = m + '0';	  			// 将数字0~9转换为字符'0'~'9'
   	} 
	else {	      									// 否则余数是A~F
      	*((*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;
	while(index_char) {
		if (index_char != '%') {
			*(buf_ptr++) = index_char;
			index_char = *(++index_ptr);
			continue;
		}
		index_char = *(++index_ptr);	 			// 得到%后面的字符
		switch(index_char) {
		case 'x':
			arg_int = va_arg(ap, int);
			itoa(arg_int, &buf_ptr, 16); 	
			index_char = *(++index_ptr); 			// 跳过格式字符并更新index_char
			break;
		}
	}
	return strlen(str);
}

/* 格式化输出字符串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); 
}

目前只写了一个16进制输出来支持,然后写一个main函数来测试

#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, "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');
   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');
   while(1);
}

/* 测试用户进程 */
void u_prog_a(void) {
   printf(" prog_a_pid:0x%x\n", getpid());
   while(1);
}

/* 测试用户进程 */
void u_prog_b(void) {
   printf(" prog_b_pid:0x%x\n", getpid());
   while(1);
}


bin/bochs -f bochsrc.disk

可以看到输出符合预期

请添加图片描述

接下来实现print函数的其他功能:

#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) {     								// 如果余数是0~9
      	*((*buf_ptr_addr)++) = m + '0';	  			// 将数字0~9转换为字符'0'~'9'
   	} 
	else {	      									// 否则余数是A~F
      	*((*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;
	while(index_char) {
		if (index_char != '%') {
			*(buf_ptr++) = index_char;
			index_char = *(++index_ptr);
			continue;
		}
		index_char = *(++index_ptr);	 			// 得到%后面的字符
		switch(index_char) {
		case 'x':
			arg_int = va_arg(ap, int);
			itoa(arg_int, &buf_ptr, 16); 	
			index_char = *(++index_ptr); 			// 跳过格式字符并更新index_char
			break;
		}
	}
	return strlen(str);
}

/* 格式化输出字符串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); 
}

实现很简单,再写一个main函数测试

#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, "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');
   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');
   while(1);
}

/* 测试用户进程 */
void u_prog_a(void) {
   printf(" prog_a_pid:0x%x\n", getpid());
   while(1);
}

/* 测试用户进程 */
void u_prog_b(void) {
   printf(" prog_b_pid:0x%x\n", getpid());
   while(1);
}

bin/bochs -f bochsrc.disk

可以看到结果符合预期

请添加图片描述

4.数据结构的准备

首先实现用户页的分配

/* 在pf表示的虚拟内存池中申请pg_cnt个虚拟页,
 * 成功则返回虚拟页的起始地址, 失败则返回NULL */
static void* vaddr_get(enum pool_flags pf, uint32_t pg_cnt) {
   	int vaddr_start = 0, bit_idx_start = -1;
   	uint32_t cnt = 0;
   	if (pf == PF_KERNEL) {
      	bit_idx_start  = bitmap_scan(&kernel_vaddr.vaddr_bitmap, pg_cnt);
      	if (bit_idx_start == -1) {
	 		return NULL;
      	}
      	while(cnt < pg_cnt) {
	 		bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1);
      	}
      	vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
   	} 
	else {	     // 用户内存池	
      	struct task_struct* cur = running_thread();
      	bit_idx_start  = bitmap_scan(&cur->userprog_vaddr.vaddr_bitmap, pg_cnt);
      	if (bit_idx_start == -1) {
	 		return NULL;
    	}
   		while(cnt < pg_cnt) {
	 		bitmap_set(&cur->userprog_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1);
     	}
      	vaddr_start = cur->userprog_vaddr.vaddr_start + bit_idx_start * PG_SIZE;

   		/* (0xc0000000 - PG_SIZE)做为用户3级栈已经在start_process被分配 */
      	ASSERT((uint32_t)vaddr_start < (0xc0000000 - PG_SIZE));
   }
   return (void*)vaddr_start;
}

之前,我们的内存管理是:1、只有分配没有释放;2、以页为单位;3、只能内核态使用;现在我们完善内存管理:1、实现释放机制;2、更细粒度的管理单位;3、用户态也可以使用;

(1)释放机制的实现很简单,是分配机制的逆操作;

(2)更细粒度的内存管理单位,需要依靠arena(竞技场)模型的理解与实现。在这个模型中,我们先申请一个完整的4KB页面,然后将这个4KB页面划分成不同的小块,如256个16B小块、8个512B的小块,然后这些独立的小块就成了分配与释放的基本单位;

(3)用户态使用,直接使用系统调用机制。

首先,我们来进行底层数据结构的建立:

#include "list.h"

/* 内存块 */
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链表
};

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

添加arena结构体

/* 内存仓库arena元信息 */
struct arena {
   struct mem_block_desc* desc;	 // 此arena关联的mem_block_desc
   uint32_t cnt;
   bool large;		   /* large为ture时,cnt表示的是页框数。否则cnt表示空闲mem_block数量 */
};

初始化管理内核不同种类型arena的不同mem_block_desc

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

//初始化管理不同种类型arena的不同mem_block_desc
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;         // 更新为下一个规格内存块
   }
}

/* 内存管理部分初始化入口 */
void mem_init() {
   put_str("mem_init start\n");
   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");
}

进程中也应该有自己的mem_block_desc数组

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的边界
};

在一个4KB页中,头12B用来存放内存仓库arena员信息,其中desc用于指向管理结构体mem_block_desc 这个结构体中又有三个成员,block_size用来指明一个mem_block的大小;block_per_arena 用来指明一个arena(这个指代有两个意思,一个是代表4KB页面分配的一整个arena,一个是代表最开始的结构中的arena元信息)被分成了几个mem_block ; freelist用来为每个被分成的内存块中建立双向链表。 回到arena,cnt的意义取决于large的值,如果large = true,那么表示本arena占用的页框数目,否则表示本arena中还有多少空闲小内存块可用。因为这个arena 是才创建的,所以large为false,cnt=7表示还有7个内存块可以使用
请添加图片描述

现在我们来编写能够与arena模型配合的sys_malloc,用于真正进行内存分配

这个函数实现了许多功能

  1. 判断使用哪个内存池

根据当前线程是否为内核线程,选择使用内核内存池或用户内存池:

  • 内核线程:使用 kernel_pool
  • 用户进程:使用 user_pool
  1. 检查申请的内存大小

如果申请的内存大小不在内存池的容量范围内,直接返回 NULL

  1. 分配大块内存(大于1024字节)

如果申请的内存大于1024字节:

  • 计算需要的页框数。
  • 调用 malloc_page 分配页框。
  • 初始化分配的内存,并设置 arena 结构体的相关字段。
  • 返回分配的内存地址。
  1. 分配小块内存(小于等于1024字节)

如果申请的内存小于等于1024字节:

  • 在内存块描述符数组中找到合适的内存块规格。
  • 如果对应的内存块描述符的空闲列表为空,分配一个新的 arena 并将其拆分成内存块,添加到空闲列表中。
  • 从空闲列表中取出一个内存块,更新 arena 的空闲内存块数。
  • 返回分配的内存块地址。
  1. 辅助函数
  • arena2block:根据 arena 和索引计算内存块的地址。
  • block2arena:根据内存块地址计算其所在的 arena 地址。
#include "interrupt.h"

/* 返回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; 
		pool_size = kernel_pool.pool_size;
		mem_pool = &kernel_pool;
		descs = k_block_descs;
	} 
	else {				      // 用户进程pcb中的pgdir会在为其分配页表时创建
		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) {
			memset(a, 0, page_cnt * PG_SIZE);	 // 将分配的内存清0  

			/* 对于分配的大块页框,将desc置为NULL, cnt置为页框数,large置为true */
			a->desc = NULL;
			a->cnt = page_cnt;
			a->large = true;
			lock_release(&mem_pool->lock);
			return (void*)(a + 1);		 // 跨过arena大小,把剩下的内存返回
		} 
		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);       // 分配1页框做为arena
			if (a == NULL) {
				lock_release(&mem_pool->lock);
				return NULL;
			}
			memset(a, 0, PG_SIZE);

			/* 对于分配的小块内存,将desc置为相应内存块描述符, 
			* cnt置为此arena可用的内存块数,large置为false */
			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 < 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中的空闲内存块数减1
		lock_release(&mem_pool->lock);
		return (void*)b;
	}
}

然后是测试代码,修改main

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

bin/bochs -f bochsrc.disk

可以看到结果符合预期

请添加图片描述

接下来实现内存释放的功能

接下来实现页级别的内存回收,页回收是页分配的逆操作:1、清除物理内存池中位图的位;2、清除虚拟地址对应的页表表项;3、清除虚拟内存池中位图的位;

  1. pfree 函数

这个函数将物理地址 pg_phy_addr 回收到物理内存池中,实质上是清除物理内存池中位图的相应位:

  • 判断内存池类型:根据物理地址判断是用户物理内存池还是内核物理内存池。
  • 计算位图索引:根据物理地址计算出在位图中的索引。
  • 清除位图位:将位图中对应的位清零,表示该物理页已空闲。
  1. page_table_pte_remove 函数

这个函数去掉页表中虚拟地址 vaddr 的映射,只去掉 vaddr 对应的页表项(PTE):

  • 获取页表项指针:通过 pte_ptr 函数获取虚拟地址对应的页表项指针。
  • 清除页表项:将页表项的存在位(P位)清零。
  • 刷新TLB:通过 invlpg 指令刷新TLB,确保页表修改生效。
  1. vaddr_remove 函数

这个函数在虚拟地址池中释放以 _vaddr 起始的连续 pg_cnt 个虚拟页地址,实质上是清除虚拟内存池位图的相应位:

  • 计算位图起始索引:根据虚拟地址计算出在位图中的起始索引。
  • 清除位图位:将位图中对应的位清零,表示该虚拟页已空闲。
  1. mfree_page 函数

这个函数释放以虚拟地址 vaddr 为起始的 pg_cnt 个物理页框:

  • 获取物理地址:通过 addr_v2p 函数获取虚拟地址对应的物理地址。
  • 检查地址有效性:确保物理地址在合法范围内。
  • 判断内存池类型:根据物理地址判断是用户物理内存池还是内核物理内存池。
  • 循环释放页框:逐个释放物理页框,清除页表项,并更新位图。
//将物理地址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);	 // 将位图中该位清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;
		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);  // 获取虚拟地址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);
	}
}

  1. 检查指针有效性

首先,函数通过 ASSERT(ptr != NULL) 确保传入的指针 ptr 非空。

  1. 判断内存池类型

根据当前线程是否为内核线程,选择使用内核内存池或用户内存池:

  • 内核线程:使用 kernel_pool
  • 用户进程:使用 user_pool
  1. 获取内存块和 arena

通过 block2arena 函数,将内存块指针 b 转换为其所属的 arena 结构体指针 a,以获取相关元信息。

  1. 回收大块内存(大于1024字节)

如果 arena 的描述符 descNULLlarge 标志为 true,表示这是一个大块内存:

  • 调用 mfree_page 函数释放对应的页框。
  1. 回收小块内存(小于等于1024字节)

如果 arena 的描述符 desc 不为 NULLlarge 标志为 false,表示这是一个小块内存:

  • 将内存块添加到 free_list 中。
  • 检查 arena 中的所有内存块是否都已空闲,如果是,则释放整个 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_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); 
			} 
		}   
		lock_release(&mem_pool->lock); 
	}
}

写一个main函数测试 创建两个线程,分别循环1000次,每个循环中多次调用sys_malloc和sys_free,分别申请和释放不同大小的内存。在循环开始前,分别打印出thread_[ab]start ,在结束时分别打印thread_[ab]end 可以看到结果符合预期

最后实现malloc 和 free的系统调用,借助于之前系统调用框架的三步:

首先在syscall.h中增加系统调用号

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

然后在syscall.c 中增加用户调用函数的接口

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

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

然后再系统调用初始化函数syscall_init中注册系统调用函数

#include "memory.h" 

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

最后写一个main函数来测试

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

bin/bochs -f bochsrc.disk

用户进程prog_a 三次malloc 调用,返回的地址分别是0x804800c 、0x804810c 、0x804820c, 它们各相差0x100, 也就是差256 字节,用户进程prog_b 与它相同,原因是用户进程独享内存空间,虚拟地址并不冲突。线程thread_a 调用sys_malloc 后返回的地址分别是0xc013400c 、0xc013410c 、0xc013420c, 地址之间也是相差0x100, 即256 字节。下面的线程thread_b 同样调用sys_malloc, 返回的
地址出现了累加的现象,在k_thread_a 地址分配的基础上,从0xc013430c 开始,然后依次是0xc013440c和0xc013450c, 原因是内核线程共享内存空间,虚拟地址必须唯一。测试符合预期

请添加图片描述

本章小结

本章进一步完善了内核。主要实现了系统调用的整体框架,在此之上实现了更细粒度的内存管理机制,并实现了内存分配(malloc)和释放(free)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值