第十二章-系统调用

news2024/11/20 1:27:07

Ⅰ.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++;          
   }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1081530.html

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

相关文章

拼多多怎么引流商家?建议收藏的几个方法,拼多多引流脚本详细使用教学分享

大家好我是你们的小编一辞脚本&#xff0c;今天给大家分享新的知识&#xff0c;很开心可以在CSDN平台分享知识给大家,很多伙伴看不到代码我先录制一下视频 在给大家做代码&#xff0c;几个关于商家引流的知识今天给大家分享一下拼多多引流脚本的知识和视频演示 不懂的小伙伴可…

【微服务治理】Spring Cloud 断路器Hystrix实战应用

系列文章目录 第一章 Java线程池技术应用 第二章 CountDownLatch和Semaphone的应用 第三章 Spring Cloud 简介 第四章 Spring Cloud Netflix 之 Eureka 第五章 Spring Cloud Netflix 之 Ribbon 第六章 Spring Cloud 之 OpenFeign 第七章 Spring Cloud 之 GateWay 第八章 Sprin…

es elasticsearch 基础

es https://www.elastic.co/guide/en/elasticsearch/reference/6.8/getting-started.html 倒排索引&#xff1a; 正排–>从目录到文章内容 倒排–>内容到目录文章标题 if we dont need full text search ,we dont need to create 倒排 using text type 分词&#xff1a…

优雅而高效的JavaScript——解构赋值

&#x1f643;博主&#xff1a;小猫娃来啦 &#x1f643;文章核心&#xff1a;优雅而高效的JavaScript——解构赋值 文章目录 什么是解构赋值数组解构赋值基本用法默认值剩余参数 对象解构赋值基本用法默认值剩余参数 解构赋值的优势和应用场景代码简化和可读性提高交换变量值函…

硬件基本功--过流、过压保护电路

1.简介 过流保护(OCP)&#xff1a;当电路电流超过预定最大值时&#xff0c;使保护装置动作的一种保护方式。不允许超过预定最大值电流&#xff0c;不然会烧坏电路的器件。过压保护(OVP)&#xff1a;被保护电路电压超过预定的最大值时&#xff0c;使电源断开或使受控设备电压降低…

企业如何实现财务无纸化?票档一体化建设势在必行

随着“大智移云物区”等信息技术的发展&#xff0c;传统的财务管理开始向信息化、数字化转型&#xff0c;电子发票、电子凭证也逐渐取代传统的纸质档案资料&#xff0c;促使企业转型升级并逐步实现财务无纸化&#xff0c;助力降本增效。 同时&#xff0c;在政策强力推动下&…

【赠书活动】如何提高C++代码的性能

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

C++QT-day5

#include <iostream> //多继承using namespace std;class Sofa { private:string sitting; public:Sofa(){cout << "Sofa无参构造函数" << endl;}Sofa(string sitting):sitting(sitting){cout << "Sofa有参构造函数" << …

数据可视化——ucharts的使用

目录 1.引入 uCharts插件 2.使用组件 3.修改默认渲染数据的内容 1.修改柱状图上面渲染的内容 2.修改点击展示的内容 1.修改展示内容 2.自定义展示内容 4.修改其他设置查看官方文档 官网地址&#xff1a;uCharts官网 - 秋云uCharts跨平台图表库 uCharts是一款基于canvas API…

线性数据结构集会

目录 序言 一、数组 数组的优点 数组的缺点 数组的适用场景 二、链表 链表的优点 链表的缺点 链表的使用场景 链表的种类 Java单向链表的实现 三、队列 队列的特点 四、栈 栈的特点 栈的适用场景 五、时间复杂度简述 序言 线性数据结构是一种将数据元素以线性…

C语言,洛谷题:你的飞碟在这

文中要把字母A对应到1&#xff0c;Z对应到26&#xff0c;这时候就要用上ASCII表了。A对应的ASCII码值是65&#xff0c;要让A对应1&#xff0c;只要将A减去64&#xff0c;后面的字母也是一样的只要减去64&#xff0c;就能对应其26字母中的顺序。 要存储字符串对应的每个元素的数…

计算机毕业设计选什么题目好?springboot 学生考勤管理系统

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…

Java实验一 Java语言基础(12题)

文章目录&#xff1a; 1、我国历法中的天干、地支和生肖的排列是有规律的。天干的顺序是“甲乙丙丁戊己庚辛壬癸”&#xff0c;地支的顺序是“子丑寅卯辰巳午未申酉戌亥”&#xff0c;生肖的顺序是“鼠牛虎兔龙蛇马羊猴鸡狗猪”。天干、地支、生肖的计算方法非常简单&#xff…

Etsy玩家必看之7个运营技巧

2023年跨境电商行业趋势愈发旺盛&#xff0c;目前正处于红利期&#xff0c;而作为近年来的电商网红“Etsy”&#xff0c;以其低成本低竞争高回报的优势吸引了大批的跨境电商玩家。但仅仅入驻照搬其他平台经验很难出单&#xff0c;如果你正烦恼这个问题&#xff0c;那么接下来的…

软件测试学习(一)基础概念、实质、说明书测试、分类、动态黑盒测试

软件测试概念、背景 软件无处不在。然而&#xff0c;软件是人写的一所以不完美。 世界上有完美的软件吗&#xff1f;NO 世界上没有完美的软件。所有软件都可能存在缺陷、错误或漏洞&#xff0c;无论是操作系统、应用程序、游戏还是其他类型的软件。这些问题可能会导致功能问题…

1.软件开发-HTML结构-元素剖析

元素的嵌套 代码注释 ctrl/ URL url 统一资源定位符 一个给定的独特资源在web上的地址 URI

那些年,我们一起发现的Bug

一、背景 在这篇文章中&#xff0c;分享一些自己在工作中或别人发现的一些常见Bug&#xff0c;与大家共同成长~ 二、常见Bug分类 1、前后命名不一致 举个例子 接口入参中的名称是&#xff1a;aslrboot Java代码中使用的名称是&#xff1a;aslrBoot Codis中存储的名称是&…

LCD简介

lcd简介 LCD简介1、分辨率2、像素格式3、LCD时间参数4、RGB LCD屏幕时序6、像素时钟 此文章摘抄于正点原子的嵌入式linux驱动开发指南&#xff0c;仅作为笔记&#xff0c;以放便忘记时查阅 LCD简介 LCD全称是Liquid Crystal Display,也就是液晶显示器&#xff0c;其显示的基本原…

机器人的组成

机器人是一个机电一体化的设备&#xff0c;从控制的角度来看&#xff0c;机器人系统可以分成四大部分&#xff0c;即执行机构、驱动系统、传感系统和控制系统. 各部分之间的控制关系如下图所示。 执行机构 执行机构是直接面向工作对象的机械装置&#xff0c;相当于人体的手和脚…

3年测试经验,测试用例应该达到这个水平才合格

状态迁移法主要关注在测试状态转移的正确性上面。对于一个有限状态机&#xff0c;通过测试验证其在给定的条件内是否能够产生需要的状态变化&#xff0c;有没有不可达的状态和非法的状态&#xff0c;是否可能产生非法的状态转移等。通过构造能导致状态迁移的事件&#xff0c;来…