1. 概念:

Linux哲学:万物皆文件,所以UART和TTY本质是一条抽象链路

终端一词囊括了很多:

  • /dev/ttySx 串口设备终端,对应windows的COMx
  • /dev/pts: 当你ssh登陆机器看后台时,就会发现诸如pts/0pts/1
  • /dev/tty: 控制终端(Control),就是当前tty设备,所以你echo到这里都会显示到你输入echo的地方
  • /dev/ttyn:控制台终端(Console),当你在控制台上登录时,使用的是tty1

Linux Kernel 和8250 UART相关的config:

root@n250-046-171:/sys/kernel/debug# cat /boot/config-5.4.56.bsk.6-amd64 | grep CONFIG_SERIAL_8250
CONFIG_SERIAL_8250=y
# CONFIG_SERIAL_8250_DEPRECATED_OPTIONS is not set
CONFIG_SERIAL_8250_PNP=y
CONFIG_SERIAL_8250_FINTEK=y
CONFIG_SERIAL_8250_CONSOLE=y
CONFIG_SERIAL_8250_DMA=y
CONFIG_SERIAL_8250_PCI=y
CONFIG_SERIAL_8250_EXAR=y
CONFIG_SERIAL_8250_CS=m
CONFIG_SERIAL_8250_NR_UARTS=28
CONFIG_SERIAL_8250_RUNTIME_UARTS=28
CONFIG_SERIAL_8250_EXTENDED=y
CONFIG_SERIAL_8250_MANY_PORTS=y
CONFIG_SERIAL_8250_SHARE_IRQ=y
# CONFIG_SERIAL_8250_DETECT_IRQ is not set
CONFIG_SERIAL_8250_RSA=y
CONFIG_SERIAL_8250_DWLIB=y
CONFIG_SERIAL_8250_DW=y
# CONFIG_SERIAL_8250_RT288X is not set
# CONFIG_SERIAL_8250_LPSS is not set
CONFIG_SERIAL_8250_MID=y

查看uart制造商

udevadm info /dev/ttyS18

2.流程:

利用bpftrace我们追踪一下echo "xxxxx" /dev/ttyS12的内核栈

    _raw_spin_unlock_irqrestore+17
    uart_write+434
    n_tty_write+954
    tty_write+400
    vfs_write+165
    ksys_write+89
    do_syscall_64+89
    entry_SYSCALL_64_after_hwframe+68

可以看到先是到VFS层,然后到TTY CORE层实现并注册到VFS的operation结构体的.write方法,即tty_write

2.1 tty_write()

Write data to a tty device via the line discipline.

所有的tty类型设备的写入都会走到这里。然后会进入到下一层displine(又称线路规程)的write函数,来辅助驱动层进行规整输入输出。(ops->write)

...
ret = do_tty_write(ld->ops->write, tty, file, from);
...

2.2 n_tty_write()

适配层的write,处理输入输出N_TTY 终端驱动实现了规范终端模式,用户输入的字符会被缓冲并进行一些特定的处理,直到满足某些条件后才被传递给应用程序。这样提供了一些便利的特性,例如行编辑、回显和特殊字符处理等。

因为我们写入的是ttySxxx,是uart tty设备,所以从这里开始就和其他类型write分路扬镳了。

while (nr > 0) {
	mutex_lock(&ldata->output_lock);
	c = tty->ops->write(tty, b, nr); //调入下一层write
	mutex_unlock(&ldata->output_lock);
  • uart_write()
    这个就是最后一层面向驱动的write了。内部流程为写Circle Buffer(关键词xmit),关于这个缓冲区,内核 实现了一套宏去管理。开个坑:Linux circ_buf

接下来是驱动层。我们trace serial8250_start_tx这个函数

[serial8250_start_tx] comm:poduart[19]

        serial8250_start_tx+1
        uart_write+419
        kretprobe_trampoline+0

2.3 serial8250_start_tx()

static void serial8250_start_tx(struct uart_port *port)
{
	struct uart_8250_port *up = up_to_u8250p(port);
	struct uart_8250_em485 *em485 = up->em485;

	serial8250_rpm_get_tx(up); //RunPowerMangement

	if (em485 &&
	    em485->active_timer == &em485->start_tx_timer)
		return;

	if (em485)
		start_tx_rs485(port); //如果是485协议,需要经过这个函数,但这个函数只是在__start_tx前做了一些更多特性的判断或使能,最终还是__start_tx
	else
		__start_tx(port);
}

2.4 start_tx_rs485

这个函数主要处理RS-485相关的配置和定时器操作,以确保在发送期间正确控制RTS状态,并在需要时引入延迟。实际的发送操作由__start_tx()函数执行。

2.5 __start_tx

static inline void __start_tx(struct uart_port *port)
{
	struct uart_8250_port *up = up_to_u8250p(port);  // 将 struct uart_port 指针转换为 struct uart_8250_port 指针,以便访问特定于 8250 UART 的配置和状态

	if (up->dma && !up->dma->tx_dma(up))  // 如果启用了 DMA 传输且 DMA 传输函数返回成功,则通过 DMA 进行发送,而不执行后续操作
		return;

	if (serial8250_set_THRI(up)) {  // 设置发送寄存器可写标志(THRE)
		if (up->bugs & UART_BUG_TXEN) {  // 如果启用了 UART 的 UART_BUG_TXEN 标志(用于处理特定的 8250 UART 硬件缺陷)
			unsigned char lsr;

			lsr = serial_in(up, UART_LSR);  // 读取 UART 的 LSR 寄存器
			up->lsr_saved_flags |= lsr & LSR_SAVE_FLAGS;  // 保存 LSR 寄存器的特定标志位
			if (lsr & UART_LSR_THRE)  // 如果 LSR 寄存器的 THRE 标志已设置,表示发送缓冲区已经空闲
				serial8250_tx_chars(up);  // 执行实际的发送操作,将字符发送到 UART
		}
	}

	/*
	 * Re-enable the transmitter if we disabled it.
	 */
	if (port->type == PORT_16C950 && up->acr & UART_ACR_TXDIS) {  // 在某些特定的 UART 类型(PORT_16C950)中,如果发送器被禁用
		up->acr &= ~UART_ACR_TXDIS;  // 清除 UART 的 ACR 寄存器的 UART_ACR_TXDIS 标志
		serial_icr_write(up, UART_ACR, up->acr);  // 将新的 ACR 值写入 UART 的 ACR 寄存器,重新启用发送器
	}
}

2.6 serial8250_tx_chars()

实际发送操作

3.分层

UART从用户态到内核态做了以下分层:

  • TTY CORE (字符设备接口)
  • LINE DISCIPLINE(胶水层,类似VFS,可以理解为该链路的iptables
  • TTY DRIVER (对hardware抽象)

3.1 TTY CORE:

实现了用户使用的块设备接口:open、close、hangup、write、flush_buffer、poll、ioctl等

3.2 LINE Discipline(ldisc):

进行线路规整,默认规整函数注册的为n_tty开头的函数,用来调整输入输出以适配驱动

3.3 TTY Driver:

最终的驱动层。

4. Data Structure:

  • tty_struct

    include/linux/tty.h

struct tty_struct {
    int magic;
    struct kref kref;
    struct device *dev;
    struct tty_driver *driver;
    const struct tty_operations *ops;
    int index;
    struct tty_ldisc *ldisc;
    ...
    char name[64];
    ...
    struct tty_port *port;
} __randomize_layout;
  • tty_operations

    include/linux/tty_driver.h

struct tty_operations {
    struct tty_struct * (*lookup)(struct tty_driver *driver,
            struct file *filp, int idx);
    int  (*install)(struct tty_driver *driver, struct tty_struct *tty);
    void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
    int  (*open)(struct tty_struct * tty, struct file * filp);
    void (*close)(struct tty_struct * tty, struct file * filp);
    int  (*write)(struct tty_struct * tty,
              const unsigned char *buf, int count);
    ...
} __randomize_layout;
  • tty_ldisc_ops

    include/linux/tty_ldisc.h

struct tty_ldisc_ops {
...
	/*
	 * The following routines are called from above.
	 */
	int (*open)(struct tty_struct *);
...
	ssize_t (*read)(struct tty_struct *tty, struct file *file,
			unsigned char *buf, size_t nr,
			void **cookie, unsigned long offset);
	ssize_t (*write)(struct tty_struct *tty, struct file *file,
			 const unsigned char *buf, size_t nr);
	int (*ioctl)(struct tty_struct *tty, struct file *file,
			 unsigned int cmd, unsigned long arg);
...
	/*
	 * The following routines are called from below.
	 */
	void    (*receive_buf)(struct tty_struct *, const unsigned char *cp,
				   char *fp, int count);
...
	struct  module *owner;
	int refcount;
};
  • uart_state
struct uart_state {
	struct tty_port		port;

	enum uart_pm_state	pm_state;
	struct circ_buf		xmit;

	atomic_t		refcount;
	wait_queue_head_t	remove_wait;
	struct uart_port	*uart_port;
};

4.排障

4.1故障排查-UART部分不通

4.1.1 STEP1:

观察velaadb devices -l | wc -l发现adb都在线,uart不通,查看pod线程均在working

4.1.2 STEP2:

restart proc_pod发现重启非常卡、慢 (BTW:现象机为net_arch2.0,1.0暂未发现大片adb在线uart挂掉情况)

4.1.3 STEP3: 使用bpftraceuart_write

#!/usr/bin/bpftrace
#include <linux/tty.h>
kprobe:uart_write
{
    $ag = (struct tty_struct*)arg0;
    if ($ag->name == "ttyS18") {
        printf("n_tty_write first argument (name): %s, process: %s\n", $ag->name,comm);
        @[kstack()] = count();
        print(kstack)
    }
}

kretprobe:uart_write
{
   printf("n_tty_write return value: %ld,process: %s \n", retval,comm);

}

观察到: 1. echo 写入异常uart可以触发uart_write 2.异常node的写入返回值均为77

4.1.4 STEP4:二分法排查函数链路

继续深度追踪:

uart_write -> __uart_start

异常点发现, uart_write()------>serial8250_start_tx()----X--->serial8250_tx_chars()

4.1.5 STEP5: 具体定位

正常serial8250_tx_chars()函数栈:

命令:bpftrace -e 'kprobe:serial8250_tx_chars / tid == 8384 / { @[kstack] = count(); }'

推测: 所有写入由中断方式触发 serial8250_tx_chars

    @[
         serial8250_tx_chars+1
         serial8250_handle_irq.part.19+225
         serial8250_default_handle_irq+51
         serial8250_interrupt+92
        __handle_irq_event_percpu+64
         handle_irq_event_percpu+48
         handle_irq_event+60
         handle_edge_irq+145
         do_IRQ+77
         ret_from_intr+0
         __fsnotify_parent+65
         vfs_write+341
         ksys_write+89
         do_syscall_64+89
         entry_SYSCALL_64_after_hwframe+68
     ]: 1    

Trace 中断 irq_handler_entry:

bpftrace -p 8085 -e 'tracepoint:irq:irq_handler_entry { @[comm] = count(); }' > ~/ttt.txt

serial8250_start_tx()----X--->serial8250_tx_chars()过程中 经过一个static inline function,无法trace,只能分析源码,查看中途是否return掉导致无法走到最后。

serial8250_start_tx() --->\_\_start_tx()--->serial8250_tx_char()

  • 发现关键点:
static inline void __start_tx(struct uart_port *port)
{
...
    if (serial8250_set_THRI(up)) { //这里会check 
        if (up->bugs & UART_BUG_TXEN) {
            unsigned char lsr;

            lsr = serial_in(up, UART_LSR);
            up->lsr_saved_flags |= lsr & LSR_SAVE_FLAGS;
            if (lsr & UART_LSR_THRE)
                serial8250_tx_chars(up); //正常应该进入
        }
    }
...
}
static inline bool serial8250_set_THRI(struct uart_8250_port *up)
{
    if (up->ier & UART_IER_THRI)  //这一步会导致return false
        return false;
    up->ier |= UART_IER_THRI;
    serial_out(up, UART_IER, up->ier);
    return true;
}

我们退而求其次,在serial8250_start_tx()流程中的serial8250_rpm_get_tx(up)去hook,查看struct uart_8250_port up的ier位。

4.1.6 发现最终触发异常位置:

Normal:5--->b'101 Abnormal:7--->b'111

  • Bit 0(Received Data Available,RDA)设置为 1,表示启用接收数据可用中断。当接收缓冲区中有新的数据可供读取时,将触发中断。

  • Bit 1(Transmitter Holding Register Empty,THRE)设置为 1,表示启用发送保持寄存器空中断。当发送缓冲区为空且可以接受新的数据时,将触发中断。

  • Bit 2(Receiver Line Status,RLS)设置为 0,表示禁用接收线路状态变化中断。这意味着不会在接收线路状态发生变化(如接收错误、数据溢出等)时触发中断。

  • 其他位域可能设置为 0 或 1,具体取决于系统的需求和硬件的支持。

结论

发现寄存器异常后,重启node后发现uart的IER.THRE初始为正常状态b'0101,然后立即变为异常的b'0111。
于是接管异常uart读写,kill掉uart dameon,进行echo测试,寄存器IER.THRE维持正常状态。

但是:echo数据缓慢,且仍无法在node端读出。
此时探究卡顿点,使用perf打热力图

  热力图发现关键词异常点:“exar_shutdown” "tty_ldisc_kill"
追溯耗时相对最长的mem_serial_in

接管所有uart通信后,5分钟发现恢复正常,adb uart均通,无法再次手动复现掉线问题。

根据源码分析,tx_chars结束后会调用stop_tx,同时将ier.thri清零,故推测中断风暴问题。