第十一章-用户进程

news2024/11/18 8:11:56

Ⅰ.为什么需要任务状态段TSS

1.LDT简介

已经不使用LDT加载任务了。因为当前运行的任务,其 LDT 位于 LDTR 指向的地址,这样 CPU 才能从中拿到任务运行所需要的资源(指令和数据)。因此,每切换一个任务时,需要用 lldt 指令重新加载新任务的 LDT 到 LDTR。 虽然介绍了 LDT,但咱们并不打算使用它,因为每加入一个任务都需要在 GDT 中添加新的 LDT 描述符,还要重新加载 LDTR,比较麻烦。

(1)LDT的使用方法

按照内存分段的模式,内存中的程序分为了数据段、代码段等资源,由于每个程序的资源不同,因此Intel提出为每个程序提供专门的数据结构来管理资源,就是LDT表。

由于LDT表是任务私有的,因此,在内存中的位置并非固定,那么如何访问呢?

利用GDT表全局唯一的特性,由于GDT表位置是可以通过GDTR寄存器找到,GDT表中存储的是描述符,描述符中存储的是任务起始地址和偏移量,那LDT也是通过起始地址+偏移量得到的,因此可以将LDT作为描述符存入GDT表。下图为LDT描述符格式:

在这里插入图片描述

在 LDT 描述符中,描述符的 D 位和 L 位固定为 0 。

LDT 描述符属于系统段描述符,因此 S 为 0。在 S 为 0 的前提下,若 TYPE 的值为 0010 ,这表示此描述符是 LDT 描述符。其字段意义同段描述符相同。

LDT&GDT描述符格式
32bits12bits20bits
段基址段属性段界限
GDTR&LDTR中存储内容为:
32bits16bits
段基地址段界限
(2)LDT的加载

根据GDT表中的LDT描述符可以得到LDT在内存的起始地址+偏移量,通过lldt指令将LDT装入LDTR寄存器,CPU就可以找到LDT表了。

lldt指令使用方法:lldt "16 位通用寄存器 "或" 16 位内存单元" 。不管操作数中寄存器还是内存,其值必须是 LDT 选择子。

加载GDT表的指令为lgdt,lgdt "16位内存单元"&"32位内存单元" 。前 16 位表示 GDT 的偏移大小,后 32 位表示 GDT 的起始地址。

lgdt 的操作数是 GDT 表的偏移量及起始地址,而 lldt 的操作数是 ldt 在 GDT 中的选择子。

(3)LDT的访问

在这里插入图片描述

选择器是中 16 位的LDT选择子,描述符缓冲器中是 LDT 的起始地址及偏移大小等属性。 LDT 中的描述符全部用于指向任务自己的内存段,该如何引用它们呢?

段选择子16位,除了描述符索引外,第0~1位 RPL,表示请求特权级;第2位为TI位,表示是在LDT/GDT表检索,TI=1表示在LDT中检索,TI=0,表示在GDT中检索。
在这里插入图片描述

此外,GDT的第0个段描述符不可用,为了防止GDT未初始化就使用;而LDT的可用,因为只有初始化以后的程序才会有LDT。

2.TSS的作用

加载新任务时, CPU 自动把当前任务(旧任务)的状态存入当前任务的 TSS ,然后将新任务 TSS 中的数据载入到对应的寄存器中,这就实现了任务切换。 TSS 就是任务的代表, CPU 用不同的 TSS 区分不同的任务,因此任务切换的本质就是 TSS 的换来换去

CPU如何知道TSS换了?

在 CPU 中有一个专门存储 TSS 信息的寄存器:TR 寄存器,它始终指向当前正在运行的任务,因此,“在 CPU 眼里”,任务切换的实质就是 TR 寄存器指向不同的 TSS 。

(1)TSS结构

在这里插入图片描述

TSS 描述符属于系统段描述符,因此 S 为 0 ,在 S 为 0 的情况下, TYPE 的值为10B1 。我们这里关注
一下 B 位, B 表示 busy 位, B 位为 0 时,表示任务不繁忙, B 位为 l 时,表示任务繁忙。

当任务刚被创建时,此时尚未上 CPU 执行,因此,此时的 B 位为 0, TYPE 的值为 1001。当任务开始上 CPU 执行时,处理器自动地把 B 位置为1,此时 TYPE 的值为 1011 。当任务被换下 CPU 时,处理器把 B 位置0。 注意, B 位是由 CPU 来维护的,不需要咱们人工干预。

TSS 中还有“ I/O 位图”和“上一个任务的 TSS 指针”

