【Free Style】CGO: Go与C互操作技术(二):C调Go基本原理
C调Go的过程相对Go调C来说更为复杂,又可以分为两种情况。一是从Go调用C进入的状态回调Go,这是比较常用的情况。二是从一个原生的C线程回调Go,这个情况更为复杂,runtime为这样的过程做了大量的准备。出现一个原生的C线程回调Go的情况,可能是主程序是C,也可能是Go调入C之后,在C中又创建了新的线程。我们着重说明上面提到的比较常见的情况下的C调Go。对于第二种从原生的C线程回调Go的情况,也大致介绍一下相关原理。
示例代码的逻辑为:在Go中定义一个add3函数,然后把这个函数export给C使用;在C中定义一个add3_c函数,并调用这个Go中的add3;在Go的主程序中再调用C中的add3_c函数。示例代码如下:
cgo_export.c
关注上述main.go中的代码,add3即为用于被C调用的函数。在函数上方的//export add3即为告诉cgo在编译时生成一个用于C调用的add3函数。这个又cgo生成add3函数,才是C程序正真调用的函数。
上面提到add3函数定义在_cgo_export.c中。代码如下:
前面的博客介绍了Go调C的原理,所以在本文中默认读者已经清楚了Go是如何调用进入C的。这作为本文的基本出发点,即从add3_c函数开始,如何通过调用上面定义的add3函数,进而调入到Go中定义的add3函数。上面cgo产生的这个add3函数,只是一个桩函数。这个函数的执行,需要等待runtime初始化之后。然后在这里面定义了一个按照Go函数的入参规则定义的一个结构体。与Go调C不同,这里面发生一次显式的参数拷贝。_cgoexp_3f63814d8a5f_add3为Go中定义的桩函数,与Go调C类似,只是这个是一个实实在在的Go函数,在链接的时候暴露到C中。这个函数的细节在后面继续深入。在知道了这个代表的意义之后,就可以理解crosscall2的作用。
crosscall2类似于asmcgocall,它是从C直接通过ABI call进入Go的函数。接收Go中的函数地址,以及参数地址和参数的大小。经过这个函数,已经开始进入Go程序之中,在执行Go函数,但并不是用户代码。在进入用户代码之前,还有很长的路要走。它的函数声明为:
通过这些编译制导语句,在链接时把这个函数的地址暴露给C程序。以此为入口,由C进入Go之中。这个函数中的_cgoexpwrap_3f63814d8a5f_add3又是一个接口函数。这个函数已经是很接近用户定义的函数了。
C调Go的主要故事发生在_cgo_runtime_cgocallback函数之后,即runtime.cgocallback函数。
在介绍cgocallback之前,我们再次考虑Go函数与C函数的不同。在Go中运行的用户代码,必须受runtime的管控,这是基本出发点。当程序运行从C进入Go之中,同样要遵守这样的规则。即需要给Go函数准备必须的G,M,P。如果此时的C程序,是从Go中进入的,那此时C调Go即可直接使用原来的G即可。如果,此时的C程序运行在一个原生的C线程上。那这个纯粹的pthread是没有M的概念的。为让Go程序有一个M的环境执行,runtime需要给这个原生的C线程安装一些东西,把它伪装成一个M。此外,此时的运行仍然处于系统调用状态,以及线程栈上。还需要退出系统调用状态和切换到普通的G栈上。
这里cgocallback函数只是一个跳板函数。之后进入cgocallback_gofunc。
这个函数有三个任务:
判断当前线程是否为Go的线程,如果不是则把它伪装一下
把栈从线程栈切换到G栈
把函数地地址、参数地址等信息入栈,并记录栈顶地址
这个函数中需要判断出当前的线程是从Go调C的状态,还是一个原生的C线程状态。如果是原生的C线程,在这个函数中会做一些操作把当前线程伪装成一个Go的线程。这个情况在后面再进一步讨论。下面讨论是Go的线程的状态。cgocallback_gofunc在确认当前的线程是Go的线程之后,把栈从线程栈切换到G栈,然后把函数地址和参数地址入栈,栈顶信息记录到G中,后进入到cgocallbackg中。
这个函数实现线程系统调用状态的退出,此时程序运行在G栈上。进入cgocallbackg1函数。
cgocallbackg1
这个函数,首先会判断当前是否需要补充extrem用于补给原生的C线程伪装成Go线程所用的组件。这个在后面介绍原生的C线程调用Go时,会再次提到。之后,这个函数会重新拿到传入函数的地址和参数地址,并经过reflectcall函数给所执行函数选择合适frame的执行函数。
着实是一种丑陋的方式啊,没办法。
这是一个plan 9的宏定义,实现了callXXXXX等一系列函数。在这个函数根据传递进来的参数地址,把参数值从C的内存中传递到Go的内存,并负责把在Go中计算的结果传会C的内存。
至此终于进入了_cgoexpwrap_3f63814d8a5f_add3函数。
对于一个纯C的线程,需要一个extram的结构负责把该线程伪装成一个Go的线程。当然,这又要分主程序是C和Go两种情况。先考虑主程序是Go的情况。在一个Go程序初始化时,在使用cgo的情况下,就会为这样的情况准备了一个extram,在mstart1中
extram是一个全局链表,记录着所有的extram。extram就是一个M,只是这个M没有一个绑定的线程而已。除此之外,runtime中的M全是和线程绑定在一起的。另外,这个extram,在创建时就处于系统调用状态,并且不仅有一个g0还有一个g,这都是为C调Go准备的。
在一个纯C的线程拿走一个extram之后,此时系统并没有为这个链表补充新的。等到拿走extram的线程执行到cgocallbackg1时,才会为这个链表补充一个新的extram,留待其他的线程使用。
这种情况下,Go一般是便以为c-shared/c-archive的库给C调用。C函数调入进Go,必须按照Go的规则执行。这个规则是不会变的,所以当主程序是C调用Go时,也同样有一个Go的runtime与C程序并行执行。这个runtime的初始化在对应的c-shared的库加载时就会执行。因此,在一开始就有两个线程执行,一个C的,一个是Go的。此时,Go的初始化入口为_rt0_amd64_linux_lib,这是在链接时写入的。关于这个c-shared的使用,后面会有专门的文章介绍这个出现的一个问题,敬请期待。
最后给出一个C调Go的原理示意图
- 点赞
- 收藏
- 关注作者
评论(0)