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

配合视频学习体验更佳!
第一小节:https://www.bilibili.com/video/BV1yk4y1N7r6/?spm_id_from=333.999.0.0&vd_source=701807c4f8684b13e922d0a8b116af31
第二小节:https://www.bilibili.com/video/BV1uX4y1H7Wg/?spm_id_from=333.999.0.0&vd_source=701807c4f8684b13e922d0a8b116af31

代码仓库:https://github.com/xukanshan/the_truth_of_operationg_system

intel cpu提供了TSS这种原生机制用于进程切换:

TSS:是用于存储任务状态的一个数据结构,每个任务都有自己的TSS。这个数据结构包含了在任务切换时需要保存和恢复的信息,例如处理器寄存器的值、堆栈指针、页目录基地址寄存器的值等,它放在内存的一块连续区域中。TSS是硬件级别任务切换机制的一部分。以下是TSS的使用方式:

  1. 初始化TSS:在操作系统启动时,它会初始化一个或多个TSS。每个TSS都被初始化为包含一个任务的初始状态。比如,TSS可能被初始化为指向新任务的代码段的入口点,堆栈指针可能被初始化为指向新任务的栈顶,等等。

  2. 将TSS的描述符添加到GDT中:操作系统会为每个TSS在全局描述符表(GDT)中创建一个描述符。TSS描述符包含了TSS的基地址和大小,以及其他一些标志。一旦TSS描述符被添加到GDT中,处理器就可以使用这个描述符来访问这个TSS了。

  3. 使用LTR指令加载TSS:当操作系统想要切换到一个新任务时,它会使用LTR(Load Task Register)指令将新任务的TSS描述符加载到任务寄存器(TR)中,TR寄存器中是这个任务的TSS的GDT选择子。这个操作告诉处理器新任务的TSS在哪里。

  4. 任务切换:当处理器执行任务切换时,它会自动保存当前任务的状态到当前任务的TSS中,并从新任务的TSS中恢复新任务的状态。这包括保存和恢复处理器寄存器的值、堆栈指针的值,等等。

在这里插入图片描述
现代x86体系上的操作系统并没有采用intel设计CPU时想的那种任务切换方式(见书P494、P495),因为其开销过大而导致效率过低,而是采用的一种基于TSS机制(因为这是硬件提供的,绕不开)的缩减版任务切换方式,在这种情况下,TSS主要被用于存储每个处理器的内核栈地址,以支持从用户模式到内核模式的切换,以下是关于利用TSS实现任务切换的一些要点。
1、当一个中断发生在用户态(特权级 3),处理器将从当前任务的 TSS 中获取 SS0 和 ESP0 字段的值。
2、每个 CPU 中只创建一个 TSS,在各个 CPU 上执行的所有任务都共享一个 TSS。
3、在 TR 加载 TSS 后,该 TR 寄存器将永远指向那一个 TSS,之后再也不会重新加载 TSS。
4、在进程切换时,只需要把 TSS 中的 SS0 和 ESP0 更新为新任务的内核栈的段地址以及栈指针。
5、Linux 对 TSS 的操作是一次性加载 TSS 到 TR,之后不断修改同一个 TSS 的内容,不再重复加载。
6、Linux 中任务切换不使用 call 和 jmp 指令,避免了任务切换的低效。
任务的状态信息存储位置: 当用户态触发中断后,由特权级 3 陷入特权级 0 后,CPU 自动从当前任务的 TSS 中获取 SS0 和 ESP0 字段的值,作为特权级 0 的栈,然后手动执行一系列 push 指令将任务的状态保存在特权级0的栈中

TSS作为绕不开的硬件机制,所以我们必须要先进入这种机制。也就是必须要GDT表中为其创建一个TSS段描述符,然后用加载选择子进入TR寄存器。现在我们来编写代码来做这件事情,为用户进程做准备。

