编写设备驱动程序

第 10 章 映射设备和内核内存

一些设备驱动程序允许应用程序通过 mmap(2) 访问设备或内核内存。例如,帧缓存器驱动程序允许将帧缓存器映射到用户线程中。另一个示例是使用共享的内核内存池与应用程序通信的伪驱动程序。本章介绍有关以下主题的信息:

内存映射概述

驱动程序必须采取如下步骤才能导出设备或内核内存:

  1. cb_ops(9S) 结构的 cb_flag 标志中设置 D_DEVMAP 标志。

  2. 定义 devmap(9E) 驱动程序入口点并视需要定义 segmap(9E) 入口点,以导出映射。

  3. 使用 devmap_devmem_setup(9F) 设置到设备的用户映射。要设置到内核内存的用户映射,请使用 devmap_umem_setup(9F)

导出映射

本节介绍如何使用 segmap(9E)devmap(9E) 入口点。

segmap(9E) 入口点

segmap(9E) 入口点负责设置 mmap(2) 系统调用所请求的内存映射。许多内存映射设备的驱动程序使用 ddi_devmap_segmap(9F) 作为入口点,而不是定义自己的 segmap(9E) 例程。通过提供 segmap () 入口点,驱动程序能够在创建映射之前或之后管理常规任务。例如,驱动程序可以检查映射权限并分配专用映射资源。驱动程序还可以调整映射,以适应非按页对齐的设备缓冲区。segmap() 入口点在返回之前必须调用 ddi_devmap_segmap(9F) 函数。ddi_devmap_segmap() 函数调用驱动程序的 devmap(9E) 入口点以执行实际映射。

segmap() 函数的语法如下:

int segmap(dev_t dev, off_t off, struct as *asp, caddr_t *addrp,
     off_t len, unsigned int prot, unsigned int maxprot,
     unsigned int flags, cred_t *credp);

其中:

dev

要映射其内存的设备。

off

设备内存中的偏移,映射将从此位置开始。

asp

指向设备内存将要映射到的地址空间的指针。

请注意,此参数可以是 struct as *(如示例 10–1 中所示),也可以是 ddi_as_handle_t(如示例 10–2 中所示)。这是因为 ddidevmap.h 包括以下声明:

typedef struct as *ddi_as_handle_t
addrp

指向设备内存将要映射到的地址空间中的地址的指针。

len

所映射的内存的长度(以字节为单位)。

prot

指定保护的位字段。可能的设置有 PROT_READ、PROT_WRITE、PROT_EXEC、PROT_USER 和 PROT_ALL。有关详细信息,请参见手册页。

maxprot

尝试的映射可用的最大保护标志。如果用户打开只读的特殊文件,则 PROT_WRITE 位可能会被屏蔽。

flags

指示映射类型的标志。可能的值包括 MAP_SHARED 和 MAP_PRIVATE。

credp

指向用户凭证结构的指针。

在以下示例中,驱动程序控制允许只写映射的帧缓存器。如果应用程序尝试进行读取访问,并随后调用 ddi_devmap_segmap(9F) 来设置用户映射,则驱动程序返回 EINVAL


示例 10–1 segmap(9E) 例程

static int
xxsegmap(dev_t dev, off_t off, struct as *asp, caddr_t *addrp,
    off_t len, unsigned int prot, unsigned int maxprot,
    unsigned int flags, cred_t *credp)
{
    if (prot & PROT_READ)
        return (EINVAL);
    return (ddi_devmap_segmap(dev, off, as, addrp,
        len, prot, maxprot, flags, cred));
}

以下示例说明如何处理其缓冲区未在寄存器空间中按页对齐的设备。本示例将映射从偏移 0x800 开始的缓冲区,因此 mmap(2) 会返回与该缓冲区起始位置对应的地址。devmap_devmem_setup(9F) 函数映射整页,要求映射按页对齐,并返回页起始位置的地址。如果此地址是通过 segmap(9E) 传递的,或者未定义 segmap() 入口点,则 mmap() 将返回对应于页起始位置的地址,而不是对应于缓冲区起始位置的地址。在本示例中,缓冲区偏移将与 devmap_devmem_setup 返回的按页对齐地址相加,因此,得到的返回地址就是所需的缓冲区起始地址。


示例 10–2 使用 segmap() 函数更改 mmap() 调用返回的地址

#define    BUFFER_OFFSET 0x800

int
xx_segmap(dev_t dev, off_t off, ddi_as_handle_t as, caddr_t *addrp, off_t len,
    uint_t prot, uint_t maxprot, uint_t flags, cred_t *credp)
{
        int rval;
        unsigned long pagemask = ptob(1L) - 1L;

        if ((rval = ddi_devmap_segmap(dev, off, as, addrp, len, prot, maxprot,
            flags, credp)) == DDI_SUCCESS) {
                /*
                  * The address returned by ddi_devmap_segmap is the start of the page
                  * that contains the buffer.  Add the offset of the buffer to get the
                  * final address.
                  */
                *addrp += BUFFER_OFFSET & pagemask);
        }
        return (rval);
}

