编写设备驱动程序

第 11 章 设备上下文管理

一些设备驱动程序(如用于图形硬件的驱动程序)可为用户进程提供对设备的直接访问。这些设备通常要求一次仅有一个进程访问设备。

本章介绍了可供设备驱动程序用于管理对此类设备的访问的接口集。本章提供有关以下主题的信息:

设备上下文简介

本节介绍设备上下文和上下文管理模型。

什么是设备上下文?

设备的上下文是指设备硬件的当前状态。设备驱动程序可代表进程管理该进程的设备上下文。驱动程序必须分别为访问设备的每个进程保留单独的设备上下文。设备驱动程序负责在进程访问设备时恢复正确的设备上下文。

上下文管理模型

帧缓存器可作为设备上下文管理的一个很好的示例。使用加速的帧缓存器,用户进程可以通过内存映射访问直接处理设备的控制寄存器。由于这些进程不使用传统的系统调用,因此访问设备的进程无需调用设备驱动程序。但是,如果进程要访问设备,则必须通知设备驱动程序。驱动程序需要恢复正确的设备上下文并且提供所需的任何同步。

要解决这一问题,可以使用设备上下文管理接口,在用户进程访问设备的内存映射区域时通知设备驱动程序,并控制对设备硬件的访问。设备驱动程序负责同步和管理各种设备上下文。用户进程访问映射时,设备驱动程序必须为该进程恢复正确的设备上下文。

每次用户进程执行以下任一操作时,都会通知设备驱动程序:

下图显示了映射到一个设备的内存中的多个用户进程。驱动程序授予了进程 B 对设备的访问权限,进程 B 不再向驱动程序通知访问情况。但是,如果进程 A 或进程 C 访问设备,仍通知驱动程序。

图 11–1 设备上下文管理

图中显示了三个进程 A、B 和 C,其中进程 B 拥有对设备的独占访问权限。

在将来某一时刻,进程 A 将访问设备。设备驱动程序会得到通知,并阻止进程 B 将来对该设备的访问。然后,驱动程序会为进程 B 保存设备上下文。驱动程序恢复进程 A 的设备上下文,然后授予进程 A 访问权限,如下图所示。此时,如果进程 B 或进程 C 访问设备,则会通知设备驱动程序。

图 11–2 切换到用户进程 A 的设备上下文

该图继续说明上图中的示例,其中独占的设备访问权限已切换给进程 A。

在多处理器计算机中,多个进程可能会尝试同时访问设备。此情况会引起抖动。有些设备需要较长的时间才能恢复设备上下文。要防止恢复设备上下文所需的 CPU 时间超过实际使用该设备上下文所需的时间,可以使用 devmap_set_ctx_timeout(9F) 设置进程访问设备所需的最短时间。

内核可以保证一旦设备驱动程序向某一进程授予了访问权限,便不允许其他任何进程在 devmap_set_ctx_timeout(9F) 指定的时间间隔内请求访问同一设备。

上下文管理操作

执行设备上下文管理的常规步骤如下:

  1. 定义 devmap_callback_ctl(9S) 结构。

  2. 根据需要分配用于保存设备上下文的空间。

  3. 通过 devmap_devmem_setup(9F) 设置到设备的用户映射和驱动程序通知。

  4. 通过 devmap_load(9F)devmap_unload(9F) 管理用户对设备的访问。

  5. 根据需要释放设备上下文结构。

devmap_callback_ctl 结构

设备驱动程序必须分配并初始化 devmap_callback_ctl(9S) 结构,以便通知系统用于设备上下文管理的入口点例程。

此结构使用以下语法:

struct devmap_callback_ctl {    
    int devmap_rev;
    int (*devmap_map)(devmap_cookie_t dhp, dev_t dev,
    uint_t flags, offset_t off, size_t len, void **pvtp);
    int (*devmap_access)(devmap_cookie_t dhp, void *pvtp,
    offset_t off, size_t len, uint_t type, uint_t rw);
    int (*devmap_dup)(devmap_cookie_t dhp, void *pvtp,
    devmap_cookie_t new_dhp, void **new_pvtp);
    void (*devmap_unmap)(devmap_cookie_t dhp, void *pvtp,
    offset_t off, size_t len, devmap_cookie_t new_dhp1,
    void **new_pvtp1, devmap_cookie_t new_dhp2,
    void **new_pvtp2);
};
devmap_rev

devmap_callback_ctl 结构的版本号。版本号必须设置为 DEVMAP_OPS_REV

devmap_map

必须设置为驱动程序的 devmap_map(9E) 入口点的地址。

devmap_access

必须设置为驱动程序的 devmap_access(9E) 入口点的地址。

devmap_dup

必须设置为驱动程序的 devmap_dup(9E) 入口点的地址。

devmap_unmap

必须设置为驱动程序的 devmap_unmap(9E) 入口点的地址。

用于设备上下文管理的入口点

以下入口点用于管理设备上下文:

devmap_map() 入口点

devmap(9E) 的语法如下所示:

int xxdevmap_map(devmap_cookie_t handle, dev_t dev, uint_t flags,
    offset_t offset, size_t len, void **new-devprivate);

驱动程序从其 devmap() 入口点返回并且系统已建立到设备内存的用户映射后,将会调用 devmap_map() 入口点。通过 devmap() 入口点,驱动程序可以执行其他处理操作或分配特定于映射的专用数据。例如,为了支持上下文切换,驱动程序必须分配上下文结构。然后,驱动程序必须将上下文结构与映射关联。

系统期望驱动程序在 *new-devprivate 中返回一个指向分配的专用数据的指针。驱动程序必须存储用于定义专用数据中的映射范围的 offsetlen。然后当系统调用 devmap_unmap(9E) 时,驱动程序将使用此信息来确定要取消映射的映射量。

flags 指示驱动程序是否应为映射分配专用上下文。例如,如果 flags 设置为 MAP_PRIVATE,则驱动程序可以分配用于存储设备上下文的内存区域。如果设置了 MAP_SHARED,驱动程序将返回指向共享区域的指针。

以下示例说明了 devmap() 入口点。驱动程序分配了一个新的上下文结构。然后,驱动程序便可保存通过入口点传入的相关参数。接下来,将通过分配或通过将映射附加至已经存在的共享上下文来为映射指定新的上下文。映射访问设备的最短时间间隔设置为 1 毫秒。


示例 11–1 使用 devmap() 例程

static int
int xxdevmap_map(devmap_cookie_t handle, dev_t dev, uint_t flags,
    offset_t offset, size_t len, void **new_devprivate)
{
    struct xxstate *xsp = ddi_get_soft_state(statep,
                  getminor(dev));
    struct xxctx *newctx;

    /* create a new context structure */
    newctx = kmem_alloc(sizeof (struct xxctx), KM_SLEEP);
    newctx->xsp = xsp;
    newctx->handle = handle;
    newctx->offset = offset;
    newctx->flags = flags;
    newctx->len = len;
    mutex_enter(&xsp->ctx_lock);
    if (flags & MAP_PRIVATE) {
        /* allocate a private context and initialize it */
        newctx->context = kmem_alloc(XXCTX_SIZE, KM_SLEEP);
        xxctxinit(newctx);
    } else {
        /* set a pointer to the shared context */
        newctx->context = xsp->ctx_shared;
    }
    mutex_exit(&xsp->ctx_lock);
    /* give at least 1 ms access before context switching */
    devmap_set_ctx_timeout(handle, drv_usectohz(1000));
    /* return the context structure */
    *new_devprivate = newctx;
    return(0);
}

devmap_access() 入口点

对转换无效的映射进行访问时,将会调用 devmap_access(9E) 入口点。映射转换在以下几种情况下无效:作为对 mmap(2) 的响应通过 devmap_devmem_setup(9F) 创建映射;通过 fork(2) 复制映射或通过调用 devmap_unload(9F) 显式使映射无效。