(2)基于TSS的任务切换

在这里插入图片描述

入图,任务的执行需要GDT、LDT、TSS等信息,GDT在全局统一,而LDT和TSS根据不同任务具有不同的参数,因此在任务切换时,需要重新装在任务的LDT、TSS,同时需要保存当前任务的LDT、TSS指针,保证任务能够正常恢复现场。

注意:

  • TSS 和 LDT 都只能且必须在 GDT 中注册描述符。
  • TR 寄存器中存储的是 TSS 的选择子, LDTR 寄存器中存储的是 LDT 的选择子, GDTR 寄存器中存储的是GDT 的起始地址及界限偏移(由于第0个描述符作为初始标记,因此大小减1)
3.实现TSS

由于每次创建新的TSS代表任务切换,因此TSS的实现包括tss初始化、载入GDT表

(1)创建gdt描述符并初始化
/*			global.h 定义gdt描述符结构体			*/
struct gdt_desc {
   uint16_t limit_low_word;
   uint16_t base_low_word;
   uint8_t  base_mid_byte;
   uint8_t  attr_low_byte;
   uint8_t  limit_high_attr_high;
   uint8_t  base_high_byte;
}; 
/*			tss.c	创建并初始化gdt描述符		*/
static struct gdt_desc make_gdt_desc(uint32_t* desc_addr, uint32_t limit, uint8_t attr_low, uint8_t attr_high){
    uint32_t desc_base = (uint32_t)desc_addr;
    struct gdt_desc desc;

    desc.limit_low_word = limit & 0x0000ffff;
    desc.base_low_word = desc_base & 0x0000ffff;
    desc.base_mid_byte = (desc_base & 0x00ff0000) >> 16;
    desc.base_high_byte = desc_base >> 24;
    desc.attr_low_byte = (uint8_t)(attr_low);
    // limit总共20位,分为低16位和高4位
    desc.limit_high_attr_high = ((limit & 0x000f0000) >> 16) + (uint8_t)(attr_high);
    return desc;
}
(2)tss结构
/* 任务状态段tss结构 */
struct tss {
    uint32_t backlink;
    uint32_t* esp0;
    uint32_t ss0;
    uint32_t* esp1;
    uint32_t ss1;
    uint32_t* esp2;
    uint32_t ss2;
    uint32_t cr3;
    uint32_t (*eip) (void);
    uint32_t eflags;
    uint32_t eax;
    uint32_t ecx;
    uint32_t edx;
    uint32_t ebx;
    uint32_t esp;
    uint32_t ebp;
    uint32_t esi;
    uint32_t edi;
    uint32_t es;
    uint32_t cs;
    uint32_t ss;
    uint32_t ds;
    uint32_t fs;
    uint32_t gs;
    uint32_t ldt;
    uint32_t trace;
    uint32_t io_base;
}; 
(3)TSS初始化

初始化包括开辟TSS空间、载入GDT

/* 在gdt中创建tss并重新加载gdt */
void tss_init() {
   put_str("tss_init start\n");
   uint32_t tss_size = sizeof(tss);
   memset(&tss, 0, tss_size);
   tss.ss0 = SELECTOR_K_STACK;
   tss.io_base = tss_size;

/* gdt段基址为0x900,把tss放到第4个位置,也就是0x900+0x20的位置 */

  /* 在gdt中添加dpl为0的TSS描述符 */
  *((struct gdt_desc*)0xc0000920) = make_gdt_desc((uint32_t*)&tss, tss_size - 1, TSS_ATTR_LOW, TSS_ATTR_HIGH);

  /* 在gdt中添加dpl为3的数据段和代码段描述符 */
  *((struct gdt_desc*)0xc0000928) = make_gdt_desc((uint32_t*)0, 0xfffff, GDT_CODE_ATTR_LOW_DPL3, GDT_ATTR_HIGH);
  *((struct gdt_desc*)0xc0000930) = make_gdt_desc((uint32_t*)0, 0xfffff, GDT_DATA_ATTR_LOW_DPL3, GDT_ATTR_HIGH);
   
  /* gdt 16位的limit 32位的段基址 */
   uint64_t gdt_operand = ((8 * 7 - 1) | ((uint64_t)(uint32_t)0xc0000900 << 16));   // 7个描述符大小
   asm volatile ("lgdt %0" : : "m" (gdt_operand));
   asm volatile ("ltr %w0" : : "r" (SELECTOR_TSS));
   put_str("tss_init and ltr done\n");
}

Ⅱ.实现用户进程

1.实现用户进程原理