devmap(9E) 入口点

devmap(9E) 入口点通过 segmap(9E) 入口点内部的 ddi_devmap_segmap(9F) 函数调用。

devmap(9E) 入口点是作为 mmap(2) 系统调用的结果调用的。调用 devmap(9E) 函数可将设备内存或内核内存导出到用户应用程序。devmap() 函数用于进行以下操作:

devmap() 函数的语法如下所示:

int devmap(dev_t dev, devmap_cookie_t handle, offset_t off,
     size_t len, size_t *maplen, uint_t model);

其中:

dev

要映射其内存的设备。

handle

系统创建的设备映射句柄,用来描述到设备或内核中的连续内存的映射。

off

应用程序映射中的逻辑偏移,必须通过驱动程序将其转换为设备或内核内存中的对应偏移。

len

所映射的内存的长度(以字节为单位)。

maplen

使驱动程序可将不同的内核内存区域或多个物理上不连续的内存区域与一个连续的用户应用程序映射相关联。

model

当前线程的数据模型类型。

系统在一次 mmap(2) 系统调用中将创建多个映射句柄。例如,映射可能包含多个物理上不连续的内存区域。

devmap(9E) 的首次调用使用参数 offlen 的初始值。应用程序将这些参数传递给 mmap(2)devmap(9E)*maplen 设置为从 off 到连续内存区域结尾之间的长度。*maplen 值必须向上进位为页面大小的倍数。 *maplen 值可被设置为小于原始映射长度 len。如果这样,系统将使用调整了 offlen 参数的新映射句柄反复调用 devmap(9E),直到达到初始映射长度为止。

如果一个驱动程序支持多个应用程序数据模型,则必须将 model 传递给 ddi_model_convert_from(9F)ddi_model_convert_from() 函数可以确定当前线程与设备驱动程序之间是否存在数据模型不匹配的情况。设备驱动程序可能必须调整数据结构的形状,然后才能将结构导出到支持不同数据模型的用户线程。有关更多详细信息,请参见附录 C

如果逻辑偏移 off 超出了驱动程序导出的内存范围,则 devmap(9E) 入口点必将返回 -1

将设备内存与用户映射相关联

通过驱动程序的 devmap(9E) 入口点调用 devmap_devmem_setup(9F) 可将设备内存导出到用户应用程序。

devmap_devmem_setup(9F) 函数的语法如下所示:

int devmap_devmem_setup(devmap_cookie_t handle, dev_info_t *dip,
    struct devmap_callback_ctl *callbackops, uint_t rnumber, 
    offset_t roff, size_t len, uint_t maxprot, uint_t flags, 
    ddi_device_acc_attr_t *accattrp);

其中:

handle

系统用来标识映射的不透明的设备映射句柄。

dip

指向设备的 dev_info 结构的指针。

callbackops

指向 devmap_callback_ctl(9S) 结构的指针,此指针可在映射时向驱动程序通知用户事件。

rnumber

寄存器地址空间集的索引号。

roff

在设备内存中的偏移。

len

导出的长度(以字节为单位)。

maxprot

允许驱动程序为导出的设备内存中的不同区域指定不同的保护。

flags

必须设置为 DEVMAP_DEFAULTS

accattrp

指向 ddi_device_acc_attr(9S) 结构的指针。

rofflen 参数描述了寄存器集 rnumber 指定的设备内存中的一个范围。reg 属性用于描述 rnumber 所引用的寄存器规格。对于只有一个寄存器集的设备,将 rnumber 设置为 0 即可。范围通过 rofflen 定义。如果用户的应用程序映射位于通过 devmap(9E) 入口点传入的 offset 位置上,则可对此范围进行访问。驱动程序通常将 devmap(9E) 偏移直接传递给 devmap_devmem_setup(9F)。然后,mmap(2) 的返回地址将映射到寄存器集的起始地址。

通过 maxprot 参数,驱动程序可为导出的设备内存中的不同区域指定不同保护。例如,要禁止对某个区域进行写访问,可以只为该区域设置 PROT_READPROT_USER

以下示例说明如何将设备内存导出到应用程序。驱动程序首先确定请求的映射是否位于设备内存区域之内。设备内存的大小通过使用 ddi_dev_regsize(9F) 来确定。使用 ptob(9F)btopr(9F) 可将映射的长度向上舍入为页面大小的倍数。然后调用 devmap_devmem_setup(9F) 可将设备内存导出到应用程序。


