编写适用于 Oracle® Solaris 11.2 的设备驱动程序

退出打印视图

更新时间: 2014 年 9 月
 
 

使设备驱动程序支持 64 位

本附录为要将设备驱动程序转换为支持 64 位内核的设备驱动程序编写人员提供信息。本附录还介绍了 32 位设备驱动程序和 64 位设备驱动程序之间的区别,并说明了将 32 位设备驱动程序转换为 64 位设备驱动程序的步骤。这些信息仅适用于常规字符设备驱动程序和块设备驱动程序。

本附录提供有关以下主题的信息:

64 位驱动程序设计简介

对于仅需支持 32 位内核的驱动程序,现有 32 位设备驱动程序将仍然有效,无需重新编译。但是,大多数设备驱动程序需要进行一些更改才能在 64 位内核中正确运行,且所有设备驱动程序都需要重新编译以创建 64 位驱动程序模块。本附录中的信息旨在指导您利用通用源代码来生成 32 位和 64 位环境的驱动程序,从而提高代码可移植性并降低维护工作量。

开始修改设备驱动程序以便使用 64 位环境之前,应了解 32 位环境与 64 位环境之间的区别。特别是必须熟悉 C 语言数据类型模型 ILP32 和 LP64。请参见下表。

表 C-1  ILP32 与 LP64 数据类型对比
C 类型
ILP32
LP64
char
8
8
short
16
16
int
32
32
long
32
64
long long
64
64
float
32
32
double
64
64
long double
96
128
pointer
32
64

因 ILP32 与 LP64 之间的差异而导致的特定于驱动程序的问题是本附录的主题。

除了清理常规代码以支持 LP64 的数据模型更改,驱动程序编写人员还必须提供对 32 位和 64 位应用程序的支持。

ioctl(9E)、devmap(9E) 和 mmap(9E) 入口点使应用程序和设备驱动程序之间可直接共享数据结构。如果这些数据结构在 32 位环境与 64 位环境中的大小不同,则必须修改入口点,以便驱动程序可确定应用程序的数据模型与内核的数据模型是否相同。如果数据模型不同,则可对数据结构进行调整。请参见I/O Control Support for 64-Bit Capable Device Drivers32-bit and 64-bit Data Structure MacrosAssociating Kernel Memory With User Mappings

在许多驱动程序中,只有少量 ioctl 需要这种处理。其他 ioctl 无需更改即可应用,只要这些 ioctl 传递的数据结构大小不变。

常规转换步骤

以下各节提供了有关转换驱动程序以在 64 位环境中运行的信息。驱动程序编写人员可能需要执行以下一项或多项任务:

  1. 使用硬件寄存器的固定宽度类型。

  2. 使用固定宽度的公共访问函数。

  3. 检查并扩展派生类型的用法。

  4. 检查 DDI 数据结构中更改的字段。

  5. 检查 DDI 函数中更改的参数。

  6. 根据需要修改用于处理用户数据的驱动程序入口点。

  7. 检查 x86 平台上使用 64 位 long 类型的结构。

下面详细说明这些步骤。

完成每一步骤后,请修复所有编译器警告,然后使用 lint 查找其他问题。对于 SC5.0(或更高)版本的 lint,要想找出 64 位问题,必须在使用该命令时指定 –Xarch=v9–errchk=longptr64 选项。


注 - 请勿忽略 LP64 转换期间出现的编译警告。以前在 ILP32 环境中可安全忽略的警告现在可能表示比较严重的问题。

完成所有步骤后,同时将驱动程序作为 32 位和 64 位模块进行编译和测试。

使用硬件寄存器的固定宽度类型

许多处理硬件设备的设备驱动程序使用 C 数据结构说明硬件的布局。在 LP64 数据模型中,使用 long 或 unsigned long 类型定义硬件寄存器的数据结构几乎肯定不正确,因为 long 类型现在是 64 位。首先包括 <sys/inttypes.h>,然后将此类数据结构更新为使用 int32_t 或 uint32_t,而不是 32 位设备数据的 long。此方法可保留 32 位数据结构的二进制布局。例如,将以下代码:

struct device_regs {
    ulong_t        addr;
    uint_t         count;
};      /* Only works for ILP32 compilation */

更改为:

struct device_regs {
    uint32_t        addr;
    uint32_t        count;
};      /* Works for any data model */

使用固定宽度的公共访问函数

