操做系统是一门重要的基础知识,了解这门基础知识不只能帮助咱们写出更优秀的程序,还能提升咱们的学习能力。当我发现有时候常常看不懂大佬的文章,听不懂大佬间谈话,看不懂项目文档的时候,我想我是时候补充一下基础知识了。本系列篇章内容基于Operating Systems: Three Easy Pieces的读后感,是一份操做系统知识的概括总结。linux
咱们的电脑使用一块CPU“同时”运行着各式各样的应用程序,操做系统经过分时共享的方式,让每一个程序轮流使用CPU,就好像每一个程序都有本身的CPU同样(就好像共享单车那样)。为了使CPU可以被各进程分时共享,操做系统要掌握分配CPU使用权的权利,同时也要履行服务好各项进程的义务。本篇文章就操做系统如何行使“权利”与履行“义务”作了一些概括总结——操做系统如何掌握系统的控制权?操做系统如何协调各项进程“共享”CPU?算法
操做系统是有“被害妄想症”的(事实上,它必需要有被害妄想症...),它不信任用户进程,总想着用户进程充满恶意,会阻碍系统的正常运做。因而乎,只有操做系统才有权限直接访问诸如内存,硬盘,以及其余系统资源。一但有用户进程试图越过操做系统执行这些“危险”的访问操做,该进程就会被杀死。让用户进程直接访问内存,硬盘等资源的确也是很危险的一件事,试想一下若是一个进程能够任意读取和修改其余进程的内存数据,那么基本上全部的进程都不能正常运行了。因此操做系统必需要限制用户进程的行为。编程
操做系统经过划分用户态和内核态来限制用户进程的行为:数据结构
为了区分用户态和内核态,操做系统须要硬件的帮助。CPU要提供某种权限机制,来区分用户态和内核态的访问权限。例如,x86 CPU提供4种特权级别(privilege level):0,1,2,3,等级越低权限越高(能够执行的指令种类越多)。在任一时刻,CPU都是在一个特定的特权级下运行,若是进程执行了不符合当前特权级别的指令,CPU会抛出异常,该进程会崩溃退出,从而起到保护做用。函数
然而,咱们的应用程序老是会须要访问内存以及磁盘的。但在用户态下,用户进程并无执行直接访问这些硬件资源的指令权限。用户进程须要经过操做系统提供的编程接口——系统调用(system call)与操做系统进行交互,由操做系统在内核态中来执行相应的指令。用户进程经过触发软中断(trap)让CPU陷入内核态,由操做系统在内核态处理用户请求。处理完毕后,操做系统经过执行return-from-trap指令似CPU返回用户态,将结果返回给用户进程,继续用户进程的执行。性能
当某些急切须要CPU处理的事件发生的时候,能够由软件或硬件触发一个中断信号,让CPU停下手头上的事情,立马处理当前触发中断的事件,中断处理完毕后,CPU将继续执行被暂停的程序。每一个中断都有一个中断编号,其对应的处理函数入口地址保存在中断向量表(interrupt vector table)中。在操做系统的启动阶段,操做系统会设置中断向量表,当中断产生的时候,CPU根据中断号查询中断向量表,从而得知执行哪个中断处理函数。能够对中断进行一个简单的分类:学习
int 0x80
汇编指令来触发系统调用软中断(中断编号为0x80)。假设系统调用的中断编号为0x80,CPU可以经过0x80得知这是一个系统调用,执行系统调用中断处理函数。可是咱们的系统调用中断处理函数还不知道究竟是什么系统调用,是读写文件?仍是分配内存?操做系统经过给系统调用编号来解决这个问题。当要执行系统调用的时候,用户进程会将系统调用编号(system-call number),以及调用参数,传给系统调用中断处理函数(经过CPU寄存器传递),后者根据系统调用编号执行相应的代码。编码
图片修改自 pages.cs.wisc.edu/~remzi/OSTE… Figure 6.2操作系统
在操做系统启动阶段初始化中断向量表,并告知CPU硬件中断向量表的访问地址。当用户进程执行系统调用的时候,CPU将部分寄存器信息保存到当前进程的内核栈中,切换至内核态,开始执行系统调用中断处理函数。系统调用中断处理函数根据系统调用编号执行相应系统调用,执行完毕后经过return-from-trap指令,恢复发生中断前的寄存器内容,切换至用户态,继续执行用户进程。指针
后文有对①②步骤的详细说明。
当用户进程正在CPU上运行时,意味着咱们的操做系统并无运行。既然操做系统没有在运行,那实际上操做系统是没有办法作任何事的。为了让操做系统可以夺回CPU的控制权,咱们再一次须要硬件的帮助——借助时钟设备来按期触发时钟中断(timer interrupt)。当时钟中断触发后,当前进程会暂停执行,CPU切换至内核态,执行中断处理函数(interrupt handler),此时,操做系统从新得到了CPU的使用权,根据进程调度算法的选择,它能够暂停当前进程运行,运行另外一个进程。
当进程从新得到CPU使用权的时候,CPU须要知道他上一次执行到哪里才能正确继续执行下去。当操做系统决定要执行进程切换的时候,会执行上下文切换(context switch)——保存当前进程的上下文信息,恢复即将执行的进程的上下文信息。
清闲的午后,你正在房间里看书(阅读进程)。忽然线上出bug了(中断),你夹好书签(保存上下文信息)合上书,火急火燎打开电脑开始排查(切换到修bug进程)。处理完成以后,回到房间继续看书。多亏合书以前夹好了书签,打开书你即可以继续上次的阅读了。
进程控制块(PCB)是操做系统用于描述一个进程信息的数据结构。进程的上下文信息保存在PCB中。PCB保存下列信息:
更多信息可参考:en.wikipedia.org/wiki/Proces…
操做系统为每一个进程独立分配一段栈空间,当进程因某种缘由(中断/上下文切换)暂停运行时,用于保存CPU或进程的上下文信息。当进程须要继续执行的时候,从内核栈中恢复这些信息。
若是仅仅是中断(例如系统调用),而未发生进程切换,其实并不须要保存进程的上下文信息。具体缘由在后续的图解中详细说明。
进程切换步骤如图所示:
图片修改自 pages.cs.wisc.edu/~remzi/OSTE… Figure 6.3
时钟中断的产生暂停了进程A的运行,由硬件将部分寄存器信息保存到内核栈,并切换至内核态。在时钟中断处理函数中,若是操做系统决定要从进程A切换到进程B(根据进程调度算法),开始调用switch()
方法执行上下文切换:
上下文切换完成后,操做系统执行return-from-trap指令,恢复进程B的寄存器,切换到用户态,开始执行进程B。
图中①②③④步骤都涉及到保存寄存器或恢复寄存器的操做。实际上,只有②跟③是属于上下文切换的范畴——从进程A的上下文切换至进程B的上下文。而①和④步骤由CPU硬件完成,由中断触发,跟进程切换没有直接关系。其目的是——保存CPU进入内核态前的状态,确保调用return-from-trap指令后,能恢复正确的用户态状态(进入内核态前的状态),其实也能够把这个步骤归称之为上下文切换,只不过这是CPU内核态与用户态之间的上下文切换(mode switch)。
操做系统经过区分用户态和内核态,配合CPU硬件提供的权限机制来限制用户进程的执行权限。用户进程只能经过系统调用来访问硬件资源(内存,硬盘等)。操做系统经过时钟中断来保持对操做系统的控制,使用上下文切换的方式来保障进程在CPU上正常切换。执行一次系统调用或上下文切换的代价仍是挺高的(CPU作了不少数据搬运的工做),在编码中,咱们能够经过减小系统调用的次数来提高程序的性能(好比合并读写操做)。