Featured image of post 《操作系统真象还原》第十一章 —— 创建用户进程

《操作系统真象还原》第十一章 —— 创建用户进程

本文介绍了操作系统用户进程创建的相关内容

本章节所有代码托管在miniOS_32

章节任务介绍

上一节中,我们实现了操作系统的“输入”功能,通过编写键盘驱动程序,成功让操作系统接受键盘输入并在屏幕进行打印

但是到目前为止,我们所有的程序都是在最高特权级0级下工作,而本节,我们将实现用户使用的用户程序,其特权级为3

任务简介

进程与内核线程最大的区别是进程有单独的4GB空间,当然这指的是虚拟地址。

  • 因此,我们需要单独为每个进程维护一个虚拟地址池
  • 此外,为了维护每个进程的虚拟内存池与用户物理内存池的映射关系,我们还需要为每个进程创建页目录表
  • 最后,每个进程的特权级是3,而此前我们一直在0特权级下工作,因此我们还需要完成从特权级0到特权级3的转换

本节的主要任务有:

  1. 实现用户进程
    • 内核态完成进程PCB的初始化,同时为每个进程创建虚拟内存池
    • 内核态为进程分配资源,包括内存、堆栈等,为每个进程创建页目录表
    • 完成进程从特权级0到特权级3的转换,然后执行用户进程的执行函数

创建用户进程

在开始书写本节任务之前,我们首先回忆一下之前我们的内核线程是如何创建和启动的

image-20250105155728517

如图所示,当创建线程并执行时,我们

  • 首先在内核空间中申请了一块PCB
  • 初始化PCB的内容,包括
    • 内核线程的名字、优先级、栈顶位置以及就绪队列和全部队列的维护节点等
  • 初始化线程栈的运行信息,这包括
    • 初始化eip寄存器为通用线程启动函数kernel_thread
    • 初始化线程执行函数的地址、执行函数的参数
  • 最后运行时通过ret指令将kernel_thread弹出并赋值给eip,这样eip寄存器指向指令的时候就相当于在执行函数kernel_thread,而kernel_thread函数可以取出真正的执行函数的地址和参数,去执行线程的执行函数

进程PCB的初始化

由此,我们可以借助内核线程的创建流程去创建进程

首先,我们可以在PCB的数据结构中添加两个字段——页目录表基址、虚拟内存池

这样当初始化PCB时

  • 如果是线程PCB的初始化,则不初始化上述两个字段
  • 如果是进程PCB的初始化,则只需要复用线程PCB的初始化,然后添加上上述两个字段的初始化即可

如下所示,我们首先为PCB结构体task_struct添加两个字段pgdiruserprog_vaddr,分别用以记录进程的页目录表基址和进程虚拟内存池的初始化

/thread/thread.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/* 进程或线程的pcb,程序控制块, 此结构体用于存储线程的管理信息*/
struct task_struct
{
   uint32_t *self_kstack; // 用于存储线程的栈顶位置,栈顶放着线程要用到的运行信息
   enum task_status status;
   uint8_t priority; // 线程优先级
   char name[16];    // 用于存储自己的线程的名字

   uint8_t ticks;                      // 线程允许上处理器运行还剩下的滴答值,因为priority不能改变,所以要在其之外另行定义一个值来倒计时
   uint32_t elapsed_ticks;             // 此任务自上cpu运行后至今占用了多少cpu嘀嗒数, 也就是此任务执行了多久*/
   struct list_elem general_tag;       // general_tag的作用是用于线程在一般的队列(如就绪队列或者等待队列)中的结点
   struct list_elem all_list_tag;      // all_list_tag的作用是用于线程队列thread_all_list(这个队列用于管理所有线程)中的结点
   uint32_t *pgdir;                    // 进程自己页目录表的虚拟地址
   struct virtual_addr userprog_vaddr; // 每个用户进程自己的虚拟地址池
   uint32_t stack_magic;               // 如果线程的栈无限生长,总会覆盖地pcb的信息,那么需要定义个边界数来检测是否栈已经到了PCB的边界
};

接下来我们主要进行这两项字段的初始化

进程页目录表的初始化