在这里插入图片描述

在 thread_start(……,function,……)的调用中,function 是我们最终在线程中执行的函数。在tread_start 内部,先是通过 get_kernel_pages(1)在内核内存池中获取 1 个物理页做线程的 pcb,即 thread,接着调用init_thread 初始化该线程 pcb 中的信息,然后再用 thread_create 创建线程运行的栈,实际上是将栈中的返回地址指向了 kernel_thread 函数,因此相当于调用了 kernel_thread,在 kernel_thread中通过调用 function 的方式使 function 得到执行。

此处可以深挖怎么实现的内存池分配一页(4k)的虚拟内存,涉及到位图操作位图的分配方式,利用bitmap_scan()函数,由于位图是用每一位来表示定长内存空间的使用情况,因此使用bits数组的0//1来表示内存的使用情况,当需要分配内存时,从bits数组中找到第一个为值0的,向后检索到cnt个长度,如果都为0,那么就分配,同时将bits数组更新。

2.实现用户进程的虚拟地址空间

值得注意的是,进程是拥有计算机资源的,因此需要开辟单独的空间,如页表,为了获取需要标记进程虚拟内存分配情况,因此进程的PCB结构体不仅需要保存自己页表的虚拟地址,还需要保存自己的虚拟内存

进程PCB如下:

/* 进程或线程的pcb,程序控制块 */
struct task_struct {
   uint32_t* self_kstack;	 // 各内核线程都用自己的内核栈
   enum task_status status;
   char name[16];
   uint8_t priority;
   uint8_t ticks;	   // 每次在处理器上执行的时间嘀嗒数

/* 此任务自上cpu运行后至今占用了多少cpu嘀嗒数,
 * 也就是此任务执行了多久*/
   uint32_t elapsed_ticks;

/* general_tag的作用是用于线程在一般的队列中的结点 */
   struct list_elem general_tag;				    

/* all_list_tag的作用是用于线程队列thread_all_list中的结点 */
   struct list_elem all_list_tag;

   uint32_t* pgdir;              // 进程自己页表的虚拟地址
   struct vitual_addr userprog_vaddr;	// 用户进程的虚拟地址
   uint32_t stack_magic;	 // 用这串数字做栈的边界标记,用于检测栈的溢出
};
3.为进程创建页表和3特权级栈

大多数情况下,用户进程在特权级 3 下工作,因此,我们还要为用户进程创建在 3 特权级的栈。栈也是内存区域,所以,咱们还得为进程分配内存(虚拟内存)作为 3 级栈空间。

(1)内存池分配过程回忆

内存分配涉及到内核内存池和用户内存池。内存池分配过程:

  • bitmap_scan()遍历内存池,找到第一个满足pg_cnt大小的空闲内存,返回内存在位图数组bits中的起始地址;
  • 若找到了,则修改bits数组的标记位为1,反之返回NULL;
  • 返回分配的内存起始地址
/* 在pf表示的虚拟内存池中申请pg_cnt个虚拟页,
 * 成功则返回虚拟页的起始地址, 失败则返回NULL */
static void *vaddr_get(enum pool_flags pf, uint32_t pg_cnt)
{
   int vaddr_start = 0, bit_idx_start = -1;
   uint32_t cnt = 0;
   if (pf == PF_KERNEL)
   {
      bit_idx_start = bitmap_scan(&kernel_vaddr.vaddr_bitmap, pg_cnt);
      if (bit_idx_start == -1)
      {
         return NULL;
      }
      while (cnt < pg_cnt)
      {
         bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1);
      }
      vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
   }
   // 用户内存池分配
   else
   {
      struct task_struct *pthread = running_thread();
      // 得到第一个满足pg_cnt大小的空闲的内存地址
      bit_idx_start = bitmap_scan(&pthread->userprog_vaddr.vaddr_bitmap, pg_cnt);
      if (bit_idx_start == -1)
      {
         return NULL;
      }
      while (cnt < pg_cnt)
      {
         // 将bits数组对应标志位设置为1
         bitmap_set(&pthread->userprog_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1);
      }
      // 返回分配的虚拟内存页的起始地址
      vaddr_start = pthread->userprog_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
      /* 调试语句 */
      ASSERT((uint32_t)(vaddr_start) < (0xc0000000 - PG_SIZE));
   }
   return (void *)vaddr_start;
}
(2)将虚拟内存与物理内存联系起来

(2.1)进程位图

						memory.c
