转:Linux内核编码风格

翻译自linux源码Documentation文件夹下的CodingStyle。

Linux内核代码风格

这是一个简短的文档,描述了 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;
        /* fall through */
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 语 句中的 “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();
}

3.1) 空格

Linux 内核的空格使用方式 (主要) 取决于它是用于函数还是关键字。(大多数) 关键字 后要加一个空格。值得注意的例外是 sizeof, typeof, alignof 和 attribute,这 些关键字某些程度上看起来更像函数 (它们在 Linux 里也常常伴随小括号而使用,尽管 在 C 里这样的小括号不是必需的,就像 struct fileinfo info; 声明过后的 sizeof info)。

所以在这些关键字之后放一个空格:

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() 或者类似的名字,你不应该叫它 cntuser()

在函数名中包含函数类型 (所谓的匈牙利命名法) 是脑子出了问题——编译器知道那些类 型而且能够检查那些类型,这样做只能把程序员弄糊涂了。难怪微软总是制造出有问题 的程序。

本地变量名应该简短,而且能够表达相关的含义。如果你有一些随机的整数型的循环计 数器,它应该被称为 i 。叫它 loop_counter 并无益处,如果它没有被误解的 可能的话。类似的, tmp 可以用来称呼任意类型的临时变量。

如果你怕混淆了你的本地变量名,你就遇到另一个问题了,叫做函数增长荷尔蒙失衡综 合症。请看第六章 (函数)。

5) Typedef

不要使用类似 vps_t 之类的东西。

对结构体和指针使用 typedef 是一个 错误 。当你在代码里看到:

vps_t a;

这代表什么意思呢?

相反,如果是这样

struct virtual_container *a;

你就知道 a 是什么了。

很多人认为 typedef 能提高可读性 。实际不是这样的。它们只在下列情况下有用:

  1. 完全不透明的对象 (这种情况下要主动使用 typedef 来 隐藏 这个对象实际上 是什么)。

    例如: pte_t 等不透明对象,你只能用合适的访问函数来访问它们。

    Note

    不透明性和 “访问函数” 本身是不好的。我们使用 pte_t 等类型的原因在于真 的是完全没有任何共用的可访问信息。

  2. 清楚的整数类型,如此,这层抽象就可以 帮助 消除到底是 int 还是 long 的混淆。

    u8/u16/u32 是完全没有问题的 typedef,不过它们更符合类别 (d) 而不是这里。

    Note

    要这样做,必须事出有因。如果某个变量是 unsigned long ,那么没有必要

    typedef unsigned long myflags_t;

    不过如果有一个明确的原因,比如它在某种情况下可能会是一个 unsigned int 而在其他情况下可能为 unsigned long ,那么就不要犹豫,请务必使用 typedef。

  3. 当你使用 sparse 按字面的创建一个 类型来做类型检查的时候。

  4. 和标准 C99 类型相同的类型,在某些例外的情况下。

    虽然让眼睛和脑筋来适应新的标准类型比如 uint32_t 不需要花很多时间,可 是有些人仍然拒绝使用它们。

    因此,Linux 特有的等同于标准类型的 u8/u16/u32/u64 类型和它们的有符号 类型是被允许的——尽管在你自己的新代码中,它们不是强制要求要使用的。

    当编辑已经使用了某个类型集的已有代码时,你应该遵循那些代码中已经做出的选 择。

  5. 可以在用户空间安全使用的类型。

    在某些用户空间可见的结构体里,我们不能要求 C99 类型而且不能用上面提到的 u32 类型。因此,我们在与用户空间共享的所有结构体中使用 __u32 和类似 的类型。

可能还有其他的情况,不过基本的规则是 永远不要 使用 typedef,除非你可以明 确的应用上述某个规则中的一个。

总的来说,如果一个指针或者一个结构体里的元素可以合理的被直接访问到,那么它们 就不应该是一个 typedef。

6) 函数

函数应该简短而漂亮,并且只完成一件事情。函数应该可以一屏或者两屏显示完 (我们 都知道 ISO/ANSI 屏幕大小是 80x24),只做一件事情,而且把它做好。

一个函数的最大长度是和该函数的复杂度和缩进级数成反比的。所以,如果你有一个理 论上很简单的只有一个很长 (但是简单) 的 case 语句的函数,而且你需要在每个 case 里做很多很小的事情,这样的函数尽管很长,但也是可以的。

不过,如果你有一个复杂的函数,而且你怀疑一个天分不是很高的高中一年级学生可能 甚至搞不清楚这个函数的目的,你应该严格遵守前面提到的长度限制。使用辅助函数, 并为之取个具描述性的名字 (如果你觉得它们的性能很重要的话,可以让编译器内联它 们,这样的效果往往会比你写一个复杂函数的效果要好。)

