【Linux驱动-快速回顾】一次性快速回顾TTY体系知识点(新手友好)

我将遵循一条严格的“问题驱动”和“演进”的逻辑线索来构建整个TTY知识体系。每引入一个新概念,都是为了解决前一个阶段出现的问题。这样,你不仅能知道“是什么”,更能深刻理解“为什么是这样设计的”。


第〇阶段:最原始的需求

  • 需求:一个程序(比如bash shell)需要从一个外部设备(比如一个物理键盘和显示器终端)读取用户的键盘输入,并向该设备输出字符。
  • 最直接的想法:让程序直接访问硬件I/O端口或内存地址来操作这个设备(例如,一个串口UART芯片)。
  • 立刻出现的问题
    1. 不具备可移植性:程序代码写死了针对特定硬件的操作。如果换一种串口芯片,程序就要重写。
    2. 缺乏并发管理:如果两个程序想同时访问同一个串口,硬件访问会产生冲突和混乱。
    3. 应用层负担过重:程序需要自己处理非常底层的细节,比如字节流中的退格、Ctrl+C等特殊字符,这部分逻辑在每个需要交互的程序中都会重复。

为了解决这些问题,Linux内核引入了第一个抽象层。


第一阶段:引入驱动程序,封装硬件差异

  • 解决问题:硬件访问的不可移植性和并发管理问题(上述问题1和2)。

  • 解决方案TTY驱动 (TTY Driver)

  • 逻辑推演

    1. 内核提供一个中间层,这个中间层专门负责和硬件打交道。这个中间层就是“驱动程序”。
    2. 驱动程序将复杂的硬件操作(读写寄存器、处理中断)封装起来。
    3. 为了让所有应用程序都能用同一种方式访问它,内核将这个驱动程序具象化为一个标准的文件。在Linux中,就是字符设备文件(例如 /dev/ttyS0)。
    4. 现在,应用程序不再直接访问硬件,而是通过标准的open(), read(), write()系统调用来操作这个设备文件。内核的虚拟文件系统(VFS)会将这些操作最终路由到对应的TTY驱动程序。
    5. 驱动程序内部实现了open, close, write等操作函数(在struct tty_operations中定义),当VFS收到请求时,就调用这些函数。例如,write()操作会调用驱动的write()函数,该函数再将数据写入硬件。
    6. 当硬件接收到数据时(比如用户敲击键盘),会产生一个硬件中断。驱动的中断服务程序(ISR)被执行,它从硬件读取数据。
  • 此时的状态

    • 优点:应用程序与硬件解耦,实现了可移植性。内核通过文件系统解决了并发访问问题。
    • 遗留的致命问题:应用程序从/dev/ttyS0 read()到的是最原始的字节流。如果用户输入hello然后按了退格键,程序会读到'h', 'e', 'l', 'l', 'o', 0x08。程序必须自己解释0x08是退格符,并处理自己的缓冲区。如果用户按了Ctrl+C,程序会读到0x03这个字节,它必须自己判断这是个中断信号,然后终止自己。这仍然是巨大的负担(上述问题3)。

第二阶段:引入行规程,处理终端语义

  • 解决问题:应用程序需要自行处理复杂的终端编辑和控制信号的负担。

  • 解决方案行规程 (Line Discipline, ldisc)

  • 逻辑推演

    1. 我们发现,对退格、Ctrl+C等字符的处理逻辑,对于所有交互式程序(bash, python解释器等)来说都是共通的。这段通用逻辑不应该放在应用程序里,也不应该放在驱动里(因为驱动应该只关心硬件数据收发),而应该放在一个独立的、可重用的“处理层”。
    2. 这个处理层被设计出来,并被插入到“用户-文件系统接口”和“TTY驱动”之间。这就是行规程
    3. 现在,数据流变成了:
      • 写操作 (应用 -> 硬件):应用write() -> VFS -> 行规程 -> TTY驱动 -> 硬件。
      • 读操作 (硬件 -> 应用):硬件 -> TTY驱动中断 -> 行规程 -> VFS -> 应用read()
    4. 行规程的核心功能
      • 上行数据处理 (硬件->应用):当驱动把从硬件收到的原始字节流(如'h', 'e', 'l', 'l', 'o', 0x08)交给行规程时,行规程内部维护一个行缓冲区。它看到0x08(退格),就会从自己的缓冲区里删除最后一个字符’o’。只有当它看到行结束符(回车\r或换行\n)时,才会把缓冲区里最终正确的内容(“hell”)打包好,通知VFS数据已就绪,让等待read()的应用程序返回。这个过程称为规范模式 (Canonical Mode)
      • 信号生成:当行规程收到特定的控制字符,如0x03 (Ctrl+C),它不会将这个字符传递给应用程序。相反,它会向与这个终端关联的前台进程组发送一个SIGINT信号。
      • 下行数据处理 (应用->硬件):应用程序write()一个换行符\n,行规程可以根据配置,自动将其转换为回车+换行 (\r\n),以兼容某些老式终端设备。
    5. 引入模式切换:我们意识到,并非所有程序都需要这种行编辑。比如vim编辑器需要立即知道用户按了j键来下移光标,不能等用户按回车。文件传输程序更是需要原始的二进制数据流。因此,行规程必须支持不同的工作模式。
      • 规范模式 (Canonical Mode):默认模式,提供行编辑、行缓冲。为交互式Shell设计。
      • 非规范/原始模式 (Non-canonical/Raw Mode):不进行任何处理,收到任何字符都立即将其传递给应用程序。为编辑器、数据传输等程序设计。
  • 此时的状态

    • 优点:我们有了一个非常强大的分层模型。驱动负责硬件,行规程负责终端语义,应用负责自身逻辑。各司其职。
    • 遗留的问题:谁来管理这一切?当open("/dev/ttyS0")时,谁来创建和关联一个TTY驱动实例和一个行规程实例?当数据在它们之间流动时,谁来调用正确的函数进行传递?这些胶水代码和管理逻辑放在哪里?

我们成功地将系统拆分成了两个功能明确的组件:
1. TTY驱动:一个纯粹的硬件适配器。
2. 行规程:一个纯粹的终端语义处理器。

这种分离非常优雅,但也立即产生了一系列新的、尖锐的工程问题。
这些组件虽然各自强大,但它们是相互隔离的,无法自行协同工作。
  • 新出现的核心矛盾
    1. 状态管理问题:假设用户A打开了 /dev/ttyS0,希望以9600波特率、规范模式工作。同时,用户B打开了 /dev/ttyS1,希望以115200波特率、原始模式工作。这两个会话的状态(波特率、模式、行编辑缓冲区)是完全独立的。这个**“会话状态”**应该存储在哪里?

      • 不能存在TTY驱动里:驱动代码是为一类设备(如所有8250串口)服务的,是无状态的、可共享的。
      • 不能存在行规程模块里:行规程代码(如N_TTY)也是通用的,它本身不知道自己正在为哪个具体的会话服务。
      • 因此,必须有一个专门的数据结构,用于表示和存储每一个被打开的、活动的TTY会话的独特上下文
    2. 生命周期管理问题:谁来负责在用户open()设备时,创建上述的“会话状态”数据结构?又由谁在用户close()设备时,销毁它以释放资源?

    3. “接线员”问题 (Orchestration):现在我们有了驱动和行规程,但它们之间如何通信?

      • 当驱动的中断程序收到一个字节,它如何知道应该把它交给哪一个行规程实例去处理?(ttyS0ttyS1的行规程实例是不同的)。
      • 当应用程序调用write()时,数据流应该是“应用 -> 行规程 -> 驱动”。这个调用链是如何建立的?驱动本身不应该知道行规程的存在,否则就破坏了我们辛苦建立的解耦。必须有一个“中间人”来引导数据正确流转。

这些问题指向同一个答案:我们需要一个更高层次的框架层,来管理这些组件的生命周期、维护它们的会话状态,并充当它们之间的“总调度台”。这个框架层,就是 TTY核心 (TTY Core)