/userprog/process.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 为进程创建页目录表,并初始化(系统映射+页目录表最后一项是自己的物理地址,以此来动态操作页目录表),成功后,返回页目录表虚拟地址,失败返回空地址
uint32_t *create_page_dir(void)
{
   // 用户进程的页表不能让用户直接访问到,所以在内核空间来申请
   uint32_t *page_dir_vaddr = get_kernel_pages(1);
   if (page_dir_vaddr == NULL)
   {
      console_put_str("create_page_dir: get_kernel_page failed!");
      return NULL;
   }
   // 将内核页目录表的768号项到1022号项复制过来
   memcpy((uint32_t *)((uint32_t)page_dir_vaddr + 768 * 4), (uint32_t *)(0xfffff000 + 768 * 4), 255 * 4);
   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; // 页目录表最后一项填自己的地址,为的是动态操作页表
   return page_dir_vaddr;
}

如上所示,进程页目录表的初始化很简单,其主要逻辑为

  • 从内核空间中申请一页内存给页目录表
  • 将内核页目录表的768号项到1022号项复制到刚刚创建的页目录表中,原因是内核页目录表的768号项到1022号项映射的是操作系统的内核空间,为了让所有进程共享操作系统的内核,故而需要将这段映射复制给每个进程

页目录表初始化后需要激活,也就是将页目录表的基址加载到cr3寄存器中,激活的时间是由调度函数调度该进程时完成的,此处只是书写相关激活代码,将来有调度函数完成调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/* 激活页表 */
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);
   }
   asm volatile("movl %0, %%cr3" : : "r"(pagedir_phy_addr) : "memory"); // 更新页目录寄存器cr3,使新页表生效
}

进程虚拟内存池的初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 初始化进程pcb中的用于管理自己虚拟地址空间的虚拟内存池结构体
void create_user_vaddr_bitmap(struct task_struct *user_prog)
{
   // 虚拟内存池的起始地址
   user_prog->userprog_vaddr.vaddr_start = USER_VADDR_START;
   /*
   (0xc0000000 - USER_VADDR_START)表示用户栈的大小
   故,(0xc0000000 - USER_VADDR_START) / PG_SIZE表示用户栈占用的页面数
   由于位图中每个比特位对应记录的是一个页面
   因此(0xc0000000 - USER_VADDR_START) / PG_SIZE / 8表示用户栈位图所占用的比特数
   DIV_ROUND_UP表示向上取整,如9/10=1
   因此整行代码表示管理用户栈的位图所需要的内存大小(页面数)
   */
   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);
}

虚拟内存池的初始化主要有两部分

  • 虚拟内存池的基址
  • 管理虚拟内存池的位图的初始化

其中,下述代码表示虚拟内存池基址的初始化

1
user_prog->userprog_vaddr.vaddr_start = USER_VADDR_START;

该基址的宏定义如下所示

1
2
// linux下大部分可执行程序的入口地址(虚拟)都是这个附近,我们也仿照这个设定
#define USER_VADDR_START 0x8048000

剩余部分的代码则表示对位图的初始化,包括

  • 为位图申请合适的存放空间
  • 初始化位图的内容

至此,我们将复用线程PCB的初始化逻辑,再上述两个字段的初始化,就完成了进程PCB初始化的内容部分,代码流程如下所示

1
2
3
4
5
6
7
8
   /* pcb内核的数据结构,由内核来维护进程信息,因此要在内核内存池中申请 */
   struct task_struct *thread = get_kernel_pages(1);
   /*初始化pcb*/
   init_thread(thread, name, default_prio);
   // 初始化进程pcb中特有的页目录表
   thread->pgdir = create_page_dir();
   // 初始化进程虚拟内存池,进程有自己的虚拟内存
   create_user_vaddr_bitmap(thread);

image-20250106104501258

接下来,我们同样借助线程栈运行信息的初始化来初始化进程的运行信息

进程运行信息的初始化

image-20250105164925423

首先我们看线程栈的运行信息是如何初始化的

1
void thread_create(struct task_struct *pthread, thread_func function, void *func_arg);

初始时,如下图所示,thread_create会将线程的栈顶指针self_kstack指向线程栈基址,然后逐步初始化线程栈的运行信息。

image-20241221224657762

