[PATCH 2/2] tty: Implement lookahead to process XON/XOFF timely

From: Ilpo Järvinen
Date: Tue Apr 05 2022 - 17:55:26 EST


When tty is not read from, XON/XOFF may get stuck into an
intermediate buffer. As those characters are there to do software
flow-control, it is not very useful. In the case where neither end
reads from ttys, the receiving ends might not be able receive the
XOFF characters and just keep sending more data to the opposite
direction. This problem is almost guaranteed to occur with DMA
which sends data in large chunks.

If TTY is slow to process characters, that is, eats less than given
amount in receive_buf, invoke lookahead for the rest of the chars
to process potential XON/XOFF characters.

The guards necessary for ensuring the XON/XOFF character are
processed only once were added by the previous patch. All this patch
needs to do on that front is to pass the lookahead count (that can
now be non-zero) into port->client_ops->receive_buf().

Reported-by: Gilles Buloz <gilles.buloz@xxxxxxxxxxx>
Tested-by: Gilles Buloz <gilles.buloz@xxxxxxxxxxx>
Signed-off-by: Ilpo Järvinen <ilpo.jarvinen@xxxxxxxxxxxxxxx>
---
drivers/tty/n_tty.c | 55 ++++++++++++++++++++++++---------
drivers/tty/tty_buffer.c | 62 ++++++++++++++++++++++++++++++++------
drivers/tty/tty_port.c | 22 ++++++++++++++
include/linux/tty_buffer.h | 1 +
include/linux/tty_ldisc.h | 2 ++
include/linux/tty_port.h | 1 +
6 files changed, 119 insertions(+), 24 deletions(-)

diff --git a/drivers/tty/n_tty.c b/drivers/tty/n_tty.c
index e1b761740056..749218c6474f 100644
--- a/drivers/tty/n_tty.c
+++ b/drivers/tty/n_tty.c
@@ -1220,25 +1220,34 @@ n_tty_receive_signal_char(struct tty_struct *tty, int signal, unsigned char c)
process_echoes(tty);
}

