Linux kernel coding style

PS:本文是对 linux 5.4 Documentation/process/coding-style 的翻译,由于本人水平比较抱歉,以下内容大多参考自 google 翻译,请慎重参考。

这是一个简短的文档,说明 linux 内核代码首选的代码风格。 代码风格是比较个人化的, 我不会强加我的观点给任何人,但是这让任何事都是我能维护的, 对于大多数其他事我更青睐这种风格。至少请考虑下面的几点。

首先我建议打印一份 GNU 的编码标准,不要去读它。把它烧掉,这是最伟大的象征手势。

总之, 现在开始:

1 缩进

制表符是8个字符,因此缩进也是8个字符。有种异端运动他们尝试使用4字符缩进(甚至2个!),这就像企图把PI(Π?)的值定义为3一样。

解释:主要想法是缩进后,控制块的开始和结束位置是清楚的。 尤其是当您连续20个小时盯着屏幕时,如果缩进量很大,找到缩进后的内容要容易的多。

现在,有些人会说,8个字符的缩进会使代码向右移得太远,而且这样将在80个字符的终端屏幕上难以阅读。 这个问题的答案是,如果你需要三级以上的缩进,你应该修改的是你的程序。

简而言之,8字符缩进使代码更容易阅读,并添还带来了一个好处,就是在你的代码嵌套太深时发出警告。 注意这个警告。

缓解switch语句中多个缩进级别的首选方法是在同一列中对齐switch及其从属的case标签,而不是对case标签进行二次缩进 。

例如:

	switch (suffix) {
	case 'G':
	case 'g':
		mem <<= 30;
		break;
	case 'M':
	case 'm':
		mem <<= 20;
		break;
	case 'K':
	case 'k':
		mem <<= 10;
		fallthrough;
	default:
		break;
	}

不要将多条语句放在一行上,除非你有要隐藏的内容:

	if (condition) do_this;
	  do_something_everytime;

也不要将多个任务放在一行上。 内核编码风格是非常简单。 避免使用难以理解的表达式。

在注释和文档之外,除了 Kconfig 空格都不用于缩进。上面就是一个反面例子。

找一个好的编辑器,不要在行尾留空格。

2 打破长的行和字符串

在用通用编辑器时,代码风格与可读性和可维护性是有关的。(个人水平有限,原文怎么翻译都觉得别扭。)

限制行的长度为80列,这是一个强烈首选的限制。

长度超过80列的语句将合理的被分块,除非超过80列会明显怎加可读性并且没有隐藏信息。 子代码块总是比父代码块短得多,并且大体上位于右边。 具有长参数列表的函数头也是如此。 但是,切勿破坏如printk消息之类的用户可见的字符串,因为这样会破坏 grep 到他们的能力。

3 括号和空格的位置

在C代码中另一个经常出现的问题就是大括号的位置。 与缩进大小不同,没有什么技术上的原因去选择一种位置策略而不是另一种,但是首选的方式是先知 Kernighan 和 Ritchie 向我们展示那种,将前括号放在行尾,把后括号放在行首,因此:

	if (x is true) {
		we do y
	}

这适用于所有非函数的语句块(if, switch, for, while, do). 例如:

	switch (action) {
	case KOBJ_ADD:
		return "add";
	case KOBJ_REMOVE:
		return "remove";
	case KOBJ_CHANGE:
		return "change";
	default:
		return NULL;
	}

但是,有一种特殊情况就是函数:他们的前括号在下一行开头。

	int function(int x)
	{
		body of function
	}

全世界的异教徒都认为这是一种矛盾……好吧……是矛盾的,但是所有思想健全的人都知道(a)K&R是正确的,(b)K&R是正确的。 此外,函数是特殊的(在 C 语言中,你不能嵌套他们)。

请注意,右括号前一行是空行,除非是跟在相似代码之后的情况,如 do 语句中的代码部分,或者有 else 在 if 语句的代码之后,例如:

	do {
		body of do-loop
	} while (condition);

and

	if (x == y) {
		..
	} else if (x > y) {
		...
	} else {
		....
	}

理由:K&R。

另外,注意这种括号的放置也可以最小化空行(或几乎是空行)的数量,而不会损失任何可读性。 因此,由于你屏幕上的新行不是一种可再生资源(此处请考虑25行的屏幕终端),所以你有更多的空行可以写注释。

不要在单个语句的地方使用不必要地括号。

	if (condition)
		action();

and

	if (condition)
		do_this();
	else
		do_that();

如果条件语句中仅一个分支是单个语句,则不适用; 在后一种情况下两个分支中都要使用大括号:

	if (condition) {
		do_this();
		do_that();
	} else {
		otherwise();
	}

另外,当一个循环包含多个简单语句时要用大括号:

	while (condition) {
		if (test)
			do_something();
	}

3.1 空格

Linux内核的代码风格对空格的用发(主要)取决于函数与关键字。 在(大多数)关键字之后使用空格。 需要注意的例外是 sizeoftypeofalignof 和 __attribute__,它们看起来有点像函数(并且在Linux中通常与括号一起使用,尽管在C语言中它们并不需要,例如:在 struct fileinfo info 被声明 之后的 sizeof info;) 。

因此,在这些关键字之后使用空格:

if, switch, case, for, do, while

但是不要在 sizeoftypeofalignof 和 __attribute__ 之后加空格。例如:

	s = sizeof(struct file);

不要在括号内的表达式两边添加空格。 下面是个反例

	s = sizeof( struct file );

在声明指针数据或返回值是指针类型的函数时,* 的首选用法是与数据名称或者函数名称相邻,而不是与类型名称相邻。 例如:

	char *linux_banner;
	unsigned long long memparse(char *ptr, char **retptr);
	char *match_strdup(substring_t *s);

在大多数双目和三目运算符的两边(每边)使用一个空格,例如下面的任意一个:

=  +  -  <  >  *  /  %  |  &  ^  <=  >=  ==  !=  ?  :

但是单目运算符后没有空格:

&  *  +  -  ~  !  sizeof  typeof  alignof  __attribute__  defined

在后加加和后减减之前没有空格:

++  --

在前加加和前减减之后没有空格:

++  --

并且没有空格在 “.” 和 “->” 结构体成员运算符的两边。

在行尾不要留下多余的空白字符。 某些具有智能缩进的编辑器将在新行的开头适当插入空格,因此你可以直接开始输入下一行代码。 但是,一些编辑器不会删除这些空格如果你最后没有在这行输入代码,那么你将留下一个空白行。结果就是,你最终将得到包含行尾空格的行。

Git会警告你有关引入了行尾空格的补丁,并你可以有选择地裁剪掉行尾空格。 但是,如果应用一系列补丁,则可能会通过更改其上下文行来使该系列中的后续补丁应用失败。

4 命名

C 是一种斯巴达式的语言,因此命名也应如此。与 Modula-2 和 Pascal 程序员不同,C 程序员不会使用像 ThisVariableIsATemporaryCounter 之类的可爱名称。一名 C 程序员将会给该变量命名为 tmp,该变量更容易编写,并且至少不会比之前增加更多的理解难度。

虽然不赞成使用混合的命名,但全局变量必须使用带有全局变量描述的名称。全局函数命名为 foo 是应该枪毙的。

全局变量(仅在你真的需要它们时才使用)和全局函数一样,都需要有描述全局属性名称。如果您有一个统计活动用户数量的函数,则应命名为 count_active_users() 或类似的名称,而不应将其命名为 cntusr()

将函数的类型编写到函数名称中(所谓的匈牙利表示法)是脑瘫行为-编译器无论如何都知道它们的类型并且可以检查它们,这样只会使程序员感到困惑。难怪会 MicroSoft 制作 bug 多的程序。

本地变量命名应该简单明了。如果你有一些随机的整数循环计数器,则应将其命名为 i。如果没有误解的可能性,将他它命名为 loop_counter 是没有成效的。同样, tmp 几乎是可以用于保存任何类型的临时变量。

如果你害怕弄混你的局部变量名称,那么你会另一个问题,它被称为 函数增长不平衡综合症。请参见第6章(函数)。

5 Typedefs

请不要使用 vps_t 之类的定义。 对结构体和指针使用typedef是错误的。 当你看到一个

vps_t a;

在代码中,它是什么意思? 相反,如果它是

struct virtual_container * a;

你可以准确的说出 a 是什么。

很多人认为 typedef 有助于提高可读性。 并非如此。 它们仅对以下情况有用:

  1. 完全不透明的对象(typedef 被明确的用来隐藏目标对象是什么)。
    • 例:pte_t 等不透明对象,你只能使用适当的访问函数来使用。注意 不透明和使用访问函数本身并不好。之所以使用像 pte_t 等之类的事物,是因为那里确实没有可移植可访问的信息。
  2. 清除整数类型,这种抽象有助于避免混淆,无论它是int还是long。
    • u8 / u16 / u32是完美的类型定义,尽管它们比更适合放在第4点那里。注意 此外-这需要有一个理由。如果一些事物使用了 ‘unsingned long’ ,那么就没有理由这样做 typedef unsigned long myflags_t; 但是如果有明确的理由说明为什么在某些情况下可能是 ‘unsignec int’ 并且在其他情况下可能是 ‘unsigned long’,那么一定要继续使用 typedef。
  3. 当你使用 sparse 对正确创建的新类型做类型检查时。
  4. 在某些特殊情况下,定义与 C99 标准类型相同的新类型。
    • 尽管眼睛和大脑只需要很短的时间就能习惯uint32_t这样的标准类型,但是仍然有人反对使用它们。
    • 因此,允许使用 Linux 特有的 u8 / u16 / u32 / u64 类型并且他们于字面意思相等也与标准类型相同-尽管它们不强制使用在你自己的新代码中。
    • 编辑已使用一种或另一种类型的现有代码时,应遵循该代码现有的选择。
  5. 用户空间安全的类型。
    • 在某些用户空间可见的结构中,我们不能要求使用C99类型,也不能使用上面的 u32 类型。因此,我们在和用户空间共享的所有结构中使用 __u32 相似的类型。

或许还有其他情况,但是这个规则基本上应该是不在使用 typedef ,除非你可以明确地匹从这些规则中匹配到一个。

通常,可以合理地直接访问的指针或是结构体的元素 永远 都不应使用 typedef。

6 函数

函数应该简短且酷,并且只能做一件事。它们代码长度应当占用一到两个屏幕(众所周知,ISO / ANSI 屏幕大小为 80×24),而且做一件事并把它做好。

函数的最大长度与该函数的复杂度和缩进级别成反比。因此,如果您有一个概念上很简单的函数,它只是一个很长(但很简单)的 case 陈述,你需要去为大量不同的情况做非常多的小事情,那么有一个更长的函数是可以的。

但是,如果你有一个很复杂函数,并且怀疑一年级以下的高中生甚至可能不能完全解该函数,那么你应该更加严格地遵守最大函数长度限制。使用有描述性名称的辅助函数(如果您认为它对性能至关重要,则可以要求编译器内联它们,并且它可能会比你做的更好。)

函数的另一种度量是局部变量的数目。它们不应超过5-10,不然就是你做错了什么。重新构思函数,然后将它拆分为较小的部分。人脑通常能够轻松地跟踪约7种不同的事物,不论什么过多的话就会混淆。你知道自己很聪明,但是也许你会想知道近两周你做了什么。

在源文件中,用一个空行分隔函数。如果导出了该函数,则该函数的 EXPORT 宏应紧跟在该函数的结束括号行之后。例如:

	int system_is_up(void)
	{
		return system_state == SYSTEM_RUNNING;
	}
	EXPORT_SYMBOL(system_is_up);

在函数原型中,应包括参数名称及其数据类型。 尽管C语言不需要这样做,但是在Linux中它是首选的,因为这是一个简单的方法为读者添加了有价值的信息。

请不要将 extern 关键字与函数原型一起使用,因为这会使行更长而且不是完全必要的。

7 函数的集中退出

尽管一些人反对(使用 goto 语句),但是编译器频繁的使用等价于 goto 语句的等效项经常以无条件跳转指令。

当函数从多个位置退出并且必须执行一些常规工作例如清理时, goto 语句就会派上用场。 如果不需要执行清理语句,则直接返回。

选择一个能够说明 goto 做了什么或者 goto 存在的原因的标签名称。 一个示例:如果这个 goto 是为了释放了缓冲区,它合适的名字应该是 out_free_buffer。 避免使用诸如 err1: 和 err2: 之类的 GW-BASIC 名称,因为如果你添加或删除退出路径是,就必须重给它们新编号,并且它们的正确性仍然难以验证。