函数的另外一个衡量标准是本地变量的数量。此数量不应超过 5-10 个,否则你的函数 就有问题了。重新考虑一下你的函数,把它分拆成更小的函数。人的大脑一般可以轻松 的同时跟踪 7 个不同的事物,如果再增多的话,就会糊涂了。即便你聪颖过人,你也可 能会记不清你 2 个星期前做过的事情。

在源文件里,使用空行隔开不同的函数。如果该函数需要被导出,它的 EXPORT 宏 应该紧贴在它的结束大括号之下。比如:

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

在函数原型中,包含函数名和它们的数据类型。虽然 C 语言里没有这样的要求,在 Linux 里这是提倡的做法,因为这样可以很简单的给读者提供更多的有价值的信息。

7) 集中的函数退出途径

虽然被某些人声称已经过时,但是 goto 语句的等价物还是经常被编译器所使用,具体 形式是无条件跳转指令。

当一个函数从多个位置退出,并且需要做一些类似清理的常见操作时,goto 语句就很方 便了。如果并不需要清理操作,那么直接 return 即可。

选择一个能够说明 goto 行为或它为何存在的标签名。如果 goto 要释放 buffer, 一个不错的名字可以是 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 错误 ,就像这样:

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

这段代码的错误是,在某些退出路径上 foo 是 NULL。通常情况下,通过把它分离 成两个错误标签 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) 你已经把事情弄糟了

这没什么,我们都是这样。可能你的使用了很长时间 Unix 的朋友已经告诉你 GNU emacs 能自动帮你格式化 C 源代码,而且你也注意到了,确实是这样,不过它 所使用的默认值和我们想要的相去甚远 (实际上,甚至比随机打的还要差——无数个猴子 在 GNU emacs 里打字永远不会创造出一个好程序) (译注:Infinite Monkey Theorem)

所以你要么放弃 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 有很多选项,特别是重新格式化注释的时候,你可能需要看一下它的手册。 不过记住: indent 不能修正坏的编程习惯。

10) Kconfig 配置文件

对于遍布源码树的所有 Kconfig* 配置文件来说,它们缩进方式有所不同。紧挨着 config 定义的行,用一个制表符缩进,然而 help 信息的缩进则额外增加 2 个空 格。举个例子:

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) 数据结构

如果一个数据结构,在创建和销毁它的单线执行环境之外可见,那么它必须要有一个引 用计数器。内核里没有垃圾收集 (并且内核之外的垃圾收集慢且效率低下),这意味着你 绝对需要记录你对这种数据结构的使用情况。

引用计数意味着你能够避免上锁,并且允许多个用户并行访问这个数据结构——而不需要 担心这个数据结构仅仅因为暂时不被使用就消失了,那些用户可能不过是沉睡了一阵或 者做了一些其他事情而已。

注意上锁 不能 取代引用计数。上锁是为了保持数据结构的一致性,而引用计数是一 个内存管理技巧。通常二者都需要,不要把两个搞混了。

很多数据结构实际上有 2 级引用计数,它们通常有不同 的用户。子类计数器统 计子类用户的数量,每当子类计数器减至零时,全局计数器减一。

这种 多级引用计数 的例子可以在内存管理 (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 internals 手册也详细讲解了 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(),一个相关约定是在一 个已经开启了 DEBUG 时,使用 VERBOSE_DEBUG 来添加 dev_vdbg()。

许多子系统拥有 Kconfig 调试选项来开启 -DDEBUG 在对应的 Makefile 里面;在其他 情况下,特殊文件使用 #define DEBUG。当一条调试信息需要被无条件打印时,例如, 如果已经包含一个调试相关的 #ifdef 条件,printk(KERN_DEBUG …) 就可被使用。

14) 分配内存

内核提供了下面的一般用途的内存分配函数: kmalloc(), kzalloc(), kmalloc_array(), kcalloc(), vmalloc()vzalloc()。 请参考 API 文档以获取有关它们的详细信息。

传递结构体大小的首选形式是这样的:

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

另外一种传递方式中,sizeof 的操作数是结构体的名字,这样会降低可读性,并且可能 会引入 bug。有可能指针变量类型被改变时,而对应的传递给内存分配函数的 sizeof 的结果不变。

强制转换一个 void 指针返回值是多余的。C 语言本身保证了从 void 指针到其他任何 指针类型的转换是没有问题的。

分配一个数组的首选形式是这样的:

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

分配一个零长数组的首选形式是这样的:

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

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

15) 内联弊病

有一个常见的误解是 内联 是 gcc 提供的可以让代码运行更快的一个选项。虽然使 用内联函数有时候是恰当的 (比如作为一种替代宏的方式,请看第十二章),不过很多情 况下不是这样。inline 的过度使用会使内核变大,从而使整个系统运行速度变慢。 因为体积大内核会占用更多的指令高速缓存,而且会导致 pagecache 的可用内存减少。 想象一下,一次 pagecache 未命中就会导致一次磁盘寻址,将耗时 5 毫秒。5 毫秒的 时间内 CPU 能执行很多很多指令。