+static bool __n_tty_receive_char_special(struct tty_struct *tty, unsigned char c,
+ bool lookahead_done)
+{
+ if (!I_IXON(tty))
+ return false;
+
+ if (c == START_CHAR(tty)) {
+ if (!lookahead_done) {
+ start_tty(tty);
+ process_echoes(tty);
+ }
+ return true;
+ }
+ if (c == STOP_CHAR(tty)) {
+ if (!lookahead_done)
+ stop_tty(tty);
+ return true;
+ }
+ return false;
+}
+
static void n_tty_receive_char_special(struct tty_struct *tty, unsigned char c,
bool lookahead_done)
{
struct n_tty_data *ldata = tty->disc_data;

- if (I_IXON(tty)) {
- if (c == START_CHAR(tty)) {
- if (!lookahead_done) {
- start_tty(tty);
- process_echoes(tty);
- }
- return;
- }
- if (c == STOP_CHAR(tty)) {
- if (!lookahead_done)
- stop_tty(tty);
- return;
- }
- }
+ if (__n_tty_receive_char_special(tty, c, lookahead_done))
+ return;

if (L_ISIG(tty)) {
if (c == INTR_CHAR(tty)) {
@@ -1450,6 +1459,23 @@ n_tty_receive_char_lnext(struct tty_struct *tty, unsigned char c, char flag)
n_tty_receive_char_flagged(tty, c, flag);
}

+static void n_tty_lookahead_special(struct tty_struct *tty, const unsigned char *cp,
+ const char *fp, int count)
+{
+ char flag = TTY_NORMAL;
+
+ if (!I_IXON(tty))
+ return;
+
+ while (count--) {
+ if (fp)
+ flag = *fp++;
+ if (likely(flag == TTY_NORMAL))
+ __n_tty_receive_char_special(tty, *cp, false);
+ cp++;
+ }
+}
+
static void
n_tty_receive_buf_real_raw(struct tty_struct *tty, const unsigned char *cp,
const char *fp, int count)
@@ -2405,6 +2431,7 @@ static struct tty_ldisc_ops n_tty_ops = {
.receive_buf = n_tty_receive_buf,
.write_wakeup = n_tty_write_wakeup,
.receive_buf2 = n_tty_receive_buf2,
+ .lookahead_buf = n_tty_lookahead_special,
};

/**
diff --git a/drivers/tty/tty_buffer.c b/drivers/tty/tty_buffer.c
index 38863fe8544f..11ae042c19ff 100644
--- a/drivers/tty/tty_buffer.c
+++ b/drivers/tty/tty_buffer.c
@@ -5,6 +5,7 @@

#include <linux/types.h>
#include <linux/errno.h>
+#include <linux/minmax.h>
#include <linux/tty.h>
#include <linux/tty_driver.h>
#include <linux/tty_flip.h>
@@ -104,6 +105,7 @@ static void tty_buffer_reset(struct tty_buffer *p, size_t size)
p->size = size;
p->next = NULL;
p->commit = 0;
+ p->lookahead = 0;
p->read = 0;
p->flags = 0;
}
@@ -233,6 +235,7 @@ void tty_buffer_flush(struct tty_struct *tty, struct tty_ldisc *ld)
buf->head = next;
}
buf->head->read = buf->head->commit;
+ buf->head->lookahead = buf->head->read;

if (ld && ld->ops->flush_buffer)
ld->ops->flush_buffer(tty);
@@ -275,13 +278,15 @@ static int __tty_buffer_request_room(struct tty_port *port, size_t size,
if (n != NULL) {
n->flags = flags;
buf->tail = n;
- /* paired w/ acquire in flush_to_ldisc(); ensures
- * flush_to_ldisc() sees buffer data.
+ /*
+ * paired w/ acquire in flush_to_ldisc() and lookahead_bufs()
+ * ensures they see all buffer data.
*/
smp_store_release(&b->commit, b->used);
- /* paired w/ acquire in flush_to_ldisc(); ensures the
- * latest commit value can be read before the head is
- * advanced to the next buffer
+ /*
+ * paired w/ acquire in flush_to_ldisc() and lookahead_bufs()
+ * ensures the latest commit value can be read before the head
+ * is advanced to the next buffer
*/
smp_store_release(&b->next, n);
} else if (change)
@@ -458,6 +463,41 @@ int tty_ldisc_receive_buf(struct tty_ldisc *ld, const unsigned char *p,
}
EXPORT_SYMBOL_GPL(tty_ldisc_receive_buf);

+static void lookahead_bufs(struct tty_port *port, struct tty_buffer *head)
+{
+ head->lookahead = max(head->lookahead, head->read);
+
+ while (head) {
+ struct tty_buffer *next;
+ unsigned char *p;
+ char *f = NULL;
+ int count;
+
+ /*
+ * paired w/ release in __tty_buffer_request_room();
+ * ensures commit value read is not stale if the head
+ * is advancing to the next buffer
+ */
+ next = smp_load_acquire(&head->next);
+ /*
+ * paired w/ release in __tty_buffer_request_room() or in
+ * tty_buffer_flush(); ensures we see the committed buffer data
+ */
+ count = smp_load_acquire(&head->commit) - head->lookahead;
+ if (!count) {
+ head = next;
+ continue;
+ }
+
+ p = char_buf_ptr(head, head->lookahead);
+ if (~head->flags & TTYB_NORMAL)
+ f = flag_buf_ptr(head, head->lookahead);
+
+ port->client_ops->lookahead_buf(port, p, f, count);
+ head->lookahead += count;
+ }
+}
+
static int
receive_buf(struct tty_port *port, struct tty_buffer *head, int count)
{
@@ -468,7 +508,7 @@ receive_buf(struct tty_port *port, struct tty_buffer *head, int count)
if (~head->flags & TTYB_NORMAL)
f = flag_buf_ptr(head, head->read);

- n = port->client_ops->receive_buf(port, p, f, count, 0);
+ n = port->client_ops->receive_buf(port, p, f, count, max(head->lookahead - head->read, 0));
if (n > 0)
memset(p, 0, n);
return n;
@@ -495,7 +535,7 @@ static void flush_to_ldisc(struct work_struct *work)
while (1) {
struct tty_buffer *head = buf->head;
struct tty_buffer *next;
- int count;
+ int count, rcvd;

/* Ldisc or user is trying to gain exclusive access */
if (atomic_read(&buf->priority))
@@ -518,10 +558,12 @@ static void flush_to_ldisc(struct work_struct *work)
continue;
}

- count = receive_buf(port, head, count);
- if (!count)
+ rcvd = receive_buf(port, head, count);
+ head->read += rcvd;
+ if (rcvd < count)
+ lookahead_bufs(port, head);
+ if (!rcvd)
break;
- head->read += count;

if (need_resched())
cond_resched();
diff --git a/drivers/tty/tty_port.c b/drivers/tty/tty_port.c
index 0d41ec0e5fee..650f38d67159 100644
--- a/drivers/tty/tty_port.c
+++ b/drivers/tty/tty_port.c
@@ -44,6 +44,27 @@ static int tty_port_default_receive_buf(struct tty_port *port,
return ret;
}

+static void tty_port_default_lookahead_buf(struct tty_port *port,
+ const unsigned char *p,
+ const unsigned char *f, size_t count)
+{
+ struct tty_struct *tty;
+ struct tty_ldisc *disc;
+
+ tty = READ_ONCE(port->itty);
+ if (!tty)
+ return;
+
+ disc = tty_ldisc_ref(tty);
+ if (!disc)
+ return;
+
+ if (disc->ops->lookahead_buf)
+ disc->ops->lookahead_buf(disc->tty, p, f, count);
+
+ tty_ldisc_deref(disc);
+}
+
static void tty_port_default_wakeup(struct tty_port *port)
{
struct tty_struct *tty = tty_port_tty_get(port);
@@ -56,6 +77,7 @@ static void tty_port_default_wakeup(struct tty_port *port)

const struct tty_port_client_operations tty_port_default_client_ops = {
.receive_buf = tty_port_default_receive_buf,
+ .lookahead_buf = tty_port_default_lookahead_buf,
.write_wakeup = tty_port_default_wakeup,
};
EXPORT_SYMBOL_GPL(tty_port_default_client_ops);
diff --git a/include/linux/tty_buffer.h b/include/linux/tty_buffer.h
index 3b9d77604291..1796648c2907 100644
--- a/include/linux/tty_buffer.h
+++ b/include/linux/tty_buffer.h
@@ -15,6 +15,7 @@ struct tty_buffer {
int used;
int size;
int commit;
+ int lookahead; /* Lazy update on recv, can become less than "read" */
int read;
int flags;
/* Data points here */
diff --git a/include/linux/tty_ldisc.h b/include/linux/tty_ldisc.h
index 496f4755ea05..fc7b4ffe76c6 100644
--- a/include/linux/tty_ldisc.h
+++ b/include/linux/tty_ldisc.h
@@ -229,6 +229,8 @@ struct tty_ldisc_ops {
void (*dcd_change)(struct tty_struct *tty, unsigned int status);
int (*receive_buf2)(struct tty_struct *tty, const unsigned char *cp,
const char *fp, int count, int lookahead_count);
+ void (*lookahead_buf)(struct tty_struct *tty, const unsigned char *cp,
+ const char *fp, int count);

struct module *owner;
};
diff --git a/include/linux/tty_port.h b/include/linux/tty_port.h
index 1871a6a9cb00..bc768265bf63 100644
--- a/include/linux/tty_port.h
+++ b/include/linux/tty_port.h
@@ -40,6 +40,7 @@ struct tty_port_operations {

struct tty_port_client_operations {
int (*receive_buf)(struct tty_port *port, const unsigned char *, const unsigned char *, size_t, size_t);
+ void (*lookahead_buf)(struct tty_port *port, const unsigned char *, const unsigned char *, size_t);
void (*write_wakeup)(struct tty_port *port);
};

--
2.30.2