SerialPort.GetPortNames() 在 Linux 上只做了“有限规则的设备枚举”,
而不是“系统层面的有效串口发现”。

这是一个极容易在跨平台时踩中的坑,而且非常隐蔽。


一、现象:在 Linux 下,GetPortNames() 返回的串口不全

在 Windows 上,大多数人对下面这段代码是高度信任的:

var ports = SerialPort.GetPortNames();

你期望它返回:

COM1
COM3
COM5

迁移到 Linux 之后,你也自然会期望类似的效果,比如:

/dev/ttyS0
/dev/ttyUSB0
/dev/ttyACM0

但现实往往是:

  • 明明系统里存在串口设备
  • 你手动 ls /dev/tty* 能看到
  • 甚至你用 minicom / screen 能连上

👉 SerialPort.GetPortNames() 却返回空数组,或者只返回一部分

这在以下场景尤为常见:

  • USB‑Serial 转接器
  • 工控机 / ARM Linux
  • Docker / 容器环境
  • 非 root 用户运行程序

二、为什么这是一个“必坑”

因为它违反了一个强烈的心理预期

“GetPortNames() 应该返回所有可用串口”

但在 Linux 上,这个假设是错的。

而且这个错误不是偶发的,是设计层面的必然结果


三、根本原因:Linux 上不存在“串口列表”这一概念

这是理解这个问题的关键。

1️⃣ Windows 的世界观

在 Windows 中:

  • 串口是一个被操作系统明确管理的设备类别
  • COM 口有:
    • 注册表
    • Plug & Play 体系
    • 明确的枚举 API

所以:

GetPortNames() ≈ 查询 OS 的官方设备清单


2️⃣ Linux 的世界观(完全不同)

在 Linux 中:

  • 串口不是一个“被统一登记的资源”
  • 它只是:
    • /dev 下的一个字符设备文件
  • 是否存在、叫什么名字,取决于:
    • 内核驱动
    • udev 规则
    • 发行版策略

换句话说:

Linux 并没有“所有串口设备”的权威名单

只有:

“当前 /dev 目录下,有哪些文件看起来像串口”


四、.NET 在 Linux 下是怎么实现 GetPortNames() 的?

这是坑的技术根源。

在 Linux 上,.NET 的 SerialPort.GetPortNames() 并没有调用某个系统 API,而是采用了:

基于文件名规则的静态扫描

典型行为是:

  • 扫描 /dev
  • 只匹配少数几个固定前缀,例如:
    • ttyS*
    • ttyUSB*
    • ttyACM*

这意味着什么?

✅ 能返回的:

  • 传统串口
  • 常见 USB‑Serial 芯片

❌ 返回不了的:

  • 使用自定义 udev 规则重命名的设备
  • 非主流驱动暴露的 tty
  • 某些 SoC / 工业设备上的串口
  • 权限不足但“实际存在”的设备节点

五、一个非常隐蔽的情况:权限问题

这是实战中最容易被忽略的点

在 Linux 下:

ls /dev/ttyUSB0

你能看到设备 ≠ 你的程序能“枚举到它”。

如果:

  • 当前用户不在 dialout / uucp 等串口组
  • 设备节点权限不足

那么:

GetPortNames() 可能直接忽略这些设备

而不会给你任何异常或提示。


六、总结

SerialPort.GetPortNames() 在 Linux 上并不是“发现系统中所有可用串口”,
而只是“按有限规则扫描 /dev 下的一部分设备文件”。

所以:

它的返回结果 ≠ 实际可用串口全集


七、解决方案一:自己扫描 /dev(推荐)

这是最可靠、最可控的方式。

var ports = Directory.GetFiles("/dev", "tty*");

你可以再进一步过滤:

var ports = Directory.GetFiles("/dev")
    .Where(p =>
        p.StartsWith("/dev/ttyS") ||
        p.StartsWith("/dev/ttyUSB") ||
        p.StartsWith("/dev/ttyACM"))
    .ToArray();

✅ 优点:

  • 完全符合 Linux 思维
  • 不依赖 .NET 的内部实现
  • 可根据项目需求扩展

❌ 缺点:

  • 需要你自己维护规则

八、解决方案二:允许用户配置串口名称(强烈建议)

在工业 / 嵌入式 / Linux 场景中:

“自动发现串口”本身就是不可靠的

最佳实践反而是:

  • 不信任枚举
  • 明确配置

例如:

{
  "serialPort": "/dev/ttyUSB0"
}

或者:

--serial=/dev/serial/by-id/usb-xxxx

九、进阶建议:使用 /dev/serial/by-*

这是 Linux 下非常工程化、非常稳妥的做法。

/dev/serial/by-id/
/dev/serial/by-path/

这些路径:

  • 由 udev 自动维护
  • 稳定、不随插拔顺序变化
  • 非常适合长期运行的系统

在 .NET 中你完全可以直接使用它们:

new SerialPort("/dev/serial/by-id/usb-xxx", 9600);

十、工程建议

在 Linux 上,把 GetPortNames() 当作“提示工具”,而不是“事实来源”。

我的建议是:

  • ✅ Windows:可以信
  • ⚠️ Linux:仅供参考
  • ❌ 工业场景:不要依赖

结语:这不是 .NET 的 bug,而是世界观差异

这类问题的危险之处在于:

  • 代码没有报错
  • API 名字看起来“理所当然”
  • 只有在真实环境才暴露
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