当被ret指令弹出开始运行线程时,ret指令会把kernel_thread函数弹出给eip,然后执行kernel_thread函数,而kernel_thread函数去执行真正的线程执行函数,也就是thread_create中的参数function

image-20241221224854911

上述是线程运行信息的初始化,为了完成进程运行信息的初始化,我们利用上述过程,当ret指令弹出kernel_thread,然后执行kernel_thread的时候,我们让其转而执行进程运行信息的初始化代码start_process,也就是

1
2
3
   // 首先初始化线程栈运行环境,执行start_process
   // 然后初始化中断栈,此时的中断栈也是进程的运行栈,接着执行真正的执行函数filename
   thread_create(thread, start_process, filename);

start_process中的工作和thread_create类似,也是初始化进程运行时信息,然后借用中断退出指令iretd去执行可执行函数,我们在这里将参数filename置为真正的进程执行函数地址

/userprog/process.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 用于初始化进入进程所需要的中断栈中的信息,传入参数是实际要运行的函数地址(进程),这个函数是用线程启动器进入的(kernel_thread)
void start_process(void *filename_)
{
   void *function = filename_;
   struct task_struct *cur = running_thread();
   cur->self_kstack += sizeof(struct thread_stack); // 当我们进入到这里的时候,cur->self_kstack指向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(gs我们一般用于访问显存段,这个让内核态来访问)
   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); // 设置用户态下的eflages的相关字段
   // 初始化中断栈中的栈顶位置,我们先为虚拟地址0xc0000000 - 0x1000申请了个物理页,然后将虚拟地址+4096置为栈顶
   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");
}

image-20250105170203418

从中断返回要用到 iretd 指令,iretd 指令的主要工作流程为

  • 用栈中的数据作为返回地址
  • 加载栈中 eflags的值到 efags 寄存器
  • 如果栈中 cs.rpl若为更低的特权级,处理器的特权级检查通过后,会将栈中 cs载入到 CS寄存器,栈中ss载入 SS寄存器,随后处理器进入低特权级

其中**退出中断的出口是汇编语言函数intr_exit,这是我们定义在 kernel.S**中的,此函数用来恢复中断发生时、被中断的任务的上下文状态,并且退出中断。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
section .text
global intr_exit
intr_exit:	     
    ; 以下是恢复上下文环境
    add esp, 4			                    ; 跳过中断号
    popad
    pop gs
    pop fs
    pop es
    pop ds
    add esp, 4			                    ;对于会压入错误码的中断会抛弃错误码(这个错误码是执行中断处理函数之前CPU自动压入的),对于不会压入错误码的中断,就会抛弃上面push的0                    
    iretd				                    ; 从中断返回,32位下iret等同指令iretd

其余需要说明的是,start_process中,以下代码主要用于为用户栈开辟空间

1
proc_stack->esp = (void *)((uint32_t)get_a_page(PF_USER, USER_STACK3_VADDR) + PG_SIZE);

其中get_a_page的实现为

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 用于为指定的虚拟地址申请一个物理页,传入参数是这个虚拟地址,要申请的物理页所在的地址池的标志。申请失败,返回null
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);
	struct task_struct *cur = running_thread();
	int32_t bit_idx = -1;
	/* 若当前是用户进程申请用户内存,就修改用户进程自己的虚拟地址位图 */
	if (cur->pgdir != NULL && pf == PF_USER)
	{
		bit_idx = (vaddr - cur->userprog_vaddr.vaddr_start) / PG_SIZE;
		ASSERT(bit_idx > 0);
		bitmap_set(&cur->userprog_vaddr.vaddr_bitmap, bit_idx, 1);
	}
	else if (cur->pgdir == NULL && pf == PF_KERNEL)
	{
		/* 如果是内核线程申请内核内存,就修改kernel_vaddr. */
		bit_idx = (vaddr - kernel_vaddr.vaddr_start) / PG_SIZE;
		ASSERT(bit_idx > 0);
		bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx, 1);
	}
	else
	{
		PANIC("get_a_page:not allow kernel alloc userspace or user alloc kernelspace by 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;
}

上述所有的初始化任务代码为

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
   /* pcb内核的数据结构,由内核来维护进程信息,因此要在内核内存池中申请 */
   struct task_struct *thread = get_kernel_pages(1);
   /*初始化pcb*/
   init_thread(thread, name, default_prio);
   // 初始化进程pcb中特有的页目录表
   thread->pgdir = create_page_dir();
   // 初始化进程虚拟内存池,进程有自己的虚拟内存
   create_user_vaddr_bitmap(thread);

   // 首先初始化线程栈运行环境,执行start_process
   // 然后初始化中断栈,此时的中断栈也是进程的运行栈,接着执行真正的执行函数filename
   thread_create(thread, start_process, filename);

当初始化结束之后,就可以将该进程任务上到就绪队列当中

1
2
3
4
5
6
7
   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);

特权级转换

创建用户进程的第三个工作是实现特权级转换,在此之前我们需要知道有关特权级转换的一些常识

  • 对于 代码段,操作系统不允许高特权级(PL0)直接切换到低特权级(PL3),以确保内核代码的安全性。
  • 对于 数据段,操作系统不允许低特权级(PL3)直接切换到高特权级(PL0),以保护内核数据不被用户程序非法访问。

如果代码段想要从高特权级转换到低特权级,需要从中断返回实现,这也是为什么我们在高特权级创建好进程需要的东西后,使用ired指令进行中断返回来进入进程的执行函数,只有这样才能让用户进程运行在低特权级下

但是有个问题,那就是如果用户进程想要从低特权级进入到高特权级该怎么办呢?

这就是TSS的作用。

在操作系统中,TSS(Task State Segment,任务状态段) 是一种用于管理任务(进程或线程)状态的数据结构,主要用于处理任务切换。

如图所示,每当任务切换时,操作系统需要保存当前任务的寄存器(如通用寄存器、程序计数器、堆栈指针等),以及一些控制信息

image-20250105193820348

也就是说,TSS是保存任务切换时寄存器状态的一种数据结构

在这些寄存器中,esp0, ss0这两个字段指定了任务切换时,进入内核态时的栈指针和栈段选择子

也就是说,当用户进程想要从低特权级切换到高特权级时,就可以从TSS中取出esp0, ss0这两个字段的值,然后重新返回到内核态

因此,现在我们可以说

  1. 当创建一个用户进程时,操作系统在内核态下为进程准备好PCB以及用户栈和内存等必要的运行环境
  2. 当操作系统准备进行调度进程时,就激活页表,同时更新记录当前TSS中的esp0和ss0的值,当用户进程想要从低特权级转换到高特权级时,就可以使用此时在TSS中记录的esp0和ss0

如下,当任务进行调度切换的时候,调度函数负责激活页表,同时更新TSS中的esp0的值,让它指向线程/进程的0级栈,然后摘取就绪队列中的任务上处理器

/thread/thread.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* 实现任务调度 */
void schedule()
{
   ASSERT(intr_get_status() == INTR_OFF);
   struct task_struct *cur = running_thread();
   if (cur->status == TASK_RUNNING)
   { // 若此线程只是cpu时间片到了,将其加入到就绪队列尾
      ASSERT(!elem_find(&thread_ready_list, &cur->general_tag));
      list_append(&thread_ready_list, &cur->general_tag);
      cur->ticks = cur->priority; // 重新将当前线程的ticks再重置为其priority;
      cur->status = TASK_READY;
   }
   else
   {
      /* 若此线程需要某事件发生后才能继续上cpu运行,
      不需要将其加入队列,因为当前线程不在就绪队列中。*/
   }

   ASSERT(!list_empty(&thread_ready_list));
   thread_tag = NULL; // thread_tag清空
                      /* 将thread_ready_list队列中的第一个就绪线程弹出,准备将其调度上cpu. */
   thread_tag = list_pop(&thread_ready_list);
   struct task_struct *next = elem2entry(struct task_struct, general_tag, thread_tag);
   next->status = TASK_RUNNING;
   // 激活任务页表,同时更新TSS中的esp0的值,让它指向线程/进程的0级栈
   process_activate(next);
   switch_to(cur, next);
}

/userprog/process.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 用于加载进程自己的页目录表,同时更新进程自己的0特权级esp0到TSS中
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)
      update_tss_esp(p_thread); /* 更新该进程的esp0,用于此进程被中断时保留上下文 */
}

