Ⅰ.为什么需要任务状态段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描述符格式
32bits | 12bits | 20bits |
---|---|---|
段基址 | 段属性 | 段界限 |
GDTR&LDTR中存储内容为:
32bits | 16bits |
---|---|
段基地址 | 段界限 |
(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描述符并初始化
c
/* 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结构
c
/* 任务状态段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
c
/* 在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如下:
c
/* 进程或线程的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;
- 返回分配的内存起始地址
c
/* 在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)进程位图
c
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的映射
c
/* 页表中添加虚拟地址_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)虚拟地址和物理地址映射关系
c
/* 将虚拟地址映射到物理地址 */
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
c
/* 得到虚拟地址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)构建用户进程初始上下文信息
将进程的寄存器信息存入中断栈中,保证中断进行特权级切换时,能从中断栈中恢复
c
/* 构建用户进程初始上下文信息 */
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)恢复进程的页表信息
线程没有内存空间,也不存在页表,但是线程依赖于进程的资源,为了防止上一个任务为进程,因此必须要把页表重新装载
c
/* 激活页表 */
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 段,它们由编译器和链接器规划地址空间,在程序被操作系统加载之前它们地址就固定了
堆栈共享内存中处理代码段、数据段以及顶端的命令行参数和环境变量外的其他内存空间,堆的起始地址固定,向上生长,栈的栈顶指针固定,向下生长。因此在程序的加载之初,操作系统必须为堆和战分别指定起始地址。
堆的起始地址对应的是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 空间占满
在内核态中为进程创建用户页目录,同时将页目录表的最后一个页目录项更新为用户进程的页目录表物理地址(也就是用户的物理地址),因为内核需要知道该用户进程的页目录表在哪里。
c
/************************** 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.初始化用户进程位图
c
/* 创建用户进程虚拟地址位图 */
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.创建进程页表,加入就绪队列和全部队列
c
/* 创建用户进程 */
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表)、特权级切换、进程位图创建、进程页表创建
用户进程执行的具体过程
- 首先建立用户进程的上下文信息,为用户态进程的执行提供上下文信息;
- 为其注册LDT描述符,注册TSS任务状态段,添加进GDT表中;
- 创建进程PCB,初始化线程栈、中断栈、时间片、优先级等信息;
- 根据内存分页管理系统,利用内核位图为程序分配合适的虚拟内存空间,加载到虚拟内存;
- 创建进程位图,并完成位图长度、位的初始化;
- 特权级切换,加载用户进程的上下文信息,中断跳转到用户态开始执行用户进程;
- 当处理器运行时,从GDT表中得到程序内存起始地址+偏移量,访问LDT表得到程序各个函数的起始地址+偏移量;开始调度线程。还有特权级检查。
任务执行通过LDT表完成,那么如何找到LDT表呢????
将LDT表的起始地址和偏移量,存入GDT表中, LDT表中存储的程序的起始地址+偏移量,对应的程序的入口地址,CS+IP指向该地址后,取指令交由处理器执行。
内存分段分页与LDT、GDT关系
GDT和LDT实现虚拟地址和物理地址的映射。分页是实现地址映射的一种处理方法,建立分页机制,将程序按照页分配并调入内存,实现程序的快速执行。
memset本质是用I/O位图在内存中找到合适的空闲内存分配