陕西省建设部官方网站,网站建设应该学什么,杭州网站提升排名,网站策划书注意事项实验目的 
了解内核线程创建/执行的管理过程了解内核线程的切换和基本调度过程 
实验内容 
练习一#xff1a;分配并初始化一个进程控制块 
1.内核线程及管理 
内核线程是一种特殊的进程#xff0c;内核线程与用户进程的区别有两个#xff1a;内核线程只运行在内核态#x…实验目的 
了解内核线程创建/执行的管理过程了解内核线程的切换和基本调度过程 
实验内容 
练习一分配并初始化一个进程控制块 
1.内核线程及管理 
内核线程是一种特殊的进程内核线程与用户进程的区别有两个内核线程只运行在内核态用户进程会在在用户态和内核态交替运行所有内核线程直接使用共同的ucore内核内存空间不需为每个内核线程维护单独的内存空间而用户进程需要拥有各自的内存空间。 
把内核线程看作轻量级的进程对内核线程的管理和对进程的管理是一样的。对进程的管理是通过进程控制块结构实现的将所有的进程控制块通过链表链接在一起形成进程控制块链表对进程的管理和调度就通过从链表中查找对应的进程控制块来完成。 
2.进程控制块 
 保存进程信息的进程控制块结构的定义在kern/process/proc.h中定义如下 
