编写可移植的设备驱动程序
几乎所有的 Linux 内核设备驱动程序都可以在多种类型的处理器上工作。这仅仅是因为设备驱动程序编写者遵守了一些重要的规则。这些规则包括使用正确的变量类型,不依赖于特定的内存页大小,了解外部数据的字节序问题,设置正确的数据对齐方式,以及通过正确的接口访问设备内存位置。本文解释了这些规则,说明了为什么遵循这些规则很重要,并给出了它们在应用中的例子。
编写可移植代码时,要记住的最基本规则之一是了解您需要将变量设置成多大。不同的处理器为 int 和 long 数据类型定义了不同的变量大小。它们在指定变量大小是有符号还是无符号方面也存在差异。因此,如果您知道您的变量大小必须是特定的位数,并且它必须是有符号或无符号的,那么您需要使用内置的数据类型。以下 typedef 可以用于内核代码中的任何位置,并且在 linux/types.h 头文件中定义
u8 unsigned byte (8 bits) u16 unsigned word (16 bits) u32 unsigned 32-bit value u64 unsigned 64-bit value s8 signed byte (8 bits) s16 signed word (16 bits) s32 signed 32-bit value s64 signed 64-bit value
例如,i2c 驱动程序子系统有许多函数用于在 i2c 总线上发送和接收数据
s32 i2c_smbus_write_byte(struct i2c_client *client, u8 value); s32 i2c_smbus_read_byte_data(struct i2c_client *client, u8 command); s32 i2c_smbus_write_byte_data(struct i2c_client *client, u8 command, u8 value);所有这些函数都返回一个有符号的 32 位值,并接受一个无符号的 8 位值作为值或命令参数。由于使用了这些数据类型,因此这段代码可以移植到任何处理器类型。
如果您的变量将用于任何可能被用户空间程序看到的代码中,那么您需要使用以下可导出的数据类型。这方面的例子是通过 ioctl() 调用传递的数据结构。它们同样在 linux/types.h 头文件中定义
__u8 unsigned byte (8 bits) __u16 unsigned word (16 bits) __u32 unsigned 32-bit value __u64 unsigned 64-bit value __s8 signed byte (8 bits) __s16 signed word (16 bits) __s32 signed 32-bit value __s64 signed 64-bit value
例如,usbdevice_fs.h 头文件定义了许多不同的结构,这些结构用于直接从用户空间程序与 USB 设备通信。以下是用于向设备发送 USB 控制消息的 ioctl 的定义
struct usbdevfs_ctrltransfer { __u8 requesttype; __u8 request; __u16 value; __u16 index; __u16 length; __u32 timeout; /* in milliseconds */ void *data; }; #define USBDEVFS_CONTROL_IOWR('U', 0, struct usbdevfs_ctrltransfer)随着 64 位机器越来越普及,一个引起了很多问题的事实是指针的大小与无符号整数的大小不相同。指针的大小等于无符号长整型的大小。这可以在 get_zeroed_page() 的原型中看到
extern unsigned long FASTCALL (get_zeroed_page(unsigned int gfp_mask))get_zeroed_page() 返回一个已用零擦除干净的空闲内存页。它返回一个无符号长整型,应该将其强制转换为您需要的特定数据类型。以下代码片段来自 drivers/char/serial.c 文件中的 rs_open() 函数,展示了这是如何完成的
static unsigned char *tmp_buf; unsigned long page; if (!tmp_buf) { page = get_zeroed_page(GFP_KERNEL); if (!page) return -ENOMEM; if (tmp_buf) free_page(page); else tmp_buf = (unsigned char *)page; }有一些原生的内核数据类型您应该使用,而不是尝试使用无符号长整型。其中一些是:pid_t、key_t、gid_t、size_t、ssize_t、ptrdiff_t、time_t、clock_t 和 caddr_t。如果您需要在代码中使用任何这些类型,请使用给定的数据类型;这将防止很多问题。
正如我们在上面从 drivers/char/serial.c 中提取的示例中看到的那样,您可以向内核请求内存页。内存页的大小并不总是 4KB 数据(就像在 i386 上一样)。如果您要引用内存页,则需要使用 PAGE_SHIFT 和 PAGE_SIZE 定义。
PAGE_SHIFT 是将一位左移以获得 PAGE_SIZE 值的位数。不同的架构将其定义为不同的值。表 1 显示了一些架构的简短列表以及 PAGE_SHIFT 的值和 PAGE_SIZE 的结果值。
表 1. 一些架构以及 PAGE_SHIFT 的值和 PAGE_SIZE 的结果值
即使在相同的基本架构类型上,您也可能拥有不同的页面大小。这有时取决于配置选项(如 IA-64),或者由于处理器类型的不同变体(如 ARM 上)。
列表 1 中 drivers/usb/audio.c 中的代码片段展示了在直接访问内存时如何使用 PAGE_SHIFT 和 PAGE_SIZE。
处理器以两种方式之一存储内部数据:小端或大端。小端处理器存储数据时,最右边的字节(地址值较高的字节)是最高有效位,而大端处理器存储数据时,最左边的字节(地址值较低的字节)是最高有效位。
例如,表 2 显示了十进制值 684686 如何在两种不同的处理器类型上存储在一个 4 字节整数中(684686 十进制 = a72be 十六进制 = 00000000 00001010 01110010 10001110 二进制)。
表 2. 十进制值 684686 如何存储在一个 4 字节整数中
例如,Intel 处理器,如 i386 和 IA-64 系列,是小端机器,而 SPARC 处理器是大端。PowerPC 处理器可以在小端或大端模式下运行,但对于 Linux,它们被定义为在大端模式下运行。ARM 处理器可以是两者之一,具体取决于所使用的特定 ARM 芯片,但通常它也以大端模式运行。
由于处理器字节序类型的不同,您需要注意从外部来源接收的数据以及数据出现的顺序。例如,USB 规范规定所有多字节数据字段都采用小端格式。因此,如果您的 USB 驱动程序从 USB 连接读取多字节字段,则需要将该数据转换为处理器的本机格式。假设处理器是小端的代码可以成功地忽略来自 USB 连接的数据格式。但是,相同的代码在 PowerPC 或 ARM 处理器上将无法工作,并且是导致驱动程序在不同平台上损坏的主要原因。
值得庆幸的是,已经创建了许多有用的宏来使这项任务变得容易。以下所有宏都可以在 asm/byteorder.h 头文件中找到。
要将处理器的本机格式转换为小端格式,您可以使用以下函数
u64 cpu_to_le64 (u64); u32 cpu_to_le32 (u32); u16 cpu_to_le16 (u16);
要将小端格式转换为处理器的本机格式,您应该使用这些函数
u64 le64_to_cpu (u64); u32 le32_to_cpu (u32); u16 le16_to_cpu (u16);对于大端格式,可以使用以下函数
u64 cpu_to_be64 (u64); u32 cpu_to_be32 (u32); u16 cpu_to_be16 (u16); u64 be64_to_cpu (u64); u32 be32_to_cpu (u32); u16 be16_to_cpu (u16);如果您有一个指向要转换的值的指针,那么您应该使用以下函数
u64 cpu_to_le64p (u64 *); u32 cpu_to_le32p (u32 *); u16 cpu_to_le16p (u16 *); u64 le64_to_cpup (u64 *); u32 le32_to_cpup (u32 *); u16 le16_to_cpup (u16 *); u64 cpu_to_be64p (u64 *); u32 cpu_to_be32p (u32 *); u16 cpu_to_be16p (u16 *); u64 be64_to_cpup (u64 *); u32 be32_to_cpup (u32 *); u16 be16_to_cpup (u16 *);如果您想转换变量中的值并将修改后的值存储在同一个变量中(原位),那么您应该使用以下函数
void cpu_to_le64s (u64 *); void cpu_to_le32s (u32 *); void cpu_to_le16s (u16 *); void le64_to_cpus (u64 *); void le32_to_cpus (u32 *); void le16_to_cpus (u16 *); void cpu_to_be64s (u64 *); void cpu_to_be32s (u32 *); void cpu_to_be16s (u16 *); void be64_to_cpus (u64 *); void be32_to_cpus (u32 *); void be16_to_cpus (u16 *);如前所述,USB 协议采用小端格式。列表 2 中 drivers/usb/serial/visor.c 中的代码片段展示了如何从 USB 连接读取结构,然后将其转换为正确的 CPU 格式。
gcc 编译器通常会根据它喜欢的任何字节边界对结构体的各个字段进行对齐,以便提供更快的执行速度。例如,考虑列表 3 中显示的代码和结果输出。
输出显示编译器在偶数字节边界上对齐了 struct foo 中的字段 b 和 c。当我们想要将结构体覆盖到内存位置之上时,这不是一件好事。通常,驱动程序数据结构体的各个字段没有偶数字节填充。因此,gcc 属性 (packed) 用于告诉编译器不要在结构体中放置任何“内存空洞”。
如果我们像这样更改 struct foo 结构体以使用 packed 属性
struct foo { char a; short b; int c; } __attribute__ ((packed));
那么程序的输出变为
offset A = 0 offset B = 1 offset C = 3现在结构体中不再有内存空洞了。
这个 packed 属性可以用于打包整个结构体,如上所示,或者它也可以仅用于打包结构体中的一些特定字段。
例如,include/usb.h 中定义的 struct usb_ctrlrequest 如下
struct usb_ctrlrequest { __u8 bRequestType; __u8 bRequest; __u16 wValue; __u16 wIndex; __u16 wLength; } __attribute__ ((packed));
这确保了整个结构体都被打包,以便它可以用于将数据直接写入 USB 连接。
但是 struct usb_endpoint_descriptor 的定义看起来像
struct usb_endpoint_descriptor { __u8 bLength __attribute__ ((packed)); __u8 bDescriptorType __attribute__ ((packed)); __u8 bEndpointAddress __attribute__ ((packed)); __u8 bmAttributes __attribute__ ((packed)); __u16 wMaxPacketSize __attribute__ ((packed)); __u8 bInterval __attribute__ ((packed)); __u8 bRefresh __attribute__ ((packed)); __u8 bSynchAddress __attribute__ ((packed)); unsigned char *extra; /* Extra descriptors */ int extralen; };
这确保了结构体的第一部分被打包,并且可以用于直接从 USB 连接读取数据,但是结构体的 extra 和 extralen 字段可以对齐到编译器认为访问速度最快的任何位置。
与大多数典型的嵌入式系统不同,在 Linux 上不能直接访问 I/O 内存。这是因为 Linux 运行的各种处理器上存在各种不同的内存类型和映射。要以可移植的方式访问 I/O 内存,您必须调用 ioremap() 来获得对内存区域的访问权限,并调用 iounmap() 来释放访问权限。
ioremap() 定义为
void * ioremap (unsigned long offset, unsigned long size);
您传入要访问的区域的起始偏移量和区域的大小(以字节为单位)。您不能直接将返回值用作内存位置来读取和写入数据,而它是一个令牌,必须传递给不同的函数才能读取和写入数据。
使用 ioremap() 映射的内存读取和写入数据的函数是
u8 readb (unsigned long token); /* read 8 bits */ u16 readw (unsigned long token); /* read 16 bits */ u32 readl (unsigned long token); /* read 32 bits */ void writeb (u8 value, unsigned long token); /* write 8 bits */ void writew (u16 value, unsigned long token); /* write 16 bits */ void writel (u32 value, unsigned long token); /* write 32 bits */
在您完成内存访问后,您必须调用 iounmap() 来释放内存,以便其他人可以在需要时使用它。
列表 4 中来自 drivers/hotplug/cpqphp_core.c 中的 Compaq PCI Hot Plug 驱动程序的代码示例展示了如何正确访问 PCI 设备的资源内存。
要访问设备的 PCI 内存,您再次必须使用一些通用函数,而不是尝试直接访问内存。这是因为 PCI 总线的访问方式可能因您拥有的硬件类型而异。如果您使用通用函数,那么您的 PCI 驱动程序将能够在任何类型的具有 PCI 总线的 Linux 系统上工作。
要从 PCI 总线读取数据,请使用以下函数
int pci_read_config_byte(struct pci_dev *dev, int where, u8 *val); int pci_read_config_word(struct pci_dev *dev, int where, u16 *val); int pci_read_config_dword(struct pci_dev *dev, int where, u32 *val);
要写入数据,请使用这些函数
int pci_write_config_byte(struct pci_dev *dev, int where, u8 val); int pci_write_config_word(struct pci_dev *dev, int where, u16 val); int pci_write_config_dword(struct pci_dev *dev, int where, u32 val);
pci_read_config_* 和 pci_write_config_* 函数实际上在哪里声明的?
这些函数允许您向分配给特定 PCI 设备的特定位置写入 8 位、16 位或 32 位。如果您希望访问尚未被 Linux PCI 核心初始化的特定 PCI 设备的内存位置,您可以使用 pci_hotplug 核心代码中存在的以下函数
int pci_read_config_byte_nodev(struct pci_ops *ops, u8 bus, u8 device, u8 function, int where, u8 *val); int pci_read_config_word_nodev(struct pci_ops *ops, u8 bus, u8 device, u8 function, int where, u16 *val); int pci_read_config_dword_nodev(struct pci_ops *ops, u8 bus, u8 device, u8 function, int where, u32 *val); int pci_write_config_byte_nodev(struct pci_ops *ops, u8 bus, u8 device, u8 function, int where, u8 val); int pci_write_config_word_nodev(struct pci_ops *ops, u8 bus, u8 device, u8 function, int where, u16 val); int pci_write_config_dword_nodev(struct pci_ops *ops, u8 bus, u8 device, u8 function, int where, u32 val);
在 drivers/usb/usb-ohci.c 中的 USB OHCI 驱动程序中可以看到驱动程序读取和写入 PCI 内存的示例(参见列表 5)。
如果您在创建新的 Linux 内核设备驱动程序或修改现有驱动程序时遵循这些不同的规则,那么生成的代码将在各种处理器上成功运行。在调试仅在一个平台上工作的驱动程序时,记住这些规则也很有用(记住那些字节序问题)。
要记住的最重要资源是查看已知可在不同平台上工作的现有内核驱动程序。Linux 的优势之一是其代码的开放访问,这为有抱负的驱动程序作者提供了强大的学习工具。

Greg Kroah-Hartman 目前是 Linux USB 和 PCI 热插拔内核维护者。他在 IBM 工作,从事各种与 Linux 内核相关的工作,可以通过 greg@kroah.com 联系到他。
电子邮件: greg@kroah.com