CGO内存模型CGO是Go语言和C语言之间的桥梁。它使两者能够在二进制接口级别彼此通信,但是我们必须注意可能由于两种语言的内存模型不同而引起的问题。如果CGO处理的跨语言函数调用涉及指针的传输,则可能存在Go和C语言共享特定内存段的情况。我们知道C语言的内存在分配后是稳定的,但是Go语言的函数堆栈的动态扩展可能会导致堆栈中的内存地址移动(这是Go和C内存模型之间的最大区别) 。如果C语言在移动之前保留Go指针,则使用旧指针访问Go对象将导致程序崩溃。 Go访问C内存。 C语言空间的内存是稳定的。只要不是事先人为发行,就可以在Go语言空间中大胆地使用它。用Go语言访问C语言内存是最简单的情况。在前面的示例中,我们已经多次看到它。由于Go语言实现的局限性,我们无法使用Go语言创建大于2GB内存的切片(有关详细信息,请参阅makeslice实现代码)。但是,借助cgo技术,我们可以在C语言环境中创建超过2GB的内存,然后将其转换为Go语言中的切片使用:
package main
/*
#include
void* makeslice(size_t memsize) {
return malloc(memsize);
}
*/
import "C"
import (
"unsafe"
)
func makeByteSlize(n int) []byte {
p := C.makeslice(C.size_t(n))
return ((*[1<<31]byte)(p))[0:n:n]
}
func freeByteSlice(p []byte) {
C.free(unsafe.Pointer(&p[0]))
}
func main() {
s := makeByteSlize(1 << 31)
s[len(s)-1] = 255
print(s[len(s)-1])
freeByteSlice(s)
}
在此示例中,我们使用makeByteSlize创建大于2G内存大小的切片,从而绕过Go语言的限制(需要代码验证)。 freeByteSlice帮助器函数用于释放从C语言函数创建的切片。因为C语言存储空间是稳定的,所以基于C语言存储结构的片绝对是稳定的,并且不会由于Go语言堆栈中的更改而被移动。 C临时访问传入的Go内存。 cgo存在的一个主要因素是在过去的几十年中,以Go语言促进了使用C / C ++语言软件构建的大量软件资源的接受和吸收。许多C / C ++库需要通过指针直接处理传入的内存数据,因此cgo中有许多应用程序场景需要将Go内存传递给C语言函数。假设存在一个极端的情况:在我们将位于某个goroutinue堆栈上的一段Go语言内存传递给C语言函数之后,在执行该C语言函数的过程中,由于空间不足,该goroutinue的堆栈会扩展,这意味着原始的Go语言记忆已被移至新位置。但是此刻,C语言函数不知道Go语言内存已移动,仍然使用先前的地址来操作内存-这将导致内存越过边界。上面是必然的结果(实际情况有所不同),也就是说,对于C来说,访问传递的Go内存可能是不安全的!当然,有RPC远程过程调用经验的用户可以考虑使用完整的按值处理方法:借助C语言内存稳定性,在C语言空间中打开相同大小的内存,然后填充将内存转到C内存空间;返回的内存也是如此。以下示例是此想法的具体实现:
package main
/*
#include
#include
void printString(const char* s) {
printf("%s", s);
}
*/
import "C"
import "unsafe"
func printString(s string) {
cs := C.CString(s) // 在c中申请内存
defer C.free(unsafe.Pointer(cs))
C.printString(cs) // 释放
}
func main() {
s := "hello"
printString(s)
}
当您需要将Go字符串传输到C语言时,请首先通过C.CString将与Go语言字符串相对应的内存数据复制到新创建的C语言内存空间。尽管上述示例的处理思想是安全的,但它效率极低(因为必须多次分配内存并且元素被一个一个地复制),而且也非常麻烦。为了简化并有效处理将Go语言内存传递给C语言的问题,cgo为此情况定义了一条特殊规则:在CGO调用的C语言函数返回之前,cgo保证在此期间传递的Go语言内存不可用。时期。将会发生移动,并且C语言功能可以大胆地使用Go语言内存!根据新规则,我们可以直接传递Go字符串的内存:

package main
/*
#include
void printString(const char* s, int n) {
int i;
for(i = 0; i < n; i++) {
putchar(s[i]);
}
putchar('\n');
}
*/
import "C"
import (
"reflect"
"unsafe"
)
func printString(s string) {
p := (*reflect.StringHeader)(unsafe.Pointer(&s))
C.printString((*C.char)(unsafe.Pointer(p.Data)), C.int(len(s)))
}
func main() {
s := "hello"
printString(s)
}
当前方法更直接,并且避免分配额外的内存。完美的解决方案!当滥用任何完善的技术时,CGO看似完美的规则也有潜在的危险。我们假设被调用的C语言函数需要运行很长时间,这将导致在C语言返回之前,他引用的Go语言内存无法移动,这可能间接导致与Go内存堆栈相对应的goroutine。不动态扩展堆栈内存。这可能会导致goroutine被阻塞。因此,在需要长时间运行的C语言函数中(尤其是在纯CPU操作中,而且还因为需要等待其他资源并且需要不确定的时间来完成),您需要仔细处理传入的去语言记忆。但是,请注意,获取Go内存后需要立即传递C语言函数,并且不能将它们保存到临时变量中,然后间接传递C语言函数。因为CGO只能保证在调用C函数之后传入的Go语言内存不会移动,所以无法保证在传入C函数之前不会更改内存。以下代码是错误的:
// 错误的代码
tmp := uintptr(unsafe.Pointer(&x))
pb := (*int16)(unsafe.Pointer(tmp))
*pb = 42
由于tmp不是指针类型,因此x对象在获得Go对象的地址后可能会移动,但是因为它不是指针类型,所以Go语言不会将其更新为新的内存地址运行。在非指针类型tmp中将Go对象的地址保持与在C语言环境中保持Go对象的地址具有相同的效果:如果原始的Go对象内存已移动,则Go语言将不会同步更新它们。 C长期持有Go指针对象。作为Go程序员,在使用CGO时,他下意识地认为Go总是调用C函数。实际上,在CGO中,C语言功能还可以回调以Go语言实现的功能。特别是,我们可以用Go语言编写一个动态库,导出C语言规范的接口供其他用户调用。当C语言函数调用Go语言函数时,C语言函数成为程序的调用者,并且Go语言函数返回的Go对象存储器的生命周期自然超出了Go语言运行时的管理范围。简而言之,我们不能在C语言函数中直接使用Go语言对象的内存。尽管Go语言禁止在C语言函数中长期保存Go指针对象,但这种需求是真实的。如果您需要使用C语言访问Go语言存储对象,我们可以将Go语言空间中的Go语言存储对象映射到一个int类型的id,然后通过该id间接访问和控制Go语言对象。以下代码用于将Go对象映射到整数类型的ObjectId。用完之后,您需要手动调用free方法来释放对象ID:
package main
import "sync"
type ObjectId int32
var refs struct {
sync.Mutex
objs map[ObjectId]interface{}
next ObjectId
}
func init() {
refs.Lock()
defer refs.Unlock()
refs.objs = make(map[ObjectId]interface{})
refs.next = 1000
}
func NewObjectId(obj interface{}) ObjectId {
refs.Lock()
defer refs.Unlock()
id := refs.next
refs.next++
refs.objs[id] = obj
return id
}
func (id ObjectId) IsNil() bool {
return id == 0
}
func (id ObjectId) Get() interface{} {
refs.Lock()
defer refs.Unlock()
return refs.objs[id]
}
func (id *ObjectId) Free() interface{} {
refs.Lock()
defer refs.Unlock()
obj := refs.objs[*id]
delete(refs.objs, *id)
*id = 0
return obj
}

