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

配合视频学习体验更佳!
第一节:https://www.bilibili.com/video/BV1iu411j7Xj/?vd_source=701807c4f8684b13e922d0a8b116af31
第一节额外:https://www.bilibili.com/video/BV1d14y1X7xe/?vd_source=701807c4f8684b13e922d0a8b116af31
第二节:https://www.bilibili.com/video/BV1wW4y1Z7EQ/?vd_source=701807c4f8684b13e922d0a8b116af31
第三节:https://www.bilibili.com/video/BV12W4y1o7jH/?vd_source=701807c4f8684b13e922d0a8b116af31
第四节:https://www.bilibili.com/video/BV1sP411r7XQ/?vd_source=701807c4f8684b13e922d0a8b116af31
第五节:https://www.bilibili.com/video/BV1km4y1L7Yt/?vd_source=701807c4f8684b13e922d0a8b116af31
第六节:https://www.bilibili.com/video/BV1tV4y1t7ns/?vd_source=701807c4f8684b13e922d0a8b116af31
第七节:https://www.bilibili.com/video/BV1Eu4y1U7P5/?vd_source=701807c4f8684b13e922d0a8b116af31
第八节:https://www.bilibili.com/video/BV1Uc411c75V/?vd_source=701807c4f8684b13e922d0a8b116af31

代码仓库:https://github.com/xukanshan/the_truth_of_operationg_system

为了方便在操作系统上的应用程序开发,操作系统必须提供一系列接口供程序调用,这就是我们所说的系统调用。系统调用允许用户程序请求操作系统的服务。现在主流操作系统实现系统调用的方法都是利用中断机制,所以在实现系统调用之前,我们需要复习一下这个操作系统上中断机制的运作流程:

  1. 当外部设备有事件发生时,会通过中断代理向CPU发送一个带有号码的中断信号;
  2. CPU会根据传入的中断号码并结合IDTR寄存器中指向的IDT表找到对应这个中断号码的中断门描述符;
  3. 从上一步的中断门描述符中取出CS选择子与IP,然后跳转到这个CS:IP对应的中断处理程序执行。
  4. 然后,上述的第3步中跳转的是汇编语言编写的中断处理程序,它会负责保存现场(保存用户态的上下文到内核栈中),并跳转到用C语言编写的中断处理程序。

在x86体系的Linux中,系统调用是通过将要执行的系统调用号码放入EAX寄存器,然后主动调用INT 0x80软中断实现的。由于系统调用机制是基于中断机制,所以系统调用的流程和中断流程非常类似:

  1. 程序用int 0x80触发软中断
  2. CPU结合IDTR寄存器指向的IDT表找到0x80对应的中断门描述符;
  3. 从上一步的中断门描述符中取出CS选择子与IP,然后跳转到这个CS:IP对应的中断处理程序执行;
  4. 中断处理程序根据EAX中存放的值,去调用对应的系统调用函数。中断处理程序也是汇编编写的,它将负责保存现场(保存用户态的上下文到内核栈中)并根据EAX寄存器中的系统调用号码,跳转到对应的C语言编写的系统调用函数中去执行。

所以,我们现在来依据这个基于中断机制的系统调用流程来实现系统调用机制:

首先,我们来准备用户程序的系统调用入口,也就是用于触发int 0x80的程序。我们定义4个用户系统调用程序入口,用于不同的系统调用参数数量场景。寄存器参数传递规则参考linux 实现方法(书p525) (myos/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;						       \
})

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