第三阶段:引入TTY核心,作为会话管理者和系统框架

  • 解决问题:解决第二阶段分离组件后产生的状态管理、生命周期管理和协同工作的问题。

  • 解决方案:引入 TTY核心 (TTY Core),它不是一个具体的“功能”模块,而是整个子系统的骨架和大脑

  • 逻辑推演

    1. 解决状态管理:struct tty_struct的诞生
      为了解决每个TTY会话需要独立状态的问题,TTY核心定义了整个体系中最重要的一个数据结构:struct tty_struct

      • 它不是代码,而是一个数据容器,是一个活动TTY连接的化身
      • 每当一个TTY设备被open(),TTY核心就会为其分配一个tty_struct实例。
      • 这个结构体内部包含了指向所有相关组件的指针,例如 *driver(指向为它服务的TTY驱动)、*ldisc(指向为它服务的行规程实例),以及最重要的termios结构体(保存着该会话的所有配置)。
      • tty_struct完美地解决了状态存储问题,它就是那个“会话状态”的载体。
    2. 解决生命周期和“接线员”问题:TTY核心的职责
      有了tty_struct这个蓝图,TTY核心的职责就变得清晰了:它就是操作这个结构体、并基于它进行调度的总控程序。

      • 统一的入口:用户的open(), read(), write()等系统调用,不再直接路由到驱动,而是全部先进入TTY核心提供的标准函数,如tty_open(), tty_write()
      • tty_open():TTY核心分配tty_struct,然后根据打开的设备号,找到之前已经向核心“注册”过的对应TTY驱动,并将驱动信息填入tty_struct。同时,它会挂接一个默认的行规程(通常是N_TTY)到这个tty_struct上。至此,一个完整的、可工作的会话实例被动态组装完毕。
      • 在数据流中充当调度者
        • 写操作 (应用 -> 硬件):应用程序的write()调用进入TTY核心的tty_write()。TTY核心查看传入的tty_struct,找到其关联的行规程,调用行规程的write函数进行数据处理。然后,它再从tty_struct中找到关联的TTY驱动,调用驱动的write函数,将处理后的数据交给驱动去发送。整个数据流被完美地串联起来。
        • 读操作 (硬件 -> 应用):为了使驱动中断处理尽可能快和安全,TTY核心提供了一个缓冲机制tty_flip_buffer。驱动的中断程序只需把从硬件读到的原始数据塞进这个缓冲区,然后调用tty_flip_buffer_push()通知TTY核心即可。TTY核心会在稍后安全的时间点(软中断上下文),从缓冲区取出数据,查看是哪个tty_struct的数据,然后调用其行规程的receive_buf函数进行处理。

第四阶段:配置与扩展

我们已经有了一个完整的体系,现在需要让它变得可配置和适应更现代的场景。

  1. 如何配置这个体系? -> termios 结构

    • 问题:用户程序如何切换规范/非规范模式?如何设置串口的波特率、数据位、停止位?
    • 解决方案:TTY核心在tty_struct中维护一个名为termios的配置结构体。这个结构体包含了所有可配置的参数(输入标志c_iflag、输出标志c_oflag、控制标志c_cflag、本地标志c_lflag等)。
    • 机制:用户空间程序通过tcgetattr()tcsetattr()这两个系统调用来读取和修改内核中的termios结构。当tcsetattr()被调用时,TTY核心会通知行规程和TTY驱动:“配置变了,请更新状态”。驱动就会根据新的termios值去设置硬件寄存器(例如,修改波特率)。
  2. 如何应用于非物理串口? -> 伪终端 (PTY)

    • 问题:我们在图形界面下的终端窗口(如gnome-terminal)或者通过ssh远程登录,背后并没有一个物理的/dev/ttyS0设备。但我们使用的bash仍然能正常工作,Ctrl+C也有效。这是如何实现的?
    • 解决方案伪终端 (Pseudo-Terminal, PTY)
    • 机制:PTY是纯软件模拟的TTY设备。它总是成对出现:
      • 主设备端 (Master, PTM):例如 /dev/ptmx。它由“宿主”程序持有,比如gnome-terminalsshd服务。
      • 从设备端 (Slave, PTS):例如 /dev/pts/0。它被分配给子进程,比如bash
    • 逻辑闭环
      1. gnome-terminal启动,打开PTM。
      2. 它创建子进程来运行bash,并将PTS (/dev/pts/0)作为bash的标准输入、输出、错误。
      3. bash的角度看,它操作的/dev/pts/0和一个真实的TTY设备毫无区别。因此,整个TTY核心、行规程(N_TTY)都会被挂接上来,Ctrl+C、行编辑等功能完全复用。
      4. 数据流:
        • 用户在gnome-terminal窗口敲键盘 -> gnome-terminal程序把按键信息write()到PTM -> 内核将这些数据转发给配对的PTS -> bash从它的标准输入(即PTS)read()到数据。
        • bash执行ls命令,输出结果 -> bash把结果write()到它的标准输出(即PTS)-> 内核将数据转发给配对的PTM -> gnome-terminal程序从PTM read()到数据,然后将其绘制到窗口上。
    • 结论:PTY巧妙地将非硬件的I/O源(图形窗口、网络套接字)接入了强大的TTY体系,实现了最大程度的代码和逻辑复用。

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

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