示例 10–3 使用 devmap_devmem_setup() 例程

static int
xxdevmap(dev_t dev, devmap_cookie_t handle, offset_t off, size_t len,
    size_t *maplen, uint_t model)
{
    struct xxstate *xsp;
    int    error, rnumber;
    off_t regsize;
    
    /* Set up data access attribute structure */
    struct ddi_device_acc_attr xx_acc_attr = {
        DDI_DEVICE_ATTR_V0,
        DDI_NEVERSWAP_ACC,
        DDI_STRICTORDER_ACC
    };
    xsp = ddi_get_soft_state(statep, getminor(dev));
    if (xsp == NULL)
        return (-1);
    /* use register set 0 */
    rnumber = 0;
    /* get size of register set */
    if (ddi_dev_regsize(xsp->dip, rnumber, &regsize) != DDI_SUCCESS)
        return (-1);
    /* round up len to a multiple of a page size */
       len = ptob(btopr(len));
    if (off + len > regsize)
        return (-1);
    /* Set up the device mapping */
    error = devmap_devmem_setup(handle, xsp->dip, NULL, rnumber, 
    off, len, PROT_ALL, DEVMAP_DEFAULTS, &xx_acc_attr);
    /* acknowledge the entire range */
    *maplen = len;
    return (error);
}

将内核内存与用户映射相关联

一些设备驱动程序可能需要分配可供用户程序通过 mmap(2) 进行访问的内核内存。一个示例是为两个应用程序间的通信设置共享内存。另一个示例是在驱动程序和应用程序之间共享内存。

将内核内存导出到用户应用程序时,请执行以下步骤:

  1. 使用 ddi_umem_alloc(9F) 分配内核内存。

  2. 使用 devmap_umem_setup(9F) 导出内存。

  3. 不再需要内存时,使用 ddi_umem_free(9F) 释放内存。

为用户访问分配内核内存

使用 ddi_umem_alloc(9F) 可以分配导出到应用程序的内核内存。 ddi_umem_alloc() 的语法如下所示:

void *ddi_umem_alloc(size_t size, int flag, ddi_umem_cookie_t 
*cookiep);

其中:

size

要分配的字节数。

flag

用于确定休眠条件和内存类型。

cookiep

指向内核内存 cookie 的指针。

ddi_umem_alloc(9F) 分配按页对齐的内核内存。ddi_umem_alloc () 返回一个指向所分配内存的指针。最初,内存被零填充。分配的字节数是系统页面大小的倍数,该页面大小是通过 size 参数向上舍入得到的。分配的内存可在内核中使用。此内存也可导出到应用程序。cookiep 是指向用来描述所分配的内核内存的内核内存 cookie 的指针。驱动程序将内核内存导出到用户应用程序时,devmap_umem_setup(9F) 中会使用 cookiep

flag 参数用于指示 ddi_umem_alloc(9F) 是立即阻塞还是返回,以及分配的内核内存是否可换页。flag 参数的值如下所示:

DDI_UMEM_NOSLEEP

驱动程序无需等待内存成为可用。如果内存不可用,则返回 NULL

DDI_UMEM_SLEEP

驱动程序可以无限等待,直到内存可用为止。

DDI_UMEM_PAGEABLE

驱动程序允许内存页被换出。如果未设置,则锁定内存。

ddi_umem_lock() 函数可以执行设备锁定内存检查。此函数针对 project.max-locked-memory 中指定的限制值进行检查。如果当前项目的锁定内存使用量低于限制,则会增加项目的锁定内存字节计数。进行限制检查后,内存将会锁定。ddi_umem_unlock() 函数可以解除锁定内存,从而减少项目的锁定内存字节计数。

其中所用的记帐方法是不严密的 "full price"(足价)模式。例如,对于同一项目中 umem_lockmemory() 的具有重叠内存区域的两个调用程序会被计数两次。

有关 project.max-locked-memoryzone.max-locked_memory 对安装了区域的 Solaris 系统的资源控制的信息,请参见《Solaris 10 资源管理器开发者指南》resource_controls(5)

以下示例说明如何为应用程序访问分配内核内存。驱动程序会导出一页内核内存,它将被多个应用程序用作共享存储区。应用程序第一次映射共享页时,会在 segmap(9E) 中分配内存。如果驱动程序必须支持多个应用程序数据模型,则会再分配一页。例如,64 位驱动程序可能同时将内存导出到 64 位应用程序和 32 位应用程序。64 位应用程序共享第一页,32 位应用程序共享第二页。


示例 10–4 使用 ddi_umem_alloc() 例程

