Subject: kern/2112: configuration support for loadable drivers
To: None <gnats-bugs@NetBSD.ORG>
From: Matthias Drochner <drochner@zelux6.zel.kfa-juelich.de>
List: netbsd-bugs
Date: 02/22/1996 21:26:28
>Number:         2112
>Category:       kern
>Synopsis:       up to now, it is hard to make loadable device drivers
>Confidential:   no
>Severity:       non-critical
>Priority:       low
>Responsible:    kern-bug-people (Kernel Bug People)
>State:          open
>Class:          change-request
>Submitter-Id:   net
>Arrival-Date:   Thu Feb 22 15:50:09 1996
>Last-Modified:
>Originator:     Matthias Drochner
>Organization:
KFA Juelich
>Release:        1.1
>Environment:
	NetBSD 1.1 (or -current), arch independent
System: ULTRIX zelux6 4.2 0 RISC
Machine: mips
>Description:
	Current autoconfiguration doesn't cover loadable devices/drivers.
	This PR contains a possible solution.
	The code is used by the author for PCI driver development.
>How-To-Repeat:
	---
>Fix:
	This PR contains:
a) patches for sys/device.h and kern/subr_autoconf.c
b) a set of support functions for loading/unloading drivers, which could
   be added to kern/subr_autoconf.c or reside in a separate file.
c) a loadable driver boiler-plate as an example

The patches (a) implement the following changes to the autoconf code:
- change list of active devices (alldevs) from a linked list to a
  TAILQ - this allows removal of devices (in the opposite order of
  attaching)
- add 2 members to "struct cfdriver"
   - int cd_detach(struct device*): driver frees all resources allocated
     by the device instance (return 1 if successfull, 0 if not possible)
   - int cd_reprobe(struct device*, struct cfdata*): driver checks the
     device instance (struct device) for new childs which fit cfdata.
     (and attaches them!)
     return 1 if something found, 0 if not.
- allow extension of the database of possible devices. Currently, only
  the statically allocated array "cfdata" is searched. This patch makes
  the database a TAILQ of such arrays. New cfdata arrays can be added and
  removed at runtime.

Note: I'm fully aware that the implementation can be done more elegant.
      (put queue initializations in a common function called at startup,
      avoid the intermediate "struct cftable" - make "config" spit out
      a queueable structure directly)
      It is intented to be "minimal-invasive".

Part b consists of functions which supplement "subr_autoconf.c":
- int attach_loadable(char *parentname, int parentunit, struct cftable*)
  add the config info in "cftable" to the database, try to attach the
  first member of "cftable" to a parent described by the name string
  and the unit number (-1=don't care).
  return: 1 if successful, 0 if no device found.
- int detach_loadable(struct cftable*)
  try to detach all devices which were found by "cftable".
  return: 1 if all devices could be detached, 0 if not.

Part c shows a loadable driver as used by the author.


General note: This concept makes a clear distinction between the
drivers and the cfdata describing device instances. It is possible
to load a module which contains only cfdata describing new instances
on an existing driver. This makes it possible to handle the hairy
case of a PCI-PCI bridge: it is child and parent of a PCI bus.
btw: there can't be 2 "starred" cfdata entries referring to the
same driver at this time, to overcome this a change to the attach code
is needed.

####################################################
####################### Part a - patches:

Index: sys/device.h
===================================================================
RCS file: /zelnfs/p3/cosy/sources/netbsd/sys/sys/sys/device.h,v
retrieving revision 1.1.1.1
diff -c -2 -r1.1.1.1 device.h
*** 1.1.1.1	1995/12/20 20:01:15
--- device.h	1996/02/14 17:26:18
***************
*** 48,51 ****
--- 48,53 ----
  #define	_SYS_DEVICE_H_
  
+ #include <sys/queue.h>
+ 
  /*
   * Minimal device structures.
***************
*** 63,67 ****
  struct device {
  	enum	devclass dv_class;	/* this device's classification */
! 	struct	device *dv_next;	/* next in list of all */
  	struct	cfdata *dv_cfdata;	/* config data that found us */
  	int	dv_unit;		/* device unit number */
--- 65,69 ----
  struct device {
  	enum	devclass dv_class;	/* this device's classification */
! 	CIRCLEQ_ENTRY(device) dv_next;	/* next in list of all */
  	struct	cfdata *dv_cfdata;	/* config data that found us */
  	int	dv_unit;		/* device unit number */
***************
*** 114,117 ****
--- 116,121 ----
  	int	cd_indirect;		/* indirectly configure subdevices */
  	int	cd_ndevs;		/* size of cd_devs array */
+ 	int (*cd_detach) __P((struct device*));
+ 	int (*cd_reprobe) __P((struct device*, struct cfdata*));
  };
  
***************
*** 135,140 ****
  };
  
