第十二章-系统调用

Ⅰ.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,完成格式化部分填充

格式化字符串中有几个"%“,就从栈中取出几个形参,即使程序员传参的个数可能与”%"的个数匹配,编译器也不会报错。具体过程:

  1. 按字符读取format,当非%,将字符追加到输出缓冲区,format指针++;
  2. 当为%,switch格式化类型x/c/s/d;
  3. 当为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函数
  1. 判断是在用户内存池/内核内存池:获取当前进程,根据pgdir是否为空判定是内核/用户,用户进程在创建时pgdir就直接更新了
  2. 判断分配的是大字节(>1024B)还是小字节
  3. 如果为大字节,直接分配,无需更新内存块描述符以及arena划分
  4. 如果为小字节,先判断当前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.系统调用过程
  1. 为0x80中断号添加中断描述符,在该中断描述符中注册中断处理程序
  2. 建立系统子功能表syscall_table,将子功能函数与表绑定
  3. 封装成一个宏,支持通过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++;          
   }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值