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

8 回复

看起来Ian已经找到了答案! :+1:

更多关于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的调度器变化影响了长时间阻塞的系统调用。建议:

  1. 使用strace跟踪系统调用
  2. 检查是否涉及信号处理
  3. 验证内存对齐是否符合设备要求
  4. 考虑使用runtime.LockOSThread将设备操作绑定到特定线程

Go 1.20的发布说明中提到:“The runtime now uses a strict FIFO policy for goroutines that are ready to run”,这可能改变了I/O密集型操作的调度行为。

回到顶部