操作系统的那棵“树”---06
- 操作系统的那棵“树”
- 运转CPU
- CPU没有好好运转
- 得让CPU好好运转
- 从A跳到B我们并不陌生
- 一个栈 Yield造成的混乱
- 两个栈 两个用户TCB
- 一直在用户态那怎么行?
- 引入内核栈的切换
- 到实现idea的时候了
- 从用户代码开始
- 程序是什么?就是人的思维的C表达
- INT进入内核
- 开始sys_fork
- 开始copy_process
- 开始返回…
- main继续执行,现在我们有了什么?
- main继续,到了哪里?
- schedule
- switch_to切换
- 接下来会怎么样?
- 我们的目标是什么?
- 时钟中断
- 有那么一次时钟中断
- schedule switch_to
- 接下来会怎么样?
- 我们的目标达到了吗?
- 又有那么一次时钟中断, 再一次schedule switch_to
- 接下来会怎么样?
操作系统的那棵“树”
今天从一颗
开始,我们看看如何从小树苗长成一颗苍天大树。
运转CPU
CPU运转起来很简单,就是不断的从内存取值执行。
CPU没有好好运转
IO是个耗费时间的活,如果CPU在取值执行过程中,遇到了IO指令,那么必须等当前IO执行完毕后,才能继续取出下一条指令去执行,显然这种同步等待机制,并没有充分利用CPU的性能。
得让CPU好好运转
如何解决上面同步等待的问题呢?
- 程序间交替切换执行,程序1执行到IO指令阻塞时,切换到程序2执行
从A跳到B我们并不陌生
程序间交替执行,意味着程序间需要来回跳转执行,既然需要跳转,就需要保护现场和恢复现场,那么对应的就需要用栈来完成这两个任务。
一个栈 Yield造成的混乱
既然需要用栈来保存现场和恢复现场,那么处于节约内存考虑,一个栈够吗?
显然不行,大家自己推一遍过程就知道了,那既然不行,该怎么办呢?
两个栈 两个用户TCB
既然一个栈,那就两个栈,既然有了两个栈,随之就引出了一个问题,在两个栈切换时,如何知道当前栈的栈顶位置呢?
例如: esp栈顶指针寄存器一开始指向线程1的栈顶,但是此时要切换到线程2,那么就需要把esp指针移动到线程2的栈顶位置,那么线程2的栈顶位置搁哪保存呢?
为了方便切换时,找到对应线程的栈顶位置,因此由了TCB的诞生,TCB中保存当前线程栈顶位置和其他一些信息,因此如果要进行线程切换,首先需要通过下一个TCB,再通过TCB找到新的栈顶位置,然后将esp指针指向新栈顶位置。
一直在用户态那怎么行?
因为用户级线程的切换都是在用户态完成的,内核态是不知道当前进程中有多少个用户级线程的,那么一但进程1中某个用户级线程进入内核态后,产生了阻塞,那么此时是无法切换到进程1中其他用户级线程继续执行的,而是会直接切换到进程2继续执行。
引入内核栈的切换
既然用户级线程在用户态完成的线程切换,内核态看不见,不知道。
那么对应就有了在内核态完成切换的线程,即内核级线程。
因为通过中断进入内核态,再通过中断返回时,也是需要回到进入中断前用户态的状态的,那么就需要在内核态中设置一个栈,来保存中断进入时,用户态的状态,该栈就被叫做内核栈。
当然,因为内核态中会去进行系统调用,也需要调用函数,那么也会有保护现场和恢复现场的需要,因此肯定也是需要一个栈的。
有人问,为什么不直接使用用户栈来保存相关记录呢? 而非要在内核态再创建一个内核栈,不是浪费内存吗?
- 因为用户态和内核态本来就是两个独立的区域,并且用户态是无法直接访问内核态的,现在将内核态的相关记录放在用户态中保存,这合适吗?
随之就引入了内核栈,每个内核级线程对应一个用户栈,一个内核栈,并且因为切换是通过内核栈完成的,因此TCB中保存的是内核栈的栈顶位置。
- 线程1通过中断进入内核态,中断过程中会将用户态的相关状态压入内核栈,然后因为IO陷入阻塞状态,此时引起了线程切换
- 因为ESP此时指向线程1的栈顶位置,因此首先将ESP指向的内核栈顶位置放入线程1关联的TCB1中保存。
- 通过调度算法,找到下一个切换的线程2,然后首先得到该线程关联的TCB2
- 从TCB2中取出线程2内核栈栈顶位置,然后放入ESP中,此时ESP切换了指向,指向了线程2栈顶位置
- 线程2开始执行,然后最终会执行一条iret指令,弹出先前保存在内核栈中的用户态状态,线程2返回到了用户态继续执行
到实现idea的时候了
如果在屏幕上交替打印出A和B呢?
从用户代码开始
如果要写出交替打印A和B的程序,不就是创建两个进程,一个不断打印A,另一个不断打印B吗?
程序是什么?就是人的思维的C表达
如果把上面c语言,翻译成汇编形式,就是下面这样:
首先一上来先通过fork来创建一个进程,fork函数通过int 0x80号中断进入内核,下面看看他干了啥?
INT进入内核
int 0x80要进入内核态,中断过程中会将用户栈状态和当前标志寄存器,EIP,CS等都压入内核栈保存。
int 0x80会去进行系统调用,首先通过中断类型号0x80加上系统调用号,最终定位到sys_fork函数。
开始sys_fork
sys_fork最终会跳转到copy_process处执行。
开始copy_process
copy_process主要做的工作就是初始化PCB和当前进程对应的TSS,而新创建进程的用户态状态基本都copy父进程
包括一会该子进程开始执行的时候,也是直接从父进程进入中断时,压入栈中的EIP处开始执行,并且将eax设置为了0,这样就可以确保子进程去执行自己的代码,而不会与父进程执行相同的指令序列。
开始返回…
父进程执行完sys_fork后返回,返回后需要判断是否进入阻塞,时间片是否到期,然后这里假设这里父进程不满足切换条件,然后返回到用户态,继续去创建进程B。
main继续执行,现在我们有了什么?
进程B的创建和进程A一样,只不过此时进程A和进程B形成了一个进程就绪队列
main继续,到了哪里?
父进程创建完进程A和进程B后,进入等待状态。
wait函数,也会进行系统调用,底层会将自己的状态设置为阻塞态,然后进行进程调度。
schedule
假设此时调度算法,默认选中就绪队列中第一个元素,即切换到进程A执行。
switch_to切换
switch_to简而言之就是先将当前CPU状态拍到父进程的TSS中,然后再将进程A中的TSS状态信息拍到CPU上。
接下来会怎么样?
因为进程A的TSS中设置的初始EIP=100,并且eax等于0,因此当开始执行进程A时,首先判断eax是否为0,如果为0,则满足条件,跳转到208处执行,即不断打印A。
我们的目标是什么?
上面,我们完成了进程A的执行,进程A会不断在屏幕上打印A,那么我们的期望是A和B不断交替打印,那就需要让B进程也执行起来,然后A进程和B进程交替执行
时钟中断
加入时钟中断,每产生一次时钟中断,就把当前进程的counter–,当某次时钟中断发生时,当前进程–counter=0,说明当前进程的时间片用完了,需要进行切换。
有那么一次时钟中断
当进程A的时间片用完后,需要切换到进程B继续执行。
schedule switch_to
通过switch_to将当前CPU状态扣到进程A的TSS上面,然后将进程B的TSS拍到CPU上面,就完成了进程的切换。
接下来会怎么样?
接下来,进程B开始执行,然后不断去打印B
我们的目标达到了吗?
交替的打出A和B…
已经打出了B,完事了吗? 何为交替? 接下来会发生什么?把自己变成计算机想一想…
中断,仍然是中断…什么中断?
又有那么一次时钟中断, 再一次schedule switch_to
然后,当进程B打印了一会B后,有因为进程B的时间片到期,切换到进程A继续执行。
接下来会怎么样?
而接下来,就会重复因为时间片到期,进程间不断切换,从而完成A和B交替打印的结果