/*    将vaddr和pf池中的物理地址关联,仅支持一页空间分配  */
void* get_a_page(enum pool_flags pf, uint32_t vaddr){
   struct pool* mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;
   lock_acquire(&mem_pool.lock);

   /* 先将虚拟地址的位图置1   */
   struct takc_struct* cur = running_thread();
   int bit_index = -1;
   // 判断若是用户进程申请,则修改用户进程自己的虚拟地址位图
   if(cur->pgdir != NULL && pf == PF_USER){
      bit_index = (vaddr - cur->userprog_vaddr.vaddr_start)/PG_SIZE;
      ASSERT(bit_index > 0);
      bitmap_set(&cur.userprog_vaddr.vaddr_bitmap, bit_index, 1);
   }
   else if(cur->pgdir != NULL && pf == PF_KERNEL){
      bit_index = (vaddr - kernel_vaddr.vaddr_start)/PG_SIZE;
      ASSERT(bit_index > 0);
      bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_index, 1);
   }
   else{
      PANIC("get_a_page:not allow to get a page");
   }

   // 分配一个物理页
   void* page_phyaddr = palloc(mem_pool);
   if(page_phyaddr == NULL){
      return NULL;
   }
   // 页表中添加虚拟地址和物理地址映射
   page_table_add((void*)vaddr, page_phyaddr);
   lock_release(&mem_pool.lock);
   return (void*)vaddr;
}

(2.2)页表中添加虚拟地址vaddr与物理地址page_phyaddr的映射

/* 页表中添加虚拟地址_vaddr与物理地址_page_phyaddr的映射 */
static void page_table_add(void *_vaddr, void *_page_phyaddr)
{
   uint32_t vaddr = (uint32_t)_vaddr, page_phyaddr = (uint32_t)_page_phyaddr;
   uint32_t *pde = pde_ptr(vaddr);
   uint32_t *pte = pte_ptr(vaddr);

   /************************   注意   *************************
    * 执行*pte,会访问到空的pde。所以确保pde创建完成后才能执行*pte,
    * 否则会引发page_fault。因此在*pde为0时,*pte只能出现在下面else语句块中的*pde后面。
    * *********************************************************/
   /* 先在页目录内判断目录项的P位,若为1,则表示该表已存在 */
   if (*pde & 0x00000001)
   { // 页目录项和页表项的第0位为P,此处判断目录项是否存在
      ASSERT(!(*pte & 0x00000001));

      if (!(*pte & 0x00000001))
      {                                                      // 只要是创建页表,pte就应该不存在,多判断一下放心
         *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1); // US=1,RW=1,P=1
      }
      else
      { // 应该不会执行到这,因为上面的ASSERT会先执行。
         PANIC("pte repeat");
         *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1); // US=1,RW=1,P=1
      }
   }
   else
   { // 页目录项不存在,所以要先创建页目录再创建页表项.
      /* 页表中用到的页框一律从内核空间分配 */
      uint32_t pde_phyaddr = (uint32_t)palloc(&kernel_pool);

      *pde = (pde_phyaddr | PG_US_U | PG_RW_W | PG_P_1);

      /* 分配到的物理页地址pde_phyaddr对应的物理内存清0,
       * 避免里面的陈旧数据变成了页表项,从而让页表混乱.
       * 访问到pde对应的物理地址,用pte取高20位便可.
       * 因为pte是基于该pde对应的物理地址内再寻址,
       * 把低12位置0便是该pde对应的物理页的起始*/
      memset((void *)((int)pte & 0xfffff000), 0, PG_SIZE);

      ASSERT(!(*pte & 0x00000001));
      *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1); // US=1,RW=1,P=1
   }
}

(2.3)虚拟地址和物理地址映射关系

/*  将虚拟地址映射到物理地址  */
uint32_t addr_v2p(uint32_t vaddr){
   // 得到虚拟地址vaddr对应页目录项pte指针
   uint32_t pte = pte_ptr(vaddr);
   // 去掉低12位的页表项属性+虚拟地址vaddr的低12位
   return ((*pte & 0xfffff000)+(vaddr&0x00000fff));
}

pte_ptr

/* 得到虚拟地址vaddr对应的pte指针   */
uint32_t *pte_ptr(uint32_t vaddr)
{
   /* 先访问到页表自己 + \
    * 再用页目录项pde(页目录内页表的索引)做为pte的索引访问到页表 + \
    * 再用pte的索引做为页内偏移*/
   uint32_t *pte = (uint32_t *)(0xffc00000 +
                                ((vaddr & 0xffc00000) >> 10) +
                                PTE_IDX(vaddr) * 4);
   return pte;
}
4.用户进程

当前进程已经实现了在内核态由虚拟地址到物理地址的转换,由于当前程序的特权级一直都是0特权级下,因此接下来需要完成特权级切换步骤,让我们的进程能在3特权级下运行。

(1)构建用户进程初始上下文信息

将进程的寄存器信息存入中断栈中,保证中断进行特权级切换时,能从中断栈中恢复

/* 构建用户进程初始上下文信息 */
void start_process(void* filename_) {
    void* function = filename_;
    struct task_struct* cur = running_thread();
    cur->self_kstack += sizeof(struct thread_stack);  //跨过thread_stack,指向intr_stack
    struct intr_stack* proc_stack = (struct intr_stack*)cur->self_kstack;//可以不用定义成结构体指针	 
    proc_stack->edi = proc_stack->esi = proc_stack->ebp = proc_stack->esp_dummy = 0;
    proc_stack->ebx = proc_stack->edx = proc_stack->ecx = proc_stack->eax = 0;
    proc_stack->gs = 0;		 // 不太允许用户态直接访问显存资源,用户态用不上,直接初始为0
    proc_stack->ds = proc_stack->es = proc_stack->fs = SELECTOR_U_DATA;
    proc_stack->eip = function;	 // 待执行的用户程序地址
    proc_stack->cs = SELECTOR_U_CODE;
    proc_stack->eflags = (EFLAGS_IOPL_0 | EFLAGS_MBS | EFLAGS_IF_1);
    proc_stack->esp = (void*)((uint32_t)get_a_page(PF_USER, USER_STACK3_VADDR) + PG_SIZE) ;
    proc_stack->ss = SELECTOR_U_DATA; 
    asm volatile ("movl %0, %%esp; jmp intr_exit" : : "g" (proc_stack) : "memory");
}
(2)恢复进程的页表信息

线程没有内存空间,也不存在页表,但是线程依赖于进程的资源,为了防止上一个任务为进程,因此必须要把页表重新装载

/* 激活页表 */
void page_dir_activate(struct task_struct* p_thread) {
    /********************************************************
     * 执行此函数时,当前任务可能是线程。
     * 之所以对线程也要重新安装页表, 原因是上一次被调度的可能是进程,
     * 否则不恢复页表的话,线程就会使用进程的页表了。
     ********************************************************/

    /* 若为内核线程,需要重新填充页表为0x100000 */
    uint32_t pagedir_phy_addr = 0x100000;  // 默认为内核的页目录物理地址,也就是内核线程所用的页目录表
    if (p_thread->pgdir != NULL)	{    // 用户态进程有自己的页目录表
        pagedir_phy_addr = addr_v2p((uint32_t)p_thread->pgdir);
    }

    /* 更新页目录寄存器cr3,使新页表生效 */
    asm volatile ("movl %0, %%cr3" : : "r" (pagedir_phy_addr) : "memory");
}

/* 激活线程或进程的页表,更新tss中的esp0为进程的特权级0的栈 */
void process_activate(struct task_struct* p_thread) {
    ASSERT(p_thread != NULL);
    /* 激活该进程或线程的页表 */
    page_dir_activate(p_thread);

    /* 内核线程特权级本身就是0特权级,处理器进入中断时并不会从tss中获取0特权级栈地址,故不需要更新esp0 */
    if (p_thread->pgdir) {
        /* 更新该进程的esp0,用于此进程被中断时保留上下文 */
        update_tss_esp(p_thread);
    }
}
(3)内存划分之bss

Linux下C语言的内存空间分配

在 C 程序的内存空间中,位于低处的三个段是代码段、数据段和 bss 段,它们由编译器和链接器规划地址空间,在程序被操作系统加载之前它们地址就固定了

image-20230914205852774

堆栈共享内存中处理代码段、数据段以及顶端的命令行参数和环境变量外的其他内存空间,堆的起始地址固定,向上生长,栈的栈顶指针固定,向下生长。因此在程序的加载之初,操作系统必须为堆和战分别指定起始地址

堆的起始地址对应的是bss的结束地址,bss是在程序运行过程中才会被赋值的,只存在于内存中,并不存在于程序文件。因此链接器采取了合理的做法:由于 bss 中的内容是变量,其属性为可读写,这和数据段属性一致,故链接器将 bss 占用的内存空间大小合并到数据段占用的内存中,这样便在数据段中预留出 bss 的空间以供程序在将来运行时使用。故bss仅存在于数据段所在的内存中程序的 bss 段(数据段的一部分)会由该加载器填充为 0。 【解释了为什么未初始化的全局变量和静态变量初始为0】

