二、8.系统调用、可变参数和堆内存管理

news/2024/7/2 23:57:26

系统调用:让用户进程申请操作系统的帮助

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

先梳理下咱们系统调用的实现思路 。

  1. 用中断门实现系统调用,效仿 Linux 用 0x80 号中断作为系统调用的入口 。
  2. 在 IDT 中安装 Ox80 号中断对应的描述符,在该描述符中注册系统调用对应的中断处理例程。
  3. 建立系统调用子功能表 syscall_table ,利用 e阻寄存器中的子功能号在该表中索引相应的处理函数。
  4. 用宏实现用户空 间系统调用接口 _syscall ,最大支持 3 个参数的系统调用,故只 需要完成 _syscall[0-3]。寄存器传递参数, eax 为子功能号, ebx 保存第 1 个参数, ecx 保存第 2 个参数, edx 保存第 3 个参数 。

增加 0x80 号中断描述符

#define IDT_DESC_CNT 0x81      // 目前总共支持的中断数 0~Ox80,

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

实现系统调用接口

/* 无参数的系统调用 */
#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;						       \
})

Linux 中是用宏定义了一个函数,咱们这里是直接用大括号完成的,也许有同学对大括号的这种用法比较陌生,大括号中最后一个语句的值会作为大括号代码块的返回值,而且要在最后一个语句后添加分号’;’,否则编译时会报错 。
另外,在咱们的内联汇编中都没用到通用约束,确实简陋了很多 。


增加 0x80 号中断处理例程

;;;;;;;;;;;;;;;;   0x80号中断   ;;;;;;;;;;;;;;;;
[bits 32]
extern syscall_table ; 声明系统调用数组,数组成员是系统调用中子功能对应的处理函数
section .text
global syscall_handler
syscall_handler:
;1 保存上下文环境
    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 为系统调用子功能传入参数
    push edx			    ; 系统调用中第3个参数
    push ecx			    ; 系统调用中第2个参数
    push ebx			    ; 系统调用中第1个参数
;子功能处理函数都有自己的原型声明,声明中包括参数个数及类型,
;编译时编译器会根据函数声明在栈中匹配出正确数量的参数,进入函数体后,
;根据 C 调用约定,栈顶的 4 字节 是函数的返回地址,
;往上(高地址的栈底方向)以 4 字节为长度依次是第 1 个参数,第 2 个参数...
;在函数体中,编译器生成的取参数指令是从栈顶往上(跨过栈顶的返回地址,向高地址方向)获取参数的,
;参数个数是通过函数声明事先确定好的,因此并不会获取到错误的参数,从而保证了多余的参数用不上,
;因此,尽管我们压入了 3 个参数,但对于那些参数少于 3 个的函数也不会出错,而我们也只是浪费了一点点栈空间

;3 调用子功能处理函数
    call [syscall_table + eax*4]	    ; 调用子功能处理函数
    add esp, 12			    		   ; 跨过上面的三个参数

;4 将call调用后的返回值存入当前内核栈中eax的位置
    mov [esp + 8*4], eax	
    jmp intr_exit		    ; intr_exit返回,恢复上下文

初始化系统调用和实现 sys_getpid

getpid 的功能是获取任务自己的 pid, getpid 是给用户进程使用的接口函数,它在内核中对应的处理函数是sys_getpid

#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");
}
//为任务分配 pid

typedef int16_t pid_t;

/* 进程或线程的pcb,程序控制块 */
struct task_struct {
   ...
   pid_t pid;
   enum task_status status;
   ...
};


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

/* 初始化线程基本信息 */
void init_thread(struct task_struct* pthread, char* name, int prio) {
   memset(pthread, 0, sizeof(*pthread));
   pthread->pid = allocate_pid();
   strcpy(pthread->name, name);

   ...
}

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

enum SYSCALL_NR {
   SYS_GETPID
};//用来存放系统调用子功能号,目前里面只有 SYS_GETPID,默认值为 0,以后再增加新的系统调用后还需要把新的子功能号添加到此结构中。

/* 无参数的系统调用 */
#define _syscall0(NUMBER) ({				       \
   int retval;					               \
   asm volatile (					       \
   "int $0x80"						       \
   : "=a" (retval)					       \
   : "a" (NUMBER)					       \
   : "memory"						       \
   );							       \
   retval;						       \
})

/* 用户接口,返回当前任务pid */
uint32_t getpid() {
   return _syscall0(SYS_GETPID);
}

总结下增加系统调用的步骤:

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

在用户进程中的系统调用

int prog_a_pid = 0, prog_b_pid = 0;

