linux tty 终端

linux tty 终端

  • 一、终端
  • 二、终端的类型
    • 1、虚拟终端
    • 2、伪终端 pty
    • 3、串口终端
    • 4、控制终端
  • 三、内核中的终端
    • 1、open
    • 2、write
    • 3、read
      • 1. 读取数据
      • 2. 数据返回
      • 3. 常见的 termios
  • 四、编程实验

作者: baron
个人博客: baron-z.cn

基于网站原因csdn上面的图片有压缩, 如果不是很清楚, 可以到个人博客看原图.

    终端是计算机系统中重要的交互工具,从最初的机械式电传打字机到现代的虚拟终端,它承载了计算机发展的许多关键技术。本文将从终端的历史谈起,介绍其发展过程中的重要阶段,接着分析终端驱动的实现原理,最后通过代码实现一个简单的终端接口,帮助读者从理论到实践更好地理解终端的工作原理。希望这篇文章能为对终端技术感兴趣的读者提供一些启发和参考。

一、终端

    终端是一种输入输出设备, 常见的终端有显示器, 键盘, 控制台等. 在老一代的机器中呢, 控制台和终端是分开的, 如下所示.

在这里插入图片描述

    而随着时间的推移控制台已经和显示器合二为一了. 以上描述的是广义的终端设备. 而在计算机内部对于线程来说终端就是 /dev/tty* 和 /dev/consol 所描述的设备节点.

    1. 广义上来说终端是输入输出设备, 显示器键盘
    1. 狭义上来说终端就是 dev/tty* 和 /dev/consol 所描述的设备节点

    为什么要反复强调这个, 因为理解这两条概念对理解终端很重要.理解了这个我们在描述终端的时候, 就可以根据上下文来确定对应场景的终端表示的是什么.

二、终端的类型

1、虚拟终端

    在计算机刚发展的时候, 计算机是非常昂贵的资源, 并不是所有人都能用得起的, 而 cpu 的性能往往是过剩的. 于是人们就想一台计算机怎么个多个人用. 于是就在一台计算机上接入了多组显示器+键盘.

在这里插入图片描述

    linux 为每一个用户(一组显示器+键盘)创建一个终端, 让他们能和计算机交互, 这个终端就是虚拟终端(/dev/tty1-/dev/tty63). 注意了, 每一个虚拟终端(狭义终端)都对应一组物理的外设(广义终端).

在这里插入图片描述

    如图有三个用户, 他们分别使用了三个虚拟终端. 每个用户都可以通过 crtl + alt + fn 切换到对应的虚拟终端(/dev/ttyn). 这个并不是一对一的关系, 也可以多个用户使用一个虚拟终端, 这取决于驱动是否支持这个功能.

