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

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

一个系统功能调用分为两部分, 一部分是暴露给用户进程的接口函数,它属于用户空间,此部分只是用户进程使用系统调用的途径,只负责发需求。另一部分是与之对应的内核具体实现,它属于内核空间,此部分完成的是功能需求,就是我们一直所说的系统调用子功能处理函数。为区分这两部分,一般情况下内核空间的函数名要在用户空间函数名前加 “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 dspush espush fspush gspushad			    ; PUSHAD指令压入32位寄存器,其入栈顺序是:; EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI push 0x80			; 此位置压入0x80也是为了保持统一的栈格式;2 为系统调用子功能传入参数push edx			    ; 系统调用中第3个参数push ecx			    ; 系统调用中第2个参数push ebx			    ; 系统调用中第1个参数
;子功能处理函数都有自己的原型声明,声明中包括参数个数及类型,
;编译时编译器会根据函数声明在栈中匹配出正确数量的参数,进入函数体后,
;根据 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");
}
//为任务分配 pidtypedef 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();//两用户进程负责获得自己的PIDprocess_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');//两线程负责打印用户进程的PIDthread_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 dspush espush fspush gspushad			    ; 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_charbreak;}}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指向formatchar 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;//指向新创建的 arenastruct mem_block* b;//指向arena中的 mem_blocklock_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页框做为arenaif (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所在的arenaa->cnt--;		   // 将此arena中的空闲内存块数减1lock_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位置0asm 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://www.mzph.cn/news/47250.shtml

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

相关文章

C++day1(笔记整理)

一、Xmind整理&#xff1a; 二、上课笔记整理&#xff1a; 1.第一个c程序&#xff1a;hello world #include <iostream> //#:预处理标识符 //<iostream>:输入输出流类所在的头文件 //istream:输入流类 //ostream:输出流类using namespace std; //std&#x…

Goland 注释时自动在注释符号后添加空格

不得不说 JetBrains 旗下的 IDE 都好用&#xff0c;而且对于注释这块&#xff0c;使用 Ctrl / 进行注释的时候&#xff0c;大多会在每个注释符号后统一添加一个空格&#xff0c;比如 PyCharm 和 RubeMine 等。 # PyCharm # print("hello world") # RubyMine # req…

从Web 2.0到Web 3.0,互联网有哪些变革?

文章目录 Web 2.0时代&#xff1a;用户参与和社交互动Web 3.0时代&#xff1a;语义化和智能化影响和展望 &#x1f389;欢迎来到Java学习路线专栏~从Web 2.0到Web 3.0&#xff0c;互联网有哪些变革&#xff1f; ☆* o(≧▽≦)o *☆嗨~我是IT陈寒&#x1f379;✨博客主页&#x…

易思智能物流无人值守系统文件上传漏洞复现

0x01 产品简介 易思无人值守智能物流系统是一款集成了人工智能、机器人技术和物联网技术的创新产品。它能够自主完成货物存储、检索、分拣、装载以及配送等物流作业&#xff0c;帮助企业实现无人值守的智能物流运营&#xff0c;提高效率、降低成本&#xff0c;为现代物流行业带…

Unity 物体固定屏幕尺寸(透视模式)

物体固定屏幕尺寸 &#x1f96a;效果图&#x1f371;食用方法 &#x1f96a;效果图 如图所示物体远离摄像机后会被放大&#xff0c;靠近相机会被缩小&#xff0c;使得在屏幕上的大小保持不变&#xff1b; &#x1f371;食用方法 导入插件后使用gameObject.SetFixedScreenSi…

python 开发环境(PyCharm)搭建指南

Python 的下载并安装 参考&#xff1a;Python基础教程——搭建Python编程环境 下载 Python Python 下载地址&#xff1a;官网 &#xff08;1&#xff09;点击【Downloads】>>>点击【Windows】>>>点击【Python 3.x.x】下载最新版 Python&#xff1b; Pyt…

前端(十三)——JavaScript 闭包的奥秘与高级用法探索

&#x1f636;博主&#xff1a;小猫娃来啦 &#x1f636;文章核心&#xff1a;深入理解 JavaScript 中的闭包 文章目录 不理解闭包&#xff1f;这玩意很难&#xff1f;闭包的定义与原理闭包是什么创建一个闭包 闭包的应用场景闭包与作用域闭包与作用域之间的关系全局作用域、函…

Python爬虫实战案例——第一例

X卢小说登录(包括验证码处理) 地址&#xff1a;aHR0cHM6Ly91LmZhbG9vLmNvbS9yZWdpc3QvbG9naW4uYXNweA 打开页面直接进行分析 任意输入用户名密码及验证码之后可以看到抓到的包中传输的数据明显需要的是txtPwd进行加密分析。按ctrlshiftf进行搜索。 定位来到源代码中断点进行调…

ES6 代理