Oracle Solaris DDI 允许通过访问函数访问设备寄存器,以便可在多个平台间移植。DDI 公共访问函数以前以字节和字等单位指定数据大小。例如,ddi_getl(9F) 用于访问 32 位。此函数不存在于 64 位 DDI 环境中,且已被替换为可指定要处理的位数的函数版本。

这些例程已添加到 Solaris 2.6 操作环境的 32 位内核中,以允许驱动程序编写人员采用其早期版本。例如,要在 32 位和 64 位内核间进行移植,驱动程序必须使用 ddi_get32(9F) 而不是 ddi_getl(9F) 来访问 32 位数据。

所有公共访问例程都被替换为其固定宽度的对等例程。有关详细信息,请参见 ddi_get8(9F)、ddi_put8(9F)、ddi_rep_get8(9F) 和 ddi_rep_put8(9F) 手册页。

检查并扩展派生类型的用法

应尽可能使用系统派生的类型(如 size_t),以使产生的变量在各种函数间传递时都有效。而新的派生类型 uintptr_t 或 intptr_t 是整数类型,应该用于指针。

固定宽度的整数类型用于表示二进制数据结构或硬件寄存器的显式大小,而基础 C 语言数据类型(如 int)仍然可用于循环计数器或文件描述符。

一些系统派生类型在 32 位系统上表示 32 位,但是在 64 位系统上表示 64 位。以此方式更改大小的派生类型包括:clock_t、daddr_t、dev_t、ino_t、intptr_t、off_t、size_t、ssize_t、time_t、uintptr_t 和 timeout_id_t。

设计使用这些派生类型的驱动程序时,请特别注意这些类型的用法,尤其是驱动程序将这些值指定给其他类型(如固定宽度类型)的变量时。

检查 DDI 数据结构中更改的字段

DDI 数据结构中某些字段的数据类型(如 buf(9S))已被更改。使用这些数据结构的驱动程序应确保正确使用这些字段。下面列出了变动很大的数据结构及字段。

buf 结构更改

以下列出的字段与传输大小(现在可超过 4 GB)有关。

size_t        b_bcount;          /* was type unsigned int */
size_t        b_resid;           /* was type unsigned int */
size_t        b_bufsize;         /* was type long */

ddi_dma_attr

ddi_dma_attr(9S) 结构定义 DMA 引擎和设备的属性。因为这些属性指定寄存器大小,所以使用了固定宽度的数据类型而不是基本类型。

ddi_dma_cookie 结构更改

uint32_t     dmac_address;    /* was type unsigned long */
size_t       dmac_size;       /* was type u_int */

ddi_dma_cookie(9S) 结构包含 32 位 DMA 地址,因此使用了固定宽度的数据类型来定义该地址。其大小已重新定义为 size_t。

csi_arq_status 结构更改

uint_t    sts_rqpkt_state;         /* was type u_long */
uint_t    sts_rqpkt_statistics;    /* was type u_long */

此结构中的这些字段无需增大,已重新定义为 32 位。

scsi_pkt 结构更改

uint_t      pkt_flags;           /* was type u_long */
int     pkt_time;        /* was type long */
ssize_t     pkt_resid;           /* was type long */
uint_t      pkt_state;           /* was type u_long */
uint_t      pkt_statistics;      /* was type u_long */

由于 scsi_pkt(9S) 结构中的 pkt_flags、pkt_state 和 pkt_statistics 字段无需增大,因此这些字段已重新定义为 32 位整数。数据传输大小 pkt_resid 字段需要增大,已重新定义为 ssize_t。

检查 DDI 函数中更改的参数

本节介绍已更改的 DDI 函数参数数据类型。

getrbuf() 参数更改

struct buf *getrbuf(int sleepflag);

在以前的发行版中,sleepflag 被定义为 long 类型。

drv_getparm() 参数更改

int drv_getparm(unsigned int parm, void *value_p);

在以前的发行版中,value_p 被定义为 unsigned long 类型。在 64 位内核中,drv_getparm(9F) 可提取 32 位和 64 位。此接口未定义这些量的数据类型,可能会发生简单的编程错误。

以下新例程提供更安全的替代方法:

clock_t       ddi_get_lbolt(void);
time_t        ddi_get_time(void);
cred_t        *ddi_get_cred(void);
pid_t         ddi_get_pid(void);

强烈要求驱动程序编写人员使用这些例程而不要使用 drv_getparm(9F)

delay()timeout() 参数更改

void delay(clock_t ticks);
timeout_id_t timeout(void (*func)(void *), void *arg, clock_t ticks);

delay(9F)timeout(9F) 例程的 ticks 参数已从 long 更改为 clock_t。