int main(void) {
   put_str("I am kernel\n");
   init_all();

   //两用户进程负责获得自己的PID
   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');
   //两线程负责打印用户进程的PID
   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);
}

系统调用之栈传递参数

我们目前的系统调用是通过寄存器来传递参数的,原因和大伙说过了,若用栈传递参数的话,调用者(用户进程)首先得把参数压在 3 特权级的栈中,然后内核将其读出来再压入 0 特权级栈,这涉及到两种栈的读写,故通过寄存器传递参数效率更高。

/* 无参数的系统调用 */
#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;						       \
})
;;;;;;;;;;;;;;;;   0x80号中断   ;;;;;;;;;;;;;;;;
[bits 32]
extern syscall_table
section .text
global syscall_handler
syscall_handler:

; 系统调用传入的参数在用户栈中,此时是内核栈
;1 保存上下文环境
   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 再把参数重新压在内核栈中, 此时ebx是用户栈指针
;  由于此处只压入了三个参数, 所以目前系统调用最多支持3个参数
   push dword [ebx + 12]	    ; 系统调用的第3个参数
   push dword [ebx + 8]		    ; 系统调用的第2个参数
   push dword [ebx + 4]		    ; 系统调用的第1个参数
   mov edx, [ebx]		    ; 系统调用的子功能号

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

;4 将call调用后的返回值存入待当前内核栈中eax的位置
   mov [esp + 8*4], eax	
   jmp intr_exit		    ; intr_exit返回,恢复上下文

printf 函数是“格式化”“输出”函数,将格式化后的信息输出到标准输出(通常是屏幕)。但它只是个外壳,真正起到“格式化”作用的是 vsprintf 函数,真正起“输出”作用的是 write 系统调用。

实现系统调用 write

enum SYSCALL_NR {
   SYS_GETPID,
   SYS_WRITE
};

/* 打印字符串str */
uint32_t write(char* str) {
   return _syscall1(SYS_WRITE, str);
}
/* 打印字符串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");
}

实现 printf

可变长参数看似动态,实际是编译器在编译阶段就确定下来传入参数的数量,从而静态分配空间。程序通过格式化字符串中有多少"%"确定传入多少参数

ap (argument pointer)是个指针变量,表示参数的指针,用来指向可变参数在枝中的地址。
ap 的类型为 va_list。va_list 是什么呢?大伙儿己经知道 ap 是个指针变量了,故 va_list 本质上是指针类型,由于 ap 用于指向栈中可变参数的地址,其所指向的参数类型未知,故 va_list 应该是较通用的指针类型,是 void*char* 都可以,但从名称上看 va_list 是可变参数的列表,这让人联想到字符串 format 中一系列的参数列表 “%x%d%f…”,故 va_list 的类型是 char*

typedef char* va_list;

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

/* 同printf不同的地方就是字符串不是写到终端,而是写到buf中 */
uint32_t sprintf(char* buf, const char* format, ...) {
    va_list args;
    uint32_t retval;
    va_start(args, format);
    retval = vsprintf(buf, format, args);
    va_end(args);
    return retval;
}

/* 格式化输出字符串format */
uint32_t printf(const char* format, ...) {
    va_list args;
    va_start(args, format);	       // 使args指向format
    char buf[1024] = {0};	       // 用于存储拼接后的字符串
    vsprintf(buf, format, args);
    va_end(args);
    return write(buf);
}

完善堆内存管理

堆用于动态分配内存的区域,通常用于存储程序运行时动态创建的数据结构和对象

“arena”,该单词的意思是舞台。 arena 是很多开源项目中都会用到的内存管理概念,将一大块内存划分成多个小内存块,每个小内存块之间互不干涉,可以分别管理,这样众多的小内存块就称为 arena

arena 是由“ 一大块内存”被划分成无数“小内存块”的内存仓库,我们在原有内存管理系统的基础上实现 arena,大伙儿知道,原有系统只能分配 4KB 粒度的内存页框,因此 arena 的这“一大块内存”也是通过 malloc_page 获得的以 4KB 为粒度的内存,根据请求的内存量的大小, arena 的大小也许是 1 个页框,也许是多个页框,随后再将它们平均拆分成多个小内存块。按内存块的大小,可以划分出多种不同规格的 arena,一种规格的arena只响应一种大小以内的内存分配。我们平时调用 malloc 申请内存时,操作系统返回的地址其实就是某个内存块的起始地址,操作系统会根据 malloc 申请的内存大小来选择不同规格的内存块。因此,为支持多种容量内存块的分配,我们要提前建立好多种不同容量内存块的 arena。

arena 是个提供内在分配的数据结构,它分为两部分,一部分是元信息,用来描述自己内存池中空闲内存块数量,这其中包括内存块描述符指针,通过它可以间接获知本 arena 所包含内存块的规格大小,此部分占用的空间是固定的,约为 12 字节。另一部分就是内存地区域,这里面有无数的内存块,此部分占用 arena 大量的空间。我们把每个内存块命名为 mem_block,它们是内存分配粒度更细的资源,最终为用户分配的就是这其中的一个内存块。

起始为某一类型内存块供货的 arena 只有 1 个,当此 arena 中的全部内存块都被分配完时,系统再创建一个同规格的 arena 继续提供该规格的内存块,当此 arena 又被分配完时,再继续创建出同规格的缸ena, arena 规模逐渐增大,逐步形成 arena 集群。既然同一类内存块可以由多个 arena 提供,为了跟踪每个arena 中的空闲内存块,分别为每一种规格的内存块建立一个内存块描述符,即 mem_block_desc,在其中记录内存块规格大小,以及位于所有同类 arena 中的空闲内存块链表。

内存块描述符将所有同类 arena 中空闲内存块汇总,因此它相当于内存块超级大仓库,分配小块内存时必须先经过此入口,系统从它的空闲内存块链表 free_Iist中挑选一块内存,, 也就是说,最终所分配的内存属于此类 arena 集群中某个 arena 的某个内存块。

内存块规格有多少种,内存块描述符就有多少种,因此各种内存块描述符的区别就是 block_size 不同, free_list 中指向的内存块规格不同 。 由于有了内存块描述符, arena 中就没有必要再冗余记录本 arena 中内存块规格信息,而是用内存块描述符指针指向本 arena 所属的内存块描述符,间接获得本 arena 中内存块的规格大小,内存块描述符指针位于 arena 的元信息当中。

尽管 arena 用小内存块来满足小内存量的分配,但实际上, arena 为内存分配提供了统一的入口,无论申请的内存量是多大,都可以用同一个 arena 来分配内存。小内存块的容量虽然有几种规格,但毕竟是为满足“小”内存量分配的,最大内存块容量不会超过 1024 字节,如果申请的内存量较大,超过 1024 字节,单独的一个小内存块无法满足需求时,也会创建个 arena,但不会再将它拆分成小内存块,而是直接将整块大内存分配出去,

在内存管理系统中, arena 为任意大小内存的分配提供了统一的接口,它既支持 1024 字节以下的小块内存的分配,又支持大于 1024 字节以上的大块内存, malloc 函数实际上就是通过 arena 申请这些内存块。 arena 是个内存仓库,并不直接对外提供内存分配,只有内存块描述符才对外提供内存块,内存块描述符将同类 arena 中的空闲内存块汇聚到一起,作为某一规格内存块的分配入口。因此,内存块描述符与 arena 是一对多的关系,每个 arena 都要与唯一的内存块描述符关联起来,多个同一规格的 arena 为同一规格的内存块描述符供应内存块,它们各自的元信息中用内存块描述符指针指向同一个内存块描述符。

image-20230819125511937

底层初始化

/* 内存块 */
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	   // 内存块描述符个数
/*咱们的内存块规格大小是以2为底的指数方程来设计的,从16字节起,分别是16、32、64、128、256、 512、1024字节,共有7种规格的内存块。*/
/* 内存仓库arena元信息 */
struct arena {
    struct mem_block_desc* desc;	 // 此arena关联的mem_block_desc
    /* large为ture时,cnt表示的是本arena占用的页框数。
	 * 否则cnt表示空闲mem_block数量 */
    uint32_t cnt;
    bool large;		   
};

struct mem_block_desc k_block_descs[DESC_CNT];	// 内核内存块描述符数组
struct pool kernel_pool, user_pool;      // 生成内核内存池和用户内存池
struct virtual_addr kernel_vaddr;	 // 此结构是用来给内核分配虚拟地址

/* 为malloc做准备 */
//接收内存块描述符数组,初始化数组内 7 个描述符
void block_desc_init(struct mem_block_desc* desc_array) {				   
    uint16_t desc_idx, block_size = 16;

    /* 初始化每个mem_block_desc描述符 */
    for (desc_idx = 0; desc_idx < DESC_CNT; desc_idx++) {
        desc_array[desc_idx].block_size = block_size;

        /* 初始化arena中的内存块数量 */
        desc_array[desc_idx].blocks_per_arena = (PG_SIZE - sizeof(struct arena)) / block_size;	  
        list_init(&desc_array[desc_idx].free_list);

        block_size *= 2;         // 更新为下一个规格内存块
    }
}

/* 内存管理部分初始化入口 */
void mem_init() {
    put_str("mem_init start\n");
    uint32_t mem_bytes_total = (*(uint32_t*)(0xb00));
    mem_pool_init(mem_bytes_total);	  // 初始化内存池
    /* 初始化mem_block_desc数组descs,为malloc做准备 */
    block_desc_init(k_block_descs);
    put_str("mem_init done\n");
}

实现 sys_malloc

对计算机来说,必须本着按需分配的原则合理使用内存资源,因此内存块并不是提前“ 盲目”准备好的,它在需要时由程序动态创建,创建它的函数就是 sys_malloc,它就是 malloc 对应的子功能处理函数 sys_malloc,sys_malloc 的功能是分配并维护内存块资源,动态创建 arena 以满足内存块的分配,似乎离完成系统调用 malloc 不远了 。

/* 进程或线程的pcb,程序控制块 */
struct task_struct {
    uint32_t* self_kstack;	 // 各内核线程都用自己的内核栈
    pid_t pid;
    ...
    uint32_t* pgdir;              // 进程自己页表的虚拟地址

    struct virtual_addr userprog_vaddr;   // 用户进程的虚拟地址
    struct mem_block_desc u_block_desc[DESC_CNT];   // 用户进程内存块描述符

    ...
};
/* 创建用户进程 */
void process_execute(void* filename, char* name) { 
    ...
    //初始化内存块描述符数组
    thread->pgdir = create_page_dir();
    block_desc_init(thread->u_block_desc);

    ...
}
/* 返回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);//内存块的高 20 位地址便是 arena 所在的地址
}

/* 在堆中申请size字节内存 */
void* sys_malloc(uint32_t size) {
    enum pool_flags PF;
    struct pool* mem_pool;
    uint32_t pool_size;
    struct mem_block_desc* descs;
    struct task_struct* cur_thread = running_thread();

    /* 判断用哪个内存池*/
    if (cur_thread->pgdir == NULL) {     // 若为内核线程
        PF = PF_KERNEL; 
        pool_size = kernel_pool.pool_size;
        mem_pool = &kernel_pool;
        descs = k_block_descs;
    } else {				      // 用户进程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;//指向新创建的 arena
    struct mem_block* b;//指向arena中的 mem_block
    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);
        }    

        /* 开始分配内存块 */
        /*内存块被汇总在内存块描述符的free_list中,我们用list_pop从free_list中弹出一个内存块,
        此时得到的仅仅是内存块mem_block中list_elem的地址,因此要用到 elem2entry 宏将其转换成
        mem_block 的地址 */
        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;
    }
}

在各种 list 中的结点是 list_elem 的地址,并不是 list_elem 所在的“宿主数据结构”,比如在就绪队列 thread_ready_list 中的是 pcb 的 general_tag 的地址,而 pcb 是 general_tag 的宿主数据结构。宿主数据结构中 list_elem 的地址才是链表中的结点,而 list_elem 中存储的是前驱和后继结点的地址,也就是其他宿主数据结构的 list_elem 的地址。

当结点从链表中脱离时,要将其还原成宿主数据结构才能使用,还原工作是通过宏 elem2entry 完成的,本节的内存块分配便是通过该宏得到内存块的起始地址。内存块地址被返回给用户后,用户可以自由使用此内存块,自然也会把此内存块中的 list_elem 型变量 free_elem 覆盖,不过没关系,它并不影响该内存块的回收和分配,您懂的, free_list 中的元素是 list_elem 的地址,地址是不变的,将来回收或再次分配时依然可以正常使用

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

内存释放

我们分配内存时的一般步骤如下。

  1. 在虚拟地址池中分配虚拟地址,相关的函数是 vaddr_get,此函数操作的是内核虚拟内存池位图 kernel_vaddr.vaddr_bitmap或用户虚拟内存地位图pcb->userprog_vaddr.vaddr_bitmap
  2. 在物理内存池中分配物理地址,相关的函数是 palloc ,此函数操作的是内核物理内存地位图kernel_pool->pool_bitmap或用户物理内存池位图user_pool->pool_bitmap
  3. 在页表中完成虚拟地址到物理地址的映射,相关的函数是page_table_add

