lx_audio.c revision 7f0b8309074a5d8e9f9d8ffe7aad7bb0b1ee6b1f
/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License (the "License").
* You may not use this file except in compliance with the License.
*
* You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at usr/src/OPENSOLARIS.LICENSE.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information: Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*/
/*
* Copyright 2009 Sun Microsystems, Inc. All rights reserved.
* Use is subject to license terms.
*/
#include <sys/id_space.h>
#include <sys/lx_audio.h>
#include <sys/sysmacros.h>
/* Properties used by the lx_audio driver */
#define LXA_PROP_INPUTDEV "inputdev"
#define LXA_PROP_OUTPUTDEV "outputdev"
/* default device paths used by this driver */
#define LXA_DEV_DEFAULT "/dev/audio"
#define LXA_DEV_CUSTOM_DIR "/dev/sound/"
/* maximum possible number of concurrent opens of this driver */
#define LX_AUDIO_MAX_OPENS 1024
/*
* these are default fragment size and fragment count values.
* these values were chosen to make quake work well on my
* laptop: 2Ghz Pentium M + NVIDIA GeForce Go 6400.
*
* for reference:
* - 1 sec of stereo output at 44Khz is about 171 Kb of data
* - 1 sec of mono output at 8Khz is about 8Kb of data
*/
/* maximum ammount of fragment memory we'll allow a process to mmap */
/* forward declarations */
typedef struct lxa_state lxa_state_t;
typedef struct lxa_zstate lxa_zstate_t;
/*
* Structure and enum declarations
*/
typedef enum {
LXA_TYPE_INVALID = 0,
struct lxa_zstate {
char *lxa_zs_zonename;
/*
* but instead we're keeing them as device node properties
* so that a user can easily see the audio configuration for
* a zone via prtconf.
*/
/*
* OSS doesn't support multiple opens of the audio device.
* (multiple opens of the mixer device are supported.)
* streams. (OSS does support two opens if one is for input
* and the other is for output.)
*/
/*
* we need to cache channel gain and balance. channel gain and
* balance map to PCM volume in OSS, which are supposedly a property
* of the underlying hardware. but in solaris, channels are
* implemented in software and only exist when an audio device
* is actually open. (each open returns a unique channel.) OSS
* work even if no audio device is open. hence, if no underlying
* device is open we need to cache the gain and balance setting.
*/
};
struct lxa_state {
int lxas_flags; /* original flags passed to open */
int lxas_devs_same; /* input and output device the same? */
/* input device variables */
int lxas_idev_flags; /* flags used for open */
/* output device variables */
int lxas_odev_flags; /* flags used for open */
/*
* since we support multiplexing of devices we need to remember
* certain parameters about the devices
*/
/*
* members needed to support mmap device access. note that to
* simplifly things we only support one mmap access per open.
*/
char *lxas_umem_ptr;
};
/*
* Global variables
*/
/*
* function declarations
*/
static void lxa_mmap_output_disable(lxa_state_t *);
/*
* functions
*/
static void
{
/* disable any mmap output that might still be going on */
/*
* the global zone state so that other opens of the audio device
* can now succeed.
*/
}
/* remove this state structure from the hash (if it's there) */
(void) mod_hash_remove(lxa_state_hash,
/* close any audio device that we have open */
/* free up any memory allocated by mmaps */
/* release the id associated with this state structure */
}
static char *
getzonename(void)
{
}
static char *
{
char *zpname;
int n;
/* prepend the zone name to the property name */
return (zpname);
}
static int
lxa_devprop_verify(char *pval)
{
int n;
return (0);
/* make sure the value is an integer */
for (n = 0; pval[n] != '\0'; n++) {
return (-1);
}
}
return (0);
}
static char *
{
char *zprop_name, *pval;
char *dev_path;
int n, rv;
/* attempt to lookup the property */
if (rv != DDI_PROP_SUCCESS)
return (NULL);
if (lxa_devprop_verify(pval) != 0) {
return (NULL);
}
/* there is no audio device specified */
return (NULL);
/* use the default audio device on the system */
} else {
/* a custom audio device was specified, generate a path */
}
/*
* if this is an audio control device so we need to append
* "ctl" to the path
*/
if (lxa_type == LXA_TYPE_AUDIOCTL) {
char *tmp;
}
return (dev_path);
}
static int
{
int n, rv;
/* set a default fragment size */
/* get info for the currently open audio devices */
return (rv);
return (rv);
/* if we're only open for reading or writing then it's easy */
return (0);
return (0);
}
/*
* well if we're open for reading and writing but the underlying
* device is the same then it's also pretty easy
*/
if (lxa_state->lxas_devs_same) {
"audio device reported inconsistent features");
return (EIO);
}
return (0);
}
/*
* figure out which software features we're going to support.
* we will report a feature as supported if both the input
* and output device support it.
*/
lxa_state->lxas_sw_features = 0;
if (n & AUDIO_SWFEATURE_MIXER)
/*
* figure out which hardware features we're going to support.
* for a first pass we will report a feature as supported if
* both the input and output device support it.
*/
lxa_state->lxas_hw_features = 0;
if (n & AUDIO_HWFEATURE_MSCODEC)
/*
* if we made it here then we have different audio input and output
* devices. this will allow us to report support for additional
* hardware features that may not supported by just the input or
* output device alone.
*/
/* always report tha we support both playback and recording */
/* always report full duplex support */
/* never report that we have input to output loopback support */
return (0);
}
static int
{
/*
* check if we have configuration properties for this zone.
* if we don't then audio isn't supported in this zone.
*/
/* make sure there is at least one device to read from or write to */
return (ENODEV);
/* see if the input and output devices are actually the same device */
/* we don't respect FEXCL */
/*
* if we're opening audio devices then we need to muck
*
* certain audio device may only support input or output
* to different devices we need to make sure we don't try
* and open the output device for reading and the input
* device for writing.
*
* need to do this because some audio devices won't let
* themselves be opened multiple times for read access.
*/
/* make sure we have devices to read from and write to */
goto out;
}
} else {
}
/* get an ident to open the devices */
goto out;
}
/* open the input device */
if (rv != 0) {
"unable to open audio device: %s", idev);
"possible zone audio configuration error");
goto out;
}
}
/* open the output device */
if (rv != 0) {
/*
* If this open failed and we previously opened an
* input device, it is the responsibility of the
* caller to close that device after we return
* failure here.
*/
"unable to open audio device: %s", odev);
"possible zone audio configuration error");
goto out;
}
}
/* free up stuff */
out:
return (rv);
}
void
{
thread_exit();
/*NOTREACHED*/
}
void
{
/* we better be setup for writing to the output device */
/* setup a uio to output one fragment */
uio.uio_offset = 0;
uio.uio_extflg = 0;
/* setup a uio to output a eof (a fragment with a length of 0) */
uio_null.uio_offset = 0;
uio_null.uio_extflg = 0;
/* first drain any pending audio output */
"AUDIO_DRAIN failed, aborting audio output");
/*NOTREACHED*/
}
/*
* we depend on the ai.play.eof value to keep track of
* audio output progress so reset it here.
*/
AUDIO_INITINFO(&ai);
"AUDIO_SETINFO failed, aborting audio output");
/*NOTREACHED*/
}
/*
* we're going to need to know the sampling rate and number
* of output channels to estimate how long we can sleep between
* requests.
*/
"AUDIO_GETINFO failed, aborting audio output");
/*NOTREACHED*/
}
/* estimate how many ticks it takes to output a fragment of data */
/* queue up three fragments of of data into the output stream */
eof = 3;
/* sanity check the eof value */
/* we always start audio output at fragment 0 */
/*
* we shouldn't have allowed the mapping if it isn't a multiple
* of the fragment size
*/
while (!lxa_state->lxas_mmap_thread_exit) {
/*
* calculate the start and ending offsets of the next
* fragment to output
*/
/* setup the uio to output one fragment of audio */
/* increment the current fragment index */
/* drop the audio lock before actually outputting data */
/*
* write the fragment of audio data to the device stream
* then write a eof to the stream to tell the device to
* increment ai.play.eof when it's done processing the
* fragment we just wrote
*/
"ldi_write() failed (%d), "
"resetting audio output", rv);
goto lxa_mmap_thread_top;
}
"ldi_write(eof) failed (%d), "
"resetting audio output", rv);
goto lxa_mmap_thread_top;
}
/*
* we want to avoid buffer underrun so ensure that
* there is always at least one fragment of data in the
* output stream.
*/
if (--eof > 0) {
continue;
}
/*
* now we wait until the audio device has finished outputting
* at least one fragment of data.
*/
retry = 0;
/*
* delay for the number of ticks it takes
* to output one fragment of data
*/
if (ticks_per_frag > 0)
/* check if we've managed to output any fragments */
"AUDIO_GETINFO failed (%d), "
"resetting audio output", rv);
/* re-start mmap audio output */
goto lxa_mmap_thread_top;
}
/* institute a random retry limit */
if (retry++ < 100) {
continue;
}
"output stalled, "
"resetting audio output");
/* re-start mmap audio output */
goto lxa_mmap_thread_top;
}
} else {
/* eof counter wrapped around */
}
/* we're done with this loop so re-aquire the lock */
}
}
/*NOTREACHED*/
}
static void
{
/* if the output thread isn't running there's nothing to do */
if (lxa_state->lxas_mmap_thread_running == 0) {
return;
}
/* tell the pcm mmap output thread to exit */
/* wait for the mmap output thread to exit */
}
static void
{
/* if the output thread is already running there's nothing to do */
if (lxa_state->lxas_mmap_thread_running != 0) {
return;
}
/* setup output state */
/* kick off a thread to do the mmap pcm output */
(void (*)())lxa_mmap_thread, lxa_state,
}
static int
{
/* we only support output via mmap */
return (EINVAL);
/* if the user hasn't mmap the device then there's nothing to do */
return (EINVAL);
/* copy in the request */
return (EFAULT);
/* a zero value disables output */
if (trigger == 0) {
return (0);
}
/* a non-zero value enables output */
return (0);
}
static int
{
int ptr;
/* we only support output via mmap */
return (EINVAL);
/* if the user hasn't mmap the device then there's nothing to do */
return (EINVAL);
/* if the output thread isn't running then there's nothing to do */
if (lxa_state->lxas_mmap_thread_running == 0)
return (EINVAL);
return (EFAULT);
return (0);
}
static int
{
return (EFAULT);
return (0);
}
static int
{
/* if the device is mmaped we can't change the fragment settings */
return (EINVAL);
/* copy in the request */
return (EFAULT);
/* do basic bounds checking */
return (EINVAL);
/* don't accept size values less than 16 */
return (0);
}
static int
{
int junk;
/* only applies to output buffers */
return (EINVAL);
/* can't fail so ignore the return value */
return (0);
}
/*
* lxa_audio_info_merge() usage notes:
*
* - it's important to make sure NOT to get the ai_idev and ai_odev
* parameters mixed up when calling lxa_audio_info_merge().
*
* - it's important for the caller to make sure that AUDIO_GETINFO
* was called for the input device BEFORE the output device. (see
* the comments for merging the monitor_gain setting to see why.)
*/
static void
{
/* if we're not setup for output return the intput device info */
return;
}
/* if we're not setup for input return the output device info */
return;
}
/* get record values from the input device */
/* get play values from the output device */
/* muting status only matters for the output device */
/* we don't support device reference counts, always return 1 */
/*
* set we calcuated out earlier.
*/
if (!lxa_state->lxas_devs_same) {
/*
* if the input and output devices are different
* physical devices then we don't support input to
* output loopback so we always report the input
* to output loopback gain to be zero.
*/
ai_merged->monitor_gain = 0;
} else {
/*
* the intput and output devices are actually the
* same physical device. hence it probably supports
* intput to output loopback. regardless we should
* pass back the intput to output gain reported by
* the device. when we pick a value to passback we
* use the output device value since that was
* the most recently queried. (we base this
* decision on the assumption that io gain is
* actually hardware setting in the device and
* hence if it is changed on one open instance of
* the device the change will be visable to all
* other instances of the device.)
*/
}
/*
* for currently enabled software features always return the
* merger of the two. (of course the enabled software features
* for the input and output devices should alway be the same,
* so if it isn't complain.)
*/
"unexpected sofware feature state");
}
static int
int mode)
{
/* copy in the request */
return (EFAULT);
/*
* if the caller is attempting to enable a software feature that
* we didn't report as supported the return an error
*/
return (EINVAL);
/*
* if a process has mmaped this device then we don't allow
* changes to the play.eof field (since mmap output depends
* on this field.
*/
return (EIO);
/* initialize the new requests */
/* remove audio input settings from the output device request */
/* remove audio output settings from the input device request */
/* apply settings to the intput device */
return (rv);
/* apply settings to the output device */
return (rv);
/*
* a AUDIO_SETINFO call performs an implicit AUDIO_GETINFO to
* return values (see the coments in audioio.h.) so we need
* to combine the values returned from the input and output
* device back into the users buffer.
*/
/* copyout the results */
return (EFAULT);
}
return (0);
}
static int
{
/* get the settings from the input device */
return (rv);
/* get the settings from the output device */
return (rv);
/*
* we need to combine the values returned from the input
* and output device back into a single user buffer.
*/
/* copyout the results */
return (EFAULT);
return (0);
}
static int
{
/* get the number of channels for the underlying device */
return (rv);
/* allocate the am_control_t structure */
/* get the device state and channel state */
return (rv);
}
/* return the audio_info structure */
return (0);
}
static int
{
int rv;
/* if there is no input device, query the output device */
/* if there is no ouput device, query the intput device */
/*
* now get the audio_info and channel information for the
* underlying output device.
*/
&ai_idev)) != 0)
return (rv);
&ai_odev)) != 0)
return (rv);
/* now merge the audio_info structures */
return (0);
}
static int
{
int rv;
return (rv);
switch (cmd) {
case LXA_IOC_MIXER_GET_VOL:
break;
case LXA_IOC_MIXER_GET_MIC:
break;
}
return (EFAULT);
return (0);
}
static int
{
/* get the new mixer settings */
return (EFAULT);
/* sanity check the mixer settings */
if (!LXA_MIXER_LEVELS_OK(&lxa_ml))
return (EINVAL);
/* initialize an audio_info struct with the new settings */
AUDIO_INITINFO(&ai);
switch (cmd) {
case LXA_IOC_MIXER_SET_VOL:
break;
case LXA_IOC_MIXER_SET_MIC:
break;
}
/*
* we're going to cheat here. normally the
* MIXERCTL_SETINFO ioctl take am_control_t and the
* AUDIO_SETINFO takes an audio_info_t. as it turns
* out the first element in a am_control_t is an
* audio_info_t. also, the rest of the am_control_t
* structure is normally ignored for a MIXERCTL_SETINFO
* ioctl. so here we'll try to fall back to the code
* that handles AUDIO_SETINFO ioctls.
*/
}
static int
{
/* simply return the cached pcm mixer settings */
return (EFAULT);
}
return (0);
}
static int
{
int rv;
/* get the new mixer settings */
return (EFAULT);
/* sanity check the mixer settings */
if (!LXA_MIXER_LEVELS_OK(&lxa_ml))
return (EINVAL);
/* if there is an active output channel, update it */
/* initialize an audio_info struct with the new settings */
AUDIO_INITINFO(&ai);
return (rv);
}
}
/* update the cached mixer settings */
return (0);
}
static int
{
int i, junk;
return (EFAULT);
/* make sure that zone_name is a valid string */
for (i = 0; i < sizeof (lxa_zr.lxa_zr_zone_name); i++)
break;
if (i == sizeof (lxa_zr.lxa_zr_zone_name))
return (EINVAL);
/* make sure that inputdev is a valid string */
for (i = 0; i < sizeof (lxa_zr.lxa_zr_inputdev); i++)
break;
if (i == sizeof (lxa_zr.lxa_zr_inputdev))
return (EINVAL);
/* make sure it's a valid inputdev property value */
return (EINVAL);
/* make sure that outputdev is a valid string */
for (i = 0; i < sizeof (lxa_zr.lxa_zr_outputdev); i++)
break;
if (i == sizeof (lxa_zr.lxa_zr_outputdev))
return (EINVAL);
/* make sure it's a valid outputdev property value */
return (EINVAL);
/* get the property names */
/*
* allocate and initialize a zone state structure
* since the audio device can't possibly be opened yet
* (since we're setting it up now and the zone isn't booted
* yet) assign some some resonable default pcm channel settings.
* also, default to one mixer channel.
*/
/*
* make sure this zone isn't already registered
* a zone is registered with properties for that zone exist
* or there is a zone state structure for that zone
*/
goto err_unlock;
}
goto err_unlock;
}
(mod_hash_val_t *)&junk) == 0)
goto err_unlock;
/*
* create the new properties and insert the zone state structure
* into the global hash
*/
goto err_prop_remove;
goto err_prop_remove;
(mod_hash_val_t)lxa_zs) != 0)
goto err_prop_remove;
/* success! */
/* cleanup */
return (0);
err:
}
return (EIO);
}
static int
{
int rv, i;
return (EFAULT);
/* make sure that zone_name is a valid string */
for (i = 0; i < sizeof (lxa_zr.lxa_zr_zone_name); i++)
break;
if (i == sizeof (lxa_zr.lxa_zr_zone_name))
return (EINVAL);
/* get the property names */
if (lxa_registered_zones <= 0) {
goto err_unlock;
}
/* make sure this zone is actually registered */
goto err_unlock;
}
goto err_unlock;
}
(mod_hash_val_t *)&lxa_zs) != 0) {
goto err_unlock;
}
/*
* if the audio device is currently in use then refuse to
* unregister the zone
*/
goto err_unlock;
}
/* success! cleanup zone config state */
/*
* note, the action of removing the zone state structure from the
* hash will automatically free lxa_zs->lxa_zs_zonename.
*
* the reason for this is that we used lxa_zs->lxa_zs_zonename
* as the hash key and by default mod_hash_create_strhash() uses
* mod_hash_strkey_dtor() as a the hash key destructor. (which
* free's the key for us.
*/
(void) mod_hash_remove(lxa_zstate_hash,
(mod_hash_val_t *)&lxa_zs);
/* cleanup */
return (0);
err:
return (rv);
}
static int
{
/* devctl ioctls are only allowed from the global zone */
if (getzoneid() != 0)
return (EINVAL);
switch (cmd) {
case LXA_IOC_ZONE_REG:
case LXA_IOC_ZONE_UNREG:
}
return (EINVAL);
}
static int
/*ARGSUSED*/
{
int rv;
/*
* this is a devctl node, it exists to administer this
* pseudo driver so it doesn't actually need access to
* any underlying audio devices. hence there is nothing
* really to do here. course, this driver should
* only be administered from the global zone.
*/
if (getzoneid() != 0)
return (EINVAL);
return (0);
}
/* lookup the zone state structure */
(mod_hash_val_t *)&lxa_zs) != 0) {
return (EIO);
}
/* determine what type of device was opened */
case LXA_MINORNUM_DSP:
break;
case LXA_MINORNUM_MIXER:
break;
default:
return (EINVAL);
}
/* all other opens are clone opens so get a new minor node */
/* allocate and initialize the new lxa_state structure */
/* initialize the input and output device */
return (rv);
}
/*
* save this audio statue structure into a hash indexed
* by it's minor device number. (this will provide a convient
* way to lookup the state structure on future operations.)
*/
(mod_hash_val_t)lxa_state) != 0) {
return (EIO);
}
/* apply the currently cached zone PCM mixer levels */
AUDIO_INITINFO(&ai);
return (rv);
}
}
/*
* we only allow one active open of the input or output device.
* check here for duplicate opens
*/
return (EBUSY);
}
return (EBUSY);
}
/* not a duplicate open, update the global zone state */
}
/* make sure to return our newly allocated dev_t */
return (0);
}
static int
/*ARGSUSED*/
{
/* handle devctl minor nodes (these nodes don't have a handle */
return (0);
/* get the handle for this device */
(mod_hash_val_t *)&lxa_state) != 0)
return (EINVAL);
return (0);
}
static int
/*ARGSUSED*/
{
/* get the handle for this device */
(mod_hash_val_t *)&lxa_state) != 0)
return (EINVAL);
/*
* if a process has mmaped this device then we don't allow
* any more reads or writes to the device
*/
return (EIO);
/* we can't do a read if there is no input device */
return (EBADF);
/* pass the request on */
}
static int
/*ARGSUSED*/
{
/* get the handle for this device */
(mod_hash_val_t *)&lxa_state) != 0)
return (EINVAL);
/*
* if a process has mmaped this device then we don't allow
* any more reads or writes to the device
*/
return (EIO);
/* we can't do a write if there is no output device */
return (EBADF);
/* pass the request on */
}
static int
/*ARGSUSED*/
int *rvalp)
{
/* handle devctl minor nodes (these nodes don't have a handle */
/* get the handle for this device */
(mod_hash_val_t *)&lxa_state) != 0)
return (EINVAL);
switch (cmd) {
case LXA_IOC_GETMINORNUM:
{
return (EFAULT);
}
return (0);
}
/* deal with native ioctl */
switch (cmd) {
case LXA_IOC_MMAP_OUTPUT:
case LXA_IOC_MMAP_PTR:
case LXA_IOC_GET_FRAG_INFO:
case LXA_IOC_SET_FRAG_INFO:
}
/* deal with layered ioctls */
switch (cmd) {
case AUDIO_DRAIN:
return (lxa_audio_drain(lxa_state));
case AUDIO_SETINFO:
return (lxa_audio_setinfo(lxa_state,
case AUDIO_GETINFO:
}
}
/* deal with native ioctl */
switch (cmd) {
case LXA_IOC_MIXER_GET_VOL:
return (lxa_mixer_get_common(lxa_state,
case LXA_IOC_MIXER_SET_VOL:
return (lxa_mixer_set_common(lxa_state,
case LXA_IOC_MIXER_GET_MIC:
return (lxa_mixer_get_common(lxa_state,
case LXA_IOC_MIXER_SET_MIC:
return (lxa_mixer_set_common(lxa_state,
case LXA_IOC_MIXER_GET_PCM:
case LXA_IOC_MIXER_SET_PCM:
}
}
return (EINVAL);
}
static int
/*ARGSUSED*/
{
void *umem_ptr;
int rv;
/* get the handle for this device */
(mod_hash_val_t *)&lxa_state) != 0)
return (EINVAL);
/* we only support mmaping of audio devices */
return (EINVAL);
/* we only support output via mmap */
return (EINVAL);
/* sanity check the amount of memory the user is allocating */
if ((len == 0) ||
(len > LXA_OSS_FRAG_MEM) ||
return (EINVAL);
/* allocate and clear memory to mmap */
return (ENOMEM);
/* setup the memory mappings */
if (rv != 0) {
return (EIO);
}
/* we only support one mmap per open */
return (EBUSY);
}
return (0);
}
static int
/*ARGSUSED*/
{
if (cmd != DDI_ATTACH)
return (DDI_FAILURE);
if (instance != 0)
return (DDI_FAILURE);
/* create our minor nodes */
return (DDI_FAILURE);
return (DDI_FAILURE);
return (DDI_FAILURE);
/* allocate our data structures */
return (DDI_SUCCESS);
}
static int
/*ARGSUSED*/
{
if (cmd != DDI_DETACH)
return (DDI_FAILURE);
if (lxa_registered_zones > 0)
return (DDI_FAILURE);
return (DDI_SUCCESS);
}
static int
/*ARGSUSED*/
{
switch (infocmd) {
case DDI_INFO_DEVT2DEVINFO:
return (DDI_SUCCESS);
case DDI_INFO_DEVT2INSTANCE:
*resultp = (void *)0;
return (DDI_SUCCESS);
}
return (DDI_FAILURE);
}
/*
* Driver flags
*/
static struct cb_ops lxa_cb_ops = {
lxa_open, /* open */
lxa_close, /* close */
nodev, /* strategy */
nodev, /* print */
nodev, /* dump */
lxa_read, /* read */
lxa_write, /* write */
lxa_ioctl, /* ioctl */
lxa_devmap, /* devmap */
nodev, /* mmap */
ddi_devmap_segmap, /* segmap */
nochpoll, /* chpoll */
ddi_prop_op, /* prop_op */
NULL, /* cb_str */
NULL,
};
0,
NULL,
NULL,
ddi_quiesce_not_needed, /* quiesce */
};
/*
* Module linkage information for the kernel.
*/
&mod_driverops, /* type of module */
"linux audio driver", /* description of module */
&lxa_ops /* driver ops */
};
static struct modlinkage modlinkage = {
&modldrv,
};
/*
* standard module entry points
*/
int
_init(void)
{
return (mod_install(&modlinkage));
}
int
_fini(void)
{
return (mod_remove(&modlinkage));
}
int
{
}