《操作系统真象还原》第十一章——用户进程
文章目录
- 前言
- 为什么要有TSS
- TSS简介
- TSS描述符和TSS结构
- 现代操作系统采用的任务切换方式
- 定义并初始化TSS
- 修改global.h
- 编写tss.c
- 测试
- 实现用户进程
- 实现用户进程的原理
- 维护虚拟地址空间,修改thread.h
- 为进程创建页表和3特权级栈,修改memory.c
- 进入特权级3
- 用户进程创建的流程
- 实现用户进程——上
- 修改global.h
- 编写process.c
- bss简介
- 实现用户进程——下
- 完整的process.c
- 对应的process.h
- 用户进程的调度
- 修改schedule函数
- 测试用户进程
- main.c
- makefile
- 结果截图
- 结语
前言
上章博客链接:
本章研究用户进程,内容分为三节:为什么要有tss,定义并初始化tss,实现用户进程。实现用户进程下设10个小节。第一部分中,ldt相关知识和cpu原生多任务,我们先略过。
为什么要有TSS
TSS简介
TSS是Task State Segment的缩写,即任务状态段。
原文:CPU执行任务时,需要把任务运行所需要的数据加载到寄存器、栈和内存中。于是问题来了,任务的数据和指令是CPU的处理对象,任务的执行要占用一套存储资源,如寄存器和内存,这些存储资源中装的是任务的数据和指令,它们属于 CPU的大餐,但CPU很不情愿直接用内存这个低速的、不方便的容器就餐,它最喜欢的容器是寄存器, 因为它的速度和CPU很般配,这才能让CPU吃得更爽。因此内存中的数据往往被加载到高速的寄存器后 再处理,处理完成后,再将结果回写到低速的内存中,所以,任何时候,寄存器中的内容才是任务的最新 状态。采取轮流使用CPU的方式运行多任务,当前任务在被换下CPU时,任务的最新状态,也就是寄存 器中的内容应该找个地方保存起来,以便下次重新将此任务调度到CPU上时可以恢复此任务的最新状态, 这样任务才能继续执行,否则就出错了。
人们给每个任务都关联了一个任务状态段。当加载新任务时,CPU 自动把当前任务 (旧任务)的状态存入当前任务的TSS,然后将新任务TSS中的数据载入到对应的寄存器中,这就实现了任务 切换。TSS就是任务的代表,CPU用不同的TSS区分不同的任务,因此任务切换的本质就是TSS的换来换去。
以上是理想情况,和实际不同。
TSS描述符和TSS结构
tss本质是内存上的一段区域,同样有tss的段描述符,称为tss描述符,同样保存在gdt里,结构如下。
重点解释以下type段b位。B表示busy位,B位为0时,表示任务不繁忙,B位为1时,表示任务繁忙。
任务繁忙有两方面的含义,一方面就是指此任务是否为当前正在CPU上运行的任务。另一方面是指 此任务嵌套调用了新的任务,CPU正在执行新任务,此任务暂时挂起,等新任务执行完成后CPU会回到 此任务继续执行,所以此任务马上就会被调度执行了。
B位存在的意义可不是单纯为了表示任务忙不忙,而是为了给当前任务打个标记,目的是避免当前任 务调用自己,也就是说任务是不可重入的。
以下是tss的结构截图
主要再说明一下三个特权级切换栈吧。三组栈用来保存特权级切换时目标栈的栈指针。首先,除非从中断返回,cpu不允许直接从高特权级转移到低特权级。所以我们不会从比3更低的特权级进入3,因而没有ss3和esp3。
我们的操作系统效仿linux,只用到0和3两个特权级,只要设置ss0和esp0即可。
有专门的tss寄存器tr,用来保存tss的起始地址和偏移大小。
将tss加载到寄存器TR的 指令是ltr,其指令格式为:ltr “16位通用寄存器”或“16位内存单元”
。寄存器或内存单元必须是gdt的选择子
现代操作系统采用的任务切换方式
现代操作系统为什么不用多个TSS实现任务切换?
简而言之,效率低,便携差,不够灵活。
结论:我们使用TSS唯一的理由是为0特权级的任务提供栈。
Linux为每个CPU创建一个TSS,在各个CPU上的所有任务共享同一 个TSS,各 CPU的TR寄存器保存各CPU上的TSS,在用 ltr指令加载TSS后,该TR寄存器永远指向同 一个TSS,之后再也不会重新加载TSS。
Linux 在 TSS中只初始化了SS0、esp0和I/O位图字段,除此之外TSS便没用了,就是个空架子,不 再做保存任务状态之用。 那任务的状态信息保存在哪里呢? 是这样的,当CPU由低特权级进入高特权级时,CPU会“自动”从TSS中获取对应高特权级的栈指 针(TSS是CPU内部框架原生支持的嘛,当然是自动从中获取新的栈指针)。我们具体说一下,Linux只 用到了特权3级和特权0级,因此CPU从3特权级的用户态进入0特权级的内核态时(比如从用户进程 进入中断),CPU自动从当前任务的TSS中获取SS0和esp0字段的值作为0特权级的栈,然后Linux“手 动”执行一系列的push指令将任务的状态的保存在0特权级栈中,也就是TSS中SS0和esp0所指向的栈。
总结:任务信息一样存在栈里。
定义并初始化TSS
修改global.h
在kenel目录下。
// 这个文件定义了段选择子的属性和IDT描述符的属性
#ifndef __KERNEL_GLOBAL_H
#define __KERNEL_GLOBAL_H
#include "../lib/kernel/stdint.h"
/*----------GDT段描述符属性----------*/
#define DESC_G_4K 1 // 粒度为4k
#define DESC_D_32 1 // 操作数为32位
#define DESC_L 0 // 64位模式
#define DESC_AVL 0 // 软件位,置0
#define DESC_P 1 // 段是否有效
#define DESC_DPL_0 0 // 4个特权级
#define DESC_DPL_1 1
#define DESC_DPL_2 2
#define DESC_DPL_3 3
/*S说明:代码段和数据段是存储段,tss和门描述符属于系统段*S为1时表示存储段,S为0时表示系统段*/
#define DESC_S_CODE 1
#define DESC_S_DATA 1
#define DESC_S_SYS 0#define DESC_TYPE_CODE 8 // 1000
#define DESC_TYPE_DATA 2 // 0010
#define DESC_TYPE_TSS 9 // 1001/*----------段选择子属性----------*/
#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) // 显存段// GDT四号位置是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 // 用户栈段/*----------64位GDT描述符----------*/
// 把上面定义的GDT位属性连接起来,构建64位的GDT描述符
#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描述符属性----------*/
// 专门定义一下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)/*----------定义GDT描述符的结构----------*/
struct gdt_desc
{uint16_t limit_low_word; // 低16位段界限uint16_t base_low_word; // 低16位段基址uint8_t base_mid_byte; // 中8位段基址uint8_t attr_low_byte; // 8位段属性uint8_t limit_high_byte_and_attr_high_byte; // 4位段界限+4位段属性uint8_t base_hight_byte; // 高8位段基址
};/*----------IDT中断描述符表属性----------*/
#define IDT_DESC_P 1
#define IDT_DESC_DPL0 0
#define IDT_DESC_DPL1 1
#define IDT_DESC_DPL2 2
#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 EFLAGS_MBS (1 << 1) // 此项必须要设置
#define EFLAGS_IF_1 (1 << 9) // if位1,屏蔽中断信号,关中断
#define EFLAGS_IF_0 0 // 开中断
#define EFLAGS_IOPL_3 (3 << 12) // 测试用,允许用户进程跳过系统调用直接操作硬件
#define EFLAGS_IOPL_0 (0 << 12) // 默认状态
#define DIV_ROUND_UP(X, STEP) ((X + STEP - 1) / STEP) // 实现除法后向上取整
#define PG_SIZE 4096
#endif
编写tss.c
我们创建一个新的文件夹userprog,用户进程相关文件都放在这里。
#include "./tss.h"
#include "../lib/kernel/stdint.h"
#include "../kernel/global.h"
#include "../lib/kernel/print.h"
#include "../lib/string.h"#define PG_SIZE 4096 // 标准页大小
#define GDT_BASE_ADDR 0xc0000903 // gdt基地址,可以用info gdt查看
/*TSS结构,由程序员定义,CPU维护*/
struct tss
{uint32_t backlink; // 上一个TSS的指针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; // ldtuint32_t trace;uint32_t io_base; // io位图
};
static struct tss tss;/*更新tss中esp0字段的值为 pthread的0级栈*/
void upedate_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_byte_and_attr_high_byte =(((limit & 0x00ff0000) >> 16) + (uint8_t)attr_high);desc.base_hight_byte = ((desc_base & 0xff000000) >> 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; // 0特权级栈就是内核栈,将选择子赋给ss0字段tss.io_base = tss_size;// 注册TSS*((struct gdt_desc *)(GDT_BASE_ADDR + 0x20)) =make_gdt_desc((uint32_t *)&tss, tss_size - 1, TSS_ATTR_LOW, TSS_ATTR_HIGH);// 注册用户级代码段和数据段*((struct gdt_desc *)(GDT_BASE_ADDR + 0x28)) =make_gdt_desc((uint32_t *)0, 0xfffff, GDT_CODE_ATTR_LOW_DPL3, GDT_ATTR_HIGH);*((struct gdt_desc *)(GDT_BASE_ADDR + 0x30)) =make_gdt_desc((uint32_t *)0, 0xfffff, GDT_DATA_ATTR_LOW_DPL3, GDT_ATTR_HIGH);/*构建GDT的操作数,用于LGDT指令,传递信息到GDTR寄存器*结构:|16位GDT限长 (Limit)|32位 GDT基地址(Base Address)|16位保留(通常为0)|*Limit类似数组下标,需要-1*/uint64_t gdt_operand =((8 * 7 - 1) | ((uint64_t)(uint32_t)GDT_BASE_ADDR << 16));// 将GDT信息和TSS信息用lgdt和ltr指令分别写入GDTR和TR寄存器asm volatile("lgdt %0" : : "m"(gdt_operand));asm volatile("ltr %w0" : : "r"(SELECTOR_TSS));put_str("tss_init done!\n");
}
对应的tss.h,比较短。
#ifndef __USERPROG_TSS_H
#define __USERPROG_TSS_H
void upedate_tss_esp(struct task_struct *pthread);
void tss_init();
#endif
编码部分基本完成。
测试
首先是修改init
// 完成所有的初始化工作
#include "./init.h"
#include "../lib/kernel/stdint.h"
#include "../lib/kernel/print.h"
#include "./interrupt.h"
#include "../device/timer.h"
#include "./memory.h"
#include "../thread/thread.h"
#include "../device/console.h"
#include "../device/keyboard.h"
#include "../userprog/tss.h"/*负责初始化所有模块 */
void init_all()
{put_str("init_all\n");idt_init(); // 中断初始化mem_init(); // 内存初始化timer_init(); // 定时器初始化thread_init(); // 线程初始化console_init(); // 控制台初始化,最好放到开中断之前keyboard_init(); // 键盘初始化tss_init(); // TSS和GDT初始化
}
main.c需要把中断关闭,也不需要多线程。
makefile:
BUILD_DIR = ./build
ENTRY_POINT = 0xc0001500
AS = nasm
CC = gcc
LD = ld
LIB = -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/ -I thread/ -I userprog
ASFLAGS = -f elf
CFLAGS = -Wall -m32 -fno-stack-protector $(LIB) -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes
LDFLAGS = -m elf_i386 -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map
OBJS = $(BUILD_DIR)/main.o $(BUILD_DIR)/init.o $(BUILD_DIR)/interrupt.o \$(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o \$(BUILD_DIR)/debug.o $(BUILD_DIR)/string.o $(BUILD_DIR)/memory.o \$(BUILD_DIR)/bitmap.o $(BUILD_DIR)/thread.o $(BUILD_DIR)/list.o \$(BUILD_DIR)/switch.o $(BUILD_DIR)/sync.o $(BUILD_DIR)/console.o \$(BUILD_DIR)/keyboard.o $(BUILD_DIR)/ioqueue.o $(BUILD_DIR)/tss.o \################ c代码编译 ##################
$(BUILD_DIR)/main.o: kernel/main.c lib/kernel/print.h \lib/kernel/stdint.h kernel/init.h kernel/debug.h \kernel/memory.h thread/thread.h kernel/interrupt.h \device/console.h device/ioqueue.h device/keyboard.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/init.o: kernel/init.c kernel/init.h lib/kernel/print.h \lib/kernel/stdint.h kernel/interrupt.h device/timer.h \kernel/memory.h thread/thread.h device/console.h \device/keyboard.h userprog/tss.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/interrupt.o: kernel/interrupt.c kernel/interrupt.h \lib/kernel/stdint.h kernel/global.h kernel/io.h \lib/kernel/print.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/timer.o: device/timer.c device/timer.h lib/kernel/stdint.h \kernel/io.h lib/kernel/print.h kernel/interrupt.h \thread/thread.h kernel/debug.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/debug.o: kernel/debug.c kernel/debug.h \lib/kernel/print.h kernel/interrupt.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/string.o: lib/string.c lib/string.h \kernel/debug.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/memory.o: kernel/memory.c kernel/memory.h \lib/kernel/stdint.h lib/kernel/bitmap.h kernel/debug.h \lib/string.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/bitmap.o: lib/kernel/bitmap.c lib/kernel/bitmap.h \lib/string.h kernel/interrupt.h lib/kernel/print.h \kernel/debug.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/thread.o: thread/thread.c thread/thread.h \lib/kernel/stdint.h lib/kernel/list.h lib/string.h \kernel/memory.h kernel/interrupt.h kernel/debug.h \lib/kernel/print.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/list.o: lib/kernel/list.c lib/kernel/list.h \lib/kernel/stdint.h kernel/interrupt.h kernel/debug.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/sync.o: thread/sync.c thread/sync.h \lib/kernel/stdint.h thread/thread.h kernel/debug.h \kernel/interrupt.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/console.o: device/console.c device/console.h \lib/kernel/print.h thread/sync.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/keyboard.o: device/keyboard.c device/keyboard.h \lib/kernel/print.h kernel/interrupt.h kernel/io.h \lib/kernel/stdint.h device/ioqueue.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/ioqueue.o: device/ioqueue.c device/ioqueue.h \lib/kernel/stdint.h thread/thread.h thread/sync.h \kernel/interrupt.h kernel/debug.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/tss.o: userprog/tss.c userprog/tss.h \lib/kernel/stdint.h thread/thread.h kernel/global.h \lib/kernel/print.h lib/string.h$(CC) $(CFLAGS) $< -o $@############## 汇编代码编译 ###############
$(BUILD_DIR)/kernel.o: kernel/kernel.S$(AS) $(ASFLAGS) $< -o $@$(BUILD_DIR)/print.o: lib/kernel/print.S$(AS) $(ASFLAGS) $< -o $@$(BUILD_DIR)/switch.o: thread/switch.S$(AS) $(ASFLAGS) $< -o $@############## 连接所有目标文件 #############
$(BUILD_DIR)/kernel.bin: $(OBJS)$(LD) $(LDFLAGS) $^ -o $@.PHONY : mk_dir hd clean allmk_dir:if [ ! -d $(BUILD_DIR) ]; then mkdir $(BUILD_DIR); fihd:dd if=$(BUILD_DIR)/kernel.bin \of=/home/hongbai/bochs/bin/c.img \bs=512 count=200 seek=10 conv=notruncclean:cd $(BUILD_DIR) && rm -f ./*build: $(BUILD_DIR)/kernel.binall: mk_dir build hd
调试了一会,解决了一些代码上的错误,博客里的代码已经修改
正确运行截图
用info gdt查看gdt
可以看到我们的GDT表多了三个项,包括TSS项。
实现用户进程
这一节很长很长,书中下面分了十个小节,我的博客可能会把一些小节合并,做好准备吧。
实现用户进程的原理
我们打算在线程的基础上实现进程。先回顾我们的线程创建流程:

我们只需要把function替换成创建进程的函数,就完成了最主要的工作。
维护虚拟地址空间,修改thread.h
我们知道,进程是享有内存空间的,当然是虚拟的4GB内存,我们给每个进程维护一个虚拟地址池,用来记录这个进程的虚拟中,哪些地址已经被分配,哪些还能继续用。
只加入了几个头文件,task_struct结构体多了一个成员。
struct task_struct {uint32_t *self_kstack; // 线程自己的内核栈栈顶指针enum thread_status status; // 线程的状态uint8_t priority; // 线程的优先级uint8_t ticks; // 线程的时间片,在处理器上运行的时间滴答数uint32_t elapsed_ticks; // 线程的运行时间,也就是这个线程已经执行了多久char name[16]; // 线程的名字struct list_elem general_tag; // 用于线程在一般队列中的节点struct list_elem all_list_tag; // 用于线程在thread_all_list队列中的节点uint32_t *pgdir; // 如果是进程,这是进程的页表的虚拟地址,线程则置为NULLstruct virtual_addr userprog_v_addr; //用户进程的虚拟地址,后续转化为物理地址后存入cr3寄存器uint32_t stack_magic; // 线程栈的魔数,边界标记,用来检测栈溢出
};
回顾cr3寄存器:我们在启动分页时,需要把页目录基址物理地址存入cpu的cr3寄存器,在寻址时需要用到。
为进程创建页表和3特权级栈,修改memory.c
我们在创建进程时,需要给每一个进程单独创建一套页目录表-页表结构(二级页表)。另外我们之前的创建线程过程运行在内核态0特权级,但是我们的用户进程要运行在3特权级,所以需要创建3特权级栈。
这两部分工作完成在memory,修改memory.c。
// 实现memery.h中的函数
#include "./memory.h"
#include "./debug.h"
#include "../lib/kernel/print.h"
#include "../lib/string.h"
#include "../thread/thread.h"
#include "../thread/sync.h"#define PAGE_SIZE 4096 // 定义页面大小为4KB
// 关于位图地址,安排在0xc009a000,我的整个系统支持4个4kb位图,共管理512MB的内存
#define MEM_BITMAP_BASE 0xc009a000 // 内核内存池位图基地址
#define K_HEAP_START 0xc0100000 // 内核堆起始地址#define PDE_INDEX(addr) ((addr & 0xffc00000) >> 22) // 获取页目录项索引
#define PTE_INDEX(addr) ((addr & 0x003ff000) >> 12) // 获取页表项索引struct pool
{struct bitmap pool_bitmap; // 内存池位图struct lock lock; // 创建用户进程会用到,让用户进程申请内存的行为互斥uint32_t phy_addr_start; // 物理内存池起始地址uint32_t pool_size; // 内存池大小
};
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 bit_idx_start = -1; // 位图索引uint32_t vaddr_start = 0; // 虚拟地址uint32_t cnt = 0; // 计数器if (pf == PF_KERNEL){ // 内核内存池// 扫描位图,找到空余的pg_cnt个位,对应相等的页bit_idx_start = bitmap_scan(&kernel_vaddr.vaddr_bitmap, pg_cnt);if (bit_idx_start == -1){ // 没有找到合适的位置return NULL;}// 找到后把位图中pg_cnt个位置设置为1,表示这些页已分配while (cnt < pg_cnt){bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt, 1); // 设置位图cnt++;}vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PAGE_SIZE; // 计算虚拟地址}else{ // 用户内存池/*整体和内核内存池申请过程一致,进行扫描-设置-更新三个过程*因为我们是通过线程建立用户进程,需要先获取目前线程状态*/struct task_struct *cur = running_thread();bit_idx_start = bitmap_scan(&cur->userprog_v_addr.vaddr_bitmap, pg_cnt);while (cnt < pg_cnt){bitmap_set(&cur->userprog_v_addr.vaddr_bitmap, bit_idx_start + cnt, 1);cnt++;}vaddr_start = cur->userprog_v_addr.vaddr_start + bit_idx_start * PAGE_SIZE;// 这是在用户内存,最大页起点就是分界线0xc0000000-一个页大小ASSERT((uint32_t)vaddr_start < 0xc0000000 - PAGE_SIZE);}return (void *)vaddr_start; // 返回虚拟地址
}/* 得到虚拟地址vaddr对应的pte的指针 */
uint32_t *pte_ptr(uint32_t vaddr)
{uint32_t *pte = (uint32_t *)(0xffc00000 + ((vaddr & 0xffc00000) >> 10) + PTE_INDEX(vaddr) * 4); // 计算页表项地址return pte; // 返回页表项地址
}/* 得到虚拟地址vaddr对应的pde的指针 */
uint32_t *pde_ptr(uint32_t vaddr)
{uint32_t *pde = (uint32_t *)(0xfffff000 + PDE_INDEX(vaddr) * 4); // 计算页目录项地址return pde; // 返回页目录项地址
}/* 在m_pool 指向的物理内存池中分配一个物理页,成功返回页物理地址,失败返回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); // 设置位图uint32_t page_phyaddr = m_pool->phy_addr_start + bit_idx * PAGE_SIZE; // 计算物理地址return (void *)page_phyaddr; // 返回物理地址
}/* 页表中添加虚拟地址_vaddr与物理地址_page_phyaddr的映射 */
static void page_table_add(void *_vaddr, void *_page_phyaddr)
{uint32_t vaddr = (uint32_t)_vaddr; // 虚拟地址uint32_t page_phyaddr = (uint32_t)_page_phyaddr; // 物理地址uint32_t *pde = pde_ptr(vaddr); // 获取页目录项指针uint32_t *pte = pte_ptr(vaddr); // 获取页表项指针if (*pde & 0x00000001){ // 页目录项存在ASSERT(!(*pte & 0x00000001));if (!(*pte & 0x00000001)){ // 页表项不存在*pte = page_phyaddr | PG_US_U | PG_RW_W | PG_P_1; // 设置页表项}else{ // 页表项存在PANIC("page_table_add: pte repeat");*pte = page_phyaddr | PG_US_U | PG_RW_W | PG_P_1; // 设置页表项}}else{ // 页目录项不存在uint32_t pde_phyaddr = (uint32_t)palloc(&kernel_pool); // 分配一个物理页作为页表*pde = pde_phyaddr | PG_US_U | PG_RW_W | PG_P_1; // 设置页目录项memset((void *)((uint32_t)pte & 0xfffff000), 0, PAGE_SIZE); // 清空页表ASSERT(!(*pte & 0x00000001));*pte = page_phyaddr | PG_US_U | PG_RW_W | PG_P_1; // 设置页表项}
}/* 分配pg_cnt个页空间,成功则返回起始虚拟地址,失败时返回NULL */
/* 上面三个函数的合成 */
void *malloc_page(enum pool_flags pf, uint32_t pg_cnt)
{ASSERT(pg_cnt > 0 && pg_cnt < 3840); // 检查页数void *vaddr_start = vaddr_get(pf, pg_cnt); // 获取虚拟地址if (vaddr_start == NULL){ // 没有找到合适的地址return NULL;}uint32_t vaddr = (uint32_t)vaddr_start; // 虚拟地址起始位置uint32_t 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 += PAGE_SIZE; // 移动到下一个页}return vaddr_start; // 返回虚拟地址
}/* 从内核物理内存池中申请pg_cnt页内存,成功则返回其虚拟地址,失败则返回NULL */
void *get_kernel_pages(uint32_t pg_cnt)
{void *vaddr = malloc_page(PF_KERNEL, pg_cnt); // 申请内存if (vaddr != NULL) // 申请成功{memset(vaddr, 0, pg_cnt * PAGE_SIZE); // 清空内存}return vaddr; // 返回虚拟地址
}/* 从用户内存池申请pg_cnt页内存 */
void *get_user_page(uint32_t pg_cnt)
{lock_acquire(&user_pool.lock); // 保证互斥void *vaddr = malloc_page(PF_USER, pg_cnt);if (vaddr != NULL){memset(vaddr, 0, pg_cnt * PAGE_SIZE);}lock_release(&user_pool.lock);return vaddr;
}/* 将虚拟地址vaddr和内存池物理地址关联 */
/* 仅支持一页*/
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();int bit_idx = -1;if (cur->pgdir != NULL && pf == PF_USER){bit_idx = (vaddr - cur->userprog_v_addr.vaddr_start) / PAGE_SIZE;ASSERT(bit_idx > 0);bitmap_set(&cur->userprog_v_addr.vaddr_bitmap, bit_idx, 1);}else if (cur->pgdir == NULL && pf == PF_KERNEL){bit_idx = (vaddr - kernel_vaddr.vaddr_start) / PAGE_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 addt_v_to_p(uint32_t vaddr)
{uint32_t *pte = pte_ptr(vaddr);return ((*pte & 0xfffff000) + (vaddr & 0x00000fff));
}/* 初始化内存池 */
void mem_pool_init(uint32_t all_mem)
{put_str(" mem_pool_init start\n");uint32_t page_table_size = 256 * PAGE_SIZE; // 计算页表使用的内存的大小uint32_t used_mem = page_table_size + 0x100000; // 计算已用内存uint32_t free_mem = all_mem - used_mem; // 计算总可用内存uint16_t all_free_pages = free_mem / PAGE_SIZE; // 计算总可用页数uint16_t kernel_free_pages = all_free_pages / 2; // 内核可用页数uint16_t user_free_pages = all_free_pages - kernel_free_pages; // 用户可用页数uint32_t kbm_len = kernel_free_pages / 8; // 内核位图长度uint32_t ubm_len = user_free_pages / 8; // 用户位图长度uint32_t kp_start = used_mem; // 内核内存池起始地址uint32_t up_start = kp_start + kernel_free_pages * PAGE_SIZE; // 用户内存池起始地址kernel_pool.phy_addr_start = kp_start; // 设置内核内存池起始地址user_pool.phy_addr_start = up_start; // 设置用户内存池起始地址kernel_pool.pool_size = kernel_free_pages * PAGE_SIZE; // 设置内核内存池大小user_pool.pool_size = user_free_pages * PAGE_SIZE; // 设置用户内存池大小kernel_pool.pool_bitmap.btmp_bytes_len = kbm_len; // 设置内核位图长度user_pool.pool_bitmap.btmp_bytes_len = ubm_len; // 设置用户位图长度kernel_pool.pool_bitmap.btmp_bits = (void *)MEM_BITMAP_BASE; // 设置内核位图地址user_pool.pool_bitmap.btmp_bits = (void *)(MEM_BITMAP_BASE + kbm_len); // 设置用户位图地址/* 输出内存池信息 */put_str(" all_free_pages: ");put_int(all_free_pages);put_char('\n'); // 总内存512mb,使用了2mb,可用页 510mb/4kb=127000put_str(" kernel_free_pages: ");put_int(kernel_free_pages);put_char('\n');put_str(" user_free_pages: ");put_int(user_free_pages);put_char('\n');put_str(" kernel_pool_bitmap_start: ");put_int((int)kernel_pool.pool_bitmap.btmp_bits);put_char('\n');put_str(" kernel_pool_phy_addr_start: ");put_int(kernel_pool.phy_addr_start);put_char('\n');put_str(" user_pool_bitmap_start: ");put_int((int)user_pool.pool_bitmap.btmp_bits);put_char('\n');put_str(" user_pool_phy_addr_start: ");put_int(user_pool.phy_addr_start);put_char('\n');/* 将池内位图置0 */bitmap_init(&kernel_pool.pool_bitmap); // 初始化内核位图bitmap_init(&user_pool.pool_bitmap); // 初始化用户位图/* 初始化内核虚拟地址的位图 */kernel_vaddr.vaddr_bitmap.btmp_bytes_len = kbm_len; // 设置内核虚拟地址位图长度kernel_vaddr.vaddr_bitmap.btmp_bits =(void *)(MEM_BITMAP_BASE + kbm_len + ubm_len); // 设置内核虚拟地址位图地址kernel_vaddr.vaddr_start = K_HEAP_START; // 设置内核虚拟地址起始位置bitmap_init(&kernel_vaddr.vaddr_bitmap); // 初始化内核虚拟地址位图lock_init(&kernel_pool.lock);lock_init(&user_pool.lock);put_str(" mem_pool_init done\n");
}/* 内存管理初始化入口 */
void mem_init(void)
{put_str("mem_init start\n");uint32_t mem_bytes_total = (*(uint32_t *)(0xb00)); // 之前loader在开启分页时就获取了全部内存的大小,放到了0xb00中mem_pool_init(mem_bytes_total); // 初始化内存池put_str("mem_init done\n");
}
对应的,修改memory.h
// 声明了虚拟将地址池结构体
#ifndef __KERNEL_MEMORY_H
#define __KERNEL_MEMORY_H
#include "../lib/kernel/stdint.h"
#include "../lib/kernel/bitmap.h"/* 内存池标记 */
enum pool_flags
{PF_KERNEL = 1, // 内核内存池PF_USER = 2, // 用户内存池
};#define PG_P_1 1 // 页表项的存在
#define PG_P_0 0 // 页表项的不存在
#define PG_RW_R 0 // 可读可执行
#define PG_RW_W 2 // 可读可写可执行
#define PG_US_S 0 // 内核特权级
#define PG_US_U 4 // 用户特权级
// 虚拟地址结构体,内部有一个位图结构体,还有一个虚拟地址起始位置
struct virtual_addr
{struct bitmap vaddr_bitmap; // 虚拟地址位图uint32_t vaddr_start; // 虚拟地址起始位置
};extern struct pool kernel_pool; // 内核内存池
extern struct pool user_pool; // 用户内存池
uint32_t *pte_ptr(uint32_t vaddr); /* 得到虚拟地址vaddr对应的pte的指针 */
uint32_t *pde_ptr(uint32_t vaddr); /* 得到虚拟地址vaddr对应的pde的指针 */
void *malloc_page(enum pool_flags pf, uint32_t pg_cnt); /* 分配pg_cnt个页空间,成功则返回起始虚拟地址,失败时返回NULL */
void *get_kernel_pages(uint32_t pg_cnt); /* 从内核物理内存池中申请pg_cnt页内存 */
void *get_user_page(uint32_t pg_cnt);
void *get_a_page(enum pool_flags pf, uint32_t vaddr);
uint32_t addt_v_to_p(uint32_t vaddr);
void mem_pool_init(uint32_t all_mem); // 内存池初始化
void mem_init(void); // 内存管理初始化
#endif
关于这部分代码,由于有一些是之前写的,感觉有点绕,等这章结束后还需要反复理解。
进入特权级3
如何从0特权级进入3特权级?一般情况下肯定是不行,必须借助中断返回iret才能实现。我们模拟一个中断返回过程,实现进入低特权级。以下是关键点。
栈指的是intr_struct,定义在thread.h,初始包含中断号和各种寄存器
- 关键点1:从中断返回,必须要经过intr_exit,即使是“假装”
- 关键点2:必须提前准备好用户进程所用的栈结构,在里面填装好用户进程的上下文信息, 借一系列pop出栈的机会,将用户进程的上下文信息载入CPU的寄存器,为用户进程的运行准备好环境。
- 关键点3:我们要在栈中存储的CS选择子,其RPL 必须为3。即人为设定后续的用户进程请求访问内存时,永远是3特权级。
- 关键点4:栈中段寄存器的选择子必须指向DPL为3的内存段。即我们的目的地是3特权级的内存段。
- 关键点5:必须使栈中eflags的IF位为1。即可以继续响应中断。
- 关键点6:必须使栈中eflags的IOPL位为0。 即不允许用户进程访问硬件。
复习特权级:CPL是当前正在执行的代码段的特权级
DPL是段描述符、门描述符中的特权级
RPL是段选择子的低两位,表示请求访问时的权限,人为设定
允许访问的公式是:max(cpl,rpl)<=dpl
用户进程创建的流程
目前我们完成了内存管理的更新,算是给用户进程创建提供了工具,马上就要开始具体的实现细节了,郑钢老师特意放了两个流程图,便于我们从整体上把握用户进程创建的过程。
实现用户进程——上
修改global.h
新增了几个宏定义,关于bool之类的我都定义在stdint里了,就不再写了。
/*----------用户进程相关属性----------*/
#define EFLAGS_MBS (1 << 1) // 此项必须要设置
#define EFLAGS_IF_1 (1 << 9) // if位1,屏蔽中断信号,关中断
#define EFLAGS_IF_0 0 // 开中断
#define EFLAGS_IOPL_3 (3 << 12) // 测试用,允许用户进程跳过系统调用直接操作硬件
#define EFLAGS_IOPL_0 (0 << 12) // 默认状态
#define DIV_ROUND_UP(X, STEP) ((X + STEP - 1) / STEP) // 实现除法后向上取整
#define PG_SIZE 4096
编写process.c
位置在userprog目录下,目前是第一部分,总共有三个函数
#include "./process.h"
#include "../kernel/global.h"
#include "../lib/kernel/stdint.h"
#include "../lib/kernel/print.h"
#include "../thread/thread.h"
#include "./userprog.h"
#include "../kernel/debug.h"
#include "./tss.h"extern void intr_exit(void); // 相关实现在kernel.s里,是中断返回程序/*构建用户进程初始化上下文信息*/
void start_process(void *filename_)
{void *function = filename_; // 用户进程中,具体程序的入口struct task_struct *cur = running_thread(); // 进程栈// 关于以下两行代码,在《真象还原》的509、510页有非常清晰的解释cur->self_kstack += sizeof(struct thread_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;// 初始化ds,ds保存数据段基址,ss保存栈段基址,都指向用户数据段选择子proc_stack->ds = proc_stack->es = proc_stack->fs = SELECTOR_U_DATA;// 初始化cs:ip,cs保存代码段基址,指向用户代码段选择子proc_stack->eip = function;proc_stack->cs = SELECTOR_U_CODE;// 初始化elfgsproc_stack->eflags = (EFLAGS_IOPL_0 | EFLAGS_MBS | EFLAGS_IF_1);// 初始化ss:spproc_stack->esp = (void *)((uint32_t)get_a_page(PF_USER, USER_STACK3_VADDR) + PG_SIZE);proc_stack->ss = SELECTOR_U_DATA;// 通过内联汇编,欺骗cpu,让它进行一次中断返回,把proc_stack中的数据压入cpuasm volatile("movl %0,%%esp;jmp intr_exit" : : "g"(proc_stack) : "memory");
}/*激活页表,pthread可能是用户进程或者内核线程,*如果是内核线程,重新加载内核页表,否则加载进程的页表*/
void page_dir_activate(struct task_struct* pthread){// 默认值就是内核线程的页目录表基址uint32_t page_dir_phy_addr = 0x100000;if(pthread->pgdir != NULL){ // 说明pthread是一个用户进程,激活进程的二级页表结构page_dir_phy_addr = addt_v_to_p((uint32_t)pthread->pgdir);}// 通过内联汇编,更新cr3中页目录表的物理地址基址asm volatile("movl %0,%%cr3" : : "r"(page_dir_phy_addr) : "memory");
}/*激活线程或进程的页表,更新tss中的esp0为 0特权级的栈*/
void process_activate(struct task_struct *pthread){ASSERT(pthread != NULL);//激活页表page_dir_activate(pthread);//如果是内核线程,没有特权级转变,不需要更新pcb里的esp0//如果是用户进程,需要更新esp0if(pthread->pgdir!=NULL){update_tss_esp(pthread);}
}
proc_stack->esp = (void *)((uint32_t)get_a_page(PF_USER, USER_STACK3_VADDR) + PG_SIZE);
这一行代码涉及到一个宏定义,目录是userprog/userprog.h
#ifndef __USERPROG_USERPROG_H
#define __USERPROG_USERPROG_H
/*关于下面这个宏定义的取值,参考P511*我们模仿c程序的内存分布,用户进程虚拟地址从高到低分别是:*命令行参数和环境变量、栈、堆、bss、data、text*/
#define USER_STACK3_VADDR (0xc0000000 - 0x1000)#endif
我觉得难点是地址判断,栈顶指针的移动是和最早初始化task_struct有关的,要结合那块的代码去看,可以参考书上509、510页。
bss简介
这一小节插在用户进程编写中间。下图是linux下c程序的内存方案。
复制书上的原文解释一下:在C程序的内存空间中,位于低处的三个段是代码段、数据段和bss段,它们由编译器 和链接器规划地址空间,在程序被操作系统加载之前它们地址就固定了。而堆是位于bss段的上面,栈是 位于堆的上面,它们共享4GB空间中除了代码段、数据段及顶端命令行参数和环境变量等以外的其余可 用空间,它们的地址由操作系统来管理,在程序加载时为用户进程分配栈空间,运行过程中为进程从堆中 分配内存。堆向上扩展,栈向下扩展,因此在程序的加载之初,操作系统必须为堆和栈分别指定起始地址。
在Linux 中,堆的起始地址是固定的,这是由struct mm_struct 结构中的 start_brk 来指定的,堆的结 束地址并不固定,这取决于堆中内存的分配情况,堆的上边界是由同结构中的brk来标记的。
关于bss,bss 中的数据是未初始化的全局变量和局部静态变量,程序运行后才会为它们赋值,因此 在程序运行之初,里面的数据没意义,由操作系统的程序加载器将其置为0就可以了,虽然这些未初始化的全 局变量及局部静态变量起初是用不上的,但它们毕竟也是变量,即使是短暂的生存周期也要占用内存,必须 提前为它们在内存中“占好座”,bss区域的目的也正在于此,就是提前为这些未初始化数据预留内存空间。
bss数据最终会被归并到数据段,所以我们不需要单独知道bss的结束地址,只需要确保堆的起始地址在用户进程地址最高的段之上即可。
实现用户进程——下
这部分是三个函数,接着上面的process.c写,贴出完整的程序
完整的process.c
#include "./process.h"
#include "../kernel/global.h"
#include "../lib/kernel/stdint.h"
#include "../thread/thread.h"
#include "./userprog.h"
#include "../kernel/debug.h"
#include "./tss.h"
#include "../device/console.h"
#include "../lib/string.h"
#include "../kernel/interrupt.h"/*构建用户进程初始化上下文信息*/
void start_process(void *filename_)
{void *function = filename_; // 用户进程中,具体程序的入口struct task_struct *cur = running_thread(); // 进程栈// 关于以下两行代码,在《真象还原》的509、510页有非常清晰的解释cur->self_kstack += sizeof(struct thread_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;// 初始化ds,ds保存数据段基址,ss保存栈段基址,都指向用户数据段选择子proc_stack->ds = proc_stack->es = proc_stack->fs = SELECTOR_U_DATA;// 初始化cs:ip,cs保存代码段基址,指向用户代码段选择子proc_stack->eip = function;proc_stack->cs = SELECTOR_U_CODE;// 初始化elfgsproc_stack->eflags = (EFLAGS_IOPL_0 | EFLAGS_MBS | EFLAGS_IF_1);// 初始化ss:spproc_stack->esp = (void *)((uint32_t)get_a_page(PF_USER, USER_STACK3_VADDR) + PG_SIZE);proc_stack->ss = SELECTOR_U_DATA;// 通过内联汇编,欺骗cpu,让它进行一次中断返回,把proc_stack中的数据压入cpuasm volatile("movl %0,%%esp;jmp intr_exit" : : "g"(proc_stack) : "memory");
}/*激活页表,pthread可能是用户进程或者内核线程,*如果是内核线程,重新加载内核页表,否则加载进程的页表*/
void page_dir_activate(struct task_struct *pthread)
{// 默认值就是内核线程的页目录表基址uint32_t page_dir_phy_addr = 0x100000;if (pthread->pgdir != NULL){ // 说明pthread是一个用户进程,激活进程的二级页表结构page_dir_phy_addr = addt_v_to_p((uint32_t)pthread->pgdir);}// 通过内联汇编,更新cr3中页目录表的物理地址基址asm volatile("movl %0,%%cr3" : : "r"(page_dir_phy_addr) : "memory");
}/*激活线程或进程的页表,更新tss中的esp0为 0特权级的栈*/
void process_activate(struct task_struct *pthread)
{ASSERT(pthread != NULL);// 激活页表page_dir_activate(pthread);// 如果是内核线程,没有特权级转变,不需要更新pcb里的esp0// 如果是用户进程,需要更新esp0if (pthread->pgdir != NULL){update_tss_esp(pthread);}
}/*创建页目录表,成功返回页目录的虚拟地址,失败返回-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 error:get_kernel_page failed!\n");return NULL;}/*1.复制页表*/// 0x300 = 768,一个项4个字节 0xfffff000是最后一个分页,它同时映射页目录表本身,关键词:递归页表memcpy((uint32_t *)((uint32_t)page_dir_activate + 0x300 * 4), (uint32_t *)(0xfffff000 + 0x300 * 4), 1024);/*2.更新页目录基址*/uint32_t new_page_dir_phy_addr = addt_v_to_p((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_VADDR_START// 该值定义在 process.h 中,其值为 0x8048000,可以通过readelf查看user_prog->userprog_v_addr.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_v_addr.vaddr_bitmap.btmp_bits = get_kernel_pages(bitmap_pg_cnt);user_prog->userprog_v_addr.vaddr_bitmap.btmp_bytes_len = (0xc0000000 - USER_VADDR_START) / PG_SIZE / 8;bitmap_init(&user_prog->userprog_v_addr.vaddr_bitmap); // 通过初始化填充0,代表内存已经被使用
}/*通过线程创建用户进程*/
#define default_prio 31 // 临时定义
void process_execute(void *filename, char *name)
{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
#ifndef __USERPROG_PROCESS_H
#define __USERPROG_PROCESS_H
#define USER_VADDR_START 0x8048000
extern void intr_exit(void); // 相关实现在kernel.s里,是中断返回程序void start_process(void *filename_);
void page_dir_activate(struct task_struct *pthread);
void process_activate(struct task_struct *pthread);
uint32_t *create_page_dir(void);
void create_user_vaddr_bitmap(struct task_struct *user_prog);
void process_execute(void *filename, char *name);#endif
用户进程的调度
修改schedule函数
其实也就加了一行,我贴出完整的schedule函数好了
/* 实现任务调度 */
void schedule(void)
{ASSERT(intr_get_status() == INTR_OFF); // 确保中断关闭struct task_struct *cur = running_thread(); // 获取当前线程pcbif (cur->status == TASK_RUNNING){ // 如果当前线程是运行状态ASSERT(!elem_find(&thread_ready_list, &cur->general_tag)); // 确保当前线程不在就绪队列list_append(&thread_ready_list, &cur->general_tag); // 将当前线程加入就绪队列cur->status = TASK_READY; // 设置当前线程状态为就绪cur->ticks = cur->priority; // 重置时间片}else{}ASSERT(!list_empty(&thread_ready_list)); // 确保就绪队列不为空struct list_elem *thread_tag = list_pop(&thread_ready_list); // 从就绪队列中取出一个线程节点struct task_struct *next = (struct task_struct *)((uint32_t)thread_tag & 0xfffff000);next->status = TASK_RUNNING; // 设置下一个线程状态为运行process_activate(next); // 激活任务页表switch_to(cur, next); // 任务切换
}
测试用户进程
最后一个小节了,我们要修改main.c,编写makedown
main.c
// 内核的入口函数
#include "../lib/kernel/print.h"
#include "./init.h"
#include "../thread/thread.h"
#include "../device/console.h"
#include "./interrupt.h"
#include "../userprog/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("HongBai's OS 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){};
}void k_thread_a(void *arg)
{while (1){console_put_str(" v_a:0x");console_put_int(test_var_a);}
}void k_thread_b(void *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++;}
}
makefile
BUILD_DIR = ./build
ENTRY_POINT = 0xc0001500
AS = nasm
CC = gcc
LD = ld
LIB = -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/ -I thread/ -I userprog -I userprog/
ASFLAGS = -f elf
CFLAGS = -Wall -m32 -fno-stack-protector $(LIB) -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes
LDFLAGS = -m elf_i386 -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map
OBJS = $(BUILD_DIR)/main.o $(BUILD_DIR)/init.o $(BUILD_DIR)/interrupt.o \$(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o \$(BUILD_DIR)/debug.o $(BUILD_DIR)/string.o $(BUILD_DIR)/memory.o \$(BUILD_DIR)/bitmap.o $(BUILD_DIR)/thread.o $(BUILD_DIR)/list.o \$(BUILD_DIR)/switch.o $(BUILD_DIR)/sync.o $(BUILD_DIR)/console.o \$(BUILD_DIR)/keyboard.o $(BUILD_DIR)/ioqueue.o $(BUILD_DIR)/tss.o \$(BUILD_DIR)/process.o################ c代码编译 ##################
$(BUILD_DIR)/main.o: kernel/main.c lib/kernel/print.h \lib/kernel/stdint.h kernel/init.h kernel/debug.h \kernel/memory.h thread/thread.h kernel/interrupt.h \device/console.h userprog/process.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/init.o: kernel/init.c kernel/init.h lib/kernel/print.h \lib/kernel/stdint.h kernel/interrupt.h device/timer.h \kernel/memory.h thread/thread.h device/console.h \device/keyboard.h userprog/tss.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/interrupt.o: kernel/interrupt.c kernel/interrupt.h \lib/kernel/stdint.h kernel/global.h kernel/io.h \lib/kernel/print.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/timer.o: device/timer.c device/timer.h lib/kernel/stdint.h \kernel/io.h lib/kernel/print.h kernel/interrupt.h \thread/thread.h kernel/debug.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/debug.o: kernel/debug.c kernel/debug.h \lib/kernel/print.h kernel/interrupt.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/string.o: lib/string.c lib/string.h \kernel/debug.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/memory.o: kernel/memory.c kernel/memory.h \lib/kernel/stdint.h lib/kernel/bitmap.h kernel/debug.h \lib/string.h thread/sync.h thread/thread.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/bitmap.o: lib/kernel/bitmap.c lib/kernel/bitmap.h \lib/string.h kernel/interrupt.h lib/kernel/print.h \kernel/debug.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/thread.o: thread/thread.c thread/thread.h \lib/kernel/stdint.h lib/kernel/list.h lib/string.h \kernel/memory.h kernel/interrupt.h kernel/debug.h \lib/kernel/print.h userprog/process.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/list.o: lib/kernel/list.c lib/kernel/list.h \lib/kernel/stdint.h kernel/interrupt.h kernel/debug.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/sync.o: thread/sync.c thread/sync.h \lib/kernel/stdint.h thread/thread.h kernel/debug.h \kernel/interrupt.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/console.o: device/console.c device/console.h \lib/kernel/print.h thread/sync.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/keyboard.o: device/keyboard.c device/keyboard.h \lib/kernel/print.h kernel/interrupt.h kernel/io.h \lib/kernel/stdint.h device/ioqueue.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/ioqueue.o: device/ioqueue.c device/ioqueue.h \lib/kernel/stdint.h thread/thread.h thread/sync.h \kernel/interrupt.h kernel/debug.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/tss.o: userprog/tss.c userprog/tss.h \lib/kernel/stdint.h thread/thread.h kernel/global.h \lib/kernel/print.h lib/string.h$(CC) $(CFLAGS) $< -o $@$(BUILD_DIR)/process.o: userprog/process.c userprog/process.h \kernel/global.h lib/kernel/stdint.h thread/thread.h \userprog/userprog.h kernel/debug.h userprog/tss.h \device/console.h lib/string.h kernel/interrupt.h$(CC) $(CFLAGS) $< -o $@############## 汇编代码编译 ###############
$(BUILD_DIR)/kernel.o: kernel/kernel.S$(AS) $(ASFLAGS) $< -o $@$(BUILD_DIR)/print.o: lib/kernel/print.S$(AS) $(ASFLAGS) $< -o $@$(BUILD_DIR)/switch.o: thread/switch.S$(AS) $(ASFLAGS) $< -o $@############## 连接所有目标文件 #############
$(BUILD_DIR)/kernel.bin: $(OBJS)$(LD) $(LDFLAGS) $^ -o $@.PHONY : mk_dir hd clean allmk_dir:if [ ! -d $(BUILD_DIR) ]; then mkdir $(BUILD_DIR); fihd:dd if=$(BUILD_DIR)/kernel.bin \of=/home/hongbai/bochs/bin/c.img \bs=512 count=200 seek=10 conv=notruncclean:cd $(BUILD_DIR) && rm -f ./*build: $(BUILD_DIR)/kernel.binall: mk_dir build hd
结果截图
还有比较严重的bug,明天调试吧。
结语
两天完成了第11章,只能说内容比我想象的更多吧。最后还没调试好,估计可能是内存分页出了问题。今天太晚了,明天继续调试。
今天力扣刷题两道,涉及双指针,相关博客正在路上。
目标是5.1-5.3弄完12、13章。