第11章-用户进程
这是一个网站有所有小节的代码实现,同时也包含了Bochs等文件
11.1 为什么要有任务状态TSS
Linux 任务切换未采用 Intel 的做法,而是用了一套自己的方法,只是用了 TSS 的一小部分功能。
操作系统最直接控制的就是 CPU,要想让 CPU 这颗奔腾的心永远地跳下去,首先必须把内存分成段,把内存按“内存块”访问,其次必须让代码段寄存器 cs
和指令寄存器EIP
指向下一条待执行的指令。
程序是一堆数据和指令的集合,它们只有被加载到内存并让 CPU 的寄存器中指向它们后,CPU 才能执行该程序。程序从文件系统上被加载到内存后,位于内存中的程序便称为映像,也称为任务。
CPU 只把 CS:[E]IP
指向的内存当成指令,把DS
指向的内存当作普通数据,因此必须人为地保证填充到这些段寄存器中的值是正确的。
11.2 LDT
LDT 是 Local Descriptor Table 的缩写,即局部描述符表。LDT 属于任务私有的结构,它是每个任务都有的,其位置自然就不固定。LDT 必须像其他描述符那样在 GDT 注册,之后便能够用选择子找到它。。为了在 GDT 中注册,必须也得为它找个描述符,用此描述符来描述某任务的 LDT 的起始地址及偏移大小,此描述符便称为 LDT 描述符
在 LDT 中,描述符的 D 位和 L 位固定为 0 。 LDT 描述符属于系统段描述符,因此 S 为 0。在 S 为 0 的前提下,若 TYPE 的值为 0010 ,这表示此描述符是 LDT 描述符。
和 GDT 一样, CPU 专门准备了个寄存器来存储其位置及偏移量,想必您又猜到了,对,这就是 LDTR 。lldt 的指令格式为:
lldt “ 16 位通用寄存器 ”或“ 16 位内存单元”
选择子是 16 位的,其高 13 位是索引值,用来在 GDT 或 LDT 中索引段描述符,第 O~ l 位 RPL,表示请求特权级,第 2 位是 TI 位,此位用来指定选择子中的高 13 位是在 GDT中索引段描述符,还是在 LDT 中索引段描述符。 TI 位也就是 Table Indicator,当此位为 1 时, 表示从 LDT中检索,反之当此位为 0 时,表示从 GDT 中检索选择子。
当 TI 为 0 时, CPU 到 GDTR 中找 GDT,当 TI 为 1 时, CPU 到 LDTR 中找 LDT 。
选择子的高 13 位表示可索引的描述符范围, 2 的 13 次方等于 8192,也就是说一个任务最多可定义 8192个内存段。由于 LDT 描述符放在 GDT 中,如果任务是用 LDT 来实现的话,最多可同时创建 8192 个任务。当前运行的任务,其 LDT 位于 LDTR 指向的地址,这样 CPU 才能从中拿到任务运行所需要的资源。因此,每切换一个任务时,需要用 lldt 指令重新加载新任务的 LDT 到 LDTR。
11.3 TSS
TSS 是由程序员“提供”的,由 CPU 来“维护”。"提供”就是指 TSS 是程序员为任务单独定义的一个结构体变量,“维护”是指 CPU 自动用此结构体变量保存任务的状态和自动从此结构体变量中载入任务的状态。当加载新任务时, CPU 自动把当前任务( 旧任务)的状态存入当前任务的 TSS ,然后将新任务 TSS 中的数据载入到对应的寄存器中,这就实现了任务切换。 TSS 就是任务的代表, CPU 用不同的 TSS 区分不同的任务,因此任务切换的本质就是 TSS 的换来换去。
TSS 和其他段一样,本质上是一片存储数据的内存区域, Intel 打算用这片内存区域保存任务的最新状态(也就是任务运行时占用的寄存器组等),因此它也像其他段那样,需要用某个描述符结构来“描述”它,这就是 TSS 描述符, TSS 描述符也要在 GDT 中注册,这样才能“找到它。
**这里关注一下B位,B表示busy位,B位为0时,表示任务不繁忙,B位为1时,表示任务繁忙。**B 位是由 CPU 来维护的,不需要咱们人工干预。
任务繁忙有两方面的含义, 一方面就是指此任务是否为当前正在 CPU 上运行的任务。另一方面是指此任务嵌套调用了新的任务, CPU 正在执行新任务,此任务暂时挂起,等新任务执行完成后 CPU 会回到 此任务继续执行,所以此任务马上就会被调度执行了。
嵌套任务调用的情况还会影响 eflags
寄存器中的 NT 位,这表示任务嵌套。
TSS 同其他普通段一样,是位于内存中的区域,因此可以把 TSS 理解为 TSS 段,只不过 TSS 中的数据井不像其他普通段那样散乱, TSS 中的数据是按照固定格式来存储的,所以 TSS 是个数据结构。
和 LDT 一样, CPU 对 TSS 的处理也采取了类似的方式,它提供了一个寄存器来存储 TSS 的起始地址及偏移大小。但也许让人有点意外,这个寄存器不叫 TSSR,而是称为 TR。 TSS 和 LDT 一样,必须要在 GDT 中注册才行,这也是为了在引用描述符的阶段做安全检查。因此 TSS 是通过选择子来访问的,将tss加载到寄存器 TR 的指令是 itr
其指令格式为:
itr "16 位通用寄存器”或” 16 位内存单元”
TSS 和 LDT 都只能且必须在 GDT 中注册描述符,TR 寄存器中存储的是 TSS 的选择子, LDTR 寄存器中存储的是 LDT的选择子, GDTR 寄存器中存储的是GDT 的起始地址及界限偏移(大小-1)。
11.4 现代操作系统采用的任务切换方式
尽管 CPU 提供了0、 1 、 2 、 3 共 4 个特权级,但我们效仿 Linux 只用其中的 2 个,内核处理特权级0,用户进程处于特权级 3 。 我们使用 TSS 唯一的理由是为 0 特权级的任务提供枝。 Linux 为每个 CPU 创建一个 TSS,在各个 CPU 上的所有任务共享同一个 TSS,各 CPU 的 TR 寄存器保存各 CPU 上的 TSS,在用 ltr 指令加载 TSS 后,该 TR 寄存器永远指向同一个 TSS,之后再也不会重新加载 TSS。在进程切换时,只需要把 TSS 中的 ss0及 esp0更新为新任务的内核战的段地址及栈指针。
实际上 Linux 对 TSS 的操作是一次性加载 TSS 到 TR,之后不断修改同一个 TSS 的内容,不再进行重复加载操作。Linux 在 TSS 中只初始化了 sso 、 esp0 和 1/0 位图宇段,除此之外 TSS 便没用了,就是个空架子
以下是关于利用TSS实现任务切换的一些要点。
- 当一个中断发生在用户态(特权级 3),处理器将从当前任务的 TSS 中获取 SS0 和 ESP0 字段的值。
- 每个 CPU 中只创建一个 TSS,在各个 CPU 上执行的所有任务都共享一个 TSS。
- 在 TR 加载 TSS 后,该 TR 寄存器将永远指向那一个 TSS,之后再也不会重新加载 TSS。
- 在进程切换时,只需要把 TSS 中的 SS0 和 ESP0 更新为新任务的内核栈的段地址以及栈指针。
- Linux 对 TSS 的操作是一次性加载 TSS 到 TR,之后不断修改同一个 TSS 的内容,不再重复加载。
- Linux 中任务切换不使用 call 和 jmp 指令,避免了任务切换的低效。
- 任务的状态信息存储位置: 当用户态触发中断后,由特权级 3 陷入特权级 0 后,CPU 自动从当前任务的 TSS 中获取 SS0 和 ESP0 字段的值,作为特权级 0 的栈,然后手动执行一系列 push 指令将任务的状态保存在特权级0的栈中
TSS作为绕不开的硬件机制,所以我们必须要先进入这种机制。也就是必须要GDT表中为其创建一个TSS段描述符,然后用加载选择子进入TR寄存器。现在我们来编写代码来做这件事情,为用户进程做准备。
11.2 定义并初始化TSS
/*
* @Author: Adward-DYX 1654783946@qq.com
* @Date: 2024-04-09 10:07:42
* @LastEditors: Adward-DYX 1654783946@qq.com
* @LastEditTime: 2024-04-19 10:14:20
* @FilePath: /OS/chapter8/8.4/kernel/global.h
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
#ifndef __KERNEL_GLOBAL_H
#define __KERNEL_GLOBAL_H
#include "stdint.h"
/**************************************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 //=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 RPL0 0
#define RPL1 1
#define RPL2 2
#define RPL3 3
#define TI_GDT 0
#define TI_LDT 1
#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; //段界限地址15-0
uint16_t base_low_word; //段基址15-0
uint8_t base_mid_byte; //段基址 23-16
uint8_t attr_low_byte; //P、DPL、S、Type位
uint8_t limit_high_attr_high; //G、D/B、L、AVL、段界限地址19-16
uint8_t base_high_byte; //段基址31-24
};
/*-----------------------IDT描述符属性------------------*/
#define IDT_DESC_P 1 //有效位
#define IDT_DESC_DPL0 0 //描述符特权级别
#define IDT_DESC_DPL3 3
#define IDT_DESC_32_TYPE 0xE //32的门
#define IDT_DESC_16_TYPE 0x6 //16位的门
#define IDT_DESC_ATTR_DPL0 ((IDT_DESC_P<<7)+(IDT_DESC_DPL0<<5)+IDT_DESC_32_TYPE)
#define IDT_DESC_ATTR_DPL3 ((IDT_DESC_P<<7)+(IDT_DESC_DPL3<<5)+IDT_DESC_32_TYPE)
#define NULL ((void*)0)
#define bool int
#define true 1
#define false 0
#define PG_SIZE 4096
#endif // !__KERNEL_GLOBAL_H
/*
* @Author: Adward-DYX 1654783946@qq.com
* @Date: 2024-04-19 09:38:31
* @LastEditors: Adward-DYX 1654783946@qq.com
* @LastEditTime: 2024-06-23 14:24:08
* @FilePath: /OS/chapter11/11.2/userprog/tss.c
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
#include "tss.h"
#include "stdint.h"
#include "global.h"
#include "string.h"
#include "print.h"
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级栈*/
void update_tss_esp(struct task_struct* pthread){
tss.esp0 = (uint32_t*)((uint32_t)pthread+PG_SIZE); //这里也就是会的到内核栈为0xc009F000
}
/*创建gdt描述符*/
static struct gdt_desc make_gdt_desc(uint32_t* desc_addr, uint32_t limit, uint32_t attr_low, uint32_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,此函数除了用来初始化 tss 并将其安装到 GDT 中外,还另外在 GOT 中安装
两个供用户进程使用的描述符, 一个是 DPL 为 3 的数据段,另一个是 DPL 为 3 的代码段*/
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; //将tss的io_base字段置为tss的大小tss_size,这表示此TSS中并没有IO位图
//当 IO 位图的偏移地址大于等于 TSS 大小减 l 时,就表示没有 IO 位图 。
/*gdt 段基址为 Ox900 ,把 tss 放到第 4 个位置,也就是 Ox900+0x20 的位置,因为一个GDT描述有8字节64位*/
/*在 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");
}
11.3 实现用户进程
11.3.1 实现用户进程的原理
11.3.2 用户进程的虚拟地址空间
进程与内核线程最大的区别是进程有单独的 4GB 空间,这指的是虚拟地址,物理地址空间可未必有那么大,看似无限的虚拟地址经过分页机制之后,最终要落到有限的物理页中。每个进程都拥有 4GB 的虚拟地址空间,虚拟地址连续而物理地址可以不连续,这就是保护模式下分页机制的优势。
为演示此特性, 我们需要单独为每个进程维护一个虚拟地址池,用此地址池来记录该进程的虚拟中,哪些已被分配,哪些可以分配。
与各个进程相关的数据,如果数据量不大的话,最好是存储在该进程的 pcb中,这样便于管理。在上一节中您已经知道,进程是基于线程实现的,因此它和线程一样使用相同的 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; //进程自己的页表的虚拟地址
uint32_t stack_magic; ///栈的边界标记,用于检测栈的溢出
};
11.3.3 为进程创建页表和3特权级栈
**进程与线程的区别是进程拥有独立的地址空间,不同的地址空间就是不同的页表,因此我们在创建进程的过程中需要为每个进程单独创建一个页表。**我们这里所说的页表是“页目录表+页表",页目录表用来存放页目录项 PDE,每个 PDE 又指向不同的页表。
页表且然用于管理内存,但它本身也要用内存来存储,所以要为每个进程单独申请存储页目录项及页表项的虚拟内存页。
除此之外,咱们之前创建的线程属于内核的线程,它们运行在特权级0。和它们相比,用户进程还多了个特权级 3,大多数情况下,用户进程在特权级 3 下工作,因此,我们还要为用户进程创建在 3 特权级的栈。栈也是内存区域,所以,咱们还得为进程分配内存(虚拟内存)作为 3 级栈空间。
/*在 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* 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;
}
......
/*将地址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){
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; //0x100000为低端1MB内存
uint32_t free_mem = all_mem - used_mem;
uint16_t all_free_pages = free_mem / PG_SIZE; //1 页为 4KB,不管总内存是不是 4k 的倍数
//对于以页为单位的内存分配策略,不足 1 页的内存不用考虑了
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; //kernel Bitmap的长度,位图中的一位表示一页,以字节为单位
uint32_t ubm_length = user_free_pages / 8; //user Bitmap长度
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;
/** 内核内存池和用户内存池位图
* 位图是全局的数据,长度不固定。
* 全局或静态的数组需要在编译时知道其长度,
* 而我们需要根据总内存大小算出需要多少字节,
* 所以改为指定一块内存来生成位图。
*/
//内核使用的最高地址是 Oxc009f000,这是主线程的校地址
//(内核的大小预计为 70KB 左右)
//32MB内存占用的位图是2KB
///内核内存池的位图先定在 MEM_BITMAP_BASE(Oxc009a000 )处
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);
/*将位图置0*/
bitmap_init(&kernel_pool.pool_bitmap);
bitmap_init(&user_pool.pool_bitmap);
/*下面初始化内核虚拟地址的位图,按实际物理内存大小生成数组。*/
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_ini t done \n");
}
11.3.4 进入特权级3
从特权级0进入特权级3有几个关键点,这主要是涉及特权级方面的内容:
- 从中断返回,必须要经过 intr exit,即使是“假装”
- 必须提前准备好用户进程所用的栈结构,在里面填装好用户进程的上下文信息,借一系列 pop 出栈的机会,将用户进程的上下文信息载入CPU的寄存器,为用户进程的运行准备好环境。
- 我们要在栈中存储的 cs 选择子,其 RPL必须为 3 。
- 栈中段寄存器的选择子必须指向 DPL 为3 的内存段
- 必须使栈中 eflags 的 IF位为1
- 必须使栈中 eflags 的 IOPL位为0
11.3.5 用户进程创建的流程
在 process_execute
中,先调用函数 get_kernel_pages
申请 1 页内存创建进程的 pcb
,这里的 pcb
就是thread
,接下来调用函数init_thread
对出thread
进行初始化。随后调用函数create_user_vaddr_bitmap
为用户进程创建管理虚拟地址空间的位图。接着调用也thread_create
创建线程,此函数的作用是将函数start_process
和用户进程 user_prog
作为 kernel_thread
的参数,以使 kernel_thread
能够调用start_proces(user_prog)
。接下来是调用函数 create_page_dir
为进程创建页表,随后通过函数 list_append
将进程 pcb
,也就是 thread
加入就绪队列和全部队列,至此用户进程的创建部分完成,现在就等着进程运行了。
11.3.6 实现用户进程–上
-
通用寄存器:
EAX
:累加器,用于存储函数返回值和算术运算结果。EBX
:基址寄存器,通常用作内存访问的基址。ECX
:计数寄存器,通常用作循环计数器。EDX
:数据寄存器,用于存储除法和乘法的中间结果。
-
指针寄存器:
ESP
:栈指针寄存器,指向栈顶。EBP
:基址指针寄存器,用于存储当前堆栈帧的基址。
-
索引寄存器:
ESI
:源索引寄存器,通常用于字符串和内存块操作中的源数据地址。EDI
:目的索引寄存器,通常用于字符串和内存块操作中的目的数据地址。
-
段寄存器:
CS
:代码段寄存器,存储代码段基址。DS
:数据段寄存器,存储数据段基址。ES
:附加段寄存器,通常用于字符串和内存块操作中的附加数据段基址。SS
:堆栈段寄存器,存储堆栈段基址。
-
标志寄存器:
EFLAGS
:标志寄存器,存储条件码和控制指令标志。
构造用户进程的上下文环境,免不了标志寄存器 eflags 的属性位,这里我们在 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
特权级从0到3的途径之一是中断返回:执行iret时,此时cs中加载的是用户程序代码段的选择子(RPL = 3)。所以完成特权级的切换核心就是围绕在中断返回时让CS是用户程序的代码段选择子。
我们先要初始化中断栈的数据,为第一次启动进程做准备。由于我们的进程是基于线程,所以我们新加入的函数模块最好不要修改原有的线程执行逻辑。在原有内核线程中,我们是通过switch_to的ret指令进入了kernel_thread这个线程启动器,由线程启动器去执行我们真正的内核线程。
所以,我们在这个基础上修改,让这个线程启动器kernel_thread进入中断栈初始化函数,也就是我们到时候送入kernel_thread的参数不是我们要执行的进程地址,而是中断栈初始化函数start_process。也就是在ret基础上加上了iret。
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);
//下面这一句是在初始化中断栈中的栈顶位置,我们先为虚拟地址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");
}
/*激活页表*/
void page_dir_activate(struct task_struct* p_thread){
/**
* 执行此函数时,当前任务可能是线程。
* 之所以对线程也要重新安装页表,原因是上一次被调度的可能是进程,
* 否则不恢复页表的话,线程就会使用进程的页表了。
* **/
/*为内核线程,需要重新填充页表为0xlOOOOO*/
uint32_t pagedir_phy_addr = 0x100000;
//默认为内核的也目录地址,也就是内核线程所用的页目录表
if(p_thread->pgdir != NULL) //用户态进程有自己的页表目录
pagedir_phy_addr = addr_v2p((uint32_t)p_thread->pgdir);//因为页表需要单独的内存空间,创建页表(由代码 11-6-2 中的函数 create_page_dir 完成)时必然要为页表申请内存,内存管理模块返回的地址是虚拟地址,因此页表地址也是虚拟地址,所以在把页表加载到 CR3 之前,咱们要将其转换成物理地址 。
/*更新页目录寄存器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);
}
}
11.3.7 bss简介
在操作系统的视角中,它只关心程序中这些节的属性是什么,以便加载程序时为其分配不同的段选择子,从而使程序内存指向不同的段描述符,起到保护内存的作用。因此最好是链接器把目标文件中属性相同的节合井到一起,这样操作系统便可统一为其分配内存了。按照属性来划分节,大致上有三种类型。
- 可读写的数据,如数据节.data 和未初始化节.bss 。
- 只读可执行的代码,如代码节 .text 和初始化代码节 .init 。
- 只读数据,如只读数据节 .rodata,一般情况下字符串就存储在此节。
经过这样的划分,所有节都可归并到以上三种之一,这样方便了操作系统加载程序时的内存分配。由链接器把目标文件中相同属性的节归井之后的节的集合,便称为 segment,它存在于二进制可执行文件中,也就是 C 程序运行时内存空间中分布的代码段、数据段等段。
未运行之前或运行之初,程序中 bss 中的内容都是未初始化的数据,它们也是变量,只不过这些变量的值在最初时是多少都无所谓,它们的意义是在运行过程中才产生的,故程序文件中无需存在 bss 实体,因此不占用文件大小。在程序运行后那些位于 bss 中的未初始化数据便被赋予了有意义的值,那时 bss 开始变得有意义,故 bss 仅存在于内存中。
由于 bss 中的内容是变量,其属性为可读写,这和数据段属性一致,故链接器将 bss 占用的内存空间大小合并到数据段占用的内存中,这样便在数据段中预留出 bss 的空间以供程序在将来运行时使用。 故 bss仅存在于数据段所在的内存中 。因此 , bss 的作用就是为程序运行过程中使用的未初始化数据变量提前预留了内存空间。程序的 bss 段(数据段的一部分)会由该加载器填充为 0。
11.3.8 实现用户进程—下
Linux 分了 3GB 给用户空间,自己本身占 1GB ,所有用户进程的最高 1GB 空间都指向 Linux 所在的内存空间,这样操作系统就被所有用户进程共享 。
我们目前使用的是二级页表,加载到页目录表寄存器 CR3 中的是页目录表的物理地址,页目录表中一共包含 1024 个页目录项(pde),页目录项大小为 4B ,故页目录表大小为 4kb。每个页目录项仅表示 1 个页表,页目录项中存储的是页表所在物理页框的物理地址及页目录项的属性。 每个页表可容纳 1024 个页表项(pte),页表项大小为 4B,故每个页表本身占用 4kb。每个页表项仅表示一个物理页框,页表项中存储的是 4kb大小的物理页框的物理地址及页表项的属性,因此每个页表可表示的地址空间为1024*4kb=4MB,一个页目录表中可包含 1024 个页表,因此可表示 1024**4MB= GB 大小的地址空间。
现有有两种方法实现共享:
- 为每一个用户进程单独准备一份内核的拷贝映像。 (不可行)
- 为每一个用户进程准备一份内核的符号链接(软链接〉。 (使用的这个方式)
符号链接是 Linux 系统中的概念。文件都有名称,对于用户来说,文件是通过文件名来访问的,可以把文件名理解为存储在磁盘上的文件实体的访问入口。符号链接是为同一个文件实体多创建了一个访问入口,相当于为原文件起个别名,就像人有大名和小名,都是指同一个人。
现在也就是把用户进程页目录表中的第 768~ 1023 个页目录项用内核页目录表的第 768~ 1023 个页目录项代替,其实就是将内核所在的页目录项复制到进程页目录表中同等位置,这样就能让用户进程的高 1GB 空间指向内核。
/*创建页表目录,将当前页表的表示内核空间的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_pages failed!\n");
return NULL;
}
/*******************************1 先复制页表*******************************/
/****page_dir_vaddr + 0x300*4是内核也目录的第768项**/
memcpy((uint32_t*)((uint32_t)page_dir_vaddr+0x300*4), (uint32_t*)(0xfffff000+0x300*4), 1024); //其中 Oxfffff000 便是用来访问内核页目录表的基地址(也是第 0 个页目录项〕,这里是 1024 ,即 1024/4=256 个页目录项的大小
/**************************************************************************/
/*******************************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;
}
/*创建用户进程虚拟地址位图*/
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_read_list,&thread->general_tag));
list_append(&thread_read_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);
}
我们这里做一个总结,归纳进程是怎么创建与被调度切换的。
如同内核线程一样,进程需要有一个自己的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发返回(时钟中断的)回到用户态继续执行。
参考链接:https://blog.csdn.net/kanshanxd/article/details/131487723