Linux下重启后Go关闭串口的Syscall阻塞问题排查
问题分析
我开发了一个仅读取设备数据的Go语言串口服务,随系统启动,仅在重启/关机时停止。热重启系统或重载cdc_acm串口驱动后,首次停止服务时调用serial.Port.Close()会触发SYS_CLOSE系统调用阻塞至少30秒;但同会话内重复启停服务无延迟,冷启动也正常。用Python脚本测试同样复现该问题,阻塞发生在汇编层SYSCALL指令,文件描述符已关闭但调用未返回。
核心原因
- 串口驱动与硬件状态不匹配:冷启动后硬件处于初始干净状态,热重启/驱动重载后,硬件可能残留未完成的通信事务,驱动内部状态也可能不一致。调用
close()时,Linux内核的cdc_acm驱动会等待硬件完成未处理的IO事务或尝试重置硬件,因状态异常触发30秒默认超时。 - 持续读取的连锁阻塞:服务中
readDonglegoroutine一直在读取串口,热重启后硬件处于半连接状态,虽然读取因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
相关产品推荐
相关产品推荐