既然要使用和更新TSS,当然离不开TSS的初始化,以下介绍TSS的初始化。

TSS只是一段内存区域,与其他普通段一样,TSS也有自己的描述符,即TSS描述符,用它来描述一个TSS的信息,此描述符需要定义在GDT中。

寄存器TR始终指向当前任务的TSS。任务切换就是改变TR的指向,CPU自动将当前寄存器组的值(快照)写入TR 指向的 TSS,同时将新任务TSS中的各寄存器的值载入CPU中对应的寄存器,从而实现了任务切换。

image-20250105194840776

TSS必须要在GDT中注册才行,这也是为了在引用描述符的阶段做安全检查。因此TSS是通过选择子来访问的,将tss加载到寄存器TR的指令是 ltr,其指令格式为:

1
ltr “16 位通用寄存器”或“16 位内存单元

image-20250105194802982

因此,同GDT一样,我们只需要在gdt中创建tss描述符,并重新加载gdt,然后让TR寄存器指向TSS描述符即可

/userprog/tss.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include "tss.h"
#include "stdint.h"
#include "global.h"
#include "string.h"
#include "print.h"

//定义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;
    uint16_t trace;
    uint16_t io_base;
};

static struct tss tss;

//用于更新TSS中的esp0的值,让它指向线程/进程的0级栈
void update_tss_esp(struct task_struct* pthread) {
   tss.esp0 = (uint32_t*)((uint32_t)pthread + PG_SIZE);
}

//用于创建gdt描述符,传入参数1,段基址,传入参数2,段界限;参数3,属性低字节,参数4,属性高字节(要把低四位置0,高4位才是属性)
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.attr_low_byte = (uint8_t)(attr_low);
   desc.limit_high_attr_high = (((limit & 0x000f0000) >> 16) + (uint8_t)(attr_high));
   desc.base_high_byte = desc_base >> 24;
   return desc;
}

