操作系统实验四实验报告
来源:互联网 发布:mac版飞秋 下载 编辑:程序博客网 时间:2024/04/29 03:29
实验四:内核线程管理
练习1:分配并初始化一个进程控制块
首先来看几个比较重要的数据结构,kern/process/proc.h
中定义的进程控制块及kern/trap/trap.h
中定义的中断帧
struct proc_struct { enum proc_state state; // Process state int pid; // Process ID int runs; // the running times of Proces uintptr_t kstack; // Process kernel stack volatile bool need_resched; // bool value: need to be rescheduled to release CPU? struct proc_struct *parent; // the parent process struct mm_struct *mm; // Process's memory management field struct context context; // Switch here to run process即进程上下文 struct trapframe *tf; // Trap frame for current interrupt即中断上下文 uintptr_t cr3; // CR3 register: the base addr of Page Directroy Table(PDT) uint32_t flags; // Process flag char name[PROC_NAME_LEN + 1]; // Process name list_entry_t list_link; // Process link list list_entry_t hash_link; // Process hash list}struct trapframe { /* below here is a struct of general registers and defined by software */ // 当中断异常发生时,此处结构内的通用寄存器信息由软件负责压栈保存 struct pushregs tf_regs; /* below here are segement registers and defined by software */ // 当中断异常发生时,此处的段寄存器信息由软件负责压栈保存 uint16_t tf_gs; uint16_t tf_padding0; uint16_t tf_fs; uint16_t tf_padding1; uint16_t tf_es; uint16_t tf_padding2; uint16_t tf_ds; uint16_t tf_padding3; uint32_t tf_trapno; /* below here defined by x86 hardware */ // 当中断异常发生时,此处的信息由硬件压栈保存 uint32_t tf_err; uintptr_t tf_eip; uint16_t tf_cs; uint16_t tf_padding4; uint32_t tf_eflags; /* below here only when crossing rings, such as from user to kernel, defined by hardware */ // 仅发生特权级改变时,此处额外的信息由硬件压栈保存 uintptr_t tf_esp; uint16_t tf_ss; uint16_t tf_padding5;}struct context { uint32_t eip; uint32_t esp; uint32_t ebx; uint32_t ecx; uint32_t edx; uint32_t esi; uint32_t edi; uint32_t ebp;}
这里可以看到struct context
和struct trapframe
中有很多寄存器是一样的,前者用于进程上下文切换,后者用于中断上下文切换。注意这两者的含义是不一样的,在本实验中一个进程开始执行需要系统进行初始化,此时tf
被用来保存中断帧,而进程执行时是通过context
来完成切换的,详细见练习3的说明。
结构中一些重要的成员变量说明如下
mm
内存管理信息,包括内存映射列表、页表指针等state
进程所处的状态,有PROC_UNINIT
、PROC_SLEEPING
、PROC_RUNNABLE
、PROC_ZOMBIE
四种,定义在enum proc_state
中parent
父进程,在所有进程中只有内核创建的第一个内核线程idleproc
没有父进程,内核根据父子关系建立树形结构维护一些特殊的操作context
进程的上下文,保存寄存器,用于进程切换tf
中断帧,总是指向内核栈的某个位置,当进程发生中断异常,从用户跳到内核时,中断帧记录了进程在中断前的状态,由于允许中断嵌套,因此ucore
在内核栈上维护了tf
链,保证tf
总是能够指向当前的trapframe
kstack
每个线程都有内核栈,并且位于内核地址空间的不同位置,对于内核线程,该栈就是运行时程序使用的栈,对于普通进程,该栈就是发生特权级改变时需保存被打断硬件信息的栈
另外为了管理系统中所有的进程控制块,ucore
维护了一些重要的全局变量
static struct proc *current
当前占用处理机并处于运行状态的进程控制块指针static list_entry_t hash_list[HASH_LIST_SIZE]
所有进程控制块的哈希表,hash_link
将基于pid
链接入这个哈希表
根据实验手册及代码的提示,练习1的代码如下
把proc进行初步初始化(即把proc_struct中的各个成员变量清零)。但有些成员变量设置了特殊的值
// alloc_proc - alloc a proc_struct and init all fields of proc_structstatic struct proc_struct *alloc_proc(void) { struct proc_struct *proc = kmalloc(sizeof(struct proc_struct)); if (proc != NULL) { proc->state = PROC_UNINIT; // 设置进程为初始态 proc->pid = -1; // 设置进程pid的未初始化值 proc->runs = 0; proc->kstack = 0; proc->need_resched = 0; proc->parent = NULL; proc->mm = NULL; memset(&(proc->context), 0, sizeof(struct context)); proc->tf = NULL; proc->cr3 = boot_cr3; // 使用内核页目录表的基址 proc->flags = 0; memset(proc->name, 0, PROC_NAME_LEN + 1); } return proc;}
之后proc_init
会进一步初始化idleproc
内核线程
练习2:为新创建的内核线程分配资源
do_fork
是创建线程的主要函数,kernel_thread
通过调用do_fork
函数来完成内核线程的创建工作,练习2中要求完成的do_fork
函数主要完成了6项工作
- 分配并初始化进程控制块(
alloc_proc
) - 分配并初始化内核栈(
setup_stack
) - 根据
clone_flag
标志复制或共享内存管理结构(copy_mm
) - 设置进程在内核正常运行和调度所需要的中断帧和上下文(
copy_thread
) - 把设置好的进程控制块放入
hash_list
和proc_list
两个全局进程链表中 - 将进程状态设置为“就绪”态
/* do_fork - parent process for a new child process * @clone_flags: used to guide how to clone the child process * @stack: the parent's user stack pointer. if stack==0, It means to fork a kernel thread. * @tf: the trapframe info, which will be copied to child process's proc->tf */intdo_fork(uint32_t clone_flags, uintptr_t stack, struct trapframe *tf) { int ret = -E_NO_FREE_PROC; struct proc_struct *proc; if (nr_process >= MAX_PROCESS) { goto fork_out; } ret = -E_NO_MEM; //LAB4:EXERCISE2 YOUR CODE /* * Some Useful MACROs, Functions and DEFINEs, you can use them in below implementation. * MACROs or Functions: * alloc_proc: create a proc struct and init fields (lab4:exercise1) * setup_kstack: alloc pages with size KSTACKPAGE as process kernel stack * copy_mm: process "proc" duplicate OR share process "current"'s mm according clone_flags * if clone_flags & CLONE_VM, then "share" ; else "duplicate" * copy_thread: setup the trapframe on the process's kernel stack top and * setup the kernel entry point and stack of process * hash_proc: add proc into proc hash_list * get_pid: alloc a unique pid for process * wakeup_proc: set proc->state = PROC_RUNNABLE * VARIABLES: * proc_list: the process set's list * nr_process: the number of process set */ // 1. call alloc_proc to allocate a proc_struct // 2. call setup_kstack to allocate a kernel stack for child process // 3. call copy_mm to dup OR share mm according clone_flag // 4. call copy_thread to setup tf & context in proc_struct // 5. insert proc_struct into hash_list && proc_list // 6. call wakeup_proc to make the new child process RUNNABLE // 7. set ret vaule using child proc's pid if((proc = alloc_proc()) == NULL){ goto fork_out; // 分配失败,直接返回 } if(setup_kstack(proc) != 0){ goto bad_fork_cleanup_kstack; // 堆栈初始化失败,释放已占用的空间并返回 } if(copy_mm(clone_flags, proc) != 0){ goto bad_fork_cleanup_proc; // 复制或共享内存管理结构失败,释放已占用的空间并返回 } copy_thread(proc, stack, tf); // 复制中断帧和上下文 proc->pid = get_pid(); // 分配pid hash_proc(proc); // 将新进程加入哈希表 list_add(&proc_list, &(proc->list_link)); nr_process ++; wakeup_proc(proc); // 唤醒进程 ret = proc->pid; // 返回进程pidfork_out: return ret;bad_fork_cleanup_kstack: put_kstack(proc);bad_fork_cleanup_proc: kfree(proc); goto fork_out;}
参考答案中有更多的考虑,不同部分及说明如下
proc->parent = current; // 设置父进程为当前的进程 bool intr_flag; // 由于要操作全局数据结构,而ucore允许中断嵌套,为了避免出现线程安全问题,这里需要利用到kern/sync/sync.h中的锁,至于线程安全问题可以参考相关的博文 local_intr_save(intr_flag); { proc->pid = get_pid(); hash_proc(proc); list_add(&proc_list, &(proc->list_link)); nr_process ++; } local_intr_restore(intr_flag);
练习3:阅读代码,理解proc_run函数和它调用的函数如何完成进程切换
参考piazza上同学的单步调式
https://piazza.com/class/i5j09fnsl7k5x0?cid=331
1、kern_init
调用了proc_init
函数,后者启动了创建内核线程的步骤
idleproc = alloc_proc()
通过kmalloc
函数获得了proc_struct
作为第0个进程的控制块,并初始化proc_init
函数对idleproc
内核线程进行进一步初始化
idleproc->pid = 0; // 设置pid=0,即第0个内核线程idleproc->state = PROC_RUNNABLE; // 设置状态为可运行,等待处理机调度执行该进程idleproc->kstack = (uintptr_t)bootstack; // 直接将ucore启动时的内核栈作为改线程的内核栈(以后其他线程的内核栈需要分配获得)idleproc->need_resched = 1; // 设置为可以被调度,根据cpu_idle函数,只要idleproc在执行且可以被调度,就立刻执行schedule函数切换到其他有用进程执行set_proc_name(idleproc, "idle"); // 线程命名为idlenr_process ++; // 线程数+1current = idleproc; // 设置当前线程为idleproc
调用pid = kernel_thread(init_main, "Hello world!!", 0)
创建一个内核线程init_main
2、kernel_thread
函数创建了内核线程的临时中断帧,并调用do_fork
函数来进行进一步产生新的内核线程
// kernel_thread - create a kernel thread using "fn" function// NOTE: the contents of temp trapframe tf will be copied to // proc->tf in do_fork-->copy_thread functionintkernel_thread(int (*fn)(void *), void *arg, uint32_t clone_flags) { struct trapframe tf; // 临时变量tf来保存中断帧,并传递给do_fork memset(&tf, 0, sizeof(struct trapframe)); // tf清零初始化 tf.tf_cs = KERNEL_CS; // 设置代码段为内核代码段KERNEL_CS tf.tf_ds = tf.tf_es = tf.tf_ss = KERNEL_DS; // 设置数据段为内核数据段KERNEL_DS tf.tf_regs.reg_ebx = (uint32_t)fn; tf.tf_regs.reg_edx = (uint32_t)arg; tf.tf_eip = (uint32_t)kernel_thread_entry; // 设置入口为函数kernel_thread_entry(定义在kern/process/entry.S中),该函数主要为函数fn做准备 return do_fork(clone_flags | CLONE_VM, 0, &tf); // 调用do_fork进一步完成创建工作,第二个参数为0代表创建的是内核线程}
kernel_thread_entry: # void kernel_thread(void) pushl %edx # push arg 将fn的参数压栈 call *%ebx # call fn 调用fn pushl %eax # save the return value of fn(arg) 将fn的返回值压栈 call do_exit # call do_exit to terminate current thread 退出线程
3、do_fork
完成的工作在练习2中已充分说明,这里详细说明最重要的copy_thread
函数
// copy_thread - setup the trapframe on the process's kernel stack top and// - setup the kernel entry point and stack of processstatic voidcopy_thread(struct proc_struct *proc, uintptr_t esp, struct trapframe *tf) { proc->tf = (struct trapframe *)(proc->kstack + KSTACKSIZE) - 1; // proc->kstack指向给进程分配的KSTACKPAGE大小的空间,加上大小之后就指向最高地址,然后强制类型转换后-1即在内存顶部空出一块trap frame大小的空间,用于复制传进来的临时帧 *(proc->tf) = *tf; // 复制临时帧 proc->tf->tf_regs.reg_eax = 0; proc->tf->tf_esp = esp; proc->tf->tf_eflags |= FL_IF; proc->context.eip = (uintptr_t)forkret; // 设置指令寄存器为forkret函数,则进入子进程后就会调用forkret proc->context.esp = (uintptr_t)(proc->tf); // 设置栈顶为内核栈存放了tf后的最大地址,即子进程栈顶从tf下面开始}
随后到do_fork
完成所有工作,返回到kernel_thread
再返回到proc_init
再返回到kern_init
4、此时在kern_init
后续有cpu_idle
和schedule
进行进程调度
// cpu_idle - at the end of kern_init, the first kernel thread idleproc will do below worksvoidcpu_idle(void) { while (1) { if (current->need_resched) { // 显然此时current = idleproc且已被设置为需要被调度,故进入schedule函数调度 schedule(); } }}
5、schedule
将返回之前创建的进程,并调用proc_run
进行运行
// proc_run - make process "proc" running on cpu// NOTE: before call switch_to, should load base addr of "proc"'s new PDTvoidproc_run(struct proc_struct *proc) { if (proc != current) { bool intr_flag; // 由于要切换进程,同样要注意线程安全问题,加锁 struct proc_struct *prev = current, *next = proc; local_intr_save(intr_flag); { current = proc; // 设置要切换的进程为接下来的正在运行进程 load_esp0(next->kstack + KSTACKSIZE); // 修改TSS任务状态栈,将TSS的ts_esp0指向下一个进程的堆栈空间,参数值参考3中的注释 lcr3(next->cr3); // 修改页表基址 switch_to(&(prev->context), &(next->context)); // 切换,函数定义在kern\process\switch.S中 } local_intr_restore(intr_flag); }}
6、proc_run
完成一些准备工作后,调用switch_to
最终完成切换
高地址--------------------|arg1:next->context|--------------------|arg0:prev->context|--------------------|return address | --->esp--------------------switch_to: # switch_to(from, to) # save from's registers movl 4(%esp), %eax # eax points to from [esp+4]即prev->context,将指向prev->context的指针放入eax中 popl 0(%eax) # save eip !popl 按照context的顺序保存原进程的各寄存器 movl %esp, 4(%eax) # save esp::context of from movl %ebx, 8(%eax) # save ebx::context of from movl %ecx, 12(%eax) # save ecx::context of from movl %edx, 16(%eax) # save edx::context of from movl %esi, 20(%eax) # save esi::context of from movl %edi, 24(%eax) # save edi::context of from movl %ebp, 28(%eax) # save ebp::context of from高地址--------------------|arg1:next->context|--------------------|arg0:prev->context| --->esp-------------------- # restore to's registers movl 4(%esp), %eax # not 8(%esp): popped return address already 由于前面已经popl,故这里不是8而是[esp+4]即next->context放入eax中 # eax now points to to 按照context的顺序获取新进程的各寄存器 movl 28(%eax), %ebp # restore ebp::context of to movl 24(%eax), %edi # restore edi::context of to movl 20(%eax), %esi # restore esi::context of to movl 16(%eax), %edx # restore edx::context of to movl 12(%eax), %ecx # restore ecx::context of to movl 8(%eax), %ebx # restore ebx::context of to movl 4(%eax), %esp # restore esp::context of to pushl 0(%eax) # push eip 前面将原进程的返回地址弹出,现在将next->context的eip压入栈 ret
注意,新进程就是前面创建的init_main
,参考3可以知道当时proc->context.eip = (uintptr_t)forkret
,当switch_to
返回时,把栈顶的内容赋值给EIP寄存器,此时跳转到了forkret
进行执行
7、forkret
调用forkrets
完成准备并最终进入init_main
// forkret -- the first kernel entry point of a new thread/process// NOTE: the addr of forkret is setted in copy_thread function// after switch_to, the current proc will execute here.static voidforkret(void) { forkrets(current->tf);} # return falls through to trapret....globl __trapret__trapret: # restore registers from stack popal # restore %ds, %es, %fs and %gs popl %gs popl %fs popl %es popl %ds # get rid of the trap number and error code addl $0x8, %esp iret .globl forkretsforkrets: # set stack to this new process's trapframe movl 4(%esp), %esp #esp指向当前进程的中断帧即esp指向current->tf jmp __trapret
注意参考3可以知道proc->context.esp = (uintptr_t)(proc->tf)
,而在6中switch_to
最后压入了proc->context.eip
,故在forkrets
中[esp+4]即指向context.esp
,这里就是中断帧proc->tf
,参考栈的内容、struct trapframe
及__trapret
就会理解跳转情况
switch_to之后的栈在内存中的存放:高地址-------------|context.ebp||...........| -------------|context.esp| --->[esp+4] = proc->tf-------------|context.eip| --->espstruct trapframe { struct pushregs tf_regs; // 通用寄存器,对应__trapret: popal uint16_t tf_gs; // 对应popl %gs uint16_t tf_padding0; uint16_t tf_fs; // 对应popl %fs uint16_t tf_padding1; uint16_t tf_es; // 对应popl %es uint16_t tf_padding2; uint16_t tf_ds; // 对应popl %ds uint16_t tf_padding3; uint32_t tf_trapno; // [esp]此时的esp指向这里 /* below here defined by x86 hardware */ uint32_t tf_err; // [esp+4] uintptr_t tf_eip; // [esp+8]对应addl $0x8, %esp uint16_t tf_cs; ...};
当iret
返回时就会进入此时的esp所指向的proc->tf.tf_eip
,在2中这个值被初始化为kernel_thread_entry
.text.globl kernel_thread_entrykernel_thread_entry: # void kernel_thread(void) pushl %edx # push arg call *%ebx # call fn pushl %eax # save the return value of fn(arg) call do_exit # call do_exit to terminate current thread
由此进入init_main
,当返回时调用do_exit
完成所有过程
完成代码后使用make qemu
查看结果如下,程序正确
(THU.CST) os is loading ......省略部分输出check_alloc_page() succeeded!check_pgdir() succeeded!check_boot_pgdir() succeeded!...省略部分输出kmalloc_init() succeeded!check_vma_struct() succeeded!...省略部分输出check_pgfault() succeeded!check_vmm() succeeded....省略部分输出check_swap() succeeded!++ setup timer interruptsthis initproc, pid = 1, name = "init"To U: "Hello world!!".To U: "en.., Bye, Bye. :)"kernel panic at kern/process/proc.c:344: process exit!!.Welcome to the kernel debug monitor!!Type 'help' for a list of commands.
- 操作系统实验四实验报告
- 操作系统课程实验报告(四)
- 操作系统实验一实验报告
- 操作系统实验二实验报告
- 操作系统实验三实验报告
- 操作系统实验五实验报告
- 操作系统实验六实验报告
- 操作系统实验七实验报告
- 操作系统实验八实验报告
- 操作系统实验报告
- 操作系统实验报告 lab1
- 操作系统实验报告 lab2
- 操作系统实验报告 lab3
- 操作系统 lab4 实验报告
- 操作系统实验报告 lab4
- 操作系统实验报告 lab5
- 操作系统实验报告 lab8
- 操作系统实验报告 lab6
- 基于WIFI模块(ESP8266)与非同一个局域网内服务器建立连接
- Zookeeper安装
- HDU4540 ——威威猫系列故事——打地鼠
- 腾讯TBS X5 WebView的简单使用
- javafx 窗口始终悬浮
- 操作系统实验四实验报告
- Memory barrier(内存屏障)
- HTML文本框录入字母自动大写
- 修改新人出生点,以及修改死亡复活点的方法
- nohup和&的区别
- strtok_r 源码(测试过的,正确的,之前在网上找的内存错误)
- entrySet用法 以及遍历map的用法
- linux中 likely与unlikely
- 创建表空间