操作系统理解
Roger的好奇心
Roger觉得操作系统是这个世界上最神奇的发明。他可以随意放大缩小窗口,随意拖动窗口到屏幕的任意位置,随意切换各个窗口,而这一切竟然都不会干扰到软件的正常运转。无论怎样放大缩小、位置移动、窗口切换,按钮还是那个按钮、文本框还是那个文本框。用暴风影音看电影的时候,你可以让迅雷慢慢下载另外一部电影。这时候,说不定QQ还会突然闪起来,告诉你有一个好友发了一条消息过来。Roger被好奇心折磨地寝食不安:谁能告诉我这一切是怎么做到的?
于是,他满腔热血地跑去听操作系统原理课程,希望揭开自己心中的谜团。主讲的教授确实是操作系统的专家,但是他的课程里充斥了进程、线程、文件系统一堆一堆的概念。一整个学期下来,Roger发现自己的问题依然存在。他仍然不知道操作系统是如何成功地管理众多的软件。他想找个同学讨论,但是大家对这个话题都不感兴趣,没人愿意绞尽脑汁去思考这么“深刻”的题目。
迷茫、疑惑纠缠着Roger。最后,他走投无路了,终于决定用最笨的方法获得好奇心的救赎:自己动手写操作系统!
一个人的战斗
没有战友的人是孤独的,但同时也是最强大的。在那些日子里,Roger为写出自己的OS殚精竭虑、埋头苦干。用天朝的一句话来总结就是,特别能吃苦、特别能战斗、特别能攻关、特别能奉献。终于,在经过七重地狱的煎熬之后,他终于完成了有四个进程同时运行的操作系统内核,随之而来的,是一次顿悟------一次关于运行环境的顿悟。
运行环境的本质
“运行环境”这个词大概来源于英文的run time environment。运行环境,是一个程序运行的环境(呵呵,貌似是废话)。
硬件系统与操作系统
为什么X86硬件系统(冯诺依曼体系)能够完美支撑操作系统?因为,它通过一系列机制提供了操作系统的运行环境。这个运行环境包括:DS/CS/ES/SS、通用寄存器组、GDT、LDT、IDT、RAM、ROM等等。实际上,从硬件系统的角度去看,根本没有操作系统的概念,更没有进程、线程、图形软件的概念。从硬件系统的角度看,从加电启动到关闭电源,自始至终只有一个程序运转在它上面,对于这个程序的内部细节(包括操作系统、进程、线程等概念),硬件系统一无所知。对于硬件系统来说,一个boot loader和一个linux系统完全没有区别。好了,说完硬件系统和操作系统的关系。再接下去讲操作系统、进程的关系。
操作系统与进程
操作系统如何支持多个进程看上去独立、并发地运行呢?因为它为每一个进程提供了一个运行环境。如果去阅读linux源代码,就会发现,操作系统为每一个进程维护了一张进程表。这张进程表的内容包括:DS/CS/ES/SS、通用寄存器组、GDT、LDT、IDT、RAM、ROM等等。呵呵,是不是很眼熟啊。是很眼熟,这张进程表,实质就是对硬件系统的一个完整映射!每当操作系统想去运行一个进程的时候,它就会先将当前进程的状态保存到进程表中,然后将目标进程的进程表数据加载到整个硬件系统上面。这样一来,进程完全被操作系统“欺骗”了,在一个进程看来,它自始至终都在不间断地运转,没有人打扰它。操作系统就好像一个容器,这个容器中,有很多“桶”,每一个桶中都有一个进程。当需要“运行”一个进程的时候,操作系统就将当前的“桶”从“插座”上取下来,放回容器中,然后将“目标桶”安装在“插座”上。(顺便提一下线程。操作系统使用线程表管理线程。每个进程都拥有自己的线程表。一个线程表的内容可能包括: SS/PC、通用寄存器组、LDT等。因此,在对同一个进程中的线程进行切换的时候,只需要切换将SS/PC、通用寄存器组、LDT。所谓的线程的context switch代价比进程小,就是这个意思。)
那么,操作系统如何在一个进程的运行过程中获取CPU的执行权呢,毕竟,进程不会主动将控制权交给操作系统。答案是:时钟中断。当中断发生的时候,硬件系统会终止当前代码的执行,转而执行中断向量指向的代码。操作系统通过中断函数,重新获取CPU的执行权。
第二次涅槃
Roger终于揭开了操作系统第一层厚厚的面纱,他内心的喜悦无以言表。现在,他对文章开头提出的问题有了更深层次的认识。这些问题的本质是:通信与数据分发。经过九九八十一天的修行,他百尺竿头更进一步,悟出了下面的道理。
通信
硬件系统将外部世界发生的事情告诉操作系统的过程,Roger将其定义为“通信”。那么,如何完成通信呢?答案是:中断。当Roger敲了一下键盘、或者点了一次鼠标、或者将U盘插在笔记本上、或者另外一台电脑向Roger的笔记本发送了一个网络包,所有这些事情,都会触发硬件的中断。每一种类型的中断都有一个唯一的identify id,如同每一个人都有一个身份证号。当一次中断发生的时候,X86在IDT(中断描述符表)查找这个中断对应的处理程序,然后将控制权交给这段程序(即操作系统)。通过这种方式,操作系统获取了外部世界的信息,如果它想发送信息给外部世界,只要使用汇编接口命令去操作键盘、鼠标或者网卡就可以了。
数据分发与“交互界面”
现在,OS获取了数据。如何分发给各个进程呢?Roger打算为进程提供一个操作系统API-----get_key_input()。进程如果调用了这个函数,则当Roger敲一次键盘的时候,就可以从返回值中获取输入的字符了。同时,Roger计划沿用linux的命令行输入风格,为用户提供多个console。对于一个使用了get_key_input()函数的程序,console会等待用户的键盘输入。在同一个console中,不能同时运行两个调用了get_key_input()的程序(你可以尝试在linux 同一个console中启动两个这样的程序,观察有什么反应,呵呵)。因此,当一个程序启动的时候,OS会为它关联一个console。如果程序内部调用了get_key_input()函数,那么OS就会将与它关联的console的键盘输入分发给这个程序。
抽象地看上面的console,其实一个“console”代表了“交互界面”的概念。每一个进程都有一组“交互界面”,例如键盘输入、鼠标点击、网络读写。如果将整个OS的键盘输入看做一个100平方米的大界面,那么其中20平方米可能属于进程A,另外50平方米属于进程B,剩下30平方米属于进程C。一次键盘输入的字符“落在”哪个区域内,OS就会将字符分发给所属的进程。
将我们的“交互界面”概念演绎开来。
在windows xp系统里,整个桌面是鼠标的交互界面。每一个区域都隶属于一个进程,OS负责维护这一套信息。每一次窗口的放大缩小、拖动、切换,OS都会相应地更新区域与进程的对应关系。这样,无论鼠标点击发生在什么地方,OS会确切地知道这个鼠标事件应该分发给哪一个进程。
键盘输入稍微复杂一些。OS会记录一个状态:键盘focus。例如,Roger用鼠标点击了浏览器窗口,键盘focus就会变为浏览器中的某个输入框或按钮(实际上,每个进程也会记录自己的键盘focus;当鼠标点到自己的时候,就会恢复之前的focus)。每次Roger敲了下键盘,OS会将输入分发给focus的那个进程。
网络简单一些,每一个网络连接都会以连接双方的ip地址、端口作为唯一标识。即 (ip_a:port_a,ip_b:port_b)。通过检查网络包中的ip和端口信息,就可以确定分发给哪一个进程。
同构的窗口软件
经过重重磨练,Roger终于彻底解开了自己对操作系统的谜团。这一天,一个学弟跑来提出一个问题:Roger,您能讲解下MFC的事件触发原理吗?说实话,Roger真没研究过这个问题。但是有了对操作系统的深刻认识,他仅仅思考了10分钟,便给出了答案。
MFC是什么呢?是一个桌面软件开发框架。使用MFC,开发者仅仅需要将按钮啊、输入框啊、菜单啊拖动到窗口中,加几个响应函数,一个桌面软件就宣告诞生了!对于初次接触MFC的同学们来说,又是一个神奇的东东。那么,其背后的原理是什么呢?
一句话:MFC的事件触发、操作系统的消息分发机制是同构的。
为什么说MFC、操作系统是同构的呢? 对应关系如下:
MFC ===操作系统 循环 ===中断 窗口组件 ===进程 |
操作系统通过中断捕获消息,MFC没有中断,它使用一个简单的循环。在每一次循环中,首先查询,是否有来自我的交互界面的消息啊?如果没有,继续下一次循环。如果有,取出每一个消息。例如,是一个鼠标点击。MFC检查自己的各个窗口组件,看鼠标点击“落在”哪一个组件上,就去调用哪一个组件的鼠标响应函数。例如,是一个键盘输入。MFC检查自己的键盘focus,focus在哪一个组件上,就去调用哪一个组件的键盘响应函数。
侯捷先生以前写过一本《深入浅出MFC》,乳臭未干的Roger同学当时看得头都大了。现在回头想一想,整本书不就是讲了个消息分发么……
总结
作为一个大龄计算机专业者,到现在才看清操作系统的这两点本质,实在有些惭愧。我相信,很多计算机界的牛人,在本科、高中甚至初中已经对此了如指掌。总之,学海无涯,希望好奇心能够一直驱动我前进。同时,如果能有同学愿意成为我的专业好友,共同钻研努力,我将不胜欣喜。