我们使用地图来管理Go语言对象和id对象之间的映射关系。其中,NewObjectId用于创建绑定到该对象的id,并且id对象的方法可以用于解码原始Go对象,还可以用于结束id与原始Go的绑定目的。以下函数集随C接口规范一起导出,并且可以由C语言函数调用:
package main
/*
extern char* NewGoString(char* );
extern void FreeGoString(char* );
extern void PrintGoString(char* );
static void printString(char* s) {
char* gs = NewGoString(s);
PrintGoString(gs);
FreeGoString(gs);
}
*/
import "C"
//export NewGoString
func NewGoString(s *C.char) *C.char {
gs := C.GoString(s)
id := NewObjectId(gs)
return (*C.char)(unsafe.Pointer(uintptr(id)))
}
//export FreeGoString
func FreeGoString(p *C.char) {
id := ObjectId(uintptr(unsafe.Pointer(p)))
id.Free()
}
//export PrintGoString
func PrintGoString(p *C.char) {
id := ObjectId(uintptr(unsafe.Pointer(p)))
gs := id.Get().(string)
print(gs)
}
func main() {
C.printString(C.CString("hello\n"))}
在printString函数中,我们通过NewGoString创建了一个相应的Go字符串对象,返回的实际上是一个id,不能直接使用。我们使用PrintGoString函数将id解析为Go语言字符串并进行打印。该字符串完全超越了C语言功能中Go语言的内存管理。即使Go字符串地址由于在PrintGoString调用之前的堆栈缩放而改变,它仍然可以正常工作,因为与该字符串对应的id是稳定的。 ,通过Go语言空间中的id解码获得的字符串也有效。导出的C函数无法返回Go内存。在Go语言中,Go从固定的虚拟地址空间分配内存。 C语言分配的内存不能使用Go语言保留的虚拟内存空间。在CGO环境中,Go语言运行时将检查默认情况下Go语言是否分配了导出返回的内存,如果已分配,它将抛出运行时异常。
/*
extern int* getGoPtr();
static void Main() {
int* p = getGoPtr();
*p = 42;
}
*/
import "C"
func main() {
C.Main()
}
//export getGoPtr
func getGoPtr() *C.int {
return new(C.int)
}
尽管getGoPtr返回C语言类型的指针,但是内存本身是从Go语言的新函数分配的,该函数是由Go语言运行时统一管理的内存。然后,我们使用C语言的Main函数调用getGoPtr函数,并且默认情况下将发送运行时异常:

$ go run main.go
panic: runtime error: cgo result has Go pointer
goroutine 1 [running]:
main._cgoexpwrap_cfb3840e3af2_getGoPtr.func1(0x20051dc0)
command-line-arguments/_obj/_cgo_gotypes.go:60 +0x3a
main._cgoexpwrap_cfb3840e3af2_getGoPtr(0x20016078)
command-line-arguments/_obj/_cgo_gotypes.go:62 +0x67
main._Cfunc_Main()
command-line-arguments/_obj/_cgo_gotypes.go:43 +0x41
main.main()
/Users/chai/go/src/github.com/chai2010 \
/advanced-go-programming-book/examples/ch2-xx \
/return-go-ptr/main.go:17 +0x20
exit status 2
该异常表示cgo函数返回的结果包含Go语言分配的指针。指针检查操作发生在getGoPtr函数的C语言版本中,该函数是C语言和cgo生成的Go语言的桥梁。以下是cgo生成的C语言版本getGoPtr函数的具体细节(在cgo生成的_cgo_export.c文件中定义):
int* getGoPtr()
{
__SIZE_TYPE__ _cgo_ctxt = _cgo_wait_runtime_init_done();
struct {
int* r0;
} __attribute__((__packed__)) a;
_cgo_tsan_release();
crosscall2(_cgoexp_95d42b8e6230_getGoPtr, &a, 8, _cgo_ctxt);
_cgo_tsan_acquire();
_cgo_release_context(_cgo_ctxt);
return a.r0;
}
_cgo_tsan_acquire是从LLVM项目移植的内存指针扫描功能。它将检查cgo函数返回的结果是否包含Go指针。应当注意,由cgo返回的指针的默认检查成本很高,尤其是当cgo函数返回的结果是复杂的数据结构时,将花费更多时间。如果确保由cgo函数返回的结果是安全的,则可以通过设置环境变量GODEBUG = cgocheck = 0来关闭指针检查行为。
$ GODEBUG=cgocheck=0 go run main.go
关闭cgocheck函数后,运行上面的代码不会导致上面的异常。但应注意,如果Go运行时释放了使用C语言期间的相应内存,则将导致更严重的崩溃。 cgocheck的默认值为1,它对应于检测的简化版本。如果需要完整的检测功能,可以将cgocheck设置为2。
本文来自电脑杂谈,转载请注明本文网址:
http://www.pc-fly.com/a/shoujiruanjian/article-360750-1.html
你10万放家里
谢教授的问题在于
就当他无害通过