6.828 - lab3
先看看xv6的进程创建和切换机制,再做jos实验lab3
xv6
1. 创建第一个进程
userinit()1. 从进程结构数组找到一个空闲的进程结构 struct proc
2. 为进程创建页目录pgdir,并在其中对内核区域进行映射(setupkvm, kmap)
3. 申请一个物理页,复制initcode,并将其映射到0地址开始。
4. 设置进程的trapframe数据,进程的栈如下图。
5. 将进程状态设置为RUNNABLE
2. 进程切换
刚刚创建好第一个进程init,那么如何切换到该进程环境中去执行?scheduler()
1. 到进程数组中找到状态为RUNNABLE的进程。当前系统只有init这个进程
2. switchuvm()。其中做了几件事
a. 设置当前任务的TSS段
b. 设置tss段中的ss0和esp0。用户模式通过异常中断等切换到内核态时,
特权级提高。处理器会从当前tr寄存器指向的tss段中拿出ss0和esp0设置0特权
级的栈。把之前的ss和esp值压入内核栈中。
c. tr寄存器指向刚设置好的tss段描述符
3. 切换到新的页目录。这之后就运行在进程的地址空间了,但还在内核态。
void switchuvm(struct proc *p) { pushcli(); cpu->gdt[SEG_TSS] = SEG16(STS_T32A, &cpu->ts, sizeof(cpu->ts)-1, 0); cpu->gdt[SEG_TSS].s = 0; cpu->ts.ss0 = SEG_KDATA << 3; cpu->ts.esp0 = (uint)proc->kstack + KSTACKSIZE; ltr(SEG_TSS << 3); if(p->pgdir == 0) panic("switchuvm: no pgdir"); lcr3(v2p(p->pgdir)); // switch to new address space popcli(); }
4. 切换到进程的context中去。新建进程context中的eip指向forkret,那么ret指令
之后就跳到forkret中去。注意此时esp指向进程内核栈中trapret这个位置(见上图)。
swtch(&cpu->scheduler, proc->context);
swtch: movl 4(%esp), %eax movl 8(%esp), %edx # Save old callee-save registers pushl %ebp pushl %ebx pushl %esi pushl %edi # Switch stacks movl %esp, (%eax) movl %edx, %esp # Load new callee-save registers popl %edi popl %esi popl %ebx popl %ebp ret
5. forkret()的最后一条指令肯定是ret,这个指令从栈中找eip。
此时找到的就是trapret了。
6. 继续见图中,栈指针esp现在指向内核栈中trapframe。
trapret就将trapframe中的数据全部弹出来。
.globl trapret trapret: popal popl %gs popl %fs popl %es popl %ds addl $0x8, %esp # trapno and errcode iret注意最后一条iret弹出了eip,cs,eflags。在userinit()中有设置它们的值。
此时弹出cs后,由于cs的特权级发生了改变,所以要继续在栈中弹出
ss和esp(这两个指定用户态的栈)。
由内核态切到用户态就是这个iret指令完成的。
userinit()中设置进程的trapframe
p->tf->cs = (SEG_UCODE << 3) | DPL_USER;
p->tf->ds = (SEG_UDATA << 3) | DPL_USER; p->tf->es = p->tf->ds; p->tf->ss = p->tf->ds; p->tf->eflags = FL_IF; p->tf->esp = PGSIZE; p->tf->eip = 0; // beginning of initcode.S
3. fork()
fork()是创建一个新进程,与userinit()类似。userinit()根据已知信息创建第一个进程,fork()则是在当前进程的基础上创建一个新进程,新进程与父进程有相同的页表。
以上分析可见,xv6中的进程切换有使用tss(任务状态段),但并未直接使用它来作为任务切换。其唯一的作用是在用户态切入内核态时,提供进程的内核栈ss0和esp0。
jos lab3
Part A: User Environments and Exception Handling
1. 进程创建
env_create()-> env_alloc(&e, 0);
-> load_icode(e, binary, size);
首先使用env_alloc()分配一个进程,其中要为新进程创建页目录,并与内核共用UTOP以上的虚拟地址。
如何做到共用虚拟地址呢?只要复制页目录上相应的目录项即可,那么进程的页目录和内核页目录指向相同的内核页表。
load_icode()的目的是要在新进程的地址空间内申请到一片空间,然后将要运行的代码binary复制到该空间去。
由于binary是ELF格式的,所以在开辟空间和复制数据的时候要严格参照ELF头信息中地址.
用户程序如何与内核链接在一起?这样做的目的是什么?
参看如下kern/Makefrag中kernel生成过程。其中ld使用了"-b binary"的参数来将一些数据以二进制的形式与内核链接在一起。
在这里是直接将生成的ELF格式的应用程序(例如hello)以二进制的形式与内核链接在一起。
链接之后,会有一系列符号(例如_binary_obj_user_hello_start, _binary_obj_user_hello_end)。
这些符号代表了ELF文件hello在kernel中的地址信息。这样一来,内核中的代码就可以知道hello程序的位置了。
JOS在没有文件系统时,以这样的方式来加载并执行应用程序。
KERN_BINFILES := user/hello \ user/buggyhello \ user/buggyhello2 \ user/evilhello \ user/testbss \ user/divzero \ user/breakpoint \ user/softint \ user/badsegment \ user/faultread \ user/faultreadkernel \ user/faultwrite \ user/faultwritekernel $(V)$(LD) -o $@ $(KERN_LDFLAGS) $(KERN_OBJFILES) $(GCC_LIB) -b binary $(KERN_BINFILES)最后enu_run()会执行某个进程。可以用gdb来跟踪,看看是否成功切换到用户模式,是否成功执行到了hello的代码。
1. 设置断点
break env_pop_tf
2. env_pop_tf里面将栈切换到新进程的trapframe上。此时我们查看栈上的内容是否正确
特别要注意栈中的 eip(应该是hello的入口地址), cs(代码段且是用户权限), ss(数据段用户权限)
x/5x $esp
3. 继续si单步。执行完iret指令之后,会跳到hello中,而hello的第一条指令在lib/entry.S中。
4. hello中是打印动作。其最后调用sys_cputs()来打印。这是一个系统调用。
在hello.sym中找到sys_cputs地址并设置断点。单步执行,最后会停在syscall(lib/syscall.c)的int指令上。
当前的中断系统还没设置好,跟踪到这里就会出错。
经过这一系列的跟踪,可以了解进程的创建过程,以及系统调用的执行过程。
2. 中断和异常处理
中断和异常都属于保护机制里的内容,当它们发生时,处理器会由用户模式切换到内核模式(CPL=0)。区别是:
中断是由处理器外部设备产生的异步通知事件,比如外部I/O中断。
异常是由代码产生的同步事件,比如除0或访问无效内存。
中断异常处理的保护机制:
1. 中断描述符表(IDT: Interrupt Descriptor Table)
x86有256个中断/异常入口,称之为中断向量。那么IDT中就有256个描述符与这些中断向量一一
对应。中断描述符中存放着中断处理程序的eip和cs, 同时cs的低2位表示中断处理程序运行
的权限。
2. 任务状态段(TSS: Task State Segment)
跳入中断处理程序之前,要保存返回信息(比如cs和eip),这样中断处理完成之后可以返回到
被中断处继续执行。但是将将这些返回信息保存到哪里才能保证不会被用户程序破坏呢?
出于这个原因,当x86发生中断或陷阱(trap)并且引起了权限等级从用户模式切换到内核模式,
此时esp也将切换为内核栈,内核栈的地址由TSS指定。然后,处理器再将之前用户模式的
ss, esp, eflags, cs, eip压入内核栈中,如果是陷阱的话,还会再压入一个错误码。
TSS还有其他的功用,但JOS只用它来获取内核栈地址。
3. 中断/异常嵌套
中断或异常时如果没有发生权限级别的切换,则不会切换栈。比如,当前处理器正在处理一个中断,此时处于内核模式。如果这时发生了中断或异常,由于没有从低优先级到高优先级切换,此时不会
会仍继续使用之前的ss和esp,并继续向其中压入eflags, cs, eip。
内核需要很好的处理中断/异常嵌套,也要防止嵌套过深而导致内核栈溢出。
中断门和陷阱门的区别在于处理器对于IF标志的处理上:
经过中断门后,处理器会清掉IF位以屏蔽所有中断,防止了中断嵌套。IRET指令又会恢复IF标志。
经过陷阱门时不改变IF位。
Part B: Page Faults, Breakpoints Exceptions, and System Calls
1. 缺页异常处理
发生缺页异常时,处理器会将产生错误的线性地址存入CR2寄存器中。
2. 断点异常
The processor checks the DPL of the interrupt or trap gate only if an exception orinterrupt is generated with an INT n, INT 3, or INTO instruction. Here, the CPL
must be less than or equal to the DPL of the gate. This restriction prevents
application programs or procedures running at privilege level 3 from using a
software interrupt to access critical exception handlers, such as the page-fault
handler, providing that those handlers are placed in more privileged code
segments (numerically lower privilege level). For hardware-generated interrupts
and processor-detected exceptions, the processor ignores the DPL of interrupt
and trap gates.
先看下上面这段英文说明。当软件使用int指令产生异常时会检查CPL<=DPL,即当前运行环境
必须有权限去访问到IDT中对应的描述符,否则会产生一般保护错误(General Protection)。
所以设置breakpoint的中断向量时,DPL=3