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.操作系统演进过程的主要内容,如果未能解决你的问题,请参考以下文章

灯丝温度对于紫外线灯击穿的影响

灯丝温度对于紫外线灯击穿的影响

分布式 - 分布式体系架构:IT架构的演进过程

饿了么:业务井喷时订单系统架构的演进

与“十“俱进 阿里数据库运维10年演进之路

早期操作系统的发展阶段(10k字)