Golang中GOGC开启导致的内存损坏问题

Golang中GOGC开启导致的内存损坏问题 错误 unexpected fault address 可能由数据竞争或内存损坏引发。经过全面代码审查后,我更倾向于内存损坏而非数据竞争。

以下代码偶尔会触发 panic,即使在我初始化切片时也是如此。

package controlcan

import "C"

cReceive := make([]C.struct__CAN_OBJ, 2500)

package main

import "controlcan"

pReceive := make([]controlcan.CanObj, 2500)

两者都会偶尔触发错误:

unexpected fault address 0xffffffffffffffff
fatal error: fault
[signal 0xc0000005 code=0x0 addr=0xffffffffffffffff pc=0x41c65d]

goroutine 41 [running]:
runtime.throw(0xcc969a, 0x5)
        /usr/local/go/src/runtime/panic.go:619 +0x88 fp=0xc0428ffb38 sp=0xc0428ffb18 pc=0x42d0b8
runtime.sigpanic()
        /usr/local/go/src/runtime/signal_windows.go:170 +0x13a fp=0xc0428ffb68 sp=0xc0428ffb38 pc=0x43fcca
runtime.gcMarkRootPrepare()
        /usr/local/go/src/runtime/mgcmark.go:72 +0x5d fp=0xc0428ffb70 sp=0xc0428ffb68 pc=0x41c65d
runtime.gcStart(0x0, 0x1, 0x0, 0x0)
        /usr/local/go/src/runtime/mgc.go:1350 +0x30f fp=0xc0428ffba0 sp=0xc0428ffb70 pc=0x419b6f
runtime.mallocgc(0x10000, 0xc54660, 0xc0422ee001, 0xc0423ded60)
        /usr/local/go/src/runtime/malloc.go:803 +0x448 fp=0xc0428ffc40 sp=0xc0428ffba0 pc=0x411c48
runtime.makeslice(0xc54660, 0x9c4, 0x9c4, 0xc04202ce00, 0xc04202c000, 0x411b23)
        /usr/local/go/src/runtime/slice.go:61 +0x7e fp=0xc0428ffc70 sp=0xc0428ffc40 pc=0x43fffe
controlcan.Receive(0x4, 0x0, 0x0, 0xc04267e000, 0x9c4, 0x9c4, 0x64, 0x0, 0x0, 0x0)
        /media/sf_GOPATH0/src/controlcan/controlcan.go:262 +0x75 fp=0xc0428ffd70 sp=0xc0428ffc70 pc=0xa0d795
posam/protocol/usbcan.(*Channel).receive(0xc04229d490)
        /media/sf_GOPATH0/src/posam/protocol/usbcan/usbcan.go:469 +0x536 fp=0xc0428fffd8 sp=0xc0428ffd70 pc=0xa10926
runtime.goexit()
        /usr/local/go/src/runtime/asm_amd64.s:2361 +0x1 fp=0xc0428fffe0 sp=0xc0428fffd8 pc=0x457531
created by posam/protocol/usbcan.(*Channel).Start
        /media/sf_GOPATH0/src/posam/protocol/usbcan/usbcan.go:242 +0x3aa

我困惑了几天,直到应用程序使用 GOGC=off 运行。除了内存使用量增加外,一切运行正常。

根据 Dave Cheney 在 “cgo is not Go” 中的说法:

C 语言不了解 Go 的调用约定或可增长堆栈,因此向下调用 C 代码时必须记录 goroutine 堆栈的所有细节,切换到 C 堆栈,并运行不知道如何被调用或负责程序的更大 Go 运行时的 C 代码。

以及 JimB 在 stackoverflow 中的说法:

另一种可能性是,如果您将指针传递给 C 语言,该指针超出范围,并在 cgo 调用返回之前偶尔被 GC 回收

我意识到在使用 cgo 时可能触发内存损坏。但在上述情况下,唯一需要的是类型 CanObj,而不是任何变量。