2、伪终端 pty

    随着计算机网络的发展,用户希望能够远程访问计算机系统. 这种需求催生了远程登录协议和工具,如 Telnet 和 rlogin,它们允许用户通过网络连接到远程计算机.为了支持这些远程访问协议,系统需要一种机制来模拟本地终端,以便远程用户能够像在本地终端一样与计算机进行交互. 于是伪终端 pty 就诞生了. 对应的设备节点 /dev/pts/*

    伪终端 PTY 的机制如下所示. 伪终端分为两个部分 master 和 slave. 他们分别对接进程. 两个进程互相不知道对方的存在.他们面对的都是伪终端 pty 设备. 于是他们只需要打开设备, 读设备, 写设备, 关闭设备就行了. 这里 “进程1” 写一个数据, 数据会从主设备发送到从设备. 然后对接从设备的 “进程2” 就能读到数据. 同理也可以反过来.

在这里插入图片描述

    终端模拟器就是利用伪终端机制实现的的应用程序. 常见的有 xshell, mobaxterm 等等. 下图展示了终端模拟器的通讯流程.

在这里插入图片描述

3、串口终端

    这个也是因为历史原因导致的, 在很久很久以前. 我们的终端设备显示器和键盘, 都是通过串口来传输数据的. 因此内核的 tty_drvier 就接入了 uart_driver 用来接收串口数据. 这个东西也就一直保留到了现在. 对应的设备节点就是 /dev/ttyS*.

在这里插入图片描述

4、控制终端

    控制终端就是当前进程持有的终端 /dev/tty. linux 中进程都可以和终端(狭义的终端/dev/tty*)进行交互. 而同一时刻只能有一个进程和控制终端(/dev/tty) 进行交互. 这个持有控制终端的进程叫做控制进程. 可以通过 lsof /dev/tty查看当前的控制进程.

lsof /dev/tty
COMMAND  PID      USER   FD   TYPE   DEVICE  SIZE/OFF NODE  NAME
zsh     1532      baron  10u   CHR    5,0      0t0     9   /dev/tty

    可以看出当前的控制进程为 zsh. 而 /dev/tty 关联的终端就是控制终端. 通过命令 tty就可以知道当前被关联的终端.

tty
/dev/pts/6

    当前的控制终端就是伪终端 /dev/pts/6. 前面描述的虚拟终端(/dev/tty1)伪终端(/dev/pty/*)串口终端(/dev/ttyS0). 都能成为控制终端.谁被控制进程持有的 /dev/tty关联, 谁就是控制终端. 下面伪代码体现了关联的过程.

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <errno.h>
#include <sys/ioctl.h>int main() {int fd_ttyS0, fd_tty;// 打开 /dev/ttyS0 设备文件fd_ttyS0 = open("/dev/ttyS0", O_RDWR | O_NOCTTY);if (fd_ttyS0 < 0) {perror("Failed to open /dev/ttyS0");exit(EXIT_FAILURE);}// 打开 /dev/tty 获取当前控制终端fd_tty = open("/dev/tty", O_RDONLY);if (fd_tty < 0) {perror("Failed to open /dev/tty");close(fd_ttyS0);exit(EXIT_FAILURE);}// 断开当前控制终端if (ioctl(fd_tty, TIOCNOTTY) < 0) {perror("Failed to disconnect from current control terminal");close(fd_tty);close(fd_ttyS0);exit(EXIT_FAILURE);}// 设置 /dev/ttyS0 作为新的控制终端if (ioctl(fd_ttyS0, TIOCSCTTY) < 0) {perror("Failed to set /dev/ttyS0 as control terminal");close(fd_tty);close(fd_ttyS0);exit(EXIT_FAILURE);}// 关闭文件描述符close(fd_tty);close(fd_ttyS0);// 执行其他操作...return 0;
}

三、内核中的终端

    虚拟终端(/dev/tty1)伪终端(/dev/pty/*)串口终端(/dev/ttyS0). 等终端在内核中都是通过 tty_driver 注册进内核的.只是对应的配置略有差别如下图所示.

在这里插入图片描述

    终端就是通过 tty_register_driver注册进内核的字符设备, 如下图所示为 tty driver 的链路结构. 图中蓝色的双向箭头线路, 就是数据读写的流程. 整个过程可以分为三个部分, 字符设备(cdev) --> 线路规程(line discipline) --> tty 驱动(tty core) --> 外部. 他们在内核中的代码位置 driver/tty/*.

在这里插入图片描述
    终端的读写并不是直接与外部设备相连的。在 TTY 体系中,TTY driver(TTY 核心)负责与外部设备的交互,这个外部设备可能是串口设备, 也可能是虚拟终端(VT)对应的显示器.

    写操作先把数据写入到线路规程, 等到合适的时机在发送出去(例如按下enter键). 然而读操作并不是直接从外部设备读取数据,而是从线路规程(line discipline)的缓冲区 (n_tty_data) 中读取。这个缓冲区的数据来源于 TTY driver >的 port->buf 缓冲区,它通过 flush_to_ldisc函数将数据写入线路规程的缓冲区 n_tty_data. 而TTY driver 的 port->buf 缓冲区又是由 VT/UART/PTY 等驱动通过 tty_insert_flip_char/tty_insert_flip_string这样的接口主动写入的.

    因此, 当用户空间调用 read 时, 实际上是检查线路规程的缓冲区 n_tty_data 中是否有数据。如果有数据, 则返回给用户;如果没有数据, 则会根据 read 的调用参数决定是否阻塞等待数据的到来.

    有些串口驱动只要有数据会不断地向线路规程的缓冲区写入数据, 这也解释了为什么在某些情况下, 打开串口设备时可能会出现一堆历史数据. 即使你没有主动读取数据, 驱动程序仍然在后台填充缓冲区, 导致一旦你打开设备并读取时, 这些积累的数据会一股脑地被读取出来.

    为了避免这种情况,应用程序在打开串口后,可以使用 tcflush命令清空线路规程的缓冲区,从而确保读取到的数据是最新的,而不是之前积累的旧数据.

#include <termios.h>
#include <unistd.h>int fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY);// 清空输入缓冲区
tcflush(fd, TCIFLUSH);// 清空输出缓冲区
tcflush(fd, TCOFLUSH);// 清空输入和输出缓冲区
tcflush(fd, TCIOFLUSH);

    那么为什么不直接读写要在中间加个线路规程呢, 因为终端是机器和人打交道的, 在传统的终端场景中, 人们在输入时可能会出现错误, 比如输入错字,然后希望通过退格键删除错误的输入. 或者在输入完一整行数据后按下回车键, 才将数据发送出去。线路规程就负责在用户按下回车键之前,对输入的数据进行缓冲和预处理,例如处理退格键、编辑操作、信号等. 具体来说,线路规程会对输入进行以下处理:

  • 字符回显:当用户输入字符时,线路规程会将字符回显在屏幕上。
  • 编辑操作:支持退格键删除错误输入的字符,以及其他基本的行编辑功能。
  • 信号处理:处理诸如中断信号(如 Ctrl-C)等特殊输入。
  • 行缓冲:在用户按下回车键之前,输入的数据被缓冲起来,直到用户按下回车键,线路规程才将整行数据传递给应用程序

1、open

    应用程序 open 某个 tty 设备节点, 会调用到对应的 tty_open, 主要调用流程如下.

tty_open()-->
/*********************************************  tty_alloc_file ******************************************************************/tty_alloc_file()--> // 分配一个 tty_file_private 并设置回调 file->private_data = priv;tty=tty_open_current_tty()--> // 尝试获取设备对应的控制终端if(!tty){tty = tty_driver_lookup_tty()--> // 通过设备号返回对应的 tty_driver 和对应的索引下标 indexif(!tty){retval = tty_reopen(tty); // 增加引用计数 tty->count++}else{tty_init_dev()-->alloc_tty_struct()--> // 创建 tty_struct 并初始化tty_driver_install_tty()--> // 设置默认的线路规程为 N_tty, 设置默认的 termios, 增加引用计数 tty->count++tty->port = driver->ports[idx];--> // 获取 porttty->port->itty = tty; // tty_ldisc_setup()--> // 回调 n_tty_open,初始化线路规程的 n_tty_data 等变量}}retval = tty->ops->open(tty, filp) // 最后回调 tty_driver 的 open 函数.

    大体分为两种情况:

    1. 第一次打开终端会创建一个 tty_struct设置默认的线路规程 N_tty , 增加 tty_structcount计数, 初始化 N_tty线路规程, 最后回调 tty->ops->open(tty, filp)
    1. 第二次打开则直接获取第一次创建的 tty_struct然后增加其引用计数. 然后回调 tty->ops->open(tty, filp)

    tty_structtty_open时创建的, 他作为中间结构, 在 tty 操作过程中, 通过这个结构体可以找到任何我们想要的 tty 相关的数据结构. 当 count引用计数为 0 时被销毁.
    线路规程中 N_TTY中的 N 是 NEW 的意思, 新的线路规程, 当前默认的线路规程. 对应的还有 O_TTY, 也就是 old tty. pty 设备用的就是 o_tty.

