tty: Implement lookahead to process XON/XOFF timely
authorIlpo Järvinen <ilpo.jarvinen@linux.intel.com>
Mon, 6 Jun 2022 15:36:51 +0000 (18:36 +0300)
committerGreg Kroah-Hartman <gregkh@linuxfoundation.org>
Fri, 10 Jun 2022 11:51:31 +0000 (13:51 +0200)
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.

We need to keep track of how many characters have been processed by the
lookahead to avoid processing the flow control char again on the normal
path. Bookkeeping occurs parallel on two layers (tty_buffer and n_tty)
to avoid passing the lookahead_count through the whole call chain.

When a flow-control char is processed, two things must occur:
  a) it must not be treated as normal char
  b) if not yet processed, flow-control actions need to be taken
The return value of n_tty_receive_char_flow_ctrl() tells caller a), and
b) is kept internal to n_tty_receive_char_flow_ctrl().

If characters were previous looked ahead, __receive_buf() makes two
calls to the appropriate n_tty_receive_buf_* function. First call is
made with lookahead_done=true for the characters that were subject to
lookahead earlier and then with lookahead=false for the new characters.
Either of the calls might be skipped when it has no characters to
handle.

Reported-by: Gilles Buloz <gilles.buloz@kontron.com>
Reviewed-by: Andy Shevchenko <andy.shevchenko@gmail.com>
Signed-off-by: Ilpo Järvinen <ilpo.jarvinen@linux.intel.com>
Link: https://lore.kernel.org/r/20220606153652.63554-2-ilpo.jarvinen@linux.intel.com
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
drivers/tty/n_tty.c
drivers/tty/tty_buffer.c
drivers/tty/tty_port.c
include/linux/tty_buffer.h
include/linux/tty_ldisc.h
include/linux/tty_port.h

index 640c9e87104494ecffb759cd330a4df6bc51bd96..8d80384df87441b4c09fe4f8716c2bb7772f4344 100644 (file)
@@ -118,6 +118,9 @@ struct n_tty_data {
        size_t read_tail;
        size_t line_start;
 
+       /* # of chars looked ahead (to find software flow control chars) */
+       size_t lookahead_count;
+
        /* protected by output lock */
        unsigned int column;
        unsigned int canon_column;
@@ -333,6 +336,8 @@ static void reset_buffer_flags(struct n_tty_data *ldata)
        ldata->erasing = 0;
        bitmap_zero(ldata->read_flags, N_TTY_BUF_SIZE);
        ldata->push = 0;
+
+       ldata->lookahead_count = 0;
 }
 
 static void n_tty_packet_mode_flush(struct tty_struct *tty)
@@ -1225,12 +1230,30 @@ static bool n_tty_is_char_flow_ctrl(struct tty_struct *tty, unsigned char c)
        return c == START_CHAR(tty) || c == STOP_CHAR(tty);
 }
 
-/* Returns true if c is consumed as flow-control character */
-static bool n_tty_receive_char_flow_ctrl(struct tty_struct *tty, unsigned char c)
+/**
+ * n_tty_receive_char_flow_ctrl - receive flow control chars
+ * @tty: terminal device
+ * @c: character
+ * @lookahead_done: lookahead has processed this character already
+ *
+ * Receive and process flow control character actions.
+ *
+ * In case lookahead for flow control chars already handled the character in
+ * advance to the normal receive, the actions are skipped during normal
+ * receive.
+ *
+ * Returns true if @c is consumed as flow-control character, the character
+ * must not be treated as normal character.
+ */
+static bool n_tty_receive_char_flow_ctrl(struct tty_struct *tty, unsigned char c,
+                                        bool lookahead_done)
 {
        if (!n_tty_is_char_flow_ctrl(tty, c))
                return false;
 
+       if (lookahead_done)
+               return true;
+
        if (c == START_CHAR(tty)) {
                start_tty(tty);
                process_echoes(tty);
@@ -1242,11 +1265,12 @@ static bool n_tty_receive_char_flow_ctrl(struct tty_struct *tty, unsigned char c
        return true;
 }
 
-static void n_tty_receive_char_special(struct tty_struct *tty, unsigned char c)
+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) && n_tty_receive_char_flow_ctrl(tty, c))
+       if (I_IXON(tty) && n_tty_receive_char_flow_ctrl(tty, c, lookahead_done))
                return;
 
        if (L_ISIG(tty)) {
@@ -1401,7 +1425,8 @@ static void n_tty_receive_char(struct tty_struct *tty, unsigned char c)
        put_tty_queue(c, ldata);
 }
 
