宜昌市网站建设_网站建设公司_HTML_seo优化
2026/1/3 5:21:46 网站建设 项目流程

从零开始:手把手教你编译与加载虚拟串口驱动

你有没有遇到过这样的情况?想调试一个串口通信协议,却发现笔记本根本没有RS-232接口;或者在CI/CD流水线里跑自动化测试时,因为缺少物理串口设备而卡住?更别提嵌入式仿真、远程调试这些对硬件依赖极强的场景了。

其实,解决这些问题并不需要额外购买USB转串工具。现代Linux系统早已提供了更优雅的方案——虚拟串口驱动(Virtual Serial Port Driver)。它能让你在没有一块真实芯片的情况下,完整模拟出一对或多对串行端口,让应用程序“以为”自己正在和真实的UART设备打交道。

今天我们就来走一遍这条“无中生有”的技术路径:从源码编写到模块加载,全程零基础可操作。不仅告诉你怎么做,更要讲清楚每一步背后的逻辑。


为什么我们需要虚拟串口?

先别急着敲代码。我们得明白——这项技术到底解决了什么问题。

传统串口通信依赖于物理UART控制器和电平转换芯片(比如MAX232),但这类资源在现代PC上越来越稀缺。更重要的是,在开发阶段频繁插拔硬件容易出错,且难以实现自动化测试。

而虚拟串口完全不同:

  • 它是纯软件实现的TTY设备。
  • 不连接任何引脚,也不产生实际电平信号。
  • 却能让minicomscreen甚至自定义串口程序毫无察觉地读写数据。

它的核心价值在于:把硬件依赖变成内存中的数据流转

举个典型用例:你在写一个Modbus RTU解析器,但手头没有PLC设备。这时候就可以用虚拟串口创建两个端点——一个当作“主机”发送请求,另一个当作“从机”返回模拟响应。整个过程完全脱离硬件,却能100%验证协议逻辑。


虚拟串口是怎么工作的?

要理解虚拟串口,就得先搞懂Linux的TTY子系统。

TTY不是简单的“串口”,而是一套抽象框架

很多人误以为TTY就是串口,其实不然。TTY(Teletype)最早指的是电传打字机,后来演变为Linux中用于字符输入输出的一套通用机制。今天我们看到的终端窗口、SSH会话、控制台,背后都是TTY在支撑。

当你的程序调用open("/dev/ttyS0", O_RDWR)时,内核并不会直接去操作某个寄存器。它会通过TTY核心层,找到对应的驱动,并执行注册的操作函数集(struct tty_operations)。

虚拟串口的本质,就是注册一个假的TTY驱动,让它看起来像真的串口设备,但实际上所有操作都在内存中完成。

数据怎么“发”出去又“收”回来?

最常用的模式是“环回对”(loopback pair)。例如创建/dev/ttyV0/dev/ttyV1,当你往ttyV0写数据时,这些字节会被自动放入ttyV1的接收缓冲区,反之亦然。

这就像两个人拿着对讲机通话,只不过他们之间的“无线电波”其实是同一块内存区域里的复制粘贴。

关键就在于两个函数:

tty_insert_flip_char(dest_tty, data, TTY_NORMAL); tty_flip_buffer_push(dest_tty);

前者把数据塞进目标TTY的翻转缓冲区(flip buffer),后者触发软中断,通知用户空间可以读取新数据了。

⚠️ 注意:“flip buffer”这个名字听起来奇怪,其实是历史遗留术语——它指的是“双缓冲切换”,防止读写冲突。


动手写一个最简版虚拟串口驱动

下面这段代码虽然只有几十行,但它已经具备了一个完整虚拟串口驱动的核心骨架。

