Unix昔日之魂(三):不可修复设计
在本系列的第二篇文章中,我们记录了人们发现并不是很完美的两种设计模式,虽然并没有对其进行完整的修复,在不断的持续发展过程中,人们也已经在很大程度上修复了这两种设计。尽管有一些证据表明并没有犯原始性错误,但最终呈现的结果却没有像我们本可以做到的那么精致,这至少似乎表明当前的设计模式是足够好的,而且逐渐踏上了日益完善的路途。
然而,这其中仍然有一些设计错误存在并且难以轻易改正。有时候进行一个设计的修复会具备这样的特征,即对其进行的修复行为永远不会产生可用的东西。在这种情况下,我们可以认为最佳的处理方式应该是停止使用原有设计,去创建一个完全不同的、但可以实现相同需求的新设计。在本篇文章中,我们将会探索Unix系统的两种设计,人们已经对其修复进行了诸多尝试,但依然无法清晰的看到结果是否在向好的方面发展。一种情况是,方法的巨大转变会产生比原先设计更加简洁和多功能的新设计;另一种情况是,我们仍然对出现一个合适的替代品抱有期待。在对这两种“不可修复设计”进行探索后,我们将试图解决如何把一个最终发现可以修复的劣等设计和不可修复的设计区分开来。
我们的第一个不可修复设计主要涉及信号传递过程。特别之处在于当信号在被传递时,它是一个作为异步“信号处理”的注册函数。这种在某种程度上有缺陷的设计表明一个清晰的事实,UCB大学(加州大学伯克利分校,是BSD Unix系统的诞生地)的开发人员发现了引用sigvec( )系统命令和其他一些命令的需求,以让个人信号得以被暂时封锁。他们也改变了某些系统命令的语义,使得当信号在系统命令可用状态下到达时,可以实现重启而不是终止运行。
要试图解决这些改变似乎有两个特殊困难。首先,第一个问题在于究竟何时重新可用化信号处理器。在原始Unix系统设计中,一个信号处理器是一次命中的——即它只有在信号首次到达的时候做出反应。如果你想要获取随后的信号,那么你需要明确地使得信号处理器再次使自己可用。这将会导致竞争,比如,如果一个信号在信号处理器重新可用之前被传递,那么该信号就会永久丢失。要想结束这种竞争就需要创建一个设备使得信号处理器永远处于可使用状态,并且有信号正在被传递时阻止新的传递产生。
另外一个问题在于,如果一个信号在系统命令处于可用状态时到达,又该怎么办?可选择的方案有等待系统命令完成,完全放弃该系统命令,允许它返回部分结果或者允许它在信号被处理后再重该启命令。在不同的状况下每一种选择都会是正确答案,sigvec()试图提供更多的控制,能让程序员在这些方案中有所选择。
然而,即使是这些改变也不能够使信号真的可用,因此System V(在AT&T)的开发人员发现需要一个sigaction( )命令,以增加一些额外的标志去控制信息传递的细节完善,比如发送信号的UID程序。
由于这些改变,尤其是来自于UCB的改变,都聚焦于提供可信赖的信息传递,有人或许就会有所期待,至少可信赖程度的问题会被解决。然而事实并不总是如此。select( ) 系统命令(与poll()相关)并不能很好的进行信号处理,因此,最终不得不发明和应用pselect( ) 与 ppoll( )命令。我们鼓励对此感兴趣的读者去探索它们的历史发展。随着信号传递语义的不断丰富提高,所有的开发者组队选择去定义更多由不同事件产生的信号。虽然信号传递在它们被添加之前都非常的麻烦,但这很可能是新生需求在推动设计去找到突破点。
SIGCHLD 和 SIGCLD是一个有趣的例子,当子集存在或者母集为它wait( ) 时,SIGCHLD 和 SIGCLD会被发送。这两者之间的差别在于(除了字母H的差别和不同始发组的差别),SIGCHLD是每次只传递一个事件(正如其他信号一样),而SIGCLD 除非被阻塞,否则在等待子集时它会连续不断的传递信号。在语言硬件中断时, SIGCHLD是一种边缘触发,而SIGCLD 是一种层级触发。对于层级触发式信号的选择或许会是一种试图提高可靠性的替代性尝试。增加SIGCLD 比定义一个新数字或者在正确的时间发送信号要内容丰富的多。为sigaction()新添加的两个标志是为了处理信号时可以便于调整细节。这是信号并不需要的额外复杂性,并且通常认为这种复杂性并不属于这里。
近年来,信号类型的集合已经延伸到了包含“实时”信号的地步。这些信号是用户定义的信号(比如SIGUSR1 和SIGUSR2),而且在某种程度上,只有在有明确需要时实时信号才会被传递。首先,实时信号是一个队列,所以目标程序的信号处理器在信号被发送时会被多次调用。这和在传递过程中只设置标志的常规信号有所不同。如果一个程序有被给定的常规信号受阻,并且信号被发送了多次,然后,当程序解阻该信号后,我们仍然只能看到单一的传递事件,然而在实时信号中将会看到很多。这是一个很不错的理念,但是按照队列深度的话,引入的可靠性就会有所限制,信号也依然会丢失。其次(该属性需要第一个属性的支持),实时信号可以携带小数据,典型的是一个数字或者指针。这可以用sigqueue( )精确发送或者不直接发送,比如timer_create().
可以思考的是,这种为更多的事件增添更多的信号正是在本系列文章开篇之初就探讨过的“完整开发”模式的尚佳例证。然而,当增添新的信号类型需要对原始设计做出大量改变时,这等价于,似乎原始设计并没有足够强大到让我们充分开发利用。从这个视角可以看出,尽管原始信号设计相当简单和精致,它也确实命中注定有所缺陷。对重新配置信号的需求使得它们难以被可靠的使用,中断系统命令的确切语义也很难准确获得,并且开发人员需要反复拓展设计使它和新类型的信号相处融洽。
在信号的传奇故事中,最新的一个进步是signalfd( ) 系统命令,它已经在2007年被引入了Linux系统的2.6.22版本。这个系统命令拓展了“所有的事物都有文件描述符与之对应”的理念,该理念对信号也同样适用。使用由signalfd( )返回的新型描述符时,事件将会被正常的进行异步处理,信号处理器可以像所有的I/0事件一样被同步处理。这种方法使得大多数关于信号的传统问题都会消失。队列会变得非常自然,重新配置也将不会成为重要之事。与系统命令的交互变得不再有趣,一种显而易见的方式提供了额外的携带信号的数据。signalfd( )不是试图去修复一个问题重重的异步传递机制,而是用同步机制,这会更加容易处理并且能更好的和Unix系统设计的其他方面进行整合——尤其是文件描述符的普遍应用。
尽管很可能毫无意义,但想象这一过程是很有趣的:当首次发现问题时,结果很可能是它已经运用这种方法处理了信号。我们或许会有新的文件描述符类型而不是增添新的信号类型,并且使用的那些信号集或许会消失而不是成长。相反实时信号或许会成为一个普遍有用的,基于文件描述符的沟通进程形式。
应该被指出的是,有一些信号signalfd( )是无法使用的。包括 SIGSEGV, SIGILL,和一些由于程序试图做不可能完成的事情时产生的其他信号。在后期对要编程的信号进行编队时,唯一的替换选择是把这些控制转换成一个信号处理器,或者是终止程序。原始的信号设计能够完美的处理这些情况。当系统命令可用时(系统命令返回EFAULT而不是产生一个信号),这些情况不会出现,并且重新配置信号处理器时,这些问题也会变得不再相关。
所以,在一些早期的使用例子中(比如SIGSEGV)信号处理器是切实可行的,这似乎看起来它们很早就已经超越了自身的能力,才产生了有损设计并重复的试图修复它们。尽管现在已经可以写代码控制信号传递的可靠性,但仍然非常容易将它弄错。我们在signalfd( )中发现的更换定然会使得事件处理起来更加的轻松和可靠。
我们的第二个可以被替换的不可修复设计实例是控制文件访问的所有者/权限模型。被认为是H. L. Mencken 众所周知的名言“总是存在一个每个人都家喻户晓的解决问题的方法——巧妙,合理但错误。”这对计算机的问题也同样适用,而且Unix系统权限模型可能恰好也是这样的一种方法。最初的理念有一种迷惑性的简单:每个文件6字节并可以提供简洁宽泛的权限控制。当设计一个安装在32KB(或者更少)的RAM里的操作系统时,这种简洁性是非常吸引人的,考虑或许有天它如何可以被拓展并不是当务之急,尽管有些不幸,但这也可以被理解。
这种权限模型最主要的问题在于太过于简单和宽泛。事实上可以看到这种模型的宽度在于,每个文件都存储了它的所有者,组类所有者和权限位。因此每个文件都有独特的所有权或者是访问权限。这比它本身所需要的弹性度要大得多。在大多数情况下,所有的文件都会被给定目录,或者目录树有相同的所属权与相同的权限。这一事实在Andrew文件系统中有所利用,该文件系统只在单位目录数据中存储了所属权和访问权限,而且不会有功能丧失。
当它只需要每个文件腾出6个字节的空间时,为灵活性而付出的代价或许似乎会很小。然而,当所需的不相同所有者超过65536,或者需要更多的权限位和分组时,存储这些信息将会产生巨大的成本。然而更大的成本也具有可用性。
当计算机能够很轻松的记住每个文件的六个字节时,人们却无法非常容易的记住为什么要安排这些各种各样不同的设置,并且很可能会创建一系列不连续的、不正确的因此也不会特别安全的权限设置。作者会有在大学里通常被给定根目录权限为“0777”(每个人都有访问权限)的记忆,这仅仅是因为需要与朋友共享一个文件,但他并不理解什么是安全模型。
在固定的、小位数的权限位中可以看到Unix系统权限模型的过度简化,并且特别的,它只有一个拥有优先访问权限的组类。另一个源自于Alan Kay的计算机工程准则是“简单的东西本应该简单,而复杂的东西应该变成可能。”Unix系统的权限模型最大化的利用了简单,但是一旦需求超过了一系列惯例,进一步的提升将会变得无法实现。这时候简单确实是简单,但复杂已经绝对变成不可能了。
此时我们开始探寻对“修复”模型的真正努力。原始设计为每个程序给定了“"user"”和“"group",并在每个文件中有与之匹配一致的 "owner" and "group owner",它们用来确定访问路径。“唯一组类”的限制在于它限制了两方面;在程序这一方面,UCB的Unix系统开发人员发现这种限制很容易被扩大。它们允许程序拥有一个检查文件系统的组类清单。(不妙的是,这个清单最初有一个严格的上限16,这个上限限制了它进入NFS协议,这很难改变并且至今仍然令我们很苦恼。)
要改变单一文件的这种限制非常困难,因为这需要在文件系统中改变数据的编码方式,允许多重组类的单一文件。由于每个组类也需要自己的权限位,所以每个文件将需要一个组类清单和权限位,这些理所当然的被认为是“访问控制清单”或者是“ACLs”。POXIS标准化进行了诸多尝试去创建标准ACLs,但是过去一直都没有到提上草案的阶段。一些Unix系统安装启用时实施了这些草案,但是普遍都没有成功。
NFSv4工作组(处于IETF保护伞下)过去在其他目标中选择攻克创建一个可以在POSIX和WIN32系统里提供互操作性的文件系统。在此番努力过程中,他们还发展了ACLs的另一个标准,可以支持WIN32系统的访问模型,但在POSIX系统中仍然不可用。这是否会更进一步发展扔有待观望,但这似乎是一个非常合理的势头,一个积极的计划试图将它整合入Linux系统(在"richacls"的旗帜下)和各种各样的Linux文件系统。
使用ACLs的结果之一是,需要存储权限信息的单一文件存储空间不可以大于六个字节,也不是固定的长度。通常来说,这比任何的固定大小要更具有挑战性。这些安装启用了ACLs的文件系统确实使用了“拓展属性”,并且大多数都限制了大小——每个文件系统都选择一个不同的限制大小。愉快的是,大多数实际使用的ACLs可以适合所有的属性限制。
一些文件系统——至少是ext3——尝试发现,多样化文件具有相同的拓展属性并只存储这些属性的一份拷贝,而不是存储对每份文件的拷贝。这在某种程度上节约了较大的可以成为特殊单一文件(通常并不是)的ACLs的空间成本(也节约了访问时间成本),但是对解决之前提过的可用性并没有任何作用。在那篇文章中, 非常值得引用Samba的主要开发者之一Jeremy Allison和他对WIN32系统ACLs关于互操作性的一些经验。他写道:“Windows ACLs是超越人类理解力的噩梦,它太复杂了以至于无法可用。 ”这篇文章非常值得一读并且可以追寻到一个正确的理解,要记住的是richacls,就像 NFSv4 ACLs很大程度上是基于WIN32 ACLs的。
不幸的是,没有办法呈现任何确切的代替Unix系统权限模型而不是修复它们的例子。一种竞争者或许是SELinux处理文件访问的一部分。这并不是目的在于替换通常的权限而是努力去提高它们的强制访问控制。SELinux 与Unix权限系统很大程度上一脉相承,伴随着每个文件利益的安全协议,并且在提高可用性的问题上没有任何作用。
然而,这里的两个局部方法或许可以提供某种视角。一种局部方法随着chroot( )系统命令在Level 7 Unix中开始出现。这看起来似乎chroot( )并不是最初为了访问控制而创建,而是为了获取一个独立的命名空间去为分布创建一个干净的文件系统。然而,它已经习惯于被用来提供某种层次,尤其是为匿名FTP服务器的访问控制。
在Linux系统中已经按照每个程序的可能性加强了这种概念,不仅仅是拥有自己的文件系统根,而且有私人设置的挂载点,通过这个去创建一个完全自定义化的命名空间。而且,这在一个命名空间中对给定的文件系统进行挂载读写,在另一个命名空间中只读是可能的,显而易见,两个不可能在第三命名空间中实现。这种功能是一种非常困难的控制访问权限方法的暗示。它允许成为单一挂载而不是增加控制成为单一文件。这导致文件地址成为了决定如何能被访问的重要部分。尽管这减少了部分灵活性,但似乎可以成为一种理念——人类的经验之谈可以帮助我们更好的理解事物。如果我们想要保存一个私人文件,我们或许会把它放进一个带锁抽屉。如果我们想要让它为大众可读,我们会分发复印件。如果我们想要让它在团队内可以被任何人书写,我们会把它钉在团队公告栏上。
这种方式清晰表明它并没有Unix系统模型那么灵活,作为权限控制也没有那么精细,但它可以很好的对其弥补,变得更加易于理解。当然,对其自身而言,这种方式并不是一个很好的替换,但它似乎正在向功能性成长,但如果它将会有突破自身性的发展,那么对它进行区分还为时过早。激动人心的发现在于,这种方式是基于我们探讨的第一种模式中所观察到的Unix系统的众多优势之一,即可以被完整开发的“分层命名空间”。
一种与之不同的局部方式可以在使用Apache Web服务器的访问控制中看到。这些都是用特定领域语言进行编码和存储在被控制的文件附近的集中化文件或者是".htaccesss" 文件中。这种访问控制方式具备大量优点,对于编码入任何基于Unix系统权限模型的事物都会是一个有力挑战:
权限模型是分层的,并匹配着文件系统模型。因此可以在最有意义的地方设置控制,并且可以在其整体中轻易进行评估。当这些控制设置在高层无法在低层进行解除时,实行强制访问控制就会信手拈来。
请求访问的身份可以是任意的而不是仅仅来自于被内核所知的身份集。Apache允许基于源IP地址或者加密码的用户名。任何其他使用插件模型的都处于可用状态。
通过一个CGI程序可以间接的提供访问。于是,模型可以通过写一个合适的脚本模拟该访问使得任意行为可以被控制,而不是试图对所有可能的访问限制进行二次猜测,这些访问限制或许是令人满意的并且在新的ACL中规定了权限字节。
这非常显而易见,这种模型将不会轻易的和基于内核的访问检查契合,并且在任何情况下,这种模型将会比简单的模型拥有更高的执行成本。如此而言它将不会适合于被广泛应用。然而,这种模型将适合于小规模的需求并且基于方法,它不会契合于单一命名空间。这种成本对于灵活性而言或许会是个合理的价格。
尽管像这样的替代方式或许会非常吸引人,但在引进过程中它将会比signalfd()的引进面临更大的障碍。对信号处理器而言signalfd()可以作为一个简单的替换被添加。在新程序充分利用新功能时,程序可以继续使用旧模型而且没有任何损失。对权限模型而言,同时运行两个计划并不容易。惯于使用ACLs的人们很可能已经很认真地调整了一系列ACLs使得它们适应自己的需求,而且,一个可同时替换的访问机制很可能损坏一些东西。所以,这需要在新的安装环境中进行调整的事而不是强加在现有的用户基础上。
如果我们有一个可以令人信服的“不可修复设计”模式,就有可能将它们和可修复设计区分开来,比如我们上次发现的那些设计模式。在所有情况中,每一次个人修复似乎都是解决真实存在的问题的好主意,也不会明显导致更多问题产生。在某种情况下,这一系列小步骤可以产生一个很好的结果,然而在其他情况下,这些步骤只会帮助你得出足够多的过去那些问题以至于可以发现更大的问题。
我们可以用数学术语来说明一个局部最大值与整体最大值是完全不同的。或者,用登山术语来说,从一个只能提供一座山峰较好视野的错误峰顶是很难识别真正的峰顶的。在每个案例中,丢失的片段都会是一定规模的视角。如果我们能够看到全局画面,我们将可以更加容易的确定是否一条特别的路径会到达有用的地点或者是否掉头回归基础和重新开始是最佳方式。
试图将本次讨论追溯到软件工程的领域后,明确的是如果我们找到可以提供一个清楚和宽泛视角的位置,我们将只能阻止不可修复设计。我们需要能够超越当即的问题去看到更大的局面并准备好把它解决掉。唯一所知的软件工程视角来源是我们已有的经验,但在我们之中很少有人具备足够多的经验将多样化的事实和抽象层次分析的很清楚,而这正是做出正确决策的必需。无论我们是通过咨询前辈获得经验,还是通过研究众多相关的工作,或者是找到其记录模式将他人的经验进行概述,能够利用任何可用的经验都是至关重要的,而不是冒着风险简单的将急救绷带贴到一个不可修复设计上。
因此,并没有简单的方式将一个不可修复设计和可修复设计区分开来。这需要只能通过可用的经验去利用广阔的视角才能实现。在早期看到鉴别不可修复设计的困难时,我们期望在本系列文章的最后能够在有问题的设计中探索一种致命的模式。在不可修复设计表现的需要修复时提供了大量的更深层次问题的线索,但随之这些设计并没有提供这种线索。这些更深层次问题的线索必须在其他的地方得以发现。
【版权声明】
原文作者未做权利声明,视为共享知识产权进入公共领域,自动获得授权。
......