相关文章

深入浅出:让机器听懂世界的耳朵——梅尔频率倒谱系数(MFCCs)

深入浅出:让机器听懂世界的耳朵——梅尔频率倒谱系数(MFCCs) 在人工智能的浪潮中,语音识别、声纹支付、音乐推荐等技术早已融入我们的日常生活。你是否曾好奇,计算机是如何理解并区分各种复杂的声音信号的?…

Ubuntu22.04安装/使用Gazebo时踩的一些坑

首先,本人原本打算安装gazebo11的,因为官方好像不支持ubuntu22.04,所以要通过PPA和ROS2 humble来安装,安装过程跟着教程来的,也就是下面这篇 ubuntu22.04安装gazebo11(ROS2 Humble)-CSDN博客 …

CPT203-Software Engineering: Introduction 介绍

目录 1.专业名词定义 1.1计算机软件的定义 1.2软件系统的定义 1.3软件工程的定义 2.软件的失败与成功 2.1 失败 2.2 成功 3.软件开发 Professional software development 3.1 分类 3.2 专业软件开发 professional software development 3.3专业软件开发产品特性 3.4…

诊断工程师进阶篇 --- 车载诊断怎么与时俱进?

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 做到欲望极简,了解自己的真实欲望,不受外在潮流的影响,不盲从,不跟风。把自己的精力全部用在自己。一是去掉多余,凡事找规律,基础是诚信;二是…

奥特曼论人工智能、OpenAI与创业

来自Y Combinator的YouTube视频,展示了OpenAI首席执行官萨姆奥特曼分享的深刻见解。他讨论了OpenAI从一个看似疯狂的通用人工智能(AGI)梦想,如何发展成为一个全球性的现象。奥特曼强调了早期决策的关键性、吸引顶尖人才的策略&…

React Ref使用

受控与非受控组件 Ref 1.获取原生dom 类组件中&#xff1a;在componentDidMount方法内使用document.getElementById的方法获取到dom元素 1 目标dom增加ref属性 设置为字符串 <h2 reftitleref></h2>function changeRef(){this.refs.titleref.innerHtml }2 函数组件…

地下管线安全的智能监测先锋:智能标志桩图像监测装置解析​

​在城市与乡村的地下&#xff0c;纵横交错的管线是能源与信息传输的关键通道。但深埋地下的电缆、燃气管道等设施&#xff0c;因难以直观监测&#xff0c;面临施工误挖、自然灾害等风险。传统防护手段力不从心&#xff0c;TLKS-PAZ01 智能标志桩图像监测装置的诞生&#xff0c…

Camera相机人脸识别系列专题分析之十六:人脸特征检测FFD算法之libcvface_api.so数据结构详细注释解析

【关注我&#xff0c;后续持续新增专题博文&#xff0c;谢谢&#xff01;&#xff01;&#xff01;】 上一篇我们讲了&#xff1a; 这一篇我们开始讲&#xff1a; Camera相机人脸识别系列专题分析之十六&#xff1a;人脸特征检测FFD算法之libcvface_api.so数据结构详细注释解析…

【字节跳动】数据挖掘面试题0012:数据分析、数据挖掘、数据建模的区别

文章大纲 数据分析、数据挖掘、数据建模的区别一、核心定义与目标二、技术方法差异三、应用场景对比四、三者的关联与递进关系五、面试应答策略 数据分析、数据挖掘、数据建模的区别 一、核心定义与目标 数据分析&#xff1a; 是对已有的数据进行收集、清洗、整理&#xff0c;并…

预警:病毒 “黑吃黑”,GitHub 开源远控项目暗藏后门

在开源生态蓬勃发展的当下&#xff0c;黑客们也将黑手伸向了代码共享平台。当黑产开发者以为在共享 “行业秘笈” 时&#xff0c;殊不知已经掉入了黑客布置的陷阱 —— 看似方便的后门远程控制源码和游戏作弊外挂源码等 “圈内资源”&#xff0c;实则是植入了恶意代码的投毒诱饵…

