Code Management

来自osdev
跳到导航 跳到搜索

从本质上讲,操作系统是一个相当大的项目。 因此,它需要适当的代码管理,否则你的代码库很可能会变的一团乱,随着项目的增长,它只会变得越来越难维护,你最终会每月重写你的代码库,因为它已经变得不可能维护,而不是实际实现新功能。 另一方面,代码管理必须扩展到整个项目: 哪怕是不不打算增长很大或不需要复杂组织的项目。

代码库中的一致性

编写一个易于维护的代码库的第一步是确保一致性。 这适用于各个领域,这将在本节中详细介绍。

在源代码层面

在大型项目中,重要的是所有源文件都以相同的方式写入。 它从编码样式开始。 每个人都有自己的并且喜欢使用它,但是大型项目需要为整个项目强加一种编码风格,否则愿意贡献的人将花费所有时间来解密其他人的编码风格,并且失去任何帮助的意愿,导致你的项目被放弃。 大多数项目还规定,每个源文件都包含一个标题注释,该注释通常包含对文件目的,文件名和许可条件的描述 (或对许可的引用,例如GPL)。 此外,项目应强加编码标准的其他方面,例如tab策略 (通过tab字符或空格执行缩进的方式,以及每个缩进级别有多少个空格),首选注释样式 (C++样式的 “//” 或C样式的 “/*... */”) 或所有源文件的编码 (纯ASCII,UTF-8等)。

也许最重要的定义是接口中的命名约定 (即函数、结构、类型、宏、变量等是如何命名的) (更多细节请参见下面的 #接口、实现和黑盒)。 这对于不公开的内部 (例如C中的 “static”) 功能不太重要,尽管在所有地方都保持相同的标准是很好的做法。

最后一个问题是用于完成操作系统中多个位置使用的某些任务的方法。 例如,c++语言允许两种分配内存的方式 (「new」和C标准「malloc」)。 一个好的项目具有定义明确的约定,在整个项目中都遵循这些约定。

幸运的是,随着现代IDE的发展,遵循项目的编码约定已成为一个简单的问题,即设置IDE以使用正确的编码/Tab策略/文件头,并使用IDE的自动源格式化实用程序来生成正确格式化的源文件。 有些IDE甚至可以直接在你创建的每个源文件的开头创建标题注释块,因此你只需填写空白即可。

在源代码树中

拥有一致组织至关重要的另一个地方是在项目树中。 换句话说,在子目录中分类文件的方式 必须一致 这在大型项目中是一个更值得关注的问题,并且在开发操作系统时变得至关重要。 因为有一天你会想把你的操作系统移植到其它架构,除了独立于体系结构的源文件 (例如,特定x86的分页管理) 之外,有必要放置与体系结构相关的源文件 (例如,命令行Shell) (请参阅 可移植性)。 GRUB除了包含用于不同平台的子目录 (例如x86、ARM、SPARC、MIPS...) 的源文件之外,还有一个 arch目录。 然后,你应该以统一的方式命名源文件。 例如,C++头必须在整个项目中具有相同的扩展名 (.h.hpp.hxx)。 这里是另一个例子: 假设你有一个可扩展的命令行shell,并且在不同的源文件中实现了命令。 你必须决定是否要专门为每个命令创建一个子目录 (推荐:“cmd/xxxxxx.c” 和 “cmd/yyyyy.c”),或者将它们命名为 “cmdxxxxx.c” 和 “cmdyyyyy.c” 或其它命名方案。

在版本控制方案中

尽管这通常不如以前的关注重要,但你需要保持不同版本编号的方式一致。 一些数字版本顺序 (例如 “0.1”,然后 “0.2”,然后是 “0.3”...),而其他人则按顺序增加小版本 (例如 “1.2” 中的 “2”),以进行小更新,并且仅在对项目进行重大改进时才增加主要版本编号 (版本中的第一个编号)。 有些还添加了修订和内部版本号,每次对文件进行更改时,都会增加一些随机数,这就给出了很难记住的怪异版本号,例如 “2.2.11127.56150”。

此外,你可以为每个发行版指定一个特殊名称。 使用该策略的项目包括Mac OS X (例如Leopard,Lion,Mountain Lion...),Android (例如姜饼,果冻豆,冰淇淋三明治...),Windows (例如Millennium,XP,Vista),可能还有其它我不知道的。随便挑个你喜欢的,或者完全不同的,甚至更简单的,比如「myos 1」,然后「myos 2」,然后「myos 3」……毕竟这是你的项目!

“语义版本控制” 是统一版本控制方案的尝试。你可能已经使用了一个接近这个的方案。

接口、实现和黑盒

在处理大型代码库 (例如操作系统的代码库) 时,代码会很快变成一团糟。 为了避免这种情况,你需要适当的代码组织。 请注意,在不使用此组织的情况下仍然可以进行编码,但是组织代码库会大大增加项目成功和稳定的机会。

这都是关于从 “实现” 中拆分 “接口”。 「接口」由C++中的函数原型(function prototypes)、结构体定义(structure definitions)、类型定义(typedefs)、结构体(structures)以及可能的预处理器宏(preprocessor macros),加上C++中的类组成。 请注意,不同的语言支持不同的接口: 而一些语言如 DJava 通过特殊的关键字和构造提供内置支持,而其它语言则不支持 (汇编是不支持接口的,也很少有操作系统完全用汇编编写)。

接口

接口是用户可见的部分。 它通常存在于头文件中,并且至少对于c语言而言由函数原型,结构和类型定义以及预处理器符号和宏组成。 这部分不应该包含任何代码,除了通常将调用推迟到其它函数的小内联函数之外,参数格式不同。 一个例子是无处不在的实用程序 “inb/inw/inl/outb/outw/outl” 内联汇编函数。

在编写操作系统内核时,通常希望接口与平台无关。 这可以通过使用 'typedef' 来表示每个元素来实现。 例如,在实现分页时,你可以将特定的typedef用于物理地址,将另一个用于线性地址。 如果将物理地址称为 “phys_addr_t”,则只需将 “phys_addr_t” 的typedef从 “uint32_t” 更改为 “uint64_t”,就可以在使内核适应x86_64时重用相同的接口 (假设以x86开头)。 你也可以只使用条件编译,或者你选择的语言提供的一些类似功能。

接口完全独立于实现是非常重要的。 这将允许你稍后为不同的平台编写硬件抽象层的第二个实现 (对于初学者来说,是抽象硬件的内核的一部分),而不必更改接口。 假设你的接口足够独立于平台,并且你的内核使用它并且只有它来访问硬件,那么移植到一个新的平台将仅仅是编写一个新的HAL的问题,其余的将神奇地编译并完美地工作在新的体系结构上 (更多信息,见 可移植性)。

为此,你还必须确保内核的其余部分确实与平台无关。 实际上,你内核的接口绝对不应该直接使用固定宽度类型 (例如 “uint32_t”)。 如果平台需要固定宽度 (例如特殊寄存器),那么你应该为其使用特殊的typedef (例如special_register_t)。 否则,如果你只想确保给定的容量,则使用最接近你想要的标准类型。 一个很好的例子是C标准库,它对文件偏移,大小,时间等使用特殊类型,但在必要时使用 (unsigned) char/int/long。 正如你所看到的,通过无数不同的平台,C库的接口保持不变。 精心设计的内核接口允许将相同的东西应用于...你的内核。

实现

实现与接口相反。也就是说,它包含了接口中定义的功能的实际 “实现”。 细节实现是允许 (事实上,必须,在内核的上下文中) 取决于平台。 在实现中,你编写直接访问特定硬件 (最明显的是内联程序集) 的代码。 (根据你的内核设计,解决特定硬件的代码应放置在单独的驱动程序中)。 但是,如果你希望你的代码保持可读,我建议你仍然使用C预处理器来获取 “魔术” 值,例如内存映射设备的固定地址 (例如VGA的文本模式视频存储器的0xB8000地址),具有显式名称 (例如VGA_TEXT_MEMORY)。

只要它正确地实现了接口,实现就可以像它想要或需要的那样丑陋。 但是,要在接口和实现之间实现适当的隔离,重要的是,不能从外部访问实现内部的功能。 C语言为此提供了 “static” 关键字。

特殊情况是较大的组件,其实现可以视为单独的库。 在这种情况下,一种常见的做法是拥有一个内部实现特定的接口。 但是,不将内部函数声明为 “静态”,因此由开发人员确保了不从实现外部使用它们。 使用这种技术的专业库不计其数。 行业标准的 “boost C++库” 及其 “detail” 目录是很好的例子。 此类组件的示例是复杂的设备驱动程序,例如用于硬件加速视频卡的驱动程序或复杂的文件系统。

黑盒

如果还没有完成,你可能会希望扩展你的开发团队。 但是一旦你有更多的人在代码库上工作,你就会面临一个问题: 他们越多,开发所需的时间就越长。 这样做的原因是,负责内核一部分的每个开发人员都需要考虑由负责内核其它部分的其他开发人员编写的代码。 那是黑盒发挥作用的时候。 “黑盒” 方法包括将内核视为一组单独的库 (硬件抽象层是一个库,VFS是另一个库等)。 一旦你的项目被正确地分成几部分,你可以给每个开发人员他的库,不再担心冲突。 结合到接口和实现隔离,你将让每个团队尝试尽可能地实现其接口的一部分。 有了适当设计的界面,这将使项目负责人的开发过程更快,更易于管理。

这种方法还将在开发团队的招聘过程中为你提供帮助。 实际上,你会意识到,有很多人决定开始自己的OS项目,目标是尽可能地做最好的GUI,但是当他们意识到他们之前必须实现无数其它事情时,就会失去继续该项目的意愿。 如果你在论坛上宣布,你正在寻找人们在一个已经成熟的内核下设计一个GUI,这些人会很乐意加入你的项目,因为它允许他们只在他们喜欢的事情上工作,而不用担心其余的。

它还将使修复错误变得更加容易。 因为内核的每个部分都清楚地与其余部分分开,所以很容易找到代码中的单个问题并修复它,而不必修改依赖它的内核的许多其它部分,因为缺乏精确的接口。 因此,你的内核最终有机会比平均水平更稳定。

现在出现了接口的问题。 由于每个组件都与其余组件整齐地隔离,并且实现了该接口,因此你需要有一个设计良好的接口,并明确有关谁可以修改它的规则。 如果有人可以更改界面,那么拥有一个界面是没有用的,因为你的团队会花时间调整实现以适应新的界面,而不是实际编码。 一个被证明有效的 (好) 方案是在你的项目页面上有一个论坛,该论坛的主题专门用于讨论界面。 然后,当有人想要改变接口时,他需要向整个开发者社区展示他的改变,并提出争论,然后由你来决定是否应该改变。

更改中工作

很明显,你的代码将不得不改变,有时改变并不像你想象的那么好。 在这种情况下,你将希望回滚更改。 你可能还会遇到这样的情况: 你删除了代码的一部分 (例如,你想完全更改你的调度程序),后来又想再次看到该代码 (例如,你的新调度程序支持不同的方法,现在你想重新包含旧调度程序的代码)。

注意: 如果你已经在使用版本控制系统,则最好跳过此部分 :)

版本控制系统

版本控制系统 (VCS) 是一个管理源文件更改的程序 (对于OSX用户,这很像为源代码提供了一台时光机)。 具体来说,你在每次重大更改后执行 “提交” 操作 (例如,在重写调度程序或添加新驱动程序后,不要在每次修改后提交),并且VCS将记住更改前后文件的状态。 这意味着,如果你改变主意,想再次看到你的代码,那么VCS将能够把它还给你。 VCS还使你能够生成 “补丁”,这些文件仅包含同一文件的两个版本之间的差异。

但是使用VCS的要点是,它使两个 (或更多) 人可以一次在同一代码库上工作,而不会相互干扰。 VCS还允许你将源代码放置在互联网上,并让整个团队使用一台服务器进行合作。 许多源代码托管网站 (例如Google代码) 都支持通过VCS访问你的代码库,并且在代码中使用此类工具可以使你有更多机会让人们为你的项目做出贡献。