-static void n_tty_receive_char_closing(struct tty_struct *tty, unsigned char c)
+static void n_tty_receive_char_closing(struct tty_struct *tty, unsigned char c,
+                                      bool lookahead_done)
 {
        if (I_ISTRIP(tty))
                c &= 0x7f;
@@ -1409,9 +1434,12 @@ static void n_tty_receive_char_closing(struct tty_struct *tty, unsigned char c)
                c = tolower(c);
 
        if (I_IXON(tty)) {
-               if (c == STOP_CHAR(tty))
-                       stop_tty(tty);
-               else if (c == START_CHAR(tty) ||
+               if (c == STOP_CHAR(tty)) {
+                       if (!lookahead_done)
+                               stop_tty(tty);
+               } else if (c == START_CHAR(tty) && lookahead_done) {
+                       return;
+               } else if (c == START_CHAR(tty) ||
                         (tty->flow.stopped && !tty->flow.tco_stopped && I_IXANY(tty) &&
                          c != INTR_CHAR(tty) && c != QUIT_CHAR(tty) &&
                          c != SUSP_CHAR(tty))) {
@@ -1457,6 +1485,27 @@ n_tty_receive_char_lnext(struct tty_struct *tty, unsigned char c, char flag)
                n_tty_receive_char_flagged(tty, c, flag);
 }
 
+/* Caller must ensure count > 0 */
+static void n_tty_lookahead_flow_ctrl(struct tty_struct *tty, const unsigned char *cp,
+                                     const unsigned char *fp, unsigned int count)
+{
+       struct n_tty_data *ldata = tty->disc_data;
+       unsigned char flag = TTY_NORMAL;
+
+       ldata->lookahead_count += count;
+
+       if (!I_IXON(tty))
+               return;
+
+       while (count--) {
+               if (fp)
+                       flag = *fp++;
+               if (likely(flag == TTY_NORMAL))
+                       n_tty_receive_char_flow_ctrl(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)
@@ -1496,7 +1545,7 @@ n_tty_receive_buf_raw(struct tty_struct *tty, const unsigned char *cp,
 
 static void
 n_tty_receive_buf_closing(struct tty_struct *tty, const unsigned char *cp,
-                         const char *fp, int count)
+                         const char *fp, int count, bool lookahead_done)
 {
        char flag = TTY_NORMAL;
 
@@ -1504,12 +1553,12 @@ n_tty_receive_buf_closing(struct tty_struct *tty, const unsigned char *cp,
                if (fp)
                        flag = *fp++;
                if (likely(flag == TTY_NORMAL))
-                       n_tty_receive_char_closing(tty, *cp++);
+                       n_tty_receive_char_closing(tty, *cp++, lookahead_done);
        }
 }
 
 static void n_tty_receive_buf_standard(struct tty_struct *tty,
-               const unsigned char *cp, const char *fp, int count)
+               const unsigned char *cp, const char *fp, int count, bool lookahead_done)
 {
        struct n_tty_data *ldata = tty->disc_data;
        char flag = TTY_NORMAL;
@@ -1540,7 +1589,7 @@ static void n_tty_receive_buf_standard(struct tty_struct *tty,
                }
 
                if (test_bit(c, ldata->char_map))
-                       n_tty_receive_char_special(tty, c);
+                       n_tty_receive_char_special(tty, c, lookahead_done);
                else
                        n_tty_receive_char(tty, c);
        }
@@ -1551,21 +1600,30 @@ static void __receive_buf(struct tty_struct *tty, const unsigned char *cp,
 {
        struct n_tty_data *ldata = tty->disc_data;
        bool preops = I_ISTRIP(tty) || (I_IUCLC(tty) && L_IEXTEN(tty));
+       size_t la_count = min_t(size_t, ldata->lookahead_count, count);
 
        if (ldata->real_raw)
                n_tty_receive_buf_real_raw(tty, cp, fp, count);
        else if (ldata->raw || (L_EXTPROC(tty) && !preops))
                n_tty_receive_buf_raw(tty, cp, fp, count);
-       else if (tty->closing && !L_EXTPROC(tty))
-               n_tty_receive_buf_closing(tty, cp, fp, count);
-       else {
-               n_tty_receive_buf_standard(tty, cp, fp, count);
+       else if (tty->closing && !L_EXTPROC(tty)) {
+               if (la_count > 0)
+                       n_tty_receive_buf_closing(tty, cp, fp, la_count, true);
+               if (count > la_count)
+                       n_tty_receive_buf_closing(tty, cp, fp, count - la_count, false);
+       } else {
+               if (la_count > 0)
+                       n_tty_receive_buf_standard(tty, cp, fp, la_count, true);
+               if (count > la_count)
+                       n_tty_receive_buf_standard(tty, cp, fp, count - la_count, false);
 
                flush_echoes(tty);
                if (tty->ops->flush_chars)
                        tty->ops->flush_chars(tty);
        }
 
+       ldata->lookahead_count -= la_count;
+
        if (ldata->icanon && !L_EXTPROC(tty))
                return;
 
@@ -2446,6 +2504,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_flow_ctrl,
 };
 
 /**
index bfa431a8e6902a68148a0f56a0c4e6f7d9e8e088..754fa43670cc56a7590395ce81dddd85f28a03a0 100644 (file)
@@ -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;
 }
@@ -234,6 +236,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);
@@ -276,13 +279,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)
@@ -459,6 +464,40 @@ 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, *f = NULL;
+               unsigned 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)
 {
@@ -496,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))
@@ -519,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();
index 880608a6577343a5792d3e8f399d4f69f7a8139e..dce08a6d7b5e843d70653c809fab67edfb1a0c47 100644 (file)
@@ -43,6 +43,26 @@ 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, unsigned int 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);
@@ -55,6 +75,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);
index 3b9d776042915c6df82380a452301202f09d0037..1796648c290738a234646def8203bd175ef4526e 100644 (file)
@@ -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 */
index e85002b56752b1fc840f685e5c422c3b32e39643..33678e1936f6a6bdd123236d8a8e078f57610813 100644 (file)
@@ -186,6 +186,18 @@ int ldsem_down_write_nested(struct ld_semaphore *sem, int subclass,
  *     indicate all data received is %TTY_NORMAL. If assigned, prefer this
  *     function for automatic flow control.
  *
+ * @lookahead_buf: [DRV] ``void ()(struct tty_struct *tty,
+ *                     const unsigned char *cp, const char *fp, int count)
+ *
+ *     This function is called by the low-level tty driver for characters
+ *     not eaten by ->receive_buf() or ->receive_buf2(). It is useful for
+ *     processing high-priority characters such as software flow-control
+ *     characters that could otherwise get stuck into the intermediate
+ *     buffer until tty has room to receive them. Ldisc must be able to
+ *     handle later a ->receive_buf() or ->receive_buf2() call for the
+ *     same characters (e.g. by skipping the actions for high-priority
+ *     characters already handled by ->lookahead_buf()).
+ *
  * @owner: module containting this ldisc (for reference counting)
  *
  * This structure defines the interface between the tty line discipline
@@ -229,6 +241,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);
+       void    (*lookahead_buf)(struct tty_struct *tty, const unsigned char *cp,
+                                const unsigned char *fp, unsigned int count);
 
        struct  module *owner;
 };
index 58e9619116b75fdbf795ffb8c11458be0a2b9a82..fa3c3bdaa2347abe0020e0e9c6a79cb4729c4e2f 100644 (file)
@@ -40,6 +40,8 @@ struct tty_port_operations {
 
 struct tty_port_client_operations {
        int (*receive_buf)(struct tty_port *port, const unsigned char *, const unsigned char *, size_t);
+       void (*lookahead_buf)(struct tty_port *port, const unsigned char *cp,
+                             const unsigned char *fp, unsigned int count);
        void (*write_wakeup)(struct tty_port *port);
 };