Go调用C
2021.07

9.3 Go调用C

从这里开始,将深入挖掘关于运行时库部分对于cgo的支持。还记得前面那个test.go吗?这里将继续以它为例子进行分析。

从Go中调用C的函数test,cgo生成的代码调用是runtime.cgocall(cgoCfunc_test, frame):

void
·_Cfunc_test(struct{uint8 x[8];}p)
{
    runtime·cgocall(_cgo_1b9ecf7f7656_Cfunc_test, &p);
}

其中cgocall的第一个参数cgoCfunc_test是一个由cgo生成并由gcc编译的函数:

void
_cgo_1b9ecf7f7656_Cfunc_test(void *v)
{
    struct {
        int p0;
        char __pad4[4];
    } __attribute__((__packed__)) *a = v;
    test(a->p0);
}

runtime.cgocall将g锁定到m,调用entersyscall,这样不会阻塞其它的goroutine或者垃圾回收,然后调用runtime.asmcgocall(cgoCfunc_test, frame)。

void
runtime·cgocall(void (*fn)(void*), void *arg)
{
    runtime·lockOSThread();
    runtime·entersyscall();
    runtime·asmcgocall(fn, arg);
    runtime·exitsyscall();

    endcgo();
}

将g锁定到m是保证如果在cgo内又回调了Go代码,切换回来时还是在同一个栈中的。关于C调用Go,具体到下一节再分析。

runtime.entersyscall宣布代码进入了系统调用,这样调度器知道在我们运行外部代码,于是它可以创建一个新的M来运行goroutine。调用asmcgocall是不会分裂栈并且不会分配内存的,因此可以安全地在”syscall call”时调用,不用考虑GOMAXPROCS计数。

runtime.asmcgocall是用汇编实现的,它会切换到m的g0栈,然后调用cgoCfunc_test函数。由于m的g0栈不是分段栈,因此切换到m->g0栈(这个栈是操作系统分配的栈)后,可以安全地运行gcc编译的代码以及执行cgoCfunc_test(frame)函数。

cgoCfunc_test使用从frame结构体中取得的参数调用实际的C函数test,将结果记录在frame中,然后返回到runtime.asmcgocall。

重获控制权之后,runtime.asmcgocall切回之前的g(m->curg)的栈,并且返回到runtime.cgocall。

当runtime.cgocall重获控制权之后,它调用exitsyscall,然后将g从m中解锁。exitsyscall后m会阻塞直到它可以运行Go代码而不违反$GOMAXPROCS限制。

以上就是Go调用C时,运行时库方面所做的事情,是不是很简单呢?因为总结起来就两点,第一点是runtime.entersyscall,让cgo产生的外部代码脱离goroutine调度系统。第二点就是切换m的g0栈,这样就不必担忧分段栈方面的问题。

前面讲到m的g0栈时,留了个疑问的。那就是新建M的函数newm只给m的g0栈分配了8K内存,好像并不是一个“无穷”的栈,怎么回事呢?这里回答这个问题……不过我会再额外提两个新问题,希望读者跟着思考(好贱哦,哈哈)。

其实m的g0栈的大小并不在调用newm时分配的8K。在newm函数的最后一步是调用runtime·newosproc,这个函数会调用到操作系统的系统调用,分配一条系统线程。并且做了一个后处理过程–它将m的g0栈指针改掉了!m的g0栈指针会被重新设置为线程的栈,所以前面说m的g0栈是一个“无穷”的栈是正确的,那个分配8K内存的地方只是一个烟雾弹迷惑人的。

好吧,提两个疑问结束这一节内容:

m的g0栈对于每个m是有一个的,cgo调用会切换到这个栈中进行。那么,如果有多次cgo调用同时发生,共用同一个m的栈岂不会冲突?怎么处理?

这一节只分配到了Go调用C,那么如果Go调用C的代码中,又回调了Go函数,这时系统是如何处理的?