C 语言中的标准数据类型 int、long 的长度直接与平台相关,在驱动中,关键部分代码直接使用这些类型时需要特别小心。表 23.1 给出了在几个不同的平台下 char、short、int、long、ptr、long long 的长度。
因此,在 Linux 系统中,针对不同的体系结构重新 typedef 出了 u8、u16、u32、u64、s8、s16、s32、s64 等类型。例如,在 i386/arm 下这些类型的定义如代码清单 23.1 所示,而在 ppc64 下这些类型的定义则如代码清单 23.2 所示,可见影响 C 语言基本数据类型大小的主要因素是 CPU 字长。
代码清单 23.1 i386/arm 平台下 u8、u16、u32、u64、s8、s16、s32、s64 的定义
typedef signed char s8;
typedef unsigned char u8;
typedef signed short s16;
typedef unsigned short u16;
typedef signed int s32;
typedef unsigned int u32;
typedef signed long long s64;
typedef unsigned long long u64;
代码清单 23.2 ppc64 平台下 u8、u16、u32、u64、s8、s16、s32、s64 的定义
typedef signed char s8;
typedef unsigned char u8;
typedef signed short s16;
typedef unsigned short u16;
typedef signed int s32;
typedef unsigned int u32;
typedef signed long s64;
typedef unsigned long u64;
由于 u8、u16、u32、u64、s8、s16、s32、s64 是被针对不同的体系结构单独定义的,因此,在任何平台下对其进行 sizeof 运算的结果都是不变的,是确定长度的数据类型。这些类型都应该只在内核空间使用。在 Linux 用户空间中,如果要使用确定长度的数据类型,应该使用上述定义加“__”的版本,如__u8、__u16、
__u32 等。设备驱动中如果要向用户空间导出头文件,在头文件中也应该定义__sxx、__uxx 等数据类型。
上面给出的 _ _uxx、_ _sxx 类型定义都是 Linux 系统所特有的,为更好地向其他平台移植,驱动中最好使用 int8_t、int16_t、int32_t、uint8_t、uint16_t、uint32_t、int64_t、uint64_t这些 C99 标准确定长度类型。
Linux 系统中定义许多以_t 为后缀的数据类型,这些类型的使用将使内核屏蔽实际数据类型间存在的差异。例如,file_operations 中的 read()、write()成员函数返回值为 ssize_t类型,llseek()返回的是 loff_t 类型。此外,ssize_t、pid_t 这些类型也被赋予一些含义,如 pid_t 是进程 ID 类型。
23.1.2 结构体对齐
在 C 语言中使用结构体时有一个需要特别注意的事项,那就是结构体的对齐。struct 是一种复合数据类型,其构成元素既可以是基本数据类型的变量,也可以是一些复合数据类型(如结构体、共用体等)的数据单元。对于结构体,编译器很可能会自动进行成员变量的对齐,以提高存取效率。默认情况下,编译器为结构体的每个成员按其自然对齐(natural alignment)条件分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构体的地址相同。
自然对齐指按结构体的成员中 sizeof 最大的成员对齐(如果 sizeof 大于 CPU 的字长,仍然按照 CPU 字长对齐),例如对于 32 位系统:
struct naturalalign {
char a;
short b;
char c;
};
在上述结构体中,size 最大的是 short,其长度为两个字节,因而结构体中的 char 成员 a、c都以 2 为单位对齐,sizeof(naturalalign)的结果等于 6。
如果改为:
struct naturalalign {
char a;
int b;
char c;
};
其结果为 12。
在 Linux 内核编程中,为了防止编译器自动在结构体的数据间插入空隙,使用__attribute__((packed))定义结构体,如:
struct naturalalign {
char a;
int b;
char c;
}__attribute__((packed)); //不要在数据间插入空隙
sizeof(naturalalign)的结果等于 6。
23.1.3 Little Endian 与 Big Endian
采用 Little Endian 模式的 CPU 对操作数的存放方式是从低字节到高字节,而 Big Endian 模式的 CPU 对操作数的存放方式是从高字节到低字节。例如,16bit 宽的数 0x1234 在 Little Endian 模式 CPU内存中的存放方式(假设从地址 0x4000 开始存放)为:
低内存地址处存放低位数据
而在 Big Endian 模式,CPU 内存中的存放方式则为:
低内存地址处存放高位数据
32bit 宽的数 0x12345678 在 Little Endian 模式 CPU 内存中的存放方式(假设从地址 0x4000开始存放)为:
低内存地址处存放低位数据
而在 Big Endian 模式 CPU 内存中的存放方式则为:
低内存地址处存放高位数据
内核中定义如下多个宏来进行 Big Endian 模式与 Little Endian 模式的互换,包括 cpu_to_le64、
le64_to_cpu、cpu_to_le32、le32_to_cpu、cpu_to_le16、le16_to_cpu。
内核中定义如下多个宏来进行 Big Endian 模式与 Big Endian 模式的互换:
cpu_to_be64、be64_to_cpu、cpu_to_be32、be32_to_cpu、cpu_to_be16、be16_to_cpu。
23.1.4 内存页面大小
一般情况下,内存页面的大小是 4KB(即 PAGE_SIZE 定义为 4KB),但是这并非是一定的,实际上,页面大小在一个 4~64KB 的范围内是可变的,即使在相同的平台下也可以定义不同的PAGE_SIZE 和 PAGE_SHIFT。
当在内核空间中通过 get_free_pages()函数申请内存时,如果它的第二个参数为 order,意味着申请 PAGE_SIZE * 2的 order次方 的内存,同样是申请 64KB 内存,如果 PAGE_SIZE 为 4KB,应该传入的 order 是 4,如果 PAGE_SIZE 是 16KB,应该传入的参数是 2。为了保证在申请 64KB 内存时,在任何 PAGE_SIZE 的情况下都成立,可以使用如代码清单 23.3 所示的方法。
代码清单 23.3 通过 get_order()获得要申请内存的 order
#include <asm/page.h>
int order = get_order(64*1024);
buf = get_free_pages(GFP_KERNEL, order);