Linux内核编码风格 【ChatGPT】

简介: Linux内核编码风格 【ChatGPT】

这是一份描述 Linux 内核首选编码风格的简短文档。编码风格是非常个人化的,我不会强加我的观点给任何人,但这是我必须要能够维护的任何东西,我也更希望大多数其他东西也是如此。请至少考虑这里提出的观点。

首先,我建议打印一份 GNU 编码标准的副本,然后不要阅读它。把它们烧掉,这是一个很好的象征性姿态。

无论如何,以下是具体要求:

1) 缩进

制表符为 8 个字符,因此缩进也是 8 个字符。有些异端运动试图将缩进设为 4(甚至 2!)个字符深,这就好比试图将圆周率的值定义为 3。

原因:缩进的整个理念在于清晰地定义控制块的起始和结束位置。特别是当你连续盯着屏幕看了 20 个小时后,如果你有大的缩进,你会发现更容易看清楚缩进是如何工作的。

有些人会声称,8 个字符的缩进会使代码在 80 个字符的终端屏幕上移动得太远,使得难以阅读。对此的答复是,如果你需要超过 3 层的缩进,那你的程序本来就有问题,应该修复它。

简而言之,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;

不要使用逗号来避免使用大括号:

if (condition)
        do_this(), do_that();

对于多个语句,始终使用大括号:

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

也不要在一行上放置多个赋值。内核编码风格非常简单,避免复杂的表达式。

在注释、文档以及除了 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 语句中的 while 或者在 if 语句中的 else,如下所示:

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

以及

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

原因:K&R。

另外,请注意,这种大括号放置方式还可以最小化空行(或几乎空行)的数量,而不会损失可读性。因此,由于你屏幕上的换行符不是可再生资源(想想 25 行终端屏幕),你有更多的空行可以放置注释。

不要在一个单语句中不必要地使用大括号。

if (condition)
        action();

以及

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

如果条件语句的一个分支只有一个语句,这个规则不适用;在这种情况下,两个分支都要使用大括号:

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

同样,在循环中包含多个简单语句时,也要使用大括号:

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

3.1) 空格

Linux 内核风格对于空格的使用取决于(大部分)函数与关键字的用法。在(大部分)关键字后使用一个空格。值得注意的例外是 sizeof、typeof、alignof 和 __attribute__,它们看起来有点像函数(在 Linux 中通常与括号一起使用,尽管在语言中并不需要,例如:sizeof info after struct fileinfo info; is declared)。

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

if, switch, case, for, do, while

但在 sizeof、typeof、alignof 或 __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()。

将函数的类型编码到名称中(所谓的匈牙利命名法)是愚蠢的——编译器已经知道类型并可以检查,这只会让程序员感到困惑。

局部变量的名称应该简短而明确。如果你有一个随机的整数循环计数器,它可能应该被命名为i。如果没有可能被误解的情况,将其命名为loop_counter是没有意义的。同样,tmp可以是任何类型的用于保存临时值的变量。

如果你害怕混淆局部变量的名称,那么你有另一个问题,那就是所谓的函数生长激素失衡综合症。请参阅第6章(函数)。

对于符号名称和文档,避免引入“主/从”(或独立于“主”而使用“从”)和“黑名单/白名单”的新用法。

建议替代“主/从”的名称为:

  • '{primary,main} / {secondary,replica,subordinate}'
  • '{initiator,requester} / {target,responder}'
  • '{controller,host} / {device,worker,proxy}'
  • 'leader / follower'
  • 'director / performer'

建议替代“黑名单/白名单”的名称为:

  • 'denylist / allowlist'
  • 'blocklist / passlist'

引入新用法的例外情况是为了维护用户空间ABI/API,或者在更新现有(截至2020年)硬件或协议规范的代码时,这些规范要求使用这些术语。对于新的规范,尽可能将规范中的术语翻译为内核编码标准。

5) 类型定义

请不要使用像 vps_t 这样的东西。对于结构体和指针来说,使用 typedef 是一个错误。当你在源代码中看到

vps_t a;

这意味着什么?相比之下,如果它写成

struct virtual_container *a;

你就能清楚地知道 a 是什么。

很多人认为 typedef 能提高可读性。但事实并非如此。它们只对以下情况有用:

  • 完全不透明的对象(其中 typedef 被积极使用来隐藏对象的实际类型)。
    例如:pte_t 等不透明对象,只能通过适当的访问函数来访问。

注意

不透明性和访问函数本身并不是好的特性。我们之所以为 pte_t 等类型提供它们,是因为在那里真的没有任何可移植访问的信息。

  • 明确的整数类型,其中抽象有助于避免混淆,无论它是 int 还是 long。
    u8/u16/u32 这样的 typedef 是完全合适的,尽管它们更适合于下面提到的情况(d)。

注意

再次强调 - 这需要有一个理由。如果某个东西是 unsigned long,那么没有理由这样做:

typedef unsigned long myflags_t;

  • 但如果有明确的理由,说明在某些情况下它可能是 unsigned int,在其他配置下可能是 unsigned long,那么请毫不犹豫地使用 typedef。
  • 当你使用 sparse 来创建一个用于类型检查的新类型。
  • 在某些特殊情况下,与标准 C99 类型完全相同的新类型。
    尽管眼睛和大脑很快就能适应像 uint32_t 这样的标准类型,但有些人仍然反对它们的使用。
    因此,允许使用 Linux 特定的 u8/u16/u32/u64 类型及其带符号的等价类型,尽管它们在你自己的新代码中并不是强制的。
    当编辑已经使用其中一组类型的现有代码时,你应该遵循该代码中已有的选择。
  • 适用于用户空间的类型。
    在对用户空间可见的某些结构中,我们不能要求 C99 类型,也不能使用上面提到的 u32 形式。因此,在所有与用户空间共享的结构中,我们使用 __u32 和类似的类型。

也许还有其他情况,但基本规则应该是:绝对不要使用 typedef,除非你能清楚地符合上述规则之一。

一般来说,指针或具有可以合理直接访问的元素的结构不应该被定义为 typedef。

6) 函数

函数应该简短而精炼,只做一件事。它们应该适合于一到两个屏幕的文本(众所周知,ISO/ANSI屏幕尺寸为80x24),并且只做一件事,并且做得很好。

函数的最大长度与函数的复杂性和缩进级别成反比。因此,如果你有一个概念上简单的函数,只是一个长的(但简单的)case语句,需要为许多不同的情况做很多小事情,那么有一个更长的函数是可以接受的。

然而,如果你有一个复杂的函数,并且怀疑一个不太聪明的高中一年级学生甚至可能不理解函数的全部内容,那么你应该更加严格地遵守最大长度限制。使用具有描述性名称的辅助函数(如果你认为这对性能至关重要,你可以要求编译器将其内联,它可能会比你做得更好)。

函数的另一个衡量标准是局部变量的数量。它们不应该超过5-10个,否则你做错了什么。重新思考函数,并将其拆分为更小的部分。人脑通常可以轻松地跟踪大约7个不同的事物,再多就会感到困惑。你知道你很聪明,但也许你希望在两周后理解自己做了什么。

在源文件中,用一个空行分隔函数。如果函数是导出的,那么EXPORT宏应该紧随闭合函数大括号的后面。例如:

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

6.1) 函数原型

在函数原型中,包括参数名称及其数据类型。尽管这不是C语言要求的,但在Linux中这是一种简单的方式,为读者添加有价值的信息。

不要在函数声明中使用extern关键字,因为这会使行变得更长,而且并非绝对必要。

在编写函数原型时,请保持元素的顺序规则。例如,使用以下函数声明示例:

__init void * __must_check action(enum magic value, size_t size, u8 count,
char *fmt, ...) __printf(4, 5) __malloc;

函数原型的首选元素顺序是:

  • 存储类(下面是static __always_inline,注意__always_inline在技术上是一个属性,但在使用上类似于inline)
  • 存储类属性(这里是__init——即部分声明,但也包括__cold等)
  • 返回类型(这里是void *)
  • 返回类型属性(这里是__must_check)
  • 函数名称(这里是action)
  • 函数参数(这里是(enum magic value, size_t size, u8 count, char *fmt, ...),注意参数名称应始终包括在内)
  • 函数参数属性(这里是__printf(4, 5))
  • 函数行为属性(这里是__malloc)

请注意,对于函数定义(即实际的函数体),编译器不允许在函数参数后使用函数参数属性。在这些情况下,它们应该放在存储类属性之后(例如,与上面的声明示例相比,注意__printf(4, 5)的位置已经改变):

static __always_inline __init __printf(4, 5) void * __must_check action(enum magic value,
size_t size, u8 count, char *fmt, ...) __malloc
{
       ...
}

7) 函数的集中退出

尽管有些人不赞成,但类似于goto语句的无条件跳转指令经常被编译器频繁使用。

当一个函数从多个位置退出并且需要进行一些常见的工作,比如清理工作时,goto语句就派上用场了。如果不需要清理工作,那么就直接返回。

选择能够说明goto做了什么或者为什么存在的标签名称。一个好的名称示例可能是out_free_buffer:如果goto释放了缓冲区。避免使用类似GW-BASIC的名称,比如err1:和err2:,因为如果你添加或删除了退出路径,你就必须重新编号它们,而且它们使得正确性难以验证。

使用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;
}

要注意的一种常见错误类型是一种错误的bug,看起来像这样:

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

这段代码的bug在于在某些退出路径上foo是NULL。通常的修复方法是将其拆分为两个错误标签err_free_bar:和err_free_foo::

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

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

8) 评论

评论是好的,但过度评论也是有危险的。永远不要试图在评论中解释你的代码是如何工作的:最好的做法是编写代码使其工作显而易见,而且解释糟糕的代码是浪费时间。

通常,你希望你的评论告诉你的代码做了什么,而不是怎么做的。此外,尽量避免在函数体内放置注释:如果函数如此复杂以至于你需要单独注释其中的部分,你可能应该回到第6章一段时间。你可以添加小注释来记录或警告一些特别聪明(或丑陋)的东西,但尽量避免过多。相反,将注释放在函数头部,告诉人们它做了什么,可能还有为什么这样做。

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

长(多行)注释的首选样式是:

/*
 * 这是Linux内核源代码中多行注释的首选样式。
 * 请一致使用。
 *
 * 描述:左侧是一列星号,
 * 开头和结尾几乎是空行。
 */

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

/* net/和drivers/net/中文件的首选注释样式如下。
 *
 * 它几乎与通常的首选注释样式相同,
 * 但没有初始几乎空行。
 */

注释数据也很重要,无论它们是基本类型还是派生类型。为此,每行只使用一个数据声明(多个数据声明不使用逗号)。这样可以为每个项目留下一小段注释,解释其用途。

9) 你把它搞砸了

没关系,我们都会犯错。你可能被你长期使用的Unix用户助手告知GNU emacs会自动为你格式化C源代码,你可能已经注意到是的,它确实会这样做,但它使用的默认值不尽人意(事实上,它比随机输入还糟糕——即使有无数只猴子在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工具来帮助你遵循这些规则,自动重新格式化代码的部分,并审查完整的文件以发现编码风格错误、拼写错误和可能的改进。它还可以方便地对#includes进行排序,对齐变量/宏,对文本进行重新排列等。有关更多详细信息,请参阅文件Documentation/process/clang-format.rst。

10) Kconfig配置文件

对于源树中的所有Kconfig*配置文件,缩进略有不同。在config定义下的行使用一个制表符缩进,而帮助文本则额外缩进两个空格。例如:

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
      ...

有关配置文件的完整文档,请参阅文件Kconfig Language。

11) 数据结构

在单线程环境之外具有可见性的数据结构在创建和销毁时应始终具有引用计数。在内核中,没有垃圾回收(在内核之外,垃圾回收是缓慢且低效的),这意味着你绝对必须对你的使用进行引用计数。

引用计数意味着你可以避免锁定,并允许多个用户并行访问数据结构,而不必担心结构突然消失,因为它们在睡眠或做其他事情时。

请注意,锁定并不是引用计数的替代品。锁定用于保持数据结构的一致性,而引用计数是一种内存管理技术。通常两者都是需要的,并且它们不应该混淆。

许多数据结构确实可以具有两个级别的引用计数,当存在不同类别的用户时。子类计数计算子类用户的数量,并在子类计数为零时仅一次递减全局计数。

这种多级引用计数的示例可以在内存管理(struct mm_struct: mm_users和mm_count)和文件系统代码(struct super_block: s_count和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/dev_printk.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启用的消息。

许多子系统在相应的Makefile中有Kconfig调试选项来打开-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时会引入错误的机会。

对返回值进行多余的强制类型转换是多余的。C编程语言保证了从void指针到任何其他指针类型的转换。

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

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

分配零初始化的数组的首选形式如下:

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

这两种形式都会检查分配大小n * sizeof(...)是否溢出,并在发生溢出时返回NULL。

这些通用分配函数在使用__GFP_NOWARN时都会在失败时输出堆栈转储,因此在返回NULL时没有必要再输出额外的失败消息。

15) 内联疾病

似乎有一种普遍的误解,即gcc有一个名为inline的神奇的“让我变快”的选项。虽然使用内联可能是合适的(例如作为替代宏的一种方式,参见第12章),但往往并非如此。大量使用内联关键字会导致内核变得更大,从而降低整个系统的速度,因为CPU的icache占用更多的空间,而且页面缓存可用的内存更少。想一想吧;页面缓存缺失会导致磁盘寻道,这很容易需要5毫秒。在这5毫秒内,有很多CPU周期可以使用。

一个合理的经验法则是不要在函数中放置超过3行代码的内联。这个规则的一个例外是参数被知道是编译时常量的情况,由于这个常量性,你知道编译器将能够在编译时优化掉大部分函数。对于这种后一种情况的一个很好的例子,请参见kmalloc()内联函数。

通常人们会争论,将内联添加到只使用一次的静态函数中总是有益的,因为没有空间的权衡。虽然从技术上讲这是正确的,但gcc能够自动内联这些函数,而当第二个用户出现时,删除内联的维护问题超过了告诉gcc做一些它本来就会做的事情的潜在价值。

16) 函数返回值和名称

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

混淆这两种表示方式是一个难以找到错误的常见问题。如果C语言在整数和布尔值之间有明确的区分,编译器会帮助我们找到这些错误...但实际上并没有。为了防止此类错误,请始终遵循以下约定:

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

例如,add_work是一个命令,add_work()函数在成功时返回0,失败时返回-EBUSY。同样,pci_dev_present是一个谓词,pci_dev_present()函数在成功找到匹配设备时返回1,否则返回0。

所有导出的函数必须遵守这个约定,所有公共函数也应如此。私有(静态)函数不需要遵守,但建议遵守。

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

17) 使用bool

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

在使用bool类型时,应使用true和false的定义,而不是1和0。

bool函数返回类型和堆栈变量在适当的情况下始终可以使用。使用bool可以提高可读性,通常比'int'更好地存储布尔值。

如果缓存行布局或值的大小很重要,则不要使用bool,因为其大小和对齐方式根据编译的体系结构而变化。为了对齐和大小进行优化的结构不应使用bool。

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

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

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

18) 不要重新发明内核宏

头文件include/linux/kernel.h包含了许多宏,你应该使用它们,而不是自己编写它们的某个变体。例如,如果你需要计算数组的长度,可以利用宏:

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

类似地,如果你需要计算某个结构成员的大小,可以使用宏:

#define sizeof_field(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中通过poke硬件。

考虑编写简单的辅助函数,封装常见的内联汇编片段,而不是反复以稍有差异的方式编写它们。记住,内联汇编可以使用C参数。

大型的、非平凡的汇编函数应该放在.S文件中,并在C头文件中定义相应的C原型。汇编函数的C原型应使用asmlinkage。

你可能需要将asm语句标记为volatile,以防止GCC在没有注意到任何副作用的情况下将其删除。但并不总是需要这样做,不必要地这样做可能会限制优化。

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

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

21) 条件编译

在可能的情况下,不要在 .c 文件中使用预处理条件编译指令(#if、#ifdef);这样做会使代码难以阅读,逻辑难以理解。相反,应该在一个头文件中定义函数,并在 .c 文件中无条件地调用这些函数,同时在 #else 分支提供空操作的存根版本。编译器会避免为存根调用生成任何代码,产生相同的结果,但逻辑仍然易于理解。

更倾向于将整个函数编译掉,而不是部分函数或表达式。不要在表达式中使用 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 */

22) 不要使内核崩溃

一般来说,决定使内核崩溃应该由用户而不是内核开发者来决定。

避免使用 panic()

应该谨慎使用 panic(),主要只在系统启动期间使用。例如,在启动期间耗尽内存并且无法继续时,可以使用 panic()。

使用 WARN() 而不是 BUG()

不要添加使用任何 BUG() 变体的新代码,比如 BUG()、BUG_ON() 或 VM_BUG_ON()。而是使用 WARN*() 变体,最好是 WARN_ON_ONCE(),可能还带有恢复代码。如果没有合理的方式至少部分恢复,恢复代码是不需要的。

“我懒得处理错误”不是使用 BUG() 的借口。对于没有办法继续的主要内部损坏,仍然可以使用 BUG(),但需要有充分的理由。

使用 WARN_ON_ONCE() 而不是 WARN() 或 WARN_ON()

通常情况下,优先使用 WARN_ON_ONCE() 而不是 WARN() 或 WARN_ON(),因为对于给定的警告条件,如果发生,很可能会发生多次。这可能会填满并覆盖内核日志,并且甚至可能会使系统变慢,以至于过多的日志记录会成为另一个额外的问题。

不要轻易使用 WARN*()

WARN() 用于意外的、不应该发生的情况。WARN() 宏不应该用于任何在正常操作中预期发生的事情。这些不是前置或后置条件断言,例如。再次强调:WARN*() 不能用于预期很容易触发的条件,例如用户空间操作。如果需要通知用户存在问题,pr_warn_once() 是一个可能的替代方案。

不要担心 panic_on_warn 用户

关于 panic_on_warn 的一些话:请记住,panic_on_warn 是一个可用的内核选项,许多用户设置了这个选项。这就是为什么上面有一个“不要轻易使用 WARN()”的说明。然而,panic_on_warn 用户的存在并不是避免谨慎使用 WARN() 的有效理由。这是因为,无论谁启用了 panic_on_warn,都明确要求内核在 WARN*() 触发时崩溃,这样的用户必须准备好应对系统更有可能崩溃的后果。

使用 BUILD_BUG_ON() 进行编译时断言

使用 BUILD_BUG_ON() 是可以接受和鼓励的,因为它是一个在编译时生效而在运行时没有影响的断言。

附录 I) 参考资料

《C 程序设计语言》第二版,作者:Brian W. Kernighan 和 Dennis M. Ritchie。Prentice Hall, Inc.,1988 年。ISBN 0-13-110362-8(平装),0-13-110370-9(精装)。

《编程实践》作者:Brian W. Kernighan 和 Rob Pike。Addison-Wesley, Inc.,1999 年。ISBN 0-201-61586-X。

GNU 手册 - 符合 K&R 和本文的 cpp、gcc、gcc internals 和 indent,均可从 https://www.gnu.org/manual/ 获取。

WG14 是 C 语言国际标准化工作组,网址:http://www.open-std.org/JTC1/SC22/WG14/

内核编码风格,作者:greg@kroah.com,OLC 2002:http://www.kroah.com/linux/talks/ols_2002_kernel_codingstyle_talk/html/

相关文章
|
7天前
|
存储 安全 Linux
探索Linux操作系统的心脏:内核
在这篇文章中,我们将深入探讨Linux操作系统的核心—内核。通过简单易懂的语言和比喻,我们会发现内核是如何像心脏一样为系统提供动力,处理数据,并保持一切顺畅运行。从文件系统的管理到进程调度,再到设备驱动,我们将一探究竟,看看内核是怎样支撑起整个操作系统的大厦。无论你是计算机新手还是资深用户,这篇文章都将带你领略Linux内核的魅力,让你对这台复杂机器的内部运作有一个清晰的认识。
23 3
|
17天前
|
存储 缓存 算法
Linux中的红黑树(rbtree)【ChatGPT】
Linux中的红黑树(rbtree)【ChatGPT】
32 13
|
17天前
|
Linux API 调度
CPU热插拔在内核中的支持 【ChatGPT】
CPU热插拔在内核中的支持 【ChatGPT】
31 14
|
17天前
|
存储 缓存 编译器
Linux kernel memory barriers 【ChatGPT】
Linux kernel memory barriers 【ChatGPT】
40 11
|
17天前
|
网络协议 Ubuntu Linux
用Qemu模拟vexpress-a9 (三)--- 实现用u-boot引导Linux内核
用Qemu模拟vexpress-a9 (三)--- 实现用u-boot引导Linux内核
|
17天前
|
Linux
用clang编译Linux内核
用clang编译Linux内核
|
17天前
|
缓存 Linux 开发工具
虚拟映射的内核栈支持 【ChatGPT】
虚拟映射的内核栈支持 【ChatGPT】
|
17天前
|
Linux API SoC
Linux电压和电流调节器框架 【ChatGPT】
Linux电压和电流调节器框架 【ChatGPT】
|
17天前
|
Linux API 调度
关于在Linux内核中使用不同延迟/休眠机制 【ChatGPT】
关于在Linux内核中使用不同延迟/休眠机制 【ChatGPT】
|
17天前
|
Linux API
Linux里的高精度时间计时器(HPET)驱动 【ChatGPT】
Linux里的高精度时间计时器(HPET)驱动 【ChatGPT】