那么这个错误的原因是什么?我该如何更好地使用 cgo?

以下是相关问题的链接:

谢谢!


更多关于Golang中GOGC开启导致的内存损坏问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang中GOGC开启导致的内存损坏问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


这个问题确实与cgo和Go垃圾收集器(GC)的交互有关。当GOGC开启时,垃圾收集器会在运行时扫描内存,而cgo代码中的C类型结构体可能包含Go GC无法正确处理的指针或内存布局。

问题分析

从堆栈跟踪可以看出,panic发生在runtime.gcMarkRootPrepare()中,这是GC标记阶段准备根对象时发生的。问题在于C.struct__CAN_OBJ可能包含Go GC无法识别的指针模式,导致GC在扫描时访问了无效的内存地址。

解决方案

1. 使用unsafe.Pointer进行手动内存管理

package controlcan

import (
    "C"
    "unsafe"
)

// 使用C.malloc分配内存,避免Go GC干预
func allocateCanObjs(count int) unsafe.Pointer {
    size := C.size_t(unsafe.Sizeof(C.struct__CAN_OBJ{})) * C.size_t(count)
    return C.malloc(size)
}

func freeCanObjs(ptr unsafe.Pointer) {
    C.free(ptr)
}

// 将unsafe.Pointer转换为切片(仅用于访问,GC不管理)
func canObjsSlice(ptr unsafe.Pointer, count int) []C.struct__CAN_OBJ {
    return (*[1 << 30]C.struct__CAN_OBJ)(ptr)[:count:count]
}

使用示例:

// 分配
ptr := allocateCanObjs(2500)
defer freeCanObjs(ptr)

// 使用
cReceive := canObjsSlice(ptr, 2500)
// 使用cReceive访问数据

2. 使用cgo指针传递规则

确保传递给C代码的Go指针在C调用期间保持活跃:

package controlcan

import "C"

// 使用runtime.KeepAlive确保指针在C调用期间不被回收
func safeReceive() {
    cReceive := make([]C.struct__CAN_OBJ, 2500)
    
    // 假设这是调用C函数的代码
    // C.some_c_function(unsafe.Pointer(&cReceive[0]))
    
    // 确保cReceive在C调用完成前不被GC
    runtime.KeepAlive(cReceive)
}

3. 使用固定大小的数组而非切片

package controlcan

import "C"

// 使用固定大小的数组
var cReceive [2500]C.struct__CAN_OBJ

// 或者使用全局变量确保生命周期
var globalReceive = make([]C.struct__CAN_OBJ, 2500)

4. 设置合理的内存屏障

package controlcan

import (
    "C"
    "runtime"
    "unsafe"
)

type CanBuffer struct {
    data unsafe.Pointer
    size int
}

func NewCanBuffer(size int) *CanBuffer {
    elemSize := unsafe.Sizeof(C.struct__CAN_OBJ{})
    ptr := C.malloc(C.size_t(elemSize) * C.size_t(size))
    return &CanBuffer{
        data: ptr,
        size: size,
    }
}

func (b *CanBuffer) Slice() []C.struct__CAN_OBJ {
    return (*[1 << 30]C.struct__CAN_OBJ)(b.data)[:b.size:b.size]
}

func (b *CanBuffer) Free() {
    if b.data != nil {
        C.free(b.data)
        b.data = nil
    }
}

使用示例:

buffer := NewCanBuffer(2500)
defer buffer.Free()

cReceive := buffer.Slice()
// 安全使用cReceive

根本原因

当GOGC开启时,垃圾收集器会扫描Go堆中的对象。对于包含C结构体的切片,GC可能错误地解释C结构体内部的字节模式为Go指针,导致访问无效内存地址。使用GOGC=off之所以有效,是因为它完全禁用了垃圾收集,避免了这种错误的指针扫描。

这些解决方案通过手动管理C内存或确保正确的指针生命周期来避免GC的干扰,从而解决内存损坏问题。

回到顶部