关于驱动程序的可移植性

差不多所有的linux内核设备驱动都可以运行在不止一种处理器上。这仅仅因为设备驱动作者遵循一些重要规则。这些规则包括使用合适的变量类型,而不是依赖于特定内存页大小,提防外部数据的大小端模式,设立合适的数据对齐并通过合适接口访问设备内存位置。本文解释了这些规则,展示了依据这些的重要性并给出了使用的例子。

内核内部数据类型
要牢记的其中一个重要的基本规则就是在写可移植代码时要注意你的变量有多大。不同的处理器为int和long数据类型定义不同的变量大小。变量大小在有符号和无符号时都不同。因此,如果你知道变量的大小是一个特定的比特值并且必须是有符号或无符号,就使用内建的数据类型吧。下列的类型定义可在内核的任何地方使用,它们都定义在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头文件中:
 struct usbdevfs_ctrltransfer {
     __u8 requesttype;
     __u8 request;
     __u16 value;
     __u16 index;
     __u16 length;
     __u32 timeout;  /* in milliseconds */
     void *data;
 };

例如,usbdevice_fs.h头文件定义了一些用户空间程序和USB设备直接通话的结构。下面是ioctl用来发送USB控制信息给设备的定义:
 #define USBDEVFS_CONTROL_IOWR('U'0struct usbdevfs_ctrltransfer)

有一点引起了很多麻烦,由于64位机器日渐流行,它指针的大小也和无符号整数不一样了。指针大小和无符号长整形相等了。这可以从get_zeroed_page()中看到:
extern unsigned long FASTCALL
     (get_zeroed_page(unsigned int gfp_mask))
get_zeroed_page()返回一片已经用0擦写过的内存页。它返回一个应该转化为你需要的特定数据类型的无符号长整形。下列的来自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;
 }

你应该使用内核本来就有的数据类型而不是试着去用一个无符号长整型数。它们中的一些是:pit_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值。不同的结构定义了不同的值。下面列出了一些架构上的PAGE_SHIFT和PAGE_SIZE值的短表:
架构 PAGE_SHIFT PAGE_SIZE
i386      12         4K
MIPS    12         4K
Alpha   13         8K
m68k   12         4K
m68k   13         8K
ARM     12         4K
ARM     14         16K
ARM     15         32K
IA-64    12         4K
IA-64    13         8K
IA-64    14         16K
IA-64    16          64K
就算在相同的架构类型上,也有不同的页大小。这取决于配置选项(像IA-64)或不同的处理器型号(像ARM)。
drivers/audio.c的片段显示了当直接访问内存时如何使用PAGE_SHIFT和PAGE_SIZE:
static int dmabuf_mmap(...)
{
  size >>= PAGE_SHIFT;
  for (nr = 0; nr < size; nr++)
     if (!db->sgbuf[nr])
            return -EINVAL;
  db->mapped = 1;
  for (nr = 0; nr < size; nr++) {
     if (remap_page_range (start,
                           virt_to_phys(db->sgbuf[nr]),
                           PAGE_SIZE, prot))
           return -EAGAIN;
     start += PAGE_SIZE;
  }
  return 0;
}

端问题
处理器以一种或两种方式储内部数据:小端或大端。小端处理器存储数据的最右字节(高地址值)为有意义的,大端处理器存储数据的最左字节(低地址值)为最有意义的。
例如,下面显示了两种处理器类型中684686存在4字节整形里面的情况(687686十六进制值= a72be,二进制值=00000000 00001010 01110010 10001110):
地址    大端    小端
0 00000000   10001110
1 00001010   01110010
2 01110010   00001010
3 10001110   00000000
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协议是小端模式的。drivers/usb/serial/visor.c的代码片段显示了怎么从USB连接中读一个结构并转换为合适的CPU格式:
struct visor_connection_info *connection_info;

/* send a get connection info request */
usb_control_msg (serial->dev,
                 usb_rcvctrlpipe(serial->dev, 0),
                 VISOR_GET_CONNECTION_INFORMATION,
                 0xc20x00000x0000,
                 transfer_buffer, 0x12300);

connection_info = (struct visor_connection_info *)transfer_buffer;

le16_to_cpus(&connection_info->num_ports);

数据对齐
gcc编译器为了提供更快的执行速度,常常在字节边界上对齐结构体中单个域。例如考虑下面代码和输出结果:
#include <stdio.h>
#include <stddef.h>

struct foo {
       char     a;
       short    b;
       int      c;
};

