一. 字长和数据类型
能够由机器一次就完成处理的数据被称为字。人们说某个机器是多少“位”的时候,其实说的就是该机器的字长,如 32 位的机器字长是 4 字节。
处理器通用寄存器(GPR's)的大小和它的字长是相同的。一般来说,对于一个体系结构来说,它各个部件的宽度——比如说内存总线——最少要和它的字长一样大。而一般来说,地址空间的大小也等于字长(所以 32 位的机器,寻址空间最大为 4G),至少 Linux 支持的体系结构中都是这样(不过实际上可寻址的空间也可能会低于字长,比如一个 64 位的体系结构虽然可能会提供 64 位的指针,但是可能只会用 48 位来寻址)。此外,C语言定义的 long 类型总等于机器的字长,而 int 类型有时会比字长小。
此外,用户用间使用的数据类型和内核空间的数据类型不一定要相互关联。sparc64 体系结构就提供了 32 位的用户空间,其中指针、int 和 long 的长度都是 32 位,而在内核空间,它的 int 长度是 32 位,指针和 long 的长度却是 64。没有什么标准来规范这些。
牢记以下准则:
ANSI C 标准规定,一个 char 的长度一定是 8 位;
尽管没有规定 int 类型的长度是 32 位,但是在 Linux 当前所有支持的体系结构中,它都是 32 位的;
short 类型也类似,在当前所有支持的体系结构中,虽然没有明文规定,但它都是 16 位的;
决不应该假定指针和 long 的长度,在 Linux 当前支持的体系结构中,它们就可以在 32 位和 64 位中变化,但是,在当前 Linux 支持的平台上,二者的大小总是相同的
由于不同的体系结构 long 的长度不同,决不应该假设 sizeof(int) == sizeof(long);
类似的,也不要假设指针和 int 长度相等。
1. 不透明数据类型
通常在定义一套特别的接口时才会用到,目的是屏蔽类型差异。
pid_t 实际上是 int
atomic_t 实际上是 int
还有 dev_t、gid_t 和 uid_t 等。处理不透明数据类型的原则是:
不要假设该类型的长度;
不要将该类型转化回其对应的 C 标准类型使用,除非是在 printk 时
编程时要保证在该类型实际存储空间和格式发生变化时代码不受影响
2. 指定数据类型
内核中还有一些数据虽然无需用不透明的类型表示,但它们被定义成了指定的数据类型。jiffy 数据和在中断控制时用到的 flags 参数就是例子,它们都应该被存放在 unsigned long 类型中。当存放和处理这些特别的数据时,一定要搞清楚它们对应的类型后再使用。
3. 长度明确的类型
内核在 <asm/types.h> 中定义了长度明确的类型,该文件又被包含在 <linux/types.h> 中:
u8 无符号字节 typedef unsigned char u8;
u16 无符号16位整数
u32 无符号32位整数
u64 无符号64位整数
s8 带符号字节 typedef singed char s8;
s16 带符号16位整数
s32 带符号32位整数
s64 带符号64位整数
上述这些类型只能在内核中使用,不可以出现在用户空间中,目的是为了保护命名空间。不过内核同时也定义了对应的用户可见的变量类型,这些类型与上面类型所不同的是增加了两个下划线前缀。如 u32 对应的是 __u32。
4. char 型的符号问题
C 标准表示 char 类型可以带符号也可以不带符号,有具体的编译器、处理器或两者共同决定。如果在代码中使用了 char 类型,那么要保证在带符号和不带符号的情况下都没问题。
二. 数据对齐
ANSI C 明确规定不允许编译器改变结构体内成员对象的次序(否则就完蛋了,尤其是开发网络程序时)。
对于数据对齐,以前的理解是,一旦不对齐,那么处理器需要读取多次才能读完。但真实情况不仅如此:不是所有的体系结构都允许访问未对齐的数据项,大部分现代体系结构在每次程序试图传输未对齐的数据时,都会产生一个异常,从而转交异常处理程序,因此带来更大的性能损失。
所以,Beaver 的 struct desc_item 结构处理为了满足 32 字节对齐,额外添加了 4 字节,否则,一个 28 字节的结构体很有可能带来灾难。
如果要访问未对齐的数据,应该使用如下的宏:
#include <asm/unaligned.h>
get_unaligned(ptr);
put_unaligned(val, ptr);
这些宏与类型无关,不管是 1 字节还是 2、4、8 字节,这些宏都有效。
自然对齐:在数据项大小的整数倍的地址处存储数据。应该始终强制数据项的自然对齐。
编译器可以协助填充数据,在确保自然对齐,这在开发网络程序时通常会带来问题。如果使用 -Wpadded 标志,那么将使 gcc 在发现结构体被填充时产生警告。而如果想消除数据填充,在内核态可以使用 __attribute__ ((packed)),在用户态则是 #pragma pack(n)。
三. 字节顺序
所谓的字节序是指处理器的字节序。Linux 在 <asm/byteorder.h> 定义了一组宏,可以在处理器字节序和特殊字节序之间转换,与用户态常用的 ntohl、htons 等宏没有区别。
u32 cpu_to_be32(u32)
u32 cpu_to_le32(u32)
u32 be32_to_cpu(u32)
u32 le32_to_cpu(u32)
当涉及到指针时,可以使用 cpu_to_be32p 等形式。
四. 时间
绝对不要假定时钟中断发生的频率,也就是每秒产生的 jiffies 数目,相反,应该使用 Hz 来正确计量时间。
五. 页长度
绝对不要假设页的长度,而应使用 PAGE_SIZE。
用 PAGE_SHIFT 可以得到页号,定义了从最右端屏蔽多少位能够得到该地址对应的页的页号,对于 4KB 和更大的页,这个数值通常是 12 或者更大。
以上宏在 <asm/page.h>中定义,而如果用户空间需要用到,可以使用 getpagesize 库函数。
六. 链表
<linux/list.h> 定义了 list_head 结构体。
特别提到 list_entry 宏,可以将 list_head 结构指针映射回指向包含它的大结构的指针。
七. 指针和错误值
许多内核函数返回一个指针值给调用者,在大部分情况下,失败是通过返回一个 NULL 指针值来表示的。这不能反应问题的实质,大部分时候我们需要一个错误编码。因此,许多内核接口通过把错误值编码到一个指针值中来返回错误信息。这种函数必须小心使用,因为它们的返回值不能简单的和 NULL 比较。
Linux 对此提供了一组函数:
void *ERR_PTR(long error) {
return (void *)error;
}
参数 error 是调用函数需要返回的错误码。可以看到,其实现只是简单的转换成 void 指针。
long PTR_ERR(const void *ptr) {
return (long)ptr;
}
而上述函数则相反,把 void 指针转换回对应的错误码。
更重要的一个函数是,如何判断内核函数返回的指针是否携带错误码,即,是否能调用 PTR_ERR()。所以就有了:
long IS_ERR(const void *ptr) {
return (unsigned long)ptr > (unsigned long)-1000L;
}
只有在 IS_ERR 对某指针返回真时,才能调用 PTR_ERR。原理参见:http://blog.163.com/arm_linux_learn/blog/static/1921553082011102843732189/