首先,我们修改myos/kernel/global.h,在其中添加模块化的段描述符字段,为我们后面拼凑段描述符(TSS段描述符、用户程序用的代码段,数据段,栈段描述符)做准备。并且定义TSS的选择子、同时也要定义用户程序用的代码段、数据段、栈段选择子 字段含义查看p151

// ----------------  GDT描述符属性  ----------------

#define	DESC_G_4K    1
#define	DESC_D_32    1
#define DESC_L	     0	        // 64位代码标记,此处标记为0便可。
#define DESC_AVL     0	        // cpu不用此位,暂置为0  
#define DESC_P	     1
#define DESC_DPL_0   0
#define DESC_DPL_1   1
#define DESC_DPL_2   2
#define DESC_DPL_3   3
#define DESC_S_CODE	1
#define DESC_S_DATA	DESC_S_CODE
#define DESC_S_SYS	0
#define DESC_TYPE_CODE	8	// x=1,c=0,r=0,a=0 代码段是可执行的,非依从的,不可读的,已访问位a清0.  
#define DESC_TYPE_DATA  2	// x=0,e=0,w=1,a=0 数据段是不可执行的,向上扩展的,可写的,已访问位a清0.
#define DESC_TYPE_TSS   9	// B位为0,不忙

//定义不同的用户程序用的段描述符选择子
#define SELECTOR_U_CODE	   ((5 << 3) + (TI_GDT << 2) + RPL3)
#define SELECTOR_U_DATA	   ((6 << 3) + (TI_GDT << 2) + RPL3)
#define SELECTOR_U_STACK   SELECTOR_U_DATA

#define GDT_ATTR_HIGH		 ((DESC_G_4K << 7) + (DESC_D_32 << 6) + (DESC_L << 5) + (DESC_AVL << 4))    //定义段描述符的高32位的高字
#define GDT_CODE_ATTR_LOW_DPL3	 ((DESC_P << 7) + (DESC_DPL_3 << 5) + (DESC_S_CODE << 4) + DESC_TYPE_CODE)  //定义用户程序用的代码段描述符高32位的低字
#define GDT_DATA_ATTR_LOW_DPL3	 ((DESC_P << 7) + (DESC_DPL_3 << 5) + (DESC_S_DATA << 4) + DESC_TYPE_DATA)  //定义用户程序用的数据段描述符高32位的低字

//---------------  TSS描述符属性  ------------
#define TSS_DESC_D  0   //这个D/B位在其他段描述中用于表示操作数的大小,但这里不是,实际上它根本就没有被使用(总是设置为0)。
                        //这是因为TSS的大小和结构并不依赖于处理器运行在16位模式还是32位模式。
                        //无论何时,TSS都包含了32位的寄存器值、32位的线性地址等等,因此没有必要用D/B位来表示操作的大小

#define TSS_ATTR_HIGH ((DESC_G_4K << 7) + (TSS_DESC_D << 6) + (DESC_L << 5) + (DESC_AVL << 4) + 0x0)    //TSS段描述符高32位高字
#define TSS_ATTR_LOW ((DESC_P << 7) + (DESC_DPL_0 << 5) + (DESC_S_SYS << 4) + DESC_TYPE_TSS)            //TSS段描述符高32位低字
#define SELECTOR_TSS ((4 << 3) + (TI_GDT << 2 ) + RPL0)

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;
}; 

#define PG_SIZE 4096

接下来我们写函数,来完成TSS段描述符的创建与初始化

myos/userprog/tss.c

#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");
   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表中添加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");
}


建立myos/userprog/tss.h 增加函数声明

#ifndef __USERPROG_TSS_H
#define __USERPROG_TSS_H
#include "thread.h"
void update_tss_esp(struct task_struct* pthread);
void tss_init(void);
#endif

修改myos/kernel/init.c

#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "timer.h"
#include "memory.h"
#include "thread.h"
#include "console.h"
#include "keyboard.h"
#include "tss.h"