static int
xxsegmap(dev_t dev, off_t off, struct as *asp, caddr_t *addrp, off_t len,
    unsigned int prot, unsigned int maxprot, unsigned int flags, 
    cred_t *credp)
{
    int error;
    minor_t instance = getminor(dev);
    struct xxstate *xsp = ddi_get_soft_state(statep, instance);

    size_t mem_size;
        /* 64-bit driver supports 64-bit and 32-bit applications */
    switch (ddi_mmap_get_model()) {
        case DDI_MODEL_LP64:
             mem_size = ptob(2);
             break;
        case DDI_MODEL_ILP32:
             mem_size = ptob(1);
             break;
    }
       
    mutex_enter(&xsp->mu);
    if (xsp->umem == NULL) {

        /* allocate the shared area as kernel pageable memory */
        xsp->umem = ddi_umem_alloc(mem_size,
            DDI_UMEM_SLEEP | DDI_UMEM_PAGEABLE, &xsp->ucookie);
    }
    mutex_exit(&xsp->mu);
    /* Set up the user mapping */
    error = devmap_setup(dev, (offset_t)off, asp, addrp, len,
        prot, maxprot, flags, credp);
    
    return (error);
}

将内核内存导出到应用程序

使用 devmap_umem_setup(9F) 可将内核内存导出到用户应用程序。devmap_umem_setup() 必须通过驱动程序的 devmap(9E) 入口点进行调用 。devmap_umem_setup() 的语法如下所示:

int devmap_umem_setup(devmap_cookie_t handle, dev_info_t *dip,
    struct devmap_callback_ctl *callbackops, ddi_umem_cookie_t cookie,
    offset_t koff, size_t len, uint_t maxprot, uint_t flags,
    ddi_device_acc_attr_t *accattrp);

其中:

handle

用于描述映射的不透明结构。

dip

指向设备的 dev_info 结构的指针。

callbackops

指向 devmap_callback_ctl(9S) 结构的指针。

cookie

ddi_umem_alloc(9F) 返回的内核内存 cookie。

koff

cookie 指定的内核内存中的偏移。

len

导出的长度(以字节为单位)。

maxprot

用于为导出的映射指定可能的最大保护。

flags

必须设置为 DEVMAP_DEFAULTS

accattrp

指向 ddi_device_acc_attr(9S) 结构的指针。

handle 是系统用来标识映射的设备映射句柄。handle 通过 devmap(9E) 入口点传入。dip 是指向设备的 dev_info 结构的指针。callbackops 允许向驱动程序通知有关映射的用户事件。导出内核内存时,大多数驱动程序都会将 callbackops 设置为 NULL

kofflen 用于在 ddi_umem_alloc(9F) 分配的内核内存中指定一个范围。如果用户的应用程序映射位于通过 devmap(9E) 入口点传入的偏移上,则可对此范围进行访问。驱动程序通常将 devmap(9E) 偏移直接传递给 devmap_umem_setup(9F)。然后,mmap(2) 的返回地址将映射到 ddi_umem_alloc(9F) 返回的内核地址。kofflen 必须按页对齐。

通过 maxprot,驱动程序可为导出的内核内存中的不同区域指定不同的保护。例如,通过仅设置 PROT_READPROT_USER,一个区域可能不允许写访问。

以下示例说明如何将内核内存导出到应用程序。驱动程序首先检查请求的映射是否位于分配的内核内存区域之内。如果 64 位驱动程序收到来自 32 位应用程序的映射请求,则会将该请求重定向到内核存储区的第二页。此重定向可确保仅有编译到相同数据模型的应用程序才能共享相同的页。


示例 10–5 devmap_umem_setup(9F) 例程

static int
xxdevmap(dev_t dev, devmap_cookie_t handle, offset_t off, size_t len,
    size_t *maplen, uint_t model)
{
    struct xxstate *xsp;
    int    error;

    /* round up len to a multiple of a page size */
    len = ptob(btopr(len));
    /* check if the requested range is ok */
    if (off + len > ptob(1))
        return (ENXIO);
    xsp = ddi_get_soft_state(statep, getminor(dev));
    if (xsp == NULL)
        return (ENXIO);

    if (ddi_model_convert_from(model) == DDI_MODEL_ILP32)
        /* request from 32-bit application. Skip first page */
        off += ptob(1);

    /* export the memory to the application */
    error = devmap_umem_setup(handle, xsp->dip, NULL, xsp->ucookie,
        off, len, PROT_ALL, DEVMAP_DEFAULTS, NULL);
    *maplen = len;
    return (error);
}

释放为用户访问导出的内核内存

卸载驱动程序时,必须通过调用 ddi_umem_free(9F) 释放 ddi_umem_alloc(9F) 分配的内存。

void ddi_umem_free(ddi_umem_cookie_t cookie);

cookieddi_umem_alloc(9F) 返回的内核内存 cookie。