一个基本的原则是如果一个函数有 3 行以上,就不要把它变成内联函数。这个原则的一 个例外是,如果你知道某个参数是一个编译时常量,而且因为这个常量你确定编译器在 编译时能优化掉你的函数的大部分代码,那仍然可以给它加上 inline 关键字。 kmalloc() 内联函数就是一个很好的例子。

人们经常主张给 static 的而且只用了一次的函数加上 inline,如此不会有任何损失, 因为没有什么好权衡的。虽然从技术上说这是正确的,但是实际上这种情况下即使不加 inline gcc 也可以自动使其内联。而且其他用户可能会要求移除 inline,由此而来的 争论会抵消 inline 自身的潜在价值,得不偿失。

16) 函数返回值及命名

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

混合使用这两种表达方式是难于发现的 bug 的来源。如果 C 语言本身严格区分整形和 布尔型变量,那么编译器就能够帮我们发现这些错误… 不过 C 语言不区分。为了避免 产生这种 bug,请遵循下面的惯例:

如果函数的名字是一个动作或者强制性的命令,那么这个函数应该返回错误代
码整数。如果是一个判断,那么函数应该返回一个 "成功" 布尔值。

比如, add work 是一个命令,所以 add_work() 在成功时返回 0,在失败时返回 -EBUSY。类似的,因为 PCI device present 是一个判断,所以 pci_dev_present() 在成功找到一个匹配的设备时应该返回 1,如果找不到时应该返回 0。

所有 EXPORTed 函数都必须遵守这个惯例,所有的公共函数也都应该如此。私有 (static) 函数不需要如此,但是我们也推荐这样做。

返回值是实际计算结果而不是计算是否成功的标志的函数不受此惯例的限制。一般的, 他们通过返回一些正常值范围之外的结果来表示出错。典型的例子是返回指针的函数, 他们使用 NULL 或者 ERR_PTR 机制来报告错误。

17) 不要重新发明内核宏

头文件 include/linux/kernel.h 包含了一些宏,你应该使用它们,而不要自己写一些 它们的变种。比如,如果你需要计算一个数组的长度,使用这个宏

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

类似的,如果你要计算某结构体成员的大小,使用

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

还有可以做严格的类型检查的 min() 和 max() 宏,如果你需要可以使用它们。你可以 自己看看那个头文件里还定义了什么你可以拿来用的东西,如果有定义的话,你就不应 在你的代码里自己重新定义。

18) 编辑器模式行和其他需要罗嗦的事情

有一些编辑器可以解释嵌入在源文件里的由一些特殊标记标明的配置信息。比如,emacs 能够解释被标记成这样的行:

-*- mode: c -*-

或者这样的:

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

Vim 能够解释这样的标记:

/* vim:set sw=8 noet */

不要在源代码中包含任何这样的内容。每个人都有他自己的编辑器配置,你的源文件不 应该覆盖别人的配置。这包括有关缩进和模式配置的标记。人们可以使用他们自己定制 的模式,或者使用其他可以产生正确的缩进的巧妙方法。

19) 内联汇编

在特定架构的代码中,你可能需要内联汇编与 CPU 和平台相关功能连接。需要这么做时 就不要犹豫。然而,当 C 可以完成工作时,不要平白无故地使用内联汇编。在可能的情 况下,你可以并且应该用 C 和硬件沟通。

请考虑去写捆绑通用位元 (wrap common bits) 的内联汇编的简单辅助函数,别去重复 地写下只有细微差异内联汇编。记住内联汇编可以使用 C 参数。

大型,有一定复杂度的汇编函数应该放在 .S 文件内,用相应的 C 原型定义在 C 头文 件中。汇编函数的 C 原型应该使用 asmlinkage

你可能需要把汇编语句标记为 volatile,用来阻止 GCC 在没发现任何副作用后就把它 移除了。你不必总是这样做,尽管,这不必要的举动会限制优化。

在写一个包含多条指令的单个内联汇编语句时,把每条指令用引号分割而且各占一行, 除了最后一条指令外,在每个指令结尾加上 nt,让汇编输出时可以正确地缩进下一条 指令:

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

20) 条件编译

只要可能,就不要在 .c 文件里面使用预处理条件 (#if, #ifdef);这样做让代码更难 阅读并且更难去跟踪逻辑。替代方案是,在头文件中用预处理条件提供给那些 .c 文件 使用,再给 #else 提供一个空桩 (no-op stub) 版本,然后在 .c 文件内无条件地调用 那些 (定义在头文件内的) 函数。这样做,编译器会避免为桩函数 (stub) 的调用生成 任何代码,产生的结果是相同的,但逻辑将更加清晰。

最好倾向于编译整个函数,而不是函数的一部分或表达式的一部分。与其放一个 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 */

附录 I) 参考

