为什么要有任务状态段TSS
现在该说说 TSS 的事了 。 TSS 是 Task State Segment 的缩写,即任务状态段。
LDT简介
程序是一堆数据和指令的集合,它们只有被加载到内存并让 CPU 的寄存器中指向它们后,CPU 才能执行该程序。程序从文件系统上被加载到内存后,位于内存中的程序便称为映像,也称为任务。
按照内存分段的方式,内存中的程序映像自然被分成了代码段、数据段等资源,这些资源属于程序私有的部分,因此 Intel 建议,为每个程序单独赋予一个结构来存储其私有的资源,这个结构就是 LDT 。
LDT 是 Local Descriptor Table 的缩写,即局部描述符表。LDT 属于任务私有的结构,它是每个任务都有的,其位置自然就不固定。为了使用它,最起码得先能够找到它。我想您肯定想到了, GDT 是全局唯一的结构,它的位置是固定且己知的,它己经由 LGDT 指令将其起始地址及偏移量存储到 GDTR 寄存器中 。因此 , LDT 必须像其他描述符那样在 GDT 注册,之后便能够用选择子找到它。
TSS的作用
Intel 的建议是给每个任务“关联”一个任务状态段,这就是 TSS (Task State Segment),用它来表示任务。
之所以称为“关联”,是因为 TSS 是由程序员“提供”的,由 CPU 来“维护”。"提供”就是指 TSS 是程序员为任务单独定义的一个结构体变量,“维护”是指 CPU 自动用此结构体变量保存任务的状态(任务的上下文环境,寄存器组的值)和自动从此结构体变量中载入任务的状态。当加载新任务时, CPU 自动把当前任务( 旧任务)的状态存入当前任务的 TSS ,然后将新任务 TSS 中的数据载入到对应的寄存器中,这就实现了任务切换。 TSS 就是任务的代表, CPU 用不同的 TSS 区分不同的任务,因此任务切换的本质就是 TSS 的换来换去。
在 CPU 中有一个专门存储 TSS 信息的寄存器,这就是TR 寄存器,它始终指向当前正在运行的任务,因此,“在 CPU 眼里”,任务切换的实质就是 TR 寄存器指向不同的 TSS。
TSS 和其他段一样,本质上是一片存储数据的内存区域, Intel 打算用这片内存区域保存任务的最新状态(也就是任务运行时占用的寄存器组等),因此它也像其他段那样,需要用某个描述符结构来“描述”它,这就是 TSS 描述符, TSS 描述符也要在 GDT 中注册,这样才能“找到它“。
B 位存在的意义可不是单纯为了表示任务忙不忙,而是为了给当前任务打个标记,目的是避免当前任务调用自己,也就是说任务是不可重入的。不可重入的意思是当前任务只能调用其他任务,不能自己调用自己 。
顺便说一句,嵌套任务调用的情况还会影响 eflags 寄存器中的 NT 位,这表示任务嵌套( Nest Tast),后面在介绍任务调度时会细说。
TSS 同其他普通段一样,是位于内存中的区域,因此可以把 TSS 理解为 TSS 段,只不过 TSS 中的数据井不像其他普通段那样散乱, TSS 中的数据是按照固定格式来存储的,所以 TSS 是个数据结构。
TSS 是 CPU 原生支持的数据结构,因此 CPU 能够直接、正确识别其中的所有宇段。当任务被换下CPU 时, CPU 会自动将当前寄存器中的值存储到 TSS 中的对应位置,当有新任务上 CPU 运行时, CPU会自动从新任务的 TSS 中找到相应的寄存器值加载到对应的寄存器中。
也许您要问了,每个任务都有自己的 TSS 结构,而 TSS 是个内存区域, CPU 是怎么知道它在哪里的呢?和 LDT 一样, CPU 对 TSS 的处理也采取了类似的方式,它提供了一个寄存器来存储 TSS 的起始地址及偏移大小。但也许让人有点意外,这个寄存器不叫 TSSR,而是称为 TR CTask Register )
有了 TSS 后,任务在被换下 CPU 时,由 CPU 自动地把当前任务的资源状态(所有寄存器、必要的内存结构,如械等)保存到该任务对应的 TSS 中(由寄存器 TR 指定〉。 CPU 通过新任务的 TSS 选择子加载新任务时,会把该回S 中的数据载入到! CPU 的寄存器中,同时用此 TSS 描述符更新寄存器 TR。注意啦,以上动作是 CPU自动完成的,不需要人工干预,这就是前面所说的硬件一级的原生支持。不过话又说回来了,第一个任务的 TSS是需要手工加载的,否则第一个任务的状态该没有地方保存了。
CPU原生支持的任务切换方式
为了支持多任务, CPU 厂商提供了 LDT 及 TSS 这两种原生支持,他们要求为每一个任务分别配一个 LDT 及 TSS (这由咱们程序员来构建), LDT 中保存的是任务自己的实体资源,也就是数据和代码,TSS 中保存的是任务的上下文状态及三种特权级的战指针、 1/0 位图等信息。既然 LDT 和 TSS 用来表示一个任务,那么任务切换就是换这两个结构:将新任务对应的 LDT 信息加载到 LD四寄存器,对应的 TSS 信息加载到 τR 寄存器。下面我们看看 CPU 是怎样进行任务切换的。
TSS 被 CPU 用于保存任务的状态及任务状态的恢复,而 LDT 是任务的实体资源, CPU 厂商只是建议这样做,其实没有 LDT 的话也是可以的。比如我们可以把任务自己的段描述符放在- GDT 中,或者干脆采用平坦模型直接用那个 4GB 大小的全局描述符。任务的段放在 GDT,还是 LDτ 中,无非就是在用选择子选择它们时有区别,区别您懂的,就是选择子中 TI 位的取值, 0 是从 GDT 中选择段描述符, 1 是从 LDT中选择段描述符。描述符及描述符表只是逻辑上对内存区域的划分(当然这也包括其他各种属性,但对此来说并不重要),任务要想执行,归根结底都是用 cs:回]ip 指向这个任务的代码段内存区域以及 DS 指向其数据段内存区域,所以任务私有的实体资摞不是必须放在它自己的 LDT 中。
综上所述, LDT 是可有可无的,真正用于区分一个任务的标志是 TSS ,所以用于任务切换的根本方法必然是和任务的 TSS 选择子相关。
进行任务切换的方式有“中断+任务门”,“ call 或 jmp+任务门”和 iretd
- 通过“中断+任务门”进行任务切换
和任务相关的是 TSS 选择子,咱们这个时钟中断的描述符是中断门描述符,中断门中存储的不是 TSS 选择子,而是目标中断处理例程的代码段选择子及偏移地址,因此处理器并没有把此中断门描述符中的中断处理程序当成新的任务。
当调用一个新任务时,处理器做了两件准备工作 。
• 自动将新任务 eflags 中的 NT 位置为 1 ,这就表示新任务能够执行的原因是被别的任务调用,也就是嵌套调用。
• 随后处理器将旧任务的 TSS 选择子写入新任务 TSS 的“上一个任务的 TSS 指针”宇段中。
- call、jump切换任务
现代操作系统采用的任务切换方式
硬件是软件的舞台,软件再强大也要向硬件 CPU 低头, CPU 要求用 TSS 这是硬指标, Linux 也得遵守。不过为了“应付”这一指标, Linux 为每个 CPU 创建一个 TSS,在各个 CPU 上的所有任务共享同一个 TSS,各 CPU 的 TR 寄存器保存各 CPU 上的 TSS,在用 ltr 指令加载 TSS 后,该 TR 寄存器永远指向同一个 TSS,之后再也不会重新加载 TSS。在进程切换时,只需要把 TSS 中的 sso 及 espO 更新为新任务的内核战的段地址及槐指针。
Linux 在 TSS 中只初始化了 sso 、 espO 和 1/0 位图宇段,除此之外 TSS 便没用了,就是个空架子,不再做保存任务状态之用。
那任务的状态信息保存在哪里呢?
是这样的,当 CPU 由低特权级进入高特权级时, CPU 会“自动”从 TSS 中获取对应高特权级的战指针( TSS 是 CPU 内部框架原生支持的嘛,当然是自动从中获取新的战指针) 。 我们具体说一下, Linux 只用到了特权 3 级和特权 0 级,因此 CPU 从 3 特权级的用户态进入 0 特权级的内核态时(比如从用户进程进入中断), CPU 自动从当前任务的 TSS 中获取 sso 和 espO 字段的值作为 0 特权级的枝,然后 Linux “手动”执行一系列的 p山h 指令将任务的状态的保存在 0 特权级棋中,也就是 TSS 中 sso 和 e甲。所指向的战。
要知道,人家 Intel 当初是打算让 TR 寄存器指向不同任务的 TSS 以实现任务切换的, Linux 这里只换了 TSS 中的部分内容,而 TR 本身没换,还是指向同一个 TSS ,这种“自欺欺人”的好处是任务切换的开销更小了,因为和修改 TSS 中的内容所带来的开销相比,在 TR 中加载 TSS 的开销要大得多 。 您想,每次切换任务都要用!tr 指令重新加载新任务的 TSS 到寄存器 TR, TSS 是位于内存中的,而内存很慢的,随着任务数量一多,这种频繁重复加载的开销就更为“可观”。
定义并初始化TSS
#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 // 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 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;
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;
};
//-------------- 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位的门,不用,定义它只为和32位门区分
#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
#include "tss.h"
#include "stdint.h"
#include "global.h"
#include "string.h"
#include "print.h"
/* 任务状态段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;
};
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);
}
/* 创建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;
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");
}
实现用户进程
实现用户进程的原理
用户进程的虚拟地址空间
/* 进程或线程的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; // 用这串数字做栈的边界标记,用于检测栈的溢出
};
为进程创建页表和3特权级栈
进程与线程的区别是进程拥有独立的地址空间,不同的地址空间就是不同的页表,因此我们在创建进程的过程中需要为每个进程单独创建一个页表。我们这里所说的页表是“页目录表+页表飞页目录表用来存放页目录项 PDE,每个 PDE 又指向不同的页表。
#include "memory.h"
#include "bitmap.h"
#include "stdint.h"
#include "global.h"
#include "debug.h"
#include "print.h"
#include "string.h"
#include "sync.h"
/*************** 位图地址 ********************
* 因为0xc009f000是内核主线程栈顶,0xc009e000是内核主线程的pcb.
* 一个页框大小的位图可表示128M内存, 位图位置安排在地址0xc009a000,
* 这样本系统最大支持4个页框的位图,即512M */
#define MEM_BITMAP_BASE 0xc009a000
/*************************************/
#define PDE_IDX(addr) ((addr & 0xffc00000) >> 22)
#define PTE_IDX(addr) ((addr & 0x003ff000) >> 12)
/* 0xc0000000是内核从虚拟地址3G起. 0x100000意指跨过低端1M内存,使虚拟地址在逻辑上连续 */
#define K_HEAP_START 0xc0100000
/* 内存池结构,生成两个实例用于管理内核内存池和用户内存池 */
struct pool {
struct bitmap pool_bitmap; // 本内存池用到的位图结构,用于管理物理内存
uint32_t phy_addr_start; // 本内存池所管理物理内存的起始地址
uint32_t pool_size; // 本内存池字节容量
struct lock lock; // 申请内存时互斥
};
struct pool kernel_pool, user_pool; // 生成内核内存池和用户内存池
struct virtual_addr kernel_vaddr; // 此结构是用来给内核分配虚拟地址
/* 在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对应的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;
}
/* 得到虚拟地址vaddr对应的pde的指针 */
uint32_t* pde_ptr(uint32_t vaddr) {
/* 0xfffff是用来访问到页表本身所在的地址 */
uint32_t* pde = (uint32_t*)((0xfffff000) + PDE_IDX(vaddr) * 4);
return pde;
}
/* 在m_pool指向的物理内存池中分配1个物理页,
* 成功则返回页框的物理地址,失败则返回NULL */
static void* palloc(struct pool* m_pool) {
/* 扫描或设置位图要保证原子操作 */
int bit_idx = bitmap_scan(&m_pool->pool_bitmap, 1); // 找一个物理页面
if (bit_idx == -1 ) {
return NULL;
}
bitmap_set(&m_pool->pool_bitmap, bit_idx, 1); // 将此位bit_idx置1
uint32_t page_phyaddr = ((bit_idx * PG_SIZE) + m_pool->phy_addr_start);
return (void*)page_phyaddr;
}
/* 页表中添加虚拟地址_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未创建时,
* *pte只能出现在下面最外层else语句块中的*pde后面。
* *********************************************************/
/* 先在页目录内判断目录项的P位,若为1,则表示该表已存在 */
if (*pde & 0x00000001) {
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会起作用
PANIC("pte repeat");
}
} else { // 页目录项不存在,所以要先创建页目录项再创建页表项.
/* 页表中用到的页框一律从内核空间分配 */
uint32_t pde_phyaddr = (uint32_t)palloc(&kernel_pool);
*pde = (pde_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
/******************* 必须将页表所在的页清0 *********************
* 必须把分配到的物理页地址pde_phyaddr对应的物理内存清0,
* 避免里面的陈旧数据变成了页表中的页表项,从而让页表混乱.
* pte的高20位会映射到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
}
}
/* 分配pg_cnt个页空间,成功则返回起始虚拟地址,失败时返回NULL */
void* malloc_page(enum pool_flags pf, uint32_t pg_cnt) {
ASSERT(pg_cnt > 0 && pg_cnt < 3840);
/*********** malloc_page的原理是三个动作的合成: ***********
1通过vaddr_get在虚拟内存池中申请虚拟地址
2通过palloc在物理内存池中申请物理页
3通过page_table_add将以上两步得到的虚拟地址和物理地址在页表中完成映射
***************************************************************/
void* vaddr_start = vaddr_get(pf, pg_cnt);
if (vaddr_start == NULL) {
return NULL;
}
uint32_t vaddr = (uint32_t)vaddr_start, cnt = pg_cnt;
struct pool* mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;
/* 因为虚拟地址是连续的,但物理地址可以是不连续的,所以逐个做映射*/
while (cnt-- > 0) {
void* page_phyaddr = palloc(mem_pool);
/* 失败时要将曾经已申请的虚拟地址和物理页全部回滚,
* 在将来完成内存回收时再补充 */
if (page_phyaddr == NULL) {
return NULL;
}
page_table_add((void*)vaddr, page_phyaddr); // 在页表中做映射
vaddr += PG_SIZE; // 下一个虚拟页
}
return vaddr_start;
}
/* 从内核物理内存池中申请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;
}
/* 将地址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为低端1M内存
uint32_t free_mem = all_mem - used_mem;
uint16_t all_free_pages = free_mem / PG_SIZE; // 1页为4k,不管总内存是不是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;
/********* 内核内存池和用户内存池位图 ***********
* 位图是全局的数据,长度不固定。
* 全局或静态的数组需要在编译时知道其长度,
* 而我们需要根据总内存大小算出需要多少字节。
* 所以改为指定一块内存来生成位图.
* ************************************************/
// 内核使用的最高地址是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");
}
/* 内存管理部分初始化入口 */
void mem_init() {
put_str("mem_init start\n");
uint32_t mem_bytes_total = (*(uint32_t*)(0xb00));
mem_pool_init(mem_bytes_total); // 初始化内存池
put_str("mem_init done\n");
}
进入特权级3
关键点 1 :从中断返回,必须要经过 intr exit,即使是“假装”
关键点 2:必须提前准备好用户进程所用的战结构,在里面填装好用户进程的上下文信息,借一系列 pop 出械的机会,将用户进程的上下文信息载入臼U 的寄存器,为用户进程的运行准备好环境。
关键点 3 :我们要在枝中存储的 cs 选择子,其 RPL必须为 3 。
关键点 4,战中段寄存器的选择子必须指向 DPL 为 3 的内存段。
关键点 5:必须使战中 eflags 的 IF 位为 1.
关键点 6:必须使战中 eflags 的 IOPL 位为 0。
用户进程创建的流程
实现用户进程
#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
#define PG_SIZE 4096
#include "process.h"
#include "global.h"
#include "debug.h"
#include "memory.h"
#include "thread.h"
#include "list.h"
#include "tss.h"
#include "interrupt.h"
#include "string.h"
#include "console.h"
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);
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");
}
/* 击活页表 */
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;
}
/************************** 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;
/*****************************************************************************/
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_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);
}
用户程序内存空间的最顶端用来存储命令行参数及环境变量,这些内容是由某操作系统下的 C 运行库写进去的,将来实现从文件系统加载用户进程并为其传递参数时会介绍这部分。紧接着是横空间和堆空间,横向下扩展,堆向上扩展,技与堆在空间上是相接的,这两个空间由操作系统管理分配,由于战与堆是相向扩展的,操作系统需要检测战与堆的碰撞。最下面的未初始化数据段 bss、初始化数据段 data 及代码段 text由链接器和编译器负责。
bss简介
在 C 程序的内存空间中,位于低处的三个段是代码段、数据段和 bss 段,它们由编译器和链接器规划地址空间,在程序被操作系统加载之前它们地址就固定了。而堆是位于 bss 段的上面,战是位于堆的上面,它们共享 4GB 空间中除了代码段、数据段及顶端命令行参数和环境变量等以外的其余可用空间,它们的地址由操作系统来管理,在程序加载时为用户进程分配战空间,运行过程中为进程从堆中分配内存 。 堆向上扩展,战向下扩展,因此在程序的加载之初,操作系统必须为堆和战分别指定起始地址。
在 Linux 中,堆的起始地址是固定的,这是由 structmm s阳.ct 结构中的 start brk 来指定的,堆的结束地址并不固定,这取决于堆中内存的分配情况,堆的上边界是由同结构中的 brk 来标记的。
照属性来划分节,大致上有三种类型。
( 1)可读写的数据,如数据节.data 和未初始化节.bss 。
(2 )只读可执行的代码,如代码节 .text 和初始化代码节 .init 。
(3 )只读数据,如只读数据节 .rodata,一般情况下字符串就存储在此节。
未运行之前或运行之初,程序中 bss 中的内容都是未初始化的数据,它们也是变量,只不过这些变量的值在最初时是多少都无所谓,它们的意义是在运行过程中才产生的,- 故程序文件中无需存在 bss 实体,因此不占用文件大小。在程序运行后那些位于 bss 中的未初始化数据便被赋予了有意义的值,那时 bss 开始变得有意义,故 bss 仅存在于内存中。您看,既然 bss 中的数据也是变量,就肯定要占用内存空间,需要把空间预留出来,但它们并不在文件中存在,对于这种只占内存又不占文件系统空间的数据,链接器采取了合理的做法:由于 bss 中的内容是变量,其属性为可读写,这和数据段属性一致,故链接器将 bss 占用的内存空间大小合并到数据段占用的内存中,这样便在数据段中预留出 bss 的空间以供程序在将来运行时使用。注意,这里所说的是 bss 的尺寸会被合并到数据段,并不是 bss 中的实际内容也会被合并到数据段中,毕竟起初 bss 中的内容无意义,将它的内容合并到其他段中真的是“毫无意义气当程序文件被操作系统加载器加载时,加载器会为程序的各个段分配内存,由于 bss 己被归并到数据段中,故 bss仅存在于数据段所在的内存中 。因此 , bss 的作用就是为程序运行过程中使用的未初始化数据变量提前预留了内存空间。程序的 bss 段(数据段的一部分)会由该加载器填充为 0。由此可见,为生成在某操作系统下运行的用户程序,编译器和操作系统需要相互配合。
堆的起始地址应该在
bss 之上,现在我们知道 bss 已经被归并到数据段了,数据段的类型是可加载的 LOAD 型,程序将来加载运行时,操作系统的程序加载器会为该程序的数据段分配内存,也就是 bss 段的内存区域也会顺便被分配,因此我们不需要单独知道 bss 的结束地址,只要知道数据段的起始地址及大小,便可以确定堆的起始地址了。有关程序加载的内容,将来我们实现了从文件系统中加载运行用户程序时大伙儿就清楚了,目前我们只要了解虽然堆的起始地址应该在 bss 之上,但由于 bss 己融合到数据段中,要实现用户进程的堆,已不需要知道 bss 的结束地址,将来咱们加载程序时会获取程序段的起始地址及大小,因此只要堆的起始地址在用户进程地址最高的段之上就可以了。
什么是符号链接?
符号链接是 Linux 系统中的概念。文件都有名称,对于用户来说,文件是通过文件名来访问的,可以把文件名理解为存储在磁盘上的文件实体的访问入口。符号链接是为同一个文件实体多创建了一个访问入口,相当于为原文件起个别名,就像人有大名和小名,都是指同一个人。
让进程跑起来——用户进程的调度
/* 实现任务调度 */
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);
}