Linux内核编程规范与代码风格
这是⼀篇阐述Linux内核编程代码风格的⽂档,译者以学习为⽬的进⾏翻译。
1 缩进
Tab的宽度是⼋个字符,因此缩进的宽度也是⼋个字符。有些异教徒想让缩进变成四个字符,甚⾄是两个字符的宽度,这些⼈和那些把 PI 定义为 3 的⼈是⼀个路⼦的。
注意:缩进的全部意义在于清晰地定义语句块的开始与结束,特别是当你盯着屏幕20个⼩时之后,你会发现长的缩进宽度的作⽤。
现在有些⼈说⼋个字符的宽度太宽了,这会让代码往右移很远,在⼀块⼋⼗字符宽的屏幕上,这样的代码会很难阅读。对此的回答是,如果你写的代码需要超过三层的缩进,那么你把⼀切都搞砸了,你应该修复你的程序。
简⽽⾔之,⼋个字符宽度的缩进让代码更容易阅读,并且额外的好处就是提醒你,不要在⼀个函数⾥写太多层的嵌套逻辑。请记住这个警⽰。
switch语句的缩进⽅式是让case与switch对齐:
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 换⾏
规范代码风格的⽬的是提⾼代码的可读性和维护性。
单⾏的宽度限制为⼋⼗列,这是强烈推荐的设置。
任何⼀⾏超过⼋⼗列宽度的语句都应该拆分成多个⾏,除⾮超过⼋⼗列的部分可以提⾼可读性且不会隐藏信息。拆分出来的⼦句长度总是应该⽐其主句要短,并且应该尽量靠右。这条法则同样适⽤于⼀个有很长的参数列表的函数头。然⽽,千万不要把⽤户可见的字符串,⽐如 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
}
异教徒们会认为这样的风格是不⼀致的,但是所有有脑⼦的⼈都知道尽管是 K&R 也是不⼀致的(译者注:K&R这本书的第⼀版和第⼆版有不⼀致的地⽅)。除此之外,我们知道函数是很特殊的,在 C 语⾔中,你不能有嵌套函数。
注意到,右括号⼀般是单独成⼀⾏的,除⾮右括号之后紧随着紧密结合的语句,例如 do-while 语句和 if 语句:
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();
这⼀点不适⽤于只有⼀个 case 有单⾏,其他 case 有多⾏的情况:
if (condition) {
do_this();
do_that();
} else {
otherwise();
}
在⼀个循环中超过⼀个语句的情况也同样需要使⽤括号:
while (condition) {
if (test)
do_something();
}
3.1 空格
Linux 内核风格的空格主要⽤在⼀些关键字上,即在关键字之后添⼀个空格。值得关注的例外是⼀些长得像函数的关键字,⽐如:sizeof, typeof, alignof, attribute,在 Linux 中,这些关键字的使⽤都会带上⼀对括号,尽管在 C 语⾔的使⽤上并不需要带上括号。
所以在下⾯这些关键字之后添加⼀个空格:
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 通常会警告你,让你除去这些多余的空格,并且可以帮你删掉这些东西。但是,如果你让 Git ⼀直帮你这样修补你的代码,这很可能导致代码⾏的上下错乱,之后的⾃动修补的失败。
4 命名
C 是⼀种简洁粗旷的语⾔,因此,你的命名也应该是简洁的。C 程序员不会像 Modula-2 和 Pascal 程序员那样使⽤ThisVariableIsATemporaryCounter 这种“可爱”的名字,⼀个 C 程序员会把这种变量命名为 tmp ,如此简洁易写。
尽管看到⼀个混合⼤⼩写的名字让⼈皱眉,不过对于全局变量来说,⼀个具有描述性的名字还是很有必要的。去调⽤⼀个名为 foo 的全局函数同样让⼈难以接受。
全局变量(只有当你真正需要的时候才⽤它)和全局函数需要使⽤描述性的名字。如果你有⼀个计算活跃
⽤户数量的函数,你应该起这样⼀个名字count_active_users()或者类似的,⽽不是这样⼀个名字cntusr()。
起⼀个包含函数类型的名字(匈⽛利命名法)是摧残⼤脑的⾏为,编译器知道函数的类型并且会检查类型,这样的名字不会起到任何帮助,它仅仅会迷惑程序员。所以,也难怪微软做出了那么多充满了 bug 的程序。
局部变量名应该简短,如果你需要写⼀个循环,定义⼀个计数器,在不产⽣歧义的情况下,你⼤可命名为 i ,命名为 loop_counter 是⽣产⼒很低的⾏为。同样地,tmp 可以是任何类型的临时变量。
如果你担⼼会弄混变量名,那么你遇到了另⼀个问题,你患上了函数增长荷尔蒙失调综合症。
5 T yp e d e f s
请不要使⽤ vps_t 这种东西,这是 typedef 的错误⽤法,当你看到
vps_t a;
这种写法时,它究竟是个什么东西?相反,如果是这样的写法
struct virtual_container *a;
你就很容易知道 a 代表着什么。
很多⼈认为 typedef 是⽤来帮助提⾼可读性的,但是事实往往不是这样的。typedef 仅仅有如下⽤处:
a. 封装对象(typedef 可以⽅便的隐藏对象)
例如,pte_t 会把对象封装起来,你仅仅只能通过合适的“访问函数”(成员函数)来访问这个对象。
注意:封装和“访问函数”(成员函数)本⾝就不是好东西,我们使⽤ pte_t 这种东西的理由就是,它指向的对象本⾝绝对没有东西可以访问(我们压根⼉不使⽤封装和成员函数那⼀套)。
b. 指明整数类型,这种抽象可以帮助我们避免⼀些使⽤ int 和 long 的疑虑
u8/u16/u32 是完美的使⽤ typedef 的例⼦。
注意:你必须要有明确的理由来使⽤这些⽤法,如果⼀些地⽅使⽤的本⾝就是 unsigned long ,那么你没有任何理由这样做
typedef unsigned long myflags_t;
但是如果你有明确的理由来解释为什么在某种情况下使⽤ unsigned int,⽽在其他情况下使⽤ unsigned
long,那么⼤可使⽤ typedef。
c. 使⽤ sparse 去新建⼀个类型来做类型检查
d. 在某些情况下新建⼀个与 C99 标准相等的类型
尽管只需要花⼀⼩段眼睛和⼤脑的时间来适应新标准的类型,如 uint32_t,但是⼀些⼈还是反对使⽤他们。
因此,你可以使⽤ Linux 独有的 u8/u16/u32/u64 和他们的有符号版本,也可以使⽤和他们等价的新标准的类型,他们的使⽤都不是强制的。
当你所编辑的代码已经使⽤了某⼀种版本时,你应该按照原样使⽤相同的版本。
e. ⽤户空间中的类型安全
⽤户空间中的某些特定的结构体中,我们不能使⽤ C99 定义的新类型以及上述的 u32,取⽽代之,我们统⼀使⽤ __u32 之类的类型。
也许还有其他情况,但是基本的规则就是,如果你不能满⾜上述其中⼀条情况,你就永远不要使⽤ typedef。
通常,⼀个指针或者⼀个有可访问元素的结构体,都不应该使⽤ typedef。
6 函数
函数应该短⼩精悍,⼀个函数只⼲⼀件事。⼀个函数的代码两个屏幕就应该装得下(ISO/ANSI标准屏幕⼤⼩是80x24),简单说就是,做⼀件事并且把它做好。
数的最⼤长度与函数的复杂度和缩进程度成反⽐,所以,如果你有⼀个简单的函数,函数⾥⾯只是需要处理⼀个⼜⼀个的 case,每个 case 只是⼲⼀些⼩事,函数长度长⼀些也没关系。
然⽽,如果你的函数⼗分复杂,你怀疑⼀个不像你⼀样天才的⾼中⽣看不懂,你应该遵守函数最⼤的长度的限制,使⽤⼀些有描述性名称的辅助函数。如果你认为函数的性能⾄关重要,你可以让编译器把这些辅助函数编译成内联函数,⼀般情况下编译器可以⽐你做得更好。
另⼀个测量函数的因素是局部变量的数量,他们不应该超出5-10个这个范围,否则你就犯了⼀些错误。重新思考这个函数,把它拆分成更⼩的⼏段。⼈类的⼤脑⼀般只能同时关注七件不同的事,更多需要关注的事情意味着更多的困扰。尽管你认为你是个天才,但是你也希望理解⼀段你两周之前写的代码。
字符串长度测量函数
在源⽂件中,⽤⼀个空⾏分割不同的函数,如果函数需要导出到外部使⽤,那么它对应的 EXPORT 宏应当紧随在函数之后,例如:
int system_is_up(void)
{
return system_state == SYSTEM_RUNNING;
}
EXPORT_SYMBOL(system_is_up);
函数原型中,参数名应该与参数类型引起写出来,尽管 C 语⾔允许只写上参数类型,但是我们更推荐参数名,因为这是⼀种为读者提供有价值信息的简单⽅式。
不要在函数原型之前使⽤extern关键字,因为这是不必要且多余的。
7 集中函数出⼝
尽管许多⼈反对,但是 goto 语句频繁地以⽆条件跳转的形式被编译器使⽤。
当函数有多个出⼝,并且返回之前需要做很多相似的⼯作时,⽐如清理空间,这时候 goto 语句是⼗分⽅便的。当然了,如果没有类似的清理⼯作要在返回之前做,那么直接返回即可。
根据 goto 的作⽤来决定⼀个 label 的名字,如果 goto 语⾔要去释放缓存,那么out_free_buffer:会是⼀个好名字。避免使⽤ 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 被称作 one err bug,它长得像这样:
err:
kfree(foo->bar);
kfree(foo);
return ret;
bug 在于某些 goto 语句跳转到此时,foo 仍然是 NULL,修复此 bug 的简单⽅式就是将⼀个 label 拆分成两个,err_free_bar:和err_free_foo::err_free_bar:
kfree(foo->bar);
err_free_foo:
kfree(foo);
return ret;
事实上,你应该进⾏测试,模拟错误情况的发⽣,测试所有的出⼝代码。
8 注释
注释是好的,但是要避免过分注释。永远不要去尝试解释你的代码如何⼯作,⽽是花时间在写出好的代码来,解释⼀段烂代码是浪费时间。
⼀般来说,你应该去说明你的代码做了什么,⽽不是怎么做。同样地,尽量避免在函数体内写注释,如果你的函数如此复杂,以致于你需要在函数体内分⼏段注释来解释,那么你应该回到第六节去看看。你可以写⼀⼩段的注释来标记或者提醒⼤家哪些地⽅写得真聪明(或者真烂),但是不要做得太过分。除此之外,你应该把注释写在函数开头,告诉⼈们这个函数⼲了什么,为什么要这样⼲。
多⾏注释推荐的格式如下:
/*
* 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.
*/
对⼀些数据和变量进⾏注释也是必要的,⽆论他们是基本类型的还是派⽣类型的。为了进⾏注释,你应该在⼀⾏内只声明⼀个变量,不要使⽤逗号进⾏多个声明,这让你有地⽅对每⼀个变量进⾏注释。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。