Skip site navigation (1)Skip section navigation (2)
Date:      Fri, 10 Aug 2012 15:21:13 +0000 (UTC)
From:      Hans Petter Selasky <hselasky@FreeBSD.org>
To:        src-committers@freebsd.org, svn-src-all@freebsd.org, svn-src-head@freebsd.org
Subject:   svn commit: r239179 - in head/sys: dev/usb/serial sys
Message-ID:  <201208101521.q7AFLD3M047835@svn.freebsd.org>

next in thread | raw e-mail | index | archive | help
Author: hselasky
Date: Fri Aug 10 15:21:12 2012
New Revision: 239179
URL: http://svn.freebsd.org/changeset/base/239179

Log:
  Switch unit management in UCOM to unrhdr.
  
  Extend the callback table of UCOM to include a
  "ucom_free" function pointer which is called when
  all refs on a UCOM super structure is gone.
  
  Implement various helper functions to handle
  refcounting and draining on the UCOM super
  structure.
  
  Implement macro which can be used in device
  drivers to avoid module unload before all
  pending TTY references are gone.
  
  The UCOM API is backwards compatible after this
  change and device drivers require no changes
  to function with this change. Only a recompilation
  of UCOM device drivers is required. The FreeBSD
  version has been bumped in that regard.
  
  Discussed with:	kib, ed
  MFC after:	2 weeks

Modified:
  head/sys/dev/usb/serial/usb_serial.c
  head/sys/dev/usb/serial/usb_serial.h
  head/sys/sys/param.h

Modified: head/sys/dev/usb/serial/usb_serial.c
==============================================================================
--- head/sys/dev/usb/serial/usb_serial.c	Fri Aug 10 15:02:49 2012	(r239178)
+++ head/sys/dev/usb/serial/usb_serial.c	Fri Aug 10 15:21:12 2012	(r239179)
@@ -146,7 +146,7 @@ static usb_proc_callback_t ucom_cfg_para
 static int	ucom_unit_alloc(void);
 static void	ucom_unit_free(int);
 static int	ucom_attach_tty(struct ucom_super_softc *, struct ucom_softc *);
-static void	ucom_detach_tty(struct ucom_softc *);
+static void	ucom_detach_tty(struct ucom_super_softc *, struct ucom_softc *);
 static void	ucom_queue_command(struct ucom_softc *,
 		    usb_proc_callback_t *, struct termios *pt,
 		    struct usb_proc_msg *t0, struct usb_proc_msg *t1);