2、write

    应用程序调用 write 会回调内核的 tty_wrte, 调用流程如下.

tty_write()-->ld = tty_ldisc_ref_wait(tty)--> // 返回线路规程结构体 tty_ldiscdo_tty_write(ld->ops->write, tty, file, buf, count)-->ld->ops->write(tty, file, tty->write_buf, size)-->n_tty_write(tty, file, tty->write_buf, size)-->tty->ops->write(tty, b, nr) // 最后回调 tty_driver 中注册的回调函数.

     write 的流程相对比较简单, 基本就是一路把 buffer 传下去. 果设置了 OPOST,输出字符会经过处理后再发送。如果未设置 OPOST,则字符会原样发送到终端或设备,不经过任何特殊处理. 常见的处理包括将换行符 \n自动转换为回车符 \r和换行符 \n组合(CR + NL),这是大多数终端默认的行为.

3、read

    应用程序调用 read 会回调内核的 tty_read. read 的过程总共分为两步, 读取数据数据返回.

1. 读取数据

这段代码比较长, 分段描述.

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;unsigned char __user *b = buf;DEFINE_WAIT_FUNC(wait, woken_wake_function); // 定义等待队列函数.int c;int minimum, time;ssize_t retval = 0;long timeout;int packet;size_t tail;c = job_control(tty, file);if (c < 0)return c;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); // 获取要读的最小字符数 tty->termios.c_cc[VMIN]if (minimum) {time = (HZ / 10) * TIME_CHAR(tty); // 计算字符间的时间间隔超时设置, 0.1s * tty->termios.c_cc[VTIME]if (time)ldata->minimum_to_wake = 1; // 设置最小唤醒读操作的字符数, 只要读取到 1 个字符或者时间超时就可以唤醒读取操作。else if (!waitqueue_active(&tty->read_wait) || // 如果没有进程在等待读取数据,或者之前设置的唤醒条件比当前的 VMIN 要求更严格(即,minimum_to_wake 比 minimum 大),那么就将唤醒的字符数设定为 minimum(根据 VMIN 来决定),以便能够及时唤醒等待的进程。(ldata->minimum_to_wake > minimum))ldata->minimum_to_wake = minimum;} else {// 计算字符间的时间间隔超时设置, 0.1s * tty->termios.c_cc[VTIME]timeout = (HZ / 10) * TIME_CHAR(tty);ldata->minimum_to_wake = minimum = 1;}}packet = tty->packet;tail = ldata->read_tail;// 将 wait 加入等待队列.add_wait_queue(&tty->read_wait, &wait);

    这段代码会根据原始模式还是规范模式设置唤醒条件

  • 原始模式:按行缓冲输入,并处理特殊字符,适合需要行编辑的场景(如命令行)
  • 规范模式:不缓冲输入,字符立即传递,无特殊字符处理,适合实时响应输入的应用.

    默认情况下无论是原始模式还是规范模式都是一样的设置. 即

