10.操作系统演进过程
Posted PacosonSWJTU
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了10.操作系统演进过程相关的知识,希望对你有一定的参考价值。
【README】
1.本文内容总结自 B站 《操作系统-哈工大李治军老师》的《操作系统的那棵树》,内容非常棒,墙裂推荐;
2.思维僵化与发散
the mind is not a vessel that needs filing, but wood that needs igniting.
头脑不是需要归档的容器,而是需要点燃的木头。
3.发散思维(与各位共勉)
- 单向思维:灯丝材料出了问题,换其他材料吧;
- 发散思维:会不会是外部环境的问题,而不是灯丝本身问题。如 真空环境? 问题的维度发生改变;
【1】cpu运转起来
要管理cpu,就要使用cpu;
如何使用cpu?
为pc寄存器设置初值,然后cpu进行取指执行;
【2】cpu运转效率低
cpu执行一会,就需要等待一段时间,效率低;如 操作io,cpu需要阻塞等待io响应;
- 以操作磁盘为例,cpu向磁盘控制器发送请求磁盘数据请求,磁盘磁头需要寻址到具体位置,读取数据到缓冲区,准备好数据后,磁盘控制器才会通知cpu说数据准备好了;
在 磁盘被请求到磁盘准备好数据这一段时间,cpu都只能等待阻塞,所以效率低;
【3】 解决cpu运行效率低的问题
1)多个程序交替执行,解决cpu低效问题
- 在程序1阻塞时,cpu切换到程序2继续运行;
- 等待程序1阻塞结束后,再切换到程序1执行;
【注】本文程序指的是执行指令序列,其中执行序列可以称为线程,进程包含线程和执行资源;
【4】多道程序交替执行的问题
从程序1跳转到程序2,结合栈来修改cpu的pc寄存器值;
当使用一个栈来实现程序切换,从程序1某条指令A切换到程序2,切换回程序1时,无法正确切换到指令A的下一条指令;
【例1】cpu使用同一个栈进行多道程序切换(交替执行)的问题;
- 程序1-函数A: 地址100的函数A在调用函数B前,把104压栈(以便返回后继续执行下一条指令,即地址104上的指令,下同),接着调用函数B;
- 程序1-函数B: 地址200的函数B执行时,调用函数yield使得当前程序让出cpu给其他程序执行,先把204 压栈,再调用函数yield ;
- 程序2-函数C: 因为程序1调用函数yield,所以cpu切换到程序2执行;调用程序2的地址300的函数C;函数C先把地址304压栈,再调用函数D;
- 程序2-函数D: 执行函数D,先把地址404压栈,在调用函数yield 使得当前程序让出cpu给其他程序执行,如程序1;
【问题】函数D调用yield后,cpu会切换到程序1执行
- cpu执行程序1的下一条指令是栈顶弹出的404地址上的指令,而不是程序1的下一条指令地址204; 这显然是不对的,因为404地址上的指令是程序2的,这就会造成程序执行终止的情况,因为整个程序状态不正确,上下文不正确;
(例1 如上图)
【解决方法】基于各自栈的多道程序切换(交替执行)方式
- 每个程序各自单独使用一个内存栈,多个交替执行的程序互不影响;
- 为了管理内存栈,操作系统引入了线程控制块tcb,tcb存储内存栈基址,栈指针等栈元素;
- 对应地,函数yield修改为: 先找到程序2的tcb2,通过tcb2找到新栈2,进而切换到新栈2;
【小结】
操作系统引入的在多道程序中,每道程序使用单独的内存栈,解决了在用户态,多道程序切换的问题;
【5】内核态的多道程序切换问题
【5.1】背景
1)线程会从用户态进入内核态,内核态由于内存地址空间与用户态完全隔离,所以内核态无法查看到用户态的栈,也就无法切换到其他程序(进程或线程);
2)当内核态线程在执行过程中阻塞,cpu需要以某种方式切换到其他线程;这种方式就是 内核态的栈切换;
- 即 程序切换(进程切换或线程切换),需要切换一套栈,包括用户栈和内核栈;
- 其中用户栈在用户态的内存地址空间,内核栈在内核态的内存地址空间;
3)内核态线程切换步骤:
- Step1)用户栈1切换到内核栈1;
- Step2)通过内核栈1找到tcb1;
- Step3)tcb1切换到tcb2;
- Step4)通过tcb2找到内核栈2,并切换到内核栈2;
- Step5)内核栈2切换到用户栈2;从而完成内核态的线程切换过程;
【6】多道程序切换(用户态和内核态)的具体代码实现
【代码例子】
- 在屏幕上交替打印出 A和 B ;
1)业务C代码
main()
if (!fork()) while(1) printf(“A”);
if (!fork()) while(1) printf(“B”);
wait();
2)业务汇编代码
main()
mov __NR_fork, %eax // 系统调用编号
int 0x80 // 中断,展开后调用系统调用,进入内核
100: mov %eax, res // 子线程的eax是0,父线程非0
cmpl res, 0 // res 与 0 比较
jne 208 // res不等于0,则跳到208
200: printf("A") // 子线程代码
jmp 200
208: ... // 父线程代码
304: wait()
3)调用步骤
Step1)INT中断进入内核
Int 0x80 中断,调用 system_call
system_call: call _sys_call_table(%eax,4) |
Step2)system_call 调用 sys_fork ;
Step3)sys_fork 调用 copy_process ;
sys_fork: pushl …… call copy_process ret |
Step4)copy_process 代码细节
- copy_process根据父线程的模样做出了子线程,包括TCB,新的内核栈;
- 把TCB中的tss都初始化好,把用户栈与内核栈关联起来,tss存储了父线程执行时的物理寄存器的值,包括eip=100(父进程当前执行指令的下一条指令的地址),esp(栈指针),eax=0;
copy_process(... long eip, ...) // 参数列表为寄存器值列表
p = (PCB*) get_free_page();
p->tss.esp0 = p+4k;
p->tss.esp = esp;
p->tss.eax = 0;
p->tss.eip = eip;
...
Tss 赋值:
Tss->eip=100 | 父线程当前执行指令的下一条指令的地址; |
Tss->esp=p+4k | 根据pcb内存起始地址,偏移4k得到内核栈起始地址; |
Tss->esp=esp | |
Tss->eax=0 | 子线程tss的eax元素等于0,与父线程非0区分开; |
【6.1】父线程创建完第一个子线程后返回
1)业务C代码
main()
if (!fork()) while(1) printf(“A”); // 创建第1个子线程
if (!fork()) while(1) printf(“B”); // 创建第2个子线程
wait();
2)业务汇编代码
main()
mov __NR_fork, %eax // 系统调用编号
int 0x80 // 中断,展开后调用系统调用,进入内核
100: mov %eax, res // 子线程的eax是0,父线程非0
cmpl res, 0 // res 与 0 比较
jne 208 // res不等于0,则跳到208
200: printf("A") // 子线程代码
jmp 200
208: ... // 父线程代码
304: wait()
3)父线程调用fork 创建完第1个子线程后,接着调用fork创建第2个子线程;
4)最后,父线程执行wait() 等待,让出CPU,让子线程执行;
main()
......
wait();
C代码wait()函数的汇编代码:
mov __NR_wait
int 0x80
system_call:
call sys_waitpid
sys_waitpid() // exit.c 文件中;
current->state = TASK_INTERRUPTIBLE;
schedule(); // 调度
5)schedule()调度函数的汇编代码,调用switch_to() 函数
schedule()
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p) -> counter;
next = i; // 选择一个线程,作为切换到的目标线程
...
switch_to(next);
6)switch_to()函数
主程序运行线程A打印字符A;
但目标是交替打印A和B,而不仅仅打印A;
【6.2】交替打印A和B(时钟中断)
1)借助时钟中断,把线程A切换到线程B;
2)时钟中断C代码:
void sched_init(void) // 在 sched.c 中
set_intr_gate(0x20, &timer_interrupt);
void timer_interrupt:
...
call do_timer
void do_timer(...)
// 当前线程时间片计数先减去1,然后判断其值 是否大于0,若大于0则返回;
if ( (--current->counter > 0) ) return ;
current->counter = 0;
// 若等于或小于0 则 切换到其他线程
schedule();
【代码解说】
- 若 线程A时间片等于0,则切换到线程B打印字符B;
【补充】
- 只要为每个线程(如线程A,线程B)设置时间片初值;
- 每次调用该值都会减1;
- 减1后,若时间片值小于等于0,则切换到其他线程执行,进而实现线程交替切换,交替执行的场景,即交替打印AB;
【小结】操作系统演进过程
- 第一阶段: 让cpu运行起来;
- 第二阶段: 多道程序交替运行,解决cpu运行低效问题;
- 第三阶段: 引入了栈切换,每道程序独享一套栈(用户栈+内核栈),切换栈(用户栈+内核栈)达到切换多道程序的目的,使得程序可以交替运行;
- 也可以说,切换栈就是切换内核栈,因为 内核栈切换包含了 用户栈的切换;
- 第四阶段: 程序切换的触发条件有很多,本文引入了时钟中断来实现;
- 为每个程序1设置一个时间片初始值,时钟每拨一次,时间片值减1,若值等于0,则切换到其他程序2;
- 同样,程序2的运行也有一个上限时间片;一旦时间片等于0,则切换到其他程序3;
以上是关于10.操作系统演进过程的主要内容,如果未能解决你的问题,请参考以下文章