Text terminal (and of course,
console,
pseudo-terminal,
virtual console and
terminal emulator) or TTY (
teletype printer) as it is usually called, due to historical reason, is something any average Linux/UNIX user would use on a daily basis, in one form or another.
Its internal mechanism is indeed something that has always catch my attention and curiosity. Since today is a holiday, I took some time off to trace the code behind the serial interface (like ttyS0). Below is what I learn...(or so I think).
Before diving deep into the rest of the source, lets look at some definition.
#include/linux/serial_reg.h:
#define UART_RX 0 /* In: Receive buffer */
#define UART_LSR 5 /* In: Line Status Register */
#define UART_LSR_BI 0x10 /* Break interrupt indicator */
#define UART_LSR_DR 0x01 /* Receiver data ready */
And also the central structure tty_struct, which due to history reason, also include data for the line discipline.
#include/linux/tty.h:
struct tty_struct {
...
struct tty_ldisc *ldisc;
...
struct tty_bufhead buf;
...
wait_queue_head_t read_wait;
...
* The following is data for the N_TTY line discipline. For
* historical reasons, this is included in the tty structure.
...
char *read_buf;
...
struct tty_port *port;
};
When there are data on the serial (UART) interface, and the interrupt is trigger, the interrupt service routine serial8250_interrupt() is invoked.
drivers/serial/8250.c:
static irqreturn_t serial8250_interrupt(int irq, void *dev_id)
{
...
if (!(iir & UART_IIR_NO_INT)) {
serial8250_handle_port(up);
...
}
static void serial8250_handle_port(struct uart_8250_port *up)
{
...
if (status & (UART_LSR_DR | UART_LSR_BI))
receive_chars(up, &status);
...
}
static void
receive_chars(struct uart_8250_port *up, unsigned int *status)
{
...
do {
if (likely(lsr & UART_LSR_DR))
ch = serial_inp(up, UART_RX);
...
uart_insert_char(&up->port, lsr, UART_LSR_OE, ch, flag);
ignore_char:
lsr = serial_inp(up, UART_LSR);
} while ((lsr & (UART_LSR_DR | UART_LSR_BI)) && (max_count-- > 0));
...
tty_flip_buffer_push(tty);
...
}
#include/linux/serial_core.h:
static inline void
uart_insert_char(struct uart_port *port, unsigned int status,
unsigned int overrun, unsigned int ch, unsigned int flag)
{
...
tty_insert_flip_char(tty, ch, flag);
...
}
From the interrupt service routine, the callee will eventually copy the data to the corresponding tty_buffer.
#include/linux/tty_flip.h:
static inline int tty_insert_flip_char(struct tty_struct *tty,
unsigned char ch, char flag)
{
...
struct tty_buffer *tb = tty->buf.tail;
...
tb->char_buf_ptr[tb->used++] = ch;
...
}
Then, the data is push/flush to the corresponding line discipline.
drivers/char/tty_buffer.c:
* Queue a push of the terminal flip buffers to the line discipline. This
void tty_flip_buffer_push(struct tty_struct *tty)
{
...
if (tty->low_latency)
flush_to_ldisc(&tty->buf.work.work);
else
schedule_delayed_work(&tty->buf.work, 1);
}
static void flush_to_ldisc(struct work_struct *work)
{
...
struct tty_ldisc *disc;
...
disc->ops->receive_buf(tty, char_buf,
flag_buf, count);
...
}
#include/linux/tty_ldisc.h:
struct tty_ldisc_ops {
...
/*
* The following routines are called from below.
*/
void (*receive_buf)(struct tty_struct *, const unsigned char *cp,
char *fp, int count);
void (*write_wakeup)(struct tty_struct *);
...
};
struct tty_ldisc {
struct tty_ldisc_ops *ops;
...
};
The line discipline's receive_buf hook will copy the data to its own read buffer and wake up the process that is waiting (sleeping) for the incoming data.
drivers/char/n_tty.c:
* Called by the terminal driver when a block of characters has
* been received. This function must be called from soft contexts
* not from interrupt context. The driver is responsible for making
* calls one at a time and in order (or using flush_to_ldisc)
static void n_tty_receive_buf(struct tty_struct *tty, const unsigned char *cp,
char *fp, int count)
{
...
memcpy(tty->read_buf + tty->read_head, cp, i);
...
if (!tty->icanon && (tty->read_cnt >= tty->minimum_to_wake)) {
kill_fasync(&tty->fasync, SIGIO, POLL_IN);
if (waitqueue_active(&tty->read_wait))
wake_up_interruptible(&tty->read_wait);
}
...
}
struct tty_ldisc_ops tty_ldisc_N_TTY = {
...
.receive_buf = n_tty_receive_buf,
...
};
And that is how the program (like the bash shell that is running on ttyS0) receive the data!
Or at least that is what I think how it all works.
If time permits, it would be great to continue digging into how write operation works on ttyS0, and other "terminals" like pty etc.