以上三个步骤封装在函数 malloc_page 中。
释放内存是与分配内存相反的过程,咱们对照着设计一套释放内存的方法。

  1. 在物理地址池中释放物理页地址,相关的函数是 pfree,操作的位图同 palloc
  2. 在页表中去掉虚拟地址的映射,原理是将虚拟地址对应 pte 的 P 位置 0,相关的函数是 page_table_pte_remove
  3. 在虚拟地址池中释放虚拟地址,相关的函数是 vaddr_remove ,操作的位图同 vaddr_get
/* 将物理地址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 所在的 pte 重新写入 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);
    }
}

sys_free 是内存释放的统一接口,无论是页框级别的内存和小的内存块,都统一用 sys_free 处理。因此, sys_free 针对这两种内存的处理有各自的方法,对于大内存的处理称之为释放,就是把页框在虚拟内存池和物理内存池的位图中将相应位置 0。 对于小内存的处理称之为“回收”,是将 arena 中的内存块重新放回到内存块描述符中的空闲块链表 free list

/* 回收/释放内存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) {
                //表示此 arena 中的空闲内存块己经达到最大数,说明此 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); 
    }
}
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);
}

实现系统调用 malloc 和 free

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

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

/* 释放ptr指向的内存 */
void free(void* ptr) {
   _syscall1(SYS_FREE, ptr);
}
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 = 100000;
   while(cpu_delay-- > 0);
   sys_free(addr1);
   sys_free(addr2);
   sys_free(addr3);
   while(1);
}

http://lihuaxi.xjx100.cn/news/1469330.html

相关文章

BBS项目day04 文章详情页、点赞点菜、评论功能(根评论和子评论)、评论分页之刷新评论页面

一、路由 from django.contrib import admin from django.urls import path, re_path from app01 import views from django.views.static import serve from django.conf import settingsurlpatterns [path(admin/, admin.site.urls),# 注册path(register/, views.register)…

使用mysql:5.6和 owncloud 镜像,构建一个个人网盘。

使用mysql:5.6和 owncloud 镜像&#xff0c;构建一个个人网盘。 1.关闭防火墙 2.在docker中拉去镜像&#xff0c;运行mysql容器 最后一行是容器的默认id 用docker创建一个新的数据库mysqldb设置密码123456 3.拉去网盘owncloud镜像 4.使用软连接连接mysql 5.测试

【TypeScript】枚举类型

在 TypeScript 中&#xff0c;枚举&#xff08;Enum&#xff09;是一种用于定义命名常量集合的数据类型。枚举使代码更加可读和可维护&#xff0c;因为它们为一组具有语义的值提供了命名。 以下是 TypeScript 中枚举的基本用法和特点&#xff1a; // 声明一个枚举 enum Direc…

龙讯旷腾PWmat已部署至曙光智算平台

编者荐语&#xff1a; 近期&#xff0c;龙讯旷腾核心产品PWmat已成功部署至曙光智算AC.sugon.com平台&#xff0c;可为用户提供包括分子建模、第一性原理计算、数据可视化等在内的完备的超级计算云服务&#xff0c;让大家能够轻松上手具有完全自主知识产权的大尺度高性能材料计…

springboot后端返回图片,vue前端接收并显示的解决方案

后端图片数据返回 后端通过二进制流的形式&#xff0c;写入response中 controller层 /*** 获取签到二维码*/GetMapping("/sign-up-pict")public void signUpPict(Long id, Long semId, HttpServletResponse response) throws NoSuchAlgorithmException {signUpServ…

Java动态代理、反射

文章目录 动态代理调用者--->代理--->对象为什么需要代理代理的详细实现过程代码详情 反射反射概念反射中常用的方法所有代码 动态代理 调用者—>代理—>对象 动态代理就是无侵入式的给代码增加新的功能&#xff0c;通过接口保证后面的对象和代理需要实现同一个接…

苹果电脑怎么录屏?步骤详解,看到就是赚到

苹果电脑作为一款受欢迎的高性能设备&#xff0c;不仅在日常工作中发挥着重要作用&#xff0c;还可以用于创造内容&#xff0c;如录制屏幕内容。录屏功能能够帮助用户将屏幕上的活动记录成视频&#xff0c;方便分享、演示或存档。可是您知道苹果电脑怎么录屏吗&#xff1f;通过…

浙大数据结构第八周之08-图8 How Long Does It Take

前置知识&#xff1a; 拓扑排序&#xff1a; /* 邻接表存储 - 拓扑排序算法 */bool TopSort( LGraph Graph, Vertex TopOrder[] ) { /* 对Graph进行拓扑排序, TopOrder[]顺序存储排序后的顶点下标 */int Indegree[MaxVertexNum], cnt;Vertex V;PtrToAdjVNode W;Queue Q Cre…