(4)创建页目录表

我们目前使用的是二级页表,加载到页目录表寄存器 CR3 中的是页目录表的物理地址,页目录表中一共包含 1024 个页目录项(阱),页目录项大小为 4B ,故页目录表大小为 4阻。每个页目录项仅表示 1 个页表,页目录项中存储的是页表所在物理页框的物理地址及页目录项的属性。每个页表可容纳 1024 个页表项(阱),页表项大小为 4B,故每个页表本身占用 4阻。每个页表项仅表示一个物理页框,页表项中存储的是 4阻大小的物理页框的物理地址及页表项的属性,因此每个页表可表示的地址空间为1024叫阻=4MB,一个页目录表中可包含 1024 个页表,因此可表示 1024*4阳斗GB 大小的地址空间。目前我们的内核位于 OxcOOOOOOO 以上的地址空间,也就是位于页目录表中第 768~ 1023 个页目录项所指向的页表中,这一共是 256 个页目录项,即 lGB 空间(当然我们的内核没那么大,不会把 lGB 空间占满

在这里插入图片描述

在内核态中为进程创建用户页目录,同时将页目录表的最后一个页目录项更新为用户进程的页目录表物理地址(也就是用户的物理地址),因为内核需要知道该用户进程的页目录表在哪里。

    /************************** 1  先复制页表  *************************************/
    /*  page_dir_vaddr + 0x300*4 是内核页目录的第768项 */
    memcpy((uint32_t*)((uint32_t)page_dir_vaddr + 0x300*4), (uint32_t*)(0xfffff000+0x300*4), 1024);
    /*****************************************************************************/

    /************************** 2  更新页目录地址 **********************************/
    uint32_t new_page_dir_phy_addr = addr_v2p((uint32_t)page_dir_vaddr);
    /* 页目录地址是存入在页目录的最后一项,更新页目录地址为新页目录的物理地址 */
    page_dir_vaddr[1023] = new_page_dir_phy_addr | PG_US_U | PG_RW_W | PG_P_1;
(5)创建用户进程虚拟地址位图

因为用户进程需要占用内存空间,因此需要设置位图来标记需要的内存页框数

1.需要明确用户进程占用多少页,因为位图的1位对应一页

2设置用户进程位图的大小和长度

3.初始化用户进程位图

/* 创建用户进程虚拟地址位图 */
void create_user_vaddr_bitmap(struct task_struct* user_prog) {
    user_prog->userprog_vaddr.vaddr_start = USER_VADDR_START;
    // 总空间/页框大小/一个位图元素的管理长度8 = 总共的位图长度
    uint32_t bitmap_pg_cnt = DIV_ROUND_UP((0xc0000000 - USER_VADDR_START) / PG_SIZE / 8 , PG_SIZE);
    user_prog->userprog_vaddr.vaddr_bitmap.bits = get_kernel_pages(bitmap_pg_cnt);
    user_prog->userprog_vaddr.vaddr_bitmap.btmp_bytes_len = (0xc0000000 - USER_VADDR_START) / PG_SIZE / 8;
    bitmap_init(&user_prog->userprog_vaddr.vaddr_bitmap);
}
(6)创建用户进程

1.初始化优先级、名字等信息

2.创建进程位图,完成位图分配、长度设置、初始化等操作

3.建立进程,初始化进程上下文信息,中断栈信息

4.创建进程页表,加入就绪队列和全部队列

/* 创建用户进程 */
void process_execute(void* filename, char* name) { 
    /* pcb内核的数据结构,由内核来维护进程信息,因此要在内核内存池中申请 */
    struct task_struct* thread = get_kernel_pages(1);
    init_thread(thread, name, default_prio); 
    create_user_vaddr_bitmap(thread);
    thread_create(thread, start_process, filename);//start_process(filename)建立进程上下文信息,如中断栈
    thread->pgdir = create_page_dir();

    enum intr_status old_status = intr_disable();
    ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));
    list_append(&thread_ready_list, &thread->general_tag);

    ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));
    list_append(&thread_all_list, &thread->all_list_tag);
    intr_set_status(old_status);
}

总结:

本章实现了操作系统用户态进程创建,包括进程ldt表创建、tss创建(初始化tss、重新加载GDT表)、特权级切换、进程位图创建、进程页表创建