一、代理 Proxy 用于修改某些操作的默认行为&#xff0c;等同于在语言层面做出修改&#xff0c;所以属于一种“元编程”&#xff08;meta programming&#xff09;&#xff0c;即对编程语言进行编程。 Proxy 可以理解成&#xff0c;在目标对象之前架设一层“拦截”&#xff0…

git协议实现管理(三个步骤)

GitHub官网访问&#xff1a; https://github.com/dashboard 初次使用git的用户要使用git协议大概需要三个步骤: 一、生成密钥对 二、设置远程仓库(本文以github为例)上的公钥 三、把git的remote url远程仓库URL可访问路径修改为git协议(以上两个步骤初次设置过以后&#xff0c…

面试之HTTP

1.HTTP与HTTPS的区别 HTTP运行在TCP之上&#xff1b;HTTPS是运行在SSL之上&#xff0c;SSL运行在TCP之上两者使用的端口不同&#xff1a;HTTP使用的是80端口&#xff0c;HTTPS使用的是443端口安全性不同&#xff1a;HTTP没有加密&#xff0c;安全性较差&#xff1b;HTTPS有加密…

python刷小红书流量(小眼睛笔记访问量),metrics_report接口,原理及代码,以及x-s签名验证2023-08-21

一、什么是小眼睛笔记访问量 如下图所示&#xff0c;为笔记访问量。 二、小眼睛笔记访问量接口 1、url https://edith.xiaohongshu.com/api/sns/web/v1/note/metrics_report 2、payload data{"note_id": note_id,"note_type": note_type,"report_t…

爬虫逆向实战(十九)--某号站登录

一、数据接口分析 主页地址&#xff1a;某号站 1、抓包 通过抓包可以发现登录接口 2、判断是否有加密参数 请求参数是否加密&#xff1f; 通过查看“载荷”模块可以发现有一个jsondata_rsa的加密参数 请求头是否加密&#xff1f; 无响应是否加密&#xff1f; 无cookie是否…

【报错】yarn --version Unrecognized option: --version Error...

文章目录 问题分析解决问题 在使用 npm install -g yarn 全局安装 yarn 后,查看yarn 的版本号,报错如下 PS D:\global-data-display> yarn --version Unrecognized option: --version Error: Could

数据结构---串(赋值,求子串,比较,定位)

目录 一.初始化 顺序表中串的存储 串的链式存储 二.赋值操作&#xff1a;将str赋值给S 链式表 顺序表 三.复制操作&#xff1a;将chars复制到str中 链式表 顺序表 四.判空操作 链式表 顺序表 五.清空操作 六.串联结 链式表 顺序表 七.求子串 链式表 顺序表…

掌握AI助手的魔法工具:解密Prompt(提示)在AIGC时代的应用「上篇」

在当今的AIGC时代&#xff0c;我们面临着越来越多的人工智能技术和应用。其中一个引人注目的工具就是Prompt&#xff08;提示&#xff09;。它就像是一种魔法&#xff0c;可以让我们与AI助手进行更加互动和有针对性的对话。那么&#xff0c;让我们一起来了解一下Prompt&#xf…

广东灯具3D扫描抄数建模服务3D测绘出图纸三维逆向设计-CASAIM

灯具三维逆向建模是一种将实际物体转换为数字模型的过程。通过逆向工程技术&#xff0c;可以将现有的灯具进行3D扫描&#xff0c;然后利用专业的逆向设计软件将其转换为准确的三维模型。 以下是CASAIM实施灯具三维逆向建模的一般步骤图&#xff1a; 1. 扫描&#xff1a;三维扫…

SSL证书如何使用?SSL保障通信安全

由于SSL技术已建立到所有主要的浏览器和WEB服务器程序中&#xff0c;因此&#xff0c;仅需安装数字证书或服务器证书就可以激活功能了。SSL证书主要是服务于HTTPS&#xff0c;部署证书后&#xff0c;网站链接就由HTTP开头变为HTTPS。 SSL安全证书主要用于发送安全电子邮件、访…

社交工程和钓鱼攻击防范: 分析针对人类心理和社交工程的攻击技术,并介绍预防这些攻击的方法

第一章&#xff1a;引言 随着科技的不断进步&#xff0c;网络安全问题愈发凸显。在这个数字化时代&#xff0c;社交工程和钓鱼攻击成为黑客们获取敏感信息的常用手段。这些攻击不是基于技术漏洞&#xff0c;而是利用人类心理弱点来进行。本文将深入探讨社交工程和钓鱼攻击的原…

Flowable学习[一]

一、参考CSDN博主[水中加点糖]的博客[采用springbootflowable快速实现工作流]&#xff0c;文章地址&#xff1a;https://puhaiyang.blog.csdn.net/article/details/79845248&#xff0c;下载其发布在github上的代码 二、本地解压代码&#xff0c;并加载到idea中 三、使用docke…