尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
虚拟串口字节异常:termios行规约如何改写你的数据
如果你在 Linux 下做过虚拟串口调试大概率遇到过这种怪事往串口里写一个字节对端读到的却不是同一个字节甚至某个字节直接凭空消失。我最近用虚拟串口跑一套进程通信的模拟测试时就撞上了这个坑——发送端明明写的是0x0A接收端却读出来0x0D 0x0A等我把数据换成0x11和0x13更离谱对端连个影子都没收到。一开始我以为是代码逻辑出错了后来排查了大半天才发现凶手根本不在应用层而是内核里串口行规约line discipline悄悄改写了我发出的每一个字节。更让我意外的是这个问题几乎只会在虚拟串口上让你抓瞎。真实板子上的物理串口虽然也会走到同一套机制但大家用现成的串口助手或者调试工具时工具早就把底层参数设置好了所以一直没暴露。而虚拟串口一旦配在普通终端模式下默认的字符转换规则全开数据在传输过程中就会被加料减料甚至弹回来。这篇文章就用一次完整的排查过程来聊聊这个特殊字节问题把这层窗户纸捅破顺便把这几年在虚拟串口上踩过的坑一起收个尾。1. 特殊字节在虚拟串口中变脸的典型症状1.1 这些特殊字节具体指哪些很多时候我们把串口当一根透明管道用觉得写入的数据一定会原封不动出现在对端。实际上串口设备在内核中并不透明。根据字符值不同内核会做换行转换、流控解释、回显处理、缓冲整理等一堆操作。最常见的变脸字节有这么几个字节名称未被关闭时的典型结局0x0ALF 换行变成0x0D 0x0A两个字节0x0DCR 回车变成0x0A或被直接忽略0x11XON被解释为恢复发送从数据流中消失0x13XOFF被解释为暂停发送从数据流中消失0x00NUL部分模式下被丢弃或标志着特殊处理开始0x1ASUB部分驱动把它当文件结束符直接截断后续数据0x7FDEL规范模式下触发删除前一个字符的操作这些字节在文本终端场景中都有明确语义比如0x0A就是换行、0x11/0x13就是软件流控。用在虚拟串口上时它们依然保留了这套语义所以你的原始数据一旦包含这些值就会在传输过程中被解读而不是被搬运。这事对二进制协议是致命的。你辛辛苦苦拼好的帧结构里可能就藏着若干个0x0A或者0x13如果不做处理对端解析出来的数据完全是另一帧。更麻烦的是这种错误不是每次都出现和你发送的字节内容强相关代码 review 根本看不出来日志打得再漂亮也没用。1.2 数据从 write 到 read 经历了什么要理解为什么会变得先顺着一条数据走一圈。应用层调用write(fd, buf, len)后数据进入内核的 tty 子系统。tty 驱动收到数据后会先把原始字节交给线路规程line discipline处理线路规程就是内核里做字符解释和缓冲的那个模块。默认的n_tty线路规程非常热情它是为了兼容上世纪的电传打字机终端而设计的会把收到的数据当作文本流重新排版。随后数据被处理后放入 tty 的接收缓冲区等待用户态read()。你读到的已经是被改过的数据。写入方向同理应用层写入的数据先经过线路规程的输出侧处理再发送到对端。也就是说无论发还是收字节都要经过一整套终端规范的洗礼。用一个生活化的类比来看你给朋友寄了一张写着数字 10 的卡片快递途中有人自作主张把它重新排版成了1 和 0两个数字然后装在两个信封里。送到朋友手上时卡片内容已经不是原来的意思了。虚拟串口里发生的事情就是这么离谱——内核里好心的行规约一直在帮你重新排版。2. termios 行规约改写你字节的隐形规则2.1 termios 关键标志位速查要压制这套自动排版行为就得操作串口的termios配置。每个 tty 设备都关联一个struct termios里面用标志位控制各种转换逻辑。我整理了一份和特殊字节强相关的标志位速查表这些就是我在排查时逐一核对的东西标志位所在字段开启后的行为影响范围OPOSTc_oflag启用输出后处理让下面的 ONLCR 等规则生效输出侧ONLCRc_oflag把 LF(0x0A) 转成 CRLF(0x0D 0x0A)输出侧OCRNLc_oflag把 CR(0x0D) 转成 LF(0x0A)输出侧ICRNLc_iflag把输入侧 CR 转成 LF 后再送给应用输入侧IGNCRc_iflag直接忽略输入侧的 CR输入侧INLCRc_iflag把输入侧 LF 转成 CR输入侧IXONc_iflag把输入侧 XON/XOFF 字节当作流控指令输入侧IXOFFc_iflag由内核主动向对端发 XOFF 控制流量输入侧ICANONc_lflag进入规范模式数据按行缓冲遇到换行才上报输入侧ECHOc_lflag输入的字节回显给发送方双向ISIGc_lflag把 CtrlC、CtrlZ 等字节转成信号输入侧IEXTENc_lflag启用扩展终端功能CtrlV 等组合键被解释输入侧这几个标志位就是字节变种的元凶。比如你写0x0AOPOST和ONLCR同时开着内核会在输出时把单个0x0A补成0x0D 0x0A。反过来对端在收数据时如果ICRNL没关0x0D又会被转成0x0A。两边各转一次整个数据流里 CR 和 LF 的分布必然乱套。2.2 为什么真实串口没踩过、虚拟串口却频繁翻车很多人在真实串口上调试时用的工具是无意中替大家完成了 termios 初始化。不管是串口助手还是各种命令行工具在打开设备后都会把 fd 设置成原始模式raw mode也就是把上面这些会改数据的标志位全都关掉。你不需要关心这个过程工具替你做了所以从来不会察觉到 system 底下有这一层。自己写程序就不一样了。你用open(/dev/pts/3, O_RDWR | O_NOCTTY)直接打开设备此时内核给这个 tty 的是默认的终端配置大部分情况下OPOST、ICANON、ECHO都是开的。程序里没有任何处理就把数据写进去自然就踩坑了。虚拟串口和真实串口的底层机制完全相同都是经过 tty 架构所以问题在虚拟串口上会原原本本暴露出来反而成了观察这个机制的绝佳场景。还有一个容易忽略的点虚拟串口的两端都挂在一个 pty 上主从两端的行为会互相影响。主端往从端写数据时从端的 termios 输入规则生效从端往主端写时主端的 termios 输出规则生效。排查时不能只盯着一端的配置两端的 termios 都得检查。3. 完整排查案例0x0A 变 0x0D 0x0A 的定位与修复3.1 现场复现与抓包定位前阵子做模拟项目 X 的通信模块测试进程 A 和进程 B 之间通过虚拟串口交换二进制帧。为了快速搭建环境我用 socat 创建了一对虚拟串口socat -d -d pty,raw,echo0,link/tmp/ttyV0 pty,raw,echo0,link/tmp/ttyV1进程 A 打开/tmp/ttyV0进程 B 打开/tmp/ttyV1。A 发送一个帧帧内容是01 02 0A 03B 那边收到的却是01 02 0D 0A 03多了一个0D。我第一反应是发送端的组帧逻辑出了问题于是把发送缓冲区的原始数据用 hex 方式打出来确认 A 确实老老实实写了01 02 0A 03。接着怀疑接收端的解析函数过滤太激进又在 B 的读取点直接打印bytearray依然看到多出来的0D。数据从 A 的 write 到 B 的 read中间隔着一整条内核链路靠应用层日志已经定位不到问题了只能上工具。我重新用 socat 建了一对不经过任何程序转发的裸串口然后用socat -v观察数据在中间传输的原始形态socat -v -d -d pty,raw,echo0 pty,raw,echo0socat -v会把两个方向的数据以 hexdump 形式打印到屏幕上。这一打印就看出问题了发送侧写入的0A在经过内核输出处理后被扩展成0D 0A两个字节出现在链路上接收端再把它们收进去。证据链已经完整问题不在应用层而在内核 tty 的字符转换。3.2 修复方案raw 模式配置代码定位到方向后修复就很简单把两端 tty 设备都配置成 raw 模式让内核别再做任何字符转换。Linux 下最常用的是cfmakeraw()但要注意它只是把 termios 改好还需要用tcsetattr()写回设备。下面是当时在进程 A 侧使用的初始化代码#include fcntl.h #include termios.h #include unistd.h int fd open(/tmp/ttyV0, O_RDWR | O_NOCTTY); if (fd 0) { perror(open); return -1; } struct termios tio; tcgetattr(fd, tio); cfmakeraw(tio); tcsetattr(fd, TCSANOW, tio);如果你不想用 C在同一个代码里用 Python 也是常见的做法。Python 的tty.setraw()底层就是调cfmakeraw()import os import termios import tty fd os.open(/tmp/ttyV0, os.O_RDWR | os.O_NOCTTY) old termios.tcgetattr(fd) tty.setraw(fd) os.write(fd, bytes([0x01, 0x02, 0x0A, 0x03])) termios.tcsetattr(fd, termios.TCSANOW, old) os.close(fd)Python 方案里有个细节值得提一下tty.setraw()之后如果直接os.close()配置会随文件描述符释放而失效。如果后续还要用同一个 fd 做其他操作最好把旧的 termios 存起来在退出时恢复。做一次性临时测试时这个细节无所谓但在一个长时间运行的服务进程里随手恢复原配置是必要的。如果你只是临时想在命令行上验证不用写代码一条stty直接设置stty -F /tmp/ttyV0 raw -echoraw会关闭OPOST、ICANON、ECHO、ISIG、IXON等一票标志-echo确保回显关闭。不过要注意stty设置的是当前打开时关联的配置程序里如果用open()重新打开 fd配置依然会生效因为 tty 配置存储在设备上不是只跟某个 fd 绑定。3.3 修复前后的对比验证修复后我在同样的场景下重新跑了一遍。发送端写01 02 0A 03接收端收到的数据就是01 02 0A 03不再多出任何字节。再测试高风险的11 13组合数据也能原样到达因为IXON在 raw 模式下已经被关闭内核不再把这些字节当场流控信号吞掉。我还做了一组对照测试一端保持 raw一端不设置。结果很有意思数据从 raw 端写到非 raw 端输入侧该做的转换还会发生照样会变。这说明 raw 配置必须双方都设只要有一端没设透过这端的规则就会被改写。4. 虚拟串口搭建方案与选型要点4.1 三种常用搭建方式排查完这个 bug虚拟串口的选型值得单独总结一下。我自己常用三种方案各有各的适用场景。第一种就是上面用的socat把两个 pty 连接起来模拟一条串口线。socat 的 pty 支持指定链路名方便固定路径socat -d -d pty,raw,echo0,link/tmp/ttyV0 pty,raw,echo0,link/tmp/ttyV1这个方案适合在两个独立进程之间建立全双工通道。两边都打开各自的 link 路径互相收发完全不知道底层是虚拟的。缺点是多一个 socat 进程不适用于需要把虚拟串口直接嵌入自己程序内部的场景。第二种是用 Python 的pty模块在程序内直接创建 pty pairimport os import pty import tty master, slave pty.openpty() tty.setraw(master) tty.setraw(slave) # master_fd 和 slave_fd 可以在同一进程内使用 os.write(master, b\x01\x02\x0a\x03) data os.read(slave, 1024)这种方式适合在测试代码中快速搭建虚拟串口对不需要外部依赖一个脚本就能跑起来。注意os.read()在数据不足时会阻塞所以测试时最好确保对端有数据写入或者用非阻塞模式。第三种是直接使用内核提供的 ptyp 机制打开/dev/ptmx获取主端 fdptsname拿到从端路径再从主端打开从端。这其实和 Python pty 模块底层一模一样适合 C 场景。socat 和 Python 方案底层都是 pty只是封装层级不同。4.2 选型对比与坑点方案适用场景外部依赖维护成本socat模拟完整串口线路多进程或多机通信需安装 socat低进程退出即拆除Python pty 模块写自动化测试、快速验证协议无Python 标准库内置低但要注意 setraw直接 open /dev/ptmx嵌入式模拟、C 程序集成无稍高需自己管理 termios选 socat 时一个容易忽略的问题link指定的符号链接在 socat 退出后会残留下一次启动时如果 socat 检测到 link 已被占用它会报错或者自动覆盖取决于link参数和pty的组合方式。为了省事启动前先删除旧的 link 文件是稳妥做法。Python pty 方案要注意tty.setraw()的顺序。两个 fd 都要设置而且必须在收发数据之前设置。如果只在 master 端设置slave 端的 ICANON 没关从 slave 端读数据时就会面临必须等换行符才返回的尴尬局面。我看到过不少测试脚本因为漏掉对 slave 的 raw 设置导致读操作永远阻塞。自己 open/dev/ptmx时还要注意ptsname返回的从端设备在你掌握主端 fd 之前是不会稳定存在的需要先保证主端 fd 有效再去打开从端路径。另外主从两端在程序退出时如果没有做好 close另一端可能收到 SIGHUP 信号造成意外的进程终止。处理这一套时信号处理函数里最好把 fd 的关闭逻辑理顺。5. 常见问题速查与实操心得5.1 虚拟串口字节异常速查表配合这次排查我把这些年积累的字节异常问题整理成了一个速查表。遇到类似表现时直接对照这个表格就能少走弯路现象大概率原因解决方式写入 LF 读端变成 CRLFc_oflag中OPOST、ONLCR开启raw 模式或关闭这两个标志写入 CR 读端变成 LFc_iflag中ICRNL开启关闭ICRNL写入 CR 直接消失c_iflag中IGNCR开启关闭IGNCR数据要凑满一行才上报c_lflag中ICANON开启关闭ICANON并配置VMIN/VTIME发出去的数据弹回发送端c_lflag中ECHO开启关闭ECHO0x11/0x13 数据丢失c_iflag中IXON开启关闭IXON和IXOFF程序收到 SIGINT/SIGQUIT 异常退出c_lflag中ISIG开启关闭ISIG数据在链路层正常但应用层读不全线路规程缓冲区或VMIN配置不合适合理配置VMIN和VTIME这些问题的共性就是内核对你的字节做了语义解读。只要坚持一条原则——在虚拟串口上传输二进制数据两端的 fd 都必须设置成 raw 模式——就能避免绝大多数坑。5.2 写给后来者的几条注意事项第一不要依赖调试工具帮你隐藏问题。很多串口助手的透传模式会自动把 fd 设置成 raw所以你在工具里测数据一切正常一旦换成自己的程序就翻车。开发阶段建议先写一个最小测试脚本直接 open 设备把底层配置固定下来再往上加协议层。第二排查字节问题不要盯着日志里的字符串。字符串输出会把不可见字符全部隐藏成转义形式根本看不出来 CR 和 LF 的区别。任何时候都要用二进制视角比如xxd -c 16 -g 1或者hexdump -C特别是在串口数据异常时第一件事就是把两端收发数据存成二进制文件做对比。第三注意 socat 命令里的raw参数。如果不加rawsocat 创建的 pty 会保留默认行规约上面的坑一个不少。创建时就带上raw,echo0能省掉后面很多麻烦。不过即便 socat 创建时设了 raw这只会影响 socat 自己持有的 pty fd 的默认配置另一个进程打开同一链路时依然需要自行设置。第四如果程序里用了现成的串口库别理所当然地以为库帮你做了 raw 配置。有的库确实会在打开时自动设置原始模式有的库只设置波特率其他参数完全沿用系统默认值。用之前最好读一下库源码里对 termios 的处理或者直接在打开设备后调用一次tcgetattr看看OPOST和ICANON到底是 1 还是 0。最后想分享一个我自己现在一直沿用的习惯在任何涉及虚拟串口的测试程序入口处都直接调用cfmakeraw或者tty.setraw不再纠结默认配置是什么。这个习惯养成的过程就是被0x0A变成0x0D 0x0A的坑教育出来的。每次写串口通读代码先把配置打出来再跑数据收发能省下的时间远比多敲几行配置代码多得多。如果你在虚拟串口上碰到看起来完全不可理喻的字节错乱先检查 termios大概率问题就出在这里。
RELATED

