从这里开始,将深入挖掘关于运行时库部分对于cgo的支持。还记得前面那个test.go吗?这里将继续以它为例子进行分析。
从Go中调用C的函数test,cgo生成的代码调用是runtime.cgocall(_cgo_Cfunc_test, frame):
void
·_Cfunc_test(struct{uint8 x[8];}p)
{
runtime·cgocall(_cgo_1b9ecf7f7656_Cfunc_test, &p);
}
其中cgocall的第一个参数_cgo_Cfunc_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(_cgo_Cfunc_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栈,然后调用_cgo_Cfunc_test函数。由于m的g0栈不是分段栈,因此切换到m->g0栈(这个栈是操作系统分配的栈)后,可以安全地运行gcc编译的代码以及执行_cgo_Cfunc_test(frame)函数。
_cgo_Cfunc_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内存的地方只是一个烟雾弹迷惑人的。
好吧,提两个疑问结束这一节内容: