Linux 内核代码风格

1、缩进

制表符是 8 个字符,所以缩进也是 8 个字符。如果需要 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 列,强烈建议遵守这个惯例。

不要打散对用户可见的字符串,例如 printk 信息,因为这样就很难使用 grep 搜索到它们。

3、大括号的放置

首选的方式,就像 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
}

结束大括号独自占据一行,除非它后面跟着同一个语句的剩余部分,也就是 do 语 句中的 “while” 或者 if 语句中的 “else”,像这样:

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

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

也请注意这种大括号的放置方式也能使空 (或者差不多空的) 行的数量最小化,同时不失可读性。

当只有一个单独的语句的时候,不用加不必要的大括号。

if (condition)
        action();

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

这并不适用于只有一个条件分支是单语句的情况;这时所有分支都要使用大括号:

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

4、空格的放置

Linux 内核的空格使用方式主要取决于它是用于函数还是关键字。

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

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

后缀自加和自减一元操作符前不加空格:

++  --

前缀自加和自减一元操作符后不加空格:

++  --

. 和 -> 结构体成员操作符前后不加空格。

5、命名

C 是一个简朴的语言,你的命名也应该这样。和 Modula-2 和 Pascal 程序员不同, C 程序员不使用类似 ThisVariableIsATemporaryCounter 这样华丽的名字。

不过,虽然混用大小写的名字是不提倡使用的,但是全局变量还是需要一个具描述性的名字,就像全局函数。

如果有一个可以计算活动用户数量的函数,你应该命名为 count_active_users() 或者类似的名字,而不是命名为 cntuser() 。

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

6、typedef

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

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

vps_t a;

这代表什么意思呢?

相反,如果是这样

struct virtual_container *a;

你就知道 a 是什么了。

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

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

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

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

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

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

7、函数

函数应该简短而漂亮,并且只完成一件事情。函数应该可以一屏或者两屏显示完 ,只做一件事情,而且把它做好。

函数的另外一个衡量标准是本地变量的数量。此数量不应超过 5-10 个,否则你的函数就有问题了。

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

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

在函数原型中,包含函数名和它们的数据类型。在 Linux 里提倡这样的做法,因为这样可以给读者提供更多的有价值的信息。

8、集中的函数退出途径

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

9、注释

你的注释是要告诉别人你的代码做了什么,而不是怎么做的。不要把注释放在一个函数体内部,你可以做一些小注释来注明或警告某些很聪明 (或者槽糕) 的做法,但不要加太多。

你应该做的,是把注释放在函数的头部,告诉人们它做了什么, 也可以加上它做这些事情的原因。

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

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

注释数据也是很重要的,不管是基本类型还是衍生类型。为了方便实现这一点,每一行应只声明一个数据。

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.

11、宏和枚举

用于定义常量的宏的名字及枚举里的标签需要大写,不过形如函数的宏的名字可以用小写字母。

#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)

非常不好。它看起来像一个函数,不过却能导致 调用 它的函数退出;不要打 乱读者大脑里的语法分析器。

(2)依赖于一个固定名字的本地变量的宏:

#define FOO(val) bar(index, val)

可能看起来像是个不错的东西,不过它非常容易把读代码的人搞糊涂,而且容易导致看起来不相关的改动带来错误。

(3)作为左值的带参数的宏:

FOO(x) = y;

如果有人把 FOO 变成一个内联函数的话,这种用法就会出错了。

(4)忘记了优先级:

使用表达式定义常量的宏必须将表达式置于一对小括号之内。带参数的宏也要注意此类问题。

#define CONSTANT 0x4000
#define CONSTEXP (CONSTANT | 3)

(5)在宏里定义类似函数的本地变量时命名冲突:

#define FOO(x)                          \
({                                      \
        typeof(x) ret;                  \
        ret = calc_ret(x);              \
        (ret);                          \
})


ret 是本地变量的通用名字 - __foo_ret 更不容易与一个已存在的变量冲突。

12、打印内核消息

不要用不规范的单词比如 dont,而要用 do not 或者 don't 。保证这些信息简单明了,无歧义。

<linux/device.h> 里有一些驱动模型诊断宏,你应该使用它们,以确保信息对应于正确的设备和驱动,并且被标记了正确的消息级别。这些宏有:dev_err(), dev_warn(), dev_info() 等等。

对于那些不和某个特定设备相关连的信息,<linux/printk.h> 定义 了 pr_notice(), pr_info(), pr_warn(), pr_err() 和其他。

13、分配内存

内核提供了下面的一般用途的内存分配函数: kmalloc(), kzalloc(), kmalloc_array(), kcalloc(), vmalloc() 和 vzalloc()。

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

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

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

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

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

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

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

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

14、内联的使用

有一个常见的误解是 内联是 gcc 提供的可以让代码运行更快的一个选项。不过 inline 的过度使用会使内核变大,从而使整个系统运行速度变慢。

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

对于只用了一次的 static 函数,即使不加 inline,gcc 也可以自动使其内联。

15、函数返回值及命名

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

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

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

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

16、不要重新发明内核宏

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

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

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

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

还有可以做严格的类型检查的 min() 和 max() 宏。

17、内联汇编

在特定架构的代码中,你可能需要内联汇编与 CPU 和平台相关功能连接。然而,当 C 可以完成工作时,不要使用内联汇编。

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

你可能需要把汇编语句标记为 volatile,用来阻止 GCC 在没发现任何副作用后就把它移除了。

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

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

18、条件编译

尽量不要在 .c 文件里面使用预处理条件 (#if, #ifdef);这样做让代码更难阅读并且更难去跟踪逻辑。

而是在头文件中用预处理条件提供给那些 .c 文件使用,再给 #else 提供一个 stub 版本,然后在 .c 文件内无条件地调用那些函数。

在代码中,尽可能地使用 IS_ENABLED 宏来转化某个 Kconfig 标记为 C 的布尔 表达式,并在一般的 C 条件中使用它:

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

编译器会做常量折叠,然后就像使用 #ifdef 那样去包含或排除代码块,所以这不会带来任何运行时开销。

在任何有意义的 #if 或 #ifdef 块的末尾,在 #endif 同一行的后面写下注解,注释这个条件表达式。例如:

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

  • 5
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值