update lab4, lab5 report, update words.
This commit is contained in:
@@ -7,11 +7,11 @@
|
||||
|
||||
前面在物理内存管理部分,我有提到物理内存管理的一切目的,都是为了多任务的系统,否则对于单道程序的操作系统,直接让那单道程序独占内存和CPU就可以了,自然不需要内存管理,显然也不需要进程的管理。所以,进程概念的产生,其实还是处在多道程序的背景下的。
|
||||
|
||||
正是因为操作系统希望同时运行多道程序,而我们都知道程序运行需要资源,因此操作系统需要合理地把自己的资源组织管理起来,在程序运行的时候分配相应的资源,在程序退出的时候回收那部分资源。那么问题来了?操作系统怎么知道把资源分配给了谁?怎么知道资源的占有者什么时候退出?又怎么知道应该从哪里回收分配出去的资源?因此,需要对每一个运行的任务进行抽象。对每一个运行的程序的抽象,就是这里进程的概念。这样,进程就成了操作系统资源分配的基本单位。
|
||||
正是因为操作系统希望同时运行多道程序,而程序运行需要资源,因此操作系统需要合理地把自己的资源组织管理起来,在程序运行的时候分配相应的资源,在程序退出的时候回收那部分资源。那么问题来了?操作系统怎么知道把资源分配给了谁?怎么知道资源的占有者什么时候退出?又怎么知道应该从哪里回收分配出去的资源?因此,需要对每一个运行的任务进行抽象。对每一个运行的程序的抽象,就是这里进程的概念。这样,进程就成了操作系统资源分配的基本单位。
|
||||
|
||||
## 进程与程序的区别
|
||||
|
||||
我们都知道,程序其实就是编译后的代码,机器码,驻留在外存或者内存当中。那么进程呢?其实上面已经提出了,进程本质上就是运行的程序。这句话涵盖了它们之间的全部区别。
|
||||
程序其实就是编译后的代码,机器码,驻留在外存或者内存当中。那么进程呢?其实上面已经提出了,进程本质上就是运行的程序。这句话涵盖了它们之间的全部区别。
|
||||
|
||||
一方面,进程必然是包含程序的,毕竟它需要运行这个程序,但是程序只是进程的一部分。【进程是运行的程序】,这里的【运行】,就体现了它们之间的区别——既然程序要运行,就必然在代码以外还需要一定的数据,就必然需要占用一定的资源,比如代码和数据驻留的内存资源,就会涉及到程序当前的执行状态,比如一些上下文的信息,还有寄存器的信息。因此,可以概括出,进程是由代码,数据,各个寄存器以及占用的资源所组成的。
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
|
||||
> 状态队列的概念
|
||||
|
||||
前面已经提过,操作系统会为不同状态的进程分别维护一个单独的队列,即状态队列。现在我们知道,随着进程状态的切换,状态队列也会发生相应的变化,进程会从从一个队列加入到另一个队列。在进程状态模型下,状态队列的切换如图所示:
|
||||
前面已经提过,操作系统会为不同状态的进程分别维护一个单独的队列,即状态队列。随着进程状态的切换,状态队列也会发生相应的变化,进程会从从一个队列加入到另一个队列。在进程状态模型下,状态队列的切换如图所示:
|
||||
|
||||

|
||||
|
||||
@@ -109,13 +109,13 @@
|
||||
|
||||
设想一种场景。现在我们有一个MP3播放器的应用程序,该应用程序主要有三个核心功能模块,即从MP3音频文件中读取数据,将读取的数据解压缩,将解压缩后的音频数据播放出来。应该如何实现这个应用程序呢?
|
||||
|
||||
这里我们主要考虑的指标是音频文件播放是否流畅、连贯。为此,就需要设计的应用程序可以及时将刚读出来的压缩数据解压缩,并且播放出来。一种简明的设计思路如下图所示:
|
||||
这里主要考虑的指标是音频文件播放是否流畅、连贯。为此,就需要设计的应用程序可以及时将刚读出来的压缩数据解压缩,并且播放出来。一种简明的设计思路如下图所示:
|
||||
|
||||

|
||||
|
||||
在该设计思路中,我们将读取数据,解压缩,播放文件依次执行。但是不难看出,这种实现存在很致命的缺陷:各个模块之间不能并行进行。可以看到,这里读取压缩音频文件涉及到I/O操作,解压缩是CPU密集型操作,因此这两个模块在理论上是可以并发执行的,即在将压缩文件读取出来的同时,CPU可以对这些数据进行解压缩,这样可以大幅度提高资源的利用效率。此外,按照我们的设想,在播放时应该也可以同时进行读文件和解压的操作,在这种实现方式都却不能做到。在这种实现下,为了播放音频文件连贯,必须一次性将全部音频文件读出并且解压缩,最后才能播放,漫长的等待时间非常影响用户体验。
|
||||
在该设计思路中,读取数据,解压缩,播放文件依次执行。但是不难看出,这种实现存在很致命的缺陷:各个模块之间不能并行进行。可以看到,这里读取压缩音频文件涉及到I/O操作,解压缩是CPU密集型操作,因此这两个模块在理论上是可以并发执行的,即在将压缩文件读取出来的同时,CPU可以对这些数据进行解压缩,这样可以大幅度提高资源的利用效率。此外,按照我们的设想,在播放时应该也可以同时进行读文件和解压的操作,在这种实现方式都却不能做到。在这种实现下,为了播放音频文件连贯,必须一次性将全部音频文件读出并且解压缩,最后才能播放,漫长的等待时间非常影响用户体验。
|
||||
|
||||
为了将各个操作并行地推进,我们很自然地联想到将各个核心模块全都加载到一个进程中,即为了运行这个MP3播放器软件,我们同时需要三个进程,这种设计思路如下图所示:
|
||||
为了将各个操作并行地推进,很自然地联想到将各个核心模块全都加载到一个进程中,即为了运行这个MP3播放器软件,我们同时需要三个进程,这种设计思路如下图所示:
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -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$的优先级,让它可以不被其他进程抢占。基于这种思想,就产生了下面的两种算法。
|
||||
|
||||
> 优先级继承
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 47 KiB |
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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`转入实际执行的位置。它们的区别仅仅在于跳转的位置不同而已,一个是内核空间,而一个是用户空间。
|
||||
|
||||
4
words.md
4
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.</br>(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)</br>
|
||||
> (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.
|
||||
|
||||
Reference in New Issue
Block a user