规范的 Linux 内核编码风格
随着越来越多的人和公司开始编写 Linux 内核代码,理解可接受的内核编程编码风格和约定变得非常重要。本文首先解释了书面的内核编码风格,正如内核的 Documentation/CodingStyle 文件中所记录的那样,然后介绍了 “不成文的” 内核编码风格规则。
为什么首先要有内核编程风格规则?为什么不让作者以他们想要的任何风格编写代码,然后让每个人都接受它呢?毕竟,代码格式化不会影响内存使用、执行速度或内核普通用户会看到的任何其他方面。规则的原因可以用 Elliot Soloway 和 Kate Ehrlich 文章(见资源)中的这段话来概括:
我们的研究支持这样一种观点,即对编程计划和编程话语规则的了解会对程序理解产生重大影响。在他们的著作 Elements of Style 中,Kernighan 和 Plauger 也指出了我们所说的“话语规则”。我们的实证结果为这些规则提供了依据:程序应该以特定的风格编写,这不仅仅是美学问题。更确切地说,以传统方式编写程序存在心理学基础:程序员强烈期望其他程序员会遵循这些话语规则。如果规则被违反,那么程序员长期以来建立的期望所带来的效用实际上就被抵消了。
还有许多其他研究表明,以通用风格编写的大量代码直接影响代码的理解、审查和快速修改的难易程度。
由于查看 Linux 内核代码的开发人员数量非常庞大,因此该项目最好有一个一致的风格指南。这使得代码更容易被初次阅读的人和几个月后重新审视自己旧代码的人理解。这也使得其他人可以轻松阅读、理解并可能修复和增强您的代码,这是开源代码的最大优势之一。
现在我们已经了解了为什么应该有规则,那么规则是什么呢?Linus Torvalds 和其他内核程序员编写了一份简短的文档,详细介绍了部分内核编程规则。该文档位于内核源代码树的 Documentation/CodingStyle 文件中。对于任何想要为 Linux 内核做出贡献的人来说,这是必读的。以下是这些规则的摘要。
UNIX 的原始作者将他们的大括号放置为:左大括号放在行尾,右大括号放在行首
if (x is true) { we do y }
因此,Linux 内核使用这种风格。此规则的例外是函数,函数的左大括号位于行首
int function(int x) { body of function }同样,这就是 Kernighan 和 Ritchie 编写代码的方式。
要获得正确的缩进和大括号风格的良好示例,请查看任何 fs/*.c 文件或 kernel/*.c 文件中的任何内容。一般来说,大多数内核都采用了正确的缩进和大括号风格,但也有一些值得注意的例外。fs/devfs/*.c 和 drivers/scsi/qla1280.* 中的代码是 不 应该如何进行缩进和大括号的良好示例。
如果要将大量代码转换为正确的格式,可以使用脚本来运行 indent 程序。该文件位于内核源代码树的 scripts/Lindent 中。
您的变量和函数应该具有描述性且简洁。不要使用像 CommandAllocationGroupSize 或 DAC960_V1_EnableMemoryMailboxInterface() 这样冗长华丽的名称,而应该使用 cmd_group_size 或 enable_mem_mailbox()。不赞成混合大小写名称,并且禁止在名称中编码变量或函数的类型(如“匈牙利命名法”)。
全局变量应仅在绝对必要时使用。局部变量应该简短明了。有效的局部循环变量名包括 “i” 和 “j”,而 “loop_counter” 则过于冗长;“tmp” 允许用于任何生命周期短的临时变量。
同样,可以在 fs/*.c 中找到正确命名的良好示例。许多驱动程序代码都有糟糕的变量名,因为它们是从其他操作系统移植过来的。不 应该如何命名函数和变量的示例包括 drivers/block/DAC960.* 和 drivers/scsi/cpqfs*。
函数应该只做一件事,并且做好。它们应该简短,并希望只包含一到两个屏幕的文本。如果您有一个函数为不同的情况做了很多小事,那么可以接受使用较长的函数。但是,如果您有一个长而复杂的函数,那就存在问题。
此外,函数中局部变量的数量是衡量函数复杂程度的指标。如果局部变量的数量超过十个,那么就存在问题。
同样,在 fs/*.c 和其他内核核心代码中有很多大小合适的函数的良好示例。一些糟糕的函数示例可以在 drivers/hotplug/ibmphp_res.c(其中一个函数长达 370 行)或 drivers/usb/usb-uhci.c(其中一个函数有 18 个局部变量)中找到。
注释是好的,但它们必须有用。糟糕的注释解释代码如何工作、谁在特定日期编写了特定函数或其他此类无用的东西。好的注释解释文件或函数做什么以及为什么这样做。它们也应该放在函数的开头,而不一定嵌入在函数内部(您正在编写小函数,对吧?)。
现在,函数注释也有一个标准格式。它是 GNOME 项目为其代码使用的文档方法的变体。如果您以这种风格编写函数注释,则其中的信息可以被工具提取并制成独立的文档。这可以通过在内核树上运行 make psdocs 或 make htmldocs 来生成 kernel-api.ps 或 kernel-api.html 文件来查看,其中包含不同内核子系统的所有公共接口。
这种风格记录在 Documentation/kernel-doc-nano-HOWTO.txt 和 scripts/kernel-doc 文件中。基本格式如下
/** * function_name(:)? (- (* @parameterx: ( (* a blank line)? * (Description:)? ( * (section header: ( (*)?*/
简短的函数描述不能跨越多行,但其他描述可以(并且它们可以包含空行)。所有进一步的描述性文本可以包含以下标记
funcname(): 函数
$ENVVAR: 环境变量
&struct_name: 结构体的名称(最多两个单词,包括 struct)
@parameter: 参数的名称
%CONST: 常量的名称
因此,一个带有单个参数的函数注释的简单示例将是
/** * my_function - does my stuff * @my_arg: my argument * * Does my stuff explained. **/
可以并且应该为结构体、联合体和枚举编写注释。它们的格式与函数格式非常相似
/** * struct my_struct - short description * @a: first member * @b: second member * * Longer description */ struct my_struct { int a; int b; };一些良好注释的函数的良好示例可以在 drivers/usb/usb.c 文件中找到,其中所有全局函数都有文档记录。arch/i386/kernel/mtrr.c 文件是具有合理数量注释的文件的良好示例,但它们的格式不正确,因此无法被文档工具提取。关于 不 应该如何为函数创建注释块的一个很好的例子是 drivers/scsi/pci220i.c。
关于数据结构的章节出现在 2.4.10-pre7 内核中。它深入探讨了每个可以存在于单线程环境之外的数据结构都需要实现引用计数,以便正确处理结构的内存管理问题。如果您向结构添加引用计数,则可以避免许多糟糕的锁定问题和竞争条件。多个线程可以访问相同的结构,而无需担心不同的线程会释放其下的数据。
本章的最后一句话是任何内核开发人员的必读内容:“记住:如果另一个线程可以找到您的数据结构,而您没有对其进行引用计数,那么您几乎肯定有一个错误。”
为什么引用计数是必要的,一个很好的例子可以在 USB 数据结构 “struct urb” 中找到。此结构由 USB 设备驱动程序创建,填充数据,发送到 USB 主机控制器,在那里它将被处理并最终通过线路发送出去。当主机控制器完成 urb 后,将通知原始设备驱动程序。当主机控制器驱动程序正在处理 urb 时,原始驱动程序可以尝试取消 urb 甚至释放它。这导致了核心 USB 子系统和不同驱动程序中的许多错误。它还在 linux-usb-devel 邮件列表上引发了关于 urb 生命周期中何时允许任何一个驱动程序接触它的长期而详细的争论。在 2.5 内核系列中,struct urb 添加了引用计数,并且 USB 核心和 USB 主机控制器驱动程序添加了少量代码来正确处理引用计数。现在,每当驱动程序想要使用 urb 时,引用计数都会递增。当使用完成后,引用计数会递减。如果这是最后一个用户,则内存将被释放,并且 urb 消失。这允许 USB 设备驱动程序大大简化其 urb 处理逻辑,修复了许多不同的竞争条件错误,并平息了关于该主题的所有争论。
在内核中可以找到各种设计良好、文档齐全且经过良好调试的函数和数据结构。请利用它们,不要仅仅因为它们不是您编写的就重新发明自己的版本。其中最常见的是字符串函数、字节顺序函数以及链表数据结构和函数。
在 include/linux/string.h 文件中,定义了许多常见的字符串处理函数。这些函数包括 strpbrk、strtok、strsep、strspn、strcpy、strncpy、strcat、strncat、strcmp、strncmp、strnicmp、strchr、strrchr、strstr、strlen、strnlen、memset、memcpy、memove、memscan、memcmp 和 memchr。并且在 include/linux/kernel.h 文件中定义了许多 “简单” 字符串函数:simple_strtoul、simple_strtol、simple_strtoull 和 simple_strtoll。因此,如果您的内核代码中需要任何类型的字符串功能,请使用内置函数。不要试图意外地重写现有函数。
不要重写代码来切换不同字节序表示之间的数据。include/asm/byteorder.h 文件(asm 将指向正确的子目录,具体取决于您的处理器架构)将引入各种函数,使您可以执行自动转换,而无需考虑处理器或数据的字节序格式。
如果您需要创建任何类型数据结构的链表,请使用 include/linux/list.h 中的代码。它包含一个结构体 struct list_head,应该包含在您要创建列表的结构体中。然后您将能够轻松地添加、删除或迭代数据结构列表,而无需编写新代码。
一些使用链表结构的良好代码示例可以在 drivers/hotplug/pci_hotplug_core.c 和 drivers/ieee1394/nodemgr.c 中找到。内核中一些应该使用链表结构的代码可以在 ATM 核心中,在 struct atm_vcc 数据结构中找到。由于 ATM 代码没有使用 struct list_head,因此每个 ATM 驱动程序都需要手动遍历数据结构列表,从而重复了大量代码。
typedef 不应在命名任何结构体时使用。大多数主要的内核结构体都没有 typedef。虽然这会缩短它们的用法,但会使代码变得晦涩难懂。例如,核心内核结构体 struct inode、struct dentry、struct file、struct buffer_head、struct user 和 struct task_struct 都没有 typedef。
使用 typedef 只会隐藏变量的真实类型。有记录显示,某些内核代码使用了嵌套最多四层的 typedef,这使得程序员无法轻易判断他们实际使用的变量类型。如果程序员没有意识到结构体的大小,可能会导致非常大的结构体被意外地在堆栈上声明或从函数返回。
typedef 也可以用作避免输入长结构体定义的拐杖。如果是这种情况,则应根据上面列出的命名规则缩短结构体名称。
永远不要定义 typedef 来表示指向结构体的指针,如下例所示
typedef struct foo { int bar; int baz; } foo_t, *pfoo_t;
这会隐藏变量的真实类型,并使用变量类型的名称来定义它是什么(请参阅前面关于匈牙利命名法的评论)。
一些 typedef 使用不当的示例位于 include/raid/md*.h 文件中,其中每个结构体都分配了一个 typedef,以及 drivers/acpi/include/*.h 文件中,其中许多结构体甚至没有分配名称,只有一个 typedef。
唯一可以接受使用 typedef 的地方是声明函数原型。这些可能每次都很难输入,因此为此声明 typedef 是不错的,例如,bh_end_io_t typedef 用作 init_buffer() 调用中的参数。这在 include/fs.h 中定义为
typedef void (bh_end_io_t) (struct buffer_head *bh, int uptodate);
由于 Linux 运行在各种不同的处理器、不同的配置选项和相同基本硬件类型的变体上,因此很容易在代码中开始出现大量的 #ifdef 语句。这不是正确的做法。相反,将 #ifdef 放在头文件中,如果代码不包含在内,则提供空的内联函数。
例如,考虑 drivers/usb/hid-core.c 中的以下代码
static void hid_process_event (struct hid_device *hid, struct hid_field *field, struct hid_usage *usage, __s32 value) { hid_dump_input(usage, value); if (hid->claimed & HID_CLAIMED_INPUT) hidinput_hid_event(hid, field, usage, value); #ifdef CONFIG_USB_HIDDEV if (hid->claimed & HID_CLAIMED_HIDDEV) hiddev_hid_event(hid, usage->hid, value); #endif }
在这里,如果未启用特定的配置选项,作者不想调用 hiddev_hid_event()。这是因为如果未启用配置选项,该函数甚至不会存在。
为了删除这个 #ifdef,对 include/linux/hiddev.h 进行了以下更改
#ifdef CONFIG_USB_HIDDEV extern void hiddev_hid_event (struct hid_device *, unsigned int usage, int value); #else static inline void hiddev_hid_event (struct hid_device *hid, unsigned int usage, int value) { } #endif
然后 drivers/usb/hid-core.c 被更改为
static void hid_process_event (struct hid_device *hid, struct hid_field *field, struct hid_usage *usage, __s32 value) { hid_dump_input(usage, value); if (hid->claimed & HID_CLAIMED_INPUT) hidinput_hid_event(hid, field, usage, value); if (hid->claimed & HID_CLAIMED_HIDDEV) hiddev_hid_event(hid, usage->hid, value); }如果未启用 CONFIG_USB_HIDDEV,编译器将用空函数调用替换对 hiddev_hid_event() 的调用,然后完全优化掉 if 语句。这使得代码可读性更高,并且随着时间的推移更易于维护。
Linux 内核由大量的源代码组成,这些源代码由数百名开发人员在多年内贡献。由于大多数代码都遵循一些简单和基本的风格和格式规则,因此人们快速理解新代码的能力得到了极大的增强。如果您想为这段代码做出贡献,请阅读 CodingStyle 指南并在您的补丁和新代码中遵循它们。当您试图说服人们接受您的贡献时,其他不成文的规则可能与书面规则同样重要,并且应该同样严格地遵守。
本文基于为 2002 年渥太华 Linux 研讨会创建的论文和演示文稿。
