往期内容
本专栏往期内容:Uart子系统
- UART串口硬件介绍
- 深入理解TTY体系:设备节点与驱动程序框架详解
- Linux串口应用编程:从UART到GPS模块及字符设备驱动
- 解UART 子系统:Linux Kernel 4.9.88 中的核心结构体与设计详解
- IMX 平台UART驱动情景分析:注册篇
- IMX 平台UART驱动情景分析:open篇
interrupt子系统专栏:
- 专栏地址:interrupt子系统
- Linux 链式与层级中断控制器讲解:原理与驱动开发
– 末片,有专栏内容观看顺序pinctrl和gpio子系统专栏:
专栏地址:pinctrl和gpio子系统
编写虚拟的GPIO控制器的驱动程序:和pinctrl的交互使用
– 末片,有专栏内容观看顺序
input子系统专栏:
- 专栏地址:input子系统
- input角度:I2C触摸屏驱动分析和编写一个简单的I2C驱动程序
– 末片,有专栏内容观看顺序I2C子系统专栏:
- 专栏地址:IIC子系统
- 具体芯片的IIC控制器驱动程序分析:i2c-imx.c-CSDN博客
– 末篇,有专栏内容观看顺序总线和设备树专栏:
- 专栏地址:总线和设备树
- 设备树与 Linux 内核设备驱动模型的整合-CSDN博客
– 末篇,有专栏内容观看顺序
目录
- 往期内容
- 1.内核代码
- 2.行规程
- 3.read过程分析
- 4.数据源头:中断
1.内核代码
硬件相关:
-
drivers/tty/serial/imx.c📎imx.c — imx系列的
-
drivers/tty/serial/stm32-usart.c📎stm32-usart.c — stm32系列的
串口核心层:
- drivers/tty/serial/serial_core.c📎serial_core.c
TTY层:
- drivers/tty/tty_io.c📎tty_io.c
2.行规程
Linux-4.9.88\drivers\tty[📎tty_ldisc.c](https://www.yuque.com/attachments/yuque/0/2024/txt/40457654/1731853957034-ca0a7ae6-b27d-4406-810a-ae72d3c13119.txt)
行规程(TTY Line Discipline)是TTY层的关键模块,用于处理从硬件到用户空间的数据流和控制流。
- 基本概念:
- 行规程位于串口驱动和TTY层之间,负责处理数据格式化、缓冲区管理,以及对数据进行规范化处理(如规范模式和原始模式)。
- 通过
tty_ldisc_ops
接口,行规程定义了一系列操作函数,比如read
、write
、ioctl
等。
- n_tty规程:
n_tty
是默认的行规程,主要用于处理规范模式和原始模式的字符数据。- 在规范模式下,
n_tty_read
会等待换行符或缓冲区满时才返回数据。 - 在原始模式下,数据直接传递到用户空间。
行规程是通过tty_ldisc_ops
结构实现扩展的,用户可以根据需要编写自定义的行规程,用于特殊协议或数据处理需求。
具体的相关结构体看之前的文章:理解UART 子系统:Linux Kernel 4.9.88 中的核心结构体与设计详解
具体的tty体系介绍看:深入理解TTY体系:设备节点与驱动程序框架详解
3.read过程分析
read
调用涉及从应用程序到硬件的完整数据流,其关键点包括:
- 应用程序读取数据:
- 用户空间通过
read
系统调用读取数据,最终映射到tty_read
函数。 - 如果数据缓冲区为空,调用线程会进入休眠状态。
- 用户空间通过
- 中断接收数据:
- 硬件接收到数据后触发中断(如
imx_rxint
),中断处理函数将数据从硬件FIFO读取到内核缓冲区。
- 硬件接收到数据后触发中断(如
- 行规程处理:
- 中断数据通过调用
ldisc->ops->receive_buf
传递到行规程的接收函数(如n_tty_receive_buf
)。 - 行规程对数据进行处理(如行编辑、缓冲区管理)后,将其存储到行规程的缓冲区中。
- 中断数据通过调用
- 唤醒应用程序:
- 行规程在接收到完整数据或触发条件(如换行符)时,通过
wake_up_interruptible
唤醒休眠的用户进程。
- 行规程在接收到完整数据或触发条件(如换行符)时,通过
- 返回用户数据:
- 应用程序进程被唤醒后,从行规程缓冲区读取数据并返回给用户。
扩展要点:
- 锁机制:通过
mutex_lock
和信号量保证并发访问的安全性。 - 数据拷贝:从硬件缓冲区到内核缓冲区,再到用户空间缓冲区,多级拷贝可能会影响性能。
\Linux-4.9.88\drivers\tty\tty_io.c
static const struct file_operations tty_fops = {.llseek = no_llseek,.read = tty_read,.write = tty_write,.poll = tty_poll,.unlocked_ioctl = tty_ioctl,.compat_ioctl = tty_compat_ioctl,.open = tty_open,.release = tty_release,.fasync = tty_fasync,
};
具体的也不再缀述了,在之前讲解open的时候,如果能看懂的话,那么read和write其实也很好懂,只不过中间过了个行规程而已,直接看tty_read
函数,在此之前先来讲一下行规程的结构体tty_ldisc
:
\Linux-4.9.88\include\linux\tty_ldisc.h
struct tty_ldisc {struct tty_ldisc_ops *ops; // 指向TTY行规约操作的结构体struct tty_struct *tty; // 指向相关的TTY结构体
};//其成员:
\Linux-4.9.88\Linux-4.9.88\include\linux\tty_ldisc.h
struct tty_ldisc_ops {int magic; // 用于验证的魔数,确保结构体的正确性char *name; // 行规约的名称int num; // 行规约的编号int flags; // 行规约的标志,定义了该行规约的特性/** The following routines are called from above.*/int (*open)(struct tty_struct *); // 打开TTY的方法void (*close)(struct tty_struct *); // 关闭TTY的方法void (*flush_buffer)(struct tty_struct *tty); // 刷新TTY缓冲区的方法ssize_t (*read)(struct tty_struct *tty, struct file *file,unsigned char __user *buf, size_t nr); // 读取TTY的数据ssize_t (*write)(struct tty_struct *tty, struct file *file,const unsigned char *buf, size_t nr); // 写入数据到TTYint (*ioctl)(struct tty_struct *tty, struct file *file,unsigned int cmd, unsigned long arg); // 控制TTY的ioctl方法long (*compat_ioctl)(struct tty_struct *tty, struct file *file,unsigned int cmd, unsigned long arg); // 兼容ioctl方法void (*set_termios)(struct tty_struct *tty, struct ktermios *old); // 设置TTY的终端IO设置unsigned int (*poll)(struct tty_struct *, struct file *,struct poll_table_struct *); // 查询TTY的状态int (*hangup)(struct tty_struct *tty); // 挂起TTY的方法/** The following routines are called from below.*/void (*receive_buf)(struct tty_struct *, const unsigned char *cp,char *fp, int count); // 接收数据到TTY缓冲区void (*write_wakeup)(struct tty_struct *); // 唤醒等待写入的进程void (*dcd_change)(struct tty_struct *, unsigned int); // 处理DCD信号变化int (*receive_buf2)(struct tty_struct *, const unsigned char *cp,char *fp, int count); // 备用接收方法struct module *owner; // 所属模块的指针int refcount; // 引用计数,管理行规约的生命周期
};
那么正式来分析tty_read
函数
Linux-4.9.88\Linux-4.9.88\drivers\tty\tty_io.cstatic ssize_t tty_read(struct file *file, char __user *buf, size_t count,loff_t *ppos)
{int i; // 用于存储读取的字节数struct inode *inode = file_inode(file); // 获取与文件关联的inode结构体struct tty_struct *tty = file_tty(file); // 获取与文件关联的tty结构体struct tty_ldisc *ld; // 指向当前行规约的指针// 检查TTY设备的状态,确保设备没有故障if (tty_paranoia_check(tty, inode, "tty_read"))return -EIO; // 如果有错误,返回输入输出错误if (!tty || tty_io_error(tty))return -EIO; // 如果TTY为空或发生IO错误,返回输入输出错误/* 我们希望等待行规约完成此时的操作 */ld = tty_ldisc_ref_wait(tty); // 获取行规约的引用并等待if (!ld)return hung_up_tty_read(file, buf, count, ppos); // 如果行规约为空,处理挂起的TTY读取if (ld->ops->read) // 检查是否有读取操作i = ld->ops->read(tty, file, buf, count); // 调用行规约的读取方法elsei = -EIO; // 如果没有读取操作,返回输入输出错误tty_ldisc_deref(ld); // 释放行规约的引用if (i > 0) // 如果读取成功tty_update_time(&inode->i_atime); // 更新文件的访问时间return i; // 返回读取的字节数或错误代码
}
其中调用i = ld->ops->read(tty, file, buf, count)
行规约的读取方法,这个方法是什么???可以去看\Linux-4.9.88\drivers\tty\n_tty.c:
\Linux-4.9.88\drivers\tty\n_tty.c
static struct tty_ldisc_ops n_tty_ops = {.magic = TTY_LDISC_MAGIC,.name = "n_tty",.open = n_tty_open,.close = n_tty_close,.flush_buffer = n_tty_flush_buffer,.read = n_tty_read,.write = n_tty_write,.ioctl = n_tty_ioctl,.set_termios = n_tty_set_termios,.poll = n_tty_poll,.receive_buf = n_tty_receive_buf,.write_wakeup = n_tty_write_wakeup,.receive_buf2 = n_tty_receive_buf2,
};
也就是调用到了n_tty_read
:
\Linux-4.9.88\Linux-4.9.88\drivers\tty\n_tty.c
static ssize_t n_tty_read(struct tty_struct *tty, struct file *file,unsigned char __user *buf, size_t nr)
{struct n_tty_data *ldata = tty->disc_data; // 获取TTY设备的线性数据结构unsigned char __user *b = buf; // 用户空间缓冲区指针DEFINE_WAIT_FUNC(wait, woken_wake_function); // 定义等待函数int c;int minimum, time;ssize_t retval = 0; // 返回值初始化为0long timeout;int packet;size_t tail;c = job_control(tty, file); // 控制当前作业的状态if (c < 0)return c; // 如果返回值小于0,表示错误,直接返回/** Internal serialization of reads.*/if (file->f_flags & O_NONBLOCK) { // 检查文件是否为非阻塞模式if (!mutex_trylock(&ldata->atomic_read_lock)) // 尝试获取锁return -EAGAIN; // 如果获取失败,返回重试错误} else {if (mutex_lock_interruptible(&ldata->atomic_read_lock)) // 获取锁,可能会被中断return -ERESTARTSYS; // 被中断返回错误}down_read(&tty->termios_rwsem); // 获取读取信号量minimum = time = 0; // 最小字符和时间初始化timeout = MAX_SCHEDULE_TIMEOUT; // 超时设为最大值if (!ldata->icanon) { // 检查是否为非规范模式minimum = MIN_CHAR(tty); // 获取最小字符数if (minimum) {time = (HZ / 10) * TIME_CHAR(tty); // 计算超时时间} else {timeout = (HZ / 10) * TIME_CHAR(tty);minimum = 1; // 如果没有最小字符,设置为1}}packet = tty->packet; // 获取数据包状态tail = ldata->read_tail; // 获取当前读取尾部位置add_wait_queue(&tty->read_wait, &wait); // 添加等待队列以便调度while (nr) { // 循环直到没有读取的字节/* First test for status change. */if (packet && tty->link->ctrl_status) { // 检查控制状态unsigned char cs;if (b != buf)break; // 如果已经读过数据,退出循环spin_lock_irq(&tty->link->ctrl_lock); // 加锁控制状态cs = tty->link->ctrl_status; // 获取控制状态tty->link->ctrl_status = 0; // 清除状态spin_unlock_irq(&tty->link->ctrl_lock); // 解锁if (put_user(cs, b)) { // 将控制状态写入用户缓冲区retval = -EFAULT; // 如果失败,返回错误break;}b++; // 移动缓冲区指针nr--; // 减少待读取字节数break;}if (!input_available_p(tty, 0)) { // 检查输入是否可用up_read(&tty->termios_rwsem); // 解锁信号量tty_buffer_flush_work(tty->port); // 刷新TTY缓冲区down_read(&tty->termios_rwsem); // 再次获取信号量if (!input_available_p(tty, 0)) { // 如果仍然不可用if (test_bit(TTY_OTHER_CLOSED, &tty->flags)) { // 检查其他TTY是否关闭retval = -EIO; // 返回输入输出错误break;}if (tty_hung_up_p(file)) // 检查文件是否挂起break;if (!timeout) // 检查超时break;if (file->f_flags & O_NONBLOCK) { // 非阻塞模式retval = -EAGAIN; // 返回重试错误break;}if (signal_pending(current)) { // 检查当前进程是否有信号retval = -ERESTARTSYS; // 返回被中断错误break;}up_read(&tty->termios_rwsem); // 解锁信号量timeout = wait_woken(&wait, TASK_INTERRUPTIBLE, timeout); // 等待被唤醒down_read(&tty->termios_rwsem); // 再次获取信号量continue; // 继续循环}}if (ldata->icanon && !L_EXTPROC(tty)) { // 检查是否为规范模式retval = canon_copy_from_read_buf(tty, &b, &nr); // 从读取缓冲区拷贝数据if (retval)break; // 如果出错,退出} else {int uncopied;/* Deal with packet mode. */if (packet && b == buf) { // 检查数据包模式if (put_user(TIOCPKT_DATA, b)) { // 写入数据包标识retval = -EFAULT; // 如果失败,返回错误break;}b++; // 移动缓冲区指针nr--; // 减少待读取字节数}uncopied = copy_from_read_buf(tty, &b, &nr); // 从读取缓冲区拷贝数据uncopied += copy_from_read_buf(tty, &b, &nr); // 再次拷贝数据if (uncopied) {retval = -EFAULT; // 如果出错,返回错误break;}}n_tty_check_unthrottle(tty); // 检查并解除限流if (b - buf >= minimum) // 检查是否已读取到最小字节数break;if (time) // 如果设置了超时时间timeout = time; // 更新超时时间}if (tail != ldata->read_tail) // 检查读取尾部是否改变n_tty_kick_worker(tty); // 触发工作线程up_read(&tty->termios_rwsem); // 解锁信号量remove_wait_queue(&tty->read_wait, &wait); // 从等待队列中移除mutex_unlock(&ldata->atomic_read_lock); // 解锁互斥量if (b - buf) // 如果有数据被读取retval = b - buf; // 更新返回值为实际读取字节数return retval; // 返回读取字节数或错误码
}
retval = canon_copy_from_read_buf(tty, &b, &nr)
,无非就是阻塞等待行规有数据,然后唤醒调用该函数从读取缓冲区拷贝数据。
4.数据源头:中断
文件:drivers\tty\serial\imx.c
📎imx.c
函数:imx_rxint
数据流的起点是硬件的中断处理。
- 中断注册:
serial_imx_probe
函数中通过devm_request_irq
注册了中断处理函数(如imx_rxint
)。- 接收中断(RX)用于处理UART接收到的数据;发送中断(TX)则负责处理发送缓冲区的空闲信号。
- 中断处理逻辑:
- 中断触发后,
imx_rxint
从硬件寄存器中读取数据,并通过tty_insert_flip_char
或tty_insert_flip_string
将数据填入TTY缓冲区。 - 使用
tty_schedule_flip
将缓冲区中的数据传递给行规程。
- 中断触发后,
- 效率优化:
- 为了避免频繁的中断触发,通常结合DMA或FIFO机制处理大批量数据。
- 中断处理函数需要尽可能简短高效,以避免阻塞其他中断的处理。
在之前讲解imx.c的probe函数中,就有个注册中断处理函数,有中断,才能将数据传给行规,行规有数据了,才能唤醒休眠等待的read:
static int serial_imx_probe(struct platform_device *pdev)
{if (txirq > 0) {ret = devm_request_irq(&pdev->dev, rxirq, imx_rxint, 0,dev_name(&pdev->dev), sport); //这个是读中断if (ret) {dev_err(&pdev->dev, "failed to request rx irq: %d\n",ret);return ret;}ret = devm_request_irq(&pdev->dev, txirq, imx_txint, 0,dev_name(&pdev->dev), sport); //这个是写中断if (ret) {dev_err(&pdev->dev, "failed to request tx irq: %d\n",ret);return ret;}//............
}
省略的不相关的,想要具体了解的话可以看之前的关于注册过程的分析。其中读中断处理函数就是imx_rxint
:
static irqreturn_t imx_rxint(int irq, void *dev_id)
{struct imx_port *sport = dev_id; // 获取设备结构unsigned int rx, flg, ignored = 0; // rx: 接收到的数据, flg: 状态标志, ignored: 忽略的错误计数struct tty_port *port = &sport->port.state->port; // 获取 tty_port 结构unsigned long flags, temp; // 用于保存状态和临时变量spin_lock_irqsave(&sport->port.lock, flags); // 加锁并保存当前中断状态while (readl(sport->port.membase + USR2) & USR2_RDR) { // 检查是否有接收数据flg = TTY_NORMAL; // 默认状态标志sport->port.icount.rx++; // 接收计数增加rx = readl(sport->port.membase + URXD0); // 从接收数据寄存器读取数据temp = readl(sport->port.membase + USR2); // 读取状态寄存器if (temp & USR2_BRCD) { // 检查是否为中断信号writel(USR2_BRCD, sport->port.membase + USR2); // 清除中断信号if (uart_handle_break(&sport->port)) // 处理断开的连接continue; // 如果处理成功,继续下一个循环}if (uart_handle_sysrq_char(&sport->port, (unsigned char)rx)) // 检查是否为系统请求字符continue; // 如果处理成功,继续下一个循环if (unlikely(rx & URXD_ERR)) { // 检查是否有错误if (rx & URXD_BRK) // 检查是否是断开信号sport->port.icount.brk++; // 增加断开计数else if (rx & URXD_PRERR) // 检查是否是奇偶校验错误sport->port.icount.parity++; // 增加奇偶校验错误计数else if (rx & URXD_FRMERR) // 检查是否是帧错误sport->port.icount.frame++; // 增加帧错误计数if (rx & URXD_OVRRUN) // 检查是否是溢出错误sport->port.icount.overrun++; // 增加溢出错误计数if (rx & sport->port.ignore_status_mask) { // 如果错误被忽略if (++ignored > 100) // 如果忽略计数超过 100goto out; // 跳转到出错处理continue; // 继续下一个循环}rx &= (sport->port.read_status_mask | 0xFF); // 清除不相关的状态位if (rx & URXD_BRK) // 如果是断开信号flg = TTY_BREAK; // 设置状态标志为断开else if (rx & URXD_PRERR) // 如果是奇偶校验错误flg = TTY_PARITY; // 设置状态标志为奇偶校验else if (rx & URXD_FRMERR) // 如果是帧错误flg = TTY_FRAME; // 设置状态标志为帧错误if (rx & URXD_OVRRUN) // 如果是溢出错误flg = TTY_OVERRUN; // 设置状态标志为溢出}#ifdef SUPPORT_SYSRQsport->port.sysrq = 0; // 重置系统请求标志
#endif// 检查是否需要忽略读取if (sport->port.ignore_status_mask & URXD_DUMMY_READ)goto out; // 跳转到出错处理// 将接收到的字符插入到 tty 缓冲区if (tty_insert_flip_char(port, rx, flg) == 0) sport->port.icount.buf_overrun++; // 如果缓冲区溢出,增加计数}out:spin_unlock_irqrestore(&sport->port.lock, flags); // 解锁并恢复中断状态tty_flip_buffer_push(port); // 将缓冲区中的数据推送到 ttyreturn IRQ_HANDLED; // 返回中断处理成功
}
末尾不就调用到了tty_flip_buffer_push
,将缓冲区中的数据推送到 tty:
\Linux-4.9.88\drivers\tty\tty_buffer.c
void tty_flip_buffer_push(struct tty_port *port)
{tty_schedule_flip(port);
}void tty_schedule_flip(struct tty_port *port)
{struct tty_bufhead *buf = &port->buf;/* paired w/ acquire in flush_to_ldisc(); ensures* flush_to_ldisc() sees buffer data.*/smp_store_release(&buf->tail->commit, buf->tail->used);queue_work(system_unbound_wq, &buf->work); //采用工作队列的方式,唤醒等待的read
}