The C Programming Language, 第二版 作者:Brian W. Kernighan 和 Denni M. Ritchie. Prentice Hall, Inc., 1988. ISBN 0-13-110362-8 (软皮), 0-13-110370-9 (硬皮).

The Practice of Programming 作者:Brian W. Kernighan 和 Rob Pike. Addison-Wesley, Inc., 1999. ISBN 0-201-61586-X.

GNU 手册 - 遵循 K&R 标准和此文本 - cpp, gcc, gcc internals and indent, 都可以从 https://www.gnu.org/manual/ 找到

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

Kernel process/coding-style.rst,作者 greg@kroah.com 发表于 OLS 2002: http://www.kroah.com/linux/talks/ols_2002_kernel_codingstyle_talk/html/

为什么不应该使用“volatile”类型

C程序员通常认为volatile表示某个变量可以在当前执行的线程之外被改变;因此,在内核 中用到共享数据结构时,常常会有C程序员喜欢使用volatile这类变量。换句话说,他们经 常会把volatile类型看成某种简易的原子变量,当然它们不是。在内核中使用volatile几 乎总是错误的;本文档将解释为什么这样。

理解volatile的关键是知道它的目的是用来消除优化,实际上很少有人真正需要这样的应 用。在内核中,程序员必须防止意外的并发访问破坏共享的数据结构,这其实是一个完全 不同的任务。用来防止意外并发访问的保护措施,可以更加高效的避免大多数优化相关的 问题。

像volatile一样,内核提供了很多原语来保证并发访问时的数据安全(自旋锁, 互斥量,内 存屏障等等),同样可以防止意外的优化。如果可以正确使用这些内核原语,那么就没有 必要再使用volatile。如果仍然必须使用volatile,那么几乎可以肯定在代码的某处有一 个bug。在正确设计的内核代码中,volatile能带来的仅仅是使事情变慢。

思考一下这段典型的内核代码:

spin_lock(&the_lock);
do_something_on(&shared_data);
do_something_else_with(&shared_data);
spin_unlock(&the_lock);

如果所有的代码都遵循加锁规则,当持有the_lock的时候,不可能意外的改变shared_data的 值。任何可能访问该数据的其他代码都会在这个锁上等待。自旋锁原语跟内存屏障一样—— 它 们显式的用来书写成这样 —— 意味着数据访问不会跨越它们而被优化。所以本来编译器认为 它知道在shared_data里面将有什么,但是因为spin_lock()调用跟内存屏障一样,会强制编 译器忘记它所知道的一切。那么在访问这些数据时不会有优化的问题。

如果shared_data被声名为volatile,锁操作将仍然是必须的。就算我们知道没有其他人正在 使用它,编译器也将被阻止优化对临界区内shared_data的访问。在锁有效的同时, shared_data不是volatile的。在处理共享数据的时候,适当的锁操作可以不再需要 volatile —— 并且是有潜在危害的。

volatile的存储类型最初是为那些内存映射的I/O寄存器而定义。在内核里,寄存器访问也应 该被锁保护,但是人们也不希望编译器“优化”临界区内的寄存器访问。内核里I/O的内存访问 是通过访问函数完成的;不赞成通过指针对I/O内存的直接访问,并且不是在所有体系架构上 都能工作。那些访问函数正是为了防止意外优化而写的,因此,再说一次,volatile类型不 是必需的。

另一种引起用户可能使用volatile的情况是当处理器正忙着等待一个变量的值。正确执行一 个忙等待的方法是:

while (my_variable != what_i_want)
    cpu_relax();

cpu_relax()调用会降低CPU的能量消耗或者让位于超线程双处理器;它也作为内存屏障一样出 现,所以,再一次,volatile不是必需的。当然,忙等待一开始就是一种反常规的做法。

在内核中,一些稀少的情况下volatile仍然是有意义的:

  • 在一些体系架构的系统上,允许直接的I/0内存访问,那么前面提到的访问函数可以使用 volatile。基本上,每一个访问函数调用它自己都是一个小的临界区域并且保证了按照 程序员期望的那样发生访问操作。
  • 某些会改变内存的内联汇编代码虽然没有什么其他明显的附作用,但是有被GCC删除的可 能性。在汇编声明中加上volatile关键字可以防止这种删除操作。
  • Jiffies变量是一种特殊情况,虽然每次引用它的时候都可以有不同的值,但读jiffies 变量时不需要任何特殊的加锁保护。所以jiffies变量可以使用volatile,但是不赞成 其他跟jiffies相同类型变量使用volatile。Jiffies被认为是一种“愚蠢的遗留物” (Linus的话)因为解决这个问题比保持现状要麻烦的多。
  • 由于某些I/0设备可能会修改连续一致的内存,所以有时,指向连续一致内存的数据结构 的指针需要正确的使用volatile。网络适配器使用的环状缓存区正是这类情形的一个例 子,其中适配器用改变指针来表示哪些描述符已经处理过了。