相关推荐

医疗大模型落地实践:数字医生与AI协诊的人机共智架构全拆解

医疗大模型落地实践:数字医生与AI协诊的人机共智架构全拆解

最近医疗圈里“大模型 医疗”的热度高得离谱,尤其是各家医院开始试点“数字医生”之类的AI助手。我因为工作关系,接触过某头部三甲妇产儿童专科医院(下面统一用M医院代称)的数字医生项目,前后跟了将近一年&#xff0c…

📅 2026/10/17 11:59:37
Java数字签名与证书全解析:从JCA原理到踩坑实战

Java数字签名与证书全解析:从JCA原理到踩坑实战

1. 先说清楚:数字签名和证书到底在解决什么问题搞Java开发的人,早晚都会碰到数字签名和证书这两个概念。不管你是做接口对接、单点登录、支付回调验签,还是处理电子合同、软件发布包校验,背后都离不开这套机制。我第一次被问到&qu…

📅 2026/10/17 11:59:37
用Swing/AWT实现坦克大战:Java面向对象与游戏循环实战

用Swing/AWT实现坦克大战:Java面向对象与游戏循环实战

坦克大战这个项目,我在不同阶段做过三遍。第一遍是为了完成一门课程设计,当时抱着“能跑就行”的心态,代码堆在几个类里,勉强能动但极其脆弱;第二遍尝试了重构,把坦克、子弹、墙壁分别抽象出来,…

