在第一章回顾了一些重要主题:CPU、内存、程序、进程后,第二章将正式开始操作系统系列主题,本章主要来讨论操作系统是如何来对进程进行控制的,以下为本篇目录。
警察与囚犯、操作系统与进程
操作系统如何限制用户程序
软件不够硬件来凑
系统调用是如何实现的
总结
在上一节《操作系统如何看待进程》中,我们已经知道,操作系统对于用户程序是不信任的,因此操作系统需要对用户程序进行严加防范,那么操作系统该如何做到这一点呢,这就涉及到了“控制与被控制”的问题,在这里被控制的一方是用户程序,而施加控制的一方是操作系统,同时“控制与被控制”是单向的,用户程序不能翻过来控制操作系统。要做到这一点,我们首先来看一个例子你就明白啦。
警察与囚犯,操作系统与进程
我们知道囚犯被关在监牢里面,其活动范围只能局限在监牢这个狭小的范围内,如果囚犯试图越狱的话会触发监狱的警报,这时警察就会立即将囚犯抓回到监狱(这里我们不考虑电影肖申克的救赎中安迪这类越狱的情况 :) )。囚犯如果有什么需求,那么只能通过监牢上的小窗口向外面的警察喊话。警察在听到囚犯的请求后,如果认这是合法的要求,那么警察就去替囚犯完成,如果警察认为这是不合法的要求,心情好的话警察可能对其不予理睬,如果心情不爽,那么可能会对囚犯教训一番。
在这个例子中,囚犯可以要求警察替自己完成某项请求,但是警察在得到囚犯的请求后是否采取行动以及如何采取行动完全是由警察来决定的,囚犯完全没有办法来控制警察是否采取行动,以及如何采取行动。就算是警察答应了囚犯的要求,囚犯也不知道警察整个行动的过程。
因此这里的重点就是:
囚犯是没有办法控制警察的
因为囚犯被关在了牢笼里。
在这个例子中:
囚犯就好比进程
警察就好比操作系统
监狱上的小窗口就好比系统调用
而牢笼就好比进程的虚拟内存空间(还记得操作系统的魔法吗,我们将在后面的章节中重点讲解)。
系统调用的设计思想基本上就是按照警察与囚犯的模式来实现的。
进程可以通过系统调用向操作系统发起请求,希望操作系统替自己去完成某项操作,但是操作系统怎么完成取决于自己而且不受进程控制,进程也不知道操作系统是如何完成请求的,这是现代操作系统实现进程、多任务、虚拟内存等重要功能一个基本前提。
因为操作系统是整个计算机系统的控制者(操作系统能访问整个内存地址空间,能够决定CPU分配给哪个进程,可以控制各种设备,比如磁盘,网卡,键盘,鼠标,等等),如果某个进程能控制操作系统的话,那么这个进程就绕过了操作系统进而控制了整个计算机系统,这显然是不合理的(比如该进程一直使用CPU而不分配给其它进程)。
因此操作系统必须通过某种方法来限制进程。该怎么做到呢?
操作系统如何限制用户程序
我们知道操作系统其实也是一个大的C程序,本质上和我们写的C程序没有任何区别。操作系统和用户程序都需要被编译成机器指令才能被CPU执行。因此当CPU执行的是来自操作系统的机器指令时,计算机表现出来的就是操作系统正在运行(比如操作系统收发网络数据)。当CPU执行的是来自用户程序的机器指令时,计算机表现出来的就是用户程序正在运行(比如浏览器正在加载页面)。
从这里我们可以看出,单纯的依靠软件是没有办法来区分操作系统和用户程序的,因为这二者本质上都是机器指令,因此要想限制进程必须还要依靠硬件的帮助。
软件不够硬件来凑
原来CPU在执行机器指令时是有各个工作状态的,比如当CPU工作在A模式时会受到限制,不能执行所有的机器指令(比如I/O操作类指令,这类指令被称为特权指令),只能访问部分的内存空间,但当切换到B工作模式下CPU满血复活,在该工作状态下,CPU可以执行所有指令类型包括特权指令,可以访问所有的内存地址空间,在这种工作状态下CPU可以解锁全部功能。CPU在A工作模式下被称为用户模式(User mode),在B工作模式下被称为内核模式(Kernel mode),CPU中有专门的寄存器用来记录当前CPU的工作状态。
操作系统正是利用了CPU区分工作模式的功能来达到限制进程的目的的。当CPU在执行用户程序的指令时,操作系统把CPU的工作模式设置为用户模式,在这种模式下,用户程序不能直接控制外部设备,不能直接发起I/O请求,不能访问超过自己范围的内存空间(被关在监牢中)等等。当用户程序需要进行I/O操作需要向操作系统发起请求时(监狱上的窗口)会进行系统调用,此时CPU开始执行一种特殊的机器指令,这种特殊指令是专门为实现系统调用而设计的。
在一般的CPU中有一种被称为trap的指令,执行trap指令后,CPU开始切换到内核模式执行操作系统指令。CPU会跳转到提前定义好的内存地址中,这里保存的是操作系统的代码,专门用来处理trap指令。接下来的过程就不受用户程序控制了,操作系统开始接管计算机,当操作系统接管计算机后,操作系统是信任自己的,因此操作系统可以控制所有计算机中的硬件(CPU、内存、磁盘、网卡等外设),可以访问整个内存的地址空间,可以决定是否继续让某个进程使用CPU(控制进程),可以向外部设备发起I/O命令。通过检查trap指令中请求类型,操作系统首先判断进程请求是否合法,如果请求合法,那么操作系统就替用户进程完成请求,把执行结果返回给进程,之后CPU再次切换回用户模式,在得到操作系统的返回结果后用户进程继续运行。
系统调用是如何实现的
在上一节中我们已经知道了,操作系统利用CPU不同的工作模式来限制用户进程。
在这种机制,用户进程完全无法控制操作系统如何来完成进程的请求。进程知道自己的代码段在哪里,自己的数据段在哪里,自己的函数调用栈在哪里,但是进程不知道操作系统的代码段在哪里,不知道操作系统的函数调用栈在哪里。进程也不能访问超出自己范围的内存空间(32位系统是用户进程可用的内存空间是3G),而且一旦进程访问了超出自己范围的内存空间,CPU就能检测出来进程在用户模式下试图访问超过自己权限的内存地址(好比越狱),这有可能是程序员的bug也有可能是恶意病毒,CPU检测到进程切图越权后就会开始执行提前准备好的一段操作系统代码,这段代码专门用来处理进程越界访问,操作系统开始接管计算机,不管进程出于什么样的原因越界,操作系统都会无情的终结掉当前进程,这就是著名的segmentation fault。所以下次你再看到这个错误就应该明白,是你的程序企图越狱而被操作系统kill掉了。
以trap指令为交界点,CPU执行该指令前处于用户模式,CPU执行该指令后处于内核模式。
有了这些基础,系统调用就很简单啦,作为用户程序向操作系统发起请求的窗口,系统调用在程序员眼里就一个普通的C语言函数,只不过系统调用被编译成的机器指令时和普通函数的机器指令不太一样。系统调用被编译出来的机器指令中包含trap指令,CPU在执行到trap指令时就知道这是用户进程在向操作系统发起请求。
总结
在这一节中我们已经知道了,系统调用其实是操作系统利用CPU的硬件特性来实现的。在程序员眼里系统调用和普通的函数没什么区别。同时在这一节中,为了给大家比较形象的介绍系统调用,整个调用过程的描述不是很严谨,因此在下一节中我们重点关注程序员应该理解系统调用的哪些方方面面。
计算机基础决定程序员职业生涯高度
--码农的荒岛求生
操作系统系列
基础篇 | 1,什么是程序 |
3,程序员应如何理解内存:上篇 | |
4,程序员应如何理解内存:中篇 | |
5,程序员应如何理解内存:下篇 | |
6,程序员应如何理解CPU:上篇 | |
7,程序员应如何理解CPU:下篇 | |
系统调用篇 | 8,操作系统是如何看待进程的 |
PS:微信公众号从去年开始限制了留言功能,如果你有任何问题欢迎直接在公众号留言。