对于大多代码,上述几种可以使用volatile的情况都不适用。所以,使用volatile是一种 bug并且需要对这样的代码额外仔细检查。那些试图使用volatile的开发人员需要退一步想想 他们真正想实现的是什么。

非常欢迎删除volatile变量的补丁 - 只要证明这些补丁完整的考虑了并发问题。

注释

[1] https://lwn.net/Articles/233481/ [2] https://lwn.net/Articles/233482/

致谢

最初由Randy Dunlap推动并作初步研究 由Jonathan Corbet撰写 参考Satyam Sharma,Johannes Stezenbach,Jesper Juhl,Heikki Orsila, H. Peter Anvin,Philipp Hahn和Stefan Richter的意见改善了本档。

Next Previous

Linux 魔术数

这个文件是有关当前使用的魔术值注册表。当你给一个结构添加了一个魔术值,你也应该把这个魔术值添加到这个文件,因为我们最好把用于各种结构的魔术值统一起来。

使用魔术值来保护内核数据结构是一个非常好的主意。这就允许你在运行期检查(a)一个结构是否已经被攻击,或者(b)你已经给一个例行程序通过了一个错误的结构。后一种情况特别地有用—特别是当你通过一个空指针指向结构体的时候。tty源码,例如,经常通过特定驱动使用这种方法并且反复地排列特定方面的结构。

使用魔术值的方法是在结构的开始处声明的,如下:

struct tty_ldisc {
        int     magic;
        ...
};

当你以后给内核添加增强功能的时候,请遵守这条规则!这样就会节省数不清的调试时间,特别是一些古怪的情况,例如,数组超出范围并且重新写了超出部分。遵守这个规则,‪这些情况可以被快速地,安全地避免。

  • Theodore Ts’o

    31 Mar 94

给当前的Linux 2.1.55添加魔术表。

Michael Chastain <mailto:mec@shout.net> 22 Sep 1997

现在应该最新的Linux 2.1.112.因为在特性冻结期间,不能在2.2.x前改变任何东西。这些条目被数域所排序。

Krzysztof G.Baranowski <mailto: kgb@knm.org.pl> 29 Jul 1998

更新魔术表到Linux 2.5.45。刚好越过特性冻结,但是有可能还会有一些新的魔术值在2.6.x之前融入到内核中。

Petr Baudis <pasky@ucw.cz> 03 Nov 2002

更新魔术表到Linux 2.5.74。

Fabian Frederick <ffrederick@users.sourceforge.net> 09 Jul 2003