timeout = (HZ / 10) * TIME_CHAR(tty);
ldata->minimum_to_wake = 1;
  • 表示只要有超过一个字符未被读取则唤醒读取操作.
  • TIME_CHAR(tty) 的值一般是 0 , 字符间的时间间隔超时为 0
  • 原始模式的 minimum = tty->termios.c_cc[VMIN], 而规范模式 minimum = 0
    while (nr) {/* First test for status change. */if (packet && tty->link->ctrl_status) { // 如果设置了pacet且发生链路状态的变化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 (tty_put_user(tty, cs, b++)) { // 返回链路状态retval = -EFAULT;b--;break;}nr--;break;}

    在 packet 模式下,如果链路状态发生了变化,则将链路状态 tty->link->ctrl_status(例如:连接状态、信号状态等)作为数据返回给用户进程。这个用的也比较少,可以不用太过关注.

        if (((minimum - (b - buf)) < ldata->minimum_to_wake) &&((minimum - (b - buf)) >= 1))ldata->minimum_to_wake = (minimum - (b - buf));

    这段代码的目的是根据当前已经读取的数据量来调整最小唤醒字符数 minimum_to_wake. 如果进程已经读取了一些数据,且剩余要读取的数据量 (minimum - (b - buf)少于最初设定的唤醒条件,代码会更新 minimum_to_wake,确保进程在读取到剩余数据后立即唤醒.

        if (!input_available_p(tty, 0)) { // 如果没有足够的数据up_read(&tty->termios_rwsem);tty_buffer_flush_work(tty->port); // 回调 flush_to_ldisc 刷新线路规程的数据.down_read(&tty->termios_rwsem);if (!input_available_p(tty, 0)) { // 刷新玩之后如果数据还是不够// 远程已经关闭 ttyif (test_bit(TTY_OTHER_CLOSED, &tty->flags)) {retval = -EIO;break;}// tty 被挂起退出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;}}

    input_available_p()这个函数用来检测是否还有数据可以读. 返回1 表示有足够的数据可以读, 返回 0 则表示没有足够的数据

  • 规范模式下:只有当 canon_headread_tail不相等时(即有完整的一行数据),才返回有数据可读。
  • 原始模式下:检查已经提交的数据量是否满足读取的最小要求,通常是 MIN_CHAR(tty),如果满足,则返回有数据可读。
  • 进程读取数据的时候如果 n_tty_data 中没有数据,进程会在 TTY 关闭、挂起、超时、非阻塞模式 或 信号中断时退出读取操作.
  • 如果 n_tty_data 数据不足且允许阻塞,进程会进入休眠等待数据或超时.
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 (tty_put_user(tty, TIOCPKT_DATA, b++)) {retval = -EFAULT;b--;break;}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);// 读取的数数据大于等于 minimum 返回if (b - buf >= minimum)break;if (time)timeout = time;} // while(nr) {

    根据原始模式还是规范模式调用不同的接口读取数据, 读取的数据大于等于 minimum返回.

    if (tail != ldata->read_tail)n_tty_kick_worker(tty);up_read(&tty->termios_rwsem);remove_wait_queue(&tty->read_wait, &wait);if (!waitqueue_active(&tty->read_wait))ldata->minimum_to_wake = minimum;mutex_unlock(&ldata->atomic_read_lock);if (b - buf)retval = b - buf;return retval;
}

这段代码是返回时的处理不再赘述.

2. 数据返回

    从前面可以知道数据返回就是填充数据到 n_tty_data , 并且唤醒前面的 wait 进程返回数据. 对应接口 tty_flip_buffer_push.

tty_flip_buffer_push()-->tty_schedule_flip(port)-->queue_work(system_unbound_wq, &buf->work)-->flush_to_ldisc()-->receive_buf(tty, head, count)-->disc->ops->receive_buf2()--> // n_tty 默认使用这个n_tty_receive_buf_common(tty, cp, fp, count, 1); --> __receive_buf()-->

    在 __receive_buf 里面会根据 icanon 和 real_raw 决定数据拷贝的方式, 他们由 termios三个参数设置, 组合如下.

ICANONECHOISIG模式描述行为备注
111规范模式,启用回显和信号处理输入按行缓冲,输入的字符显示在屏幕上,支持信号处理(如 Ctrl+C 发送 SIGINT 信号中断程序)。规范模式 (ICANON = 1, REAL_RAW = 0)
110规范模式,启用回显,禁用信号处理输入按行缓冲,字符显示在屏幕上,但 Ctrl+C 等信号无法中断程序。规范模式 (ICANON = 1, REAL_RAW = 0)
101规范模式,禁用回显,启用信号处理输入按行缓冲,输入的字符不会显示在屏幕上,但支持 Ctrl+C 等信号中断功能。规范模式 (ICANON = 1, REAL_RAW = 0)
100规范模式,禁用回显和信号处理输入按行缓冲,输入的字符不会显示在屏幕上,且 Ctrl+C 等信号无效。规范模式 (ICANON = 1, REAL_RAW = 0)
011非规范模式,启用回显和信号处理输入字符立即传递,不需要按回车,字符显示在屏幕上,且支持 Ctrl+C 信号中断功能。非规范模式 (ICANON = 0, REAL_RAW = 0)
010非规范模式,启用回显,禁用信号处理输入字符立即传递,字符显示在屏幕上,但信号处理功能(如 Ctrl+C)无效。非规范模式 (ICANON = 0, REAL_RAW = 0)
001非规范模式,禁用回显,启用信号处理输入字符立即传递,字符不显示在屏幕上,但支持 Ctrl+C 信号中断功能。部分原始模式 (ICANON = 0, REAL_RAW = 0)
000非规范模式,禁用回显和信号处理(原始模式)输入字符立即传递,字符不显示在屏幕上,且 Ctrl+C 等信号无效。原始模式 (ICANON = 0, REAL_RAW = 1)

3. 常见的 termios

    termios 就是控制线路规程的规则, 一般驱动设置一个默认值 tty_std_termios, 应用可以根据自己的需要做修改.

标志类别标志名含义描述
输入标志 (`c_iflag`)ICRNL将回车 (`CR`) 转换为换行 (`NL`)。
IXON启用软件流控制(XON/XOFF)。
输出标志 (`c_oflag`) OPOST 启用输出处理。关闭时输出字符直接发送,不做任何处理。 ONLCR 将输出的换行符 (`NL`) 转换为回车符和换行符组合 (`CR-NL`)。 控制标志 (`c_cflag`) B38400 设置波特率为 38400 bps。 CS8 设置字符大小为 8 位。 CREAD 启用接收器,允许接收数据。 HUPCL 在最后一个进程关闭时挂断连接,释放串口。 本地标志 (`c_lflag`) ISIG 启用信号处理(例如 `Ctrl+C` 发送 `SIGINT` 信号)。 ICANON 启用规范模式(行缓冲模式),输入会被缓冲直到按下换行符或其他结束符。 ECHO 启用回显,用户输入的字符会显示在屏幕上。 ECHOE 退格时删除前一个字符,并在屏幕上同步删除。 ECHOK 在输入 `KILL` 字符时删除整行,并显示换行。 ECHOCTL 回显控制字符(例如 `Ctrl+C` 显示为 `^C`)。 ECHOKE 删除整行时回显该操作。 扩展标志 (`c_lflag`) IEXTEN 启用实现的扩展输入处理(例如终端的额外特性)。

四、编程实验

    有了前面的知识, 我们就来自己写一个我们的终端驱动程序. 这个终端的功能很简单就是输入字符立即传递, 但是需要能够响应 crtl + c 等操作. 因此就是 ICANON = 0, REAL_RAW = 0(非规范模式). 驱动程序如下.

#include <linux/module.h>
#include <linux/tty.h>
#include <linux/tty_flip.h>
#include <linux/init.h>
#include <linux/tty_driver.h>
#include <linux/slab.h>#define DRIVER_NAME "my_tty"  // 驱动程序名称
#define TTY_DRIVER_NAME "ttyMY"  // tty 设备名, 即 dev/ttyMY0 设备节点名称
#define TTY_MINORS 1  // 设备的次设备号数量struct my_tty {struct tty_port *port;  // tty 端口结构体struct tty_driver *driver;  // tty 驱动结构体struct device *dev;
};static struct my_tty *my_tty;static int my_tty_open(struct tty_struct *tty, struct file *filp)
{printk("my_tty: Device opened\n");return tty_port_open(tty->port, tty, filp);
}static void my_tty_close(struct tty_struct *tty, struct file *filp)
{printk("my_tty: Device close, tty->count: %d\n", tty->count);tty_port_close(tty->port, tty, filp); 
}static int my_tty_write(struct tty_struct *tty, const unsigned char *buf, int count)
{int i;for (i = 0; i < count; i++) {printk("%c", buf[i]);tty_insert_flip_char(tty->port, buf[i], TTY_NORMAL);  // 插入字符到 tty 缓存}printk("\n%s count:%d \n", __func__, count);if (count)tty_flip_buffer_push(tty->port);  // 上报数据return count;  // 返回写入的数据字节数
}// 判断 tty 是否有空间写入数据
static int my_tty_write_room(struct tty_struct *tty)
{if (tty->stopped)return 0;return tty_buffer_space_avail(tty->port);  // 返回当前 tty 缓冲区剩余空间
}static const struct tty_operations my_tty_ops = {.open = my_tty_open,.close = my_tty_close,.write = my_tty_write,.write_room = my_tty_write_room,
};static const struct tty_port_operations null_ops = { }; // 注册 tty 驱动
static int my_tty_driver_register(void)
{int retval;my_tty = kzalloc(sizeof(*my_tty), GFP_KERNEL);if (!my_tty)return -ENOMEM;my_tty->port = kzalloc(sizeof(*my_tty->port), GFP_KERNEL);if (!my_tty->port) {kfree(my_tty);return -ENOMEM;}my_tty->driver = tty_alloc_driver(TTY_MINORS, TTY_DRIVER_RESET_TERMIOS);if (!my_tty->driver) {kfree(my_tty->port);kfree(my_tty);return -ENOMEM;}// 初始化 tty 驱动的各个字段my_tty->driver->driver_name = DRIVER_NAME;  // 驱动名称my_tty->driver->name = TTY_DRIVER_NAME;  // tty 设备前缀my_tty->driver->major = 0;  // 动态分配主设备号my_tty->driver->type = TTY_DRIVER_TYPE_SERIAL;  // 设置为串行设备my_tty->driver->subtype = SERIAL_TYPE_NORMAL;  // 常规串行类型my_tty->driver->flags = TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV;  // 设置设备标志my_tty->driver->init_termios = tty_std_termios;  // 设置初始终端参数my_tty->driver->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL;  // 9600 波特率,8 位数据位,启用接收,挂起时关闭线路,忽略调制解调器状态my_tty->driver->init_termios.c_ispeed = 9600;  // 输入速度my_tty->driver->init_termios.c_ospeed = 9600;  // 输出速度tty_set_operations(my_tty->driver, &my_tty_ops);  // 设置驱动的操作集// 注册 tty 驱动retval = tty_register_driver(my_tty->driver);if (retval) {put_tty_driver(my_tty->driver);kfree(my_tty->port);kfree(my_tty);return retval;}// 初始化 tty 端口tty_port_init(my_tty->port);my_tty->port->ops = &null_ops;  // 使用空操作集my_tty->driver->ports[0] = my_tty->port;  // 将端口绑定到驱动my_tty->dev = tty_register_device(my_tty->driver, 0, NULL);if (IS_ERR(my_tty->dev)) {retval = PTR_ERR(my_tty->dev);tty_unregister_driver(my_tty->driver);put_tty_driver(my_tty->driver);kfree(my_tty->port);kfree(my_tty);return retval;}printk("My TTY driver loaded. Major number: %d\n", my_tty->driver->major);return 0;
}static void my_tty_driver_unregister(void)
{tty_unregister_driver(my_tty->driver);put_tty_driver(my_tty->driver);tty_port_destroy(my_tty->port);kfree(my_tty->port);kfree(my_tty);printk("My TTY driver unloaded.\n");
}static int __init my_tty_init(void)
{return my_tty_driver_register();
}static void __exit my_tty_exit(void)
{my_tty_driver_unregister();
}module_init(my_tty_init);
module_exit(my_tty_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("baron"); 
MODULE_DESCRIPTION("a simple tty driver");

应用程序编写如下:

#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
#include <termios.h>#define DEVICE "/dev/ttyMY0"
#define BUFFER_SIZE 1024int main() {int fd;char write_buf[] = "Hello, this is a test from user space!";char read_buf[BUFFER_SIZE];int bytes_written, bytes_read;struct termios tty;// 打开设备文件fd = open(DEVICE, O_RDWR | O_NOCTTY);if (fd < 0) {perror("Failed to open the device");return errno;}printf("Opened device: %s\n", DEVICE);/********************************* 关键代码设置 termios ********************************/// 获取当前终端设置if (tcgetattr(fd, &tty) != 0) {perror("Failed to get terminal attributes");close(fd);return errno;}// 设置为非规范模式,保留回显和信号处理tty.c_lflag &= ~ICANON;     // 关闭规范模式(ICANON = 0)tty.c_lflag &= ~ECHO;       // 关闭回显tty.c_lflag |= ISIG;        // 启用信号处理(Ctrl+C)// 应用新的终端设置if (tcsetattr(fd, TCSANOW, &tty) != 0) {perror("Failed to set terminal attributes");close(fd);return errno;}
/********************************* 关键代码设置 termios end ********************************/// 向设备写入数据printf("Writing to the device: %s\n", write_buf);bytes_written = write(fd, write_buf, strlen(write_buf));if (bytes_written < 0) {perror("Failed to write to the device");close(fd);return errno;}printf("Wrote %d bytes to the device\n", bytes_written);// 读取设备数据printf("Reading from the device...\n");bytes_read = read(fd, read_buf, BUFFER_SIZE);if (bytes_read < 0) {perror("Failed to read from the device");close(fd);return errno;}printf("Read %d bytes: %.*s\n", bytes_read, bytes_read, read_buf);// 关闭设备文件close(fd);printf("Closed device: %s\n", DEVICE);return 0;
}

验证结果:

# tty_test
Opened device: /dev/ttyMY0
Writing to the device: Hello, this is a test from user space! // 打印写入数据
Wrote 38 bytes to the device
Reading from the device...
Read 38 bytes: Hello, this is a test from user space! // 打印读出数据
Closed device: /dev/ttyMY0

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/68803.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

在 vscode + cmake + GNU 工具链的基础上配置 JLINK

安装 JLINK JLINK 官网链接 下载安装后找到安装路径下的可执行文件 将此路径添加到环境变量的 Path 中。 创建 JFlash 项目 打开 JFlash&#xff0c;选择新建项目 选择单片机型号 在弹出的窗口中搜索单片机 其他参数根据实际情况填写 新建完成&#xff1a; 接下来设置…

智能建筑时代的核心选择——基于SAIL-RK3576核心板的AI边缘计算网关方案

随着智能建筑技术的不断发展&#xff0c;建筑设备正日益向“智慧化”迈进。传统的建筑管理系统往往依赖中央服务器和云端平台进行数据处理和控制&#xff0c;但在实时监控、安防及能耗管理等关键环节&#xff0c;延迟和数据安全问题依然存在。此外&#xff0c;物联网设备数量激…

python列表如何不重复

python列表不重复的方法&#xff1a; python内置的set&#xff08;&#xff09;方法可以去掉列表里面重复的元素&#xff0c;调用该方法就可以让python列表不重复了 a [23, 15, 15, 56, 89, 89, 56] a set(a) print(a) 运行结果如下&#xff1a;

【Redis】事务的概念及用法

事务的概念及用法 什么是事务事务的操作开启事务&#xff08;MULTI&#xff09;执行事务&#xff08;EXEC&#xff09;中止事务&#xff08;DISCARD&#xff09;为事务提供检查&#xff08;WATCH&#xff09;取消对key的监控&#xff08;UNWATCH&#xff09; 为什么Redis不支持…

两份PDF文档,如何比对差异,快速定位不同之处?

PDF文档比对是通过专门的工具或软件&#xff0c;自动检测两个PDF文件之间的差异&#xff0c;并以可视化的方式展示出来。这些差异可能包括文本内容的修改、图像的变化、表格数据的调整、格式的改变等。比对工具通常会标记出新增、删除或修改的部分&#xff0c;帮助用户快速定位…

Flutter:搜索页,搜索bar封装

view 使用内置的Chip简化布局 import package:chenyanzhenxuan/common/index.dart; import package:ducafe_ui_core/ducafe_ui_core.dart; import package:flutter/material.dart; import package:get/get.dart; import package:tdesign_flutter/tdesign_flutter.dart;import i…

深度学习基础知识

深度学习是人工智能&#xff08;AI&#xff09;和机器学习&#xff08;ML&#xff09;领域的一个重要分支&#xff0c;以下是对深度学习基础知识的归纳&#xff1a; 一、定义与原理 定义&#xff1a;深度学习是一种使计算机能够从经验中学习并以概念层次结构的方式理解世界的机…

【Elasticsearch】腾讯云安装Elasticsearch

Elasticsearch 认识Elasticsearch安装Elasticsearch安装Kibana安装IK分词器分词器的作用是什么&#xff1f;IK分词器有几种模式&#xff1f;IK分词器如何拓展词条&#xff1f;如何停用词条&#xff1f; 认识Elasticsearch Elasticsearch的官方网站如下 Elasticsearch官网 Ela…

Ubuntu 24.04 LTS 通过 docker 安装 nextcloud 搭建个人网盘

准备 Ubuntu 24.04 LTSUbuntu 空闲硬盘挂载Ubuntu 安装 Docker DesktopUbuntu 24.04 LTS 安装 tailscale [我的Ubuntu服务器折腾集](https://blog.csdn.net/jh1513/article/details/145222679。 安装 nextcloud 参考 Ubuntu24.04系统Docker安装NextcloudOnlyoffice _。 更…

ThinkPHP 8的多对多关联

【图书介绍】《ThinkPHP 8高效构建Web应用》-CSDN博客 《2025新书 ThinkPHP 8高效构建Web应用 编程与应用开发丛书 夏磊 清华大学出版社教材书籍 9787302678236 ThinkPHP 8高效构建Web应用》【摘要 书评 试读】- 京东图书 使用VS Code开发ThinkPHP项目-CSDN博客 编程与应用开…

计算机网络 (53)互联网使用的安全协议

一、SSL/TLS协议 概述&#xff1a; SSL&#xff08;Secure Sockets Layer&#xff09;安全套接层和TLS&#xff08;Transport Layer Security&#xff09;传输层安全协议是工作在OSI模型应用层的安全协议。SSL由Netscape于1994年开发&#xff0c;广泛应用于基于万维网的各种网络…

React的应用级框架推荐——Next、Modern、Blitz等,快速搭建React项目

在 React 企业级应用开发中&#xff0c;Next.js、Modern.js 和 Blitz 是三个常见的框架&#xff0c;它们提供了不同的特性和功能&#xff0c;旨在简化开发流程并提高应用的性能和扩展性。以下是它们的详解与比较&#xff1a; Next、Modern、Blitz 1. Next.js Next.js 是由 Ve…

if_yellow_only_restart_upgrading_nodes_with_unassigned_replicas

目录标题 遇事不决&#xff0c;上githubif_yellow_only_restart_upgrading_nodes_with_unassigned_replicas问题分析如何解决并使集群恢复到正常状态1. **检查和分配未分配的副本分片**2. **查看节点日志**3. **检查资源配置**4. **手动升级节点**5. **修改 if_yellow_only_res…

第四十七章 Spring之假如让你来写MVC——闪存管理器篇

Spring源码阅读目录 第一部分——IOC篇 第一章 Spring之最熟悉的陌生人——IOC 第二章 Spring之假如让你来写IOC容器——加载资源篇 第三章 Spring之假如让你来写IOC容器——解析配置文件篇 第四章 Spring之假如让你来写IOC容器——XML配置文件篇 第五章 Spring之假如让你来写…

基于微信小程序高校订餐系统的设计与开发ssm+论文源码调试讲解

第4章 系统设计 一个成功设计的系统在内容上必定是丰富的&#xff0c;在系统外观或系统功能上必定是对用户友好的。所以为了提升系统的价值&#xff0c;吸引更多的访问者访问系统&#xff0c;以及让来访用户可以花费更多时间停留在系统上&#xff0c;则表明该系统设计得比较专…

使用批处理文件清除系统垃圾

第一步&#xff1a;打开记事本&#xff0c;里面的命令如下 echo off echo 正在清理临时文件&#xff0c;请稍候...:: 清理系统临时文件 echo 清理系统临时文件... del /q /f /s "%TEMP%\*.*" del /q /f /s "%WINDIR%\Temp\*.*" rd /s /q "%WINDIR%\T…

Linux——信号量和(环形队列消费者模型)

Linux——线程条件变量&#xff08;同步&#xff09;-CSDN博客 文章目录 目录 文章目录 前言 一、信号量是什么&#xff1f; 二、信号量 1、主要类型 2、操作 3、应用场景 三、信号量函数 1、sem_init 函数 2、sem_wait 函数 3、sem_post 函数 4、sem_destroy 函数 ​​​​​​…

垂直供排水抢险车:守护城市,抗击洪涝|深圳鼎跃

我国面积幅员辽阔&#xff0c;其灾害种类多样&#xff0c;而洪涝灾害是其中最常见的灾害&#xff0c;其容易受强降雨的影响&#xff0c;严重影响人民群众的日常生活。而在洪水肆虐的场景中&#xff0c;快速、高效地排涝和供水是防止次生灾害、保护人民生命财产安全的关键环节。…

Social LSTM:Human Trajectory Prediction in Crowded Spaces | 文献翻译

概要 行人遵循不同轨迹以避免障碍物和容纳同行者。任何在这种场景中巡航的自动驾驶车辆都需要能够遇见行人的未来位置并相应地调整其路线以避免碰撞。轨迹预测问题能够被看作一个顺序生成任务&#xff0c;其中我们对基于行人过去的位置预测其未来的轨迹感兴趣。根据最近RNN&am…

React+AntDesign实现类似Chatgpt交互界面

以下是一个基于React和Ant Design搭建的简单ChatGPT风格前端交互界面代码框架示例&#xff0c;该示例实现了基本的用户输入、发送请求以及展示回复的功能。假设后端有一个模拟接口来处理请求并返回回复。 1. 项目初始化&#xff1a; 确保你已经安装了Node.js和npm。通过以下命…