#define OFFSET_A        offsetof(struct foo, a)
#define OFFSET_B        offsetof(struct foo, b)
#define OFFSET_C        offsetof(struct foo, c)

int main ()
{
        printf ("offset A = %d\n", OFFSET_A);
        printf ("offset B = %d\n", OFFSET_B);
        printf ("offset C = %d\n", OFFSET_C);
        return 0;
}

offset A = 0
offset B = 2
offset C = 4

输出结果显示编译器在struct foo的每个字节边界对齐了b和c。当我们想在一个内存位置覆盖一个结构体时这不是一个好事情。往往驱动数据结构里面的单个域没有字节空白(padding)。由此,gcc属性(packed)用来告诉编译器别在结构体里面放置任何“内存洞(memory holes)”。

如果我们像这样使用packed属性来改变结构体foo: 
 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连接中读数据,但是结构体中另外的域则是按照编译器的思考方式对齐,这样可以更快的访问到。

I/O内存访问
和大多数典型的嵌入式系统不同,在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()来释放内存以让其他人可以使用。    

drivers/hotplug/cpqhphp_core.c中的康柏PCI热插拔驱动代码显示了怎样合理访问PCI设备资源内存:
/* get access to our device's memory */
location = pci_resource_start (pdev, 0);
size = pci_resource_len (pdev, 0);
ctrl->hpc_reg = ioremap(location, size);
if (!ctrl->hpc_reg) {
        err("cannot remap MMIO region %lx @ %lx\n",
            location, size);
        rc = -ENODEV;
        goto err_free_mem_region;
}

/* get the device number */
dev_num = readb(ctrl->hpc_reg + SLOT_MASK) >> 4;

/* Mask all general input interrupts */
       writel(0xFFFFFFFF, ctrl->hpc_reg + INT_MASK);

....

/* release our memory */
iounmap (ctrl->hpc_reg);

访问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_*函数都在哪声明的呢?如果你仔细在drivers/pci/pci.c中寻找,你将看到下列代码:
#define PCI_OP(rw,size,type) \
int pci_##rw##_config_##size (struct pci_dev *dev,
                              int pos, type value) \
{                                                  \
    int res;                                       \
    unsigned long flags;                           \
    if (PCI_##size##_BAD) return
       PCIBIOS_BAD_REGISTER_NUMBER;                \
    spin_lock_irqsave(&pci_lock, flags);           \
    res = dev->bus->ops->rw##_##size(dev, pos,
                                     value);       \
    spin_unlock_irqrestore(&pci_lock, flags);      \
    return res;                                    \
}

PCI_OP(read, byte, u8 *)
PCI_OP(read, word, u16 *)
PCI_OP(read, dword, u32 *)
PCI_OP(write, byte, u8)
PCI_OP(write, word, u16)
PCI_OP(write, dword, u32)
宏通过滥用C预处理#define PCI_OP()创建了六个pci_read_config_*和pci_write_config_*函数。  
这些函数允许你写8、16或32位数据到PCI设备赋予的特定地址。如果你希望访问特定PCI设备的内存位置还没有被Linux 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);
驱动读写PCI内存的例子可以在位于drivers/usb/usb-ohci.c的USB OHCI驱动中看到:  
pci_read_config_byte (dev, PCI_LATENCY_TIMER,
                      &latency);
if (latency) {
    pci_read_config_byte (dev, PCI_MAX_LAT, &limit);
    if (limit && limit < latency) {
        dbg ("PCI latency reduced to max %d", limit);
        pci_write_config_byte (dev, PCI_LATENCY_TIMER,
                               limit);
        ohci->pci_latency = limit;
    } else {
        /* it might already have been reduced */
        ohci->pci_latency = latency;
    }
}

总结
如果你在创建新的linux内核设备驱动或者修改一个已有驱动时按照这些规则做,结果代码就可以成功运行在各种处理器上面。这些规则对于调试只能运行在一个平台(记住那些端问题)上的驱动也是很有好处的。
要记住的最重要的资源就是查看已有的内核驱动,它们都可以工作在各种平台上。Linux的强大在于代码的开放访问,这为激发驱动作者提供了强劲的学习工具。

关于作者:Greg Kroah-Hartman当前是Linux USB和PCI Hot Plug内核的维护者。供职于IBM,做各种Linux内核相关的事情,你可以通过[email protected]联系到他。

你可能感兴趣的:(linux,function,byte,编译器,Descriptor,linux内核)