! struct	device *alldevs;	/* head of list of all devices */
  struct	evcnt *allevents;	/* head of list of all events */
  
  void *config_search __P((cfmatch_t, struct device *, void *));
--- 139,153 ----
  };
  
! CIRCLEQ_HEAD(device_head, device);
! extern struct device_head alldevs;	/* head of list of all devices */
! 
  struct	evcnt *allevents;	/* head of list of all events */
+ 
+ struct cftable{
+   struct cfdata *tab;
+   TAILQ_ENTRY(cftable) list;
+ };
+ TAILQ_HEAD(cftable_head, cftable);
+ extern struct cftable_head allcftables;
  
  void *config_search __P((cfmatch_t, struct device *, void *));
Index: kern/subr_autoconf.c
===================================================================
RCS file: /zelnfs/p3/cosy/sources/netbsd/sys/sys/kern/subr_autoconf.c,v
retrieving revision 1.1.1.1
diff -c -2 -r1.1.1.1 subr_autoconf.c
*** 1.1.1.1	1995/12/20 20:00:06
--- subr_autoconf.c	1996/02/14 18:34:58
***************
*** 52,55 ****
--- 52,56 ----
  #include <lib/libkern/libkern.h>
  #include <machine/limits.h>
+ #include <sys/queue.h>
  
  /*
***************
*** 75,78 ****
--- 76,85 ----
  };
  
+ struct cftable_head allcftables;
+ 
+ static struct cftable staticcftable={
+   cfdata
+ };
+ 
  /*
   * Apply the matching function and choose the best.  This is used
***************
*** 133,136 ****
--- 140,149 ----
  	register short *p;
  	struct matchinfo m;
+ 	struct cftable *t;
+ 
+ 	if(!allcftables.tqh_last){ /* XXX */
+ 	  TAILQ_INIT(&allcftables);
+ 	  TAILQ_INSERT_TAIL(&allcftables, &staticcftable, list);
+ 	}
  
  	m.fn = fn;
***************
*** 140,153 ****
  	m.indirect = parent && parent->dv_cfdata->cf_driver->cd_indirect;
  	m.pri = 0;
! 	for (cf = cfdata; cf->cf_driver; cf++) {
! 		/*
! 		 * Skip cf if no longer eligible, otherwise scan through
! 		 * parents for one matching `parent', and try match function.
! 		 */
! 		if (cf->cf_fstate == FSTATE_FOUND)
! 			continue;
! 		for (p = cf->cf_parents; *p >= 0; p++)
! 			if (parent->dv_cfdata == &cfdata[*p])
! 				mapply(&m, cf);
  	}
  	return (m.match);
--- 153,168 ----
  	m.indirect = parent && parent->dv_cfdata->cf_driver->cd_indirect;
  	m.pri = 0;
! 	for(t=allcftables.tqh_first; t; t=t->list.tqe_next){
! 	  for (cf = t->tab; cf->cf_driver; cf++) {
! 	    /*
! 	     * Skip cf if no longer eligible, otherwise scan through
! 	     * parents for one matching `parent', and try match function.
! 	     */
! 	    if (cf->cf_fstate == FSTATE_FOUND)
! 	      continue;
! 	    for (p = cf->cf_parents; *p >= 0; p++)
! 	      if (parent->dv_cfdata == &(t->tab)[*p])
! 		mapply(&m, cf);
! 	  }
  	}
  	return (m.match);
***************
*** 171,191 ****
  	void *match;
  	int indirect;
  
  	indirect = parent && parent->dv_cfdata->cf_driver->cd_indirect;