📅 2026/10/17 11:59:37
MORE NEWS

更多资讯

📰

YOLOv8+PyTorch实战:实时人脸口罩检测系统从零搭建指南

先说结论:这个项目我前前后后折腾了大半个月,从最初的模型选型、数据集整理,到最后的实时监控界面跑通,中间踩了不少坑,也积累了一些实测数据。如果你正准备做一个人脸口罩检测系统,并且打算用 YOLOv8 PyT…

📰

用Dify搭建团队知识问答应用:知识库、工作流与踩坑实录

1. 为什么数据科学每日总结会写到Dify1.1 先说一个每天都会撞上的场景做数据科学工作,最难的不是建模,也不是写分析报告,而是每天要应付大量重复的"再问一遍"。我写了几十天的每日总结之后发现,团队里问得最频繁的问题往…

📰

C#矢量绘图引擎:拖动缩放不闪烁、可测试、可答辩

简介:本资源是一个基于C# Windows Forms开发的交互式图形绘制工具,面向C#初学者与GUI编程学习者,解决图形绘制、拖拽、缩放等核心交互功能的实践难题。项目完整实现了矩形、圆形、菱形等基础图形的创建、实时渲染及自由变换,涵盖S…

📰

Python面向对象编程(一)

前言 面向对象编程(Object-Oriented Programming,OOP)的教科书通常会列出三大特性:封装、继承、多态。但把这套从 Java、C 课本里搬来的说法直接套到 Python 上,很容易写出别扭的代码,因为 Python 的 OOP 有…

📰

多AI助手协同下的凭证泄露与RCE风险:从SSO到命令执行的完整链路

1. 事件还原:一个标题引发的技术圈震荡“离谱,OpenAI被Opus 5黑了!”——这个标题在技术社区刷屏的时候,我正蹲在工位上调试一个CI流水线。第一反应是:又是标题党吧?但点进去看了几眼讨论,发现事…

📰

AI大模型当复习搭子:从费曼讲解到错题归因的完整备考闭环

备考这种事,最磨人的往往不是知识本身,而是那种一个人闷头刷题、错了还不知道错在哪儿的无力感。我也是在准备一门硬核专业认证时才真正把AI大模型当成“复习搭子”来用——不是临时查资料,而是让它从讲解、出题、判分到归因,全程…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