魔术数名 数字 结构 文件
PG_MAGIC ‘P’ pg_{read,write}_hdr include/linux/pg.h
CMAGIC 0x0111 user include/linux/a.out.h
MKISS_DRIVER_MAGIC 0x04bf mkiss_channel drivers/net/mkiss.h
HDLC_MAGIC 0x239e n_hdlc drivers/char/n_hdlc.c
APM_BIOS_MAGIC 0x4101 apm_user arch/x86/kernel/apm_32.c
DB_MAGIC 0x4442 fc_info drivers/net/iph5526_novram.c
DL_MAGIC 0x444d fc_info drivers/net/iph5526_novram.c
FASYNC_MAGIC 0x4601 fasync_struct include/linux/fs.h
FF_MAGIC 0x4646 fc_info drivers/net/iph5526_novram.c
PTY_MAGIC 0x5001 drivers/char/pty.c
PPP_MAGIC 0x5002 ppp include/linux/if_pppvar.h
SSTATE_MAGIC 0x5302 serial_state include/linux/serial.h
SLIP_MAGIC 0x5302 slip drivers/net/slip.h
STRIP_MAGIC 0x5303 strip drivers/net/strip.c
SIXPACK_MAGIC 0x5304 sixpack drivers/net/hamradio/6pack.h
AX25_MAGIC 0x5316 ax_disp drivers/net/mkiss.h
TTY_MAGIC 0x5401 tty_struct include/linux/tty.h
MGSL_MAGIC 0x5401 mgsl_info drivers/char/synclink.c
TTY_DRIVER_MAGIC 0x5402 tty_driver include/linux/tty_driver.h
MGSLPC_MAGIC 0x5402 mgslpc_info drivers/char/pcmcia/synclink_cs.c
USB_SERIAL_MAGIC 0x6702 usb_serial drivers/usb/serial/usb-serial.h
FULL_DUPLEX_MAGIC 0x6969 drivers/net/ethernet/dec/tulip/de2104x.c
USB_BLUETOOTH_MAGIC 0x6d02 usb_bluetooth drivers/usb/class/bluetty.c
RFCOMM_TTY_MAGIC 0x6d02 net/bluetooth/rfcomm/tty.c
USB_SERIAL_PORT_MAGIC 0x7301 usb_serial_port drivers/usb/serial/usb-serial.h
CG_MAGIC 0x00090255 ufs_cylinder_group include/linux/ufs_fs.h
LSEMAGIC 0x05091998 lse drivers/fc4/fc.c
GDTIOCTL_MAGIC 0x06030f07 gdth_iowr_str drivers/scsi/gdth_ioctl.h
RIEBL_MAGIC 0x09051990 drivers/net/atarilance.c
NBD_REQUEST_MAGIC 0x12560953 nbd_request include/linux/nbd.h
RED_MAGIC2 0x170fc2a5 (any) mm/slab.c
BAYCOM_MAGIC 0x19730510 baycom_state drivers/net/baycom_epp.c
ISDN_X25IFACE_MAGIC 0x1e75a2b9 isdn_x25iface_proto_data drivers/isdn/isdn_x25iface.h
ECP_MAGIC 0x21504345 cdkecpsig include/linux/cdk.h
LSOMAGIC 0x27091997 lso drivers/fc4/fc.c
LSMAGIC 0x2a3b4d2a ls drivers/fc4/fc.c
WANPIPE_MAGIC 0x414C4453 sdla_ include/linux/wanpipe.h
CS_CARD_MAGIC 0x43525553 cs_card sound/oss/cs46xx.c
LABELCL_MAGIC 0x4857434c labelcl_info_s include/asm/ia64/sn/labelcl.h
ISDN_ASYNC_MAGIC 0x49344C01 modem_info include/linux/isdn.h
CTC_ASYNC_MAGIC 0x49344C01 ctc_tty_info drivers/s390/net/ctctty.c
ISDN_NET_MAGIC 0x49344C02 isdn_net_local_s drivers/isdn/i4l/isdn_net_lib.h
SAVEKMSG_MAGIC2 0x4B4D5347 savekmsg arch/*/amiga/config.c
CS_STATE_MAGIC 0x4c4f4749 cs_state sound/oss/cs46xx.c
SLAB_C_MAGIC 0x4f17a36d kmem_cache mm/slab.c
COW_MAGIC 0x4f4f4f4d cow_header_v1 arch/um/drivers/ubd_user.c
I810_CARD_MAGIC 0x5072696E i810_card sound/oss/i810_audio.c
TRIDENT_CARD_MAGIC 0x5072696E trident_card sound/oss/trident.c
ROUTER_MAGIC 0x524d4157 wan_device [in wanrouter.h pre 3.9]
SAVEKMSG_MAGIC1 0x53415645 savekmsg arch/*/amiga/config.c
GDA_MAGIC 0x58464552 gda arch/mips/include/asm/sn/gda.h
RED_MAGIC1 0x5a2cf071 (any) mm/slab.c
EEPROM_MAGIC_VALUE 0x5ab478d2 lanai_dev drivers/atm/lanai.c
HDLCDRV_MAGIC 0x5ac6e778 hdlcdrv_state include/linux/hdlcdrv.h
PCXX_MAGIC 0x5c6df104 channel drivers/char/pcxx.h
KV_MAGIC 0x5f4b565f kernel_vars_s arch/mips/include/asm/sn/klkernvars.h
I810_STATE_MAGIC 0x63657373 i810_state sound/oss/i810_audio.c
TRIDENT_STATE_MAGIC 0x63657373 trient_state sound/oss/trident.c
M3_CARD_MAGIC 0x646e6f50 m3_card sound/oss/maestro3.c
FW_HEADER_MAGIC 0x65726F66 fw_header drivers/atm/fore200e.h
SLOT_MAGIC 0x67267321 slot drivers/hotplug/cpqphp.h
SLOT_MAGIC 0x67267322 slot drivers/hotplug/acpiphp.h
LO_MAGIC 0x68797548 nbd_device include/linux/nbd.h
M3_STATE_MAGIC 0x734d724d m3_state sound/oss/maestro3.c
VMALLOC_MAGIC 0x87654320 snd_alloc_track sound/core/memory.c
KMALLOC_MAGIC 0x87654321 snd_alloc_track sound/core/memory.c
PWC_MAGIC 0x89DC10AB pwc_device drivers/usb/media/pwc.h
NBD_REPLY_MAGIC 0x96744668 nbd_reply include/linux/nbd.h
ENI155_MAGIC 0xa54b872d midway_eprom drivers/atm/eni.h
CODA_MAGIC 0xC0DAC0DA coda_file_info fs/coda/coda_fs_i.h
DPMEM_MAGIC 0xc0ffee11 gdt_pci_sram drivers/scsi/gdth.h
YAM_MAGIC 0xF10A7654 yam_port drivers/net/hamradio/yam.c
CCB_MAGIC 0xf2691ad2 ccb drivers/scsi/ncr53c8xx.c
QUEUE_MAGIC_FREE 0xf7e1c9a3 queue_entry drivers/scsi/arm/queue.c
QUEUE_MAGIC_USED 0xf7e1cc33 queue_entry drivers/scsi/arm/queue.c
HTB_CMAGIC 0xFEFAFEF1 htb_class net/sched/sch_htb.c
NMI_MAGIC 0x48414d4d455201 nmi_s arch/mips/include/asm/sn/nmi.h

