You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Linux下重启后Go关闭串口的Syscall阻塞问题排查

问题分析

我开发了一个仅读取设备数据的Go语言串口服务,随系统启动,仅在重启/关机时停止。热重启系统或重载cdc_acm串口驱动后,首次停止服务时调用serial.Port.Close()会触发SYS_CLOSE系统调用阻塞至少30秒;但同会话内重复启停服务无延迟,冷启动也正常。用Python脚本测试同样复现该问题,阻塞发生在汇编层SYSCALL指令,文件描述符已关闭但调用未返回。

核心原因

  • 串口驱动与硬件状态不匹配:冷启动后硬件处于初始干净状态,热重启/驱动重载后,硬件可能残留未完成的通信事务,驱动内部状态也可能不一致。调用close()时,Linux内核的cdc_acm驱动会等待硬件完成未处理的IO事务或尝试重置硬件,因状态异常触发30秒默认超时。
  • 持续读取的连锁阻塞:服务中readDongle goroutine一直在读取串口,热重启后硬件处于半连接状态,虽然读取因ReadTimeout返回,但驱动内部仍有未完成的IO上下文。调用close()时,内核需要等待这些上下文清理完成,导致阻塞。
  • 会话间状态残留差异:同会话内启停时,串口驱动内部状态未被彻底重置(硬件未重新枚举),close时无需额外清理;而热重启/驱动重载后硬件被重新枚举,驱动初始化状态与冷启动不同,触发close时的超时逻辑。

解决方案

1. 关闭前先终止读取并清空缓冲

在关闭串口前,先停止读取goroutine,再清空串口缓冲,避免驱动残留IO上下文:

// 给Dongle结构体添加停止信号通道
type Dongle struct {
    serialPort    *serial.Port
    devicePresent bool
    stopChan      chan struct{}
}

func NewDongle(dongleName string) (d *Dongle, err error) {
    d = &Dongle{devicePresent: false, stopChan: make(chan struct{})}
    // 原初始化代码不变...
}

// 修改readDongle函数,监听停止信号
func readDongle(dongle *Dongle) {
    defer close(dongle.stopChan)
    buf := make([]byte, 128)
    for {
        select {
        case <-dongle.stopChan:
            return // 收到停止信号,退出读取循环
        default:
            n, err := dongle.serialPort.Read(buf)
            if err != nil {
                continue // 处理读取超时或错误
            }
            // 处理读取到的数据
        }
    }
}

// 改进Close方法
func (dongle *Dongle) Close() {
    if dongle.serialPort != nil {
        // 发送停止信号,等待读取goroutine退出
        dongle.stopChan <- struct{}{}
        <-dongle.stopChan
        
        // 主动清空串口读写缓冲
        _, _ = dongle.serialPort.Flush()
        
        // 关闭串口
        dongle.serialPort.Close()
    }
}

2. 强制关闭文件描述符

如果上述方法无效,直接调用系统调用关闭文件描述符,绕过驱动的阻塞清理逻辑:

import "syscall"

func (dongle *Dongle) Close() {
    if dongle.serialPort != nil {
        // 通过RawConn获取文件描述符,直接调用syscall.Close
        _ = dongle.serialPort.RawConn(func(fd uintptr) {
            syscall.Close(int(fd))
        })
        dongle.serialPort = nil
    }
}

3. 缩短驱动超时参数

通过sysfs修改cdc_acm驱动的自动挂起超时,减少close时的阻塞时间:

# 假设串口为/dev/ttyACM0,查看当前超时
cat /sys/class/tty/ttyACM0/device/power/autosuspend_delay_ms
# 设置为1秒(1000ms)
echo 1000 | sudo tee /sys/class/tty/ttyACM0/device/power/autosuspend_delay_ms

可以把这条命令加入系统启动脚本,确保每次重启后生效。

疑问解答

1. 为啥首次启停和后续启停有差异?

热重启/驱动重载后,串口硬件被重新枚举,驱动初始化时没完全重置硬件的通信状态(比如硬件FIFO残留数据),首次关闭时驱动要等这些残留事务处理完;后续同会话内启停时,硬件没被重新枚举,驱动状态一致,close时无需额外清理,所以没延迟。

2. 怎么强制关闭通信通道?

两种方式:一是直接调用syscall.Close()关闭文件描述符,绕过串口库封装;二是先停止读取goroutine,调用Flush()清空缓冲,再关闭串口。

3. 跳过关闭操作有啥危害?

进程退出时内核会自动回收文件描述符,不会泄漏,但可能导致串口硬件处于异常状态,比如残留IO事务未完成,下次启动服务可能无法正常打开串口,或读取到残留数据。长期频繁跳过关闭,可能导致驱动内部状态混乱,出现串口无法枚举等问题。

内容的提问来源于stack exchange,提问作者idontcare

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 06:15:44