/*负责初始化所有模块 */
void init_all() {
    
    
   put_str("init_all\n");
   idt_init();	     // 初始化中断
   mem_init();	     // 初始化内存管理系统
   thread_init();    // 初始化线程相关结构
   timer_init();     // 初始化PIT
   console_init();   // 控制台初始化最好放在开中断之前
   keyboard_init();  // 键盘初始化
   tss_init();       // tss初始化
}

接下来,我们实现用户进程

进程与内核线程的核心区别是有两个:

1、进程有单独的4GB空间(虚拟)

2、进程运行在特权级3,而内核线程运行在特权级0

现在我们来实现核心1,它包含两个方面,A、一个管理自己虚拟地址空间的地址池;B、一个自己独立的页表

现在实现上述核心1的A方面。这就意味着每个进程肯定要有个内存池结构体来管理这个虚拟地址空间,所以修改 myos/thread/thread.h 中的task_struct结构体,增加虚拟内存池结构体,来管理自己的虚拟地址空间

#include "memory.h"

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的边界
};

增加了这个内存池结构体,自然就意味着我们创建进程的时候我们要初始化它,由于我们创建进程大部分工作与创建线程重合,所以就不修改创建线程的函数也就是增加初始化内存池结构体的代码,因为线程没有自己的虚拟地址空间。所以我们单独写个初始化虚拟内存池结构体的函数

myos/userprog/process.c

#include "process.h"
#include "thread.h"
#include "global.h"   //定义了PG_SIZE
#include "memory.h"
#include "bitmap.h"

//用于初始化进程pcb中的用于管理自己虚拟地址空间的虚拟内存池结构体
void create_user_vaddr_bitmap(struct task_struct* user_prog) {
    
    
   user_prog->userprog_vaddr.vaddr_start = USER_VADDR_START;
   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);        //初始化位图
}

支持代码 myos/userprog/process.h

#ifndef __USERPROG_PROCESS_H 
#define __USERPROG_PROCESS_H 
#include "thread.h"
void create_user_vaddr_bitmap(struct task_struct* user_prog);
#define USER_VADDR_START 0x8048000	 //linux下大部分可执行程序的入口地址(虚拟)都是这个附近,我们也仿照这个设定

#endif

支持代码 myos/kernel/global.h

#define DIV_ROUND_UP(X, STEP) ((X + STEP - 1) / (STEP))  //用于向上取整的宏,如9/10=1

接着,我们实现上述核心1的B方面。即独立的页表

myos/userprog/process.c

#include "string.h"
#include "console.h"
//用于为进程创建页目录表,并初始化(系统映射+页目录表最后一项是自己的物理地址,以此来动态操作页目录表),成功后,返回页目录表虚拟地址,失败返回空地址
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;
}

函数声明 myos/userprog/process.h

uint32_t* create_page_dir(void);

支持代码 myos/kernel/memory.c 物理内存池结构体增加锁,为的是互斥访问物理内存池,增加函数函数addr_v_to_p与get_user_pages,其余的函数只是增加了锁相关的操作

#include "sync.h"

/* 核心数据结构,物理内存池, 生成两个实例用于管理内核物理内存池和用户物理内存池 */
struct pool {
    
    
   struct bitmap pool_bitmap;	 // 本内存池用到的位图结构,用于管理物理内存
   uint32_t phy_addr_start;	     // 本内存池所管理物理内存的起始地址
   uint32_t pool_size;		    // 本内存池字节容量
   struct lock lock;		 // 申请内存时互斥
};

//初始化内核物理内存池与用户物理内存池
static void mem_pool_init(uint32_t all_mem) {
    
    
   	put_str("   mem_pool_init start\n");
   	uint32_t page_table_size = PG_SIZE * 256;	  // 页表大小= 1页的页目录表+第0和第768个页目录项指向同一个页表+
                                                  // 第769~1022个页目录项共指向254个页表,共256个页表
   	uint32_t used_mem = page_table_size + 0x100000;	  // 已使用内存 = 1MB + 256个页表
   	uint32_t free_mem = all_mem - used_mem;
   	uint16_t all_free_pages = free_mem / PG_SIZE;        //将所有可用内存转换为页的数量,内存分配以页为单位,丢掉的内存不考虑
   	uint16_t kernel_free_pages = all_free_pages / 2;     //可用内存是用户与内核各一半,所以分到的页自然也是一半
   	uint16_t user_free_pages = all_free_pages - kernel_free_pages;   //用于存储用户空间分到的页

/* 为简化位图操作,余数不处理,坏处是这样做会丢内存。
好处是不用做内存的越界检查,因为位图表示的内存少于实际物理内存*/
   	uint32_t kbm_length = kernel_free_pages / 8;			  // 内核物理内存池的位图长度,位图中的一位表示一页,以字节为单位
   	uint32_t ubm_length = user_free_pages / 8;			  // 用户物理内存池的位图长度.

   	uint32_t kp_start = used_mem;				  // Kernel Pool start,内核使用的物理内存池的起始地址
   	uint32_t up_start = kp_start + kernel_free_pages * PG_SIZE;	  // User Pool start,用户使用的物理内存池的起始地址

   	kernel_pool.phy_addr_start = kp_start;       //赋值给内核使用的物理内存池的起始地址
   	user_pool.phy_addr_start   = up_start;       //赋值给用户使用的物理内存池的起始地址

   	kernel_pool.pool_size = kernel_free_pages * PG_SIZE;     //赋值给内核使用的物理内存池的总大小
   	user_pool.pool_size	 = user_free_pages * PG_SIZE;       //赋值给用户使用的物理内存池的总大小

   	kernel_pool.pool_bitmap.btmp_bytes_len = kbm_length;     //赋值给管理内核使用的物理内存池的位图长度
   	user_pool.pool_bitmap.btmp_bytes_len	  = ubm_length;   //赋值给管理用户使用的物理内存池的位图长度

/*********    内核内存池和用户内存池位图   ***********
 *   位图是全局的数据,长度不固定。
 *   全局或静态的数组需要在编译时知道其长度,
 *   而我们需要根据总内存大小算出需要多少字节。
 *   所以改为指定一块内存来生成位图.
 *   ************************************************/
// 内核使用的最高地址是0xc009f000,这是主线程的栈地址.(内核的大小预计为70K左右)
// 32M内存占用的位图是2k.内核内存池的位图先定在MEM_BITMAP_BASE(0xc009a000)处.
   	kernel_pool.pool_bitmap.bits = (void*)MEM_BITMAP_BASE;      //管理内核使用的物理内存池的位图起始地址
							       
/* 用户内存池的位图紧跟在内核内存池位图之后 */
   	user_pool.pool_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length);     //管理用户使用的物理内存池的位图起始地址
   /******************** 输出内存池信息 **********************/
   	put_str("      kernel_pool_bitmap_start:");put_int((int)kernel_pool.pool_bitmap.bits);
   	put_str(" kernel_pool_phy_addr_start:");put_int(kernel_pool.phy_addr_start);
   	put_str("\n");
   	put_str("      user_pool_bitmap_start:");put_int((int)user_pool.pool_bitmap.bits);
   	put_str(" user_pool_phy_addr_start:");put_int(user_pool.phy_addr_start);
   	put_str("\n");

   /* 将位图置0*/
   	bitmap_init(&kernel_pool.pool_bitmap);
   	bitmap_init(&user_pool.pool_bitmap);

	lock_init(&kernel_pool.lock);
   	lock_init(&user_pool.lock);

   /* 下面初始化内核虚拟地址的位图,按实际物理内存大小生成数组。*/
   	kernel_vaddr.vaddr_bitmap.btmp_bytes_len = kbm_length;      // 赋值给管理内核可以动态使用的虚拟地址池(堆区)的位图长度,
         //其大小与管理内核可使用的物理内存池位图长度相同,因为虚拟内存最终都要转换为真实的物理内存,可用虚拟内存大小超过可用物理内存大小在
         //我们这个简单操作系统无意义(现代操作系统中有意义,因为我们可以把真实物理内存不断换出,回收,来让可用物理内存变相变大)

  /* 位图的数组指向一块未使用的内存,目前定位在内核内存池和用户内存池之外*/
   	kernel_vaddr.vaddr_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length + ubm_length);   //赋值给管理内核可以动态使用的虚拟内存池(堆区)的位图起始地址

   	kernel_vaddr.vaddr_start = K_HEAP_START;     //赋值给内核可以动态使用的虚拟地址空间的起始地址
   	bitmap_init(&kernel_vaddr.vaddr_bitmap);     //初始化管理内核可以动态使用的虚拟地址池的位图
   	put_str("   mem_pool_init done\n");
}

/* 从内核物理内存池中申请pg_cnt页内存,成功则返回其虚拟地址,失败则返回NULL */
void* get_kernel_pages(uint32_t pg_cnt) {
    
    
	lock_acquire(&kernel_pool.lock);
   	void* vaddr =  malloc_page(PF_KERNEL, pg_cnt);
   	if (vaddr != NULL) {
    
    	   // 若分配的地址不为空,将页框清0后返回
      	memset(vaddr, 0, pg_cnt * PG_SIZE);
   	}
	lock_release(&kernel_pool.lock);
   	return vaddr;
}

/* 在用户空间中申请4k内存,并返回其虚拟地址 */
void* get_user_pages(uint32_t pg_cnt) {
    
    
   lock_acquire(&user_pool.lock);
   void* vaddr = malloc_page(PF_USER, pg_cnt);
   memset(vaddr, 0, pg_cnt * PG_SIZE);
   lock_release(&user_pool.lock);
   return vaddr;
}

//将虚拟地址转换成真实的物理地址
uint32_t addr_v2p(uint32_t vaddr) {
    
    
   uint32_t* pte = pte_ptr(vaddr);	//将虚拟地址转换成页表对应的页表项的地址
   return ((*pte & 0xfffff000) + (vaddr & 0x00000fff));		//(*pte)的值是页表所在的物理页框地址,去掉其低12位的页表项属性+虚拟地址vaddr的低12位
}

**函数声明 myos/kernel/memory.h **

uint32_t addr_v2p(uint32_t vaddr);
void* get_user_pages(uint32_t pg_cnt);

现在来完成核心2,从特权级0进入特权级3

特权级从0到3的途径之一是中断返回:执行iret时,此时cs中加载的是用户程序代码段的选择子(RPL = 3)。所以完成特权级的切换核心就是围绕在中断返回时让CS是用户程序的代码段选择子。

我们先要初始化中断栈的数据,为第一次启动进程做准备。由于我们的进程是基于线程,所以我们新加入的函数模块最好不要修改原有的线程执行逻辑。在原有内核线程中,我们是通过switch_to的ret指令进入了kernel_thread这个线程启动器,由线程启动器去执行我们真正的内核线程。

所以,我们在这个基础上修改,让这个线程启动器kernel_thread进入中断栈初始化函数,也就是我们到时候送入kernel_thread的参数不是我们要执行的进程地址,而是中断栈初始化函数start_process。也就是在ret基础上加上了iret。

myos/userprog/process.c

extern void intr_exit(void);

//用于初始化进入进程所需要的中断栈中的信息,传入参数是实际要运行的函数地址(进程),这个函数是用线程启动器进入的(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");
}


支持代码myos/userprog/process.h

#define USER_STACK3_VADDR  (0xc0000000 - 0x1000)    //定义了一页C语言程序的栈顶起始地址(虚拟),书p511
void start_process(void* filename_);
void intr_init(void* func);

支持代码myos/kernel/global.h

//定义eflages寄存器用的一些字段,含义见书p511
#define EFLAGS_MBS	(1 << 1)	// 此项必须要设置
#define EFLAGS_IF_1	(1 << 9)	// if为1,开中断
#define EFLAGS_IF_0	0		// if为0,关中断
#define EFLAGS_IOPL_3	(3 << 12)	// IOPL3,用于测试用户程序在非系统调用下进行IO
#define EFLAGS_IOPL_0	(0 << 12)	// IOPL0

支持代码myos/kernel/memory.c

#include "thread.h"

//用于为指定的虚拟地址申请一个物理页,传入参数是这个虚拟地址,要申请的物理页所在的地址池的标志。申请失败,返回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;
}

myos/kernel/memory.h 函数声明

void* get_a_page(enum pool_flags pf, uint32_t vaddr);

现在还没有完成所有的工作,上述工作仅仅是初始化或者叫创建一个进程。当进程由由于主进程的时间片到期而调度上机时。A、需要切换到进程自己的页表,这个还没有做到。B、中断退出进入进程执行,但是当进程执行过程中,由于时钟中断发生,需要从TSS中取出进程0级的ss与esp,才能顺利切换到内核栈中,所以,我们需要修改schedule函数,将进程的内核栈的esp0保存到TSS中。

实现上述两方面

myos/userprog/process.c

#include "tss.h"
#include "debug.h"

/* 激活页表 */
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,使新页表生效
}


//用于加载进程自己的页目录表,同时更新进程自己的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,用于此进程被中断时保留上下文 */
}

支持代码 myos/userprog/process.h

void page_dir_activate(struct task_struct* p_thread);
void process_activate(struct task_struct* p_thread);

在myos/thread/thread.c中的schedule使用上面的process_activate函数

#include "process.h"

/* 实现任务调度 */
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;
   process_activate(next); //激活任务页表
   switch_to(cur, next);   
}

接下来,我们进行封装,将之前写的,封装成一个用于创建进程的函数

myos/userprog/process.c

#include "interrupt.h"
	
//用于创建进程,参数是进程要执行的函数与他的名字
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);
    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);
}

支持代码,myos/userprog/process.h

#define default_prio 31 //定义默认的优先级

struct list thread_ready_list; //线程就绪队列
struct list thread_all_list;   //线程全部队列

void process_execute(void* func, char* name);

支持代码,myos/thread/thread.h

extern struct list thread_ready_list;
extern struct list thread_all_list;

测试函数myos/main.c

#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++;
   }
}

我们这里做一个总结,归纳进程是怎么创建与被调度切换的。

如同内核线程一样,进程需要有一个自己的task_struct结构体(内核空间中),这个结构体中存着进程自己的管理信息,相比于内核线程,进程的task_struct中多出了至关重要的虚拟内存池结构体用于管理进程自己的虚拟地址空间(这个结构体与它的位图都在内核空间中),以及记录自己的页目录表位置的变量(创建的页目录表与页表均放在内核空间中),这个就体现了为什么进程有自己独立的虚拟地址空间。

switch_to的ret进入的start_process函数准备进程的中断栈中的内容,通过iret去真正进入进程要执行的函数(作为对比,内核线程是switch_to中的ret进入线程启动器直接执行函数,相当于进程在线程的基础上多了iret),所以我们只要在中断栈中准备好iret返回的信息就行了,中断栈里面的段寄存器选择子字段全是DPL = 3,所以iret之后,就进入了用户态。而且中断栈中要设定好用户栈的栈顶位置(这个栈空间就要在用户空间中)。

切换到进程执行前,我们要去TSS中,设定好下次进程进入中断时用到的内核栈的栈顶。

当进程运行的好好的,发生中断后(如时钟中断),CPU会自动从TSS中取出ss0与esp0,然后将进程在用户态运行的信息保存在取出的ss0:esp0指向的内核栈中(相当于内核栈中存着用户栈的栈顶位置)。假设此时发生切换,那么内核栈的栈顶位置将会保存在task_struct结构体中与TSS中。当下次被切换上CPU时,从task_struct中取出内核栈的栈顶位置,然后从中弹出用户栈的栈顶位置与其他执行环境,最后iret发返回(时钟中断的)回到用户态继续执行。

猜你喜欢

转载自blog.csdn.net/kanshanxd/article/details/131487723