OS development 开发经验(Hobby OS-deving系列译文)
[译者前言] 本系列译文译自 OSNews 网站的 Hobby OS-deving 系列,原文作者为 Hadrien Grasland,该系列文章的版权属于原文作者及 OSNews 所有。本文是该系列第一篇的译文,Hobby OS-deving 1: Are you ready?
自从开始开发我自己的操作系统以来,已经有一年了,这期间我经常停下来,回头看看我已经完成了什么,并好奇当初是什么原因使得项目的开头这么困难。我得出的一个结论是,虽然有大量的讲述操作系统开发的文档,但这些文档大部分都是从技术角度出发,在项目管理方面着墨不多。也就是说,如何从一个模糊的想法 (“我想编写自己的操作系统”),到一个精确的规划 (我希望实现什么),或干脆在撞上南墙之前放弃它。本系列文章除了旨在使那些对 OS 开发感兴趣的朋友能够走上正轨外,也希望能兼顾如上所述项目管理的一些方面。
您正阅读的本文将回答一个相当简单,却非常重要的问题:您真的准备好要进入 OS 开发了吗?很少有像编写 OS 这样耗时长,要求高还收获晚的业余爱好。大部分的个人 OS 开发项目最后都进入了一个死角,因为事先没有很好的理解要进入的领域,因此编写了大量很难维护并且设计糟糕的代码,导致项目无疾而忠;或者因为仅仅实现一个能工作的 malloc() 就耗费了大量的时间而意志消沉,并最终被击倒。
一些不好的想法
“我想引导下一次计算革命”:简单的说,不可能。详细点说,如果设想你自己是一个小的研发团队的话,你也许可以把某个东西做的很优雅,但也就是在一个小的范围内。如果没有一个大的组织或社区在背后支持,你的研究成果无法更大规模的普及。你也不可能通过实现所有 OS 的模拟来运行它们之上的程序以吸引更多用户,因为这实在是太困难了。
“我想精通某一门编程语言,我需要一些大的挑战来练习”:有些人会尝试通过开发一个 OS 来学习一门新的编程语言,他们认为通过完成某些挑战性的工作,可以真正的理解这门语言。坦白说,这并不是一个好的想法。
首先,这会让原本已经非常困难的 OS 开发更加困难,最好还是不要高估自己的能力,尤其在面对挫折和疲惫时。
其次,因为使用一门新的编程语言,你将不可避免的犯更多错误。而这些错误通常都发生在底层,你将无法使用那些用户空间程序可以使用的 GDB 与 Valgrind 等诊断工具。
再者,即使如此,你学到的也不是这门语言的全部,而只是其中一个很小的子集。除了极少数语言以外 (比如专为编写 OS 设计的 C),大部分语言提供的标准库将无法使用,大部分语言都有某些运行时功能,是你在开发 OS 的过程中至少某段时间内无法提供的,比如 C++ 的异常与 vtables,C# 与 Java 的垃圾收集与实时指针检查,Pascal 对象的动态数组与字符串,等等。更多相关细节可以看这里 (1, 2) 。
总之,你将不得不有一段艰难的时间来编写一些丑陋的代码,而这些代码无法让你去学习某门语言,还是放弃这个想法吧。
“实现一个新的内核复杂的可怕,不过我有更简单的方式!我只需将 Linux 源代码拿来,按我的需求做一些修改,这样做应该不会那么难。”:让我通过一个练习例题来告诉你 Linux 源代码 (比如 init/main.c) 实际上是什么样的,你的第一个目标是找出真正的 main 函数;第二个目标是试着理解它所调用的函数的定义位置,以及它们可能用来做什么;第三个目标则是估算一下完全理解这些函数所需要的时间。
你现在应该知道,如果对即将进入的领域没有一个非常好的技术上的理解的话,在已有的代码基础上进行修改将是一个十分困难的选择。代码重用当然是一个相当不错的可选之路,并且有许多优势,但并不会因此使你的生活更加轻松,相反还会更加困难,因为你不光要全面的学习内核的开发,还得深入细节的学习这些已有的代码。
“我不喜欢 Linux/Windows/Mac OS X 里实现的某个功能 X,这个功能应该重写”:所有现代的操作系统都有一些自己独特的怪癖,尤其是桌面系统,毫无疑问。不过,如果你的抱怨牵涉到的问题并不大的话,你应该考虑试着去修复它,并贡献给相应的项目,除非这样做失败了,即使重写最好也只重写这个功能牵涉到的功能模块。
“我正在寻找一个令人兴奋的商机”:参见上面第一条。除非你工作一个实验室里,并且专门研究操作系统,否则只要你开发的 OS 没有什么实际用途,那么你很难从它身上赚到一毛钱 (至少很长一段时间内是如此)。
一些好的看法
“我想尝试一个全新的 OS 设计”:你有自己独到的关于 OS 应该如何设计的想法,并且这些想法在当前的 OS 中找不到?那么开发自己的 OS 可以用来验证你的想法是否实际,并进一步完善你的想法。
“我已经熟练掌握了我现在使用的语言,我想找点编程方面的挑战,或者更深入的理解我的电脑在底层是如何工作的”:OS 开发当然能满足你的想法,甚至能提供更多你想不到的。你将学会如何在没有其它诊断工具的帮助下 (除了文本输出) 去调试代码;如何在没有标准库的环境下完成相应的功能;你将了解为什么有这么多人在抱怨 x86 体系架构,即使你的 x86 架构的电脑运行的相当不错;你将了解硬件生产商提供的文档到底有多烂…
“我已经在某个内核上工作了很长时间了,我非常清楚它的代码结构,以及那些令人头大的地方”:如果这些问题需要相当大的改动,或者可能带来不兼容,那么你除了在它的基础上另起分支外,别无选择。这样做当然也是一种正确的方法,甚至比从头重写一个 OS 要更加值得赞赏,不过本系列文章并不针对这种情况。
“我想对这个 OS 所做的改动太大,无法通过简单的补丁或开一个分支就能实现”:这当然是从头开发自己 OS 的最佳选项,如果你的想法确实如此,那么欢迎你来到这儿。
“我正在寻找一个新的令人兴奋的爱好”:这要看你如何定义“兴奋”二字,如果你准备好了的话,OS 开发绝对可以称得上是这个地球上最令人兴奋的爱好之一。打一个不太恰当的比方,有点类似于培养一个孩子,或者养活一颗树。这个过程无疑是漫长的,有时候甚至令人疲惫烦躁,以至于你感觉不值得。当然如果你能挺过去,那只会让你的成就感更强烈。此外你还能在这个过程中找到不一样的乐趣,你可以完完全全控制你的电脑。
那么,你准备好了吗?
在开始项目之前,除了上面的问答阶段以外,为了能够成功上路,你还必须知道或迅速学习如下的这些东西:
普通计算机科学:你必须非常清楚并且能够熟练的操作二进制数与16进制数,布尔逻辑,数据结构 (数组,链表,哈希表,等等),以及排序算法。
计算机架构:你应该知道一个桌面电脑的整体架构。The Words and expressions ALU,中断,内存与 PCI 总线,DMA 等应该不至于对你来说太陌生。了解其它一些更高级的技术比如:TLB,多级缓存,或者 branch prediction 将在你走的更远时大有帮助,比如你开始通过优化你的代码提高效率时。
编译器与可执行文件的内部结构:你必须大体上知道在预编译,编译以及链接阶段都发生了什么,并且能够在需要时从 toolchain 中找出你可以控制的地方。你还必须学会 linker scripting 以及可执行文件的内部结构。
汇编:你必须知道什么是汇编以及它是如何工作的,你必须能够编写一些简单的汇编代码 (操作栈以及寄存器,加减数字,实现 if-else 逻辑…)。你可以用其它语言来实现大部分的 OS 功能,但至少有极少数任务,你只能通过汇编来实现。
C 或 C++:即使你打算使用其它编程语言来开发你自己的 OS,你也需要了解 C 或者 C++,因为你需要参考的其它 OS 实现,或某些 OS 开发教程基本上都是使用 C 或 C++ 完成的。
你将要使用的语言:不管你是用 C#, Java, 汇编, BASIC, C, C++, Pascal, Erlang, REBOL 还是什么其它语言来开发你的 OS,你都必须熟练掌握它。实际上,你甚至应该阅读这门语言的内部实现的文档。举个例子,你应该知道使用你选择的语言进行函数调用最终在汇编那儿是如何实现的,你应该知道你的语言需要什么样的运行时支持并且如何来实现它。
(以上部分基于 OSdev wiki 上的一些内容,并做了一些修改。)
这就是本系列文章的第一篇所要讲述的内容。如果你已经完成本文中所要求的,那么恭喜你。你应该具备正常的心智与足够的知识来开始你自己的 OS 开发之旅了。在下一篇文章里,我将讲述如何为你的 OS 项目设置一个正确的目标以及期望值,这将是你最终到达目的地的至关重要的一步。
现在你应该已经参加了之前的测试,并且做好了开发操作系统的准备了是吗?这个时候,许多 OS-deving 的业余爱好者都会试图去寻找一份教程,并且按照教程的指示,手把手地完成二进制启动,文本输入/输出,或者其他“简单”的功能。如果说他们的开发有一个计划的话,那十之八九是这样的:他们会不时想到一些很酷的功能,然后把它实现。渐渐的,如果设想的没错的话,他们的 OS 系统就这样一个功能又一个功能的组建起来,并且慢慢的超过其它系统。我认为,这并不是获得成就最好的方法(如果你的目标是有所成就的话)。在本文中,我会用我的观点阐述在这个阶段你应该做什么并且为什么要这么做。
设计的重要性
首先,过于依赖教程有损创造性。不管教程作者怎样事先提出警告,大多数情况下我们都是在盲目的跟着教程走,而没有完全理解那些描述一个个步骤的冗长文字。为什么会有这种情况呢?因为很多网络上的低水平教程都只是描述如何去创建一个用 C 语言实现的,单内核结构的,具有一个简单 shell 的 UNIX 克隆系统。想到什么了吗?这太普通了,我们身边这样的东西已经泛滥成灾。如果你花费了大把的时间用于操作系统的开发,却仅仅只是实现了又一个 UNIX 系统,并且比现有的系统支持的硬件还少,那真是一种悲哀,当然这只是我的看法,也许你并不这么想。
不用这种方法的另外一个原因是,过早的将代码塞满大脑会让我们变得鼠目寸光。如果在编码前多花些时间来设计我们的项目,很多陷阱可以就此避免。比如异步 I/O 和多进程。如果在项目的早期你就决定要使用它们,并且能坚持下去,考虑到这方面已经有足够的文档,那么实现这些功能并没有什么本质上的困难。这只是换了一种思考方式而已。但是,如果你从一开始就缺失了这一步,而之后又想在你的代码中添加这些功能,转换地过程将非常痛苦。
最后,我认为设计非常重要(即使是针对业余项目)的最后一个原因是他们的队伍都太小。他们普遍只有一两个开发人员。当然这并不见得是件坏事情:在操作系统项目的早期阶段,你肯定不希望有无止境的争吵和成堆的各种风格的代码片段。不过缺点也很明显,除非你能专注于非常有限的事情并很好的完成它,否则你无法走得更远。这也是为什么我认为在这个阶段,比起直接编写代码,你更应该先决定好这个项目的目标和你将要做到什么样的程度。
探索动机
首先,忘记你是针对某台特定的电脑,甚至忘记你是正在编写代码。作为一个只有1-2人(或者更多人)的小组中的一员,你正在设计的是可以为他人(也许只是小组中的另一员)提供服务的某个东西。所有的一切都将从这里开始。你想提供什么样的服务?何时能说你的项目是成功的?何时又能说你并没有选对方法呢?就像许多其他个人项目一样,要想在一个业余操作系统开发中取得成功,第一步就是给它一个清晰的定义。
那么继续,拿出一张纸或者用其它的文本方式(我是用我的博客),开始探究你的动机吧。如果你编写操作系统只是因为对现存的某些操作系统感到不满的话,不如花些时间想想你不喜欢它们什么。试着理解这些地方为什么会这样编写,探索他们的演变过程,阅读在这方面你能找到的书。即使这些地方的实现看起来非常随意,你也能获得些什么:那就是实现某样东西不能太随意,并且要特别注意它们的设计。如果你是想通过编写一个操作系统来学些什么的话,那就要更加详细的明确你想学些什么。也许你是想知道过程抽象是如何得来的?通过怎样的过程使一个只是执行运算的复杂而无声的电子器件,到那些没有专业知识的人也能够使用的某个东西。
所有这些中,在不影响你思考的前提下,提取出尽量多的笔记。这样做有两个原因:首先,晚些时候你能回顾这些笔记。其次,如果你能坚持那些太模糊以至于不能直接用英语表达的想法(或者任何一种你能流利表达的语言),把思想转化为文字能使你想得更加严密。试着让这些笔记尽量的精确,就像这些文字将会被发表或者被你意想不到的人阅读一样。
定义目标受众
明确你为什么要这么做之后,尽量快速的定义你的目标客户是谁。操作系统是硬件,终端用户和第三方软件开发者之间的接口,所以你必须在一定程度上将他们全部定义:你的操作系统将在哪种硬件上运行?谁可能会使用它?谁会为它编写软件?编写哪种软件?在这个阶段你不用过于具体的去定义它们,但有些事情你还是需要现在就下决定。
在硬件方面:你至少应该知道计算机终端用户是如何与硬件进行互动的(键盘?移动定位设备?分别是哪个种类?),你想要覆盖多大的屏幕范围,你所支持的最老的 CPU 具有多强运算能力和应该保证的最小内存容量是多少。你也许还想立刻知道,你的操作系统是否需要连接网络才能正常工作,你打算使用哪种大容量存储器,以及如何安装第三方软件(如果有的话)。
迅速检查一下你准备为其编写代码的硬件类型是否是对自制 OS 友好的的。大多数的台式机和笔记本电脑都是(可能即将到来的 Chrome OS 上网本是个例外),视频游戏控制器则是参差不齐的(任天堂的通常是,其他的通常不是),苹果和索尼的产品除非快过气了通常也不是。关于手机的警告:尽管听起来是一个相当诱人的编码平台,但就一般而言,他们的硬件文档通常都极其差劲,并且你应当谨记,即使你找到这样一个平台,它能够运行自制的操作系统,并且网上能找到足够的文档,它的下一代产品通常也不能保证让你足够舒坦地运行你的 OS。
终端用户大概是最难去准确定义的了,除非开发人员只为他们自己设计。你或许应该针对你的目标人群的特定特征建立起形象的编号。举几个例子:“70 岁的老奶奶在圣诞节收到孙子送的电脑。她从来没用过,并且有视力障碍。”,“40 岁留着胡子的网络系统管理员,不会去使用没有提供 bash 风格的命令行界面的电脑,希望通过自动化操作完成他的一些工作,总想着超级给力的脚本引擎。需要多用户支持,以及一个只能由他管理,并且能够实现所有一切的特别账号。”,“20岁使用平板和自由软件画画的富有创造性的女孩。电脑同所有其他事物一样,只是个工具,它不应该比路上的一卷纸还碍事。”
第三方软件开发人员:首先记住如果你想让他们对你的计划有兴趣的话,你得先像吸引终端用户一样吸引他们。人们不会为他们不会使用的平台编写代码,除非是有报酬的。另一方面,开发人员有着特殊的地位,说明白点,他们创造软件。因此你的一项任务便是明确你想让他们创造什么样的软件。
从为你编写操作系统的重要部分(经常性的发生在自由和开源软件世界里的桌面操作系统),到那些只能在内部开发并且不存在第三方开发人员的软件(最近已经不流行,除非是某种嵌入式设备)。在这两种最极端的情况之间,有很多其它可能。这里就是一些…
- 操作系统部分的设计/管理是版权所有的,但是规格说明是开放的,所以第三方开放人员可以以他们想要的方式自由实现或者重新实现系统的一部分。
- 大多数的操作系统是以专利的和闭源的方式制作,但是你需要对第三方驱动和底层工具开放(Windows 9x,Mac OS)。
- 同上,但是你要求那些需要访问底层或者其他“危险”的功能的软件通过一些许可或者签名过程(最近的 Windows 和 Symbian 操作系统发行版)。
- 你不能忍受第三方的底层应用,并且用一切方法阻止他们的存在,但是你为那些需要经常调用系统 API 的应用提供原生开发工具。
- 同上,但是被认为危险的原生第三方用户应用必须通过签名/认证的过程(iOS)。
- 没有原生第三方应用,所有程序通过应用程序管理获得有限的系统权限(Android,Windows Phone 7,以及大多数手机)。
有了这些信息,你应该对于从哪儿开始有了很好的主意,并为下一步做好了准备。
目标和边界
既然想法已经在这儿了,尝试把它写到纸上。描述你的操作系统应该达到怎样的目标,谁来使用,运行在哪种硬件上,谁为它开发什么样的程序,以及何时你可以说根据最初的目标项目是成功的,也就是完成了版本 1.0 的发布。通过定义项目的目标,你就定义了一个可以客观判断你的项目是成功还是失败的标准,这将在后面被证明是非常有价值的资源,这也是避免功能无限制膨胀,以及其他浪费你宝贵开发资源的唯一的方法。
最后,从整体上回顾你现在的项目,问问自己这个简单的问题:“我能成功吗?”如果你觉得你也许应该稍稍降低下期望值,现在就是最好的时候,因为越晚改变,造成的损失就越大。好好研究你现在收集到的一切,仔细琢磨每一件事情直到你对你制定的项目非常满意了(或者完全放弃编写操作系统的想法,如果你已经不再有兴趣),那么你就可以开始下一步了,也就是设计你的内核。
既然你已经想好你的操作系统项目的总体规划,现在就是具体实施的时候了。如果你要从头开始的话,你需要设计的第一个操作系统组件就是内核,因此这篇文章的目的就是快速引导你进入内核设计,描述你需要思考的主要方面,并且指导你在哪里能找到关于这些主题的更多内容。
内核是什么,它又是做什么的?
我认为最好在这里做个定义。内核是操作系统的核心部分,它的作用是以一种可控制的方式将硬件资源分配给其他软件。有很多因素使得集中式的硬件资源管理如此引人入胜,包括可靠性(每个应用程序具备的控制能力越小,它在运行异常时造成的损失就越小),安全性(完全相同的理由,只是这次是在有目的地致使应用程序运行发狂时),以及一些需要系统范围内的硬件管理策略的底层系统服务(抢占多任务,电源管理,内存分配…)。
除了这些通常的考虑之外,大多数现代内核的主要目的实际上是管理进程和线程抽象。进程是被隔离的一个软件实体(isolated software entity),它能够以独占的方式访问有限的硬件资源,目的是防止并发灾难。线程是可以与其他任务并发同时执行的任务(task)。这两个概念彼此独立,虽然现代多任务操作系统中的每个进程至少拥有一个专属线程。
硬件资源
到目前为止,我还没有就“硬件资源”这个概念做深入的解释,当你读到这种表述时,你最先想到的可能是一些彼此完全不同的硬件实体,譬如鼠标,键盘,存储设备等等…
然而,你应该知道这些外围设备并不是直接与 CPU 相连的。他们都是通过总线到达某一个 CPU 端口。所以如果你想保证每个进程只能访问某些外围设备的话,内核就得必须控制总线。或者你也许决定将总线也视作一种进程必须请求才能获得的硬件资源。依赖于你的硬件资源模型的精细度,你在进程隔离以及内核复杂性之间的位置也会有所不同。
使事情更加复杂的是,现代操作系统还管理着一些非常有用的,尽管从纯硬件角度考虑并不存在的硬件资源。考虑内存分配的例子。从硬件的角度看,只有一个 RAM。你的计算机也许有多个 RAM 模块,但是你的 CPU 仍把它们视为一个单独的,大块的 RAM。而你通常想把它的一部分分给一个进程,而另一部分分给另一个进程。
为了让它工作,内核不得不根据每个进程的需要,拿出它最好的菜刀,把这块连续的内存切成更小的,能安全分配给不同进程的小块。这里还需要一些机制来防止不同的进程互相窥探各自使用的内存,这可以由不同的方式实现,但是最普遍的实现是使用一个嵌入在 CPU 中的特殊的硬件,也就是内存管理单元(MMU)。这个硬件使得内核可以控制让每个进程只能访问属于自己的那块内存,并且当内核从一个进程切换到另一个时,能够在不同进程的内存访问权限间快速切换。
另一个典型的硬件资源抽象的例子是 CPU 时间。我假定你早就注意到在多核芯片出现之前,桌面操作系统就能让你同时运行多个应用程序。操作系统保证进程能够以一定的方式共享 CPU 时间,通过使 CPU 频繁地从一个进程切换到另一个,使得这一切看起来就像在正常使用条件下同时执行一样。
当然,我们无法告诉 CPU:“嘿,小伙子,你能让 A 进程以 15 的优先权运行;让 B 进程以 8 的优先权运行吗?” CPU 是非常愚蠢的,他们只是简单的读取一条二进制指令,执行它,然后读取下一条指令,除非有中断使他们从当前的任务转移。所以在现代交互式操作系统中,正是内核保证中断正常的发生,并且当中断发生时,进程的切换也同时发生。整个过程通常称做抢占式多任务处理(pre-emptive multitasking)。
最后,通常也不能让进程直接访问存储设备,而是让他们访问文件系统的某个地方。当然,同分配的内存一样,文件系统也是一个虚拟的结构,它并没有物理成分在硬盘驱动器或固态硬盘中,并且在某些时刻必须由操作系统完全管理。
总之,你需要定义哪些硬件资源是内核管理的,并且可以让进程访问他们。通常是否允许进程访问某个硬件并不是件难事,折中地想,内核总是要完成相当一部分这样的管理工作,并且有时候内核必须无中生有出一些硬件资源,例如内存分配,抢占式多任务管理和文件系统操作。
进程间通信与线程同步
通常来讲,进程彼此间越独立越好。先前说到,封闭的沙盒环境让恶意软件难成大业,并且使系统可靠性大幅的提高了。另一方面,有些场合也需要进程彼此间能方便的交换信息。
典型的例子是客户端/服务器架构:在系统某处,一个休眠的“服务”进程正在等待命令。“客户”进程能够唤醒它并且使之受控地工作。在某个时刻,“服务”进程完成并把结果返回给“客户”进程。在 UNIX 的世界里,这是非常普遍的方式。另一个进程间通信的例子是有多个交互式进程组成的应用程序。
有几种进程间通信的方法,比如这些:
- 信号:进程间通信最基本的方式,类似中断。可以看做是进程 A 在进程 B 中“响铃”。这里的铃,称之为信号,有一个唯一的数字与之对应,除此之外,别无其它。进程 B 在那一时刻也许正在等待信号的到来,也许定义了一个与之关联的函数,以便当进程收到信号时,该函数能在内核生成的一个新线程里调用。
- 管道和其他数据流:进程也经常需要交换各种类型的数据。大多数的现代操作系统都提供这样的功能,尽管因为某些遗留问题,不少操作系统只允许进程以字节为单位交换数据。
- 远程过程调用:一旦我们能够从一个进程向另一个进程发送数据,以及发送信号来调用它的某个方法,那么结合这两项,允许一个进程调用另一个进程的方法(明显的在受控制的情况下)则是非常诱人的。这种方式使得我们可以像使用共享链接库那样使用进程,再加上与共享链接库不同的独特优点,调用进程也许能访问那些调用者本不能访问的资源,这也是一种使调用者获得受控制的资源访问的方法。
- 共享内存:尽管大多数情况下进程彼此独立更好,但有时候让两个进程共享一段 RAM 也是具有实际意义的,这使得它们能够在内核不介入的情况下做任何它们想做的事。这种方法通常用在内核内部以加快数据传输,以及当需要使用共享链接库时避免重复载入多次,但有些内核也把这样的功能公开给其他需要使用的进程。
另一件与进程间通信有关的问题是同步,也就是线程必须协调工作的情况。
要着手于这个问题,注意到在多任务环境中,在某些情况下,我们有时必须保证每次只有一定数量的线程(一般来说只有一个)能够访问所给的资源。举个例子,想象一下以下情景:两个字处理进程同时打开不同的文件,然后用户野蛮地决定打印所有东西,并且迅速在两个窗口中按下了“打印”。
如果没有机制去预防这种情况,接下来将会发生:两个字处理进程同时向打印机输出数据,互相干扰并且使打印机输出混乱,一般来说输出将混合了这两个文件的内容,可以说惨不忍睹。为了防止这种情况,我们必须在打印机驱动里的某处内置一个机制,使得一次只有一个线程能打印文件。或者,如果我们有两个打印机并且无论使用哪个都可以的话,我们可以使用一种机制来保证一次只有两个线程能打印文档。
通常的机制称作信号量,一般是这样工作的:在内部,信号量拥有一个计数器来表示一个资源可以被访问多少次。每当一个线程试图访问受信号量保护的资源时,这个计数器就会被检查。如果它的值非零,它就自减一,同时线程被准许访问这个资源。如果它的值为零,那么线程就不能访问这个资源了。请注意要完全确保该机制的健壮性,这个机制必须保证信号量的值在单个处理器指令下被检查和修改,而不是同时运行在多处理器核心上,这需要一些硬件上的帮助。这并不像检查和修改一个整数值那么简单,但是具体怎么去实现就不是现在我们这个阶段的事情了。
除了信号量机制,另一个较少使用却仍然闻名的同步机制是栅栏(barrier)。它使得 N 个线程必须等待所有线程都完成相应的工作才继续执行。这个机制在某些情况下特别有用,尤其是一个任务被分为几块并行执行,并且不一定能同时完成的时候(想象一下将一个三维图片分为几组进行渲染,然后用单独的线程分别计算它们的情况)。
总之,定义完你的进程模型,你需要定义它们之间如何进行通信,以及线程如何同步了。