修改 (myos/kernel/interrput.c

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

接下来,我们定义汇编版的系统调用处理函数 (myos/kernel/kernel.S

;;;;;;;;;;;;;;;;   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管理系统调用 (myos/userprog/syscall-init.c

#define syscall_nr 32 
typedef void* syscall;
syscall syscall_table[syscall_nr];

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

现在,我们来为我们的系统增加第一个系统调用sys_get_pid用于获得进程或线程的进程号,其实它就是将进程/线程pcb中的pid值返回回来

首先,我们先在进程/线程的pcb中添加pid成员,不然都没有pid这个成员,sys_get_pid返回啥呢 修改(myos/thread/thread.h

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这个成员,那么自然我们创建进程/线程的时候要去为这个成员赋值 修改(myos/thread/thread.c

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

现在,我们来写这个sys_get_pid函数,并将这个函数地址放入我们的系统调用表syscall_table中 修改(myos/userprog/syscall-init.c

#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;
   put_str("syscall_init done\n");
}

函数声明 (myos/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

支持代码 (myos/user/syscall.h

#ifndef __LIB_USER_SYSCALL_H
#define __LIB_USER_SYSCALL_H
#include "stdint.h"
enum SYSCALL_NR {
    
    
   SYS_GETPID
};

#endif

然后,在init_all中调用syscall_init完成系统调用处理函数的安装 修改(myos/kernel/init.c

#include "syscall-init.h"

/*负责初始化所有模块 */
void init_all() {
    
    
   put_str("init_all\n");
   idt_init();	     // 初始化中断
   mem_init();	     // 初始化内存管理系统
   thread_init();    // 初始化线程相关结构
   timer_init();     // 初始化PIT
   console_init();   // 控制台初始化最好放在开中断之前
   keyboard_init();  // 键盘初始化
   tss_init();       // tss初始化
   syscall_init();   // 初始化系统调用
}

最后,我们写一个用户程序进行系统调用的入口 (myos/lib/user/syscall.c

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

然后函数声明 (myos/user/syscall.h

uint32_t getpid(void);

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

测试代码 (myos/kernel/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 = getpid();
   while(1);
}

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

之前,我们是用寄存器来进行参数传递,其实,我们也能用栈来进行参数传递。原理就是因为系统调用机制基于中断机制,当通过用户程序入口进行系统调用时,用户程序入口会将系统调用号与参数压入用户栈中,然后触发int 0x80软中断,此时特权级切换,cpu会自动在内核栈中压入用户栈的位置,如果我们从内核栈中获取到这个用户栈的位置,那么自然就能获得系统调用号与参数,然后调用对应的系统调用处理函数。

请注意,这个实验只是为你展示可以用栈传递参数,并不作为后续开发的基础!

修改(myos/user/syscall.c

/* 无参数的系统调用 */
#define _syscall0(NUMBER) ({
      
      				       \
   int retval;					               \
   asm volatile (					       \
   "pushl %[number]; int $0x80; addl $4, %%esp"		       \
   : "=a" (retval)					       \
   : [number] "i" (NUMBER)		  		       \
   : "memory"						       \
   );							       \
   retval;						       \
})

/* 一个参数的系统调用 */
#define _syscall1(NUMBER, ARG0) ({
      
      			       \
   int retval;					               \
   asm volatile (					       \
   "pushl %[arg0]; pushl %[number]; int $0x80; addl $8, %%esp" \
   : "=a" (retval)					       \
   : [number] "i" (NUMBER), [arg0] "g" (ARG0)		       \
   : "memory"						       \
   );							       \
   retval;						       \
})

/* 两个参数的系统调用 */
#define _syscall2(NUMBER, ARG0, ARG1) ({
      
      		       \
   int retval;						       \
   asm volatile (					       \
   "pushl %[arg1]; pushl %[arg0]; "			       \
   "pushl %[number]; int $0x80; addl $12, %%esp"	       \
      : "=a" (retval)					       \
      : [number] "i" (NUMBER),				       \
	[arg0] "g" (ARG0),				       \
	[arg1] "g" (ARG1)				       \
      : "memory"					       \
   );							       \
   retval;						       \
})

/* 三个参数的系统调用 */
#define _syscall3(NUMBER, ARG0, ARG1, ARG2) ({
      
      		       \
   int retval;						       \
   asm volatile (					       \
      "pushl %[arg2]; pushl %[arg1]; pushl %[arg0]; "	       \
      "pushl %[number]; int $0x80; addl $16, %%esp"	       \
      : "=a" (retval)					       \
      : [number] "i" (NUMBER),				       \
	[arg0] "g" (ARG0),				       \
	[arg1] "g" (ARG1),				       \
	[arg2] "g" (ARG2)				       \
      : "memory"					       \
   );							       \
   retval;						       \
})

修改(myos/kernel/kernel.S

;;;;;;;;;;;;;;;;   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 从内核栈中获取cpu自动压入的用户栈指针esp的值
    mov ebx, [esp + 4 + 48 + 4 + 12]                             
                                
                                ; 为系统调用子功能传入参数,由于这个函数是3个参数的用户程序系统调用入口都会使用
                                ; 所以我们为了格式统一,直接按照最高参数数量压入3个参数,  此时ebx是用户栈指针
    push dword [ebx + 12]		; 系统调用中第3个参数
    push dword [ebx + 8]		; 系统调用中第2个参数
    push dword [ebx + 4]		; 系统调用中第1个参数
    mov edx, [ebx]              ; 系统调用的子功能号             

                                ;3 调用c中定义的功能处理函数
    call [syscall_table + edx*4]	; 编译器会在栈中根据C函数声明匹配正确数量的参数
    add esp, 12			        ; 跨过上面的三个参数

                                ;4 将call调用后的返回值存入待当前内核栈中eax的位置,c语言会自动把返回值放入eax中(c语言的ABI规定)
    mov [esp + 8*4], eax	
    jmp intr_exit		        ; intr_exit返回,恢复上下文

修改kernel.S中的核心是获取到用户栈的栈顶位置

mov ebx, [esp + 4 + 48 + 4 + 12]

此时内核栈的栈顶位置 + 4 是跳过了压入的0x80,+ 48 是跳过 push ad 与 gs fs es ds,+ 4 是跳过了 push 0,最后 + 12是因为用户程序调用int 0x80触发软中断,然后导致特权级切换,cpu会自动向内核栈中按照顺序压入此时的用户程序执行时的ss, esp, eflag, cs, eip,跳过12字节(ss = 2, esp = 4, eflag = 4, cs = 2)就是此时用户栈的栈顶位置eip

之前,我们一直用的是put_str, put_ch, put_int来用于打印,这些函数只能用于内核态。现在我们要实现用户态打印功能,也就是像c语言中的printf一样。c语言的printf是libc库提供的,它调用了很多系统调用来实现功能,其一是write系统调用,如果我们写一个简单的打印hello的c函数,然后编译,用strace命令去追踪这个编译好的可执行二进制文件,如strace ./hello.bin我们能清楚看见调用了write系统调用,这个系统调用在此处发挥的功能就是向标准输出文件(也就是我们的控制台shell,控制台在Linux中抽象成了一个文件,叫标准输出)写入hello。
在这里插入图片描述
我们printf也是仿照这个,最终是调用write系统调用来实现,但是由于我们现在没有实现文件系统,所以我们的write是一个简易版本的。现在我们来增加这个write系统调用。

首先,我们来增加write系统调用号。

修改(myos/lib/users/syscall.h

enum SYSCALL_NR {
    
    
   	SYS_GETPID,
   	SYS_WRITE
};

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

修改(myos/lib/user/syscall.c

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

之后,我们声明write系统调用的用户程序入口。

修改(myos/lib/users/syscall.h

uint32_t write(char* str);

现在我们已经实现了write系统调用的用户程序入口,现在来实现真正的系统调用执行函数。并将其添加进入系统调用表中。

修改(myos/userprog/syscall-init.c

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

然后声明这个系统调用函数

修改(myos/userprog/syscall-init.h

uint32_t sys_write(char* str);

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

修改(myos/lib/kernel/print.S

.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

现在我们就完整实现了系统调用write用于输出文字。你可以在用户进程中尝试用write进行输出。

我们的write只支持:字符串输出(因为底层实现是用put_str)与一个字符串地址参数;而c语言中的printf不仅能处理字符串,还能处理数字、字符、地址等。而且参数可以无限多,如下:

printf("a = %x, b = %c, c = %p, d = %s", a, b, c, d);

所以printf绝不是简单调用write,而是增加了:1、将多种格式转换成字符串供write打印。比如上述例子中,我们将a的值直接拿去替换第一个%x,b的值去替换第二个%c…然后将整个字符串打印出来;2、支持可变参数 ,也就是不限制参数数量(一般来说,我们写一个函数,然后建立函数声明必须指定参数数量与类型,这样编译器才知道给函数开辟多大的栈空间。可变参数原理见书p536)。

可变参数依靠的是编译器的特性,其原理的核心就是,调用者依据c调用约定,从右到左依次向栈中压入参数,而被调用者是能够依据栈中的数据来找到传入的参数。就用上述printf来举例子,当一个函数调用printf,被调者的栈是如下分布:

200 d 字符串的地址				高地址
196 c 地址值
195 b 字符的ascii码值
191 a 32位值
187 "a = %x, b = %c, c = %p, d = %s" 字符串地址
183 eip 返回地址				 低地址

只要我们知道第一个参数的位置,也就是那个字符串地址,然后知道每个参数类型(知道参数类型才知道参数在栈中的大小,才能通过移动指针的方式找到参数),就能找到之后所有的参数。而这两个都不难。

现在来编写printf函数,但是由于这个函数功能非常多,所以我们现在只增加支持16进制输出。

myso/lib/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) {
    
         								// 如果余数是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); 
}

支持代码(myos/lib/stdio.h

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

测试代码(myos/kernel/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"

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

修改Makefile为新增加的文件增加编译规则,编译之后,会出现如下错误:

ld: build/stdio.o: in function `printf':
stdio.c:(.text+0x1ab): undefined reference to `__stack_chk_fail'
make: *** [makefile:105: build/kernel.bin] Error 1

这个错误是因为编译器在使用栈保护特性,但链接器找不到实现这一特性所需的函数__stack_chk_fail。栈保护是一种安全特性,可以检测到栈溢出。通常情况下,__stack_chk_fail函数是由编译器自动插入到代码中的,用于在检测到栈溢出时中止程序。这个函数通常包含在C库中,由于我们链接的程序没有链接C库,就会看到这个错误。

解决这个问题的一个办法是禁用栈保护特性。在编译代码时添加-fno-stack-protector选项来做到这一点。

修改(myos/Makefile

CFLAGS= -Wall $(LIB) -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes -m32 -fno-stack-protector

接下来,我们完善printf,增加处理%s, %c, %d的功能

修改(myos/lib/stdio.c)

char* arg_str;

/* 将参数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_char
			break;
		}
	}
	return strlen(str);
}

测试代码(myos/kernel/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"

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

在进行接下来的开发之前,我们先填坑。是否记得我们之前实现页分配时,只实现了内核页面的分配,而把用户页面分配代码是空着的!

修改(myos/kernel/memory.c

/* 在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、用户态也可以使用;

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

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

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

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

修改(myos/kernel/memory.h

#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	   // 内存块描述符个数

修改(myos/kernel/memory.c

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

struct mem_block_descstruct mem_blockstruct 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然后划分成固定大小的小块。

这三个个结构体关系如图:
在这里插入图片描述
然后初始化管理内核不同种类型arena的不同mem_block_desc

修改(myos/kernel/memory.c

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

添加函数声明,修改(myos/kernel/memory.h

void block_desc_init(struct mem_block_desc* desc_array);

内核有了管理不同类型arena的mem_block_desck数组。我们说进程是独立分配资源的单位,进程拥有自己的独立虚拟地址空间,那么进程也应该拥有管理自己不同类型arena的mem_block_desc数组。这样进程分配内存,就去进程自己的mem_block_desc数组中找对应的mem_block_desc,然后通过free_list找到空闲小块就行了。

修改(myos/thread/thread.h)为task_struct添加u_block_desc,这样每个task_struct都有了这个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的边界
};

但是,我们只初始化进程的mem_block_desc数组

修改(myos/userprog/process.c

//用于创建进程,参数是进程要执行的函数与他的名字
void process_execute(void* filename, char* name) {
    
     
    /* pcb内核的数据结构,由内核来维护进程信息,因此要在内核内存池中申请 */
    struct task_struct* thread = get_kernel_pages(1);
    init_thread(thread, name, default_prio); 
    create_user_vaddr_bitmap(thread);
    thread_create(thread, start_process, filename);
    thread->pgdir = create_page_dir();
    block_desc_init(thread->u_block_desc);
    
    enum intr_status old_status = intr_disable();
    ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));
    list_append(&thread_ready_list, &thread->general_tag);

    ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));
    list_append(&thread_all_list, &thread->all_list_tag);
    intr_set_status(old_status);
}

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

修改(myos/kernel/memory.c

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

声明函数,修改(myos/kernel/memory.h

void* sys_malloc(uint32_t size);

测试代码,修改(myos/kernel/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);
}

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

修改(myos/kernel/memory.c

//将物理地址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);
	}
}

函数声明:修改(myos/kernel/memory.h

void mfree_page(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt);
void pfree(uint32_t pg_phy_addr);

现在,我们实现与arena模型分配机制对应的回收机制,并将之前的页回收封装进入,直接实现统一的内存回收系统调用sys_free

修改(myos/kernel/memory.c

/* 回收内存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); 
	}
}

函数声明,修改(myos/kernel/memory.h

void sys_free(void* ptr);

测试代码(myos/kernel/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* 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);
}

最后,我们来将sys_mallocsys_free封装成用户程序可用的调用接口

增加系统调用号,修改(myos/lib/user/syscall.h

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

封装系统调用用户入口,修改(myos/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);
}

声明函数,修改(myos/lib/user/syscall.h

void* malloc(uint32_t size);
void free(void* ptr);

注册系统调用函数

修改(myos/userprog/syscall-init.c

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

测试函数(myos/kernel/main.c),相比作者代码,k_thread_a与k_thread_b内的消耗时间变量由100000增加到了9999999,否则不会出现书上结果,因为线程在切换前就已经释放了地址

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

猜你喜欢

转载自blog.csdn.net/kanshanxd/article/details/131697078