内核的数据类型

在讨论更高级的话题前,我们需要讨论一下可移植问题。现代版本的Linux内核的可移植性是非常好的,可以运行在许多不同的体系架构上。由于Linux的多平台特性,任何一个重要的驱动程序都应该是可移植的。

与内核代码相关的核心问题是这些代码应该能够同时访问已知长度的数据项,并充分利用不同的处理器的能力。

坚持使用严格的数据类型,并且使用-Wall -Wstrict-prototypes选项编译可以防止大多数的代码缺陷。

内核使用的数据类型主要被分成三大类:类似int这样的标准C语言类型,类似u32这样的有确定大小的类型,以及像pid_t这样的用于特定内核对象的类型。我们将讨论应该在什么情况下使用这三种典型类型,以及如何使用。

使用标准C语言类型

尽管大多数程序员习惯于自由使用像int和long这样的标准类型,但编写驱动程序时应该格外小心,以避免类型冲突和代码缺陷。

问题是,当我们需要“两个字节的填充符”或者“用四个字节字符串表示的某个东西”时,我们不能使用标准类型,因为在不同的体系上,普通C语言的数据类型所占空间的大小并不相同。下面是在不同平台上类型所占空间大小:

尽管在混合使用不同数据类型时我们必须小心谨慎,但有时有理由这样做。这样的一种情况是内存地址,使用无符号整数类型可以更好地实现内存管理;内核把物理内存看作是一个巨型数组,一个内存地址就是该数组的一个索引。此外,我们可以很方便地对指针取值;但在直接处理内存地址时,我们乎从来不会以这种方式对它们取值。使用一个整数类型可以防止这种取值,因而可避免代码缺陷。所以内核中的普通内存地址通常是unsigned long,这利用了如下事实:至少在当前Linux支持的所有平台上,指针和long整型的大小总是相同的。

为数据项分配确定的空间大小

有时内核代码需要特定大小的数据项,多半是用来匹配预定义的二进制结构,或者和用户空间进行通信,或者通过在结构体中插入“填白”字段来对齐数据。

内核提供了下列数据类型,在中声明,这个文件又被包含:

u8;  /*无符号字节8位*/
u16; /*无符号字16位*/
u32; /*无符号32位值*/
u64; /*无符号64位值*/

相应的有符号类型也存在,但是几乎没用。

如果一个用户空间程序需要使用这些类型,它可以在名字前加上两个下划线作为前缀:__u8和其他类型是独立于__KERNEL__定义的。

这些类型是Linux特有的,使用它们将阻碍软件向其他Unix变种的移植。使用新的编译器将支持C99标准类型,如uint8_t和uint32_t;如果考虑到可移植性,可以使用这些类型而不是Linux特有的变种。

接口特定的类型

内核中最常用的数据类型是由它们自己的typedef声明,这样可以防止出现任何移植性问题。例如,一个进程的标识符通常使用pid_t类型,而不是int。使用pit_t屏蔽了在实际的数据类型中的任何可能的差异。

即使没有定义接口特定的类型,也应该始终使用和内核其余部分一致的、适当的数据类型。例如,jiffies计数总是属于unsigned long类型,而不管它的实际大小如何,因此,在使用jiffies的时候应该始终使用unsigned long类型。

完整的_t类型在中定义,但很少使用这个清单。当需要要某个特定类型时,可在所需调用的函数原型或者所使用的数据结构中找到这个类型。

当我们需要打印一些接口特定的数据类型时,最行之有效的方法,就是将其强制转换成可能的最大类型,然后利用相应的格式打印。

其他有关移植性的问题

在编写一个能在不同的Linux平台间移植的驱动程序时,除了数据类型定义的问题之外,还必须注意其他一些软件上的问题。

一个通用的原则是要避免使用显式的常量值。通常代码会使用预处理的宏来使之参数化。这一节列出了最重要的移植性问题。

时间间隔

在处理时间间隔时,不要假定每秒一定有100个jiffies。因为HZ值是因平台不同是可能不同的。例如,为了检测半秒,可以将消逝的时间与HZ/2比较。更常见的,与mesc毫秒对应的jiffies数目总是msec*HZ/1000。

页大小

 

使用内存是地,要记住页的大小为PAGE_SIZE字节,而不是4KB。

让我们看一种重要的情形。如果一个驱动程序需要16KB空间来储存临时数据,我们不应该指定传递给get_free_pages的参数为2的幂,而是需要一个可移植的方案,幸运的是,内核开发人员提供了一个名为get_order的解决方案

#include
int oder = get_order(16*1024);
buf = get_free_pages(GFP_KERNEL, order);

order是要申请的页面的以2为底的对数。记住,传递给get_order的参数必须是2的幂。

字节序

小心不要做字节序的假设。尽管PC是按照先是低字节(小端)的方式存储多字节数值的,但某些高端平台是以另一种方式工作的(大端)。

在处理字节序问题是时,我们可能要编写一组#indef __LITTLE_ENDIAN条件语句,但是还有一个更好的方法。Linux内核定义了一组宏,它可以在处理器字节序和特殊字节序之间进行转换。例如:
u32 cpu_to_le32(u32);
u32 le32_to_cpu(u32);

使用这些宏可以使编写可移植代码的工作变得更加容易,而无需使用很多的条件编译。

类似的例子还有很多,我们可以在头文件中看到完整的列表。

数据对齐

编写可移植代码的最后一个问题是如何访问未对齐的数据。i386用户常常访问未对齐的数据项,但不是所有平台都允许这样做的。如果需要访问未对齐的数据,则应该使用下面的宏:

#include
get_unaligned(ptr);
put_unaligned(val, ptr);

这些宏是与数据类型无关的,对各种数据项都有效。所有版本的内核都定义了这么宏。

另一个关于对齐的问题是数据结构的跨平台可移植性。同样的数据结构(在C语言源文件中定义的)在不同的平台上可能会编译成不同的布局。编译器根据平台的习惯来对齐数据结构的字段,而不同平台的习惯是不同的。

为了编写可以在不同平台之间可移植的数据项的数据结构,除了规定特定的字节序以外,还应该始终强制数据项的自然对齐。自然对齐是指在数据项大小的整数倍(例如,8字节数据项存入8的整数倍的地址)的地址处存储数据项。强制自然对齐可以防止编译器移动数据结构的字段,你应该使用填充符字段来避免数据结构中留下空洞。

值得注意的是,不是所有平台都在64位边界对齐64位数值,所以需要填充符字段来强制对齐并确保可移植性。

最后,要注意编译器本身也许会悄悄地往结构体中插入填充数据,来确保每个字段的对齐可以在目标处理器上取得好的性能。如果正在定义一个和设备所要求的结构体相匹配的结构体,这种自动填充会破坏你的意图。解决办法是告诉编译器该结构体必须是“填满的”,不能添加填充符。

指针和错误值

许多内部的内核函数返回一个指针值给调用者,而这些函数中很多可能会失败。在大部分情况下,失败是通过返回一个NULL指针来表示的。这种技巧很有用,但它不能传递问题的确切性质。某些接口确实需要返回一个实际的错误编码,以使调用都可以根据实际出错的情况做出正确的决策。

许多内核接口通过把错误值编码到一个指针值中来返回错误信息。这种函数必须小心使用,因为它们的返回值不能简单的和NULL比较。为了帮助创建和使用这种类型的接口,中提供了一小组函数。

返回指针类型可以通过如下函数返回一个错误值:

void *ERR_PTR(long error);

这里error是通常的负的错误编码。调用者可以使用IS_ERR来检查所返回的指针是否是一个错误编码:

long IS_ERR(const void *prt);

如果需要实际的错误编码,可以通过如下函数把它提取出来:

long PTR_ERR(const void *ptr);

应该只有在IS_ERR对某值返回真时才对该值使用PTR_ERR,因为任何其他值都是有效的指针。

链表

有时,Linux内核同时存在多个链表的实现代码,为了减少重复代码的数量,内核开发者已经建立了一套标准的循环、双向链表的实现。如果需要操作链表,那么建议直接使用这一内核设施。

当使用这些链表接口时,应该始终牢记这些链表函数不进行任何锁定。如果驱动程序有可能试图对同一个链表执行并发操作的话,则有责任实现一个锁方案。否则,崩溃的链表结构休、数据丢失、内核混乱等问题是很难诊断的。驱动程序必须包含头文件来使用内核链表接口。

 

 

 

 

 

 

你可能感兴趣的:(LINUX设备驱动程序第三版)