struct proc_struct {enum proc_state state; 		// Process stateint pid; 					// Process IDint runs; 					// the running times of Procesuintptr_t kstack; 			// Process kernel stackvolatile bool need_resched; // need to be rescheduled to release CPU?struct proc_struct *parent; // the parent processstruct mm_struct *mm; 		// Processs memory management fieldstruct context context; 	// Switch here to run processstruct trapframe *tf; 		// Trap frame for current interruptuintptr_t cr3; 				// the base addr of Page Directroy Table(PDT)uint32_t flags; 			// Process flagchar name[PROC_NAME_LEN  1]; // Process namelist_entry_t list_link; 	// Process link listlist_entry_t hash_link; 	// Process hash list
};mm在Lab3中该结构用于内存管理。在对内核线程管理时由于内核线程不需要考虑换入换出该结构不需要使用因此设置为NULL。唯一需要使用的是mm中的页目录地址保存在cr3变量中。state进程状态有以下几种 PROC_UNINIT未初始化PROC_SLEEPING睡眠状态PROC_RUNNABLE可运行可能正在运行PROC_ZOMBIE等待回收 parent父进程context进程上下文用于进程切换tf中断帧指针用于中断后恢复进程状态cr3页目录的物理地址用于进程切换时快速找到页表位置kstack线程所使用的内核栈list_link所有进程控制块链接形成的链表的节点hash_link所有进程控制块有一个根据pid建立的哈希表hash_link是该链表的节点 
为了管理系统中的所有进程控制块ucore还维护了以下全局变量 
static struct proc *current当前占用CPU且处于“运行”状态进程控制块指针。通常这个变量是只读的只有在进程切换的时候才进行修改并且整个切换和修改过程需要保证操作的原子性需要屏蔽中断。static struct proc *initproc本实验中指向一个内核线程。本实验以后此指针将指向第一个用户态进程。static list_entry_t hash_list[HASH_LIST_SIZE]所有进程控制块的哈希表proc_struct中的成员变量hash_link将基于pid链接入这个哈希表中。list_entry_t proc_list所有进程控制块的双向线性列表proc_struct中的成员变量list_link将链接入这个链表中。 
3.分配并初始化一个进程控制块 
内核线程创建之前需要先创建一个进程控制块管理保存进程信息。alloc_proc函数负责分配创建一个proc_struct结构并进行基本的初始化。此时仅是创建了进程块内核线程本身还没有创建。这是练习一需要完成的部分具体的实现如下 
static struct proc_struct *
alloc_proc(void) {struct proc_struct *proc  kmalloc(sizeof(struct proc_struct));if (proc ! NULL) {proc-statePROC_UNINIT;					//初始状态proc-pid-1;								//初始PID设为-1proc-runs0;								proc-kstack0;								proc-need_resched0;						proc-parentNULL;proc-mmNULL;memset((proc - context), 0, sizeof(struct context)); proc-tfNULL;proc-cr3boot_cr3;							//内核线程在内核运行使用内核页目录proc-flags0;memset(proc-name,0,PROC_NAME_LEN);}return proc;
}4.问题一 
请说明proc_struct中struct context context和struct trapframe *tf成员变量含义和在本实验中的作用为 
context保存了进程的上下文信息即各个寄存器的值用于进程切换时恢复上下文。tf是中断帧的指针指向中断帧。中断帧记录了进程被中断前的信息除寄存器外还有中断号错误码等信息用于中断处理后进程状态的恢复。发生中断时首先从TSS中找到进程内核栈的指针切换到内核栈然后在内核栈顶建立trapframe进入内核态。当中断服务例程运行结束从中断返回时再从trapframe恢复寄存器的值并切换回用户态。用户程序在用户态通过系统调用进入内核态以及在内核态新创建的进程都通过tf指向的中断帧恢复寄存器的值从而回到用户态继续运行。 
练习二为新创建的内核线程分配资源 
1.进程资源的分配 
练习一中实现的alloc_proc为进程创建了进程控制块将新的进程创建还需要为其分配资源。具体为分配内核栈将当前的进程的代码及数据上下文等信息复制给新进程。分配资源的工作是由do_fork函数完成的。do_fork函数会完成分配资源将新进程添加到进程列表并把进程设置为可运行状态最后返回新进程号。 
使用do_fork完成资源分配需要使用一些其他函数这些函数都定义在pro.c中。首先是练习一中实现的创建进程控制块的alloc_proc函数接下来分配资源的同时也会设置进程控制块中的信息。分配内核栈使用的是setup_kstack函数通过调用alloc_pages分配大小为KPAGESIZE的页用于栈空间。复制内存管理信息使用的是copy_mm函数由于本实验创建的是内核线程常驻内存不需要进行这个工作。最后是copy_thread函数完成对原进程的上下文和中断帧的复制。 
其中中断帧和上下文的一些内容需要单独进行设置子进程将在上下文切换后完成进程切换准备运行因此上下文的eip设置为forkret上下文的esp设置为中断帧tf位置在forkret将从中断帧恢复进程状态运行进程。中断帧的eax设置为0因为子进程会返回0esp设置为父进程的用户栈指针本实验中创建内核线程创建出的线程将与父线程共享数据。对于用户进程copy_mm将复制父进程的内存空间建立新的页表及映射使子进程有自己的内存空间。 
//分配内核栈空间
static int setup_kstack(struct proc_struct *proc) {struct Page *page  alloc_pages(KSTACKPAGE);if (page ! NULL) {proc-kstack  (uintptr_t)page2kva(page);return 0;}return -E_NO_MEM;
}
//copy_mm根据clone_flags判断复制还是共享内存管理信息
static int copy_mm(uint32_t clone_flags, struct proc_struct *proc) {assert(current-mm  NULL);/* do nothing in this project */return 0;
}
//复制原进程的上下文
static void
copy_thread(struct proc_struct *proc, uintptr_t esp, struct trapframe *tf) {proc-tf  (struct trapframe *)(proc-kstack  KSTACKSIZE) - 1;	//内核栈顶*(proc-tf)  *tf;proc-tf-tf_regs.reg_eax  0;					//子进程返回0proc-tf-tf_esp  esp;							//父进程的用户栈指针proc-tf-tf_eflags | FL_IF;					//设置能够响应中断proc-context.eip  (uintptr_t)forkret;			//返回proc-context.esp  (uintptr_t)(proc-tf);		//trapframe
}通过以上三个函数就可以为新进程分配资源并复制原进程的状态。接下来就可以给这个进程设置一个pid放入进程列表了。设置pid使用get_pid函数这个函数在下面的问题一中进行分析。将进程加入进程的哈希列表使用hash_proc函数还需要使用wakeup_proc函数将进程设置为可运行状态最后返回该进程的piddo_fork函数就完成了进程的资源分配。 
//将proc加入到hash_list
static void hash_proc(struct proc_struct *proc) {list_add(hash_list  pid_hashfn(proc-pid), (proc-hash_link));
}
//sched.c中的wakeup_proc
void wakeup_proc(struct proc_struct *proc) {assert(proc-state ! PROC_ZOMBIE  proc-state ! PROC_RUNNABLE);proc-state  PROC_RUNNABLE;
}在获取pid和将进程加入链表的操作中需要使用进程链表而进程链表是一个全局变量为了保证多进程下对共享数据的使用不会产生错误需要添加互斥。此处可能产生的错误在下面问题一中具体分析此处先说明互斥是如何实现的。对共享数据的使用会产生错误是因为调度的不可控可能产生多个线程同时访问临界区的情况。因此只要避免在临界区代码处发生调度就可以实现互斥。在ucore中提供了local_intr_save和local_intr_restore函数屏蔽和使能中断。这两个函数在kern\sync中通过一系列调用最终使用cli和sti进行中断的屏蔽和使能。 
static inline bool
__intr_save(void) {if (read_eflags()  FL_IF) {intr_disable();return 1;}return 0;
}static inline void
__intr_restore(bool flag) {if (flag) {intr_enable();}
}
#define local_intr_save(x)      do { x  __intr_save(); } while (0)
#define local_intr_restore(x)   __intr_restore(x);
//使用方式如下
bool intr_flag;
local_intr_save(intr_flag);
//临界区代码
local_intr_restore(intr_flag);2.do_fork分配资源的实现 
使用以上提到的相关函数就可以实现do_fork为新创建的内核线程分配资源。需要注意的是如果分配资源的某一步不成功需要把之前分配的资源回收。最终do_fork的实现如下clone_flags为是否与父进程共享内存管理信息的标志stack为父进程的用户栈tf为父进程的中断栈。这是练习二需要完成的部分代码如下 
int
do_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;//资源分配if((procalloc_proc())NULL) {goto fork_out;}proc-parent  current;			//父进程为当前进程current为全局变量if(setup_kstack(proc)) {goto bad_fork_cleanup_proc;}if(copy_mm(clone_flags,proc)) {goto bad_fork_cleanup_kstack;}copy_thread(proc, stack, tf);	//复制上下文和中断帧//设置pid加入进程列表设置为可运行bool intr_flag;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);wakeup_proc(proc);retproc-pid;fork_out:return ret;bad_fork_cleanup_kstack:put_kstack(proc);
bad_fork_cleanup_proc:kfree(proc);goto fork_out;
}make qemu运行后可以看到init_main内核线程运行该线程只输出字符串在后续实验用于创建其他内核线程或用户进程。 
...
this initproc, pid  1, name  init
To U: Hello world!!.
To U: en.., Bye, Bye. :)
kernel panic at kern/process/proc.c:347:process exit!!.
...3.问题一 
请说明ucore是否做到给每个新fork的线程一个唯一的id请说明你的分析和理由。 
ucore通过调用get_pid函数分配pid对get_pid函数进行分析。 
分配pid前get_pid会先确认可用进程号大于最大进程数。该函数定义了两个静态全局变量next_safe最初被设置为最大进程号last_pid最初设置为1[last_pidnext_safe]就是合法的pid区间。如果last_pid在这个区间内就可以直接返回last_pid作为新分配的进程号。如果last_pidnext_safe就将next_safe设置为MAX_PID遍历链表确保last_pid和已有进程的pid不相同并更新next_safe。维护last_pid到next_safe这个区间将可用的pid范围缩小以提高了分配的效率如果区间不合法也会重新更新区间并排除和已有进程进程号相同的情况因此最终产生的进程的pid是唯一的。但是需要注意的是进程链表是全局变量如果有另一个进程get_pid后还没有把进程加入链表调度到了当前进程而当前进程又需要遍历链表排除进程号相同的情况就可能产生错误因此要在get_pid和将进程加入链表的位置添加互斥。保证互斥的方法为在do_fork中分配进程号和进程加入进程链表的部分关中断避免进程调度。 static int
get_pid(void) {static_assert(MAX_PID  MAX_PROCESS);struct proc_struct *proc;list_entry_t *list  proc_list, *le;static int next_safe  MAX_PID, last_pid  MAX_PID;if ( last_pid  MAX_PID) {last_pid  1;goto inside;}//区间合法性判断if (last_pid  next_safe) {inside:next_safe  MAX_PID;repeat:le  list;//遍历进程链表while ((le  list_next(le)) ! list) {proc  le2proc(le, list_link);if (proc-pid  last_pid) {if ( last_pid  next_safe) {if (last_pid  MAX_PID) {last_pid  1;}next_safe  MAX_PID;goto repeat;			//区间不合法重新遍历链表}}else if (proc-pid  last_pid  next_safe  proc-pid) {next_safe  proc-pid;		//更新next_safe	}}}return last_pid;
}练习三proc_run 函数及进程切换 
1.proc_run 
proc_run函数用于进程切换时运行要切换到的进程。当发生进程调度时调度程序schedule会在进程链表中寻找一个就绪state  PROC_RUNNABLE的进程并向proc_run传入进程控制块切换运行这个进程。proc_run完成的工作为将当前进程设置为要运行的新进程设置任务状态段tss中特权态0下的栈顶指针esp0为要运行的进程内核栈的栈顶切换到要运行进程的页表最后进行上下文切换。 
void proc_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中特权级0的栈顶指针lcr3(next-cr3);								//切换页表switch_to((prev-context), (next-context));	//上下文切换}local_intr_restore(intr_flag);}
}设置任务状态段tss中特权态0下的栈顶指针esp0是为了在未来进程运行时的特权级切换做好准备。在发生中断时需要切换到内核栈并保存当前的运行状态。esp0就是进程的内核栈的栈顶指针通过这个指针就可以找到进程的内核栈并从这里开始压栈保存当前的状态trapframe每个进程都有自己的内核栈因此这个值需要随进程切换而重新设置。 
//pmm.c中定义的load_esp0
void load_esp0(uintptr_t esp0) {ts.ts_esp0  esp0;
}切换页表需要使用lcr3函数重新加载cr3寄存器。在本实验中内核线程都使用内核的地址空间页目录都是boot_cr3这一步在本实验没有作用。 
//x86.h中定义的lcr3
static inline void lcr3(uintptr_t cr3) {asm volatile (mov %0, %%cr3 :: r (cr3) : memory);
}上下文切换是调用Switch.S中定义的switch_to函数完成的。函数调用时调用者的esp4esp8会依次存放传入的参数。此处开始的esp4就是原进程的context结构call指令调用函数时会将返回地址压栈因此esp处的值为返回地址首先将这个值出栈保存接下来就是将context包括的寄存器保存到相应的位置。esp8的位置是新进程的context但是由于之前使用了pop指令因此此时esp4就是切换到的进程的context将切换到的进程的上下文恢复最后的push指令会将返回地址入栈最后的ret指令就会返回到要切换到的进程运行要切换到的进程这样就完成了进程的切换。 
switch_to:                      # switch_to(from, to)# save froms registersmovl 4(%esp), %eax          # eax points to frompopl 0(%eax)                # save eip !poplmovl %esp, 4(%eax)          # save esp::context of frommovl %ebx, 8(%eax)          # save ebx::context of frommovl %ecx, 12(%eax)         # save ecx::context of frommovl %edx, 16(%eax)         # save edx::context of frommovl %esi, 20(%eax)         # save esi::context of frommovl %edi, 24(%eax)         # save edi::context of frommovl %ebp, 28(%eax)         # save ebp::context of from# restore tos registersmovl 4(%esp), %eax          # not 8(%esp): popped return address already# eax now points to tomovl 28(%eax), %ebp         # restore ebp::context of tomovl 24(%eax), %edi         # restore edi::context of tomovl 20(%eax), %esi         # restore esi::context of tomovl 16(%eax), %edx         # restore edx::context of tomovl 12(%eax), %ecx         # restore ecx::context of tomovl 8(%eax), %ebx          # restore ebx::context of tomovl 4(%eax), %esp          # restore esp::context of topushl 0(%eax)               # push eipret综上一个新内核线程建立后切换运行经历了以下步骤 
中断发生向内核栈顶压入当前寄存器值建立trapframe 
----schedule()选择需要切换到的线程 
----proc_run()设置新进程的内核栈顶(为下次中断做准备) 
----switch_to()上下文切换 
----forkret()-forkrets()-__trapret从trapframe恢复寄存器do_fork中设置上下文切换后执行forkret 
----kernel_thread_entry中执行call指令执行内核线程代码 
2.问题一 
在本实验的执行过程中创建且运行了几个内核线程 
在本实验中共创建并运行了两个内核线程。一个是idleproc另一个是initproc。 
idleproc 
idlepro是0号内核线程。kern_init调用了proc_init在proc_init中会创建该线程。该线程的need_resched设置为1运行cpu_idle函数总是要求调度器切换到其他线程。 
//proc_init中创建idle_procif ((idleproc  alloc_proc())  NULL) {panic(cannot alloc idleproc.\n);}//线程初始化idleproc-pid  0;								//0号线程idleproc-state  PROC_RUNNABLE;				//设置为可运行idleproc-kstack  (uintptr_t)bootstack;		//启动后的内核栈被设置为该线程的内核栈idleproc-need_resched  1;						set_proc_name(idleproc, idle);nr_process ;current  idleproc;
//kern_init最后会运行该内核线程调度到其他线程
void cpu_idle(void) {while (1) {if (current-need_resched) {schedule();}}
}initproc 
initproc是第1号线程未来所有的进程都是由该线程fork产生的。init_proc也是在proc_init中创建的通过调用kernel_thread创建该线程运行init_main并输出字符串。 
//init_proc的创建int pid  kernel_thread(init_main, Hello world!!, 0);if (pid  0) {panic(create init_main failed.\n);}initproc  find_proc(pid);set_proc_name(initproc, init);kernel_thread中定义了一个trapframe结构然后将该结构传入do_fork完成线程的建立。 
int kernel_thread(int (*fn)(void *), void *arg, uint32_t clone_flags) {struct trapframe tf;memset(tf, 0, sizeof(struct trapframe));tf.tf_cs  KERNEL_CS;tf.tf_ds  tf.tf_es  tf.tf_ss  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中将进入ebx指定的函数执行return do_fork(clone_flags | CLONE_VM, 0, tf);
}该线程创建完成后proc_init也完成了工作返回到kern_initkern_init会运行idle_proc的cpu_idle进行进程调度从而切换运行init_proc。切换线程是调度器schedule函数完成的该函数会在进程链表中寻找一个就绪的进程调用proc_run切换到改进程。proc_run会进行上下文切换而在do_fork中调用的copy_thread函数中将context.eip设置为了forkret进程切换完成后从forkret开始运行。forkret实际上是forkretsforkrets会从当前进程的trapframe恢复上下文然后跳转到设置好的kernel_thread_entry。 
.globl forkrets
forkrets:# set stack to this new processs trapframemovl 4(%esp), %espjmp __trapret.globl __trapret
__trapret:# restore registers from stackpopal# restore %ds, %es, %fs and %gspopl %gspopl %fspopl %espopl %ds# get rid of the trap number and error codeaddl $0x8, %espiret										//tf.tf_eip  (uint32_t)kernel_thread_entry;kernel_thread_entry会压入edx保存的参数调用ebx指向的函数保存返回值然后do_exit回收资源。通过kernel_thread_entry内核线程可以执行对应的函数并在执行结束后自动调用do_exit终止线程并回收资源。 
.globl kernel_thread_entry
kernel_thread_entry:        # void kernel_thread(void)pushl %edx              # push argcall *%ebx              # call fnpushl %eax              # save the return value of fn(arg)call do_exit            # call do_exit to terminate current thread3.问题二 
语句local_intr_save(intr_flag);…local_intr_restore(intr_flag);在这里有何作用请说明理由。 
在练习二的do_fork实现中已经使用了这两个语句。这两个函数的作用是屏蔽和使能中断他们的定义在kern\sync中通过一系列调用最终使用cli和sti进行中断的屏蔽和使能。在临界区使用这两个函数暂时屏蔽中断避免进程调度从而提供互斥。在proc_run中完成了上下文切换等重要工作如果没有互斥当前进程被设置为要切换运行的进程但还没有完成上下文的切换如果在此时发生了进程调度就可能产生错误。 
实验总结 
重要知识点 
内核线程和用户进程的区别进程控制块内核线程的创建内核线程资源分配进程(线程)切换的过程 
本实验主要是内核线程创建与切换的具体实现。在ucore中首先创建idle_proc这个第0号内核线程然后调用kernel_thread建立init_proc第1号内核线程最后回到kern_init执行idle_proc线程idle_proc总是调度到其他线程。线程具体的创建是由do_fork完成的do_fork调用alloc_proc等函数完成进程控制块的创建内核栈和pid的分配父进程上下文和中断帧的复制还会进行一些设置如将上下文的eip设置为fork_ret在trapframe中将返回值设置为0等。创建完毕后返回pid当调度器调度该线程时调度器调用proc_run完成上下文切换后就会执行fork_ret恢复中断帧从而开始执行指定的程序。