请注意,在声音记忆管理中仍然有一些特殊的为每个驱动定义的魔术值。查看include/sound/sndmagic.h来获取他们完整的列表信息。很多OSS声音驱动拥有自己从声卡PCI ID构建的魔术值-他们也没有被列在这里。

IrDA子系统也使用了大量的自己的魔术值,查看include/net/irda/irda.h来获取他们完整的信息。

HFS是另外一个比较大的使用魔术值的文件系统-你可以在fs/hfs/hfs.h中找到他们。

Linux 内核驱动接口

写作本文档的目的,是为了解释为什么Linux既没有二进制内核接口,也没有稳定 的内核接口。这里所说的内核接口,是指内核里的接口,而不是内核和用户空间 的接口。内核到用户空间的接口,是提供给应用程序使用的系统调用,系统调用 在历史上几乎没有过变化,将来也不会有变化。我有一些老应用程序是在0.9版本 或者更早版本的内核上编译的,在使用2.6版本内核的Linux发布上依然用得很好 。用户和应用程序作者可以将这个接口看成是稳定的。

执行纲要

你也许以为自己想要稳定的内核接口,但是你不清楚你要的实际上不是它。你需 要的其实是稳定的驱动程序,而你只有将驱动程序放到公版内核的源代码树里, 才有可能达到这个目的。而且这样做还有很多其它好处,正是因为这些好处使得 Linux能成为强壮,稳定,成熟的操作系统,这也是你最开始选择Linux的原因。

入门

只有那些写驱动程序的“怪人”才会担心内核接口的改变,对广大用户来说,既 看不到内核接口,也不需要去关心它。

首先,我不打算讨论关于任何非GPL许可的内核驱动的法律问题,这些非GPL许可 的驱动程序包括不公开源代码,隐藏源代码,二进制或者是用源代码包装,或者 是其它任何形式的不能以GPL许可公开源代码的驱动程序。如果有法律问题,请咨 询律师,我只是一个程序员,所以我只打算探讨技术问题(不是小看法律问题, 法律问题很实际,并且需要一直关注)。

既然只谈技术问题,我们就有了下面两个主题:二进制内核接口和稳定的内核源 代码接口。这两个问题是互相关联的,让我们先解决掉二进制接口的问题。

二进制内核接口

假如我们有一个稳定的内核源代码接口,那么自然而然的,我们就拥有了稳定的 二进制接口,是这样的吗?错。让我们看看关于Linux内核的几点事实:

  • 取决于所用的C编译器的版本,不同的内核数据结构里的结构体的对齐方 式会有差别,代码中不同函数的表现形式也不一样(函数是不是被inline 编译取决于编译器行为)。不同的函数的表现形式并不重要,但是数据 结构内部的对齐方式很关键。
  • 取决于内核的配置选项,不同的选项会让内核的很多东西发生改变:
    • 同一个结构体可能包含不同的成员变量
    • 有的函数可能根本不会被实现(比如编译的时候没有选择SMP支持 一些锁函数就会被定义成空函数)。
    • 内核使用的内存会以不同的方式对齐,这取决于不同的内核配置选 项。
  • Linux可以在很多的不同体系结构的处理器上运行。在某个体系结构上编 译好的二进制驱动程序,不可能在另外一个体系结构上正确的运行。

对于一个特定的内核,满足这些条件并不难,使用同一个C编译器和同样的内核配 置选项来编译驱动程序模块就可以了。这对于给一个特定Linux发布的特定版本提 供驱动程序,是完全可以满足需求的。但是如果你要给不同发布的不同版本都发 布一个驱动程序,就需要在每个发布上用不同的内核设置参数都编译一次内核, 这简直跟噩梦一样。而且还要注意到,每个Linux发布还提供不同的Linux内核, 这些内核都针对不同的硬件类型进行了优化(有很多种不同的处理器,还有不同 的内核设置选项)。所以每发布一次驱动程序,都需要提供很多不同版本的内核 模块。

相信我,如果你真的要采取这种发布方式,一定会慢慢疯掉,我很久以前就有过 深刻的教训…

稳定的内核源代码接口

如果有人不将他的内核驱动程序,放入公版内核的源代码树,而又想让驱动程序 一直保持在最新的内核中可用,那么这个话题将会变得没完没了。 内核开发是持续而且快节奏的,从来都不会慢下来。内核开发人员在当前接口中 找到bug,或者找到更好的实现方式。一旦发现这些,他们就很快会去修改当前的 接口。修改接口意味着,函数名可能会改变,结构体可能被扩充或者删减,函数 的参数也可能发生改变。一旦接口被修改,内核中使用这些接口的地方需要同时 修正,这样才能保证所有的东西继续工作。