devmap_access() 的语法如下所示:

int xxdevmap_access(devmap_cookie_t handle, void *devprivate,
    offset_t offset, size_t len, uint_t type, uint_t rw);

其中:

handle

用户进程所访问的映射的映射句柄。

devprivate

指向与映射关联的驱动程序专用数据的指针。

offset

所访问映射内的偏移。

len

所访问内存的长度(以字节为单位)。

type

访问操作的类型。

rw

用于指定访问的方向。

系统期望 devmap_access(9E) 调用 devmap_do_ctxmgt(9F)devmap_default_access(9F) 以便在 devmap_access() 返回前装入内存地址转换。对于支持上下文切换的映射,设备驱动程序应调用 devmap_do_ctxmgt()。系统会通过 devmap_access(9E) 向此例程传递所有参数以及指向驱动程序入口点 devmap_contextmgt(9E) 的指针,该指针用来处理上下文切换。对于不支持上下文切换的映射,驱动程序应调用 devmap_default_access(9F)devmap_default_access() 的用途是调用 devmap_load(9F) 以装入用户转换。

以下示例说明了 devmap_access(9E) 入口点。该映射分为两个区域。在偏移 OFF_CTXMG 上开始并且长度为 CTXMGT_SIZE 字节的区域支持上下文管理。其余映射支持缺省访问。


示例 11–2 使用 devmap_access() 例程

#define OFF_CTXMG      0
#define CTXMGT_SIZE    0x20000    
static int
xxdevmap_access(devmap_cookie_t handle, void *devprivate,
    offset_t off, size_t len, uint_t type, uint_t rw)
{
    offset_t diff;
    int    error;

    if ((diff = off - OFF_CTXMG) >= 0 && diff < CTXMGT_SIZE) {
        error = devmap_do_ctxmgt(handle, devprivate, off,
            len, type, rw, xxdevmap_contextmgt);
    } else {
        error = devmap_default_access(handle, devprivate,
            off, len, type, rw);
    }
    return (error);
}

devmap_contextmgt() 入口点

devmap_contextmgt(9E) 的语法如下所示:

int xxdevmap_contextmgt(devmap_cookie_t handle, void *devprivate,
    offset_t offset, size_t len, uint_t type, uint_t rw);

devmap_contextmgt() 应使用当前对设备具有访问权限的映射的句柄调用 devmap_unload(9F)。此方法可使对于该映射的转换无效。通过此方法,可确保下次访问当前映射时针对该映射调用 devmap_access(9E)。对于引起访问事件发生的映射,需要验证其映射转换。相应地,驱动程序必须为进程请求访问恢复设备上下文。并且,驱动程序必须针对映射的 handle 调用 devmap_load(9F),该映射生成了对此入口点的调用。

访问已通过调用 devmap_load() 对映射转换进行验证的部分映射时不会导致调用 devmap_access()。对 devmap_unload() 的后续调用将使映射转换无效。通过此调用,可再次调用 devmap_access()

如果 devmap_load()devmap_unload() 返回错误,devmap_contextmgt() 应立即返回该错误。如果设备驱动程序在恢复设备上下文时遇到硬件故障,则应返回 -1。否则,成功处理访问请求后,devmap_contextmgt() 应返回零。如果从 devmap_contextmgt() 返回非零值,则会向进程发送 SIGBUSSIGSEGV

以下示例说明如何管理单页设备上下文。


注 –

xxctxsave()xxctxrestore() 是与设备相关的上下文保存和恢复函数。xxctxsave() 从寄存器中读取数据并将数据保存在软状态结构中。xxctxrestore() 提取软状态结构中保存的数据并将数据写入设备寄存器。请注意,执行读取、写入和保存都需要使用 DDI/DKI 数据访问例程。



示例 11–3 使用 devmap_contextmgt() 例程

static int
xxdevmap_contextmgt(devmap_cookie_t handle, void *devprivate,
    offset_t off, size_t len, uint_t type, uint_t rw)
{
    int    error;
    struct xxctx *ctxp = devprivate;
    struct xxstate *xsp = ctxp->xsp;
    mutex_enter(&xsp->ctx_lock);
    /* unload mapping for current context */
    if (xsp->current_ctx != NULL) {
        if ((error = devmap_unload(xsp->current_ctx->handle,
            off, len)) != 0) {
            xsp->current_ctx = NULL;
            mutex_exit(&xsp->ctx_lock);
            return (error);
        }
    }
    /* Switch device context - device dependent */
    if (xxctxsave(xsp->current_ctx, off, len) < 0) {
        xsp->current_ctx = NULL;
        mutex_exit(&xsp->ctx_lock);
        return (-1);
    }
    if (xxctxrestore(ctxp, off, len) < 0){
        xsp->current_ctx = NULL;
        mutex_exit(&xsp->ctx_lock);
        return (-1);
    }
    xsp->current_ctx = ctxp;
    /* establish mapping for new context and return */
    error = devmap_load(handle, off, len, type, rw);
    if (error)
        xsp->current_ctx = NULL;
    mutex_exit(&xsp->ctx_lock);
    return (error);
}

devmap_dup() 入口点

复制设备映射(例如,由调用 fork(2) 的用户进程进行复制)时,将会调用 devmap_dup(9E) 入口点。驱动程序预期会为新映射生成新的驱动程序专用数据。

devmap_dup() 的语法如下所示:

int xxdevmap_dup(devmap_cookie_t handle, void *devprivate,
    devmap_cookie_t new-handle, void **new-devprivate);

其中:

handle

正在复制的映射的映射句柄。

new-handle

已复制的映射的映射句柄。

devprivate

指向与正在复制的映射关联的驱动程序专用数据的指针。

*new-devprivate

应设置为指向用于新映射的新驱动程序专用数据的指针。

缺省情况下使用 devmap_dup() 所创建的映射会使其映射转换无效。第一次访问映射时,无效的映射转换会强制调用 devmap_access(9E) 入口点。

以下示例说明了一个典型的 devmap_dup() 例程。


示例 11–4 使用 devmap_dup() 例程

static int
xxdevmap_dup(devmap_cookie_t handle, void *devprivate,
    devmap_cookie_t new_handle, void **new_devprivate)
{
    struct xxctx *ctxp = devprivate;
    struct xxstate *xsp = ctxp->xsp;
    struct xxctx *newctx;
    /* Create a new context for the duplicated mapping */
    newctx = kmem_alloc(sizeof (struct xxctx), KM_SLEEP);
    newctx->xsp = xsp;
    newctx->handle = new_handle;
    newctx->offset = ctxp->offset;
    newctx->flags = ctxp->flags;
    newctx->len = ctxp->len;
    mutex_enter(&xsp->ctx_lock);
    if (ctxp->flags & MAP_PRIVATE) {
        newctx->context = kmem_alloc(XXCTX_SIZE, KM_SLEEP);
        bcopy(ctxp->context, newctx->context, XXCTX_SIZE);
    } else {
        newctx->context = xsp->ctx_shared;
    }
    mutex_exit(&xsp->ctx_lock);
    *new_devprivate = newctx;
    return(0);
}

devmap_unmap() 入口点

对映射取消映射时,将会调用 devmap_unmap(9E) 入口点。用户进程退出或调用 munmap(2) 系统调用会导致取消映射。

devmap_unmap() 的语法如下所示:

void xxdevmap_unmap(devmap_cookie_t handle, void *devprivate,
    offset_t off, size_t len, devmap_cookie_t new-handle1,
    void **new-devprivate1, devmap_cookie_t new-handle2,
    void **new-devprivate2);

其中:

handle

正在释放的映射的映射句柄。

devprivate

指向与映射关联的驱动程序专用数据的指针。

off

逻辑设备内存中取消映射开始处的偏移。

len

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

new-handle1

