练习0
填写已有实验
本实验依赖实验1~4.请把已做的实验1~实验4的代码填入本实验中代码中有lab1、lab2、lab3、lab4的注释相应部分
发现缺失的是kdebug.c、trap.c、default_pmm.c、pmm.c、swap_fifo.c、vmm.c、proc.c
七个文件的相关代码,补全后发现部分文件还需要更改部分代码
alloc_proc函数
注释
//LAB4:EXERCISE1 YOUR CODE
/*
* below fields in proc_struct need to be initialized
* 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
*/
//LAB5 YOUR CODE : (update LAB4 steps)
/*
* below fields(add in LAB5) in proc_struct need to be initialized
* uint32_t wait_state; // waiting state
* struct proc_struct *cptr, *yptr, *optr; // relations between processes
*/
会发现比lab4多两行代码,而这两行代码主要就是初始化所有状态指针,由于本实验是用户进程管理的实验,所以在操作前需要全部初始化,避免在后面使用时因未定义,未初始化而产生错误。
补充代码
// alloc_proc - alloc a proc_struct and init all fields of proc_struct
static struct proc_struct *
alloc_proc(void) {
struct proc_struct *proc = kmalloc(sizeof(struct proc_struct));
if (proc != NULL) {
//LAB4:EXERCISE1 YOUR CODE
/*
* below fields in proc_struct need to be initialized
* 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
*/
//LAB5 YOUR CODE : (update LAB4 steps)
/*
* below fields(add in LAB5) in proc_struct need to be initialized
* uint32_t wait_state; // waiting state
* struct proc_struct *cptr, *yptr, *optr; // relations between processes
*/
proc->state = PROC_UNINIT;//设置进程为未初始化状态
proc->pid = -1; //未初始化的进程id=-1
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);//置空进程名
proc->wait_state = 0; //初始化进程等待状态
proc->cptr = proc->optr = proc->yptr = NULL;//进程相关指针初始化
//*cptr-->children | *yptr-->younger | *optr-->older
}
return proc;
}
do_fork函数
注释
//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
* wakup_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 wakup_proc to make the new child process RUNNABLE
// 7. set ret vaule using child proc's pid
//LAB5 YOUR CODE : (update LAB4 steps)
/* Some Functions
* set_links: set the relation links of process. ALSO SEE: remove_links: lean the relation links of process
* -------------------
* update step 1: set child proc's parent to current process, make sure current process's wait_state is 0
* update step 5: insert proc_struct into hash_list && proc_list, set the relation links of process
*/
这里是更新两段代码,更新第一点是设置父进程为当前进程并且确保其当前进程正在等待;而更新第二点语句是因为单纯的计数加并不能满足进程管理调度,所以需要用特定的set_links
函数来进行操作,避免错误
代码
/* 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
*/
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;
//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
* wakup_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 wakup_proc to make the new child process RUNNABLE
// 7. set ret vaule using child proc's pid
//LAB5 YOUR CODE : (update LAB4 steps)
/* Some Functions
* set_links: set the relation links of process. ALSO SEE: remove_links: lean the relation links of process
* -------------------
* update step 1: set child proc's parent to current process, make sure current process's wait_state is 0
* update step 5: insert proc_struct into hash_list && proc_list, set the relation links of process
*/
if ((proc = alloc_proc()) == NULL)
{
goto fork_out;
}
//设置父节点为当前进程
proc->parent = current;
assert(current->wait_state == 0);//确保当前进程正在等待
//分配内核栈
if (setup_kstack(proc) != 0) {
goto bad_fork_cleanup_proc;
}
//调用copy_mm()函数复制父进程的内存信息到子进程
if (copy_mm(clone_flags, proc) != 0) {
goto bad_fork_cleanup_kstack;
}
//调用copy_thread()函数复制父进程的中断帧和上下文信息
copy_thread(proc, stack, tf);
bool intr_flag;
local_intr_save(intr_flag);
{
proc->pid = get_pid();
hash_proc(proc);//将新进程加入hash_list
set_links(proc);//执行set_links函数,实现设置相关进程链接
}
local_intr_restore(intr_flag);
//唤醒进程,等待调度
wakeup_proc(proc);
//返回子进程的pid
ret = proc->pid;
fork_out:
return ret;
bad_fork_cleanup_kstack:
put_kstack(proc);
bad_fork_cleanup_proc:
kfree(proc);
goto fork_out;
}
idt_init函数
通过注释可知添加一行代码,用来设置相关的中断门
代码
/* idt_init - initialize IDT to each of the entry points in kern/trap/vectors.S */
void
idt_init(void) {
/* LAB1 YOUR CODE : STEP 2 */
/* (1) Where are the entry addrs of each Interrupt Service Routine (ISR)?
* All ISR's entry addrs are stored in __vectors. where is uintptr_t __vectors[] ?
* __vectors[] is in kern/trap/vector.S which is produced by tools/vector.c
* (try "make" command in lab1, then you will find vector.S in kern/trap DIR)
* You can use "extern uintptr_t __vectors[];" to define this extern variable which will be used later.
* (2) Now you should setup the entries of ISR in Interrupt Description Table (IDT).
* Can you see idt[256] in this file? Yes, it's IDT! you can use SETGATE macro to setup each item of IDT
* (3) After setup the contents of IDT, you will let CPU know where is the IDT by using 'lidt' instruction.
* You don't know the meaning of this instruction? just google it! and check the libs/x86.h to know more.
* Notice: the argument of lidt is idt_pd. try to find it!
*/
/* LAB5 YOUR CODE */
//you should update your lab1 code (just add ONE or TWO lines of code), let user app to use syscall to get the service of ucore
//so you should setup the syscall interrupt gate in here
extern uintptr_t __vectors[];
int i;
for (i = 0; i < sizeof(idt) sizeof(struct gatedesc); i ++) {
SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
}
SETGATE(idt[T_SYSCALL], 1, GD_KTEXT, __vectors[T_SYSCALL], DPL_USER);//设置相应的中断门
lidt(&idt_pd);
}
设置一个特定中断号的中断门,专门用于用户进程访问系统调用。在上述代码中,可以看到在执行加载中断描述符表lidt指令前,专门设置了一个特殊的中断描述符idt[T_SYSCALL]
,它的特权级设置为DPL_USER,中断向量处理地址在__vectors[T_SYSCALL]
处。这样建立好这个中断描述符后,一旦用户进程执行INT T_SYSCALL
后,由于此中断允许用户态进程产生(它的特权级设置为DPL_USER),所以CPU就会从用户态切换到内核态,保存相关寄存器,并跳转到__vectors[T_SYSCALL]
处开始执行,形成如下执行路径:
vector128(vectors.S)--\>
\_\_alltraps(trapentry.S)--\>trap(trap.c)--\>trap\_dispatch(trap.c)----\>syscall(syscall.c)-
在syscall中,根据系统调用号来完成不同的系统调用服务。
trap_dispatch函数
根据注释可知每当一个循环时,当时间片用完就将其设置为需要调度
代码
ticks ++;
if (ticks % TICK_NUM == 0) {
assert(current != NULL);
current->need_resched = 1;//时间片用完设置为需要调度
}
当全部修改完后就可以正式进行实验
练习1
加载应用程序并执行(需要编码)
do_execv函数调用load_icode(位于kern/process/proc.c中)来加载并解析一个处于内存中的ELF执行文件格式的应用程序,建立相应的用户内存空间来放置应用程序的代码段、数据段等,且要设置好proc_struct结构中的成员变量trapframe中的内容,确保在执行此进程后,能够从应用程序设定的起始执行地址开始执行。需设置正确的trapframe内容
load_icode函数
注释
/* LAB5:EXERCISE1 YOUR CODE
* should set tf_cs,tf_ds,tf_es,tf_ss,tf_esp,tf_eip,tf_eflags
* NOTICE: If we set trapframe correctly, then the user level process can return to USER MODE from kernel. So
* tf_cs should be USER_CS segment (see memlayout.h)
* tf_ds=tf_es=tf_ss should be USER_DS segment
* tf_esp should be the top addr of user stack (USTACKTOP)
* tf_eip should be the entry point of this binary program (elf->e_entry)
* tf_eflags should be set to enable computer to produce Interrupt
*/
load_icode
函数的主要工作就是给用户进程建立一个能够让用户进程正常运行的用户环境。
该函数主要完成的工作如下:
1、调用
mm_create
函数来申请进程的内存管理数据结构 mm 所需内存空间,并对 mm 进行初始化;2、调用
setup_pgdir
来申请一个页目录表所需的一个页大小的内存空间,并把描述ucore内核虚空间映射的内核页表(boot_pgdir所指)的内容拷贝到此新目录表中,最后让mm->pgdir
指向此页目录表,这就是进程新的页目录表了,且能够正确映射内核虚空间;3、根据可执行程序的起始位置来解析此 ELF 格式的执行程序,并调用
mm_map
函数根据 ELF格式执行程序的各个段(代码段、数据段、BSS段等)的起始位置和大小建立对应的vma
结构,并把vma
插入到mm
结构中,表明这些是用户进程的合法用户态虚拟地址空间;4.根据可执行程序各个段的大小分配物理内存空间,并根据执行程序各个段的起始位置确定虚拟地址,并在页表中建立好物理地址和虚拟地址的映射关系,然后把执行程序各个段的内容拷贝到相应的内核虚拟地址中,至此应用程序执行码和数据已经根据编译时设定地址放置到虚拟内存中了;
5.需要给用户进程设置用户栈,为此调用 mm_mmap 函数建立用户栈的 vma 结构,明确用户栈的位置在用户虚空间的顶端,大小为
256
个页,即1MB,并分配一定数量的物理内存且建立好栈的虚地址<-->物理地址
映射关系;6.至此,进程内的内存管理 vma 和 mm 数据结构已经建立完成,于是把 mm->pgdir 赋值到 cr3 寄存器中,即更新了用户进程的虚拟内存空间,此时的 init 已经被 exit 的代码和数据覆盖,成为了第一个用户进程,但此时这个用户进程的执行现场还没建立好;
7.先清空进程的中断帧,再重新设置进程的中断帧,使得在执行中断返回指令
iret
后,能够让CPU
转到用户态特权级,并回到用户态内存空间,使用用户态的代码段、数据段和堆栈,且能够跳转到用户进程的第一条指令执行,并确保在用户态能够响应中断;
至此,用户进程的用户环境已经搭建完毕。此时initproc将按产生系统调用的函数调用路径原路返回,执行中断返回指令iret后,将切换到用户进程程序的第一条语句位置_start
处开始执行。
首先相关的定义如下:
根据注释主要完成的是proc_struct
结构中tf结构体变量的设置,因为这里我们要设置好tf以便于从内核态切换到用户态然后执行程序
在执行trap
函数前,软件还需进一步保存执行系统调用前的执行现场,即把与用户进程继续执行所需的相关寄存器等当前内容保存到当前进程的中断帧trapframe中(注意,在创建进程是,把进程的trapframe放在给进程的内核栈分配的空间的顶部)。软件做的工作在vector128
和__alltraps
的起始部分:
vectors.S::vector128起始处:
pushl $0
pushl $128
......
trapentry.S::__alltraps起始处:
pushl %ds
pushl %es
pushal
……
自此,用于保存用户态的用户进程执行现场的trapframe的内容填写完毕,操作系统可开始完成具体的系统调用服务。在sys_getpid
函数中,简单地把当前进程的pid成员变量做为函数返回值就是一个具体的系统调用服务。完成服务后,操作系统按调用关系的路径原路返回到__alltraps
中。然后操作系统开始根据当前进程的中断帧内容做恢复执行现场操作。其实就是把trapframe的一部分内容保存到寄存器内容。恢复寄存器内容结束后,调整内核堆栈指针到中断帧的tf_eip处,这是内核栈的结构如下:
/* below here defined by x86 hardware */
uintptr_t tf_eip;
uint16_t tf_cs;
uint16_t tf_padding3;
uint32_t tf_eflags;
/* below here only when crossing rings */
uintptr_t tf_esp;
uint16_t tf_ss;
uint16_t tf_padding4;
这时执行“IRET”指令后,CPU根据内核栈的情况回复到用户态,并把EIP指向tf_eip的值,即INT T_SYSCALL
后的那条指令。这样整个系统调用就执行完毕了。
代码
tf->tf_cs = USER_CS;
tf->tf_ds = tf->tf_es = tf->tf_ss = USER_DS;
tf->tf_esp = USTACKTOP;//0xB0000000
tf->tf_eip = elf->e_entry;
tf->tf_eflags = FL_IF;//FL_IF为中断打开状态
ret = 0;
练习2
父进程复制自己的内存空间给子进程(需要编码)
创建子进程的函数do_fork在执行中将复制当前进程(即父进程)的用户内存地址空间中的合法内容到新进程中(子进程),完成内存资源的复制。具体是通过copy_range函数(位于kern/mm/pmm.c中)实现的,请补充copy_range的实现,确保能够正确执行
注释
/* LAB5:EXERCISE2 YOUR CODE
* replicate content of page to npage, build the map of phy addr of nage with the linear addr start
*
* Some Useful MACROs and DEFINEs, you can use them in below implementation.
* MACROs or Functions:
* page2kva(struct Page *page): return the kernel vritual addr of memory which page managed (SEE pmm.h)
* page_insert: build the map of phy addr of an Page with the linear addr la
* memcpy: typical memory copy function
*
* (1) find src_kvaddr: the kernel virtual address of page
* (2) find dst_kvaddr: the kernel virtual address of npage
* (3) memory copy from src_kvaddr to dst_kvaddr, size is PGSIZE
* (4) build the map of phy addr of nage with the linear addr start
*/
根据注释,其实这个意思比较鲜明,找寻父进程,子进程相应的地址,直接利用函数进行复制,最后建立起一种映射关系
相关函数定义:
代码
void * src_kvaddr = page2kva(page);//找寻父进程的内核虚拟页地址
void * dst_kvaddr = page2kva(npage);//找寻子进程的内核虚拟页地址
memcpy(dst_kvaddr, src_kvaddr, PGSIZE);//复制父进程到子进程
ret = page_insert(to, npage, start, perm);//建立物理地址与子进程的页地址起始位置的映射关系
//perm是权限
练习3
阅读分析源代码,理解进程执行fork/exec/wait/exit的实现,以及系统调用的实现(不需要编码)
fork
fork使用了系统调用SYS_fork
,而系统调用SYS_fork
则主要是由do_fork
和wakeup_proc
来完成的,而wakeup_proc
函数主要是将进程的状态设置为等待,即proc->wait_state = 0
do_fork()
在lab4的时候已经做过详细介绍,这里再简单说一下,主要是完成了以下工作:
- 1、分配并初始化进程控制块(
alloc_proc
函数); - 2、分配并初始化内核栈(
setup_stack
函数); - 3、根据
clone_flag
标志复制或共享进程内存管理结构(copy_mm
函数); - 4、设置进程在内核(将来也包括用户态)正常运行和调度所需的中断帧和执行上下文(
copy_thread
函数); - 5、把设置好的进程控制块放入
hash_list
和proc_list
两个全局进程链表中; - 6、自此,进程已经准备好执行了,把进程状态设置为“就绪”态;
- 7、设置返回码为子进程的 id 号。
exec
当应用程序执行的时候,会调用SYS_exec
系统调用,而当ucore收到此系统调用的时候,则会使用do_execve()
函数来实现,函数主要时完成用户进程的创建工作,同时使用户进程进入执行。
主要工作如下:
- 1、首先为加载新的执行码做好用户态内存空间清空准备。如果mm不为NULL,则设置页表为内核空间页表,且进一步判断mm的引用计数减1后是否为0,如果为0,则表明没有进程再需要此进程所占用的内存空间,为此将根据mm中的记录,释放进程所占用户空间内存和进程页表本身所占空间。最后把当前进程的mm内存管理指针为空。由于此处的initproc是内核线程,所以mm为NULL,整个处理都不会做。
- 2、接下来的一步是加载应用程序执行码到当前进程的新创建的用户态虚拟空间中。这里涉及到读ELF格式的文件,申请内存空间,建立用户态虚存空间,加载应用程序执行码等。
load_icode
函数完成了整个复杂的工作。load_icode
已经介绍过
wait
当执行wait
功能的时候,会调用系统调用SYS_wait
,而该系统调用的功能则主要由do_wait
函数实现,完成对子进程的最后回收工作,即回收子进程的内核栈和进程控制块所占内存空间。
具体的功能实现如下:
- 1、 如果
pid!=0
,表示只找一个进程 id 号为 pid 的退出状态的子进程,否则找任意一个处于退出状态的子进程; - 2、 如果此子进程的执行状态不为
PROC_ZOMBIE
,表明此子进程还没有退出,则当前进程设置执行状态为PROC_SLEEPING(睡眠)
,睡眠原因为WT_CHILD
(即等待子进程退出),调用schedule()
函数选择新的进程执行,自己睡眠等待,如果被唤醒,则重复跳回步骤 1 处执行; - 3、 如果此子进程的执行状态为
PROC_ZOMBIE
,表明此子进程处于退出状态,需要当前进程(即子进程的父进程)完成对子进程的最终回收工作,即首先把子进程控制块从两个进程队列proc_list
和hash_list
中删除,并释放子进程的内核堆栈和进程控制块。自此,子进程才彻底地结束了它的执行过程,它所占用的所有资源均已释放。
exit
用户态的函数库中提供了exit
函数,此函数最终访问SYS_exit
系统调用接口让操作系统来帮助当前进程执行退出过程中的部分资源回收。
具体过程如下:
- 1、如果current->mm != NULL,表示是用户进程,则开始回收此用户进程所占用的用户态虚拟内存空间;
- 2、这时,设置当前进程的执行状态
current->state=PROC_ZOMBIE
,当前进程的退出码current->exit_code=error_code
。此时当前进程已经不能被调度了,需要此进程的父进程来做最后的回收工作(即回收描述此进程的内核栈和进程控制块); - 3、如果当前父进程已经处于等待子进程的状态,即父进程的
wait_state
被置为WT_CHILD
,则此时就可以唤醒父进程,让父进程来帮子进程完成最后的资源回收工作。 - 4、如果当前进程还有子进程,则需要把这些子进程的父进程指针设置为内核线程
init
,且各个子进程指针需要插入到init
的子进程链表中。如果某个子进程的执行状态是PROC_ZOMBIE
,则需要唤醒init
来完成对此子进程的最后回收工作。 - 5、执行schedule()调度函数,选择新的进程执行。
所以说该函数的功能简单的说就是,回收当前进程所占的大部分内存资源,并通知父进程完成最后的回收工作。
在本实验中,与进程相关的各个系统调用属性如下所示:
系统调用名 | 含义 | 具体完成服务的函数 |
---|---|---|
SYS_exit | process exit | do_exit |
SYS_fork | create child process, dup mm | do_fork–>wakeup_proc |
SYS_wait | wait child process | do_wait |
SYS_exec | after fork, process execute a new program | load a program and refresh the mm |
SYS_yield | process flag itself need resecheduling | proc->need_sched=1, then scheduler will rescheule this process |
SYS_kill | kill process | do_kill–>proc->flags |
SYS_getpid | get the process’s pid |
由于“用户环境”限制了用户进程能够执行的指令,即用户进程无法执行特权指令,只能执行一般的指令,所以通过采用系统调用机制为用户进程提供一个获得操作系统服务的统一接口层,简化用户进程的实现,把一下共性的,繁琐的,与硬件相关、与特权指令相关的任务放到操作系统层来实现。
当调用系统函数时,一般执行INT T_SYSCALL
指令后,CPU 根据操作系统建立的系统调用中断描述符,转入内核态,然后开始了操作系统系统调用的执行过程,在执行之前,会保留系统调用前的执行现场,然后保存当前进程的trapframe中,之后操作系统就可以开始完成具体的系统调用服务,完成服务后,调用IRET,CPU根据内核栈的情况恢复到用户态,并把EIP指向tf_eip
的值。这样整个系统调用就执行完毕了。
实验截图
实验成功!!
收获
通过本次实验,我学习了用户进程的管理,通过书本与查阅资料,了解第一个用户进程创建的过程,简单的理解系统调用框架的实现机制,并且学习到通过系统调用sys_fork/sys_exec/sys_exit/sys_wait
来支持运行不同的应用程序,完成对用户进程的执行过程的基本管理。在学习中,也有不理解的地方。对虚拟内存空间不是非常理解,不太懂代码中tf->tf_cs = USER_CS;
等相关参数的构造,并且针对于中断的相关知识掌握的不到位。以后会加大对中断方面的学习理解,回顾试验中不理解的地方。