rmallocmap()rmallocmap_wait() 参数更改

struct map *rmallocmap(size_t mapsize);
struct map *rmallocmap_wait(size_t mapsize);

rmallocmap(9F)rmallocmap_wait(9F) 例程的 mapsize 参数已从 ulong_t 更改为 size_t。

scsi_alloc_consistent_buf() 参数更改

struct buf *scsi_alloc_consistent_buf(struct scsi_address *ap,
    struct buf *bp, size_t datalen, uint_t bflags,
    int (*callback )(caddr_t), caddr_t arg);

在以前的发行版中,datalen 被定义为 int 类型,bflags 被定义为 ulong 类型。

uiomove() 参数更改

int uiomove(caddr_t address, size_t nbytes,
    enum uio_rw rwflag, uio_t *uio_p);

nbytes 参数被定义为 long 类型,但是由于 nbytes 以字节为单位表示大小,因此 size_t 更适合。

cv_timedwait()cv_timedwait_sig() 参数更改

int cv_timedwait(kcondvar_t *cvp, kmutex_t *mp, clock_t timeout);
int cv_timedwait_sig(kcondvar_t *cvp, kmutex_t *mp,    clock_t timeout);

在以前的发行版中,cv_timedwait(9F)cv_timedwait_sig(9F) 例程的 timeout 参数被定义为 long 类型。由于这些例程表示时间周期,因此 clock_t 更适合。

ddi_device_copy() 参数更改

int ddi_device_copy(ddi_acc_handle_t src_handle,
    caddr_t src_addr, ssize_t src_advcnt,
    ddi_acc_handle_t dest_handle, caddr_t dest_addr,
    ssize_t dest_advcnt, size_t bytecount, uint_t dev_datasz);

src_advcnt 、dest_advcnt、dev_datasz 参数的类型已更改。这些参数以前分别被定义为 long、long 和 ulong_t 类型。

ddi_device_zero() 参数更改

int ddi_device_zero(ddi_acc_handle_t handle,
    caddr_t dev_addr, size_t bytecount, ssize_t dev_advcnt,
    uint_t dev_datasz):

在以前的发行版中,dev_advcnt 被定义为 long 类型,dev_datasz 被定义为 ulong_t 类型。

ddi_dma_mem_alloc() 参数更改

int ddi_dma_mem_alloc(ddi_dma_handle_t handle,
    size_t length, ddi_device_acc_attr_t *accattrp,
    uint_t flags, int (*waitfp)(caddr_t), caddr_t arg,
    caddr_t *kaddrp, size_t *real_length,
    ddi_acc_handle_t *handlep);

在以前的发行版中,length、flags 和 real_length 分别被定义为 uint_t、ulong_t 和 uint_t * 类型。

修改处理数据共享的例程

如果设备驱动程序使用 ioctl(9E)devmap(9E)mmap(9E) 与 32 位应用程序共享包含 long 或指针类型的数据结构,而驱动程序已针对 64 位内核进行了重新编译,则数据结构的二进制布局将不兼容。如果当前已按 long 类型定义了字段,并且未使用 64 位数据项,请更改数据结构,以使用仍为 32 位的数据类型(int 和 unsigned int)。否则,驱动程序需要识别 ILP32 和 LP64 的不同结构形式,并确定应用程序与内核之间是否出现模型不匹配。

要处理潜在的数据模型差异,需要编写可直接与用户应用程序交互的 ioctl()devmap()mmap() 驱动程序入口点,以确定参数是否来自使用与内核具有相同数据模型的应用程序。

ioctl() 中的数据共享

要确定应用程序与驱动程序之间是否存在模型不匹配,驱动程序可使用 FMODELS 掩码确定 ioctl()mode 参数的模型类型。在 mode 中采用以下值之一来标识应用程序的数据模型:

  • FLP64-应用程序使用 LP64 数据模型

  • FILP32-应用程序使用 ILP32 数据模型

I/O Control Support for 64-Bit Capable Device Drivers中的代码示例说明如 何使用 ddi_model_convert_from(9F) 处理此情况。

devmap() 中的数据共享

要使 64 位驱动程序和 32 位应用程序共享内存,64 位驱动程序生成的二进制布局必须与 32 位应用程序使用的布局相同。要导出到应用程序的映射内存可能需要包含与数据模型有关的数据结构。

