摘要:本文主要讲述了linux内核开发过程中,需要注意的有关提高代码可移植性的若干技巧,同样对编写Linux应用程序也有较好的参考意义。干货满满,请君一阅。
1. 避免使用显式常量
- 这点应该很好理解,如果你的代码中充斥着各种硬件相关的或者体系特点的常量值,如果将来改起来将是非常麻烦的。
- 解决办法:通过宏定义(预处理宏),并在头文件中予以说明。
2. 不要假定系统每秒有多少个滴答值
- 很多人,特别是在i386体系下编程的同学,习惯默认1秒是1000个滴答,但事实上并不是每隔平台都是按照这个速度运行的。
- 解决办法:任意时候,涉及到使用滴答数来计算时间间隔时,使用
HZ
(每秒的定时器中断数)。例如半秒就是HZ/2个滴答数,一毫秒就是HZ/1000个滴答数。
3.不要假定页大小是4K
不同平台的页大小是不同的,它从4KB到64KB。
解决办法:
使用宏定义
PAGE_SIZE
和PAGE_SHIFT
(<asm/page.h>)。后者通常用于将一个地址右移位PAGE_SHIFT位后,得到该地址所在的页号。当页大小为4KB时,该值取12。如果你在用户空间编程,可以使用
size_t getpagesize(void);
(<unistd.h>)库函数来获取当前系统(不是实际某硬件的页大小)的页大小(字节数),如果页大小为4KB,则函数返回4096。使用内核函数
int get_order(unsigned long)
(<asm/page.h>),它一般和get_free_page()
函数搭配使用,用来计算分配相应字节内存时需要的order数:#include <asm/page.h> char *buff; int order; order = get_order (8*1024); //要分配的字节数必须是2的幂! buff = get_free_pages (GFP_KERNEL, order); //若在用户空间使用需要在函数前加双下划线
4. 不要假定字节序
不同的体系结构其存储字节的顺序有大端和小端的区别。
解决办法:
宏定义:
__BIG_ENDIAN
和__LITTLE_ENDIAN
(<asm/byteorder.h>),任意系统会根据自己的字节序定义相应的宏变量。当处理字节序问题时,你可以采取如下编码结构:#ifdef __BIG_ENDIAN ... #else ... #endif
前面的方法虽然可以解决问题,但带来了代码冗余。所以更好的办法是利用linux内核提拱的一套宏定义来处理字节序的转换:
#include <linux/byteorder/big_endian.h> #include <linux/byteorder/little_endian.h> /* the "32" can be replaced by 64 or 16,they return the converted value */ u32 cpu_to_be32(u32); u32 cpu_to_le32(u32); u32 be32_to_cpu(u32); u32 le32_cpu_to(u32); /* 他们的指针变体:将指针所指字的字节序进行相应转换,并返回转换的值 */ u32 cpu_to_be32p(const u32*); u32 cpu_to_le32p(const u32*); u32 be32_to_cpup(const u32*); u32 le32_cpu_top(const u32*); /* 他们的另一种指针变体:将指针所指字的字节序直接进行相应的转换 */ void cpu_to_be32s(u32 *); void cpu_to_le32s(u32 *); void be32_to_cpus(u32 *); void le32_cpu_tos(u32 *);
5. 关于数据对齐
为了保持可移植性,强烈建议存取数据时,其地址要和数据大小对齐(即若数据类型是8字节大小,则其地址应该是8字节对齐)!!!但如果确实需要存取不对齐的数据时怎么办?
解决办法:
使用内核提供的宏定义函数:
#include <asm/unaligned.h> /* 这些宏是无类型的(可存取1、2、4或8字节长的数据类型) */ get_unaligned(ptr); put_unaligned(val, ptr); /*使用举例:*/ /* 移植性差的代码 */ char data[10]= {1,2,3,4,5,6,7,8,9,0}; unsigned int d[4]; d[0] = *(unsigned int *)&data[0]; //data数组不一定是4字节对齐的 /* 移植性好的代码 */ char data[10]= {1,2,3,4,5,6,7,8,9,0}; unsigned int val, *val_ptr; val_ptr = &data[0] val = get_unaligned(val_ptr);
我们知道,编译器为了目标处理器获得更好的性能,它会悄悄的强制使数据项地址和其大小对齐,但这里有两点需要特别注意:
并不是所有平台将64位(8字节)数据对其在8字节大小的边界地址上。据我所知至少有i386、i686、armv4l这3种平台的64位数据是对齐在4字节大小的边界地址上。为了可移植性,你需要填充一些额外的数据类型来强制对齐。
但如果你就是想得到一个紧凑的数据项结构时,如何避开编译器这种自动填充额外数据项呢?
- 利用声明数据属性:
struct { u16 var1; u64 var2; //此处前面可能被编译器自动插入2字节(64位数据满足32位对齐)或6字节(64位数据满足64位对齐)的额外数据项 u16 var3; u32 var4; }__attribute__((packed)) scsi; //添加了该属性申明后,就不会额外插入数据项了
6. 指针和错误值
很多时候,我们看到某些返回值为指针的函数,在执行时遇到错误的时候会默认返回NULL指针以表示错误。但调用者并不能通过该NULL得到任何关于遇到何种错误的有用信息。那有没有办法通过返回不同的指针来指示错误类型,而又不和正常返回的指针冲突呢?
解决办法:内核在
<linux/err.h>
提供了函数,使得可以将错误编码通过指针返回给调用者:void *ERR_PTR(long error); //error是常见的负值错误码 long IS_ERR(const void *ptr); //判断ERR_PTR()函数返回的指针是否含有负值错误编码 long PTR_ERR(const void *ptr); //取出ERR_PTR()函数返回的指针包含的负值错误编码
7. 使用严格的数据类型进行编译
- 在代码进行编译时选择如下选项:
-Wall -Wstrict-prototypes
,如此可避免大部分的bug。 - 或者,更严格一点,你可以将任何编译器警告作为错误来对待,此时可添选项:
-Werror
- 内核代码主要使用三种类型的数据:基本数据类型(char、short、int、long、long long等)、明确大小的Linux数据类型(u8、u16、 u32、 u64及其有符号类型变种s8、s16、s32、s64)和特定内核对象的类型(pid_t、size_t等)。
- 对于基本数据类型:你要知道不同体系结构其大小是不一样的。但一般来说char、short、int、long long大小是固定的,分别是1、2、4、8个字节大小。但long和指针类型会随平台各异(aipha、ia64、x86_64平台上是8字节,其它平台是4字节),但他们二者的大小永远是相等的,所以在内存管理方面,经常使用一个unsigned long来代替指针类型。
- 对于与明确大小的数据项:其在头文件<linux/types.h>或<asm/types.h>中定义。且如果用户空间需要使用它们,可以在名字前添加双下划线引用(类似__u8)。这些类型是linux特定的,为了更好的移植性,建议使用C99标准类型(uint8_t、uint16_t、uint32_t等)进行代替。
- 对于接口特定的数据项:其在头文件<linux/types.h>中定义,其实用原则是与内核其它部分保持一致。例如,时钟滴答数
jiffy
一直是用unsigned long类型表示的,那你就不要用typedef语句重新定义一个新的类型;又如pid_t
类型,在某些系统中是int,而在另一些系统中又是long,所以,在定义进程号变量时,你就用pid_t,而不是int或是long。另一个需要关注的是,当你需要使用printf或是printk打印这些特定类型时,如何选用打印格式的问题。在此,建议将它们转化为它所有可能类型中的值为最大的一种(通常为long或者是unsigned long)。例如pid_t可以扩展为unsigned long。