Writing Device Drivers
  Search only this book

Device Context Management

10

Some device drivers, such as those for graphics hardware, provide user processes with direct access to the device. These devices often require that only one process at a time accesses the device.
This chapter describes the set of interfaces that allow device drivers to manage access to such devices.

What Is A Device Context?

The context of a device is the current state of the device hardware. The device context for a process is managed by the device driver on behalf of the process. The device driver must maintain a separate device context for each process that accesses the device. It is the device driver's responsibility to restore the correct device context when a process accesses the device.

Context Management Model

An accelerated frame buffer is an example of a device that allows user processes (such as graphics applications) to directly manipulate the control registers of the device through memory-mapped access. Since these processes are not using the traditional I/O system calls (read(2), write(2), and ioctl(2)), the device driver is no longer called when a process accesses the device. However, it is important that the device driver be notified when a process is about to access a device so that it can restore the correct device context and provide any needed synchronization.
To resolve this problem, the device context management interfaces allow a device driver to be notified when a user processes accesses memory-mapped regions of the device and to control accesses to the device's hardware. Synchronization and management of the various device contexts is the responsibility of the device driver. When a user process accesses a mapping, the device driver must restore the correct device context for that process.
A device driver will be notified whenever one of the following events occurs on a mapping:
  • Access to a mapping by a user process
  • Duplication of a mapping by a user process
  • Freeing of a mapping by a user process
Figure 10-1 is a snapshot of multiple user processes that have memory mapped a device. Process B has been granted access to the device by the driver, and the driver is no longer notified of accesses by process B. However, the driver is still notified if either process A or process C access the device.

Graphic

Figure 10-1

At some point in the future, process A accesses the device. The device driver is notified of this and blocks future access to the device by process B. It then saves the device context for process B, restores the device context of process A, and grants access to process A. This is illustrated in Figure 10-2. At this point, the device driver will be notified if either process B or process C access the device.

Graphic

Figure 10-2

Multiprocessor Considerations

On a multiprocessor machine, multiple processes could be attempting to access the device at the same time. This can cause thrashing. The kernel prevents this from happening by guaranteeing that once a device driver has granted access to a process, no other process will be allowed to request access to the same device for at least one clock tick.
However, some devices require more time to restore a device context than others. To prevent more CPU time from being used to restore a device context than to actually use that device context, the time that a process needs to have access to the device must be increased. If more time than one click tick is
required, the driver can block new access to the device for an additional predetermined amount of time using the standard thread synchronization function calls. See "Thread Synchronization" on page 77 for more information.

Context Management Operation

In general, here are the steps for performing device context management:
  1. Define a ddi_mapdev_ctl(9S) structure.

  2. Allocate space to save device context if necessary.

  3. Set up user mappings to the device and driver notifications with ddi_mapdev(9F) and ddi_mapdev_set_device_acc_attr(9F)

  4. Manage user access to the device with ddi_mapdev_intercept(9F) and ddi_mapdev_nointercept(9F).

  5. Free the device context structure if needed.

State Structure

This section adds the following fields to the state structure. See "State Structure" on page 67 for more information.
    kmutex_t              ctx_lock;
    struct xxctx          *current_ctx;

The structure xxctx is the driver private device context structure for the examples used in this section. It looks like this:
struct xxctx {
    ddi_mapdev_handle_t       handle;
    char                      context[XXCTX_SIZE];
    struct xxstate            *xsp;
};

The context field stores the actual device context. In this case, it is simply a chunk of memory; in other cases, it may actually be a series of structure fields corresponding to device registers.

Declarations and Data Structures

Device drivers that use the device context management interfaces must include the following declaration:
char _depends_on[] = "misc/seg_mapdev";

ddi_mapdev_ctl

The device driver must allocate and initialize a ddi_mapdev_ctl(9S) structure to inform the system of its device context management entry point routines.
This structure contains the following fields:
struct ddi_mapdev_ctl {
    int mapdev_rev;
    int (*mapdev_access)(ddi_mapdev_handle_t handle,
        void *private, off_t offset);
    void (*mapdev_free)(ddi_mapdev_handle_t handle, void
        *private);
    int (*mapdev_dup)(ddi_mapdev_handle_t oldhandle,
        void *oldprivate, ddi_mapdev_handle_t newhandle,
        void **newprivate);
};

mapdev_rev is the version number of the ddi_mapdev_ctl(9S) structure. It must be set to MAPDEV_REV.
mapdev_access must be set to the address of the driver's mapdev_access(9E) entry point.
mapdev_free must be set to the address of the driver's mapdev_free(9E) entry point.
mapdev_dup must be set to the address of the driver's mapdev_dup(9E) entry point.

Associating Devices with User Mappings

When a user process requests a mapping to a device with mmap(2), the device's segmap(9E) entry point is called. The device must use ddi_mapdev(9F) and ddi_mapdev_set_device_acc_attr(9F) when setting up the memory mapping if it wants to manage device contexts. Otherwise the device driver
must use ddi_segmap_setup(9F) to set up the mapping. See Chapter 9, "Drivers for Block Devices" for more information. A ddi_segmap_setup(9E) entry point must be defined before ddi_mapdev(9F) can be used.

ddi_mapdev

int ddi_mapdev(dev_t dev, off_t offset, struct as *asp,
        caddr_t *addrp, off_t len, u_int prot, u_int maxprot,
        u_int flags, cred_t *cred, struct ddi_mapdev_ctl *m_ops,
        ddi_mapdev_handle_t *handlep, void *private_data);

ddi_mapdev(9F) is similar to ddi_segmap_setup(9F) in that they both allow a user to map device space. In addition to establishing a mapping, ddi_mapdev(9F) informs the system of the ddi_mapdev_ctl(9S) entry points and creates a mapping handle to the mapping in *handlep. This mapping handle can be used to invalidate and validate the mapping translations. If the driver invalidates the mapping translations, it will be notified of any future access to the mapping. If the driver validates the mapping translations, it will no longer be notified of accesses to the mapping. Mappings are always created with the mapping translations invalidated so that the driver will be notified on first access to the mapping.
To ensure that a device driver can distinguish between the various user processes that have memory-mapped the device, only mappings of type MAP_PRIVATE can be used with ddi_mapdev(9F).
The dev, offset, asp, addrp, len, prot, maxprot, flags, and cred arguments are passed into the segmap(9E) entry point and should be passed on to ddi_mapdev(9F) unchanged. ddi_mapdev(9F) also takes the driver-defined structure ddi_mapdev_ctl(9S) and a pointer to device private data. This pointer is passed into each entry point and is usually a pointer to the device context structure.

ddi_mapdev_set_device_acc_attr

int ddi_mapdev_set_device_acc_attr(
        ddi_mapdev_handle_t mapping_handle, off_t offset, off_t len,
        ddi_device_acc_attr_t *accattrp, unit_t rnumber);

This routine assigns device access attributes to a range of device memory in the register set given by rnumber.
*accattrp defines the device access attributes. See ddi_device_acc_att(9S) for more information.
mapping_handle is a mapping handle returned from a call to ddi_mapdev(9F)
Requests affect the entire page containing the offset and all the pages up to and including the entire page containing the last byte as indicated by offset + len. The device driver must make sure that for each page of device memory being mapped only one process has valid translations at any one time.
Code Example 10-2 shows how to set up a mapping using the device context management interfaces.
Code Example 10-1 segmap(9E) entry point
static struct ddi_mapdev_ctl xx_mapdev_ctl = {
    MAPDEV_REV,
    xxmapdev_access,
    xxmapdev_free,
    xxmapdev_dup
};

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;
    int     instance = getminor(dev);
    struct xxstate *xsp = ddi_get_soft_state(statep, instance);
    struct xxctx *newctx;
    struct ddi_device_acc_attr xxaccattr;

/* Setup data access attribute structure */
    xxaccattr_devacc_attr_version = DDI_DEVICE_ATTR_V0
    xxaccattr_devacc_attr_endian_flags = DDI_NEVERSWAP_ACC
    xxaccattr_devacc_attr_dataorder = DDI_STRICTORDER_ACC

    /* Create a new context for this mapping */
    newctx = kmem_alloc(sizeof (struct xxctx), KM_SLEEP);
    newctx->xsp = xsp;
    /* Set up mapping */
    error = ddi_mapdev(dev, off, asp, addrp, len, prot,
        maxprot, flags, credp, &xx_mapdev_ctl, &newctx->handle,

        newctx);
    if (error)
        kmem_free(newctx, sizeof (struct xxctx));

    error = ddi_mapdev_set_device_acc_attr(newctx->handle, off,
        len, &xxaccattr, 0);
    if (error)
        kmem_free(newctx, sizeof (struct xxctx));

    return (error);
}

Managing Mapping Accesses

The device driver is notified when a user process accesses an address in the memory-mapped region that does not have valid mapping translations. When the access event occurs, the mapping translations of the process that currently has access to the device must be invalidated. The device context of the process requesting access to the device must be restored, and the translations of the mapping of the process requesting access must be validated.
The functions ddi_mapdev_intercept(9F) and ddi_mapdev_nointercept(9F) are used to invalidate and validate mapping translations.

ddi_mapdev_intercept()

int ddi_mapdev_intercept(ddi_mapdev_handle_t handle,
    off_t offset, off_t len);

ddi_mapdev_intercept(9F) invalidates the mapping translations for the pages of the mapping specified by handle, offset, and len. By invalidating the mapping translations for these pages, the device driver is telling the system to intercept accesses to these pages of the mapping and notify the device driver the next time these pages of the mapping are accessed by calling the mapdev_access(9E) entry point.

ddi_mapdev_nointercept()

int ddi_mapdev_nointercept(ddi_mapdev_handle_t handle,
    off_t offset, off_t len);

ddi_mapdev_nointercept(9F) validates the mapping translations for the pages of the mapping specified by handle, offset, and len. By validating the mapping translations for these pages, the driver is telling the system not to intercept accesses to these pages of the mapping and allow accesses to proceed without notifying the device driver.
ddi_mapdev_nointercept(9F) must be called with the offset and the handle of the mapping that generated the access event for the access to complete. If ddi_mapdev_nointercept(9F) is not called on this handle, the mapping translations will not be validated and the process will receive a SIGBUS.
For both functions, requests affect the entire page containing the offset and all the pages up to and including the entire page containing the last byte as indicated by offset + len. The device driver must make sure that for each page of device memory being mapped only one process has valid translations at any one time.
Both functions return zero if they are successful. If, however, there was an error in validating or invalidating the mapping translations, that error is returned to the device driver. It is the device driver's responsibility to return this error to the system.

Device Context Management Entry Points

The following device driver entry points are used to manage device context:

mapdev_access()

int xxmapdev_access(ddi_mapdev_handle_t handle, void *devprivate,
    off_t offset);

This entry point is called when an access is made to a mapping whose translations are invalid. Mapping translations are invalidated when the mapping is created with ddi_mapdev(9F) in response to mmap(2), duplicated by fork(2), or explicitly invalidated by a call to ddi_mapdev_intercept(9F).
handle is the mapping handle of the mapping that was accessed by a user process.
devprivate is a pointer to the driver private data associated with the mapping.
offset is the offset within the mapping that was accessed.
In general, mapdev_access(9E) should call ddi_mapdev_intercept(9F), with the handle of the mapping that currently has access to the device, to invalidate the translations for that mapping. This ensures that a call to mapdev_access(9E) occurs for the current mapping the next time it is accessed. To validate the mapping translations for the mapping that caused the access event to occur, the driver must restore the device context for the process requesting access and call ddi_mapdev_nointercept(9F) on the handle of the mapping that generated the call to this entry point.
Accesses to portions of mappings that have had their mapping translations validated by a call to ddi_mapdev_nointercept(9F) do not generate a call to mapdev_access(9E). A subsequent call to ddi_mapdev_intercept(9F) will invalidate the mapping translations and allow mapdev_access(9E) to be called again.
If either ddi_mapdev_intercept(9F) or ddi_mapdev_nointercept(9F) return an error, mapdev_access(9E) should immediately return that error. If the device driver encounters a hardware failure while restoring a device context, a -1 should be returned. Otherwise, after successfully handling the access request, mapdev_access(9E) should return zero. A return of other than zero from mapdev_access(9E) will cause a SIGBUS or SIGSEGV to be sent to the process.
Code Example 10-2 shows how to manage a one-page device context.
Code Example 10-2 mapdev_access(9E) routine
static int
xxmapdev_access(ddi_mapdev_handle_t handle, void *devprivate,
    off_t offset)
{
    int error;
    struct xxctx     *ctxp = devprivate;
    struct xxstate *xsp = ctxp->xsp;
    mutex_enter(&xsp->ctx_lock);
    /* enable access callback for the current mapping */
    if (xsp->current_ctx != NULL) {
        if ((error = ddi_mapdev_intercept(xsp->current_ctx->handle,
             offset, 0)) != 0) {

                 xsp->current_ctx = NULL;
                 mutex_exit(&xsp->ctx_lock);
                 return (error);
        }
    }
    /* Switch device context - device dependent*/
    if (xxctxsave(xsp->current_ctx) < 0) {
        xsp->current_ctx = NULL;
        mutex_exit(&xsp->ctx_lock);
        return (-1);
    }
    if (xxctxrestore(ctxp) < 0){
        xsp->current_ctx = NULL;
        mutex_exit(&xsp->ctx_lock);
        return (-1);
    }
    xsp->current_ctx = ctxp;
    /* Disable access callback for handle and return */
    error = ddi_mapdev_nointercept(handle, offset, 0);
    if (error)
        xsp->current_ctx = NULL;
    mutex_exit(&xsp->ctx_lock);
    return(error);
}


Note - xxctxsave and xxctxrestore are device dependent context save and restore functions. xxctxsave reads data from the registers using the Solaris 2.5 DDI/DKI data access routines and saves it in the soft state structure. xxctxrestore takes data saved in the soft state structure and writes it to device registers using the Solaris DDI/DKI 2.5 data access routines.

mapdev_free()

void xxmapdev_free(ddi_mapdev_handle_t handle, void *devprivate);

This entry point is called when a mapping is unmapped. This can be caused by a user process exiting or calling the munmap(2) system call. Partial unmappings are not supported and will cause the munmap(2) system call to fail with EINVAL.
handle is the mapping handle of the mapping being freed.
devprivate is a pointer to the driver private data associated with the mapping.
The mapdev_free(9E) routine is expected to free any driver-private resources that were allocated when this mapping was created, either by ddi_mapdev(9F) or by mapdev_dup(9E).
There is no need to call ddi_mapdev_intercept(9F) on the handle of the mapping being freed even if it is the mapping with the valid translations. However, to prevent future problems in mapdev_access(9E), the device driver should make sure that its representation of the current mapping is set to "no current mapping".
Code Example 10-3 mapdev_free(9E) routine
static void
xxmapdev_free(ddi_mapdev_handle_t handle, void *devprivate)
{
    struct xxctx     *ctxp = devprivate;
    struct xxstate *xsp = ctxp->xsp;
    mutex_enter(&xsp->ctx_lock);
    if (xsp->current_ctx == ctxp)
        xsp->current_ctx = NULL;
    mutex_exit(&xsp->ctx_lock);
    kmem_free(ctxp, sizeof (struct xxctx));
}

mapdev_dup()

int xxmapdev_dup(ddi_mapdev_handle_t handle, void *devprivate,
    ddi_mapdev_handle_t new_handle, void **new_devprivate);

This entry point is called when a device mapping is duplicated, for example, by a user process calling fork(2). The driver is expected to generate new driver private data for the new mapping.
handle is the mapping handle of the mapping being duplicated
new_handle is the mapping handle of the mapping that was duplicated
devprivate is a pointer to the driver private data associated with the mapping being duplicated
*new_devprivate should be set to point to the new driver-private data for the new mapping.
Mappings created with mapdev_dup(9E) will, by default, have their mapping translations invalidated. This will force a call to the mapdev_access(9E) entry point the first time the mapping is accessed.
Code Example 10-4 mapdev_dup(9E) routine
static int
xxmapdev_dup(ddi_mapdev_handle_t handle, void *devprivate,
    ddi_mapdev_handle_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);
    mutex_enter(&xsp->ctx_lock);
    newctx->xsp = xsp;
    bcopy(ctxp->context, newctx->context, XXCTX_SIZE);
    newctx->handle = new_handle;
    *new_devprivate = newctx;
    mutex_exit(&xsp->ctx_lock);
    return(0);
}