Golang 1.20中与设备驱动程序交互的程序为何停止工作?
Golang 1.20中与设备驱动程序交互的程序为何停止工作? 我和一位同事负责一个大型项目,该项目需要控制和接收来自一块运行定制固件的专用PCI-express卡的数据。这是一个用于高速X射线和伽马射线探测器、超导传感器阵列的数据采集系统(仅作背景说明)。我无法断言Go是否是该项目的最佳选择——我的同事仍然认为Rust会更好——但我知道自2017年我们启动该项目(作为对C++庞然大物的替代)以来,它一直运行得非常好。
我们通过打开/关闭以及读写设备驱动程序提供的设备特殊文件来与PCIe卡通信。配置有控制寄存器,并且有一个散聚DMA用于将高速数据(通常为每秒20到200 MB,具体取决于仪器配置)传输到计算机RAM。
我发现Go 1.16、1.17、1.18和1.19都能正常运行我们的程序。然而,当我使用Go 1.20构建程序时,程序会挂起。构建成功,并且配置步骤在运行时(似乎)正常工作,设置了散聚DMA循环。当我们第一次尝试从DMA缓冲区读取时,程序就挂起了。而且这种情况只发生在Go 1.20中!
恐怕我无法提供一个最小可复现示例,因为您需要我们的特定硬件(运行我们的特定固件)和相应的设备驱动程序。我明白我无法提供足够的信息来解决这个问题。
尽管如此,也许有人能提供一些思路?关于Go 1.20,有什么特别之处是我应该了解,可能有助于我追踪问题的吗?我已经阅读了1.20版本说明很多遍,但也许我忽略了其中关键点的意义?
一些系统信息:Ubuntu 22.04,Go 1.20.3,16 GB RAM。(在另一台运行Ubuntu 20.04的PC上也注意到了同样的问题。)
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 22.04.2 LTS
Release: 22.04
Codename: jammy
$ go version
go version go1.20.3 linux/amd64
$ free
total used free shared buff/cache available
Mem: 16331116 6773036 173288 50804 9384792 9178968
Swap: 2097148 39936 2057212
$ lspci
00:00.0 Host bridge: Intel Corporation 4th Gen Core Processor DRAM Controller (rev 06)
00:01.0 PCI bridge: Intel Corporation Xeon E3-1200 v3/4th Gen Core Processor PCI Express x16 Controller (rev 06)
00:01.1 PCI bridge: Intel Corporation Xeon E3-1200 v3/4th Gen Core Processor PCI Express x8 Controller (rev 06)
00:14.0 USB controller: Intel Corporation 9 Series Chipset Family USB xHCI Controller
00:16.0 Communication controller: Intel Corporation 9 Series Chipset Family ME Interface #1
00:1a.0 USB controller: Intel Corporation 9 Series Chipset Family USB EHCI Controller #2
00:1b.0 Audio device: Intel Corporation 9 Series Chipset Family HD Audio Controller
00:1c.0 PCI bridge: Intel Corporation 9 Series Chipset Family PCI Express Root Port 1 (rev d0)
00:1c.3 PCI bridge: Intel Corporation 9 Series Chipset Family PCI Express Root Port 4 (rev d0)
00:1d.0 USB controller: Intel Corporation 9 Series Chipset Family USB EHCI Controller #1
00:1f.0 ISA bridge: Intel Corporation Z97 Chipset LPC Controller
00:1f.2 SATA controller: Intel Corporation 9 Series Chipset Family SATA Controller [AHCI Mode]
00:1f.3 SMBus: Intel Corporation 9 Series Chipset Family SMBus Controller
01:00.0 Unassigned class [ff00]: Altera Corporation Device 0004 (rev 01)
02:00.0 VGA compatible controller: NVIDIA Corporation TU117GL [T400 4GB] (rev a1)
02:00.1 Audio device: NVIDIA Corporation Device 10fa (rev a1)
04:00.0 Ethernet controller: Qualcomm Atheros Killer E220x Gigabit Ethernet Controller (rev 13)
Altera (pci 01:00.0) 就是有问题的PCIe设备。
我尝试了一些可能相关的步骤:
- 移除已弃用的
syscall包,用golang.org/x/sys/unix替换。 - 直接从Go调用诸如
C.posix_memalign(...)的函数,而不是调用一个手写的cgo包装函数,再由该包装函数调用posix_memalign。 - 直接调用
C.read(...),而不是调用unix.Read(fd, buffer),其中buffer是对先前分配的C指针调用C.GoBytes(...)的结果。
所有这些尝试都使得Go 1.16-1.19版本正常工作,而1.20版本仍然挂起。
目前,解决方法是当用户使用Go 1.20构建并尝试使用这个特定的数据源时,程序会panic。但这似乎不是一个长期的解决方案。
更多关于Golang 1.20中与设备驱动程序交互的程序为何停止工作?的实战教程也可以访问 https://www.itying.com/category-94-b0.html
看起来Ian已经找到了答案! 
更多关于Golang 1.20中与设备驱动程序交互的程序为何停止工作?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
我必须承认:你发现了一个真正的Bug,这让我印象深刻。这是我最近一段时间在这里看到的比较令人费解的帖子之一,我很高兴看起来你将得到一个解决方案。
我已经提交了一个问题(#60211)。考虑到我遇到的问题本身比较模糊,我不确定这样做是否合适。不过,或许这能带来一些有用的进展。
如果你还没有这样做,我建议你在 Go 问题跟踪器 上提交一个问题。我怀疑他们可能会就检查什么提供一些建议。
编辑:我以为我已经问过这个问题了,但我想可能我的帖子没有保存,或者我忘记保存了,但是:你能澄清一下你所说的“挂起”是什么意思吗?是进程完全停止(比如 CPU 使用率为 0%),还是似乎卡在某种循环中(比如 CPU 使用率为 100%)?如果是后者,你可以尝试在 Delve 中运行你的代码,看看它在哪里“卡住”了。
好问题。我得确认一下……
我可以确认这里的“挂起”意味着停止运行且CPU使用率为0%。使用VS Code的Golang调试器(我猜底层用的是delve),我发现挂起发生在以下代码的第二行:
gobuffer := C.GoBytes(unsafe.Pointer(buffer), C.int(bufferLength))
n, err := unix.Read(int(fd), gobuffer)
在这个例子中,bufferLength 是 33554432(即 2^25),这是 buffer *C.char 所指向数据的大小,该内存是通过调用 posix_memalign(...) 分配的。fd 是打开的设备特殊文件的文件描述符,我们本应从该文件读取数据。或者说,如果那个调用能返回的话,我们就会读到数据。
感谢回复。我会看看是否能提出一个有效的问题来提交。当你无法提供任何可复现的问题时,这并不理想,但我实在是没有其他办法了!
得知这竟然是Go 1.20 [某个库中] 一个真实的bug,我简直惊呆了!
为那些没有阅读Golang issue #60211及其相关链接的人总结一下:看起来,如果用户像下面这样打开文件,Go 1.20 没有正确设置 O_NONBLOCK 标志:
file, err := os.OpenFile(myFileName, os.O_RDWR|syscall.O_NONBLOCK, 0666)
我已经使用开源软件四分之一个世纪了,我不记得自己曾经被一个主要软件包中的真实bug绊倒过。尤其是在像被广泛使用的编程语言这样重要的东西里!我同意:我也对自己印象深刻。
非常感谢Go论坛,特别是他敦促我提交了一个issue。我当时确信问题出在我自己身上——也许我依赖了某些未公开且已改变的行为?毕竟,我并不完全理解这个设备或其驱动程序。
同时感谢Golang开发者们在意识到存在真实bug后迅速捕获并修复了它。根据我看到的动态,这个bug似乎将在Go 1.20.5(如果该版本发布的话)中得到修复,并且肯定会在1.21版本中修复。
程序是否在IO操作时挂起? 你可以尝试每次读取一个字节,并根据程序在读取某个字节时是否挂起来判断问题是否出在IO上。 posix_memalign 似乎是一个内存分配函数,请参阅 go help build
-race
启用数据竞争检测。 仅支持 linux/amd64, freebsd/amd64, darwin/amd64, darwin/arm64, windows/amd64, linux/ppc64le 和 linux/arm64(仅限48位VMA)。
-msan
启用与内存清理器的互操作。 仅支持 linux/amd64, linux/arm64, freebsd/amd64,并且仅当主机C编译器为 Clang/LLVM 时。 除 linux/amd64 外的所有平台将使用 PIE 构建模式。
-asan
启用与地址清理器的互操作。 仅支持 linux/arm64, linux/amd64。 仅支持 linux/amd64 或 linux/arm64,并且仅支持 GCC 7 及更高版本或 Clang/LLVM 9 及更高版本。
你可以尝试检测是否存在任何内存问题。
Go 1.20在内存管理和调度器方面有一些重要变化,可能导致与设备驱动程序交互时出现问题。以下是几个关键点:
1. 调度器变化
Go 1.20改进了调度器的抢占机制,这可能影响系统调用的行为:
// Go 1.20中系统调用可能被不同处理
fd, err := unix.Open("/dev/pcie_device", unix.O_RDWR, 0)
if err != nil {
return err
}
// 在Go 1.20中,长时间阻塞的read可能被不同处理
n, err := unix.Read(fd, buffer)
2. 内存对齐变化
Go 1.20对内存对齐有更严格的要求,特别是与C交互时:
// 确保DMA缓冲区的正确对齐
import "C"
// Go 1.20可能对C.malloc返回的指针有不同处理
var dmaBuffer *C.void
C.posix_memalign(unsafe.Pointer(&dmaBuffer), 4096, bufferSize)
// 转换为Go可用的切片
goBuffer := unsafe.Slice((*byte)(unsafe.Pointer(dmaBuffer)), bufferSize)
3. cgo调用优化
Go 1.20优化了cgo调用路径,可能影响阻塞系统调用:
// 直接使用系统调用可能更可靠
import "syscall"
func rawRead(fd int, p []byte) (n int, err error) {
// 绕过标准库的包装
r0, _, e1 := syscall.Syscall(syscall.SYS_READ,
uintptr(fd),
uintptr(unsafe.Pointer(&p[0])),
uintptr(len(p)))
n = int(r0)
if e1 != 0 {
err = e1
}
return
}
4. 信号处理变化
Go 1.20改进了信号处理,可能影响异步I/O:
// 检查信号处理是否影响DMA读取
import "os/signal"
func setupSignalHandling() {
// 忽略可能干扰设备I/O的信号
signal.Ignore(syscall.SIGIO, syscall.SIGURG)
}
5. 运行时内存屏障
Go 1.20增加了内存屏障,确保内存可见性:
// 使用runtime.KeepAlive确保指针不被过早回收
import "runtime"
func readFromDevice(fd int, buffer unsafe.Pointer, size int) error {
n, err := unix.Read(fd, unsafe.Slice((*byte)(buffer), size))
runtime.KeepAlive(buffer) // Go 1.20中更重要
return err
}
6. 尝试的解决方案
基于你的描述,可以尝试:
// 方案1:使用原始文件描述符
func readDMA(fd uintptr, buf []byte) (int, error) {
var n int
for {
// 非阻塞读取尝试
n, err := unix.Read(int(fd), buf)
if err == unix.EAGAIN || err == unix.EWOULDBLOCK {
runtime.Gosched()
continue
}
return n, err
}
}
// 方案2:调整GOMAXPROCS
func init() {
// 减少并发可能有助于调试
runtime.GOMAXPROCS(1)
}
// 方案3:使用runtime.LockOSThread
func readFromPCIE() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
// 设备操作代码
}
7. 调试建议
添加详细的调试信息:
import "runtime/debug"
func debugRead() {
debug.SetGCPercent(-1) // 临时禁用GC
defer debug.SetGCPercent(100)
// 你的读取代码
fmt.Printf("Goroutine: %d\n", runtime.NumGoroutine())
}
最可能的原因是Go 1.20的调度器变化影响了长时间阻塞的系统调用。建议:
- 使用
strace跟踪系统调用 - 检查是否涉及信号处理
- 验证内存对齐是否符合设备要求
- 考虑使用
runtime.LockOSThread将设备操作绑定到特定线程
Go 1.20的发布说明中提到:“The runtime now uses a strict FIFO policy for goroutines that are ready to run”,这可能改变了I/O密集型操作的调度行为。