/* 在gdt中创建tss并重新加载gdt */
void tss_init() {
   put_str("tss_init start\n");
   uint16_t tss_size = (uint16_t)sizeof(tss);
   memset(&tss, 0, tss_size);
   tss.ss0 = SELECTOR_K_STACK;
   tss.io_base = tss_size;    //io_base 字段的值大于或等于 TSS 的大小,那么这意味着 用于表示I/O 位图的数组超出了 TSS 的界限,
                              //或者说,TSS 结构实际上并没有包含 I/O 位图。在这种情况下,处理器就会假定该任务可以访问所有 I/O 端口

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

  //在gdt表中添加tss段描述符,在本系统的,GDT表的起始位置为0x00000900,那么tss的段描述就应该在0x920(0x900+十进制4*8)
  *((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");
}

总结

本章代码逻辑其实和线程的创建是一样的,都是首先初始化PCB和运行栈空间信息

只是由于进程有自己的内存空间,因此需要在初始化PCB的过程中添加上进程内存信息的初始化

另外由于用户进程涉及特权级的转换,这一部分我们中断返回实现,因此在运行栈空间的信息初始化时借助了中断返回进入用户空间执行

如下总结了用户进程创建和执行的过程流程

创建过程

image-20250105195504052

执行过程

image-20250105195540648

用户进程测试

main.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"

void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);
int test_var_a = 0, test_var_b = 0;

int main(void) {
   put_str("I am kernel\n");
   init_all();

   thread_start("k_thread_a", 31, k_thread_a, "argA ");
   thread_start("k_thread_b", 31, k_thread_b, "argB ");
   process_execute(u_prog_a, "user_prog_a");
   process_execute(u_prog_b, "user_prog_b");

   intr_enable();
   while(1);
   return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void* arg) {     
   char* para = arg;
   while(1) {
      console_put_str(" v_a:0x");
      console_put_int(test_var_a);
   }
}

/* 在线程中运行的函数 */
void k_thread_b(void* arg) {     
   char* para = arg;
   while(1) {
      console_put_str(" v_b:0x");
      console_put_int(test_var_b);
   }
}

/* 测试用户进程 */
void u_prog_a(void) {
   while(1) {
      test_var_a++;
   }
}

/* 测试用户进程 */
void u_prog_b(void) {
   while(1) {
      test_var_b++;
   }
}

编译

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
mkdir -p bin
#编译mbr
nasm -o $(pwd)/bin/mbr -I $(pwd)/boot/include/ $(pwd)/boot/mbr.S
dd if=$(pwd)/bin/mbr of=~/bochs/hd60M.img bs=512 count=1 conv=notrunc

#编译loader
nasm -o $(pwd)/bin/loader -I $(pwd)/boot/include/ $(pwd)/boot/loader.S
dd if=$(pwd)/bin/loader of=~/bochs/hd60M.img bs=512 count=4 seek=2 conv=notrunc

#编译print函数
nasm -f elf32 -o $(pwd)/bin/print.o $(pwd)/lib/kernel/print.S
# 编译kernel
nasm -f elf32 -o $(pwd)/bin/kernel.o $(pwd)/kernel/kernel.S
# 编译switch
nasm -f elf32 -o $(pwd)/bin/switch.o $(pwd)/thread/switch.S

#编译main文件
gcc-4.4 -o $(pwd)/bin/main.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/device/ -I $(pwd)/thread/ -I $(pwd)/userprog/ $(pwd)/kernel/main.c
#编译interrupt文件
gcc-4.4 -o $(pwd)/bin/interrupt.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/interrupt.c
#编译init文件
gcc-4.4 -o $(pwd)/bin/init.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ -I $(pwd)/device/ -I $(pwd)/userprog/ $(pwd)/kernel/init.c
# 编译debug文件
gcc-4.4 -o $(pwd)/bin/debug.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/debug.c
# 编译string文件
gcc-4.4 -o $(pwd)/bin/string.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/lib/string.c
# 编译bitmap文件
gcc-4.4 -o $(pwd)/bin/bitmap.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/lib/kernel/bitmap.c
# 编译memory文件
gcc-4.4 -o $(pwd)/bin/memory.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/kernel/memory.c
# 编译thread文件
gcc-4.4 -o $(pwd)/bin/thread.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ -I $(pwd)/userprog/ $(pwd)/thread/thread.c
# 编译list文件
gcc-4.4 -o $(pwd)/bin/list.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/lib/kernel/list.c
# 编译timer文件
gcc-4.4 -o $(pwd)/bin/timer.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/device/timer.c
# 编译sync文件
gcc-4.4 -o $(pwd)/bin/sync.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/thread/sync.c
# 编译console文件
gcc-4.4 -o $(pwd)/bin/console.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/device/console.c
# 编译keyboard文件
gcc-4.4 -o $(pwd)/bin/keyboard.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/device/keyboard.c
# 编译ioqueue文件
gcc-4.4 -o $(pwd)/bin/ioqueue.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/device/ioqueue.c
# 编译tss文件
gcc-4.4 -o $(pwd)/bin/tss.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ -I $(pwd)/userprog/ $(pwd)/userprog/tss.c
# 编译process文件
gcc-4.4 -o $(pwd)/bin/process.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ -I $(pwd)/device/ -I $(pwd)/userprog/ $(pwd)/userprog/process.c


#将main函数与print函数进行链接
ld -m elf_i386 -Ttext 0xc0001500 -e main -o $(pwd)/bin/kernel.bin $(pwd)/bin/main.o $(pwd)/bin/kernel.o  $(pwd)/bin/init.o $(pwd)/bin/process.o $(pwd)/bin/tss.o $(pwd)/bin/thread.o $(pwd)/bin/switch.o $(pwd)/bin/list.o $(pwd)/bin/sync.o $(pwd)/bin/console.o $(pwd)/bin/keyboard.o $(pwd)/bin/timer.o $(pwd)/bin/ioqueue.o $(pwd)/bin/interrupt.o $(pwd)/bin/memory.o $(pwd)/bin/bitmap.o  $(pwd)/bin/print.o  $(pwd)/bin/string.o $(pwd)/bin/debug.o

#将内核文件写入磁盘,loader程序会将其加载到内存运行
dd if=$(pwd)/bin/kernel.bin of=~/bochs/hd60M.img bs=512 count=200 conv=notrunc seek=9

#rm -rf bin/*

运行

image-20250105182320858

网站已运行
发表了20篇文章 · 总计 138,209字