回顾 828 的 Lab3,进程管理部分。
此部分增强 JOS 内核的功能,实现对进程的支持。
进程管理包括创建新进程、初始化、分配页目录、分配以及映射物理内存、加载程序 ELF 二进制映像到用户地址空间 以及 在用户模式运行进程。
除此之外,由于进程常常要调用操作系统提供的接口,所以还实现了对中断和异常的处理,操作系统在内核态和用户态之前进行切换。
1、接上篇,内存初始化完毕之后,进行进程初始化
实现创建和运行进程所需的函数:
- env_init(): 初始化 envs 数组中的所有 Env 结构,并将它们添加到 env_free_list 中。调用
env_init_percpu
,它将分区硬件配置为权限级别为 0 (kernal)和权限级别为 3 (user)的单独段。- env_setup_vm(): 为新环境分配一个页目录,并初始化新环境的地址空间的内核部分。
- region_alloc() :为环境分配和映射物理内存。
- load_icode(): 您将需要解析 ELF 二进制映像,就像引导加载程序已经做的那样,并将其内容加载到新环境的用户地址空间中。
- env_create():使用
env_alloc
分配环境,并调用load_icode
将 ELF 二进制文件加载到其中。- env_run(): 启动一个以用户模式运行的给定环境。
以上功能实现对用户进程的支持,但当用户进程使用 int $0x30
指令进行系统调用时,会出现错误(int 是将字符显示到控制台的系统调用)。
这是因为还没有实现 CPU 对系统调用的中断处理。
2、异常处理
异常和中断都是"受保护的控制传输",导致处理器从用户切换到内核模式 (CPL = 0),而不会给用户模式代码任何干扰内核或其他环境功能的机会。
在 x86 上,两个机制协同工作以提供此保护:
-
The Interrupt Descriptor Table
CPU 使用向量作为处理器中断描述符表 (IDT) 的索引,内核在内核专用内存中设置该表,与 GDT 很像。从此表中的合适条目中,处理器加载:
- 要加载到指令指针 (EIP) 寄存器中的值,指向用于处理该类型的异常的内核代码。
- 要加载到代码段 (CS) 寄存器中的值,其中包括在位 0-1 中运行异常处理程序的权限级别。(在 JOS 中,所有异常都在内核模式下处理,权限级别 0)
-
The Task State Segment
简而言之,JOS 使用 TSS 来保存处理器从用户模式切换到内核模式时的处理器状态。比如:EIP、CS 寄存器的值。
以便异常处理程序结束后,从中断的位置恢复处理器的状态。
JOS 将中断向量 48 (0x30) 作为系统调用中断向量。
3、设置 IDT
void
i386_init(void)
{
...
env_init();
trap_init(); // 初始化 IDT
...
}
// 设置 IDT,并把 TSS 和 IDT 加载到 CPU 特定位置。
kern/trap.c/trap_init()
--> SETGATE(idt[T_FPERR], 0, GD_KT, V_FPERR, 0)
...
--> trap_init_percpu() // Initialize and load the per-CPU TSS and IDT
--> Setup a TSS
--> Initialize the TSS slot of the gdt.
--> ltr(GD_TSS0) // Load the TSS selector
--> lidt(&idt_pd) // Load the IDT
4、创建一个进程:
// hello, world
#include <inc/lib.h>
void
umain(int argc, char **argv)
{
cprintf("hello, world\n");
cprintf("i am environment %08x\n", thisenv->env_id);
}
编译时,hello.c
被编译为 ELF 二进制可执行映像嵌入到内存中,以便内核运行。
// 内核态下
--> ENV_CREATE(user_hello, ENV_TYPE_USER) // 内核态下,创建进程,然后切换到用户态执行进程
--> ENV_PASTE3(_binary_obj_, user_hello, _start)[] // 生成二进制程序的名,以便使用
--> env_create(_binary_obj_user_hello_start, ENV_TYPE_USER) // 包含两个函数
--> env_alloc(&e, 0) // 分配并且初始化一个进程,设置各种属性
--> env_setup_vm(e) // 为进程分配并设置虚拟页表
--> load_icode(e, binary) // 加载进程的二进制程序段
--> env_run(&envs[0]) // 运行进程,如果有正在运行的进程,还需要进行上下文的切换
--> lcr3(PADDR(e->env_pgdir)) // 加载进程页表基址对应的物理地址,以便进行地址转换和内存访问
--> env_pop_tf(&e->env_tf) // (内联汇编) 把&env_tf为起始地址的一段空间'当成栈',逐步popa、pop、iret到相应寄存器。且iret结束后进入'user mode'
--> iret // 返回用户模式
// env_pop_tf 是进入用户模式之前的最后一个函数,iret 指令之后进入用户模式。
// 执行 lib/entry.S 中 start 标签处的 cmpl 指令。
// 用户态下
--> 此时 eip = env_tf.tf_eip = 0x80020, 即 lib/extry.S/_start, 之后 call libmain。
--> start executing some environment's code
--> libmain(int argc, char **argv)
--> thisenv = &envs[ENVX(sys_getenvid())] //set thisenv to point env structure
--> lib/sys_getenvid() // 系统调用
--> lib/syscall(SYS_getenvid, 0, 0, 0, 0, 0, 0) // 陷入内核态
// 向寄存器中推送值
--> TRAPHANDLER_NOEC(V_SYSCALL, T_SYSCALL) // jmp _alltraps and call trap
--> trap(tf)
--> trap_dispatch(tf) // Dispatch based on what type of trap occurred
// 系统调用
--> kern/syscall() // since tf->tf_trapno == T_SYSCALL
--> sys_getenvid() // since syscallno == SYS_getenvid and
// return curenv->env_id to tf->tf_regs.reg_eax
--> env_run(curenv)
--> lcr3(PADDR(e->env_pgdir))
--> env_pop_tf(&e->env_tf) // (内联汇编) 恢复寄存器的值
--> iret // 返回用户模式, thisenv
--> umain(argc, argv) // 执行程序代码
--> user/hello.c/umain()
--> cprintf("hello, world\n") // kern/printf.c/cprintf()
--> vcprintf() // kern/printf.c/cprintf()
--> vprintfmt((void*)putch, &cnt, fmt, ap) //lib/printfmt.c
--> punch() // kern/printf.c/punch()
--> cputchar(c) // kern/console.c
--> cons_putc(c) // kern/console.c
// output a character to the console
--> serial_putc(c);
--> lpt_putc(c);
--> cga_putc(c);
--> 输出
--> cprintf("i am environment %08x\n", thisenv->env_id); // 同上
--> exit() // lib/exit.c
--> lib/sys_env_destroy(0)
--> lib/syscall(SYS_env_destroy, 1, envid, 0, 0, 0, 0) // 系统调用
--> "i" (T_SYSCALL) // 推送值到寄存器
--> TRAPHANDLER_NOEC(V_SYSCALL, T_SYSCALL) // 陷入到内核态
// kern/..
--> trap(tf)
--> trap_dispatch(tf) // Dispatch based on what type of trap occurred
// since tf->tf_trapno == T_SYSCALL
--> syscall() // 内核态
// since syscallno == SYS_env_destroy
--> sys_env_destroy(a1)
--> env_destroy(e)
--> env_free(e)
--> monitor(NULL)
--> while (1) monitor(NULL)
lib/syscall
在用户态下调用系统调用,kern/syscall
则是在内核态下。CR2
控制寄存器存储导致错误的linear/virtual address
。- A user program 开始运行在
lib/entry.S
的顶部。- 进入用户态后,执行的第一条指令从
CS:EIP
去找,找到是lib/entry.S/_start
,_start里调用了libmain()。