简单回顾
在开始 lab3 的学习之前,我们先简单回顾下 到目前为止,我们的内核能做了什么:
lab1中,我们学习了 PC启动的过程,看到BIOS将我们编写的boot loader 载入内存,然后通过bootloader 将内核载入内存。同时,使用了一个写死的临时页表(entry_pgdir)完成了简单的地址映射;我们的内核最后执行monitor函数(一个简单的shell),这是个看起来像是xxx管理系统的C语言课程设计程序,他接收命令行输入,将输入解析成命令,并逐个调用相干函数。
但是,题目在于,如许简单的页表,只能映射4MB巨细的物理内存,假如我们的内核代码增加了(更不用说加载用户进程了),4MB不够用了,就直接G了,因此发展内核的当务之急就是解决内存的生存空间危机。因此lab2中,我们学习了如何通过 pageinfo 构成的数组pages 和 链表 page_free_list 来管理物理内存;然后学习了页表的映射原理,并编写代码实现了增删查改页表,达到pageinfo和pte之间的映射和取消映射。拥有了如许的基础办法,我们可以将所有物理内存全部利用起来。
但是到现在为止,我们的JOS的功能照旧只有一个简单的monitor,无法加载用户进程(或者说,加载运行其他的可执行文件)。为了能够实现加载用户进程,在lab3中,我们要实现进程加载、调理的基础办法。
lab3重要内容是
- 完成进程管理的初始化
- 完成中断管理的初始化
- 完成系统调用的中断处置惩罚
- 完成内存掩护
lab3 新增的代码源文件如下,没须要一开始就全看,跟着手册遇到什么看什么,最后自然就看完了。
目录文件备注inc/env.hPublic definitions for user-mode environmentstrap.hPublic definitions for trap handlingsyscall.hPublic definitions for system calls from user environments to the kernellib.hPublic definitions for the user-mode support librarykern/env.hKernel-private definitions for user-mode environmentsenv.cKernel code implementing user-mode environmentstrap.hKernel-private trap handling definitionstrap.cTrap handling codetrapentry.SAssembly-language trap handler entry-pointssyscall.hKernel-private definitions for system call handlingsyscall.cSystem call implementation codelib/MakefragMakefile fragment to build user-mode library, obj/lib/libjos.aentry.SAssembly-language entry-point for user environmentslibmain.cUser-mode library setup code called from entry.Ssyscall.cUser-mode system call stub functionsconsole.cUser-mode implementations of putchar and getchar, providing console I/Oexit.cUser-mode implementation of exitpanic.cUser-mode implementation of panicuser/*Various test programs to check kernel lab 3 codePart A 用户进程和非常处置惩罚
就像使用 pages 数组管理物理内存一样,JOS 使用 envs 数组管理所有的进程。在 lab3 中,我们的目的是加载、运行一个用户环境,但是一个操作系统当然要处置惩罚多个进程了,不外这是 lab4要做的事情了,现在我们要做的是认识JOS 维护进程的数据布局和相应的函数。
在 kern/env.c 中看到的,内核维护着三个与环境有关的重要全局变量:- struct Env *envs = NULL; // All environments
- struct Env *curenv = NULL; // The current env
- static struct Env *env_free_list; // Free environment list
复制代码 一旦 JOS 启动并运行,envs 指针就会指向一个代表系统中所有环境的 Env 布局数组。在我们的设计中,JOS 内核最多可同时支持 NENV 个活动环境,不外在任何时候运行的环境通常都要少得多。(NENV 是一个在 inc/env.h 中 #define 的常量。)分配完毕后,envs 数组将包罗一个 Env 数据布局实例,用于表示每个 NENV 大概的环境。
JOS 内核会将所有不活动的 Env 布局生存在 env_free_list 中。这种设计可以方便地分配和取消分配环境,因为只需将它们添加到空闲列表或从空闲列表中移除即可。这和 page_free_list 异曲同工。
核使用 curenv 符号随时跟踪当前正在执行的环境。在启动过程中,在第一个环境开始运行之前,curenv 初始化为 NULL。
现在我们要先认识 env 布局体,其位于 inc/env.h。- struct Env {
- struct Trapframe env_tf; // 保存的寄存器
- struct Env *env_link; // 下一个空闲的进程
- envid_t env_id; // 进程的唯一标识符
- envid_t env_parent_id; // 该进程的父进程的 env_id
- enum EnvType env_type; // 用于标识是否是特殊的系统进程
- unsigned env_status; // 进程状态
- uint32_t env_runs; // 进程运行次数
- // Address space
- pde_t *env_pgdir; // Kernel virtual address of page dir
- };
复制代码 可以看到,比较关键的有进程id、进程状态、特殊进程标识,这些在 inc/env.h 中都有定义。比较令人迷惑的是这个运行次数,临时不知道是什么寄义。先来看看进程ID的定义:- typedef int32_t envid_t;
- // An environment ID 'envid_t' has three parts:
- //
- // +1+---------------21-----------------+--------10--------+
- // |0| Uniqueifier | Environment |
- // | | | Index |
- // +------------------------------------+------------------+
- // \--- ENVX(eid) --/
- //
- // The environment index ENVX(eid) equals the environment's index in the
- // 'envs[]' array. The uniqueifier distinguishes environments that were
- // created at different times, but share the same environment index.
- //
- // All real environments are greater than 0 (so the sign bit is zero).
- // envid_ts less than 0 signify errors. The envid_t == 0 is special, and
- // stands for the current environment.
- #define LOG2NENV 10
- #define NENV (1 << LOG2NENV)
- #define ENVX(envid) ((envid) & (NENV - 1))
复制代码 注意 for 循环必须是从 NENV 至 0 进行遍历。如许才气保证 env_free_list 的循序和 envs 数组中的顺序一致。
这里看一眼 env_init_percpu()是干什么的- 练习 1. 修改 `kern/pmap.c` 中的 `mem_init()` ,分配并映射 `envs` 数组。该数组由 `Env` 结构的 `NENV` 实例组成,分配方式与分配页面数组类似。与页面数组一样,支持 `envs` 的内存也应在 `UENVS`(定义于 `inc/mlayout.h` )处映射为用户只读,这样用户进程才能读取该数组。
- 你应该运行代码并确保 `check_kern_pgdir()` 成功。
- [[lab3 - 翻译#^988084]]
复制代码 env_setup_vm
- envs = (struct Env *)boot_alloc(sizeof(struct Env) * NENV);
- memset(envs, 0, sizeof(struct Env) * NENV);
- //...
- boot_map_region(kern_pgdir, UENVS, PTSIZE, PADDR(envs), PTE_U | PTE_P);
复制代码 env_setup_vm 会在初始化一个 env 的时候发挥作用,这里看看已经写好的 env_alloc
env_alloc
- void
- i386_init(void)
- {
- extern char edata[], end[];
- // Before doing anything else, complete the ELF loading process.
- // Clear the uninitialized global data (BSS) section of our program.
- // This ensures that all static/global variables start out zero.
- memset(edata, 0, end - edata);
- // Initialize the console.
- // Can't call cprintf until after we do this!
- cons_init();
- cprintf("6828 decimal is %o octal!\n", 6828);
- // Lab 2 memory management initialization functions
- mem_init();
- // Lab 3 user environment initialization functions
- env_init();
- trap_init();
- #if defined(TEST)
- // Don't touch -- used by grading script!
- ENV_CREATE(TEST, ENV_TYPE_USER);
- #else
- // Touch all you want.
- ENV_CREATE(user_hello, ENV_TYPE_USER);
- #endif // TEST*
- // We only have one user environment for now, so just run it.
- env_run(&envs[0]);
- }
复制代码 需要注意一点,在执行 env_free 代码时,应该使用kern_pgdir,
但是 kern_pgdir 中没有 env e 的映射,以是需要先用 env e 的 env_pgdir 来获取物理地址,
然后利用 KADDR 宏从地址空间顶部的 物理内存映射区访问。
追念一下lab2中,我们在mem_init里写过:- #define ENV_PASTE3(x, y, z) x ## y ## z
- #define ENV_CREATE(x, type) \
- do { \
- extern uint8_t ENV_PASTE3(_binary_obj_, x, _start)[]; \
- env_create(ENV_PASTE3(_binary_obj_, x, _start), \
- type); \
- } while (0)
- #endif // !JOS_KERN_ENV_H
复制代码 注意,虽然每个进程都有顶部的 物理内存映射区 的页表,但是他们没有权限读写。(这么方便的功能当然只该有内核有权限,这也是执行env_free时,使用kern_pgdir的原因之一)
除此之外,我们需要注意 env_free 中需要手动调用 page_decref。lab2 和 lab3 代码中更多的是调用封装好的 page_insert(不用处置惩罚 计数递增)和 page_remove (不用处置惩罚 计数递减)。但是一旦涉及到对页表本身占用的物理页做增删处置惩罚时,就需要手动调用 page_alloc 或 page_decref 处置惩罚计数。
(比如 pgdir_walk(页表二级遍历时,大概需要访问尚未分配的 pte_table)、env_create(用户进程创建页表)和这里的 env_free )
env_run
在看代码之前,我们先想一想,让一个进程 Env e 运行起来,大概需要做那些事呢?
首先,想到的就是恢复这个进程的寄存器,让EIP指向继续执行的代码,
在那之前还要恢复cr3寄存器,加载 Env e 的页表
除此之外,我们还要考虑,env_run 是什么时候调用的,也许此时有一个进程正在运行,curenv指向其他进程。以是要考虑修改 curenv 、以及 Env e 的 status、runs等变量。- // 将 “envs ”中的所有环境标记为空闲环境,
- // 将它们的 env_ids 设置为 0,并将它们插入 env_free_list 中。
- // 确保环境在空闲列表中的顺序与它们在 envs 数组中的顺序一致(
- // 也就是说,这样第一次调用 env_alloc()时就会返回 envs[0])。
- //
- void
- env_init(void)
- {
- // Set up envs array
- // LAB 3: Your code here.
- env_free_list = NULL;
- for(int i = NENV-1;i>=0;--i){
- envs[i].env_status = ENV_FREE;
- envs[i].env_id = 0;
- envs[i].env_link = env_free_list;
- env_free_list = &envs[i];
- }
- // Per-CPU part of the initialization
- env_init_percpu(); //加载 GDT 和段描述符。
- }
复制代码 正如注释中说的,env_run 假如不是初次调用,说明这是一次进程切换。那么什么时候需要切换进程呢?
大概能想到,发生非常、进程调理等,假如是进程调理这种比较平和的方式,那么curenv 肯定是 ENV_RUNNING了,其他环境临时等 lab4 学完才气了解到。
注意最后一句 env_pop_tf(&e->env_tf); 寄存器状态恢复,就意味着进程正式运行了,因为eip被改变了。
总结:env.c中的函数关系
此时, make qemu ,内核就会将 user/hello.c 编译出来的可执行文件,通过 env_create 创建出来,并通过 env_run 运行起来。在这之前,我们来看一眼这个程序:- // 加载 GDT 和段描述符。
- void
- env_init_percpu(void)
- {
- lgdt(&gdt_pd);
- // 内核从不使用 GS 或 FS,因此我们将其设置为用户数据段。
- asm volatile("movw %%ax,%%gs" : : "a" (GD_UD|3));
- asm volatile("movw %%ax,%%fs" : : "a" (GD_UD|3));
- // 内核会使用 ES、DS 和 SS。 我们将根据需要在内核和用户数据段之间进行切换。
- asm volatile("movw %%ax,%%es" : : "a" (GD_KD));
- asm volatile("movw %%ax,%%ds" : : "a" (GD_KD));
- asm volatile("movw %%ax,%%ss" : : "a" (GD_KD));
- // 将内核文本段载入 CS。
- asm volatile("ljmp %0,$1f\n 1:\n" : : "i" (GD_KT));
- // 为了稳妥起见,清除本地描述符表(LDT),因为我们不使用它。
- lldt(0);
- }
复制代码 注意,用户程序的cprintf的声明虽然和我们刚刚编程时用的cprintf雷同,都来自 inc/stdio.h,但是他们的实现不同:
用vscode 搜索可以发现,有两个定义
内核使用的是 kern/printf.c 而 user/hello.c 使用的却是 lib/printf.c,用户的 cprintf 会调用到 lib/syscall.c 中的 sys_cputs
syscall 这个函数的定义如下:
实际上就是使用了 int 0x30 系统调用,这是一个中断。这个中断只能在内核态调用,但是XXX以是 make qemu 会导致三重故障,即:
假如统统顺遂,系统将进入用户空间并执行 hello 二进制文件,直到使用 int 指令进行系统调用。这时就会出现题目,因为 JOS 没有设置允许从用户空间过渡到内核的硬件。当中央处置惩罚器发现本身的设置不允许处置惩罚这个系统调用中断时,它就会产生一个一般掩护非常,发现本身无法处置惩罚这个非常后,又会产生一个双重故障非常,发现本身也无法处置惩罚这个非常后,最后就会放弃,这就是所谓的 "三重故障" 。通常环境下,CPU 会重置,系统会重启。虽然这对传统应用程序很重要(请参阅本博文中的原因解释)
就像这个样子:
我们用 GDB 在 env_pop_tf() 函数设置断点,然后通过指令 si,单步调试,观察 iret 指令前后寄存器的变革。
为了能让用户进程有能力处置惩罚非常,学习如何处置惩罚中断和非常
处置惩罚中断和非常
在这之前,我们需要彻底摸头 x86中断和非常机制。训练3的使命就是学习80386的手册。
Exercise 3
- // 为环境 e 初始化内核虚拟内存布局。
- // 分配一个页面目录,相应设置 e->env_pgdir,并初始化新进程地址空间的内核部分。
- // 暂时不要将任何内容映射到环境虚拟地址空间的用户部分。
- //
- // 成功时返回 0,错误时返回 <0。 错误包括
- // -E_NO_MEM 如果页面目录或表无法分配。
- //
- static int
- env_setup_vm(struct Env *e)
- {
- int i;
- struct PageInfo *p = NULL;
- // 为页面目录分配页面
- // 由于我们在构造一个新的pgdir,而不是向已经存在的kern_pgdir中插入 pde,或插入增加映射
- // 我们不能使用 page_insert、pgdir_walk等用于页表管理的方法,
- // 只能通过 物理内存管理的方法,申请物理页,并手动调整七计数
- if (!(p = page_alloc(ALLOC_ZERO)))
- return -E_NO_MEM;
- // 现在,设置 e->env_pgdir 并初始化页面目录。
- //
- // 提示:
- // 所有环境的 VA 空间在 UTOP 以上是相同的(UVPT 除外,我们在下面设置)。
- // 有关权限和布局,请参见 inc/memlayout.h。
- // 能否将 kern_pgdir 用作模板? 提示:可以。
- // (确保您在lab 2 中正确设置了权限)。
- // - UTOP 下面的初始 VA 是空的。
- // - 你不需要再调用 page_alloc。
- // - 注意:一般情况下,pp_ref 不会被维护。
- // 但 env_pgdir 是个例外 -- 你需要增加 env_pgdir 的 pp_ref 才能使 env_free 正常工作。
- // - kern/pmap.h 中的函数非常方便。
- // LAB 3: Your code here.
-
- e->env_pgdir = page2kva(p);
- memcpy(e->env_pgdir, kern_pgdir, PGSIZE);
- // UVPT 将环境自身的页表映射为只读。
- p->pp_ref ++;
- // Permissions: kernel R, user R
- e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_P | PTE_U;// p在此时被映射到UVPT
- p->pp_ref ++;//由于直接调用了page_alloc,需要手动计数
- return 0;
- }
复制代码 观察下TSS
创建中断形貌符表
JOS 在 trapentry.S 中,为每个非常或中断设置处置惩罚程序,
在 trap_init() 中用这些处置惩罚程序的地址创建 IDT。
那这些处置惩罚程序具体要做什么呢?手册提示我们:
- 每个处置惩罚程序都应在堆栈上创建一个 struct Trapframe(拜见 inc/trap.h)
2.将Trapframe 的地址作为参数调用 trap()(在 trap.c 中)。
先来看看 trapentry.S
trapentry.S
- // 分配并初始化一个新环境。
- // 成功后,新环境将存储在 *newenv_store 中。
- //
- // 成功时返回 0,失败时返回 <0。 错误包括
- // -E_NO_FREE_ENV 如果所有 NENV 环境都已分配完毕
- // 内存耗尽时返回 E_NO_MEM
- //
- int
- env_alloc(struct Env **newenv_store, envid_t parent_id)
- {
- int32_t generation;
- int r;
- struct Env *e;
- if (!(e = env_free_list))//从空闲列表中取下一个 env
- return -E_NO_FREE_ENV;
- // 为该环境分配和设置页面目录。
- if ((r = env_setup_vm(e)) < 0)
- return r;
- // 为该环境生成 env_id。
- generation = (e->env_id + (1 << ENVGENSHIFT)) & ~(NENV - 1);
- if (generation <= 0) // Don't create a negative env_id.
- generation = 1 << ENVGENSHIFT;
- e->env_id = generation | (e - envs);
- // 设置基本状态变量。
- e->env_parent_id = parent_id;
- e->env_type = ENV_TYPE_USER;
- e->env_status = ENV_RUNNABLE;
- e->env_runs = 0;
- // 清除所有已保存的寄存器状态,
- // 以防止该 Env 结构中先前环境的寄存器值 “泄漏 ”到我们的新环境中。
- memset(&e->env_tf, 0, sizeof(e->env_tf));
- // 为段寄存器设置适当的初始值。
- // GD_UD 是 GDT 中的用户数据段选择器,
- // GD_UT 是用户文本段选择器(参见 inc/memlayout.h)。
- // 每个段寄存器的低 2 位包含请求者权限级别(RPL);3 表示用户模式。
- // 当我们切换权限级别时,硬件会对 RPL 和存储在描述符中的
- // 描述符权限级别(DPL)进行各种检查。
- e->env_tf.tf_ds = GD_UD | 3;
- e->env_tf.tf_es = GD_UD | 3;
- e->env_tf.tf_ss = GD_UD | 3;
- e->env_tf.tf_esp = USTACKTOP;
- e->env_tf.tf_cs = GD_UT | 3;
- // You will set e->env_tf.tf_eip later.
- // commit the allocation
- env_free_list = e->env_link;
- *newenv_store = e;
- cprintf("[%08x] new env %08x\n", curenv ? curenv->env_id : 0, e->env_id);
- return 0;
- }
复制代码 trap_init()
- //
- // 为环境 env 分配 len 字节的物理内存,并将其映射到环境地址空间中的虚拟地址 va。
- // 不会以任何方式将映射页清零或初始化。
- // 页面应可被用户和内核写入。
- // 如果任何分配尝试失败,就会panic。
- //
- static void
- region_alloc(struct Env *e, void *va, size_t len)
- {
- // 实验 3:此处为您的代码。
- // (但仅限于 load_icode 需要时)。
- // 提示:如果调用者可以传递非页面对齐的'va'和'len'值,则使用 region_alloc 会更容易。
- // 应该将 va 向下舍入,将 (va + len) 向上舍入。
- // (注意拐角情况!)。
- void *begin = ROUNDDOWN(va, PGSIZE), *end = ROUNDUP(va+len, PGSIZE);
- while(begin < end){
- struct PageInfo *pp = page_alloc(ALLOC_ZERO);
- if(!pp){
- panic("region_alloc failed\n");
- }
- page_insert(e->env_pgdir, pp, begin, PTE_P | PTE_W | PTE_U);
- begin += PGSIZE;
- }
- }
复制代码 来看看这个最后调用的 trap_init_percpu- //
- // 为用户进程设置初始程序二进制文件、堆栈和处理器标志。
- // 该函数只在内核初始化期间,即运行第一个用户模式环境之前调用。
- //
- // 该函数将 ELF 二进制映像中的所有可加载段加载到环境的用户内存中,从 ELF 程序头中指示的相应虚拟地址开始。
- // 同时,它将程序头中标记为映射但实际上不存在于 ELF 文件中的任何部分(即程序的 bss 部分)清零。
- // 除了 Boot Loader 还需要从磁盘读取代码外,所有这些都与 Boot Loader 的工作非常相似。 看看 boot/main.c 就会明白。
- //
- // 最后,这个函数为程序的初始堆栈映射了一个页面。
- //
- // load_icode 在遇到问题时会panic
- // - load_icode 怎么会失败? 给定的输入可能有什么问题?
- static void
- load_icode(struct Env *e, uint8_t *binary)
- {
- // 提示:
- // 按照 ELF 程序段头指定的地址将每个程序段加载到虚拟内存中。
- // 只应加载 ph->p_type == ELF_PROG_LOAD 的程序段。
- // 每个程序段的虚拟地址可以在 ph->p_va 中找到,其在内存中的大小可以在 ph->p_memsz 中找到。
- // ELF 二进制文件中从 “binary + ph->p_offset ”开始的 ph->p_filesz 字节应复制到虚拟地址 ph->p_va。 剩余的内存字节应清零。
- // (ELF 头应该是 ph->p_filesz <= ph->p_memsz。)
- // 使用前一个lab中的函数分配和映射页面。
- //
- // 目前所有页面保护位都应为用户读/写。
- // ELF 程序段不一定是页面对齐的,但在本函数中可以假设没有两个程序段会接触同一个虚拟页面。
- //
- // 你可能会发现 region_alloc 这样的函数很有用。
- //
- // 如果能直接将数据移动到存储在 ELF 二进制文件中的虚拟地址,加载段就会简单得多。
- // 那么在执行此函数时,哪个页面目录应该有效呢?
- //
- // 你还必须对程序的入口点做一些处理,以确保环境在那里开始执行。
- // 参见下面的 env_run() 和 env_pop_tf())。
- // 实验 3:你的代码在这里。
- struct Elf * elfhdr = (struct Elf *) binary;
- struct Proghdr *ph = (struct Proghdr *) ((uint8_t *) elfhdr + elfhdr->e_phoff);
- if (elfhdr->e_magic != ELF_MAGIC) {
- panic("binary is not ELF format\n");
- }
- ph = (struct Proghdr *) ((uint8_t *) elfhdr + elfhdr->e_phoff);
- int ph_num = elfhdr->e_phnum;
- // 接下来需要切换到这个 进程的页表,目前进程的页表内容和 kern_pgdir 是一样的(UVPT除外)
- // 因此,使用进程的页表一样能访问到链接的二进制文件(这里的 elfhdr)
- // 为什么要切换呢?每个进程都有自己的页表,将物理内存中的代码数据映射到自己独立的地址空间
- lcr3(PADDR(e->env_pgdir));
- for(int i = 0; i < ph_num; i++){
- if(ph[i].p_type == ELF_PROG_LOAD){
- region_alloc(e, (void *)ph[i].p_va, ph[i].p_memsz);
- memset((void*) ph[i].p_va, 0, ph[i].p_memsz);
- memcpy((void*) ph[i].p_va, binary + ph[i].p_offset, ph[i].p_filesz);
- }
- }
-
- lcr3(PADDR(kern_pgdir));
- e->env_tf.tf_eip = elfhdr->e_entry;//在 env_alloc 中,唯独 eip 没有初始化
- // 现在为程序的初始堆栈映射一个页面
- // 虚拟地址 USTACKTOP - PGSIZE.
- region_alloc(e, (void *)(USTACKTOP - PGSIZE), PGSIZE);
- }
复制代码 这里的 ts 是用来存储 TSS 数据的布局体,位于 inc/mmu.h。
实际上,到了这里,Part A的训练已经完成,简单小结一下,非常发生后的处置惩罚过程
以是说,trap,带着 Trapframe 毕竟做了什么呢,在进入 Part B 之前须要将这统统弄明白
JOS 的中断处置惩罚过程
先来看看 trap()做了什么
trap
- // 使用 env_alloc 分配一个新环境,
- // 使用 load_icode 将命名的精灵二进制文件载入其中,并设置其 env_type。
- // 只有在运行第一个用户模式环境之前,内核初始化过程中才会调用该函数。
- // 新环境的父 ID 被设置为 0。
- void env_create(uint8_t *binary, enum EnvType type)
- {
- // LAB 3: Your code here.
- struct Env * new_env;
- envid_t parent_id = 0;
- int r = env_alloc(&new_env, parent_id);
- if(r< 0)
- panic("env_create error: %e", r);
- load_icode(new_env, binary);
- new_env->env_type = type;//顺带一体,env_alloc 默认已将type设为 ENV_TYPE_USER
- }
复制代码 可以看到,trap 的重要工作就是调用 trap_dispatch 处置惩罚tf,然后调用 env_run 将控制交换给用户进程。trapdispatch是个需要我们后期补全的函数。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |