Linux 内核贡献指南
每个了解 Linux 的人都知道 Linux 与其他更商业化的操作系统“不同”的方式。由于 Linux 内核是开源的,因此每个用户都有可能成为贡献者。当然,几乎每个阅读本文的人都知道这一点;这有点像对唱诗班布道。然而,事实是,大多数 Linux 用户,即使是那些精通编程艺术的用户,也从未为 Linux 内核的代码做出贡献,尽管我们大多数人都曾有过这样的想法:“哎呀,我希望 Linux 能做到这一点……” 通过本文及其他文章,我希望说服你们中的一些人以一种新的、更积极的眼光看待 Linux 内核。
有哪些不为内核工作做出贡献的合理理由?首先,也许你在法律上不允许这样做。许多程序员签署的合同限制了他们在工作之外编写代码的能力,即使是对于非商业项目。这是我选择一个与编程关系相对较小的职业的主要原因,除了偶尔的 Perl 脚本。其次,你可能不知道如何做。许多 Linux 用户是相对较新的程序员,接受过传统计算机科学的培训。我从自己的计算机科学教育中了解到,许多学校倾向于教授“现代”编程技能——在我的特定学校里,我是少数几个选择(或知道如何)在没有 IDE(集成开发环境)的情况下进行编程的人之一。可悲,但却是事实。第三,许多专业程序员现在倾向于在工作场所使用版本控制系统,并且可能不愿意为仍然使用“裸机”方法的项目(例如内核开发工作)做出贡献。最后也是最有可能的原因是,许多有能力破解 Linux 的程序员没有时间这样做。这些都是完全合格的程序员,他们有好的想法、新鲜的视角和贡献的愿望,但却选择不贡献的有效理由。我所说的一切都无法帮助他们克服其中的一些问题,但我希望我能使内核编程至少对一小部分人来说更易于接受。
这是该系列文章的第一篇,我将尝试消除版本控制背后的一些神秘感。许多开源项目,包括 Linux 内核,出于各种原因仍然使用 diff 和 patch 方法进行内容控制。大多数开源项目仍然接受这种格式的补丁,即使他们通过 CVS 或其他版本控制系统分发代码。首先,diff 和 patch 为项目维护者提供了极大的控制权。补丁可以通过电子邮件和纯文本提交和分发;维护者可以在补丁接近代码树之前阅读和判断补丁。其次,永远不用担心访问控制或 CVS 服务器宕机。第三,它很容易获得,通常不需要任何未作为每个 GNU 系统一部分分发的特殊工具,并且已经使用了多年。然而,简陋的版本控制使得跟踪更改、维护多个分支或执行 Perforce、CVS 或其他版本控制系统提供的任何其他“高级”操作变得困难。
diff 和 patch 是一组命令行程序,旨在生成更改并将更改集成到源代码树中。GNU 实用程序支持多种“diff”格式。diff 和 patch 优于较新的版本控制系统的一个主要优点是,diff,尤其是 统一 diff 格式,允许内核维护者轻松查看更改,而无需盲目地集成它们。
对于不熟悉的人来说,diff 和 patch 只是完整 GNU 实用程序集中的两个命令。虽然它们在实践中最常用,但在特定情况下经常使用其他工具。就本文档而言,我不会专注于这些实用程序,而只会简要地介绍它们。要获得更全面的了解,请查看您本地的 man 和 info 页面。
diff 是集合中的第一个命令。它有一个简单的目的:创建一个文件(通常容易混淆地称为补丁或 diff),其中包含两个文本文件或两组文本文件之间的差异。构造这些文件是为了便于将差异合并到旧文件中。diff 可以以多种格式写入,尽管通常首选统一差异格式。此命令生成的补丁比整个文件更容易分发,并且它们允许维护者快速轻松地查看更改内容并做出判断。
patch 是 diff 的逻辑补充,尽管奇怪的是,它在 diff 相对普遍使用之后才出现。patch 接受 diff 生成的补丁文件,并将其应用于文件或文件组。patch 会在发生冲突时通知用户,尽管它通常足够智能来解决简单的冲突。此外,patch 可以反向操作;使用更新的文件和原始补丁,此命令可以将文件恢复到其原始形式。
cmp 是 diff 用于二进制文件的对应命令。由于二进制文件在源代码控制中的应用受到限制,因此该命令通常不在此环境中使用。通常,包含二进制文件(例如,徽标)的项目具有一些其他机制来更新这些组件。(请记住,Linux 应用程序中常见的 XPM 图像格式实际上是基于文本的,可以使用上述命令进行控制。)
diff3 是 diff 的变体,允许计算和合并三个文件之间的差异。就我个人而言,我倾向于使用 diff 来达到这些目的,但是可能有一些原因使得这个命令在某些我尚未处理过的特定情况下很有用。
最后,sdiff 是 patch 的交互式版本,允许使用您自己的大脑进行更智能的修补。
除了内容控制之外,这些工具还有许多用途。我不想暗示它们没有用而轻视它们。但是,像许多工具一样,它们只在某些情况下才闪耀。(就像你在套装中得到的那种烦人的精细螺丝刀,你从未见过足够小的螺丝可以使用它,工具的价值仅取决于它们应用的环境。)
在讨论不同的补丁格式时,一些概念值得思考。重点可能已经不重要:几乎所有使用 diff 及其朋友的开源项目都早已确定了一种补丁格式。但是,以下是补丁中的一些特性,这些特性使一种格式比另一种格式更有用。
补丁格式中首先令人欣赏的是上下文。上下文由补丁中差异块之前和之后的额外行(通常为三行)组成。虽然上下文大大增加了补丁文件的大小,但它允许补丁不必完全依赖于补丁所基于的确切文件。这种特性在版本控制系统中非常重要,在版本控制系统中,期望您的工作文件与主副本略有不同。修补程序可以使用这些上下文行来猜测有问题的行可以放在哪里——并且通常是正确的。
其次,补丁应该是可逆的。当您想要返回到源代码的先前版本,或者当您错误地翻转了 diff 的选项并生成了固有的反向补丁时(不要笑——我们都这样做过),反向非常有用。然而,并非所有补丁格式都是可逆的。
补丁文件应该是高效的:小巧且易于阅读,但又不能太大而导致对于具有大量更改的项目(例如 Linux 内核)来说难以处理。当然,这里有一个权衡。没有上下文的补丁更有效,但绝对不是在源代码维护中非常有用。
最后,可读性是这种风格的版本控制非常重要的一个方面。补丁在弄清楚更改内容时应该很明显,并且不应要求用户将其视角从一个文本块翻转到另一个文本块以弄清楚差异。使格式易于人工阅读比创建计算机可读的格式要困难得多,尤其是在您试图平衡所有其他格式组件时。
各种 diff 格式在每个指标中的排名都不同,不同的项目可能会选择不同的格式。例如,Linux 内核使用统一差异格式。
POSIX 或“normal”diff 是 diff 实用程序使用的默认格式。它是一种简洁的格式,没有任何上下文行,但它是可逆的。我经常看到它被用作一种“通用”补丁格式,因为非 GNU 版本的 diff 实用程序可以解析它。
上下文 diff 格式是另一种与 POSIX 格式相似的可逆格式,但它支持更改周围的上下文。一些项目更喜欢这种格式,特别是当它们包含 GNU 领域之外的开发人员时。使用 GNU diff,可以使用 --context 选项指定此格式。
统一 diff 格式是另一种上下文格式,通常比上下文格式更易于阅读。与上下文 diff 不同,此格式在单个块中显示所有更改,从而消除了许多具有上下文 diff 的冗余行。由于这种格式在版本控制和轻松阅读补丁方面的相对优势,它是 Linux 内核和许多 GNU 项目的首选格式。不利的一面是,许多非 GNU 补丁程序无法识别这种格式。使用 GNU diff,可以使用 --unified 选项指定此格式。
并排格式非常适合人工阅读补丁,但不易用于版本控制。它并排显示原始文件和更改。此格式主要用于人工浏览补丁,补丁程序实际上不支持它。GNU diff 用户可以使用 --side-by-side 选项启用此格式。
ed 格式是一种旧格式,它输出 ed 文本编辑器的脚本,而不是特殊的补丁格式。在创建现代 patch 实用程序之前,需要此选项。由于它输出脚本,因此不包含上下文信息,也不包含反转信息。GNU 仅出于兼容性考虑而提供此选项,可以使用 --ed 开关调用它。
前向 ed 格式与 ed 格式类似,但更加无用。patch 无法处理此格式的文件;ed 也不能。但是,如果您坚持,GNU diff 仍然可以使用 --forward-ed 选项生成它。
RCS 格式是版本控制系统 RCS 及其派生系统使用的格式。它通常不单独使用,并且 patch 实际上无法读取该格式。
预处理器格式既不是补丁格式,也不是脚本。相反,它是一个输出文件,其中包含由 C 预处理器指令(例如 #ifdef、#endif、#elsif 等)分隔的两个文件的内容。可以通过在源文件中设置预处理器变量 (#define) 或使用 GNU 编译器的 -D 开关来编译文件的任一版本。显然,这种格式对于版本控制没有多大用处,但有时在您想要测试更改时可能很有用。
diff 和 patch 的实际用法并不复杂。虽然这些命令(像许多 GNU 工具一样)支持许多选项,允许用户改进这些工具的工作方式,但这些选项实际上不是日常使用所必需的。有关这些实用程序的所有命令行选项的更多信息,请查看其 info 页面。
在最简单的情况下,比较两个文件的 diff 命令行将是
diff old.txt new.txt > oldnew.patch
这将创建 POSIX 格式的补丁,稍后可以将其应用于类似于 old.txt 的文件。请注意,diff 的输出通常出现在标准输出 (STDOUT) 上,我们已使用重定向将该信息放入补丁文件中。对于 GNU 项目,我们通常希望以统一 diff 格式获得结果,因此我们在命令行中添加 -u(或 --unified)选项
diff -u old.txt new.txt > oldnew.patch然而,通常需要比较两个源代码树。这些树将是单个项目的多个修订版本或类似的东西。我通常首选的命令是
diff -ruN old new > oldnew.txt在此示例中,我添加了两个新开关。第一个 -r(或 --recursive)表示我们希望递归查看目录而不是文件。最后一个开关 -N(或 --new-file)表示我们不想忽略文件是否已从任一集合中添加或删除。在这种情况下,如果新目录包含一个名为 foo.txt 的文件,但旧目录不包含,则补丁的行为就好像旧目录中有一个名为 foo.txt 的零字节文件,并将其添加到补丁中。
为了真正充分利用 diff 命令作为版本控制的一种形式,必须首先完成一些准备工作。我稍后会讨论这一点。
通常,一旦生成或下载了 diff,修补文件的过程就更简单了。基于我们上面的第一个示例,我们可以这样做
patch < oldnew.patch
此命令将从标准输入读取补丁文件,并将其应用于当前目录中的任何文件。大多数补丁格式都包含有关要修补的文件名称的信息。在我们的第一个示例中,它将指定 old.txt 是原始文件,并且 patch 命令将在此处查找具有该名称的文件。如果找不到该文件,它将提示您输入应应用补丁的文件名。
在补丁过程中可能会出现许多错误。有时,diff 可能会向后制作,或者您可能想要反转补丁。通过使用 patch 的 --reverse 选项,您可以使 new.txt 再次成为 old.txt。此外,patch 实用程序可以检测到正在修补的文件是否已包含您正在应用的补丁。在这种情况下,patch 将询问您是否希望它反转或尝试无论如何应用补丁。最后,补丁可能会失败。如果发生这种情况,将创建一个名为 old.txt.rej(或类似名称)的文件,并且 patch 将退出。此时,您需要查看 .rej 文件的内容(它将采用补丁格式)并将内容手动应用于源代码。(我偶尔会通过使用更大的 patch --fuzz 值来使 reject 文件应用,但这可能会导致补丁应用程序错误和细微的错误,您稍后会为此挠头。)但是,一旦问题得到解决,您就将有效地将两组更改合并为一个。
显然,这些示例有点牵强。在实际应用中,补丁文件通常不是由制作补丁文件的同一批人应用的。相反,您可能会成为特定补丁的提供者(源代码维护者)或应用特定补丁的人(最终用户)。
虽然有点超出本文档的范围,但 Linux 内核实际上包含一个脚本,该脚本将帮助您使内核与最新修订保持同步。patch-kernel 脚本位于 /usr/src/linux/scripts 目录中,并且将按顺序应用所有必要的补丁,以使您的内核更新到最新修订版,前提是您已下载它们并将它们放在该目录中。如果您实际上不打算参与 Linux 开发工作,而只是想跟上最新最好的源代码,那么这个方便的脚本将允许您绕过 patch 的实际工作,直到您开始开发。一旦您开始将自己的更改添加到源代码树,我建议您使用手动方法。
回到使用 diff 及其同类工具作为一种简陋的版本控制系统的重点,我应该提到,显然没有一种维护源代码树的单独分支的正确或错误方法。像这些这样的低级工具为您提供了定义工作方式的框架,而不是强迫您使用一种或另一种方法。我在这里的建议只是建议,并且在过去为开源项目编写补丁时对我很有用。但是,我并没有确切地记录 Alan Cox 在内核或任何其他项目上工作的人工小时数,因此可能有一种更好的方法。请随时通过下面列出的地址给我发送电子邮件,告诉我您的想法。
在处理源代码树时,我的一般规则是始终维护多个源代码树。与 CVS 或其他版本控制系统不同,当您犯错时,通常无法返回。很容易将不稳定性添加到稳定的补丁中,并且没有简单的方法将您的更改回滚到“上次良好”的配置。例如,让我们想象一下,我有一个名为“Foogram”的假设项目的源代码树。如果我使用 CVS 维护它,我只需要一个树和一个 CVS 服务器(显然维护多个树)。由于我们没有单独服务器的优势,因此我通常至少为该项目设置两个目录:foogram 和 foogram-last。(这些是我个人的命名约定。)最后发布的版本将在 foogram 中,它已知是稳定的,并且当我想分发补丁时,我必须针对它生成所有补丁。foogram-last 目录将包含我的最新更改。这种两层方法通常足以满足一般用途。
但是,许多项目实际上变得过于复杂而无法使用这种方法。众所周知,我已经创建了第三个(或第四个或第五个)目录,其中包含相对于 foogram-last 的最新更改。出于我们的目的,我将此目录称为 foogram-work。它通常包含我对源代码树所做的,不稳定的和最新的更改,我想将它们与我更稳定的补丁分开。保持一个基线目录以针对其生成补丁非常重要,但这往往会导致目录数量快速增长,而其他目录是相对于其的,并且手动修补会变得混乱。在此示例中,我将尝试将 foogram-last 维护为 foogram-work 目录的基线,方法是确保在进行更改时合并前向稳定更改。如果这变得过于复杂,我将不得不创建 foogram-last 的副本,称为 foogram-stable,其中包含 foogram-last 的副本,foogram-work 是针对该副本绘制的,因此我可以使用在 last 和 stable 目录之间创建的补丁来应用于工作目录,以便使其保持最新状态。
已经感到困惑了吗?我当然是!显然,这是一个极端的例子,许多程序员自然会根据自己的需求简化该过程。许多项目不需要个人开发人员具有如此复杂的程度。如果您正在阅读本文并且仍然从中获得任何有用的信息,那么您可能不需要进行如此复杂的极端操作来开发您自己的开源修改补丁,而是将使用简单的方法。
简单的方法是如何只保留一个源代码树就可以完成工作的,并且这几乎适用于我需要仅修改一个或两个源文件作为补丁一部分的每个轻量级项目。在这种情况下,我建议只复制您正在编辑的源文件,并使用不同的扩展名(例如 .old);并在主树中,使用单独的文件补丁以这种方式创建 diff。这些小补丁可以在分发给项目维护者时连接在一起。以这种方式进行操作时,您应该注意从源代码树的根目录运行 diff,以便补丁稍后能够弄清楚更改后的文件在哪里,尤其是在它们位于不同目录中的情况下。
电子邮件:jpranevich@lycos.com
Joseph Pranevich (jpranevich@lycos.com) 是一位狂热的 Linux 极客,在不为 Lycos 工作时,他喜欢写作(各种类型的写作)并参与许多开源项目。