Qt中的QProcess类

Qt中的QProcess类 QProcess 是 Qt 框架中用于启动和控制外部进程的类&#xff0c;它属于 QtCore 模块。这个类提供了执行外部程序并与它们交互的功能。 一、主要功能 启动外部程序&#xff1a;可以启动系统上的其他可执行程序进程通信&#xff1a;通过标准输入、输出和错误流…

周任务自动化升级:N8N与多维表格无缝联动全解析

.自动化之言&#xff1a; 在上一篇文章中&#xff0c;我们介绍了如何利用多维表格&#xff08;如飞书多维表格或Notion&#xff09;搭建一个灵活的任务管理系统。现在我们将进一步扩展这个系统&#xff0c;借助 N8N 实现周报的自动汇总与邮件发送&#xff0c;真正实现任务管理…

Go语言的web框架--gin

本章内容&#xff0c;会介绍一下gin的运用&#xff0c;以及gin框架底层的内容&#xff0c;话不多说&#xff0c;开始进入今天的主题吧&#xff01; 一.基本使用 gin框架支持前后端不分离的形式&#xff0c;也就是直接使用模板的形式。 模板是什么&#xff1f; 这里可能有同…

企业为什么需要双因素认证?

从进入互联网时代开始&#xff0c;密码是我们个人日常的重要保护。但是单独的密码保护可能已经不再适应当前的数字化时代。密码已经不再足够安全最近发生的各种安全漏洞让我重新审视网络安全。几行代码可能就导致了全球数以百万的登录凭证被泄露。今天&#xff0c;仅仅周期性地…

Spring Boot + 本地部署大模型实现:优化与性能提升!

在Spring Boot中集成本地部署的大模型&#xff08;如LLaMA、ChatGLM等&#xff09;并进行优化&#xff0c;需要从模型选择、推理加速、资源管理和架构设计等多方面入手。以下是完整的优化方案及实现步骤&#xff1a; 一、核心优化策略 1. 模型量化 目标&#xff1a;减少显存占…

仿mudou库one thread oneloop式并发服务器

前言 我们所要实现的是一个高并发服务器的组件&#xff0c;使服务器的性能更加高效&#xff0c;是一个高并发服务器的组件&#xff0c;并不包含实际的业务。 首先需要先明确我们所要实现的目标是什么 第一点&#xff0c;实现一个高并发的服务器第二点&#xff0c;在服务器的基础…

超详细的私有化安装部署Dify服务以及安装过程中问题处理

一、什么是Dify Dify 是一款开源的大语言模型(LLM) 应用开发平台。它融合了后端即服务&#xff08;Backend as Service&#xff09;和 LLMOps 的理念&#xff0c;使开发者可以快速搭建生产级的生成式 AI 应用。即使你是非技术人员&#xff0c;也能参与到 AI 应用的定义和数据…

国产DSP,QXS320F280049,QXS320F28377D,QXS320F2800137,QXS320F28034

自定义指令集&#xff0c;自研内核架构&#xff0c;基于eclipse自研IDE&#xff0c;工具链&#xff0c;算法库。 根据自研QXS320F280049&#xff0c;做了600W和2KW数字电源方案&#xff0c;1.5KW电机方案&#xff0c;目前已在市场大量投产。 QXS320F290049应用于数字电源&#…

dotnet publish 发布后的项目,例如asp.net core mvc项目如何在ubuntu中运行,并可外部访问

复制到 Ubuntu 上的是使用 Visual Studio 或 dotnet publish 命令生成的 发布后的输出文件&#xff08;publish output&#xff09;&#xff0c;而不是原始项目源代码。在这种情况下&#xff0c;确实没有 .csproj 文件&#xff0c;所以不能直接用 dotnet run 启动。但你可以通过…

Linux多线程(十二)之【生产者消费者模型】

文章目录生产者消费者模型为何要使用生产者消费者模型生产者消费者模型优点基于BlockingQueue的生产者消费者模型BlockingQueueC queue模拟阻塞队列的生产消费模型单线程生产消费模型多线程生产消费模型生产者消费者模型 consumer/productor 321原则(便于记忆) 为何要使用生产…