用户进程执行的具体过程
  1. 首先建立用户进程的上下文信息,为用户态进程的执行提供上下文信息;
  2. 为其注册LDT描述符,注册TSS任务状态段,添加进GDT表中;
  3. 创建进程PCB,初始化线程栈、中断栈、时间片、优先级等信息;
  4. 根据内存分页管理系统,利用内核位图为程序分配合适的虚拟内存空间,加载到虚拟内存;
  5. 创建进程位图,并完成位图长度、位的初始化;
  6. 特权级切换,加载用户进程的上下文信息,中断跳转到用户态开始执行用户进程;
  7. 当处理器运行时,从GDT表中得到程序内存起始地址+偏移量,访问LDT表得到程序各个函数的起始地址+偏移量;开始调度线程。还有特权级检查。
任务执行通过LDT表完成,那么如何找到LDT表呢????

将LDT表的起始地址和偏移量,存入GDT表中, LDT表中存储的程序的起始地址+偏移量,对应的程序的入口地址,CS+IP指向该地址后,取指令交由处理器执行。

内存分段分页与LDT、GDT关系

GDT和LDT实现虚拟地址和物理地址的映射。分页是实现地址映射的一种处理方法,建立分页机制,将程序按照页分配并调入内存,实现程序的快速执行。

memset本质是用I/O位图在内存中找到合适的空闲内存分配

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

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

相关文章

Android Jetpack Compose之生命周期与副作用

文章目录 1.概述2.Composeable生命周期3.Compose副作用及API3.1.Compose副作用API3.1.1 DisposableEffect3.1.2 SideEffect 3.2 Compose异步处理副作用API3.2.1 LaunchedEffect3.2.2 rememberCoroutineScope3.2.3 rememberUpdateState3.2.4 snapshotFlow 3.3 状态创建副作用API…

什么是C++?

1.什么是C C语言是结构化和模块化的语言&#xff0c;适合处理较小规模的程序。对于复杂的问题&#xff0c;规模较大的 程序&#xff0c;需要高度的抽象和建模时&#xff0c;C语言则不合适。为了解决软件危机&#xff0c; 20世纪80年代&#xff0c; 计算机 界提出了OOP(object o…

一种管理KEIL工程输出文件的方法

开发语言&#xff1a;python&#xff1b; 针对工程&#xff1a;适配ARM公司的KEIL-V5软件的工程 管理的输出文件类型&#xff1a;BIN文件、HEX文件 脚本使用方法&#xff1a;直接放置到keil的设置项下&#xff1b; 脚本执行位置&#xff1a;程序编译之后 脚本功能&#xff1a…

C++智能指针(一)——shared_ptr初探

文章目录 1. 普通指针存在的问题2. Class shared_ptr2.1 使用 shared_ptr2.1.1 初始化 shared_ptr2.1.2 reset2.1.3 访问数据2.1.4 use_count() 3. Deleter3.1 定义一个 Deleter3.2 处理数组 1. 普通指针存在的问题 智能指针的引入&#xff0c;是为了解决普通指针在使用过程中…

Android 13.0 SystemUI修改状态栏电池图标样式为横屏显示

1.概述 在13.0的产品定制化开发中,对于原生系统中SystemUId 状态栏的电池图标是竖着显示的,一般手机的电池图标都是横屏显示的 可以觉得样式挺不错的,所以由于产品开发要求电池图标横着显示和手机的样式一样,所以就得重新更换SystemUI状态栏的电池样式了 如图: 2.SystemUI…

区块链金融的开发流程

区块链金融应用的开发流程与一般的软件开发流程有许多相似之处&#xff0c;但它还涉及到智能合约的编写、区块链网络集成和加密货币处理等特定方面的工作。以下是一般区块链金融应用的开发流程&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件…

如何查自己名下有多少个微信号?

99%的人都不知道微信有这个功能通过微信可以查自己名下绑定了多少个微信账号再也不担心身份证被别人用来绑定微信了姐妹们快去查一下吧&#xff01; ①打开微信&#xff0c;点击【我】→点击【设置】 ②点击【通用】→ 点击【辅助功能】→ 点击【微信支付】 ③点击【帮助中心】…

MDK自动生成带校验带SVN版本号的升级文件

MDK自动生成带校验带SVN版本号的升级文件 获取SVN版本信息 确保SVN安装了命令行工具&#xff0c;默认安装时不会安装命令行工具 编写一个模板头文件 svn_version.temp.h, 版本号格式为 1_0_0_SVN版本号 #ifndef __SVN_VERSION_H #define __SVN_VERSION_H#define SVN_REVISIO…

性能分析工具的使用(超详细)