#include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/tty.h> #include <linux/tty_driver.h> #include <linux/tty_flip.h> static struct tty_driver *vserial_driver; // 写入回调:将数据转发到配对端口 static int vserial_write(struct tty_struct *tty, const unsigned char *buf, int count) { struct tty_struct *peer = tty->driver->ttys[(tty->index + 1) % 2]; // 配对逻辑 int i; for (i = 0; i < count; i++) { tty_insert_flip_char(peer, buf[i], TTY_NORMAL); } tty_flip_buffer_push(peer); // 激活接收队列 return count; } // 打开设备时打印日志 static int vserial_open(struct tty_struct *tty, struct file *filp) { printk(KERN_INFO "vserial: port %d opened\n", tty->index); return 0; } // 定义支持的操作 static const struct tty_operations vserial_ops = { .open = vserial_open, .write = vserial_write, }; static int __init vserial_init(void) { int ret; // 分配TTY驱动结构体(支持2个设备) vserial_driver = alloc_tty_driver(2); if (!vserial_driver) return -ENOMEM; vserial_driver->owner = THIS_MODULE; vserial_driver->driver_name = "vserial"; vserial_driver->name = "ttyV"; // 设备节点前缀 vserial_driver->major = 0; // 动态分配主设备号 vserial_driver->type = TTY_DRIVER_TYPE_SERIAL; vserial_driver->subtype = SERIAL_TYPE_NORMAL; vserial_driver->flags = TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV; vserial_driver->init_termios = tty_std_termios; tty_set_operations(vserial_driver, &vserial_ops); ret = tty_register_driver(vserial_driver); if (ret) { put_tty_driver(vserial_driver); return ret; } printk(KERN_INFO "vserial driver loaded successfully\n"); return 0; } static void __exit vserial_exit(void) { tty_unregister_driver(vserial_driver); put_tty_driver(vserial_driver); printk(KERN_INFO "vserial driver unloaded\n"); } module_init(vserial_init); module_exit(vserial_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name"); MODULE_DESCRIPTION("Minimal Virtual Serial Port Driver");

关键点解读

代码片段说明
alloc_tty_driver(2)声明支持两个设备实例(ttyV0 和 ttyV1)
.name = "ttyV"生成的设备节点名为/dev/ttyV0,/dev/ttyV1
major=0主设备号由内核动态分配,避免冲突
TTY_DRIVER_DYNAMIC_DEV允许运行时动态添加设备节点
tty_register_driver()向内核注册驱动,触发设备创建

这个驱动非常精简,省略了锁机制、ioctl支持、错误处理等生产级功能,但对于学习来说刚刚好。


编译它:Makefile的秘密

光有C代码还不够,你还得告诉编译系统怎么把它变成.ko文件。

新建一个Makefile

obj-m += vserial.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean install: sudo insmod vserial.ko remove: sudo rmmod vserial

就这么几行,藏着Linux内核模块构建的大智慧。

-C $(KDIR) M=$(PWD) modules到底发生了什么?

这句命令的意思是:

“进入内核源码目录下的构建系统,告诉它你现在要编译一个外部模块,路径是当前目录。”

Kbuild系统会自动读取你的obj-m变量,找到vserial.c,然后使用与当前内核完全一致的编译选项进行编译,确保符号兼容性。

💡 小知识:如果你看到Invalid module format错误,几乎一定是内核版本或配置不匹配导致的。

必须安装头文件!

在Ubuntu上执行:

sudo apt install build-essential linux-headers-$(uname -r)

CentOS用户则运行:

sudo yum install kernel-devel gcc make

否则连最基本的<linux/module.h>都找不到。


加载并验证:让虚拟串口活起来

准备好一切后,开始实战。

# 1. 编译 make # 2. 查看输出结果 ls -l vserial.ko file vserial.ko # 输出应为:ELF 64-bit LSB relocatable, x86-64
# 3. 加载模块 sudo insmod vserial.ko

此时还看不到/dev/ttyV*设备?别慌。

# 4. 检查内核日志 dmesg | tail -3

你应该能看到类似输出:

[ 1234.567890] vserial driver loaded successfully [ 1234.567895] Virtual serial port 0 opened

说明驱动已注册成功。接下来手动创建设备节点:

# 获取主设备号(通常是动态分配的) grep ttyV /proc/devices # 输出示例:240 ttyV # 创建设备节点(假设主设备号是240) sudo mknod /dev/ttyV0 c 240 0 sudo mknod /dev/ttyV1 c 240 1 # 设置权限(允许普通用户访问) sudo chmod 666 /dev/ttyV*

现在你可以打开两个终端试试:

终端1:

cat /dev/ttyV1

终端2:

echo "Hello from V0" > /dev/ttyV0

如果终端1立刻显示出那句话,恭喜你!你刚刚亲手搭建了一个虚拟串口链路。


常见坑点与调试技巧

新手常踩的几个坑,我都替你趟过了:

insmod: error inserting 'vserial.ko': -1 Invalid module format

原因:内核版本不匹配或未安装对应头文件包。

✅ 解法:

uname -r # 查看当前内核版本 dpkg -l | grep headers-$(uname -r) # Ubuntu检查是否安装

没安装就补装:

sudo apt install linux-headers-$(uname -r)

/dev/ttyV*节点不存在

原因:虽然驱动注册成功,但udev规则未生效。

✅ 解法:手动mknod,或添加udev规则:

# /etc/udev/rules.d/99-vserial.rules KERNEL=="ttyV[0-1]", GROUP="dialout", MODE="0666"

然后重新加载模块即可自动创建。

❌ 数据写入后对方收不到

原因:忘了调用tty_flip_buffer_push()

这是初学者最容易犯的错误。只调用tty_insert_flip_char()只是把数据放进缓冲区,必须配合_push才能触发唤醒。

✅ 解法:务必在批量插入后加上:

tty_flip_buffer_push(dest_tty);

❌ 权限被拒绝

原因:默认设备节点属于root,普通用户无法访问。

✅ 解法:

sudo usermod -aG dialout $USER # 注销重登生效

进阶思考:如何让它更实用?

这个最小驱动当然不能直接用于产品环境,但它是一个绝佳起点。你可以在此基础上扩展:

✅ 支持更多端口对

修改alloc_tty_driver(N),并调整配对逻辑,支持ttyV2<->ttyV3等。

✅ 添加ioctl控制

允许用户设置波特率、数据位等参数(尽管只是形式上的),提升兼容性。

✅ 引入环形缓冲区

使用kfifo替代简单循环插入,提高吞吐性能。

✅ 实现阻塞/非阻塞读写

加入等待队列,使read()行为更贴近真实串口。

✅ 打包为systemd服务

# /etc/systemd/system/vserial.service [Unit] Description=Virtual Serial Driver Loader [Service] Type=oneshot ExecStart=/sbin/insmod /opt/vserial/vserial.ko ExecStartPost=/bin/sh -c '/sbin/mknod /dev/ttyV0 c $(awk "\\$2==\"ttyV\" {print \\$1}" /proc/devices) 0' ExecStartPost=/bin/sh -c '/sbin/mknod /dev/ttyV1 c $(awk "\\$2==\"ttyV\" {print \\$1}" /proc/devices) 1' RemainAfterExit=yes [Install] WantedBy=multi-user.target

启用开机自启:

sudo systemctl enable vserial

写在最后

虚拟串口驱动看似小众,实则是通向Linux内核编程世界的一扇大门。

通过这次实践,你不只是学会了编译一个.ko文件,更深入理解了:

  • TTY子系统的运作机制
  • 字符设备的注册流程
  • 内核模块的生命周期管理
  • 用户空间与内核空间的数据交互方式

更重要的是,你掌握了如何在一个没有硬件的环境中,构建出足以骗过任何串口应用的“虚拟现实”。

下次当你需要模拟GPS模块、温湿度传感器、工业PLC时,不妨想想:能不能用虚拟串口+脚本,快速搭起一个仿真环境?

如果你在实现过程中遇到了其他挑战,欢迎在评论区分享讨论。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询