很少内存映射设备会面临此问题,因为在内核数据模型发生变化时设备寄存器不会改变大小。但是,一些将映射导出到用户地址空间的伪设备可能要将不同数据结构导出到 ILP32 或 LP64 应用程序。要确定是否出现了数据模型不匹配,devmap(9E) 可使用 model 参数说明应用程序期望的数据模型。将 model 参数设置为以下值之一:

  • DDI_MODEL_ILP32-应用程序使用 ILP32 数据模型

  • DDI_MODEL_LP64-应用程序使用 LP64 数据模型

可将未经转换的模型参数传递到 ddi_model_convert_from(9F) 例程或 STRUCT_INIT()。请参见32-bit and 64-bit Data Structure Macros

mmap() 中的数据共享

由于 mmap(9E) 没有可用于传递数据模型信息的参数,因此可编写驱动程序的 mmap(9E) 入口点,以使用新的 DDI 函数 ddi_model_convert_from(9F)。此函数返回以下值之一,以指示应用程序的数据类型模型:

  • DDI_MODEL_ILP32-应用程序要求 ILP32 数据模型

  • DDI_MODEL_ILP64-应用程序要求 LP64 数据模型

  • DDI_FAILURE-未从 mmap(9E) 调用函数

ioctl()devmap() 一样,可将模型位传递到 ddi_model_convert_from(9F) 以确定是否需要进行数据转换,或可将模型传递到 STRUCT_INIT()

或者,迁移设备驱动程序以支持 devmap(9E) 入口点。

检查 x86 平台上 64 位 Long 数据类型的结构

您应认真检查 x86 平台上使用 64 位 long 类型(如 uint64_t)的结构。32 位模式编译与 64 位模式编译的对齐方式和大小可能不同。请参考以下示例。

#include &lt;studio>
#include &ltsys>

struct myTestStructure {
        uint32_t        my1stInteger;
        uint64_t        my2ndInteger;
};

main()
{
        struct myTestStructure a;

        printf("sizeof myTestStructure is: %d\n", sizeof(a));
        printf("offset to my2ndInteger is: %d\n", (uintptr_t)&a.bar - (uintptr_t)&a);
}

在 32 位系统中,该示例显示以下结果:

sizeof myTestStructure is: 12
offset to my2ndInteger is: 4

而在 64 位系统中,该示例显示以下结果:

sizeof myTestStructure is: 16
offset to my2ndInteger is: 8

因此,32 位应用程序与 64 位应用程序对结构的理解不同。这样,尝试在 32 位和 64 位两种环境中使用同一结构可能会导致问题。这种情况经常发生,尤其是在通过 ioctl() 调用将结构传入或传出内核的情况下。

已知的 ioctl 接口

许多 ioctl(9E) 操作对一类设备驱动程序通用。例如,大多数磁盘驱动程序实现 dkio(7I) 系列的众多 ioctls。这些接口中有许多将数据结构复制到内核中,或从内核中复制出数据结构,在 LP64 数据模型中这些数据结构的一部分已更改了大小。下节列出了对于 dkiofbio(7I)cdio(7I)mtio(7I) 系列的 ioctls,现在需要在 64 位驱动程序 ioctl 例程中进行显式转换的 ioctls

ioctl 命令
受影响的数据结构
参考
DKIOCGAPART
DKIOCSAPART
dk_map
dk_allmap
DKIOGVTOC
DKIOSVTOC
partition
vtoc
FBIOPUTCMAP
FBIOGETCMAP
fbcmap
FBIOPUTCMAPI
FBIOGETCMAPI
fbcmap_i
FBIOCCURSOR
FBIOSCURSOR
fbcursor
CDROMREADMODE1
CDROMREADMODE2
cdrom_read
CDROMCDDA
cdrom_cdda
CDROMCDXA
cdrom_cdxa
CDROMSUBCODE
cdrom_subcode
MTIOCTOP
mtop
MTIOCGET
mtget
MTIOCGETDRIVETYPE
mtdrivetype_request
USCSICMD
uscsi_cmd

设备大小

nblocks 属性按块设备驱动程序的每一分片导出。此属性包含 512 字节块的数量,设备的每一分片都支持这些块。nblocks 属性被定义为带符号的 32 位量,这就将分片的最大大小限制为 1 TB。

每个磁盘提供 1 TB 以上存储空间的磁盘设备必须定义 Nblocks 属性,该属性仍应包含设备可支持的 512 字节块的数量。但是,Nblocks 是带符号的 64 位量,它除去了对磁盘空间的任何实际限制。

nblocks 属性现在已过时。所有磁盘设备都应提供 Nblocks 属性。