数据库服务器的优化步骤 整个流程划分成了观察&#xff08;Show status&#xff09;和行动&#xff08;Action&#xff09;两个部分。字母 S 的部分代表观察&#xff08;会使用相应的分析工具&#xff09;&#xff0c;字母 A 代表的部分是行动&#xff08;对应分析可以采取的行…

Linux下设备树、pinctrl和gpio子系统、LED灯驱动实验

文章目录 设备树常用的of函数pinctrl子系统gpio子系统LED灯驱动实验修改设备树文件编写驱动代码执行结果在LED驱动代码中加入内核定时器 设备树 描述设备树的文件叫做DTS(Device Tree Source)&#xff0c;这个DTS文件采用树形结构描述板级设备&#xff0c;也就是开发板上的设备…

NEFU离散数学实验1-排列组合

相关概念 在离散数学中&#xff0c;组合数是一种用于计算从n个不同元素中选取m个元素的方式。以下是一些与组合数相关的概念&#xff1a; 排列&#xff1a;从n个不同元素中选取m个元素进行排列&#xff0c;排列数用P(n, m)表示&#xff0c;计算公式为P(n, m) n! / (n - m)! …

springboot篮球论坛系统springboot034

大家好✌&#xff01;我是CZ淡陌。一名专注以理论为基础实战为主的技术博主&#xff0c;将再这里为大家分享优质的实战项目&#xff0c;本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目&#xff0c;希望你能有所收获&#xff0c;少走一些弯路…

渗透测试怎么入门?(超详细解读)

1. 什么是渗透测试 渗透测试就是模拟真实黑客的攻击手法对目标网站或主机进行全面的安全评估&#xff0c;与黑客攻击不一样的是&#xff0c;渗透测试的目的是尽可能多地发现安全漏洞&#xff0c;而真实黑客攻击只要发现一处入侵点即可以进入目标系统。 一名优秀的渗透测试工程…

ubuntu安装Miniconda并举例使用

更新系统包 sudo apt update sudo apt upgrade官网下载Miniconda&#xff0c;最好是实体机下载后放进虚拟机&#xff0c;方法可以参考Xftp 7连接服务器或者本地虚拟机文章 https://docs.conda.io/en/latest/miniconda.html#linux-installers 进入安装目录执行&#xff0c;右键…

行业追踪,2023-10-11

自动复盘 2023-10-11 凡所有相&#xff0c;皆是虚妄。若见诸相非相&#xff0c;即见如来。 k 线图是最好的老师&#xff0c;每天持续发布板块的rps排名&#xff0c;追踪板块&#xff0c;板块来开仓&#xff0c;板块去清仓&#xff0c;丢弃自以为是的想法&#xff0c;板块去留让…

什么是大数据,大数据简介

大数据的概念通俗的说法 大数据&#xff0c;按照我的理解比较通俗易懂的是在数据量很多很大的情况下数据处理速度需要足够快&#xff0c;用我们以前传统意义上的的技术比如关系型数据库mysql没办法处理或者处理起来非常复杂&#xff0c;必须有一些新的处理技术也就是大数据处理…

网工内推 | 实施工程师,有软考证书优先,上市公司,最高14薪

01 新点软件 招聘岗位&#xff1a;实施工程师 职责描述&#xff1a; 1、负责一线项目组对接&#xff0c;完成项目前期信息、需求收集&#xff1b; 2、负责需求验证、管控、上线专项跟进工作&#xff1b; 3、负责在推进过程中总结与沉淀&#xff0c;提升优化对接规范/效率&…

windows 下编译libcurl openssl

参考 编译libcurl-openssl 1、拉取opensssl 建议指定为最新的发布版本 git clone -b openssl-3.1.3 --recurse-submodules https://github.com/openssl/openssl.git2、拉取curl 建议指定为最新的发布版本 git clone -b curl-8_3_0 --recurse-submodules https://github.c…

WIPO绿色专利分类范围清单

WIPO绿色专利分类范围清单 1、来源&#xff1a;WIPO绿色专利分类范围清单来源于网址&#xff1a; https://www.wipo.int/classifications/ipc/green-inventory/home&#xff09; 2、范围&#xff1a;全球范围 3、指标为key、class、IPC、l1Title、l2Title、l3Title、l4Titl…

C进阶-自定义类型:结构体、枚举、联合

本章重点&#xff1a; 结构体&#xff1a; 结构体类型的声明 结构的自引用 结构体变量的定义和初始化 结构体内存对齐 结构体传参 结构体实现位段&#xff08;位段的填充&可移植性&#xff09; 1 结构体的声明 1.1 结构的基础知识 结构是一些值的集合&#xff0c;这些值称…