mdeg.c revision 1ae0874509b6811fdde1dfd46f0d93fd09867a3f
/*
* 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
* or http://www.opensolaris.org/os/licensing.
* 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 2006 Sun Microsystems, Inc. All rights reserved.
* Use is subject to license terms.
*/
#pragma ident "%Z%%M% %I% %E% SMI"
/*
* MD Event Generator (MDEG) Module
*/
#include <sys/machsystm.h>
#include <sys/taskq.h>
#include <sys/disp.h>
#include <sys/cmn_err.h>
#include <sys/note.h>
#include <sys/mdeg.h>
#include <sys/mach_descrip.h>
#include <sys/mdesc.h>
/*
* A single client registration
*/
typedef struct mdeg_clnt {
boolean_t valid; /* structure is in active use */
mdeg_node_match_t *nmatch; /* node match filter */
mdeg_node_spec_t *pspec; /* parent match filter */
mdeg_cb_t cb; /* the client callback */
caddr_t cb_arg; /* argument to the callback */
uint64_t magic; /* sanity checking magic */
mdeg_handle_t hdl; /* handle assigned by MDEG */
} mdeg_clnt_t;
/*
* Global MDEG data
*
* Locking Strategy:
*
* mdeg.lock - lock used to sychronize system wide MD updates. An
* MD update must be treated as an atomic event. The lock is
* taken when notification that a new MD is available and held
* until all clients have been notified.
*
* mdeg.rwlock - lock used to sychronize access to the table of
* registered clients. The reader lock must be held when looking
* up client information in the table. The writer lock must be
* held when modifying any client information.
*/
static struct mdeg {
taskq_t *taskq; /* for internal processing */
boolean_t enabled; /* enable/disable taskq processing */
kmutex_t lock; /* synchronize MD updates */
md_t *md_prev; /* previous MD */
md_t *md_curr; /* current MD */
mdeg_clnt_t *tbl; /* table of registered clients */
krwlock_t rwlock; /* client table lock */
uint_t maxclnts; /* client table size */
uint_t nclnts; /* current number of clients */
} mdeg;
/*
* Debugging routines
*/
#ifdef DEBUG
uint_t mdeg_debug = 0x0;
static void mdeg_dump_clnt(mdeg_clnt_t *clnt);
static void mdeg_dump_table(void);
#define MDEG_DBG if (mdeg_debug) printf
#define MDEG_DUMP_CLNT mdeg_dump_clnt
#define MDEG_DUMP_TABLE mdeg_dump_table
#else /* DEBUG */
#define MDEG_DBG _NOTE(CONSTCOND) if (0) printf
#define MDEG_DUMP_CLNT
#define MDEG_DUMP_TABLE()
#endif /* DEBUG */
/*
* Global constants
*/
#define MDEG_MAX_TASKQ_THR 512 /* maximum number of taskq threads */
#define MDEG_MAX_CLNTS_INIT 64 /* initial client table size */
#define MDEG_MAGIC 0x4D4445475F48444Cull /* 'MDEG_HDL' */
/*
* A client handle is a 64 bit value with two pieces of
* information encoded in it. The upper 32 bits are the
* index into the table of a particular client structure.
* The lower 32 bits are a counter that is incremented
* each time a client structure is reused.
*/
#define MDEG_IDX_SHIFT 32
#define MDEG_COUNT_MASK 0xfffffffful
#define MDEG_ALLOC_HDL(_idx, _count) (((uint64_t)_idx << MDEG_IDX_SHIFT) | \
((uint64_t)(_count + 1) & \
MDEG_COUNT_MASK))
#define MDEG_HDL2IDX(hdl) (hdl >> MDEG_IDX_SHIFT)
#define MDEG_HDL2COUNT(hdl) (hdl & MDEG_COUNT_MASK)
static const char trunc_str[] = " ... }";
/*
* Utility routines
*/
static mdeg_clnt_t *mdeg_alloc_clnt(void);
static void mdeg_notify_client(void *);
static mde_cookie_t mdeg_find_start_node(md_t *, mdeg_node_spec_t *);
static boolean_t mdeg_node_spec_match(md_t *, mde_cookie_t, mdeg_node_spec_t *);
static void mdeg_get_diff_results(md_diff_cookie_t, mdeg_result_t *);
int
mdeg_init(void)
{
int tblsz;
/*
* Grab the current MD
*/
if ((mdeg.md_curr = md_get_handle()) == NULL) {
cmn_err(CE_WARN, "unable to cache snapshot of MD");
return (-1);
}
/*
* Initialize table of registered clients
*/
mdeg.maxclnts = MDEG_MAX_CLNTS_INIT;
tblsz = mdeg.maxclnts * sizeof (mdeg_clnt_t);
mdeg.tbl = kmem_zalloc(tblsz, KM_SLEEP);
rw_init(&mdeg.rwlock, NULL, RW_DRIVER, NULL);
mdeg.nclnts = 0;
/*
* Initialize global lock
*/
mutex_init(&mdeg.lock, NULL, MUTEX_DRIVER, NULL);
/*
* Initialize the task queue
*/
mdeg.taskq = taskq_create("mdeg_taskq", 1, minclsyspri, 1,
MDEG_MAX_TASKQ_THR, TASKQ_PREPOPULATE | TASKQ_DYNAMIC);
/* ready to begin handling clients */
mdeg.enabled = B_TRUE;
return (0);
}
void
mdeg_fini(void)
{
/*
* Flip the enabled switch off to make sure that
* no events get dispatched while things are being
* torn down.
*/
mdeg.enabled = B_FALSE;
/* destroy the task queue */
taskq_destroy(mdeg.taskq);
/*
* Deallocate the table of registered clients
*/
kmem_free(mdeg.tbl, mdeg.maxclnts * sizeof (mdeg_clnt_t));
rw_destroy(&mdeg.rwlock);
/*
* Free up the cached MDs.
*/
if (mdeg.md_curr)
(void) md_fini_handle(mdeg.md_curr);
if (mdeg.md_prev)
(void) md_fini_handle(mdeg.md_prev);
mutex_destroy(&mdeg.lock);
}
static mdeg_clnt_t *
mdeg_alloc_clnt(void)
{
mdeg_clnt_t *clnt;
int idx;
mdeg_clnt_t *newtbl;
uint_t newmaxclnts;
uint_t newtblsz;
uint_t oldtblsz;
ASSERT(RW_WRITE_HELD(&mdeg.rwlock));
/* search for an unused slot in the table */
for (idx = 0; idx < mdeg.maxclnts; idx++) {
clnt = &mdeg.tbl[idx];
if (!clnt->valid) {
break;
}
}
/* found any empty slot */
if (idx != mdeg.maxclnts) {
goto found;
}
/*
* There was no free space in the table. Grow
* the table to double its current size.
*/
MDEG_DBG("client table full:\n");
MDEG_DUMP_TABLE();
newmaxclnts = mdeg.maxclnts * 2;
newtblsz = newmaxclnts * sizeof (mdeg_clnt_t);
newtbl = kmem_zalloc(newtblsz, KM_SLEEP);
/* copy old table data to the new table */
oldtblsz = mdeg.maxclnts * sizeof (mdeg_clnt_t);
bcopy(mdeg.tbl, newtbl, oldtblsz);
/*
* Since the old table was full, the first free entry
* will be just past the end of the old table.
*/
clnt = &mdeg.tbl[mdeg.maxclnts];
/* clean up the old table */
kmem_free(mdeg.tbl, oldtblsz);
mdeg.tbl = newtbl;
mdeg.maxclnts = newmaxclnts;
found:
ASSERT(clnt->valid == 0);
clnt->hdl = MDEG_ALLOC_HDL(idx, MDEG_HDL2COUNT(clnt->hdl));
return (clnt);
}
static mdeg_clnt_t *
mdeg_get_client(mdeg_handle_t hdl)
{
int idx;
mdeg_clnt_t *clnt;
idx = MDEG_HDL2IDX(hdl);
/* check if index is out of bounds */
if ((idx < 0) || (idx >= mdeg.maxclnts)) {
MDEG_DBG("mdeg_get_client: index out of bounds\n");
return (NULL);
}
clnt = &mdeg.tbl[idx];
/* check for a valid client */
if (!clnt->valid) {
MDEG_DBG("mdeg_get_client: client is not valid\n");
return (NULL);
}
/* make sure the handle is an exact match */
if (clnt->hdl != hdl) {
MDEG_DBG("mdeg_get_client: bad handle\n");
return (NULL);
}
if (clnt->magic != MDEG_MAGIC) {
MDEG_DBG("mdeg_get_client: bad magic\n");
return (NULL);
}
return (clnt);
}
/*
* Send a notification to a client immediately after it registers.
* The result_t is a list of all the nodes that match their specified
* nodes of interest, all returned on the added list. This serves
* as a base of reference to the client. All future MD updates are
* relative to this list.
*/
static int
mdeg_notify_client_reg(mdeg_clnt_t *clnt)
{
md_t *mdp = NULL;
mde_str_cookie_t nname;
mde_str_cookie_t aname;
mde_cookie_t startnode;
int nnodes;
int nodechk;
mde_cookie_t *listp = NULL;
mdeg_result_t *mdeg_res = NULL;
int rv = MDEG_SUCCESS;
mutex_enter(&mdeg.lock);
/*
* Handle the special case where the node specification
* is NULL. In this case, call the client callback without
* any results. All processing is left to the client.
*/
if (clnt->pspec == NULL) {
/* call the client callback */
(*clnt->cb)(clnt->cb_arg, NULL);
goto done;
}
if ((mdp = md_get_handle()) == NULL) {
cmn_err(CE_WARN, "unable to retrieve current MD");
rv = MDEG_FAILURE;
goto done;
}
startnode = mdeg_find_start_node(mdp, clnt->pspec);
if (startnode == MDE_INVAL_ELEM_COOKIE) {
/* not much we can do */
cmn_err(CE_WARN, "unable to match node specifier");
rv = MDEG_FAILURE;
goto done;
}
/*
* Use zalloc to provide correct default values for the
* unused removed, match_prev, and match_curr lists.
*/
mdeg_res = kmem_zalloc(sizeof (mdeg_result_t), KM_SLEEP);
nname = md_find_name(mdp, clnt->nmatch->namep);
aname = md_find_name(mdp, "fwd");
nnodes = md_scan_dag(mdp, startnode, nname, aname, NULL);
if (nnodes == 0) {
MDEG_DBG("mdeg_notify_client_reg: no nodes of interest\n");
rv = MDEG_SUCCESS;
goto done;
} else if (nnodes == -1) {
MDEG_DBG("error scanning DAG\n");
rv = MDEG_FAILURE;
goto done;
}
MDEG_DBG("mdeg_notify_client_reg: %d node%s of interest\n",
nnodes, (nnodes == 1) ? "" : "s");
/* get the list of nodes of interest */
listp = kmem_alloc(sizeof (mde_cookie_t) * nnodes, KM_SLEEP);
nodechk = md_scan_dag(mdp, startnode, nname, aname, listp);
ASSERT(nodechk == nnodes);
mdeg_res->added.mdp = mdp;
mdeg_res->added.mdep = listp;
mdeg_res->added.nelem = nnodes;
/* call the client callback */
(*clnt->cb)(clnt->cb_arg, mdeg_res);
done:
mutex_exit(&mdeg.lock);
if (mdp)
(void) md_fini_handle(mdp);
if (listp)
kmem_free(listp, sizeof (mde_cookie_t) * nnodes);
if (mdeg_res)
kmem_free(mdeg_res, sizeof (mdeg_result_t));
return (rv);
}
/*
* Register to receive an event notification when the system
* machine description is updated.
*
* Passing NULL for the node specification parameter is valid
* as long as the match specification is also NULL. In this
* case, the client will receive a notification when the MD
* has been updated, but the callback will not include any
* information. The client is then responsible for obtaining
* its own copy of the system MD and performing any processing
* manually.
*/
int
mdeg_register(mdeg_node_spec_t *pspecp, mdeg_node_match_t *nmatchp,
mdeg_cb_t cb, void *cb_arg, mdeg_handle_t *hdlp)
{
mdeg_clnt_t *clnt;
/*
* If the RW lock is held, a client is calling
* register from its own callback.
*/
if (RW_LOCK_HELD(&mdeg.rwlock)) {
MDEG_DBG("mdeg_register: rwlock already held\n");
return (MDEG_FAILURE);
}
/* node spec and node match must both be valid, or both NULL */
if (((pspecp != NULL) && (nmatchp == NULL)) ||
((pspecp == NULL) && (nmatchp != NULL))) {
MDEG_DBG("mdeg_register: invalid parameters\n");
return (MDEG_FAILURE);
}
rw_enter(&mdeg.rwlock, RW_WRITER);
clnt = mdeg_alloc_clnt();
ASSERT(clnt);
/*
* Fill in the rest of the data
*/
clnt->nmatch = nmatchp;
clnt->pspec = pspecp;
clnt->cb = cb;
clnt->cb_arg = cb_arg;
clnt->magic = MDEG_MAGIC;
/* do this last */
clnt->valid = B_TRUE;
MDEG_DBG("client registered (0x%lx):\n", clnt->hdl);
MDEG_DUMP_CLNT(clnt);
mdeg.nclnts++;
if (mdeg_notify_client_reg(clnt) != MDEG_SUCCESS) {
bzero(clnt, sizeof (mdeg_clnt_t));
rw_exit(&mdeg.rwlock);
return (MDEG_FAILURE);
}
rw_exit(&mdeg.rwlock);
*hdlp = clnt->hdl;
return (MDEG_SUCCESS);
}
int
mdeg_unregister(mdeg_handle_t hdl)
{
mdeg_clnt_t *clnt;
mdeg_handle_t mdh;
/*
* If the RW lock is held, a client is calling
* unregister from its own callback.
*/
if (RW_LOCK_HELD(&mdeg.rwlock)) {
MDEG_DBG("mdeg_unregister: rwlock already held\n");
return (MDEG_FAILURE);
}
/* lookup the client */
if ((clnt = mdeg_get_client(hdl)) == NULL) {
return (MDEG_FAILURE);
}
rw_enter(&mdeg.rwlock, RW_WRITER);
MDEG_DBG("client unregistered (0x%lx):\n", hdl);
MDEG_DUMP_CLNT(clnt);
/* save the handle to prevent reuse */
mdh = clnt->hdl;
bzero(clnt, sizeof (mdeg_clnt_t));
clnt->hdl = mdh;
mdeg.nclnts--;
rw_exit(&mdeg.rwlock);
return (MDEG_SUCCESS);
}
/*
* Simple algorithm for now, grab the global lock and let all
* the clients update themselves in parallel. There is a lot of
* room for improvement here. We could eliminate some scans of
* the DAG by imcrementally scanning at lower levels of the DAG
* rather than having each client start its own scan from the root.
*/
void
mdeg_notify_clients(void)
{
md_t *md_new;
mdeg_clnt_t *clnt;
int idx;
int nclnt;
rw_enter(&mdeg.rwlock, RW_READER);
mutex_enter(&mdeg.lock);
/*
* Rotate the MDs
*/
if ((md_new = md_get_handle()) == NULL) {
cmn_err(CE_WARN, "unable to retrieve new MD");
goto done;
}
if (mdeg.md_prev) {
(void) md_fini_handle(mdeg.md_prev);
}
mdeg.md_prev = mdeg.md_curr;
mdeg.md_curr = md_new;
if (mdeg.nclnts == 0) {
MDEG_DBG("mdeg_notify_clients: no clients registered\n");
goto done;
}
/* dispatch the update notification to all clients */
for (idx = 0, nclnt = 0; idx < mdeg.maxclnts; idx++) {
clnt = &mdeg.tbl[idx];
if (!clnt->valid)
continue;
MDEG_DBG("notifying client 0x%lx (%d/%d)\n", clnt->hdl,
++nclnt, mdeg.nclnts);
(void) taskq_dispatch(mdeg.taskq, mdeg_notify_client,
(void *)clnt, TQ_SLEEP);
}
taskq_wait(mdeg.taskq);
done:
mutex_exit(&mdeg.lock);
rw_exit(&mdeg.rwlock);
}
static void
mdeg_notify_client(void *arg)
{
mdeg_clnt_t *clnt = (mdeg_clnt_t *)arg;
md_diff_cookie_t mdd = MD_INVAL_DIFF_COOKIE;
mdeg_result_t mdeg_res;
mde_cookie_t md_prev_start;
mde_cookie_t md_curr_start;
rw_enter(&mdeg.rwlock, RW_READER);
if (!mdeg.enabled) {
/* trying to shutdown */
MDEG_DBG("mdeg_notify_client: mdeg disabled, aborting\n");
goto cleanup;
}
/*
* Handle the special case where the node specification
* is NULL. In this case, call the client callback without
* any results. All processing is left to the client.
*/
if (clnt->pspec == NULL) {
/* call the client callback */
(*clnt->cb)(clnt->cb_arg, NULL);
MDEG_DBG("MDEG client callback done\n");
goto cleanup;
}
/* find our start nodes */
md_prev_start = mdeg_find_start_node(mdeg.md_prev, clnt->pspec);
if (md_prev_start == MDE_INVAL_ELEM_COOKIE) {
goto cleanup;
}
md_curr_start = mdeg_find_start_node(mdeg.md_curr, clnt->pspec);
if (md_curr_start == MDE_INVAL_ELEM_COOKIE) {
goto cleanup;
}
/* diff the MDs */
mdd = md_diff_init(mdeg.md_prev, md_prev_start, mdeg.md_curr,
md_curr_start, clnt->nmatch->namep, clnt->nmatch->matchp);
if (mdd == MD_INVAL_DIFF_COOKIE) {
MDEG_DBG("unable to diff MDs\n");
goto cleanup;
}
/*
* Cache the results of the diff
*/
mdeg_get_diff_results(mdd, &mdeg_res);
/* call the client callback */
(*clnt->cb)(clnt->cb_arg, &mdeg_res);
MDEG_DBG("MDEG client callback done\n");
cleanup:
rw_exit(&mdeg.rwlock);
if (mdd != MD_INVAL_DIFF_COOKIE)
(void) md_diff_fini(mdd);
}
static mde_cookie_t
mdeg_find_start_node(md_t *md, mdeg_node_spec_t *nspec)
{
mde_cookie_t *nodesp;
mde_str_cookie_t nname;
mde_str_cookie_t aname;
int nnodes;
int idx;
if ((md == NULL) || (nspec == NULL))
return (MDE_INVAL_ELEM_COOKIE);
nname = md_find_name(md, nspec->namep);
aname = md_find_name(md, "fwd");
nnodes = md_scan_dag(md, NULL, nname, aname, NULL);
if (nnodes == 0)
return (MDE_INVAL_ELEM_COOKIE);
nodesp = kmem_alloc(sizeof (mde_cookie_t) * nnodes, KM_SLEEP);
(void) md_scan_dag(md, NULL, nname, aname, nodesp);
for (idx = 0; idx < nnodes; idx++) {
if (mdeg_node_spec_match(md, nodesp[idx], nspec)) {
mde_cookie_t res = nodesp[idx];
kmem_free(nodesp, sizeof (mde_cookie_t) * nnodes);
return (res);
}
}
kmem_free(nodesp, sizeof (mde_cookie_t) * nnodes);
return (MDE_INVAL_ELEM_COOKIE);
}
static boolean_t
mdeg_node_spec_match(md_t *md, mde_cookie_t node, mdeg_node_spec_t *nspec)
{
mdeg_prop_spec_t *prop;
ASSERT(md && nspec);
ASSERT(node != MDE_INVAL_ELEM_COOKIE);
prop = nspec->specp;
while (prop->type != MDET_LIST_END) {
switch (prop->type) {
case MDET_PROP_VAL: {
uint64_t val;
if (md_get_prop_val(md, node, prop->namep, &val) != 0)
return (B_FALSE);
if (prop->ps_val != val)
return (B_FALSE);
break;
}
case MDET_PROP_STR: {
char *str;
if (md_get_prop_str(md, node, prop->namep, &str) != 0)
return (B_FALSE);
if (strcmp(prop->ps_str, str) != 0)
return (B_FALSE);
break;
}
default:
return (B_FALSE);
}
prop++;
}
return (B_TRUE);
}
static void
mdeg_get_diff_results(md_diff_cookie_t mdd, mdeg_result_t *res)
{
/*
* Cache added nodes.
*/
res->added.mdp = mdeg.md_curr;
res->added.nelem = md_diff_added(mdd, &(res->added.mdep));
if (res->added.nelem == -1) {
bzero(&(res->added), sizeof (mdeg_diff_t));
}
/*
* Cache removed nodes.
*/
res->removed.mdp = mdeg.md_prev;
res->removed.nelem = md_diff_removed(mdd, &(res->removed.mdep));
if (res->removed.nelem == -1) {
bzero(&(res->removed), sizeof (mdeg_diff_t));
}
/*
* Cache matching node pairs.
*/
res->match_curr.mdp = mdeg.md_curr;
res->match_prev.mdp = mdeg.md_prev;
res->match_curr.nelem = md_diff_matched(mdd, &(res->match_prev.mdep),
&(res->match_curr.mdep));
res->match_prev.nelem = res->match_curr.nelem;
if (res->match_prev.nelem == -1) {
bzero(&(res->match_prev), sizeof (mdeg_diff_t));
bzero(&(res->match_curr), sizeof (mdeg_diff_t));
}
}
#ifdef DEBUG
/*
* Generate a string that represents the node specifier
* structure. Clamp the string length if the specifier
* structure contains too much information.
*
* General form:
*
* <nodename>:{<propname>=<propval>,...}
* e.g.
* vdevice:{name=vsw,reg=0x0}
*/
static void
mdeg_spec_str(mdeg_node_spec_t *spec, char *buf, int len)
{
mdeg_prop_spec_t *prop;
int offset;
boolean_t first = B_TRUE;
char *end = buf + len;
offset = snprintf(buf, len, "%s:{", spec->namep);
buf += offset;
len -= offset;
if (len <= 0)
goto trunc;
prop = spec->specp;
while (prop->type != MDET_LIST_END) {
switch (prop->type) {
case MDET_PROP_VAL:
offset = snprintf(buf, len, "%s%s=0x%lx",
(first) ? "" : ",", prop->namep, prop->ps_val);
buf += offset;
len -= offset;
if (len <= 0)
goto trunc;
break;
case MDET_PROP_STR:
offset = snprintf(buf, len, "%s%s=%s",
(first) ? "" : ",", prop->namep, prop->ps_str);
buf += offset;
len -= offset;
if (len <= 0)
goto trunc;
break;
default:
(void) snprintf(buf, len, "}");
return;
}
if (first)
first = B_FALSE;
prop++;
}
(void) snprintf(buf, len, "}");
return;
trunc:
/* string too long, truncate it */
buf = end - (strlen(trunc_str) + 1);
(void) sprintf(buf, trunc_str);
}
/*
* Generate a string that represents the match structure.
* Clamp the string length if the match structure contains
* too much information.
*
* General form:
*
* <nodename>:{<propname>,...}
* e.g.
* nmatch=vport:{reg}
*/
static void
mdeg_match_str(mdeg_node_match_t *match, char *buf, int len)
{
md_prop_match_t *prop;
int offset;
boolean_t first = B_TRUE;
char *end = buf + len;
offset = snprintf(buf, len, "%s:{", match->namep);
buf += offset;
len -= offset;
if (len <= 0)
goto trunc;
prop = match->matchp;
while (prop->type != MDET_LIST_END) {
offset = snprintf(buf, len, "%s%s", (first) ? "" : ",",
prop->namep);
buf += offset;
len -= offset;
if (len <= 0)
goto trunc;
if (first)
first = B_FALSE;
prop++;
}
(void) snprintf(buf, len, "}");
return;
trunc:
/* string too long, truncate it */
buf = end - (strlen(trunc_str) + 1);
(void) sprintf(buf, trunc_str);
}
#define MAX_FIELD_STR 80
static void
mdeg_dump_clnt(mdeg_clnt_t *clnt)
{
char str[MAX_FIELD_STR];
if (!clnt->valid) {
MDEG_DBG(" valid=B_FALSE\n");
return;
}
mdeg_spec_str(clnt->pspec, str, MAX_FIELD_STR);
MDEG_DBG(" pspecp=%s\n", str);
mdeg_match_str(clnt->nmatch, str, MAX_FIELD_STR);
MDEG_DBG(" nmatch=%s\n", str);
}
static void
mdeg_dump_table(void)
{
int idx;
mdeg_clnt_t *clnt;
for (idx = 0; idx < mdeg.maxclnts; idx++) {
clnt = &(mdeg.tbl[idx]);
MDEG_DBG("client %d (0x%lx):\n", idx, clnt->hdl);
mdeg_dump_clnt(clnt);
}
}
#endif /* DEBUG */