! 	for (cf = cfdata; cf->cf_driver; cf++) {
! 		/*
! 		 * Skip cf if no longer eligible, otherwise scan through
! 		 * parents for one matching `parent', and try match function.
! 		 */
! 		if (cf->cf_fstate == FSTATE_FOUND)
! 			continue;
! 		for (p = cf->cf_parents; *p >= 0; p++)
! 			if (parent->dv_cfdata == &cfdata[*p]) {
! 				if (indirect)
! 					match = config_make_softc(parent, cf);
! 				else
! 					match = cf;
! 				(*fn)(parent, match);
! 			}
  	}
  }
--- 186,214 ----
  	void *match;
  	int indirect;
+ 	struct cftable *t;
+ 
+ 	if(!allcftables.tqh_last){ /* XXX */
+ 	  TAILQ_INIT(&allcftables);
+ 	  TAILQ_INSERT_TAIL(&allcftables, &staticcftable, list);
+ 	}
  
  	indirect = parent && parent->dv_cfdata->cf_driver->cd_indirect;
! 	for(t=allcftables.tqh_first; t; t=t->list.tqe_next){
! 	  for (cf = t->tab; cf->cf_driver; cf++) {
! 	    /*
! 	     * Skip cf if no longer eligible, otherwise scan through
! 	     * parents for one matching `parent', and try match function.
! 	     */
! 	    if (cf->cf_fstate == FSTATE_FOUND)
! 	      continue;
! 	    for (p = cf->cf_parents; *p >= 0; p++)
! 	      if (parent->dv_cfdata == &(t->tab)[*p]) {
! 		if (indirect)
! 		  match = config_make_softc(parent, cf);
! 		else
! 		  match = cf;
! 		(*fn)(parent, match);
! 	      }
! 	  }
  	}
  }
***************
*** 285,288 ****
--- 308,313 ----
  }
  
+ struct device_head alldevs;
+ 
  /*
   * Attach a found device.  Allocates memory for device variables.
***************
*** 298,302 ****
  	register struct device *dev;
  	register struct cfdriver *cd;
! 	static struct device **nextp = &alldevs;
  
  	if (parent && parent->dv_cfdata->cf_driver->cd_indirect) {
--- 323,334 ----
  	register struct device *dev;
  	register struct cfdriver *cd;
! 	struct cftable *t;
! 
! 	if(!alldevs.cqh_last)CIRCLEQ_INIT(&alldevs); /* XXX */
! 
! 	if(!allcftables.tqh_last){ /* XXX */
! 	  TAILQ_INIT(&allcftables);
! 	  TAILQ_INSERT_TAIL(&allcftables, &staticcftable, list);
! 	}
  
  	if (parent && parent->dv_cfdata->cf_driver->cd_indirect) {
***************
*** 316,321 ****
  		cf->cf_fstate = FSTATE_FOUND;
  
! 	*nextp = dev;			/* link up */
! 	nextp = &dev->dv_next;
  
  	if (parent == ROOT)
--- 348,352 ----
  		cf->cf_fstate = FSTATE_FOUND;
  
! 	CIRCLEQ_INSERT_TAIL(&alldevs, dev, dv_next); /* link up */
  
  	if (parent == ROOT)
***************
*** 331,338 ****
  	 * otherwise identical.
  	 */
! 	for (cf = cfdata; cf->cf_driver; cf++)
! 		if (cf->cf_driver == cd && cf->cf_unit == dev->dv_unit &&
! 		    cf->cf_fstate == FSTATE_NOTFOUND)
! 			cf->cf_fstate = FSTATE_FOUND;
  	(*cd->cd_attach)(parent, dev, aux);
  }
--- 362,371 ----
  	 * otherwise identical.
  	 */
! 	for(t=allcftables.tqh_first; t; t=t->list.tqe_next){
! 	  for (cf = t->tab; cf->cf_driver; cf++)
! 	    if (cf->cf_driver == cd && cf->cf_unit == dev->dv_unit &&
! 		cf->cf_fstate == FSTATE_NOTFOUND)
! 	      cf->cf_fstate = FSTATE_FOUND;
! 	}
  	(*cd->cd_attach)(parent, dev, aux);
  }

