Ⅰ.Linux系统调用原理
Linux系统调用都通过中断号int 0x80完成,不同的系统调用函数对应eax中不同的子功能号,因此系统调用包括int 0x80; mov %sub_num,%%eax
两个部分。
Linux系统调用包括两种方式:1.宏调用_syscall。2.库函数调用syscall。两者区别在于:
- 宏调用比较死板,对传入的参数有严格的要求,譬如每次调用需要传入传参类型、传参值、返回值类型、函数名字;而库函数调用方便,只需要传入中断号即可
- 宏调用直接使用寄存器进行函数调用,省去了多次访存的资源消耗,执行效率较高;库函数涉及到特权级切换,不同特权级栈之间的数据流动,消耗资源
对于用户来说,只希望通过一个简单的方式调用函数即可,不关系传参、返回值、返回类型,因此宏调用逐渐废弃。
add:宏调用举例,核心就是用内联汇编传参并触发中断
/* _syscallx表示传入x个形参,type为返回值类型,name为子功能函数名 */
#define _syscall3(type, name, type1, arg1, type2, arg2, type3, arg3) \
type name(type1 arg1, type2 arg2, type3 arg3){
long __res;
__asm__ volatile("push %%ebx; movl %2, %%ebx; int $0x80; push %%ebx" \
:"=a"(_res) \
:"0"(__NR_##name), "ri"((long)(arg1)), "c"((long)(arg2)), \
"d"((long)(arg3)) : "memory");
__syscall_return (type, _res);
}
解释:
"=a"(_res)
表示输出使用eax寄存器,将变量输出到_res中"0"(\__NR\_##name)
完成将子功能号加载进eax中,"0"
表示使用和第0个约束一样的寄存器,都是eax,\_\_NR\_##name
代入后为子功能号对应的宏"ri"((long)(arg1))
完成将arg1约束到通用寄存器中"c"((long)(arg2))
完成将arg2约束到通用寄存器ecx中"d"((long)(arg3))
完成将arg2约束到通用寄存器edx中
使用寄存器的好处:若用栈传递参数的话,调用者(用户进程)首先得把参数压在 3 特权级的栈中,然后内核将其读出来再压入 0 特权级栈,这涉及到两种栈的读写,故通过寄存器传递参数效率更高。
Ⅱ.系统调用实现
本部分完成的功能为,使用宏系统调用的方式实现系统调用,建立具有0-3个参数的宏调用接口
1.利用宏实现系统调用的具体思路
(1) 用中断门实现系统调用,效仿 Linux 用 0x80 号中断作为系统调用的入口 。
在kernel.S中实现0x80中断处理程序,本质是:保存上一个任务的上下文信息,根据传入的参数调用0x80的中断处理程序,执行结束后,恢复上一任务的现场
;;;;;;;;;;;;;;;;;;;; 0x80号中断处理程序 ;;;;;;;;;;;;;;;;;;;;;;
[bits 32]
extern syscall_table
section .text
global syscall_handler
syscall_handler:
;保存上下文环境
push 0
push ds
push es
push fs
push gs
pushad ;PUSHAD 指令压入 32 位寄存器,其入栈顺序是:EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI
push 0x80 ;此位置压入 0x80 也是为了保持统一的栈格式
;传入参数
push edx
push ecx
push ebx
;跳转执行
call [syscall_table + eax*4]
add esp+12 ; 跨过传入的三个参数
;返回
mov [esp + 8*4], eax ; 将返回值写入eax中
jmp intr_exit ; intr_exit 返回,恢复上下文
(2)在 IDT 中安装 0x80 号中断对应的描述符,在该描述符中注册系统调用对应的中断处理例程。
在interrupt.c中实现,本质是:根据中断处理程序、中断特权级DPL、创建0x80的中断描述符
#define IDT_DESC_CNT 0x81 // 目前总共支持的中断数,编号对应0x0~0x80
static struct gate_desc idt[IDT_DESC_CNT]; // idt是中断描述符表,本质上就是个中断门描述符数组
……
extern uint32_t syscall_handler(void); // 系统调用函数,在kernel.s中定义
……
/* 创建中断门描述符 */
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) {
p_gdesc->func_offset_low_word = (uint32_t)function & 0x0000FFFF;
p_gdesc->selector = SELECTOR_K_CODE;
p_gdesc->dcount = 0;
p_gdesc->attribute = attr;
p_gdesc->func_offset_high_word = ((uint32_t)function & 0xFFFF0000) >> 16;
}
/*初始化中断描述符表*/
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]);
}
/* 单独建立0x80号中断的中断描述符,系统调用对应的中断门dpl=3 */
make_idt_des(&idt[lastindex], IDT_DESC_ATTR_DPL3, syscall_handler);
put_str(" idt_desc_init done\n");
}
(3)建立系统调用子功能表 syscall_table ,利用 eax寄存器中的子功能号在该表中索引相应的处理函数。
本质是:构建全局的子功能表,每个元素为子功能处理函数,并绑定到子功能表数组中
/* syscall_init.c */
#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");
// SYS_GETPID对应getpid系统调用的标识位
syscall_table[SYS_GETPID] = sys_getpid;
put_str("syscall_init done\n");
}
(4)用宏实现用户空间系统调用接口_syscall ,最大支持 3 个参数的系统调用,故只需要完成_syscall[0-3]。寄存器传递参数, eax为子功能号, ebx保存第 1 个参数, ecx 保存第 2 个参数, edx 保存第 3 个参数 。
本质是:基于1、2、3步,实现宏调用接口
/* 无参数的系统调用 */
#define _syscall0(NUMBER) ({ \
int retval; \
asm volatile ( \
"int $0x80" \
: "=a" (retval) \
: "a" (NUMBER) \
: "memory" \
); \
retval; \
})
……
/* 返回当前任务pid */
uint32_t getpid() {
return _syscall0(SYS_GETPID);
}
(5)总结下当下文件结构中增加系统调用的步骤。
(1)在 syscall.h 中的结构 enum SYSCALL_NR 里添加新的子功能号。
(2)在 syscall.c 中增加系统调用的用户接口。
(3)在 syscall_init.c 中定义子功能处理函数并在 syscall_table 中注册 。
2.利用栈实现系统调用
要想在内核态下访问用户态的栈空间,关键在于如何得到用户栈的地址 。 不过好在处理器已经帮咱们埋下了伏笔,当从用户态进入内核态时,由于特权级发生了变化,处理器会自动在内核栈中压入 3 特权级栈的选择子 SS 及栈指针 esp,故我们在中断处理程序中可以从内核栈中把它们再读出来,由于我们把段描述符设置为了平坦模型,即一个段 4GB 大小,所以只要从内核栈中把 eip 读取出来就行了。有了 3 特权级栈的栈顶指针 ,再添加一定的偏移量,就能获得用户进程传入的参数。
具体研读P534
3.实现用户态下的printf函数
(1)动态内存分配的本质
printf
函数是可变长函数,操作系统最初诞生时,由于性能问题,编译时要求程序内存是已知的,即只允许使用静态内存。函数占用的也是静态内存,因此也得提前告诉编译器自己占用的内存大小 。但随着计算机的发展,出现了可变长数组、可变长函数,即允许程序动态分配内存了。真的是这样吗?
并非如此,只是操作系统的障眼法罢了。虽然函数的定义是可变长的,但实际上,在函数调用时,它的内存占用空间已经确定了。C调用约定:调用约定规定:由调用者把参数以从右向左的顺序压入栈中,并且由调用者清理堆栈中的参数。传入参数的个数在编译时期就已经确定下来了,本质还是静态内存分配。
(2)实现vsprintf,完成格式化部分填充
格式化字符串中有几个"%“,就从栈中取出几个形参,即使程序员传参的个数可能与”%"的个数匹配,编译器也不会报错。具体过程:
- 按字符读取format,当非%,将字符追加到输出缓冲区,format指针++;
- 当为%,switch格式化类型x/c/s/d;
- 当为c,直接追加;当为s,使用strcpy拼接;当为x,利用itoa函数,将传参按照int读取,转为十六进制后拼接到输出字符串;当为d,按照int读取,判断正负是否要追加’-'后,利用itoa函数得到转换后的字符串
/* 将整型转换成字符( 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;
// 如果倍数不为 0 ,则递归调用
if(i){
itoa(i, buf_ptr_addr, base);
}
// 开始赋值,base为16,因此存在数字和字母两种情况
else {
if(i < 10){
*((*buf_ptr_addr)++) = i + '0';
}
else{
*((*buf_ptr_addr)++) = i - 10 + 'A';
}
}
}
/*
* 将参数 ap 按照格式 format 输出到字符串 str ,并返回替换后 str 长度
* 若从format中识别到'%',从ap中取出一个元素拼接到str后面;反之,将format赋值给str
*/
uint32_t vsprintf(char* str, const char* format, va_list ap){
char* buf_ptr = str;
const char* index_ptr = format;
// format的每个字符
char index_char = *index_ptr;
// 参数个数
int32_t arg_int = 0;
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 'x':
arg_int = va_arg(ap, char);
itoa(arg_int, &buf_ptr, 16);
index_char = *(++index_ptr);
break;
case 'c':
*(buf_ptr++) = va_arg(ap, char);
index_char = *(++index_ptr);
break;
case 's':
arg_str = va_arg(ap, char);
strcpy(buf_ptr, arg_str);
buf_ptr += strlen(arg_str);
index_char = *(++index_ptr);
break;
case 'd':
arg_int = va_arg(ap, int);
/* 若是负数,将其转为正数后,在正数前面输出个负号'-' */
if(arg_int < 0){
arg_int = 0 - arg_int;
*(buf_ptr++) = '-';
}
/* 将int转为str赋值给buf_str */
itoa(arg_int, buf_ptr, 10);
index_char = *(++index_ptr);
break;
}
}
return strlen(str);
}
(3)实现printf
printf
函数将格式化后的信息输出到标准输出(通常是屏幕),但其只是格式化输出的外壳,真正起到“格式化”作用的是 vsprintf
函数,真正起“输出”作用的是 write 系统调用。
/* 格式化输出字符串format */
uint32_t printf(const char* format, ...){
va_list args;
// 将printf参数存入args指针指向的栈中,栈中每个元素大小为4字节
// 当遍历format遇到%号了,取arg指向的下一个元素
va_start(args, format);
char buf[1024] = {0}; // 用于存储字符串
vsprintf(buf, format, args);
va_end(args);
return write(buf);
}
4.完善堆内存
(1)内存单元管理——arena
之前的内存管理分配的内存都是以 4MB大小的页框为单位的 ,当我们需要小内存块时,就显得很浪费了,因此我们改进一下内存管理方式,使其满足任意内存大小的分配。arena 是很多开源项目中都会用到的内存管理概念,将一大块内存划分成多个小内存块,每个小内存块之间互不干涉,可以分别管理,这样众多的小内存块就称为arena。arena 是由“ 一大块内存”被划分成无数“小内存块”的内存仓库,我们在原有内存管理系统的基础上实现 arena。
根据请求的内存量的大小, arena 的大小也许是 1 个页框,也许是多个页框,随后再将它们平均拆分成多个小内存块。按内存块的大小,可以划分出多种不同规格的arena ,比如一种 arena 中全是 16 字节大小的内存块,故它只响应 16 字节以内的内存分配,另一种arena 中全是 32 字节的内存块,故它只响应 32 字节以内的内存分配。
arena 是个提供内在分配的数据结构,它分为两部分,一部分是元信息,用来描述自己内存池中空闲内存块数量,这其中包括内存块描述符指针。通过它可以间接获知本 arena 所包含内存块的规格大小,此部分占用的空间是固定的,约为 12 字节。另一部分就是内存池区域,这里面有无数的内存块,此部分占用 arena 大量的空间。
arena分配的内存空间最大不超过(4KB-12B),以2的指数划分,block_size<2KB
,因此arena内存块最大不超过1024B。故arena支持的内存块为16B、32B、64B、128B、256B、512B、1024B。
起始为某一类型内存块分配的arena 只有 1 个,当此 arena 中的全部内存块都被分配完时,系统再创建一个同规格的arena 继续提供该规格的内存块,当此arena 又被分配完时,再继续创建出同规格的arena, arena 规模逐渐增大,逐步形成 arena集群。既然同一类内存块可以由多个 arena 提供,为了跟踪每个arena中的空闲内存块,分别为每一种规格的内存块建立一个内存块描述符,在其中记录内存块规格大小。
但是arena内存分配时只允许存在一种内存规格,不同内存规格的arena对应不同的内存块描述符,因此内存块规格有多少种,内存块描述符就有多少种,各种内存块描述符的区别就是 block_size 不同, free_list 中指向的内存块规格不同 。
(2)实现arena内存块管理
(2.1)创建内存块描述符
/* 内存块 */
struct mem_block {
// 双向链表结构体,标记前驱节点和后继节点
struct list_elem free_elem;
};
/* 内存块描述符 */
struct mem_block_desc{
// 内存块大小
uint32_t block_size;
// 一个arena可容纳的mem_block数量
uint32_t block_per_arena;
// 目前可用的 mem_block 链表
struct list free_list;
};
#define DESC_CNT 7 // 内存块描述符个数,总共7个block_size对应7中内存块描述符
(2.2)初始化内存块描述符
/* 初始化7种内存块描述符,为malloc做准备 */
void block_desc_init(struct mem_block_desc* desc_array) {
uint16_t block_index, block_size = 16;
// 初始化内存块描述符数组
for(block_index = 0; block_index < DESC_CNT; block_index++){
desc_array[block_index].block_size = block_size;
// 初始化 arena 中的内存块数量
desc_array[block_index].block_per_arena = (PG_SIZE - sizeof(struct arena)) / block_size;
list_init(&desc_array[block_index].free_list);
// 更新为下一个规格内存块
block_size *= 2;
}
}
(2.3)完善arena
/* 返回 arena 中第 idx 个内存块的地址 */
static struct mem_block* arena2block(struct arena* a, uint32_t idx) {
return (struct mem_block*)((uint32_t*)a + sizeof(struct arena) + a->desc->block_size * idx);
}
/* 返回内存块 b 所在的 arena 地址,arena占一个页框大小,因此地址为高10位,剩下4KB内分配内存块 */
static struct arena* block2arena(struct mem_block* b) {
return (struct arena*)((uint32_t)b & 0xfffff000);
}
(3)实现sys_malloc函数
- 判断是在用户内存池/内核内存池:获取当前进程,根据pgdir是否为空判定是内核/用户,用户进程在创建时pgdir就直接更新了
- 判断分配的是大字节(>1024B)还是小字节
- 如果为大字节,直接分配,无需更新内存块描述符以及arena划分
- 如果为小字节,先判断当前arena是否有可用的内存块,即descs[desc_idx].free_list是否为空,
- 若为空,则需要创建新的arena,将其拆分为内存块,并添加到内存块描述符的 free_list 中
- 若不为空,直接分配内存块
(3.1)判断在用户内存池/内核内存池
/* 在堆中申请 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 = running_thread();
/* 判断用哪个池 */
// 如果为内核线程
if (cur->pgdir == NULL)
{
// arena已经在mem_init()中初始化
PF = PF_KERNEL;
mem_pool = &kernel_pool;
pool_size = kernel_pool.pool_size;
descs = cur->u_block_desc;
}
else
{
PF = PF_USER;
mem_pool = &user_pool;
pool_size = user_pool.pool_size;
descs = cur->u_block_desc;
}
……
}
(3.2)如果是大内存
// 大内存/小内存
struct arena *a;
struct mem_block *b;
lock_acquire(&mem_pool->lock);
if (size > 1024)
{
uint32_t pg_cnt = DIV_ROUND_UP(size + sizeof(struct arena), PG_SIZE);
// 分配内存块
a = malloc_page(PF, pg_cnt);
if (a != NULL)
{
memset(a, 0, pg_cnt * PG_SIZE);
a->large = true;
a->cnt = pg_cnt;
a->desc = NULL;
lock_release(&mem_pool->lock);
return (void *)(a + 1);
}
else
{
lock_release(&mem_pool->lock);
return NULL;
}
}
(2.4.3)如果是小内存
else
{
uint32_t desc_idx;
// 找到最合适的block_size
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))
{
// 分配一页框作为arena
a = malloc_page(PF, 1);
if (a == NULL)
{
lock_release(&mem_pool->lock);
return NULL;
}
memset(a, 0, PG_SIZE);
a->cnt = descs[desc_idx].block_per_arena;
a->large = false;
a->desc = &descs[desc_idx];
uint32_t block_idx;
enum intr_status old_status = intr_disable();
// 创建新的arena后,需要将其拆分为内存块,并添加到内存块描述符的 free_list 中
for(block_idx = 0; block_idx < a->cnt; block_idx++){
// 获取分配的内存块
b = arena2block(a, block_idx);
ASSERT(!elem_find(&a->desc->free_list, &b->free_elem));
list_append(&a->desc->free_list, &b->free_elem);
}
intr_set_status(old_status);
}
/* 存在空闲内存,根据descs中free_list的地址找到mem_block地址 */
b = elem2entry(struct mem_block, free_elem, list_pop(&descs[desc_idx].free_list));
memset(b, 0, sizeof(descs[desc_idx].block_size));
a = block2arena(b);
a->cnt--;
/* 释放锁 */
lock_release(&mem_pool->lock);
return NULL;
}
}
(4)实现sys_free函数
(4.1)释放物理内存
/* 释放进程的物理内存,物理空间所有进程共享,因此不存在running_thread()读取
* 1.判断是内核/用户,物理空间内,内核位于0地址
* 2.根据地址找到位图地址
*/
void pfree(uint32_t pg_phy_addr) {
uint32_t bit_idx = 0;
struct pool* mem_pool;
if(pg_phy_addr >= user_pool.phy_addr_start){
mem_pool = &user_pool;
bit_idx = (pg_phy_addr - mem_pool->phy_addr_start) / PG_SIZE;
}
else{
mem_pool = &kernel_pool;
bit_idx = (pg_phy_addr - mem_pool->phy_addr_start) / PG_SIZE;
}
bitmap_set(&mem_pool->pool_bitmap, bit_idx, 0);
}
(4.2)取消虚拟内存和物理内存的映射关系
本质是将页表pte的P位置为0,然后使用汇编命令更新快表tlb,CPU就会认为该页表无效
/* 去掉页表中虚拟地址 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
}
(4.3)释放虚拟内存
1.判断是内核/用户虚拟内存池
2.利用位图bitmap_set释放pg_cnt大小的内存空间
/* 在虚拟地址池中释放以_vaddr 起始的连续 pg_cnt 个虚拟页地址 */
static void vaddr_remove(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt) {
uint32_t bitmap_vaddr_start, vaddr = (uint32_t)vaddr, cnt = 0;
// 如果为内核内存池
if(pf == PF_KERNEL){
// kernel_pool为内核物理内存池,kernel_vaddr为内核的虚拟内存池
bitmap_vaddr_start = (vaddr - kernel_vaddr.vaddr_start) / PG_SIZE;
while(cnt < pg_cnt){
bitmap_set(&kernel_vaddr.vaddr_bitmap, bitmap_vaddr_start+cnt++, 0);
}
}
// 用户内存池,需要具体到执行的进程
else{
struct task_struct* cur = running_thread();
bitmap_vaddr_start = (vaddr - cur->userprog_vaddr.vaddr_start)/PG_SIZE;
while(cnt < pg_cnt){
bitmap_set(&cur->userprog_vaddr.vaddr_bitmap, bitmap_vaddr_start+cnt++, 0);
}
}
}
(4.4)释放某一虚拟地址起始的pg_cnt个物理页框
cnt个那么应该是循环遍历
先调用 pfree
清空物理地址位图中的相应位,再调用 page_table_pte_remove
删除页表中此地址的 pte , 最后调用 vaddr_remove
清除虚拟地址位图中的相应位。
/* 释放以虚拟地址 vaddr 为起始的 cnt 个物理页框 */
void mfree_page(enum pool_flags pf, void *_vaddr, uint32_t pg_cnt)
{
uint32_t vaddr = (uint32_t *)_vaddr, page_cnt = 0;
ASSERT((pg_cnt >= 1) && (vaddr % PG_SIZE == 0));
uint32_t *pg_phy_addr = addr_v2p(vaddr);
if (pf == PF_KERNEL)
{
vaddr -= PG_SIZE;
// 将cnt个页框再循环内依次取消掉
while (page_cnt < pg_cnt)
{
vaddr += PG_SIZE;
pg_phy_addr = addr_v2p(vaddr);
ASSERT((pg_phy_addr > kernel_pool.phy_addr_start) && (pg_phy_addr < \
user_pool.phy_addr_start) && (pg_phy_addr % PG_SIZE == 0));
// 释放物理内存
pfree(pg_phy_addr);
// 页表映射取消
page_table_pte_remove(vaddr);
page_cnt++;
}
// 释放虚拟内存
vaddr_remove(pf, vaddr, pg_cnt);
}
else
{
vaddr -= PG_SIZE;
// 将cnt个页框再循环内依次取消掉
while (page_cnt < pg_cnt)
{
vaddr += PG_SIZE;
pg_phy_addr = addr_v2p(vaddr);
ASSERT((pg_phy_addr > user_pool.phy_addr_start) && (pg_phy_addr % PG_SIZE == 0));
// 释放物理内存
pfree(pg_phy_addr);
// 页表映射取消
page_table_pte_remove(vaddr);
page_cnt++;
}
// 释放虚拟内存
vaddr_remove(pf, vaddr, pg_cnt);
}
}
(4.5)完成sys_free()函数
arena在内存分配时按照大内存和小内存的方式分配,那么在释放时也对应两种释放类型:对于大内存的处理称之为释放,就是把页框在虚拟内存池和物理内存池的位图中将相应位置 0。对于小内存的处理称之为“回收”,是将 arena 中的内存块重新放回到内存块描述符中的空闲块链表 free_list。
总结
1.说明printf在中断中不可重入的原因
1.printf的原理是系统调用,那么必定涉及到中断嵌套,需要保护现场,影响中断响应速度
2.printf系统调用的具体原理讲讲。vsprintf和write
2.arena内存分配原理
根据申请的内存大小分配合适的页框管理内存,以1024B为界,大于1024B,则分配整个页框的空间;小于1024B,则按照16、32、64、128、512、1024去匹配最合适的内存块。找到arena划分的合适的内存块后,返回分配内存块的地址。
3.分配内存/释放内存的一般步骤
(1)内存分配
(1)基于I/O位图,在用户/内核虚拟内存池中分配虚拟内存,申请虚拟地址就是将虚拟内存池位图中的相应位
清 0
(2)基于I/O位图,在用户/内核物理内存池中分配物理内存,申请物理地址就是将物理内存池位图中的相应位
清 0
(3)在页表中建立物理内存和虚拟内存的映射关系。
(2)内存释放
(1)基于I/O位图,在用户/内核物理内存池中释放物理内存,回收物理地址就是将物理内存池位图中的相应位
清 0
(3)在页表中取消物理内存和虚拟内存的映射关系,本质是将页表pte的P位置为0,CPU 只要检测到 P 位为 0,就会认为该 pte 无效,根本不会关心 pte 所指向的物理页框的地址是否属于可访问的物理内存的范围。
(2)基于I/O位图,在用户/内核虚拟内存池中释放虚拟内存,回收虚拟地址就是将虚拟内存池位图中的相应位
清 0
4.怎么根据虚拟地址找到pte和pde?
pte和pde就是在虚拟地址的基础上加上了特权级、读/写位、系统/用户、页存在属性位
5.系统调用过程
- 为0x80中断号添加中断描述符,在该中断描述符中注册中断处理程序
- 建立系统子功能表syscall_table,将子功能函数与表绑定
- 封装成一个宏,支持通过1-3个参数的系统调用,eax为子功能号,其他参数传入ebx、ecx、edx
6.位图-内存分配辅助工具
但凡涉及到虚拟地址的,都可以通过虚拟内存池+位图位号*PG_SIZE求得;物理内存池同理
(6.1)位图是如何找到第一个空闲页的下标的
每个位图元素为8位1字节,位图的每一位对应一页内存,因此一个位图元素对应8页内存。在内存分配时,首先和0xff匹配,找到存在有空闲位的元素,再通过每位&确定到具体的哪一位;
/* 先逐字节比较,蛮力法 */
while (( 0xff == btmp->bits[idx_byte]) && (idx_byte < btmp->btmp_bytes_len)) {
/* 1表示该位已分配,所以若为0xff,则表示该字节内已无空闲位,向下一字节继续找 */
idx_byte++;
}
/* 若在位图数组范围内的某字节内找到了空闲位,
* 在该字节内逐位比对,返回空闲位的索引。*/
int idx_bit = 0;
/* 和btmp->bits[idx_byte]这个字节逐位对比 */
while ((uint8_t)(BITMAP_MASK << idx_bit) & btmp->bits[idx_byte]) {
idx_bit++;
}
int bit_idx_start = idx_byte * 8 + idx_bit; // 空闲位在位图内的下标
(6.2)位图是如何找到符合大小要求的空闲内存的
依次判断接下来的pg_cnt个位图下标是否都为0,来判定是否有符合要求的内存空间。
uint32_t bit_left = (btmp->btmp_bytes_len * 8 - bit_idx_start); // 记录还有多少位可以判断
uint32_t next_bit = bit_idx_start + 1;
uint32_t count = 1; // 用于记录找到的空闲位的个数
bit_idx_start = -1; // 先将其置为-1,若找不到连续的位就直接返回
while (bit_left-- > 0) {
if (!(bitmap_scan_test(btmp, next_bit))) { // 若next_bit为0
count++;
} else {
count = 0;
}
if (count == cnt) { // 若找到连续的cnt个空位
bit_idx_start = next_bit - cnt + 1;
break;
}
next_bit++;
}
mp->bits[idx_byte]) {
idx_bit++;
}
int bit_idx_start = idx_byte * 8 + idx_bit; // 空闲位在位图内的下标
##### (6.2)位图是如何找到符合大小要求的空闲内存的
依次判断接下来的pg_cnt个位图下标是否都为0,来判定是否有符合要求的内存空间。
```c
uint32_t bit_left = (btmp->btmp_bytes_len * 8 - bit_idx_start); // 记录还有多少位可以判断
uint32_t next_bit = bit_idx_start + 1;
uint32_t count = 1; // 用于记录找到的空闲位的个数
bit_idx_start = -1; // 先将其置为-1,若找不到连续的位就直接返回
while (bit_left-- > 0) {
if (!(bitmap_scan_test(btmp, next_bit))) { // 若next_bit为0
count++;
} else {
count = 0;
}
if (count == cnt) { // 若找到连续的cnt个空位
bit_idx_start = next_bit - cnt + 1;
break;
}
next_bit++;
}