@@ -178,13 +178,37 @@ static struct ttydevsw ucom_class = {
 MODULE_DEPEND(ucom, usb, 1, 1, 1);
 MODULE_VERSION(ucom, 1);
 
-#define	UCOM_UNIT_MAX 		128	/* limits size of ucom_bitmap */
+#define	UCOM_UNIT_MAX 		128	/* maximum number of units */
+#define	UCOM_TTY_PREFIX		"U"
 
-static uint8_t ucom_bitmap[(UCOM_UNIT_MAX + 7) / 8];
-static struct mtx ucom_bitmap_mtx;
-MTX_SYSINIT(ucom_bitmap_mtx, &ucom_bitmap_mtx, "ucom bitmap", MTX_DEF);
+static struct unrhdr *ucom_unrhdr;
+static struct mtx ucom_mtx;
+static int ucom_close_refs;
 
-#define UCOM_TTY_PREFIX		"U"
+static void
+ucom_init(void *arg)
+{
+	DPRINTF("\n");
+	ucom_unrhdr = new_unrhdr(0, UCOM_UNIT_MAX - 1, NULL);
+	mtx_init(&ucom_mtx, "UCOM MTX", NULL, MTX_DEF);
+}
+SYSINIT(ucom_init, SI_SUB_KLD - 1, SI_ORDER_ANY, ucom_init, NULL);
+
+static void
+ucom_uninit(void *arg)
+{
+	struct unrhdr *hdr;
+	hdr = ucom_unrhdr;
+	ucom_unrhdr = NULL;
+
+	DPRINTF("\n");
+
+	if (hdr != NULL)
+		delete_unrhdr(hdr);
+
+	mtx_destroy(&ucom_mtx);
+}
+SYSUNINIT(ucom_uninit, SI_SUB_KLD - 2, SI_ORDER_ANY, ucom_uninit, NULL);
 
 /*
  * Mark a unit number (the X in cuaUX) as in use.
@@ -197,21 +221,14 @@ ucom_unit_alloc(void)
 {
 	int unit;
 
-	mtx_lock(&ucom_bitmap_mtx);
-
-	for (unit = 0; unit < UCOM_UNIT_MAX; unit++) {
-		if ((ucom_bitmap[unit / 8] & (1 << (unit % 8))) == 0) {
-			ucom_bitmap[unit / 8] |= (1 << (unit % 8));
-			break;
-		}
+	/* sanity checks */
+	if (ucom_unrhdr == NULL) {
+		DPRINTF("ucom_unrhdr is NULL\n");
+		return (-1);
 	}
-
-	mtx_unlock(&ucom_bitmap_mtx);
-
-	if (unit == UCOM_UNIT_MAX)
-		return -1;
-	else
-		return unit;
+	unit = alloc_unr(ucom_unrhdr);
+	DPRINTF("unit %d is allocated\n", unit);
+	return (unit);
 }
 
 /*
@@ -220,11 +237,13 @@ ucom_unit_alloc(void)
 static void
 ucom_unit_free(int unit)
 {
-	mtx_lock(&ucom_bitmap_mtx);
-
-	ucom_bitmap[unit / 8] &= ~(1 << (unit % 8));
-
-	mtx_unlock(&ucom_bitmap_mtx);
+	/* sanity checks */
+	if (unit < 0 || unit >= UCOM_UNIT_MAX || ucom_unrhdr == NULL) {
+		DPRINTF("cannot free unit number\n");
+		return;
+	}
+	DPRINTF("unit %d is freed\n", unit);
+	free_unr(ucom_unrhdr, unit);
 }
 
 /*
@@ -244,7 +263,8 @@ ucom_attach(struct ucom_super_softc *ssc
 
 	if ((sc == NULL) ||
 	    (subunits <= 0) ||
-	    (callback == NULL)) {
+	    (callback == NULL) ||
+	    (mtx == NULL)) {
 		return (EINVAL);
 	}
 
@@ -265,6 +285,11 @@ ucom_attach(struct ucom_super_softc *ssc
 	}
 	ssc->sc_subunits = subunits;
 
+	if (callback->ucom_free == NULL) {
+		ssc->sc_wait_refs = 1;
+		ucom_ref(ssc);
+	}
+
 	for (subunit = 0; subunit < ssc->sc_subunits; subunit++) {
 		sc[subunit].sc_subunit = subunit;
 		sc[subunit].sc_super = ssc;
@@ -277,6 +302,10 @@ ucom_attach(struct ucom_super_softc *ssc
 			ucom_detach(ssc, &sc[0]);
 			return (error);
 		}
+		/* increment reference count */
+		ucom_ref(ssc);
+
+		/* set subunit attached */
 		sc[subunit].sc_flag |= UCOM_FLAG_ATTACHED;
 	}
 
@@ -313,14 +342,41 @@ ucom_detach(struct ucom_super_softc *ssc
 	for (subunit = 0; subunit < ssc->sc_subunits; subunit++) {
 		if (sc[subunit].sc_flag & UCOM_FLAG_ATTACHED) {
 
-			ucom_detach_tty(&sc[subunit]);
+			ucom_detach_tty(ssc, &sc[subunit]);
 
 			/* avoid duplicate detach */
 			sc[subunit].sc_flag &= ~UCOM_FLAG_ATTACHED;
 		}
 	}
-	ucom_unit_free(ssc->sc_unit);
 	usb_proc_free(&ssc->sc_tq);
+
+	if (ssc->sc_wait_refs != 0) {
+		ucom_unref(ssc);
+		ucom_drain(ssc);
+	}
+}
+
+void
+ucom_drain(struct ucom_super_softc *ssc)
+{
+	mtx_lock(&ucom_mtx);
+	while (ssc->sc_refs >= 2) {
+		printf("ucom: Waiting for a TTY device to close.\n");
+		usb_pause_mtx(&ucom_mtx, hz);
+	}
+	mtx_unlock(&ucom_mtx);
+}
+
+void
+ucom_drain_all(void *arg)
+{
+	mtx_lock(&ucom_mtx);
+	while (ucom_close_refs > 0) {
+		printf("ucom: Waiting for all detached TTY "
+		    "devices to have open fds closed.\n");
+		usb_pause_mtx(&ucom_mtx, hz);
+	}
+	mtx_unlock(&ucom_mtx);
 }
 
 static int
