dg-publish: "true"
Linux哲学:万物皆文件,所以UART和TTY本质是一条抽象链路
终端一词囊括了很多:
/dev/ttySx
串口设备终端,对应windows的COMx/dev/pts
: 当你ssh登陆机器看后台时,就会发现诸如pts/0
、pts/1
/dev/tty
: 控制终端(Control),就是当前tty设备,所以你echo到这里都会显示到你输入echo的地方。/dev/ttyn
:控制台终端(Console),当你在控制台上登录时,使用的是tty1Linux 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
利用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
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);
...
适配层的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);
接下来是驱动层。我们trace serial8250_start_tx
这个函数
[serial8250_start_tx] comm:poduart[19]
serial8250_start_tx+1
uart_write+419
kretprobe_trampoline+0
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);
}
这个函数主要处理RS-485相关的配置和定时器操作,以确保在发送期间正确控制RTS状态,并在需要时引入延迟。实际的发送操作由__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 寄存器,重新启用发送器
}
}
实际发送操作
UART从用户态到内核态做了以下分层:
iptables
)实现了用户使用的块设备接口:open、close、hangup、write、flush_buffer、poll、ioctl等
进行线路规整,默认规整函数注册的为n_tty
开头的函数,用来调整输入输出以适配驱动
最终的驱动层。
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;
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;
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;
};
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;
};
观察vela
、adb devices -l | wc -l
发现adb都在线,uart不通,查看pod线程均在working
restart proc_pod发现重启非常卡、慢 (BTW:现象机为net_arch2.0,1.0暂未发现大片adb在线uart挂掉情况)
uart_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
继续深度追踪:
uart_write
-> __uart_start
异常点发现, uart_write()------>serial8250_start_tx()----X--->serial8250_tx_chars()
正常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位。
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清零,故推测中断风暴问题。