jasper的技术小窝

关注DevOps、运维监控、Python、Golang、开源、大数据、web开发、互联网

深入Golang之CGO

作者:jasper | 分类:Golang | 标签:   | 阅读 699 次 | 发布:2018-01-21 11:22 p.m.

在Golang中可以直接调用C语言的代码,之所以有这样的设计是因为Golang的定位虽然是"next C",但是一些底层的实现还是没有C强大的,这样可以解决一些类似的问题;其次毕竟Golang是一门年轻的语言,很多库可能不太健全,可以依靠C语言这样的老前辈自然会方便很多。下面我们就来探一探Golang中调用C的一些原理。

例子

首先我们从一个例子开始来简单地看一下在Golang中调用C的方法。

package main

/*
int PlusOne(int n)
{
    return n + 1;
}
*/
import "C"

import (
    "fmt"
)

func main() {
    var n int = 10
    var m int = int(C.PlusOne(C.int(n))) // 类型要转换
    fmt.Println(m)                       // 11
}

这样我们就在Golang里面简单地嵌入一段C代码,虽然例子比较简单,但是在写的时候在格式上有很严格的要求;另外对于使用C语言变量、函数、结构体、联合体、回调函数和动态链接库等等细节的具体使用可以去看官方的文档,我们这里还是着重在实现的原理上。

实现原理

在调用C的时候,可以看到需要的信息有两个函数名和函数的参数,会将这两个信息传入到函数cgocall之中:

func cgocall(fn, arg unsafe.Pointer) int32 {
}

下面我们来看一下整个的过程,首先要做的是锁住当前的g:

lockOSThread()
mp := getg().m
mp.ncgocall++
mp.ncgo++
mp.incgo = true

其目的是为了当调用C之后有callback的话,可以确保在相同的堆栈之中。

然后Golang会声明一个进入syscall的一个指令runtime.entersyscall,这时候调度器会创建另一个M去运行其他代码的goroutine。而在出系统调用函数之后,会调用runtime.exitsyscall。而调用cgo的真正逻辑在函数asmcgocall中完成:

entersyscall(0)
errno := asmcgocall(fn, arg)
exitsyscall(0)

需要特别说明的是enter和exit syscall在里面的作用,runtime.entersyscall会将P的M剥离并将它设置为PSyscall状态,告知系统此时其它的P有机会运行,以保证始终是GOMAXPROCS个P在运行。runtime.entersyscall函数会立刻返回,它仅仅是起到一个通知的作用,这样把cgo的C函数调用像系统调用一样独立出去了,不让它影响运行时库,这个goroutine也就不必参与调度了。而其它部分的goroutine正常的运行不受影响。而runtime.exitsyscall函数会查看当前仍然有可用的P,则让它继续运行,否 则这个goroutine就要被挂起了。

接下来我们来看一下asmcgocall的实现,asmcgocall是用汇编语言来实现的:

TEXT ·asmcgocall(SB),NOSPLIT,$0-20
    MOVQ    fn+0(FP), AX
    MOVQ    arg+8(FP), BX

    MOVQ    SP, DX

    // 切换到m的g0栈(这个栈是操作系统分配的栈)
    get_tls(CX)
    MOVQ    g(CX), R8
    CMPQ    R8, $0
    JEQ nosave
    MOVQ    g_m(R8), R8
    MOVQ    m_g0(R8), SI
    MOVQ    g(CX), DI
    CMPQ    SI, DI
    JEQ nosave
    MOVQ    m_gsignal(R8), SI
    CMPQ    SI, DI
    JEQ nosave

    // 切换到系统栈
    MOVQ    m_g0(R8), SI
    CALL    gosave<>(SB)
    MOVQ    SI, g(CX)
    MOVQ    (g_sched+gobuf_sp)(SI), SP

    // 真正运行C的部分,会gcc编译的代码以及执行
    SUBQ    $64, SP
    ANDQ    $~15, SP    // alignment for gcc ABI
    MOVQ    DI, 48(SP)  // save g
    MOVQ    (g_stack+stack_hi)(DI), DI
    SUBQ    DX, DI
    MOVQ    DI, 40(SP)  
    MOVQ    BX, DI      // DI = AMD64 ABI中的第一个参数
    MOVQ    BX, CX      // CX = Win64中的第一个参数
    CALL    AX

    // 重置g stack
    get_tls(CX)
    MOVQ    48(SP), DI
    MOVQ    (g_stack+stack_hi)(DI), SI
    SUBQ    40(SP), SI
    MOVQ    DI, g(CX)
    MOVQ    SI, SP

    MOVL    AX, ret+16(FP)
    RET

在上述的代码里面我们看到,切换到g0栈之后,由于g0可以看作一个“无穷”的栈,这样就不必担忧分段栈方面的问题。

调用结束之后,编译器会分别确保函数名和参数在函数被调用的时候是alive的,然后结束cgo,并返回函数调用的errno:

KeepAlive(fn)
KeepAlive(arg)

endcgo(mp)
return errno

endcgo里面会相应的对os thread做解锁操作。

其中还有一部分会涉及到C调用Golang的地方,比如cgo的callback,cgocallback函数也是用汇编来实现的,正好和上面的相反,它需要运行在一个真实的goroutine栈中(不是m->g0栈),所以会先exitsyscall, 再执行cgocallbackg1,然后再reentersyscall。这在实际的使用中很少会用到,这里的细节就不多说了。

总结

对于Golang调用C,其需要解决的核心问题其实都是提供一个C运行环境来执行相应的代码。C的执行环境需要一个不使用分段的栈,并且执行C代码的goroutine需要暂时地脱离调度器的管理。要达到这些要求,运行时提供的支持就是切换栈,以及runtime.entersyscall。

在每一次调用CGO的时候,都会起一个新的goroutine,并且这个goroutine会脱离sched的管理,这意味着如果我们在程序中使用了大量的CGO的时候,就会有很多goroutine产生。

另外在内存方面,Golang的内存有GC照看,C的内存要自己C.free,不然会产生内存泄漏。


转载请注明出处:http://www.opscoder.info/golang_cgo.html

【上一篇】 深入Golang之unsafe
【下一篇】 etcd-raft使用分析
其他分类: