TSS 是 x86CPU 的特定结构,被用来定义“任务”,它是内置到处理器原生支持的多任务的一种形式。
通过 call 指令+TSS 选择子的形式进行任务切换,此过程大概分成 10 步,这还是直接用 TSS 选择子进行任务切换的步骤,这已经非常繁琐了,在每一次任务切换过程中, CPU 除了做特权级检查外,还要在 TSS 的加载、保存、设置 B位,以及设置标志寄存器 eflags 的 NT 位诸多方面消耗很多精力,这导致此种切换方式效能很低。
其次,常见的指令集有两大派系,复杂指令集 CISC 和精简指令集 RISC。x86 使用的指令集属于 CISC,虽然指令功能很强,但这只是开发效率上的提升,执行效率却下降了, CISC 的强大需要更多的时钟周期作为代价。
最后,一个任务需要单独关联一个 TSS, TSS 需要在 GDT 中注册, GDT 中最多支持 8192 个描述符,为了支持更多的任务,随着任务的增减,要及时修改 GDT,在其中增减 TSS 描述符,修改过后还要重新加载 GDT。这种频繁修改描述符表的操作还是很消耗 CPU 资源的。
但是因为CPU的限制,有一件工作必须且只能用 TSS 来完成,这就是 CPU 向更高特权级转移时所使用的栈地址,需要提前在 TSS 中写入 。导致转移到更高特权级的一种情况是在用户模式下发生中断, CPU 会由低特权级进入高特权级,这会发生堆梭的切换。当一个中断发生在用户模式(特权级 3 ),处理器从当前 TSS 的 ss0 和 esp0 成员中获取用于处理中断的堆枝。因此,我们必须创建一个 TSS ,并且至少初始化 TSS 中的这些字段 。
Linux 在 TSS 中只初始化了 ss0 、 esp0 和 1/0 位图字段,除此之外 TSS 便没用了,就是个空架子,不再做保存任务状态之用。
当 CPU 由低特权级进入高特权级时, CPU 会“自动”从 TSS 中获取对应高特权级的栈指针( TSS 是 CPU 内部框架原生支持的嘛,当然是自动从中获取新的栈指针) 。 我们具体说一下, Linux 只用到了特权 3 级和特权 0 级,因此 CPU 从 3 特权级的用户态进入 0 特权级的内核态时(比如从用户进程进入中断), CPU 自动从当前任务的 TSS 中获取 ss0 和 esp0 字段的值作为 0 特权级的栈,然后 Linux “手动”执行一系列的 push 指令将任务的状态的保存在 0 特权级栈中,也就是 TSS 中 ss0 和 esp0 所指向的栈。
要知道,人家 Intel 当初是打算让 TR 寄存器指向不同任务的 TSS 以实现任务切换的, Linux 这里只换了 TSS 中的部分内容,而 TR 本身没换,还是指向同一个 TSS ,这种“自欺欺人”的好处是任务切换的开销更小了,因为和修改 TSS 中的内容所带来的开销相比,在 TR 中加载 TSS 的开销要大得多 。
另外, Linux 中任务切换不使用 call 和 jmp 指令,这也避免了任务切换的低效
定义并初始化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
/* 代码段和数据段属于存储段,tss和各种门描述符属于系统段s为1时表示存储段,为0时表示系统段.
*/
#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_K_CODE ((1 << 3) + (TI_GDT << 2) + RPL0)
#define SELECTOR_K_DATA ((2 << 3) + (TI_GDT << 2) + RPL0)
#define SELECTOR_K_STACK SELECTOR_K_DATA
#define SELECTOR_K_GS ((3 << 3) + (TI_GDT << 2) + RPL0)
/* 第3个段描述符是显存,第4个是tss */
#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))
#define GDT_CODE_ATTR_LOW_DPL3 ((DESC_P << 7) + (DESC_DPL_3 << 5) + (DESC_S_CODE << 4) + DESC_TYPE_CODE)
#define GDT_DATA_ATTR_LOW_DPL3 ((DESC_P << 7) + (DESC_DPL_3 << 5) + (DESC_S_DATA << 4) + DESC_TYPE_DATA)//--------------- TSS描述符属性 ------------
#define TSS_DESC_D 0 #define TSS_ATTR_HIGH ((DESC_G_4K << 7) + (TSS_DESC_D << 6) + (DESC_L << 5) + (DESC_AVL << 4) + 0x0)
#define TSS_ATTR_LOW ((DESC_P << 7) + (DESC_DPL_0 << 5) + (DESC_S_SYS << 4) + DESC_TYPE_TSS)
#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;
};
/* 任务状态段tss结构 */
//tss由程序员提供,CPU维护
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;
};
static struct tss tss;/* 更新tss中esp0字段的值为pthread的0级线,即线程pthread的PCB所在页的最顶端 */
void update_tss_esp(struct task_struct* pthread) {tss.esp0 = (uint32_t*)((uint32_t)pthread + PG_SIZE);
}/* 创建新的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.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;//为其 ssO 字段赋 0 级技段的选择子tss.io_base = tss_size;//将tss的io base字段置为tss的大小tss size,这表示此TSS中并没有IO位图/* gdt段基址为0x900,把tss放到第4个位置,也就是0x900+0x20的位置 *//* 在gdt中添加特权级dpl为0的TSS描述符 *///在 GDT 中一个描述符8字节,第 0 个段描述符不可用,第 1 个为代码段,第 2 个为数据段和楠,第 3 个为显存段,因此把 tss 放到第 4 个位置,也就是 0xc0000900+4*8=0xc0000900+0x20 的位置。*((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");
}
实现用户进程
进程与内核线程最大的区别是进程有单独的 4GB 空间,这指的是虚拟地址,物理地址空间可未必有那么大,看似无限的虚拟地址经过分页机制之后,最终要落到有限的物理页中。
进程是基于线程实现的,因此它和线程一样使用相同的 pcb 结构,即 struct task_struct,我们要做的就是在此结构中增加一个成员,用它来跟踪用户空间虚拟地址的分配情况。
/* 进程或线程的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 virtual_addr userprog_vaddr; // 用户进程的虚拟地址池uint32_t stack_magic; // 用这串数字做栈的边界标记,用于检测栈的溢出
};
页表虽然用于管理内存,但它本身也要用内存来存储,所以要为每个进程单独申请存储页目录项及页表项的虚拟内存页。
除此之外,咱们之前创建的线程属于内核的线程,它们运行在特权级0。和它们相比,用户进程还多了个特权级 3,大多数情况下,用户进程在特权级 3 下工作,因此,我们还要为用户进程创建在 3 特权级的栈。栈也是内存区域,所以,咱们还得为进程分配内存(虚拟内存)作为 3 级栈空间
/* 内存池结构,生成两个实例用于管理内核内存池和用户内存池 */
struct pool {struct bitmap pool_bitmap; // 本内存池用到的位图结构,用于管理物理内存uint32_t phy_addr_start; // 本内存池所管理物理内存的起始地址uint32_t pool_size; // 本内存池字节容量struct lock lock; // 申请内存时互斥
};/* 在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) { // 内核内存池...} else { // 用户内存池struct task_struct* cur = running_thread();bit_idx_start = bitmap_scan(&cur->userprog_vaddr.vaddr_bitmap, pg_cnt);if (bit_idx_start == -1) {return NULL;}while(cnt < pg_cnt) {bitmap_set(&cur->userprog_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1);}vaddr_start = cur->userprog_vaddr.vaddr_start + bit_idx_start * PG_SIZE;/* (0xc0000000 - PG_SIZE)做为用户3级栈已经在start_process被分配 */ASSERT((uint32_t)vaddr_start < (0xc0000000 - PG_SIZE));}return (void*)vaddr_start;
}/* 在用户空间中申请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;
}/* 将地址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 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;
}/* 得到虚拟地址映射到的物理地址 */
uint32_t addr_v2p(uint32_t vaddr) {uint32_t* pte = pte_ptr(vaddr);/* (*pte)的值是虚拟地址映射的页表所在的物理页框地址,* 去掉其低12位的页表项属性+虚拟地址vaddr的低12位 */return ((*pte & 0xfffff000) + (vaddr & 0x00000fff));
}/* 初始化内存池 */
static void mem_pool_init(uint32_t all_mem) {...lock_init(&kernel_pool.lock);//锁初始化lock_init(&user_pool.lock);...
}
CPU 不允许从高特权级转向低特权级,除非是从中断和调用门返回的情况下。咱们系统中不打算使用调用门,因此,咱们进入特权级 3 只能借助从中断返回的方式
关键点 1 :从中断返回,必须要经过 intr_exit,即使是“假装”
关键点 2:必须提前准备好用户进程所用的栈结构,在里面填装好用户进程的上下文信息,借一系列 pop 出栈的机会,将用户进程的上下文信息载入CPU的寄存器,为用户进程的运行准备好环境。
关键点 3 :我们要在栈中存储的 cs 选择子,其 RPL 必须为 3 。
关键点 4:栈中段寄存器的选择子必须指向 DPL 为 3 的内存段,因为在 RPL=CPL=3 的情况下,用户进程只能访问 DPL 为 3 的内存段,即代码段、数据段、栈段
关键点 5:必须使栈中 eflags 的 IF 位为 1,继续响应新的中断。
关键点 6:必须使栈中 eflags 的 IOPL 位为 0。不允许用户进程直接访问硬件
//eflags的属性位
#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#define NULL ((void*)0)
#define DIV_ROUND_UP(X, STEP) ((X + STEP - 1) / (STEP))
#define bool int
#define true 1
#define false 0
bbs
C 程序的内存空间中,位于低处的三个段是代码段、数据段和 bss 段,它们由编译器和链接器规划地址空间,在程序被操作系统加载之前它们地址就固定了。而堆是位于 bss 段的上面,栈是位于堆的上面,它们共享 4GB 空间中除了代码段、数据段及顶端命令行参数和环境变量等以外的其余可用空间,它们的地址由操作系统来管理,在程序加载时为用户进程分配栈空间,运行过程中为进程从堆中分配内存。堆向上扩展,栈向下扩展,因此在程序的加载之初,操作系统必须为堆和栈分别指定起始地址。
堆是要安排在 bss 以上的,按理说我们只要找到 bss 的结束地址就可以自由规划堆的起始地址了。
大伙儿知道, C 程序大体上分为预处理、编译、汇编和链接四个阶段。根据语法规则,编译器会在汇编阶段将汇编程序源码中的关键字 section 或 segment 编译成节,也就是之前介绍的 section,此时只是生成了目标文件,目标文件中的这些节还不是程序空间中的独立的代码段或数据段,或者说仅仅是代码段或数据段的一部分。链接器将这些目标文件中属性相同的节( section )合并成段(segment),因此一个段是由多个节组成的,我们平时所说的 C 程序内存空间中的数据段、代码段就是指合并后的 segment。
为什么要将 section 合并成 segment?这么做的原因也很简单, 一是为了保护模式下的安全检查,二是为了操作系统在加载程序时省事。因为在操作系统的视角中,它只关心程序中这些节的属性是什么,以便加载程序时为其分配不同的段选择子,从而使程序内存指向不同的段描述符,起到保护内存的作用。因此最好是链接器把目标文件中属性相同的节合井到一起,这样操作系统便可统一为其分配内存了。
照属性来划分节,大致上有三种类型。
- 可读写的数据,如数据节.data 和未初始化节.bss 。
- 只读可执行的代码,如代码节 .text 和初始化代码节 .init 。
- 只读数据,如只读数据节 .rodata,一般情况下字符串就存储在此节。
经过这样的划分,所有节都可归并到以上三种之一,这样方便了操作系统加载程序时的内存分配。由链接器把目标文件中相同属性的节归井之后的节的集合,便称为 segment,它存在于二进制可执行文件中,也就是 C 程序运行时内存空间中分布的代码段、数据段等段
未运行之前或运行之初,程序中 bss 中的内容都是未初始化的数据,它们也是变量,只不过这些变量的值在最初时是多少都无所谓,它们的意义是在运行过程中才产生的,故程序文件中无需存在 bss 实体,因此不占用文件大小。在程序运行后那些位于 bss 中的未初始化数据便被赋予了有意义的值,那时 bss 开始变得有意义,故 bss 仅存在于内存中。您看,既然 bss 中的数据也是变量,就肯定要占用内存空间,需要把空间预留出来,但它们并不在文件中存在,对于这种只占内存又不占文件系统空间的数据,链接器采取了合理的做法:由于 bss 中的内容是变量,其属性为可读写,这和数据段属性一致,故链接器将 bss 占用的内存空间大小合并到数据段占用的内存中,这样便在数据段中预留出 bss 的空间以供程序在将来运行时使用。
bss 的作用就是为程序运行过程中使用的未初始化数据变量提前预留了内存空间。程序的 bss 段(数据段的一部分)会由该加载器填充为 0。由此可见,为生成在某操作系统下运行的用户程序,编译器和操作系统需要相互配合。
在 C 语言中,函数 malloc 用来动态申请内存,所谓的动态内存申请,是指程序在运行中申请的 内存,并不是在程序加载时由操作系统加载器为程序段分配的“固定、静态”内存,这种动态申请的内存就是操作系统从申请者自己的堆中分配的。既然将来要支持malloc,那么必须有堆内存管理。
既然 bss 已经被归并到数据段了,数据段的类型是可加载的 LOAD 型,程序将来加载运行时,操作系统的程序加载器会为该程序的数据段分配内存,也就是 bss 段的内存区域也会顺便被分配,因此我们不需要单独知道 bss 的结束地址,只要知道数据段的起始地址及大小,便可以确定堆的起始地址了。
extern void intr_exit(void);/* 构建用户进程初始上下文信息 */
void start_process(void* filename_) {void* function = filename_;struct task_struct* cur = running_thread();cur->self_kstack += sizeof(struct thread_stack);//使指针 self_kstack 跨过 struct thread_stack 栈,指向 struct intr_stack 梭的最低处struct intr_stack* proc_stack = (struct intr_stack*)cur->self_kstack;// 8个通用寄存器初始化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; // 用户态用不上,直接初始为0proc_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");/*将栈esp替换为我们刚刚填充好的proc_stack,然后通过 jmp intr_exit 使程序流程跳转到中断出口地址 intr_exit,通过那里的一系列 pop 指令和 iretd 指令,将 proc_stack 中的数据载入CPU 的寄存器,从而使程序“假装”退出中断,进入特权级 3 。*/
}/* 激活页表 */
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);}
}/* 创建页目录表,将当前页表的表示内核空间的页目录表项pde复制,* 成功则返回页目录的虚拟地址,否则返回-1 */
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~ 1023 个页目录项用内核页目录表的第 768~ 1023 个页目录项代替,其实就是将内核所在的页目录项复制到进程页目录表中同等位置,这样就能让用户进程的高 1GB 空间指向内核,从而实现用户程序共享内核*//************************** 1 先复制页表 *************************************//* 第一个形参:复制的目标地址为偏移用户进程页目录表基地址 768 个页目录项的地方第二个形参:复制的源地址为内核页目录表中第 768 个页目录项的地址第三个形参:1024/4=256 个页目录项的大小*/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;/*****************************************************************************/return page_dir_vaddr;
}/* 创建用户进程虚拟地址位图 */
//按照用户进程的虚拟内存信息初始化位图结构体 struct virtual addr。
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);
}/* 创建用户进程 */
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);
}
/* 实现任务调度 */
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);
}
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++;}
}