diff --git a/thu_os/chp11.md b/thu_os/chp11.md index 38e7e4b..27928f4 100644 --- a/thu_os/chp11.md +++ b/thu_os/chp11.md @@ -7,11 +7,11 @@ 前面在物理内存管理部分,我有提到物理内存管理的一切目的,都是为了多任务的系统,否则对于单道程序的操作系统,直接让那单道程序独占内存和CPU就可以了,自然不需要内存管理,显然也不需要进程的管理。所以,进程概念的产生,其实还是处在多道程序的背景下的。 -正是因为操作系统希望同时运行多道程序,而我们都知道程序运行需要资源,因此操作系统需要合理地把自己的资源组织管理起来,在程序运行的时候分配相应的资源,在程序退出的时候回收那部分资源。那么问题来了?操作系统怎么知道把资源分配给了谁?怎么知道资源的占有者什么时候退出?又怎么知道应该从哪里回收分配出去的资源?因此,需要对每一个运行的任务进行抽象。对每一个运行的程序的抽象,就是这里进程的概念。这样,进程就成了操作系统资源分配的基本单位。 +正是因为操作系统希望同时运行多道程序,而程序运行需要资源,因此操作系统需要合理地把自己的资源组织管理起来,在程序运行的时候分配相应的资源,在程序退出的时候回收那部分资源。那么问题来了?操作系统怎么知道把资源分配给了谁?怎么知道资源的占有者什么时候退出?又怎么知道应该从哪里回收分配出去的资源?因此,需要对每一个运行的任务进行抽象。对每一个运行的程序的抽象,就是这里进程的概念。这样,进程就成了操作系统资源分配的基本单位。 ## 进程与程序的区别 -我们都知道,程序其实就是编译后的代码,机器码,驻留在外存或者内存当中。那么进程呢?其实上面已经提出了,进程本质上就是运行的程序。这句话涵盖了它们之间的全部区别。 +程序其实就是编译后的代码,机器码,驻留在外存或者内存当中。那么进程呢?其实上面已经提出了,进程本质上就是运行的程序。这句话涵盖了它们之间的全部区别。 一方面,进程必然是包含程序的,毕竟它需要运行这个程序,但是程序只是进程的一部分。【进程是运行的程序】,这里的【运行】,就体现了它们之间的区别——既然程序要运行,就必然在代码以外还需要一定的数据,就必然需要占用一定的资源,比如代码和数据驻留的内存资源,就会涉及到程序当前的执行状态,比如一些上下文的信息,还有寄存器的信息。因此,可以概括出,进程是由代码,数据,各个寄存器以及占用的资源所组成的。 @@ -99,7 +99,7 @@ > 状态队列的概念 -前面已经提过,操作系统会为不同状态的进程分别维护一个单独的队列,即状态队列。现在我们知道,随着进程状态的切换,状态队列也会发生相应的变化,进程会从从一个队列加入到另一个队列。在进程状态模型下,状态队列的切换如图所示: +前面已经提过,操作系统会为不同状态的进程分别维护一个单独的队列,即状态队列。随着进程状态的切换,状态队列也会发生相应的变化,进程会从从一个队列加入到另一个队列。在进程状态模型下,状态队列的切换如图所示: ![status_queue](images/status_queue.png) @@ -109,13 +109,13 @@ 设想一种场景。现在我们有一个MP3播放器的应用程序,该应用程序主要有三个核心功能模块,即从MP3音频文件中读取数据,将读取的数据解压缩,将解压缩后的音频数据播放出来。应该如何实现这个应用程序呢? -这里我们主要考虑的指标是音频文件播放是否流畅、连贯。为此,就需要设计的应用程序可以及时将刚读出来的压缩数据解压缩,并且播放出来。一种简明的设计思路如下图所示: +这里主要考虑的指标是音频文件播放是否流畅、连贯。为此,就需要设计的应用程序可以及时将刚读出来的压缩数据解压缩,并且播放出来。一种简明的设计思路如下图所示: ![mp3_version_1](images/mp3_version1.png) -在该设计思路中,我们将读取数据,解压缩,播放文件依次执行。但是不难看出,这种实现存在很致命的缺陷:各个模块之间不能并行进行。可以看到,这里读取压缩音频文件涉及到I/O操作,解压缩是CPU密集型操作,因此这两个模块在理论上是可以并发执行的,即在将压缩文件读取出来的同时,CPU可以对这些数据进行解压缩,这样可以大幅度提高资源的利用效率。此外,按照我们的设想,在播放时应该也可以同时进行读文件和解压的操作,在这种实现方式都却不能做到。在这种实现下,为了播放音频文件连贯,必须一次性将全部音频文件读出并且解压缩,最后才能播放,漫长的等待时间非常影响用户体验。 +在该设计思路中,读取数据,解压缩,播放文件依次执行。但是不难看出,这种实现存在很致命的缺陷:各个模块之间不能并行进行。可以看到,这里读取压缩音频文件涉及到I/O操作,解压缩是CPU密集型操作,因此这两个模块在理论上是可以并发执行的,即在将压缩文件读取出来的同时,CPU可以对这些数据进行解压缩,这样可以大幅度提高资源的利用效率。此外,按照我们的设想,在播放时应该也可以同时进行读文件和解压的操作,在这种实现方式都却不能做到。在这种实现下,为了播放音频文件连贯,必须一次性将全部音频文件读出并且解压缩,最后才能播放,漫长的等待时间非常影响用户体验。 -为了将各个操作并行地推进,我们很自然地联想到将各个核心模块全都加载到一个进程中,即为了运行这个MP3播放器软件,我们同时需要三个进程,这种设计思路如下图所示: +为了将各个操作并行地推进,很自然地联想到将各个核心模块全都加载到一个进程中,即为了运行这个MP3播放器软件,我们同时需要三个进程,这种设计思路如下图所示: ![mp3_version_2](images/mp3_version2.png) diff --git a/thu_os/chp15.md b/thu_os/chp15.md index 889fb99..b9c03c1 100644 --- a/thu_os/chp15.md +++ b/thu_os/chp15.md @@ -27,7 +27,7 @@ 为了提升系统的吞吐量,一方面可以通过减小系统内部的开销,如进程切换时的上下文切换开销,以及操作系统本身的开销。另一方面,则可以通过减小每个进程等待时间来实现。此外,操作系统需要保证吞吐量不受用户交互的影响,即在存在许多交互任务时,还是需要不时进行调度。 -而为了降低系统的相应时间,可以从调度算法的策略来着手,比如让交互式进程具有比较高的优先级,以及时处理用户的请求。此外,还需要减少平均响应时间的波动,让系统具有较好的可预测性。 +而为了降低系统的响应时间,可以从调度算法的策略来着手,比如让交互式进程具有比较高的优先级,以及时处理用户的请求。此外,还需要减少平均响应时间的波动,让系统具有较好的可预测性。 最后,调度策略还有一个公平性指标,比如在多用户的情况下,让各个用户几乎均分CPU的使用权。有一种策略是保证每个进程占用CPU的时间相同,但是这种策略显然经不起推敲,因为如果一个用户创建更多进程,他就可以获得更多的CPU时间。为了解决这个问题,真正的策略应该是保证每个进程的等待时间相同。需要指出的是,为了保证系统的公平性,往往需要付出一定的代价,比如平均响应时间会增加。 @@ -123,7 +123,7 @@ HRRN算法就是在每次调度时,总是选择响应比最高的进程,让 设想一种情况,在基于可抢占式的调度算法中,一个优先级较低的进程$P_1$当前占用CPU在执行,同时它还占用了一个共享资源`S`。此时有一个优先级很高的进程$P_2$在运行中申请共享资源`S`,由于$P_1$已经占用了该资源,$P_2$只能进入阻塞状态等待资源`S`。而这时出现了第三个进程$P_3$,它的优先级介于$P_1, P_2$之间,但是由于它并不需要资源`S`,$P_3$将抢占CPU进入运行。如果$P_3$的执行时间很长,那么$P_2$这个具有最高优先级的进程将不得不一直等待低优先级的进程执行,这就是优先级反置现象。 -为了解决这样一种情况,我们的想法是要占用资源的进程$P_1$迅速得到执行,以释放资源。为此,就需要暂时提升$P_1$的优先级,让它可以不被其他进程抢占。基于这种思想,就产生了下面的两种算法。 +为了解决这样一种情况,我们的想法是让占用资源的进程$P_1$迅速得到执行,以释放资源。为此,就需要暂时提升$P_1$的优先级,让它可以不被其他进程抢占。基于这种思想,就产生了下面的两种算法。 > 优先级继承 diff --git a/thu_os/images/proc_state.png b/thu_os/images/proc_state.png index 1c8625b..41738d9 100644 Binary files a/thu_os/images/proc_state.png and b/thu_os/images/proc_state.png differ diff --git a/thu_os/lab4_report.md b/thu_os/lab4_report.md index 69a2d43..c1be69e 100644 --- a/thu_os/lab4_report.md +++ b/thu_os/lab4_report.md @@ -134,25 +134,25 @@ proc_init(void) { alloc_proc(void) { struct proc_struct *proc = kmalloc(sizeof(struct proc_struct)); if (proc != NULL) { - proc->state = PROC_UNINIT; - proc->pid = -1; - proc->runs = 0; - proc->kstack = 0; - proc->cr3 = boot_cr3; - proc->flags = 0; - proc->parent = NULL; - proc->mm = NULL; - proc->tf = NULL; - set_proc_name(proc, "undefined"); - proc->need_resched = 0; - proc->context.eip = 0; - proc->context.esp = 0; - proc->context.ebx = 0; - proc->context.ecx = 0; - proc->context.edx = 0; - proc->context.esi = 0; - proc->context.edi = 0; - proc->context.ebp = 0; + proc->state = PROC_UNINIT; + proc->pid = -1; + proc->runs = 0; + proc->kstack = 0; + proc->cr3 = boot_cr3; + proc->flags = 0; + proc->parent = NULL; + proc->mm = NULL; + proc->tf = NULL; + set_proc_name(proc, "undefined"); + proc->need_resched = 0; + proc->context.eip = 0; + proc->context.esp = 0; + proc->context.ebx = 0; + proc->context.ecx = 0; + proc->context.edx = 0; + proc->context.esi = 0; + proc->context.edi = 0; + proc->context.ebp = 0; } return proc; } diff --git a/thu_os/lab5_report.md b/thu_os/lab5_report.md index 88ec38e..81f60a1 100644 --- a/thu_os/lab5_report.md +++ b/thu_os/lab5_report.md @@ -223,7 +223,7 @@ copy_range(pde_t *to, pde_t *from, uintptr_t start, uintptr_t end, bool share) { ### `COW`机制实现 -简单的版本的话,就是在`do_fork`函数中,只是让子进程拷贝父进程的地址空间。一旦父进程或者子进程试图写它们共享的空间,可以通过`Page Fault`机制,为该页新分配一个空间,并且修改对应进程的页表。这样,被修改的页就只有修改页的那个进程可以看到,对于其他进程修改都是不可见的。当然,要是具体实现的话,需要考虑许多问题,情况比较复杂,我以后再好好专研吧。 +简单的版本的话,就是在`do_fork`函数中,只是让子进程拷贝父进程的地址空间。一旦父进程或者子进程试图写它们共享的空间,可以通过`Page Fault`机制,为该页新分配一个空间,并且修改对应进程的页表。这样,被修改的页就只有修改页的那个进程可以看到,对于其他进程修改都是不可见的。当然,要是具体实现的话,需要考虑许多问题,情况比较复杂,我以后再好好钻研吧。 ## 练习3: 阅读分析源代码,理解进程执行`fork/exec/wait/exit`的实现,以及系统调用的实现(不需要编码) @@ -369,7 +369,7 @@ syscall(void) { 随后,该进程还不能完全退出,而是进入`ZOMBIE`状态,即僵尸态。这是因为,它在完全退出之前,还需要唤醒父进程,并且等待它的父进程对它的处理,只有父进程完成对它处理,或者父进程不存在,比如已经退出了,该进程才能退出。这也是这里的`do_exit`需要一个`error_code`参数的原因,这样父进程就可以根据这个错误码的不同值,来完成不同的处理结果。在父进程完成对它的处理前,该进程需要调用调度函数`schedule`,让出CPU的控制权。 -原理课上就讲到这么多,但是在ucore里面还做了进一步操作。因为当前要退出的进程有可能还有子进程,ucore还完成了这些子进程的移交,从而避免它们成为没有父进程的孩子。具体的操作就是将这些子进程【过继】给了第一个内核线程,其中主要就涉及到几个指针`proc_struct.parent/cptr/optr/yptr`的修改。需要指出的是,这里的`cptr`在进程具有多个孩子的情形下,总是指向最小的一个孩子。具体的代码如下: +原理课上就讲到这么多,但是在ucore里面还做了进一步操作。因为当前要退出的进程有可能还有子进程,ucore还完成了这些子进程的移交,从而避免它们成为没有父进程的孩子。具体的操作就是将这些子进程`过继`给了第一个内核线程,其中主要就涉及到几个指针`proc_struct.parent/cptr/optr/yptr`的修改。需要指出的是,这里的`cptr`在进程具有多个孩子的情形下,总是指向最小的一个孩子。具体的代码如下: ```c // do_exit - called by sys_exit @@ -590,12 +590,83 @@ copy_thread(struct proc_struct *proc, uintptr_t esp, struct trapframe *tf) { ## 进程的切换与退出 -之前我一直很迷惑,这里ucore的进程怎么实现切换的,最后又是怎么退出的,一开始感觉非常神奇,后来发现原来是最朴素的策略。原来这里根本就没有什么所谓的时间片轮转算法,没有抢占机制,一切的切换都是进程自己主动通过系统调用执行的。 +之前我一直很迷惑,这里ucore的进程怎么实现切换的,最后又是怎么退出的,一开始感觉非常神奇,实际上在`lab5`里面是实现了一个简单的`时间片轮转算法`,这里的时间片长度就是`TICK_NUM`个时钟周期: -例如一个进程切换到另一个进程,只有下面几种可能性: +```c +trap_dispatch(struct trapframe *tf) { + char c; + + int ret=0; + + switch (tf->tf_trapno) { + ...... + case IRQ_OFFSET + IRQ_TIMER: + if(++ticks == TICK_NUM){ + ticks = 0; + print_ticks(); + current->need_resched = 1; + } + break; +``` + +在设置了`need_resched`标志位后,在`trap`函数中进行进程的切换: + +```c +void +trap(struct trapframe *tf) { + // dispatch based on what type of trap occurred + // used for previous projects + if (current == NULL) { + trap_dispatch(tf); + } + else { + // keep a trapframe chain in stack + struct trapframe *otf = current->tf; + current->tf = tf; + + bool in_kernel = trap_in_kernel(tf); + + trap_dispatch(tf); + + current->tf = otf; + if (!in_kernel) { + if (current->flags & PF_EXITING) { + do_exit(-E_KILLED); + } + if (current->need_resched) { + schedule(); + } + } + } +} +``` + +此外,一个进程切换到另一个进程,还有下面几种可能性: + 主动调用`schedule`,实现进程的切换。 + 通过`do_wait`函数,在没有子进程退出时,会调用`schedule`来完成切换。 + 通过`do_exit`函数,进入`ZOMBIE`状态后,会调用`schedule`来完成切换。 -此外,进程的退出也是一样,都是自己调用了`exit`系统调用来退出的,这和我们正常的系统不一样,所以一开始不能理解。我们实际用的系统,对于我平时写的沙雕程序,应该是系统或者编译器自动在程序结束时添加了系统调用`exit`,来退出的。 +此外,进程的退出,是自己调用了`exit`系统调用来退出的,这和我们正常的系统不一样,所以一开始不能理解。我们实际用的系统,对于我平时写的沙雕程序,应该是系统或者编译器自动在程序结束时添加了系统调用`exit`来退出的。 + +## 内核线程与用户进程的创建 + +通过`lab4`和`lab5`的讨论,应该可以看到内核线程和用户进程的创建是具有比较大的区别的,同时它们也具有一些共性,在这里做一个简单的总结。 + +对于一般的用户进程的创建(并非第一个用户进程),都是通过它的父进程调用`fork`来实现的,这里应该保证子进程与父进程几乎是相同的,包括执行相同的程序,具有相同的内存,打开文件表等。为了保证父子进程执行同一个程序,在`copy_thread`中将父进程的`tf`拷贝到子进程的`tf`中,并且设置子进程的 + +```c +proc->tf->tf_regs.reg_eax = 0; +proc->tf->tf_esp = esp; + +context.eip = (uintptr_t)forkrets; +context.esp = (uintptr_t)proc->tf; +``` + +这样就可以使子进程中断返回后,是从父进程被中断的位置开始执行,并且`fork`的返回值是零。而父进程的返回值则是子进程的`pid`。 + +对于内核线程的创建(并非第零号内核线程),总是通过某个内核线程调用`kernel_thread`,来将该内核线程的执行位置设置为`kernel_thread_entry`,在其中再跳转到实际执行的代码。可以看到,内核线程的创建没有用到它的父亲线程的`tf`信息,而是通过`kernel_thread`硬构建了一个。因此内核子线程自然不会从父亲线程处开始执行。 + +但是无论如何,两种线程都需要在`do_fork`中调用`copy_thread`函数,设置其上下文信息为`forkrets`,此后再通过`iret`跳转到它们的起始执行位置。这是两种线程的共性。 + +而对于第零个内核线程和第一个用户进程,它们两者的创建却具有相同的思想,即都是硬构造出自己的`tf`帧,然后通过`iret`转入实际执行的位置。它们的区别仅仅在于跳转的位置不同而已,一个是内核空间,而一个是用户空间。 diff --git a/words.md b/words.md index 6b62a0f..175d4e1 100644 --- a/words.md +++ b/words.md @@ -1994,8 +1994,8 @@ Some Words - The committee will appoint an independent auditor to examine the annual accounts. + compulsive -> (adj)doing something wrong or harmful a lot and unable to stop doing it.
(not compulsory) -> (adj)If a film, play, sports event, boot, etc. is compulsive, it is so interesting or exciting that you do not want to stop watching or reading it. +> (adj)doing something wrong or harmful a lot and unable to stop doing it.(not compulsory)
+> (adj)If a film, play, sports event, book, etc. is compulsive, it is so interesting or exciting that you do not want to stop watching or reading it. - a compulsive liar/thief/eater - He was a compulsive gambler and often heavily in debt.