《操作系统真象还原》 第十一章 用户进程
一、任务状态段TSS
刚开始的CPU只支持单任务,但后来随着多任务的需求越来越多,CPU厂商提供了硬件级别的解决方案,主要就是通过TSS和LDT实现。单核CPU想实现多任务,唯一的方案就是让多个任务共享CPU,让多个任务以轮询的方式上CPU运行。CPU执行任务时,需要将所需要的数据加载到寄存器、栈和内存中,因此,当一个任务被换下CPU时,此时寄存器中的数据内容应该保存下来,下次该任务重新上CPU时,便于恢复最新的数据内容,于是,CPU厂商就为每一个任务关联了一个任务状态段(TSS),程序员为每个人任务建立与一个TSS结构体变量,CPU会自动将任务的状态保存在该结构体变量中,再将新的任务数据状态读取到寄存器中,TSS就是任务的代表,CPU用不同的TSS区分不同的任务。TSS本质上就是一段内存区域,需要用某个描述符来“描述”它,TSS描述符也要在GDT中注册,TSS描述符结构和TSS结构如下图所示:


TSS 描述符属于系统段描述符,因此 S 为 0,在 S 为 0 的情况下,TYPE 的值为 10B1。我们这里关注一下 B 位,B 表示 busy 位,B 位为 0 时,表示任务不繁忙,B 位为 1 时,表示任务繁忙。
TSS结构中基本上都是寄存器的名称,保存着任务运行中的最新状态,在TSS中保存着三组栈,分别为SS0 和 esp0,SS1 和 esp1,SS2 和 esp2。CPU在不同的特权级下用不同的栈,我们用到0特权级(内核态)和3特权级(用户态),因此只需要设置SS0和esp0就够了。通过TR寄存器将TSS加载到内存中。
在现代操作系统中为了提高任务切换的效率,使用TSS唯一的目的就是为0特权级的任务提供栈,TSS是CPU原生支持的,当用户进程发生中断时,CPU会自动从当前任务的TSS中获取SSO和esp0字段作为0特权级的栈。
二、定义和初始化TSS
在global.h中添加模块化的段描述符字段,为我们后面拼凑段描述符(TSS段描述符、用户程序用的代码段,数据段,栈段描述符)做准备。并且定义TSS的选择子、同时也要定义用户程序用的代码段、数据段、栈段选择子 。
// ---------------- 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 DIV_ROUND_UP(X, STEP) ((X + STEP - 1) / (STEP)) //用于向上取整的宏,如9/10=1#define PG_SIZE 4096
在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");
}该文件中主要根据TSS的结构定义了tss结构体,update_tss_esp()函数用于更新tss中的0特权级栈指针,make_gdt_desc()函数可以用来创建gdt描述符。
创建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在init.c文件的init_all()函数中添加tss_init()初始化tss
#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(); // 初始化PITconsole_init(); // 控制台初始化最好放在开中断之前keyboard_init(); // 键盘初始化tss_init(); // tss初始化
}三、创建用户进程
用户进程与内核进程的区别:
1.每个用户进程都拥有自己的4GB虚拟地址空间。
2.用户进程在3特权级下运行,而内核线程是在0特权级下运行。
在thread.h文件中的task_struct结构体中添加struct virtual_addr userprog_vaddr变量,指向用户进程的虚拟地址空间。
#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的边界
};在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); //初始化位图
}
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现在,用户进程已经拥有了管理自己虚拟地址空间的结构,接下来,我们需要为用户进程创建自己独立的页表。
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;
}
此函数通过调用get_kernel_pages()函数向内核空间申请了一页大小的内存保存自己的页目录。
process.h文件
uint32_t* create_page_dir(void);memory.c文件
#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位
}
该文件主要是在之前的内存池中加入了锁机制,使得访问物理内存池的操作是互斥的,添加get_user_pages()函数和addr_v2p()函数,get_user_pages()函数用于在用户地址空间申请内存,addr_v2p()函数是将一个虚拟地址转换为真实物理地址。
memory.h文件
uint32_t addr_v2p(uint32_t vaddr);
void* get_user_pages(uint32_t pg_cnt);接下来,我们需要实现从0特权级转换到3特权级,一般情况下,CPU不允许从高特权级向低特权级转换,除非是通过中断或者调用门返回的时候,我们这里使用中断返回的情况,但是,目前我们还处于内核态,用户进程还没有执行,怎么会有中断返回呢?这时就要用到我们在第九章thread_create()函数在线程的一页空间中所预留的intr_stack中断栈。

中断栈结构体的定义:
struct intr_stack {uint32_t vec_no; // kernel.S 宏VECTOR中push %1压入的中断号uint32_t edi;uint32_t esi;uint32_t ebp;uint32_t esp_dummy; // 虽然pushad把esp也压入,但esp是不断变化的,所以会被popad忽略uint32_t ebx;uint32_t edx;uint32_t ecx;uint32_t eax;uint32_t gs;uint32_t fs;uint32_t es;uint32_t ds;/* 以下由cpu从低特权级进入高特权级时压入 */uint32_t err_code; // err_code会被压入在eip之后void (*eip) (void);uint32_t cs;uint32_t eflags;void* esp;uint32_t ss;
};所以,只要我们初始化好中断栈中的内容,然后使用iret指令退出中断,就可以执行用户进程了,进程是基于线程实现的,在创建线程的过程中,最后是通过线程启动器kthread_start()执行该线程的程序,因此,我们在创建用户进程时让kthread_start()去执行初始化用户进程的中断栈的内容,让中断栈的eip指针指向用户进程要执行的程序,当iret指令退出中断时,就会执行用户进程。
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_stackstruct 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");
}start_process()函数就是线程启动器所执行的函数,用于初始化中断栈的内容。
process.h文件
#define USER_STACK3_VADDR (0xc0000000 - 0x1000) //定义了一页C语言程序的栈顶起始地址(虚拟),书p511
void start_process(void* filename_);
void intr_init(void* func);添加一些需要用到的属性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
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;
}此函数用于为指定的虚拟地址向内核空间或者用户空间申请一页的内存空间。
memory.h
void* get_a_page(enum pool_flags pf, uint32_t vaddr);
我们完成了进程的创建和初始化,当进程调度上处理机时,我们还需要将页表切换到进程自己的页表以及当发生时钟中断时,需要通过TSS的ss0和esp0找到对应的内核栈,所以,我们需要再schedule()函数中将内核栈的esp0保存在TSS中。
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,用于此进程被中断时保留上下文 */
}process.h文件
void page_dir_activate(struct task_struct* p_thread);
void process_activate(struct task_struct* p_thread);在thread.c文件中调用上面的process_active()函数
#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);
}对上面所写的所有函数进行封装 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);
}process.h文件
#define default_prio 31 //定义默认的优先级void process_execute(void* func, char* name);编写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++;}
}
