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

From: Andy Shevchenko
Date: Mon Jun 06 2022 - 10:20:50 EST


On Mon, Jun 6, 2022 at 3:45 PM Ilpo Järvinen
<ilpo.jarvinen@xxxxxxxxxxxxxxx> wrote:
>
> 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 callchain.

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 approriate n_tty_receive_buf_* function. First call is

appropriate

> 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.

Seems good to me, but I am not an expert in TTY layer, perhaps Jiri or
somebody else who is more familiar with this can look into. I can
suggest to add Johan Hovold (because he maintains USB serial) and Rob
Herring (because he introduced serdev).

Reviewed-by: Andy Shevchenko <andy.shevchenko@xxxxxxxxx>
Also see a few nit-picks below.

> Reported-by: Gilles Buloz <gilles.buloz@xxxxxxxxxxx>
> Signed-off-by: Ilpo Järvinen <ilpo.jarvinen@xxxxxxxxxxxxxxx>
> ---
> drivers/tty/n_tty.c | 90 +++++++++++++++++++++++++++++++-------
> drivers/tty/tty_buffer.c | 59 +++++++++++++++++++++----
> drivers/tty/tty_port.c | 21 +++++++++
> include/linux/tty_buffer.h | 1 +
> include/linux/tty_ldisc.h | 13 ++++++
> include/linux/tty_port.h | 2 +
> 6 files changed, 161 insertions(+), 25 deletions(-)
>
> diff --git a/drivers/tty/n_tty.c b/drivers/tty/n_tty.c
> index 640c9e871044..917b5970b2e0 100644
> --- a/drivers/tty/n_tty.c
> +++ b/drivers/tty/n_tty.c
> @@ -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

@c

> + * 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,26 @@ 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_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 count == 0 at the beginning of this function, this becomes an
infinite loop, perhaps adding a comment that explains that this never
happens?

> + 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 +1544,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 +1552,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 +1588,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 +1599,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 +2503,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,
> };
>
> /**
> diff --git a/drivers/tty/tty_buffer.c b/drivers/tty/tty_buffer.c
> index bfa431a8e690..754fa43670cc 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;
> }
> @@ -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();
> diff --git a/drivers/tty/tty_port.c b/drivers/tty/tty_port.c
> index 880608a65773..dce08a6d7b5e 100644
> --- a/drivers/tty/tty_port.c
> +++ b/drivers/tty/tty_port.c
> @@ -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);
> 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 e85002b56752..bf8143fbcff3 100644
> --- a/include/linux/tty_ldisc.h
> +++ b/include/linux/tty_ldisc.h
> @@ -186,6 +186,17 @@ 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

... by ->receive_buf() or ->receive_buf2().

> + * 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

Ditto.

> + * actions for high-priority characters already handled by lookahead_buf).

Similar.

> + *
> * @owner: module containting this ldisc (for reference counting)
> *
> * This structure defines the interface between the tty line discipline
> @@ -229,6 +240,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;
> };
> diff --git a/include/linux/tty_port.h b/include/linux/tty_port.h
> index 58e9619116b7..fa3c3bdaa234 100644
> --- a/include/linux/tty_port.h
> +++ b/include/linux/tty_port.h
> @@ -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);
> };
>
> --
> 2.30.2
>


--
With Best Regards,
Andy Shevchenko