####################################################
####################### Part b - support functions:

#include <sys/param.h>
#include <sys/device.h>
#include <sys/malloc.h>
#include <sys/queue.h>

static int haschild(dev)
struct device *dev;
{
  struct device *d;

  for(d=alldevs.cqh_first; d != (void*)&alldevs; d=d->dv_next.cqe_next){
    if(d->dv_parent==dev)
      return(1);
  }
  return(0);
}

static int detach_devices(cond, condarg, callback)
int (*cond) __P((struct device*, void*));
void *condarg;
void (*callback) __P((struct device*));
{
  struct device *d;
  int alldone=1;

  d=alldevs.cqh_last; /* inverse order */
  while(d != (void*)&alldevs){
    if((*cond)(d, condarg)){
      struct cfdriver *drv=d->dv_cfdata->cf_driver;

      /* device not busy? */
      /* driver's detach routine decides,
       upper layer (eg bus dependent code) is notified via callback */
      if((!haschild(d))&&(drv->cd_detach)&&((*(drv->cd_detach))(d))){
	int needit, i;
	struct device *help;

	if(callback)(*callback)(d);

	/* remove reference in driver's devicelist */
	if((d->dv_unit>=drv->cd_ndevs)||(drv->cd_devs[d->dv_unit]!=d))
	  panic("bad unit in detach_devices");
	drv->cd_devs[d->dv_unit]=NULL;

	/* driver is not needed anymore? */
	needit=0;
	for(i=0;i<drv->cd_ndevs;i++)
	  if(drv->cd_devs[i])needit=1;

	if(!needit){
	  /* free devices array (alloc'd in config_make_softc) */
	  free(drv->cd_devs, M_DEVBUF);
	  drv->cd_ndevs=0;
	}

	/* remove entry in global device list */
	CIRCLEQ_REMOVE(&alldevs, d, dv_next);
	printf("%s removed\n", d->dv_xname);

	/* free memory for dev data (alloc'd in config_make_softc) */ 
	help=d->dv_next.cqe_prev;
	free(d, M_DEVBUF);
	d=help;
	continue;
      }else alldone=0;
    }
    d=d->dv_next.cqe_prev;
  }
  return(alldone);
}

#ifdef notyet
static int dev_matches_cfdata(dev, cfdata)
struct device *dev;
struct cfdata *cfdata;
{
  return(/* device uses same driver ? */
	 (dev->dv_cfdata->cf_driver==cfdata->cf_driver)
	 /* device instance described by this cfdata? */
	 &&((cfdata->cf_fstate==FSTATE_STAR)
	    ||((cfdata->cf_fstate==FSTATE_FOUND)
	       &&(dev->dv_unit==cfdata->cf_unit)))
	 );
}

int config_detach(dev, callback)
struct cfdata *dev;
void (*callback) __P((struct device*));
{
  return(detach_devices(dev_matches_cfdata, dev, callback));
}
#endif

int attach_loadable(parentname, parentunit, cftable)
char *parentname;
int parentunit;
struct cftable *cftable;
{
  int found=0;
  struct device *d;

  TAILQ_INSERT_TAIL(&allcftables, cftable, list);

  for(d=alldevs.cqh_first; d != (void*)&alldevs; d=d->dv_next.cqe_next){
    struct cfdriver *drv=d->dv_cfdata->cf_driver;

    if((!strcmp(parentname, drv->cd_name))
       &&((parentunit==-1)||(parentunit==d->dv_unit))){
      int s;

      s=splhigh(); /* ??? */
      found|=drv->cd_reprobe(d, &(cftable->tab[0]));
      splx(s);
    }
  }

  if(!found)TAILQ_REMOVE(&allcftables, cftable, list);

  return(found);
}

static int devcf_intable(dev, tbl)
struct device *dev;
struct cftable *tbl;
{
  struct cfdata *cf;

  for(cf=tbl->tab; cf->cf_driver; cf++){
    if(dev->dv_cfdata==cf)return(1);
  }
  return(0);
}

int detach_loadable(cftable)
struct cftable *cftable;
{
  if(!detach_devices(devcf_intable, cftable, 0))
    return(0);
  TAILQ_REMOVE(&allcftables, cftable, list);
  return(1);
}

####################################################
####################### Part c - example:

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/conf.h>
#include <sys/exec.h>
#include <sys/lkm.h>
#include <sys/device.h>
#include <sys/queue.h>

#ifdef DDB
#include <machine/db_machdep.h>
#include <ddb/db_sym.h>

static int testmod_symbols_loaded=0;
#endif

extern struct cfdriver pciviccd;
static int loc[]={-1, -1};
static short pv[]={-1};

static struct cfdata testmodcfdata[]={
  {&pciviccd, 0, FSTATE_STAR, loc, 0, pv+0, 0},
  {0}
};

static struct cftable testmodcftable={
  testmodcfdata
};

static int testmodinit(lkmtp,cmd)
struct lkm_table *lkmtp;
int cmd;	
{
  struct exec *e=(struct exec*)lkmtp->area;

  if(lkmexists(lkmtp)) return(EEXIST);

#ifdef DDB
  if((!N_BADMAG(*e))
     &&(lkmtp->size>(e->a_text+e->a_data+e->a_bss))){
    u_int ourend;
    extern u_int db_lastsym;

    ourend=lkmtp->area+e->a_text+e->a_data+e->a_bss;
    X_db_sym_init(ourend, lkmtp->area+lkmtp->size,
		  lkmtp->private.lkm_any->lkm_name);
    if(db_lastsym < ourend)
      db_lastsym=ourend;
    testmod_symbols_loaded=1;
#if(DDB > 1)
    Debugger();
#endif
  }
#endif /* DDB */

  if(!attach_loadable("pci", -1, &testmodcftable)){
    printf("no device found\n");
#ifdef DDB
    if(testmod_symbols_loaded)
      db_del_symbol_table(lkmtp->private.lkm_any->lkm_name);
#endif
    return(ENXIO);
  }
  return(0);
}

static int testmodcleanup(lkmtp,cmd)
struct lkm_table *lkmtp;
int cmd;	
{
  if(!detach_loadable(&testmodcftable)){
    printf("pcivic devices still in use\n");
    return(EBUSY);
  }
  if(pciviccd.cd_ndevs){
    printf("pcivic driver still in use\n");
    return(EBUSY);
  }
#ifdef DDB
  if(testmod_symbols_loaded)
    db_del_symbol_table(lkmtp->private.lkm_any->lkm_name);
#endif
  return(0);
}

cdev_decl(pcivic);

static struct cdevsw devsw={
  pcivicopen, pcivicclose,
  pcivicread, pcivicwrite,
  pcivicioctl,
  (dev_type_stop((*)))enodev,0,(dev_type_select((*)))enodev,
  pcivicmmap,
  0
};

MOD_DEV("testmod", LM_DT_CHAR, -1, &devsw)

int testmod(lkmtp, cmd, ver)
struct lkm_table *lkmtp;	
int cmd;
int ver;
{
  DISPATCH(lkmtp,cmd,ver,testmodinit,testmodcleanup,lkm_nofunc);
}

####################################################
example for a more complex case:
(it works with a modified PCI framework - cf cgd's alpha port)

extern struct cfdriver ppbcd; /* in this loadable module */
extern struct cfdriver pcicd; /* already in kernel */
static int loc[]={-1, -1};
static short pv[]={0, -1};

static struct cfdata testmodcfdata[]={
  {&ppbcd, 0, FSTATE_STAR, loc, 0, pv+1, 0},
  {&pcicd, 1, FSTATE_NOTFOUND, loc, 0, pv+0, 0},
  {0}
};
 ...........
 ...........
static int testmodcleanup(lkmtp,cmd)
struct lkm_table *lkmtp;
int cmd;	
{
  if(!detach_loadable(&testmodcftable)){
    printf("ppb/pcibus device still in use\n");
    return(EBUSY);
  }
  if(testmodcfdata[0].cf_driver->cd_ndevs){
    printf("driver still in use\n");
    return(EBUSY);
  }
 ...........
 ...........
>Audit-Trail:
>Unformatted:
To: gnats-bugs@gnats.netbsd.org
Subject: configuration support for loadable drivers
From: drochner
Reply-To: drochner@zelux6.zel.kfa-juelich.de