举一个例子,内核的USB驱动程序接口在USB子系统的整个生命周期中,至少经历 了三次重写。这些重写解决以下问题:

  • 把数据流从同步模式改成非同步模式,这个改动减少了一些驱动程序的 复杂度,提高了所有USB驱动程序的吞吐率,这样几乎所有的USB设备都 能以最大速率工作了。
  • 修改了USB核心代码中为USB驱动分配数据包内存的方式,所有的驱动都 需要提供更多的参数给USB核心,以修正了很多已经被记录在案的死锁。

这和一些封闭源代码的操作系统形成鲜明的对比,在那些操作系统上,不得不额 外的维护旧的USB接口。这导致了一个可能性,新的开发者依然会不小心使用旧的 接口,以不恰当的方式编写代码,进而影响到操作系统的稳定性。 在上面的例子中,所有的开发者都同意这些重要的改动,在这样的情况下修改代 价很低。如果Linux保持一个稳定的内核源代码接口,那么就得创建一个新的接口 ;旧的,有问题的接口必须一直维护,给Linux USB开发者带来额外的工作。既然 所有的Linux USB驱动的作者都是利用自己的时间工作,那么要求他们去做毫无意 义的免费额外工作,是不可能的。 安全问题对Linux来说十分重要。一个安全问题被发现,就会在短时间内得到修 正。在很多情况下,这将导致Linux内核中的一些接口被重写,以从根本上避免安 全问题。一旦接口被重写,所有使用这些接口的驱动程序,必须同时得到修正, 以确定安全问题已经得到修复并且不可能在未来还有同样的安全问题。如果内核 内部接口不允许改变,那么就不可能修复这样的安全问题,也不可能确认这样的 安全问题以后不会发生。 开发者一直在清理内核接口。如果一个接口没有人在使用了,它就会被删除。这 样可以确保内核尽可能的小,而且所有潜在的接口都会得到尽可能完整的测试 (没有人使用的接口是不可能得到良好的测试的)。

要做什么

如果你写了一个Linux内核驱动,但是它还不在Linux源代码树里,作为一个开发 者,你应该怎么做?为每个发布的每个版本提供一个二进制驱动,那简直是一个 噩梦,要跟上永远处于变化之中的内核接口,也是一件辛苦活。 很简单,让你的驱动进入内核源代码树(要记得我们在谈论的是以GPL许可发行 的驱动,如果你的代码不符合GPL,那么祝你好运,你只能自己解决这个问题了, 你这个吸血鬼<把Andrew和Linus对吸血鬼的定义链接到这里>)。当你的代码加入 公版内核源代码树之后,如果一个内核接口改变,你的驱动会直接被修改接口的 那个人修改。保证你的驱动永远都可以编译通过,并且一直工作,你几乎不需要 做什么事情。

把驱动放到内核源代码树里会有很多的好处:

  • 驱动的质量会提升,而维护成本(对原始作者来说)会下降。
  • 其他人会给驱动添加新特性。
  • 其他人会找到驱动中的bug并修复。
  • 其他人会在驱动中找到性能优化的机会。
  • 当外部的接口的改变需要修改驱动程序的时候,其他人会修改驱动程序
  • 不需要联系任何发行商,这个驱动会自动的随着所有的Linux发布一起发 布。

和别的操作系统相比,Linux为更多不同的设备提供现成的驱动,而且能在更多不 同体系结构的处理器上支持这些设备。这个经过考验的开发模式,必然是错不了 的 😃

感谢

感谢 Randy Dunlap, Andrew Morton, David Brownell, Hanna Linder, Robert Love, and Nishanth Aravamudan 对于本文档早期版本的评审和建议。

英文版维护者: Greg Kroah-Hartman <greg@kroah.com>

程序设计语言

内核是用C语言 c-language 编写的。更准确地说,内核通常是用 gcc-std=gnu89 gcc-c-dialect-options 下编译的:ISO C90的 GNU 方言( 包括一些C99特性)

这种方言包含对语言 gnu-extensions 的许多扩展,当然,它们许多都在内核中使用。

对于一些体系结构,有一些使用 clangicc 编译内核 的支持,尽管在编写此文档时还没有完成,仍需要第三方补丁。

属性

在整个内核中使用的一个常见扩展是属性(attributes) gcc-attribute-syntax 属性允许将实现定义的语义引入语言实体(如变量、函数或类型),而无需对语言进行 重大的语法更改(例如添加新关键字) n2049

在某些情况下,属性是可选的(即不支持这些属性的编译器仍然应该生成正确的代码, 即使其速度较慢或执行的编译时检查/诊断次数不够)

内核定义了伪关键字(例如, pure ),而不是直接使用GNU属性语法(例如, __attribute__((__pure__)) ),以检测可以使用哪些关键字和/或缩短代码, 具体 请参阅 include/linux/compiler_attributes.h

posted @ 2020-03-11 22:23  Silly-man  阅读(261)  评论(0编辑  收藏  举报