Skip site navigation (1)Skip section navigation (2)
Date:      Thu, 12 Jan 2017 00:48:07 +0000 (UTC)
From:      Ian Lepore <ian@FreeBSD.org>
To:        src-committers@freebsd.org, svn-src-all@freebsd.org, svn-src-head@freebsd.org
Subject:   svn commit: r311954 - in head: lib/libc/gen share/man/man4 sys/kern sys/sys
Message-ID:  <201701120048.v0C0m7I2061346@repo.freebsd.org>

next in thread | raw e-mail | index | archive | help
Author: ian
Date: Thu Jan 12 00:48:06 2017
New Revision: 311954
URL: https://svnweb.freebsd.org/changeset/base/311954

Log:
  Rework tty_drain() to poll the hardware for completion, and restore
  drain timeout handling to historical freebsd behavior.
  
  The primary reason for these changes is the need to have tty_drain() call
  ttydevsw_busy() at some reasonable sub-second rate, to poll hardware that
  doesn't signal an interrupt when the transmit shift register becomes empty
  (which includes virtually all USB serial hardware).  Such hardware hangs
  in a ttyout wait, because it never gets an opportunity to trigger a wakeup
  from the sleep in tty_drain() by calling ttydisc_getc() again, after
  handing the last of the buffered data to the hardware.
  
  While researching the history of changes to tty_drain() I stumbled across
  some email describing the historical BSD behavior of tcdrain() and close()
  on serial ports, and the ability of comcontrol(1) to control timeout
  behavior.  Using that and some advice from Bruce Evans as a guide, I've
  put together these changes to implement the hardware polling and restore
  the historical timeout behaviors...
  
   - tty_drain() now calls ttydevsw_busy() in a loop at 10 Hz to accomodate
     hardware that requires polling for busy state.
  
   - The "new historical" behavior for draining during close(2) is retained:
     the drain timeout is "1 second without making any progress".  When the
     1-second timeout expires, if the count of bytes remaining in the tty
     layer buffer is smaller than last time, the timeout is extended for
     another second.  Unfortunately, the same logic cannot be extended all
     the way down to the hardware, because the interface to that layer is a
     simple busy/not-busy indication.
  
   - Due to the previous point, an application that needs a guarantee that
     all data has been transmitted must use TIOCDRAIN/tcdrain(3) before
     calling close(2).
  
   - The historical behavior of honoring the drainwait setting for TIOCDRAIN
     (used by tcdrain(3)) is restored.
  
   - The historical kern.drainwait sysctl to control the global default
     drainwait time is restored, but is now named kern.tty_drainwait.
  
   - The historical default drainwait timeout of 300 seconds is restored.
  
   - Handling of TIOCGDRAINWAIT and TIOCSDRAINWAIT ioctls is restored
     (this also makes the comcontrol(1) drainwait verb work again).
  
   - Manpages are updated to document these behaviors.
  
  Reviewed by:	bde (prior version)

Modified:
  head/lib/libc/gen/tcsendbreak.3
  head/share/man/man4/tty.4
  head/sys/kern/tty.c
  head/sys/sys/tty.h

Modified: head/lib/libc/gen/tcsendbreak.3
==============================================================================
--- head/lib/libc/gen/tcsendbreak.3	Thu Jan 12 00:34:37 2017	(r311953)
+++ head/lib/libc/gen/tcsendbreak.3	Thu Jan 12 00:48:06 2017	(r311954)
@@ -28,7 +28,7 @@
 .\"	@(#)tcsendbreak.3	8.1 (Berkeley) 6/4/93
 .\" $FreeBSD$
 .\"
-.Dd June 4, 1993
+.Dd January 11, 2017
 .Dt TCSENDBREAK 3
 .Os
 .Sh NAME
@@ -137,17 +137,44 @@ is not a terminal.
 A signal interrupted the
 .Fn tcdrain
 function.
+.It Bq Er EWOULDBLOCK
+The configured timeout expired before the
+.Fn tcdrain
+function could write all buffered output.
 .El
 .Sh SEE ALSO
 .Xr tcsetattr 3 ,