使用goto 语句的基本原理:

  • 无条件语句更容易理解和跟踪
  • 减少嵌套
  • 防止在进行修改时不更新单个退出点而导致错误
  • 节省了编译器优化冗余代码的工作
	int fun(int a)
	{
		int result = 0;
		char *buffer;

		buffer = kmalloc(SIZE, GFP_KERNEL);
		if (!buffer)
			return -ENOMEM;

		if (condition1) {
			while (loop1) {
				...
			}
			result = 1;
			goto out_free_buffer;
		}
		...
	out_free_buffer:
		kfree(buffer);
		return result;
	}

要注意的常见错误类型是“一个 err 造成的 bugs”,如下所示:

	err:
		kfree(foo->bar);
		kfree(foo);
		return ret;

这段代码的 bug 是在一些退出路径中, foo 是空指针。通常这个问题的解决方法是将 err 标签分成两个,err_free_bar: 和 err_free_foo: 。

	 err_free_bar:
		kfree(foo->bar);
	 err_free_foo:
		kfree(foo);
		return ret;

理想情况下,你应该模拟错误以测试所有出口路径。

8 注释

有注释很好,但是有过有过多的注释也是一种威胁。永远不要尝试在注释中解释代码是如何工作的:写出一个 内容 显而易见的代码要好的多,并且解释写得不好的代码就是在浪费时间。

通常,你希望你的注释去说明你的代码做了什么,而不怎样实现。另外,请尽量避免在函数体内添加注释:如果函数过于复杂以至于需要分别注释代码中的各部分,你大概需要回到第六章。您可以写一些短的注释去提醒或警告某些特别聪明(或丑陋)的事情,但要避免过多。相反,把注释放在函数的开头,告诉人们它做了什么,以及它这么做可能的原因。

在注释内核API函数时,请使用 kernel-doc 格式。详细信息,请参见文档 Documentation/doc-guide/ 中的文件和脚本 scripts/kernel-doc。

长(多行)注释的首风格式是:

	/*
	 * This is the preferred style for multi-line
	 * comments in the Linux kernel source code.
	 * Please use it consistently.
	 *
	 * Description:  A column of asterisks on the left side,
	 * with beginning and ending almost-blank lines.
	 */

对于net /和drivers / net /中的文件,长(多行)注释的首选风格略有不同。

	/* The preferred comment style for files in net/ and drivers/net
	 * looks like this.
	 *
	 * It is nearly the same as the generally preferred comment style,
	 * but there is no initial almost-blank line.
	 */

给数据写注释也很重要,无论是基本类型还是派生类型。 为此,每行仅声明一个数据(多个数据声明不使用逗号)。 这给你留出了对每个项目进行简短注释的空间,以说明其用途。

9 你搞砸了

那还好,我们都搞砸过。 可能已经有人告诉过你 GNU emacs 会自动为你格式化 C 代码帮助你长期使用 Unix ,并且您已经注意到那是对的,确实可以这样做,但是使用它的默认值并不理想(实际上 ,它们比随机输入更糟糕-无数的猴子用 GNU emacs 输入永远不会写出一个好的程序)。

因此,您可以选择摆脱 GNU emacs ,或将其更改为更合理的值。 为了实现后者,你可以将以下内容粘贴到.emacs文件中:

  (defun c-lineup-arglist-tabs-only (ignored)
    "Line up argument lists by tabs, not spaces"
    (let* ((anchor (c-langelem-pos c-syntactic-element))
           (column (c-langelem-2nd-pos c-syntactic-element))
           (offset (- (1+ column) anchor))
           (steps (floor offset c-basic-offset)))
      (* (max steps 1)
         c-basic-offset)))

  (dir-locals-set-class-variables
   'linux-kernel
   '((c-mode . (
          (c-basic-offset . 8)
          (c-label-minimum-indentation . 0)
          (c-offsets-alist . (
                  (arglist-close         . c-lineup-arglist-tabs-only)
                  (arglist-cont-nonempty .
		      (c-lineup-gcc-asm-reg c-lineup-arglist-tabs-only))
                  (arglist-intro         . +)
                  (brace-list-intro      . +)
                  (c                     . c-lineup-C-comments)
                  (case-label            . 0)
                  (comment-intro         . c-lineup-comment)
                  (cpp-define-intro      . +)
                  (cpp-macro             . -1000)
                  (cpp-macro-cont        . +)
                  (defun-block-intro     . +)
                  (else-clause           . 0)
                  (func-decl-cont        . +)
                  (inclass               . +)
                  (inher-cont            . c-lineup-multi-inher)
                  (knr-argdecl-intro     . 0)
                  (label                 . -1000)
                  (statement             . 0)
                  (statement-block-intro . +)
                  (statement-case-intro  . +)
                  (statement-cont        . +)
                  (substatement          . +)
                  ))
          (indent-tabs-mode . t)
          (show-trailing-whitespace . t)
          ))))

  (dir-locals-set-directory-class
   (expand-file-name "~/src/linux-trees")
   'linux-kernel)

这将使 emacs 更好地配合〜/ src / linux-trees下C文件的内核编码风格。

但是,即使您无法对 emacs 进行合理的格式化,也不是完全失败:使用indent。

现在,再说一次,GNU indent 与GNU emacs 有同样脑瘫的设置,这就是为什么你需要给他提供一些命令行选项的原因。但是,这并不算太糟,因为即使 GNU indent 的作者也意识到 K&R 的权威(GNU人并不邪恶,他们只是在这件事上被严重误导了),所以您只需给 indent 添加命令行参数 -kr -i8(代表着K&R(8个字符的缩进),或使用 ‘scripts/Lindent’脚本以最新的风格进行缩进。

indent 有很多选项,尤其是在注释重新格式化时,你可能需要看一下 man 手册。但是请记住: indent 不是一个修复糟糕格式的程序。

注意,你还可以使用 clang-format 工具来帮助你遵循这些规则,还能快速自动地重新格式化你的部分代码,并检查整个文件,发现编码风格的错误,错别字和可能的改进。它对于排序#include,对齐变量/宏,重排文本和其他类似任务也非常方便。更多详细信息,请参见文件 Documentation/process/clang-format.rst 。

10 Kconfig 配置文件

对于整个源代码树中的所有 Kconfig 配置文件,缩进略有不同。 config 定义下的行以一个 tab 进行缩进,而帮助文本需要在另外加两个空格进行缩进。 例:

  config AUDIT
	bool "Auditing support"
	depends on NET
	help
	  Enable auditing infrastructure that can be used with another
	  kernel subsystem, such as SELinux (which requires this for
	  logging of avc messages output).  Does not do system-call
	  auditing without CONFIG_AUDITSYSCALL.

严重危险的特性(例如对某些文件系统的写支持)应在其提示字符串中突出显示:

  config ADFS_FS_RW
	bool "ADFS write support (DANGEROUS)"
	depends on ADFS_FS
	...

配置文件的完整文档,请看 Documentation/kbuild/kconfig-language.rst。

11 数据结构

在单线程环境之外的外部可见的数据结构它的创建和销毁应该始终有应用计数。 在内核中,不存在垃圾回收(在内核之外,垃圾回收是缓慢且低效的), 这意味着你必须对所有的使用(这个数据结构)的引用计数。

引用计数意味着你可以避免上锁,并且允许多个用户并行访问数据结构-而且不用担心它们只是因为睡眠或做其他事情的这段时间里数据结构突然消失。

请注意,上锁不能代替引用计数。上锁用于保持数据结构的一致性, 而引用计数是一种内存管理技术。通常两者都是必需的,并且他们不能相互混淆。

当存在不同类型的用户时,许多数据结构确实可以具有两级引用计数。子类计数对子类用户的数量进行计数,并且仅在子类计数变为零时将全局计数减一。

这种“多级引用计数”的例子可以在内存管理(struct mm_struct : mm_users and mm_count)和文件系统代码(struct super_block : s_count and s_active)中找到。

请记住:如果另一个线程可以找到您的数据结构,并且您没有对它的引用计数,你几乎可以确定有一个bug。

12 宏,枚举和 RTL

宏定义常量的名称和枚举中的标签都使用大写字母。

	#define CONSTANT 0x12345

定义多个相关常量时,最好使用枚举。

虽然宏名称应该是大写字母,但是类似于函数的宏应该用小写字母命名。

通常,内联函数比类似于函数的宏更好。

具有多个语句的宏应放在 do-while 块中:

	#define macrofun(a, b, c)			\
		do {					\
			if (a == 5)			\
				do_this(b, c);		\
		} while (0)

使用宏时应避免的事情:

  1. 影响控制流的宏:
	#define FOO(x)					\
		do {					\
			if (blah(x) < 0)		\
				return -EBUGGERED;	\
		} while (0)

这是一个非常糟糕的想法。 它看起来像一个函数,但是退出了调用它的函数。 不要破坏那些要阅读代码的内部解析器。

  1. 依赖于具有魔术名称局部变量的宏:
	#define FOO(val) bar(index, val)

或许看起来不错,但是当人们阅读代码时,它会非常令人困惑,并且很容易在看似无害的修改上造成破坏。

  1. 将带参数的宏做为左值:FOO(x)= y; 如果有人将FOO变成内联函数的话,他会咬你。
  2. 忘记优先级:带有表达式的宏定义常量必须将表达式用括号括起来。 当心带参数的宏有类似问题。
	#define CONSTANT 0x4000
	#define CONSTEXP (CONSTANT | 3)
  1. 在宏函数中定义局部变量时,命名空间中变量发生冲突:
	#define FOO(x)				\
	({					\
		typeof(x) ret;			\
		ret = calc_ret(x);		\
		(ret);				\
	})

ret是局部变量的通用名称-__foo_ret与现有变量发生冲突的可能性较小。

cpp手册详尽地处理了宏。 gcc内部手册还介绍了RTL,RTL在内核中经常与汇编语言一起使用。

13 打印内核信息

内核开发人员喜欢被视为学者。注意内核信息的拼写,以产生良好的印象。不要使用像 dont 这样的残缺词;使用 “do not” 或 “don’t” 替代 。让信息简洁,清晰,明确。

内核信息不必以句点终止。

把数字放在括号中 (%d) 没有任何意义,应当避免。

在 <linux / device.h> 中有许多驱动模型诊断宏,你应该使用这些宏来确保信息与正确的设备和驱动程序匹配,并以正确的级别进行标记:dev_err(), dev_warn(), dev_info() 等。对于与特定设备无关的信息, <linux / printk.h> 中定义了 pr_notice(), pr_info(), pr_warn(), pr_err() 等。

打印出好的调试信息是一个很大的挑战;一旦有了它们,它们可以为远程故障排除提供巨大帮助。但是,调试信息的打印方式与其他非调试信息的打印方式不同。虽然其他 pr_XXX() 函数会无条件打印,但 pr_debug() 不会;除非定义了 DEBUG 或设置了 CONFIG_DYNAMIC_DEBUG ,否则默认情况下不会编译。 dev_dbg() 也是如此,并且相关的约定使用 VERBOSE_DEBUG 将 dev_vdbg() 信息添加到已由 DEBUG 启用的打印信息中。

许多子系统都有 Kconfig 的调试选项,可以在相应的 Makefile 中打开 -DDEBUG;在其他情况下,在特定文件添加 #define DEBUG。并且调试信息应当被无条件打印出,如果它已经在与调试相关的 #ifdef 代码段中,可以使用 printk(KERN_DEBUG …​) 。

14 分配内存

内核提供了以下通用内存分配器:kmalloc(), kzalloc(), kmalloc_array(), kcalloc(), vmalloc() 和 vzalloc()。更多相关信息,请参阅API文档。Documentation/core-api/memory-allocation.rst

传递结构大小的首选形式如下:

	p = kmalloc(sizeof(*p), ...);

拼写出结构名的另一种形式会影响可读性,并且在更改指针变量类型但传递给内存分配器的相应sizeof不变的情况下,可能会导致 bug。

强制转换返回值为空的指针是多余的。C语言可以保证从 void 指针转换到任何其他类型的指针。

分配数组的首选形式如下:

	p = kmalloc_array(n, sizeof(...), ...);

分配为零的数组的首选形式如下:

	p = kcalloc(n, sizeof(...), ...);

两种形式都检查分配大小 n * sizeof(…​)的溢出,如果发生则返回 NULL。

这些通用的内存分配函数在不带 __GFP_NOWARN 的情况下使用时都会在失败时生成栈转储,因此在返回 NULL 时不会发出额外的失败信息。

15 内联的弊端

这里有一个常见的误解就是 gcc 有一个神奇的“使我更快”加速选项,称为内联 (inline ) 。尽管可以适当地使用内联(例如,作为替换宏的一种方法,请参见第12章),但通常不是那么合适。大量使用 inline 关键字会导致更大的内核,那样又会降低整个系统的速度,这是因为 CPU 的 icache 占用量更大,并且导致可用的缓存页会更少。考虑一下;页面高速缓存未命中会导致查找磁盘,这很容易花费5毫秒。有很多cpu周期在这5毫秒浪费掉了。

一个合理的经验规则是不要对超过三行代码的函数进行内联。这个规则的一个例外情况是参数在编译时已知为常量,并且由于此常量,你知道编译器在编译时能够优化掉大部分函数。有关后面这种情况的一个很好的示例,请参见 kmalloc() 内联函数。

人们常常争辩说,将内联添加到静态且仅使用一次的函数终是一个胜利,因为没有空间上的损失。尽管从技术上讲这是正确的,且 gcc 能够在没有帮助的情况下自动内联这些内容,并且当第二个用户出现时删除内联的维护问题超过了指示出 gcc 去做一些无论如何它都会执行的事情的潜在价值。(这里很可能翻的不对<( ̄︶ ̄)>)

16 函数返回值和命名

函数可以返回许多不同类型的值,最常见的一种是指示函数成功还是失败的值。这样的值可以表示为整数的错误代码(-Exxx =失败,0 =成功)或(成功的)布尔值(0 =失败,非零=成功)。

混合使用这两种表示形式这是那些难以发现的 bug 的丰富的来源。如果 C 语言对整型和布尔型之间有很强的区分,则编译器会为我们找到这些错误……但事实并非如此。为防止此类错误,请遵循以下约定:

如果函数的名称是一个动作或一个重要的命令,
该函数应返回一个整型的错误代码。
如果函数名称是一个谓词,这个函数应返回一个“成功”布尔值。

例如,add work 是一条命令,并且 add_work() 函数返回 0 表示成功,或返回 -EBUSY 表示失败。同样,”PCI device present” 是一个谓词,如果 pci_dev_present() 函数成功找到匹配的设备,则返回1,否则返回0。

所有被导出函数必须遵守此约定,所有公共函数也应遵守此约定。私有(静态)功能不需要,但是它们也推荐这样做。

返回值是计算的实际结果而不是指示计算是否成功的函数不受此规则的约束。通常,它们通过返回超出范围的结果来指示失败。典型的例子是返回指针的函数。他们使用 NULL 或 ERR_PTR 机制来报告失败。

17 使用布尔值

Linux 内核 bool 类型是 C99 _Bool 类型的别名。 bool值只能计算为0或1,并且隐式或显式转换为 bool 会自动将值转换为 true 或 false。使用布尔型时,不需要构造,从而消除了一类错误。

使用布尔值时,应使用 true 和 false 定义,而不是1和0。

bool 函数的返回类型和栈变量不论是否合适总是比较好用。鼓励使用布尔值来提高可读性,并且在存储布尔值时通常比使用“ int”是更好。

如果高速缓存线路布局或值的大小很重要,请不要使用布尔型,因为它的大小和对齐方式会根据编译的架构而变化。对对齐和大小进行了优化的结构不应使用布尔型。

如果结构具有许多 true/false ,请考虑将它们合并为 1 bit 成员的位段,或使用适当的固定宽度类型例如 u8。

类似地,对于函数参数,可以将许多 true/false值合并为单个按位“ flags”参数,并且如果调用位置具有裸露的true / false常量,则“ flags”通常是更具可读性的选择。

否则,在结构和参数中限制使用bool可以提高可读性。

18 不要重新写内核里的宏

头文件 include/linux/kernel.h 中包含许多你应使用的宏,而不是自己显示的写一些它们的变体。 例如,如果您需要计算数组的长度,请用下面的宏:

	#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))

同样的,如果需要计算某些结构体成员的大小,请使用

	#define FIELD_SIZEOF(t, f) (sizeof(((t*)0)->f))

如果需要,内核还有min()和max()宏会进行严格的类型检查。 自由的去细读该头文件,去看看是否已经定义了你需要的那些宏,你不应让他们重复出现在你的代码中。

19 编辑器行模式和其他残留

一些编辑器可以将解释的配置信息通过用特殊标记表示的嵌入在源文件中。 例如,emacs解释标记行如下:

	-*- mode: c -*-

或是像这样:

	/*
	Local Variables:
	compile-command: "gcc -DMAGIC_DEBUG_FLAG foo.c"
	End:
	*/

Vim解释标记如下:

	/* vim:set sw=8 noet */

不要在源文件中包含任何这些。 人们有自己的个人编辑器配置,并且您的源文件不应覆盖它们。 这包括用于缩进和模式配置的标记。 人们可以使用自己的自定义模式,或者可以使用其他魔术方法来使缩进工作正确。

20 内联汇编

在特定架构的代码中,你可能需要使用内联汇编与 CPU 或平台功能交互。必要时不要犹豫。虽然 C 语言可以完成这项工作时,但是请不要随意使用内联汇编。你应该尽可能用 C 语言操作硬件。

考虑编写简单的辅助函数,这些函数封装常用的内联汇编,而不是有一点改动就反复写内联汇编。请记住,内联汇编可以使用 C 语言参数。

大型,重大的汇编函数应放在.S文件中,并在C头文件中定义相应的 C 原型。汇编函数的C原型应使用 asmlinkage 宏。

你可能需要将汇编语句声明为 volatile ,以防止 GCC 在未发现任何副作用的情况下将其删除。但是,你不一定总是需要这样做,并且在不必要地地方这样做会限制优化。

当编写包含多个指令的单个内联汇编语句时,请将每条指令放在单独的行中,并用引号引起来的字符串结束,并用\ n \ t结束除最后一条以外的每个字符串,以正确缩进汇编输出中的下一条指令:

	asm ("magic %reg1, #42\n\t"
	     "more_magic %reg2, %reg3"
	     : /* outputs */ : /* inputs */ : /* clobbers */);

21 条件编译

尽可能不要在.c文件中使用预处理条件 (#if,#ifdef) ;这样做会使代码更难阅读,逻辑也更难跟踪。相反在头文件中使用此类条件,以定义在那些.c文件中使用的函数,在#else情况下提供空操作的分支版本,然后从.c文件中无条件调用这些函数。编译器将避免为分支调用生成任何代码,从而产生相同的结果,但是逻辑将易于跟踪。

最好编译出整个函数,而不是编译部分函数或部分的表达式。不要将 ifdef 放入表达式中,而是将部分或全部表达式分解为单独的辅助函数,然后将条件语句用于该函数。

如果你有在特定配置中可能未使用的函数或变量,那么编译器会警告其定义未使用,请将该定义标记为 __maybe_unused 而不是将其封装在预处理条件中去。 (但是,如果函数或变量始终不使用,请将其删除。)

在代码内,如果可能,使用 IS_ENABLED 宏将 Kconfig 符号转换为 C 布尔表达式,并在普通的 C 条件中使用它:

	if (IS_ENABLED(CONFIG_SOMETHING)) {
		...
	}

编译器将依据条件不断折叠,并像#ifdef一样包含或排除代码块,因此这不会增加任何运行时开销。 但是,这种方法仍然允许C编译器查看块中的代码,并检查其是否正确(语法,类型,符号引用等)。 因此,如果块内的代码引用了如果不满足条件将不存在的符号,则仍必须使用 #ifdef。

请在任何重要的 #if 或 #ifdef 块的末尾(多行),在 #endif 的同一行后面添加注释,并表面所使用的条件表达式。 例如:

	#ifdef CONFIG_SOMETHING
	...
	#endif /* CONFIG_SOMETHING */

附录 Ⅰ 参考文献

The C Programming Language, Second Edition by Brian W. Kernighan and Dennis M. Ritchie. Prentice Hall, Inc., 1988. ISBN 0-13-110362-8 (paperback), 0-13-110370-9 (hardback).

The Practice of Programming by Brian W. Kernighan and Rob Pike. Addison-Wesley, Inc., 1999. ISBN 0-201-61586-X.

GNU manuals – where in compliance with K&R and this text – for cpp, gcc, gcc internals and indent, all available from http://www.gnu.org/manual/

WG14 is the international standardization working group for the programming language C, URL: http://www.open-std.org/JTC1/SC22/WG14/

Kernel :ref:process/coding-style.rst <codingstyle>, by greg@kroah.com at OLS 2002: http://www.kroah.com/linux/talks/ols_2002_kernel_codingstyle_talk/html/

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注