转自华清远见《Linux设备驱动开发详解》
----------------------------------------------------------------------------------------------------------------------------
1 Linux 程序命名习惯
Linux 程序的命名习惯和 Windows 程序的命名习惯及著名的匈牙利命名法有很大的不同。 在Windows 程序中,习惯以如下方式命名宏、变量和函数:#define PI 3.1415926 /* 用大写字母代表宏*/
int minValue, maxValue; /* 变量:第一个单词全写,其后的单词第一个字母小写*/
void SendData(void); /* 函数:所有单词第一个字母都大写*/
这种命名方式在程序员中非常盛行,意思表达清晰且避免了匈牙利法的臃肿,单词之间通过首字母大写来区分。通过第一个单词的首字母是否大写可以区分名称属于变量还是属于函数,而看到整串的大写字母可以断定为宏。
但是Linux 不以这种习惯命名,对应于上面的一段程序,在Linux 程序的命名如下所示:
#define PI 3.1415926
int min_value, max _value;
void send_data(void);
2 GNU C与ANSI C
Linux 系统上可用的 C 编译器是 GNU C编译器,它建立在自由软件基金会的编程许可证的基础上,因此可以自由发布。GNU C对标准C 进行一系列扩展,以增强标准C 的功能。1.零长度数组
GNU C允许使用零长度数组,在定义变长对象的头结构时,这个特性非常有用。例如:struct var _data
{
int len;
char data[0];
};
char data[0]仅仅意味着程序中通过var_data 结构体实例的 data[index] 成员可以访问 len 之后的第index
个地址,它并没有为data[]数组分配内存,因此 sizeof(struct var_data)=sizeof(int) 。
假设struct var_data的数据域保存在 struct var_data紧接着的内存区域,通过如下代码可以遍历这些数
据:
struct var _data s;
...
for (i = 0; i < s.len; i++)
{
printf("%02x", s.data[i]);
}
2.case 范围
GNU C支持 case x…y 这样的语法,区间[x,y] 的数都会满足这个 case 的条件,请看下面的代码:switch (ch)
{
case '0'... '9': c -= '0';
break;
case 'a'... 'f': c -= 'a' - 10;
break;
case 'A'... 'F': c -= 'A' - 10;
break;
}
代码中的case '0'... '9' 等价于标准C 中的如下代码:
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
3.语句表达式
GNU C 把包含在括号中的复合语句看做是一个表达式,称为语句表达式,它可以出现在任何允许表达式的地方。我们 可以在语句表达式中使用原本只能在复合语句中使用的循环变量、局部变量等,例如:#define min_t(type,x,y) \
({ type _ _x = (x); type _ _y = (y); _ _x < _ _y ? _ _x: _ _y; })
int ia, ib, mini;
float fa, fb, minf;
mini = min _t(int, ia, ib);
minf = min _t(float, fa, fb);
因为重新定义了_ _xx 和_ _y这两个局部变量,所以以上述方式定义的宏将不会有副作用。在标准C中,对应的如下宏则会产生副作用:
#define min(x,y) ((x) < (y) ? (x) : (y))
代码min(++ia,++ib)会被展开为((++ia) < (++ib) ? (++ia): (++ib)) ,传入宏的参数被增加两次。
4.typeof 关键字
typeof(x) 语句可以获得 x 的类型,因此,我们可以借助typeof 重新定义min 这个宏:#define min(x,y) ({ \
const typeof(x) _x = (x); \
const typeof(y) _y = (y); \
(void) (& _x == &_y); \
_x < _y ? _x : _y; })
我们不需要像min_t(type,x,y)这个宏那样把type 传入,因为通过 typeof(x) 、typeof(y) 可以获得type 。代码行(void) (&_x == &_y) 的作用是检查_x 和_y的类型是否一致。
5.可变参数的宏
标准C 只支持可变参数的函数,意味着函数的参数是不固定的,例如 printf() 函数的原型为:int printf( const char *format [, argument]... );
而在GNU C中,宏也可以接受可变数目的参数,例如:
#define pr _debug(fmt,arg...) \
printk(fmt,##arg)
这里arg 表示其余的参数可以是零个或多个,这些参数以及参数之间的逗号构成 arg 的值,在宏扩展时替换 arg ,例如下列代码:
pr_debug("%s:%d",filename,line)
会被扩展为:
printk("%s:%d", filename, line)
使用“##”的原因是处理 arg 不代表任何参数的情况,这时候,前面的逗号就变得多余了。使用“##”之后,GNU C预处理器会丢弃前面的逗号,这样,代码:
pr_debug("success!\n")
会被正确地扩展为:
printk("success!\n")
而不是:
printk("success!\n",)
6.标号元素
标准C 要求数组或结构体的初始化值必须以 固定的顺序出现,在 GNU C 中,通过指定索引或结构体成员名,允许初始化值以 任意顺序出现。指定数组索引的方法是在初始化值前添加“[INDEX] = ”,当然也可以用“[FIRST… LAST] =”的形式指定一个范围。例如下面的代码定义一个数组,并把其中的所有元素赋值为0 :
unsigned char data[MAX] = { [0 ... MAX-1] = 0 };
下面的代码借助结构体成员名初始化结构体:
struct file_operations ext2 _file_operations =
{
llseek: generic _file_llseek,
read: generic _file_read,
write: generic_file_write,
ioctl: ext2 _ioctl,
mmap: generic _file_mmap,
open: generic _file_open,
release: ext2 _release_file,
fsync: ext2 _sync_file,
};
但是,Linux 2.6 推荐类似的代码应该尽量采用标准 C 的方式,如下所示:
struct file_operations ext2 _file_operations =
{
.llseek = generic_file_llseek,
.read = generic_file_read,
.write = generic_file_write,
.aio _read = generic _file_aio _read,
.aio _write = generic_file_aio _write,
.ioctl = ext2_ioctl,
.mmap = generic_file_mmap,
.open = generic_file_open,
.release = ext2 _release_file,
.fsync = ext2_sync_file,
.readv = generic_file_readv,
.writev = generic_file_writev,
.sendfile = generic_file_sendfile,
};
7.当前函数名
GNU C 预定义了两个标志符保存当前函数的名字,_ _FUNCTION_ _保存函数在源码中的名字,_ _PRETTY_FUNCTION_ _保存带语言特色的名字。在C 函数中,这两个名字是相同的。void example()
{
printf("This is function:%s", _ _FUNCTION _ _);
}
代码中的_ _FUNCTION_ _ 意味着字符串“example”。
8.特殊属性声明
GNU C允许声明函数、变量和类型的特殊属性,以便进行手工的代码优化和定制代码检查的方法。指定一个声明的属性,只需要在声明后添加__attribute__ (( ATTRIBUTE ))。其中 ATTRIBUTE 为属性说明,如果存在多个属性,则以逗号分隔。GNU C 支持noreturn、format 、section 、aligned 、packed 等十多个属性。noreturn 属性作用于函数,表示该函数从不返回。这会让编译器优化代码,并消除不必要的警告信息。
例如:
# define ATTRIB _NORET _ _attribute_ _((noreturn)) ....
asmlinkage NORET_TYPE void do_exit(long error _code) ATTRIB_NORET;
format 属性也用于函数,表示该函数使用 printf 、scanf 或 strftime 风格的参数,指定 format 属性可以让编译器根据格式串检查参数类型。例如:
asmlinkage int printk(const char * fmt, ...) _ _attribute_ _ ((format (printf, 1, 2)));
上述代码中的第一个参数是格式串,从第二个参数开始都会根据printf() 函数的格式串规则检查参数。
unused 属性作用于函数和变量,表示该函数或变量可能不会被用到,这个属性可以避免编译器产生警告信息。
aligned 属性用于变量、结构体或联合体,指定变量、结构体或联合体的对界方式,以字节为单位,例
如:
struct example_struct
{
char a;
int b;
long c;
} _ _attribute_ _((aligned(4)));
表示该结构类型的变量以4 字节对界。
packed 属性作用于变量和类型,用于变量或结构体成员时表示使用最小可能的对界,用于枚举、结构体或联合体类型时表示该类型使用最小的内存。例如:
struct example_struct
{
char a;
int b;
long c _ _attribute_ _((packed));
};
编译器对结构体成员及变量对界的目的是为了更快地访问结构体成员及变量占据的内存。例如,对于一个 32 位的整型变量,若以 4 字节方式存放(即低两位地址为00 ),则CPU 在一个总线周期内就可以读取32 位;若不然,CPU 需要两次总线周期才能组合
为一个 32 位整型。
9.内建函数
GNU C提供了大量的内建函数,其中大部分是标准 C 库函数的 GNU C编译器内建版本,例如memcpy()等,它们与对应的标准C 库函数功能相同。不属于库函数的其他内建函数的命名通常以_ _builtin 开始,如下所示。
内建函数_ _builtin_return_address (LEVEL) 返回当前函数或其调用者的返回地址,参数 LEVEL 指定调用栈的级数,如0 表示当前函数的返回地址,1 表示当前函数的调用者的返回地址。
内建函数_ _builtin_constant_p(EXP)用于判断一个值是否为编译时常数,如果参数EXP 的值是常数,函数返回 1,否则返回 0。
内建函数_ _builtin_expect(EXP, C) 用于为编译器提供分支预测信息,其返回值是整数表达式 EXP 的值,C 的值必须是编译时常数。
例如,下面的代码检测第1 个参数是否为编译时常数以确定采用参数版本还是非参数版本的代码:
#define test_bit(nr,addr) \
(_ _builtin_constant _p(nr) ? \
constant _test_bit((nr),(addr)) : \
variable _test_bit((nr),(addr)))
3 do { } while(0)
在Linux 内核中,经常会看到 do{}while(0)这样的语句,许多人开始都会疑惑,认为 do{}while(0)毫无意义,因为它只会执行一次,加不加do{}while(0)效果是完全一样的,其实do{}while(0)主要用于宏定义中。这里用一个简单点的宏来演示:
#define SAFE_FREE(p) do{ free(p); p = NULL;} while(0)
假设这里去掉do…while(0),即定义SAFE_DELETE 为:
#define SAFE_FREE(p) free(p); p = NULL;
那么以下代码:
if(NULL != p)
SAFE_DELETE(p)
else
...//do something
会被展开为:
if(NULL != p)
free(p); p = NULL;
else
...//do something
展开的代码中存在两个问题:
(1 )if 分支后有两个语句,导致 else 分支没有对应的 if ,编译失败;
(2 )假设没有 else 分支,则 SAFE_FREE 中的第二个语句无论 if 测试是否通过都会执行。 将SAFE_FREE 的定义加上{} 就可以解决上述问题了,即:
#define SAFE_FREE(p) { free(p); p = NULL;}
这样,代码
if(NULL != p)
SAFE_DELETE(p)
else
...//do something
会被展开为:
if(NULL != p)
{ free(p); p = NULL; }
else
...//do something
但是,在C 程序中,每个语句后面加分号是一种约定俗成的习惯,那么,如下代码:
if(NULL != p)
SAFE_DELETE(p);
else
...//do something
将被扩展为:
if(NULL != p)
{ free(p); p = NULL; };
else
...//do something
这样,else 分支就又没有对应的if 了,编译将无法通过。假设用了do{}while(0),情况就不一样了,同样的代码会被展开为:
if(NULL != p)
do{ free(p); p = NULL;} while(0);
else
...//do something
不会再出现编译问题。 do{}while(0)的使用完全是为了保证宏定义的使用者能无编译错误地使用宏,它不对其使用者做任何假设。
4 goto
用不用goto 一直是一个著名的争议话题,Linux 内核源代码中对goto 的应用非常广泛,但是一般 只限于错误处理中,其结构如下:if(register_a()!=0)
{
goto err;
}
if(register_b()!=0)
{
goto err1;
}
if(register_c()!=0)
{
goto err2;
}
if(register_d()!=0)
{
goto err3;
}
...
err3:
unregister _c();
err2:
unregister _b();
err1:
unregister _a();
err:
return ret;
用于错误处理的goto 的用法简单而高效,只需 保证在错误处理时注销、资源释放的顺序与正常的注册、释放申请的顺序相反。