-.Xr termios 4
+.Xr termios 4 ,
+.Xr tty 4 ,
+.Xr comcontrol 8
 .Sh STANDARDS
 The
 .Fn tcsendbreak ,
-.Fn tcdrain ,
 .Fn tcflush
 and
 .Fn tcflow
 functions are expected to be compliant with the
 .St -p1003.1-88
 specification.
+.Pp
+The
+.Fn tcdrain
+function is expected to be compliant with
+.St -p1003.1-88
+when the drain wait value is set to zero with
+.Xr comcontrol 8 ,
+or with
+.Xr ioctl 2
+.Va TIOCSDRAINWAIT ,
+or with
+.Xr sysctl 8
+.Va kern.tty_drainwait .
+A non-zero drain wait value can result in
+.Fn tcdrain
+returning
+.Va EWOULDBLOCK
+without writing all output.
+The default value for
+.Va kern.tty_drainwait
+is 300 seconds.
+

Modified: head/share/man/man4/tty.4
==============================================================================
--- head/share/man/man4/tty.4	Thu Jan 12 00:34:37 2017	(r311953)
+++ head/share/man/man4/tty.4	Thu Jan 12 00:48:06 2017	(r311954)
@@ -28,7 +28,7 @@
 .\"     @(#)tty.4	8.3 (Berkeley) 4/19/94
 .\" $FreeBSD$
 .\"
-.Dd December 26, 2009
+.Dd January 11, 2017
 .Dt TTY 4
 .Os
 .Sh NAME