系统用来描述新区域的句柄,该新区域在 off - 1 位置结束。new-handle1 的值可以为 NULL

new-devprivate1

要由驱动程序通过用于新区域的专用驱动程序映射数据进行填充的指针,该新区域在 off -1 位置结束。如果 new-handle1 NULL,则会忽略 new-devprivate1

new-handle2

系统用来描述新区域的句柄,该新区域在 off + len 位置开始。new-handle2 的值可以为 NULL

new-devprivate2

要由驱动程序通过用于新区域的驱动程序专用映射数据进行填充的指针,该新区域在 off + len 位置开始。如果 new-handle2NULL,则会忽略 new-devprivate2

devmap_unmap() 例程预期会释放通过 devmap_map(9E)devmap_dup(9E) 创建此映射时分配的任何驱动程序专用资源。如果只是取消映射部分映射,则驱动程序必须在释放旧的专用数据之前为其余映射分配新的专用数据。不必针对已释放的映射的句柄调用 devmap_unload(9F),即使此句柄指向具有有效转换的映射时也是如此。不过,为了避免将来出现 devmap_access(9E) 问题,设备驱动程序应确保当前的映射表示形式设置为“无当前映射”。

以下示例说明了一个典型的 devmap_unmap() 例程。


示例 11–5 使用 devmap_unmap() 例程

static void
xxdevmap_unmap(devmap_cookie_t handle, void *devprivate,
    offset_t off, size_t len, devmap_cookie_t new_handle1,
    void **new_devprivate1, devmap_cookie_t new_handle2,
    void **new_devprivate2)
{
    struct xxctx *ctxp = devprivate;
    struct xxstate *xsp = ctxp->xsp;
    mutex_enter(&xsp->ctx_lock);

    /*
     * If new_handle1 is not NULL, we are unmapping
     * at the end of the mapping.
     */
    if (new_handle1 != NULL) {
        /* Create a new context structure for the mapping */
        newctx = kmem_alloc(sizeof (struct xxctx), KM_SLEEP);
        newctx->xsp = xsp;
           if (ctxp->flags & MAP_PRIVATE) {
               /* allocate memory for the private context and copy it */
            newctx->context = kmem_alloc(XXCTX_SIZE, KM_SLEEP);
            bcopy(ctxp->context, newctx->context, XXCTX_SIZE);
        } else {
            /* point to the shared context */
            newctx->context = xsp->ctx_shared;
        }
        newctx->handle = new_handle1;
        newctx->offset = ctxp->offset;
        newctx->len = off - ctxp->offset;
        *new_devprivate1 = newctx;
    }
    /*
     * If new_handle2 is not NULL, we are unmapping
     * at the beginning of the mapping.
     */
    if (new_handle2 != NULL) {
        /* Create a new context for the mapping */
        newctx = kmem_alloc(sizeof (struct xxctx), KM_SLEEP);
        newctx->xsp = xsp;
        if (ctxp->flags & MAP_PRIVATE) {
            newctx->context = kmem_alloc(XXCTX_SIZE, KM_SLEEP);
            bcopy(ctxp->context, newctx->context, XXCTX_SIZE);
        } else {
            newctx->context = xsp->ctx_shared;
        }
        newctx->handle = new_handle2;
        newctx->offset = off + len;
        newctx->flags = ctxp->flags;
        newctx->len = ctxp->len - (off + len - ctxp->off);
        *new_devprivate2 = newctx;
    }
    if (xsp->current_ctx == ctxp)
        xsp->current_ctx = NULL;
    mutex_exit(&xsp->ctx_lock);
    if (ctxp->flags & MAP_PRIVATE)
        kmem_free(ctxp->context, XXCTX_SIZE);
    kmem_free(ctxp, sizeof (struct xxctx));
}

将用户映射与驱动程序通知关联

用户进程通过 mmap(2) 请求到设备的映射时,将会调用驱动程序的 segmap(9E) 入口点。如果驱动程序需要管理设备上下文,则在设置内存映射时,驱动程序必须使用 ddi_devmap_segmap(9F)devmap_setup(9F)。这两个函数都会调用驱动程序的 devmap(9E) 入口点,该入口点使用 devmap_devmem_setup(9F) 将设备内存与用户映射关联。有关如何映射设备内存的详细信息,请参见第 10 章

驱动程序必须向系统通知 devmap_callback_ctl(9S) 入口点才能获取对用户映射的访问通知。驱动程序通过向 devmap_devmem_setup(9F) 提供一个指向 devmap_callback_ctl(9S) 结构的指针来通知系统。devmap_callback_ctl(9S) 结构描述了一组用于上下文管理的入口点。系统通过调用这些入口点来通知设备驱动程序管理有关设备映射的事件。

系统会将每个映射与一个映射句柄关联。此句柄会传递给每个用于上下文管理的入口点。该映射句柄可用来使映射转换无效和对映射转换进行验证。如果驱动程序使映射转换无效,则会向该驱动程序通知将来对映射的任何访问。如果驱动程序对映射转换进行验证,则不再向该驱动程序通知对映射的访问。映射总是在映射转换无效的情况下创建,以便第一次访问映射时将会通知驱动程序。

以下示例说明如何使用设备上下文管理接口设置映射。


示例 11–6 支持上下文管理的 devmap(9E) 入口点

static struct devmap_callback_ctl xx_callback_ctl = {
    DEVMAP_OPS_REV, xxdevmap_map, xxdevmap_access,
    xxdevmap_dup, xxdevmap_unmap
};

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;
    uint_t rnumber;
    int    error;
    
    /* Setup 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 (ENXIO);
    len = ptob(btopr(len));
    rnumber = 0;
    /* Set up the device mapping */
    error = devmap_devmem_setup(handle, xsp->dip, &xx_callback_ctl,
        rnumber, off, len, PROT_ALL, 0, &xx_acc_attr);
    *maplen = len;
    return (error);
}

管理映射访问

用户进程访问没有有效的映射转换的内存映射区域中的地址时,将会通知设备驱动程序。访问事件发生时,必须使当前对设备具有访问权限的进程的映射转换无效。必须恢复请求访问设备的进程的设备上下文。并且,必须对请求访问的进程的映射转换进行验证。

函数 devmap_load(9F)devmap_unload(9F) 用于验证映射转换和使其无效。

devmap_load() 入口点

devmap_load(9F) 的语法如下所示:

int devmap_load(devmap_cookie_t handle, offset_t offset,
    size_t len, uint_t type, uint_t rw);

devmap_load() 可以验证对于 handleoffsetlen 指定的映射页的映射转换。通过验证对这些页的映射转换,驱动程序将告知系统不要拦截对这些映射页的访问。并且,系统不得在未通知设备驱动程序的情况下允许继续进行访问。

必须通过映射的偏移和句柄调用 devmap_load(),该映射可生成访问事件以便完成访问。如果不针对此句柄调用 devmap_load(9F),则不会验证映射转换,并且进程将收到 SIGBUS

devmap_unload() 入口点

devmap_unload(9F) 的语法如下所示:

    int devmap_unload(devmap_cookie_t handle, offset_t offset,
     size_t len);

devmap_unload() 可使对 handleoffsetlen 指定的映射页的映射转换无效。通过使对这些页的映射转换无效,设备驱动程序将告知系统拦截对这些映射页的访问。并且,下次通过调用 devmap_access(9E) 入口点访问这些映射页时,系统必须通知设备驱动程序。

对于这两个函数而言,请求会影响包含 offset 的整页,直到包含由 offset + len 所表示的最后一个字节的整页(包含该页)。设备驱动程序必须确保对于所映射的每页设备内存而言,在任意时刻仅有一个进程具有有效转换。

如果成功,两个函数都将返回零。但是,如果在对映射转换进行验证或使其无效时出现错误,则该错误将返回给设备驱动程序。设备驱动程序必须将此错误返回给系统。