@@ -356,7 +412,6 @@ ucom_attach_tty(struct ucom_super_softc 
 	sc->sc_tty = tp;
 
 	DPRINTF("ttycreate: %s\n", buf);
-	cv_init(&sc->sc_cv, "ucom");
 
 	/* Check if this device should be a console */
 	if ((ucom_cons_softc == NULL) && 
@@ -373,7 +428,7 @@ ucom_attach_tty(struct ucom_super_softc 
 		t.c_ospeed = t.c_ispeed;
 		t.c_cflag = CS8;
 
-		mtx_lock(ucom_cons_softc->sc_mtx);
+		UCOM_MTX_LOCK(ucom_cons_softc);
 		ucom_cons_rx_low = 0;
 		ucom_cons_rx_high = 0;
 		ucom_cons_tx_low = 0;
@@ -381,56 +436,55 @@ ucom_attach_tty(struct ucom_super_softc 
 		sc->sc_flag |= UCOM_FLAG_CONSOLE;
 		ucom_open(ucom_cons_softc->sc_tty);
 		ucom_param(ucom_cons_softc->sc_tty, &t);
-		mtx_unlock(ucom_cons_softc->sc_mtx);
+		UCOM_MTX_UNLOCK(ucom_cons_softc);
 	}
 
 	return (0);
 }
 
 static void
-ucom_detach_tty(struct ucom_softc *sc)
+ucom_detach_tty(struct ucom_super_softc *ssc, struct ucom_softc *sc)
 {
 	struct tty *tp = sc->sc_tty;
 
 	DPRINTF("sc = %p, tp = %p\n", sc, sc->sc_tty);
 
 	if (sc->sc_flag & UCOM_FLAG_CONSOLE) {
-		mtx_lock(ucom_cons_softc->sc_mtx);
+		UCOM_MTX_LOCK(ucom_cons_softc);
 		ucom_close(ucom_cons_softc->sc_tty);
 		sc->sc_flag &= ~UCOM_FLAG_CONSOLE;
-		mtx_unlock(ucom_cons_softc->sc_mtx);
+		UCOM_MTX_UNLOCK(ucom_cons_softc);
 		ucom_cons_softc = NULL;
 	}
 
 	/* the config thread has been stopped when we get here */
 
-	mtx_lock(sc->sc_mtx);
+	UCOM_MTX_LOCK(sc);
 	sc->sc_flag |= UCOM_FLAG_GONE;
 	sc->sc_flag &= ~(UCOM_FLAG_HL_READY | UCOM_FLAG_LL_READY);
-	mtx_unlock(sc->sc_mtx);
+	UCOM_MTX_UNLOCK(sc);
+
 	if (tp) {
+		mtx_lock(&ucom_mtx);
+		ucom_close_refs++;
+		mtx_unlock(&ucom_mtx);
+
 		tty_lock(tp);
 
 		ucom_close(tp);	/* close, if any */
 
 		tty_rel_gone(tp);
 
-		mtx_lock(sc->sc_mtx);
-		/* Wait for the callback after the TTY is torn down */
-		while (sc->sc_ttyfreed == 0)
-			cv_wait(&sc->sc_cv, sc->sc_mtx);
+		UCOM_MTX_LOCK(sc);
 		/*
 		 * make sure that read and write transfers are stopped
 		 */
-		if (sc->sc_callback->ucom_stop_read) {
+		if (sc->sc_callback->ucom_stop_read)
 			(sc->sc_callback->ucom_stop_read) (sc);
-		}
-		if (sc->sc_callback->ucom_stop_write) {
+		if (sc->sc_callback->ucom_stop_write)
 			(sc->sc_callback->ucom_stop_write) (sc);
-		}
-		mtx_unlock(sc->sc_mtx);
+		UCOM_MTX_UNLOCK(sc);
 	}
-	cv_destroy(&sc->sc_cv);
 }
 
 void
@@ -476,7 +530,7 @@ ucom_queue_command(struct ucom_softc *sc
 	struct ucom_super_softc *ssc = sc->sc_super;
 	struct ucom_param_task *task;
 
-	mtx_assert(sc->sc_mtx, MA_OWNED);
+	UCOM_MTX_ASSERT(sc, MA_OWNED);
 
 	if (usb_proc_is_gone(&ssc->sc_tq)) {
 		DPRINTF("proc is gone\n");
@@ -520,7 +574,7 @@ ucom_shutdown(struct ucom_softc *sc)
 {
 	struct tty *tp = sc->sc_tty;
 
-	mtx_assert(sc->sc_mtx, MA_OWNED);
+	UCOM_MTX_ASSERT(sc, MA_OWNED);
 
 	DPRINTF("\n");
 
@@ -621,7 +675,7 @@ ucom_open(struct tty *tp)
 	struct ucom_softc *sc = tty_softc(tp);
 	int error;
 
-	mtx_assert(sc->sc_mtx, MA_OWNED);
+	UCOM_MTX_ASSERT(sc, MA_OWNED);
 
 	if (sc->sc_flag & UCOM_FLAG_GONE) {
 		return (ENXIO);
@@ -699,7 +753,7 @@ ucom_close(struct tty *tp)
 {
 	struct ucom_softc *sc = tty_softc(tp);
 
-	mtx_assert(sc->sc_mtx, MA_OWNED);
+	UCOM_MTX_ASSERT(sc, MA_OWNED);
 
 	DPRINTF("tp=%p\n", tp);
 
@@ -726,7 +780,7 @@ ucom_ioctl(struct tty *tp, u_long cmd, c
 	struct ucom_softc *sc = tty_softc(tp);
 	int error;
 
-	mtx_assert(sc->sc_mtx, MA_OWNED);
+	UCOM_MTX_ASSERT(sc, MA_OWNED);
 
 	if (!(sc->sc_flag & UCOM_FLAG_HL_READY)) {
 		return (EIO);
@@ -770,7 +824,7 @@ ucom_modem(struct tty *tp, int sigon, in
 	struct ucom_softc *sc = tty_softc(tp);
 	uint8_t onoff;
 
-	mtx_assert(sc->sc_mtx, MA_OWNED);
+	UCOM_MTX_ASSERT(sc, MA_OWNED);
 
 	if (!(sc->sc_flag & UCOM_FLAG_HL_READY)) {
 		return (0);
@@ -889,7 +943,7 @@ static void
 ucom_line_state(struct ucom_softc *sc,
     uint8_t set_bits, uint8_t clear_bits)
 {
-	mtx_assert(sc->sc_mtx, MA_OWNED);
+	UCOM_MTX_ASSERT(sc, MA_OWNED);
 
 	if (!(sc->sc_flag & UCOM_FLAG_HL_READY)) {
 		return;
@@ -967,7 +1021,7 @@ ucom_cfg_status_change(struct usb_proc_m
 
 	tp = sc->sc_tty;
 
-	mtx_assert(sc->sc_mtx, MA_OWNED);
+	UCOM_MTX_ASSERT(sc, MA_OWNED);
 
 	if (!(sc->sc_flag & UCOM_FLAG_LL_READY)) {
 		return;
@@ -1029,7 +1083,7 @@ ucom_cfg_status_change(struct usb_proc_m
 void
 ucom_status_change(struct ucom_softc *sc)
 {
-	mtx_assert(sc->sc_mtx, MA_OWNED);
+	UCOM_MTX_ASSERT(sc, MA_OWNED);
 
 	if (sc->sc_flag & UCOM_FLAG_CONSOLE)
 		return;		/* not supported */
@@ -1071,7 +1125,7 @@ ucom_param(struct tty *tp, struct termio
 	uint8_t opened;
 	int error;
 
-	mtx_assert(sc->sc_mtx, MA_OWNED);
+	UCOM_MTX_ASSERT(sc, MA_OWNED);
 
 	opened = 0;
 	error = 0;
@@ -1138,7 +1192,7 @@ ucom_outwakeup(struct tty *tp)
 {
 	struct ucom_softc *sc = tty_softc(tp);
 
-	mtx_assert(sc->sc_mtx, MA_OWNED);
+	UCOM_MTX_ASSERT(sc, MA_OWNED);
 
 	DPRINTF("sc = %p\n", sc);
 
@@ -1165,7 +1219,7 @@ ucom_get_data(struct ucom_softc *sc, str
 	uint32_t cnt;
 	uint32_t offset_orig;
 
-	mtx_assert(sc->sc_mtx, MA_OWNED);
+	UCOM_MTX_ASSERT(sc, MA_OWNED);
 
 	if (sc->sc_flag & UCOM_FLAG_CONSOLE) {
 		unsigned int temp;
@@ -1244,7 +1298,7 @@ ucom_put_data(struct ucom_softc *sc, str
 	char *buf;
 	uint32_t cnt;
 
-	mtx_assert(sc->sc_mtx, MA_OWNED);
+	UCOM_MTX_ASSERT(sc, MA_OWNED);
 
 	if (sc->sc_flag & UCOM_FLAG_CONSOLE) {
 		unsigned int temp;
@@ -1325,10 +1379,14 @@ ucom_free(void *xsc)
 {
 	struct ucom_softc *sc = xsc;
 
-	mtx_lock(sc->sc_mtx);
-	sc->sc_ttyfreed = 1;
-	cv_signal(&sc->sc_cv);
-	mtx_unlock(sc->sc_mtx);
+	if (sc->sc_callback->ucom_free != NULL)
+		sc->sc_callback->ucom_free(sc);
+	else
+		ucom_unref(sc->sc_super);
+
+	mtx_lock(&ucom_mtx);
+	ucom_close_refs--;
+	mtx_unlock(&ucom_mtx);
 }
 
 static cn_probe_t ucom_cnprobe;
@@ -1381,7 +1439,7 @@ ucom_cngetc(struct consdev *cd)
 	if (sc == NULL)
 		return (-1);
 
-	mtx_lock(sc->sc_mtx);
+	UCOM_MTX_LOCK(sc);
 
 	if (ucom_cons_rx_low != ucom_cons_rx_high) {
 		c = ucom_cons_rx_buf[ucom_cons_rx_low];
@@ -1394,7 +1452,7 @@ ucom_cngetc(struct consdev *cd)
 	/* start USB transfers */
 	ucom_outwakeup(sc->sc_tty);
 
-	mtx_unlock(sc->sc_mtx);
+	UCOM_MTX_UNLOCK(sc);
 
 	/* poll if necessary */
 	if (kdb_active && sc->sc_callback->ucom_poll)
@@ -1414,7 +1472,7 @@ ucom_cnputc(struct consdev *cd, int c)
 
  repeat:
 
-	mtx_lock(sc->sc_mtx);
+	UCOM_MTX_LOCK(sc);
 
 	/* compute maximum TX length */
 
@@ -1430,7 +1488,7 @@ ucom_cnputc(struct consdev *cd, int c)
 	/* start USB transfers */
 	ucom_outwakeup(sc->sc_tty);
 
-	mtx_unlock(sc->sc_mtx);
+	UCOM_MTX_UNLOCK(sc);
 
 	/* poll if necessary */
 	if (kdb_active && sc->sc_callback->ucom_poll) {
@@ -1441,6 +1499,50 @@ ucom_cnputc(struct consdev *cd, int c)
 	}
 }
 
+/*------------------------------------------------------------------------*
+ *	ucom_ref
+ *
+ * This function will increment the super UCOM reference count.
+ *------------------------------------------------------------------------*/
+void
+ucom_ref(struct ucom_super_softc *ssc)
+{
+	mtx_lock(&ucom_mtx);
+	ssc->sc_refs++;
+	mtx_unlock(&ucom_mtx);
+}
+
+/*------------------------------------------------------------------------*
+ *	ucom_unref
+ *
+ * This function will decrement the super UCOM reference count.
+ *
+ * Return values:
+ * 0: UCOM structures are still referenced.
+ * Else: UCOM structures are no longer referenced.
+ *------------------------------------------------------------------------*/
+int
+ucom_unref(struct ucom_super_softc *ssc)
+{
+	int retval;
+	int free_unit;
+
+	mtx_lock(&ucom_mtx);
+	retval = (ssc->sc_refs < 2);
+	free_unit = (ssc->sc_refs == 1);
+	ssc->sc_refs--;
+	mtx_unlock(&ucom_mtx);
+
+	/*
+	 * This function might be called when the "ssc" is only zero
+	 * initialized and in that case the unit number should not be
+	 * freed.
+	 */
+	if (free_unit)
+		ucom_unit_free(ssc->sc_unit);
+	return (retval);
+}
+
 #if defined(GDB)
 
 #include <gdb/gdb.h>

Modified: head/sys/dev/usb/serial/usb_serial.h
==============================================================================
--- head/sys/dev/usb/serial/usb_serial.h	Fri Aug 10 15:02:49 2012	(r239178)
+++ head/sys/dev/usb/serial/usb_serial.h	Fri Aug 10 15:21:12 2012	(r239179)
@@ -107,6 +107,7 @@ struct ucom_callback {
 	void    (*ucom_stop_write) (struct ucom_softc *);
 	void    (*ucom_tty_name) (struct ucom_softc *, char *pbuf, uint16_t buflen, uint16_t unit, uint16_t subunit);
 	void    (*ucom_poll) (struct ucom_softc *);
+	void	(*ucom_free) (struct ucom_softc *);
 };
 
 /* Line status register */
@@ -135,6 +136,8 @@ struct ucom_super_softc {
 	struct usb_process sc_tq;
 	int sc_unit;
 	int sc_subunits;
+	int sc_refs;
+	int sc_wait_refs;
 	struct sysctl_oid *sc_sysctl_ttyname;
 	struct sysctl_oid *sc_sysctl_ttyports;
 	char sc_ttyname[16];
@@ -158,7 +161,6 @@ struct ucom_softc {
 	struct ucom_cfg_task	sc_line_state_task[2];
 	struct ucom_cfg_task	sc_status_task[2];
 	struct ucom_param_task	sc_param_task[2];
-	struct cv sc_cv;
 	/* Used to set "UCOM_FLAG_GP_DATA" flag: */
 	struct usb_proc_msg	*sc_last_start_xfer;
 	const struct ucom_callback *sc_callback;
@@ -179,7 +181,6 @@ struct ucom_softc {
 	uint8_t	sc_lsr;
 	uint8_t	sc_msr;
 	uint8_t	sc_mcr;
-	uint8_t	sc_ttyfreed;		/* set when TTY has been freed */
 	/* programmed line state bits */
 	uint8_t sc_pls_set;		/* set bits */
 	uint8_t sc_pls_clr;		/* cleared bits */
@@ -190,6 +191,12 @@ struct ucom_softc {
 #define	UCOM_LS_RING	0x08
 };
 
+#define	UCOM_MTX_ASSERT(sc, what) mtx_assert((sc)->sc_mtx, what)
+#define	UCOM_MTX_LOCK(sc) mtx_lock((sc)->sc_mtx)
+#define	UCOM_MTX_UNLOCK(sc) mtx_unlock((sc)->sc_mtx)
+#define	UCOM_UNLOAD_DRAIN(x) \
+SYSUNINIT(var, SI_SUB_KLD - 3, SI_ORDER_ANY, ucom_drain_all, 0)
+
 #define	ucom_cfg_do_request(udev,com,req,ptr,flags,timo) \
     usbd_do_request_proc(udev,&(com)->sc_super->sc_tq,req,ptr,flags,NULL,timo)
 
@@ -204,4 +211,8 @@ uint8_t	ucom_get_data(struct ucom_softc 
 void	ucom_put_data(struct ucom_softc *, struct usb_page_cache *,
 	    uint32_t, uint32_t);
 uint8_t	ucom_cfg_is_gone(struct ucom_softc *);
+void	ucom_drain(struct ucom_super_softc *);
+void	ucom_drain_all(void *);
+void	ucom_ref(struct ucom_super_softc *);
+int	ucom_unref(struct ucom_super_softc *);
 #endif					/* _USB_SERIAL_H_ */

Modified: head/sys/sys/param.h
==============================================================================
--- head/sys/sys/param.h	Fri Aug 10 15:02:49 2012	(r239178)
+++ head/sys/sys/param.h	Fri Aug 10 15:21:12 2012	(r239179)
@@ -58,7 +58,7 @@
  *		in the range 5 to 9.
  */
 #undef __FreeBSD_version
-#define __FreeBSD_version 1000015	/* Master, propagated to newvers */
+#define __FreeBSD_version 1000016	/* Master, propagated to newvers */
 
 /*
  * __FreeBSD_kernel__ indicates that this system uses the kernel of FreeBSD,



Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?201208101521.q7AFLD3M047835>