@@ -238,7 +238,16 @@ Start output on the terminal (like typin
 Make the terminal the controlling terminal for the process (the process
 must not currently have a controlling terminal).
 .It Dv TIOCDRAIN Fa void
-Wait until all output is drained.
+Wait until all output is drained, or until the drain wait timeout expires.
+.It Dv TIOCGDRAINWAIT Fa int *timeout
+Return the current drain wait timeout in seconds.
+.It Dv TIOCSDRAINWAIT Fa int *timeout
+Set the drain wait timeout in seconds.
+A value of zero disables timeouts.
+The default drain wait timeout is controlled by the tunable
+.Xr sysctl 8
+OID
+.Va kern.tty_drainwait .
 .It Dv TIOCEXCL Fa void
 Set exclusive use on the terminal.
 No further opens are permitted except by root.

Modified: head/sys/kern/tty.c
==============================================================================
--- head/sys/kern/tty.c	Thu Jan 12 00:34:37 2017	(r311953)
+++ head/sys/kern/tty.c	Thu Jan 12 00:48:06 2017	(r311954)
@@ -95,6 +95,10 @@ static const char	*dev_console_filename;
 
 #define	TTY_CALLOUT(tp,d) (dev2unit(d) & TTYUNIT_CALLOUT)
 
+static int  tty_drainwait = 5 * 60;
+SYSCTL_INT(_kern, OID_AUTO, tty_drainwait, CTLFLAG_RWTUN,
+    &tty_drainwait, 0, "Default output drain timeout in seconds");
+
 /*
  * Set TTY buffer sizes.
  */
@@ -125,34 +129,56 @@ tty_watermarks(struct tty *tp)
 static int
 tty_drain(struct tty *tp, int leaving)
 {
-	size_t bytesused;
+	sbintime_t timeout_at;
+	size_t bytes;
 	int error;
 
 	if (ttyhook_hashook(tp, getc_inject))
 		/* buffer is inaccessible */
 		return (0);
 
-	while (ttyoutq_bytesused(&tp->t_outq) > 0 || ttydevsw_busy(tp)) {
-		ttydevsw_outwakeup(tp);
-		/* Could be handled synchronously. */
-		bytesused = ttyoutq_bytesused(&tp->t_outq);
-		if (bytesused == 0 && !ttydevsw_busy(tp))
-			return (0);
-
-		/* Wait for data to be drained. */
-		if (leaving) {
-			error = tty_timedwait(tp, &tp->t_outwait, hz);
-			if (error == EWOULDBLOCK &&
-			    ttyoutq_bytesused(&tp->t_outq) < bytesused)
-				error = 0;
-		} else
-			error = tty_wait(tp, &tp->t_outwait);
+	/*
+	 * For close(), use the recent historic timeout of "1 second without
+	 * making progress".  For tcdrain(), use t_drainwait as the timeout,
+	 * with zero meaning "no timeout" which gives POSIX behavior.
+	 */
+	if (leaving)
+		timeout_at = getsbinuptime() + SBT_1S;
+	else if (tp->t_drainwait != 0)
+		timeout_at = getsbinuptime() + SBT_1S * tp->t_drainwait;
+	else
+		timeout_at = 0;
 
-		if (error)
+	/*
+	 * Poll the output buffer and the hardware for completion, at 10 Hz.
+	 * Polling is required for devices which are not able to signal an
+	 * interrupt when the transmitter becomes idle (most USB serial devs).
+	 * The unusual structure of this loop ensures we check for busy one more
+	 * time after tty_timedwait() returns EWOULDBLOCK, so that success has
+	 * higher priority than timeout if the IO completed in the last 100mS.
+	 */
+	error = 0;
+	bytes = ttyoutq_bytesused(&tp->t_outq);
+	for (;;) {
+		if (ttyoutq_bytesused(&tp->t_outq) == 0 && !ttydevsw_busy(tp))
+			return (0);
+		if (error != 0)
 			return (error);
+		ttydevsw_outwakeup(tp);
+		error = tty_timedwait(tp, &tp->t_outwait, hz / 10);
+		if (timeout_at == 0 && error == EWOULDBLOCK)
+			error = 0;
+		if (error != EWOULDBLOCK)
+			continue;
+		if (getsbinuptime() < timeout_at)
+			error = 0;
+		else if (leaving && ttyoutq_bytesused(&tp->t_outq) < bytes) {
+			/* In close, making progress, grant an extra second. */
+			error = 0;
+			timeout_at += SBT_1S;
+			bytes = ttyoutq_bytesused(&tp->t_outq);
+		}
 	}
-
-	return (0);
 }
 
 /*
@@ -1015,6 +1041,7 @@ tty_alloc_mutex(struct ttydevsw *tsw, vo
 	tp->t_devsw = tsw;
 	tp->t_devswsoftc = sc;
 	tp->t_flags = tsw->tsw_flags;
+	tp->t_drainwait = tty_drainwait;
 
 	tty_init_termios(tp);
 
@@ -1755,6 +1782,14 @@ tty_generic_ioctl(struct tty *tp, u_long
 	case TIOCDRAIN:
 		/* Drain TTY output. */
 		return tty_drain(tp, 0);
+	case TIOCGDRAINWAIT:
+		*(int *)data = tp->t_drainwait;
+		return (0);
+	case TIOCSDRAINWAIT:
+		error = priv_check(td, PRIV_TTY_DRAINWAIT);
+		if (error == 0)
+			tp->t_drainwait = *(int *)data;
+		return (error);
 	case TIOCCONS:
 		/* Set terminal as console TTY. */
 		if (*(int *)data) {

Modified: head/sys/sys/tty.h
==============================================================================
--- head/sys/sys/tty.h	Thu Jan 12 00:34:37 2017	(r311953)
+++ head/sys/sys/tty.h	Thu Jan 12 00:48:06 2017	(r311954)
@@ -62,6 +62,7 @@ struct tty {
 	struct mtx	*t_mtx;		/* TTY lock. */
 	struct mtx	t_mtxobj;	/* Per-TTY lock (when not borrowing). */
 	TAILQ_ENTRY(tty) t_list;	/* (l) TTY list entry. */
+	int		t_drainwait;	/* (t) TIOCDRAIN timeout seconds. */
 	unsigned int	t_flags;	/* (t) Terminal option flags. */
 /* Keep flags in sync with db_show_tty and pstat(8). */
 #define	TF_NOPREFIX	0x00001	/* Don't prepend "tty" to device name. */



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