【Free Style】CGO: Go与C互操作技术(二):C调Go基本原理

举报
赵志强 发表于 2017/11/17 15:53:29 2017/11/17
【摘要】 C调Go的过程相对Go调C来说更为复杂,又可以分为两种情况。一是从Go调用C进入的状态回调Go,这是比较常用的情况。二是从一个原生的C线程回调Go,这个情况更为复杂,runtime为这样的过程做了大量的准备。出现一个原生的C线程回调Go的情况,可能是主程序是C,也可能是Go调入C之后,在C中又创建了新的线程。我们着重说明上面提到的比较常见的情况下的C调Go。对于第二种从原生的C线程回调Go的情况,

CGo的过程相对GoC来说更为复杂,又可以分为两种情况。一是从Go调用C进入的状态回调Go,这是比较常用的情况。二是从一个原生的C线程回调Go,这个情况更为复杂,runtime为这样的过程做了大量的准备。出现一个原生的C线程回调Go的情况,可能是主程序是C,也可能是Go调入C之后,在C中又创建了新的线程。我们着重说明上面提到的比较常见的情况下的CGo。对于第二种从原生的C线程回调Go的情况,也大致介绍一下相关原理

示例代码

示例代码的逻辑为:在Go中定义一个add3函数,然后把这个函数exportC使用;在C中定义一个add3_c函数,并调用这个Go中的add3;在Go的主程序中再调用C中的add3_c函数。示例代码如下



image.png

image.png

image.png 

cgo_export.c
关注上述
main.go中的代码,add3即为用于被C调用的函数。在函数上方的//export add3即为告诉cgo在编译时生成一个用于C调用的add3函数。这个又cgo生成add3函数,才是C程序正真调用的函数

上面提到add3函数定义在_cgo_export.c中。代码如下:

image.png

image.png

面的博客介绍了GoC的原理,所以在本文中默认读者已经清楚了Go是如何调用进入C的。这作为本文的基本出发点,即从add3_c函数开始,如何通过调用上面定义的add3函数,进而调入到Go中定义的add3函数。上面cgo产生的这个add3函数,只是一个桩函数。这个函数的执行,需要等待runtime初始化之后。然后在这里面定义了一个按照Go函数的入参规则定义的一个结构体。与GoC不同,这里面发生一次显式的参数拷贝。_cgoexp_3f63814d8a5f_add3Go中定义的桩函数,与GoC类似,只是这个是一个实实在在的Go函数,在链接的时候暴露到C中。这个函数的细节在后面继续深入。在知道了这个代表的意义之后,就可以理解crosscall2的作用

crosscall2类似于asmcgocall,它是从C直接通过ABI call进入Go的函数。接收Go中的函数地址,以及参数地址和参数的大小。经过这个函数,已经开始进入Go程序之中,在执行Go函数,但并不是用户代码。在进入用户代码之前,还有很长的路要走。它的函数声明为:

image.png

通过这些编译制导语句,在链接时把这个函数的地址暴露给C程序。以此为入口,由C进入Go之中。这个函数中的_cgoexpwrap_3f63814d8a5f_add3又是一个接口函数。这个函数已经是很接近用户定义的函数了


image.png

C调Go的主要故事发生在_cgo_runtime_cgocallback函数之后,即runtime.cgocallback函数

cgocallback

在介绍cgocallback之前,我们再次考虑Go函数与C函数的不同。在Go中运行的用户代码,必须受runtime的管控,这是基本出发点。当程序运行从C进入Go之中,同样要遵守这样的规则。即需要给Go函数准备必须的GMP。如果此时的C程序,是从Go中进入的,那此时CGo即可直接使用原来的G即可。如果,此时的C程序运行在一个原生的C线程上。那这个纯粹的pthread是没有M的概念的。为让Go程序有一个M的环境执行,runtime需要给这个原生的C线程安装一些东西,把它伪装成一个M。此外,此时的运行仍然处于系统调用状态,以及线程栈上。还需要退出系统调用状态和切换到普通的G栈上

image.png

这里cgocallback函数只是一个跳板函数。之后进入cgocallback_gofunc

cgocallback_gofunc

这个函数有三个任务

  • 判断当前线程是否为Go的线程,如果不是则把它伪装一

  • 把栈从线程栈切换到G

  • 把函数地地址、参数地址等信息入栈,并记录栈顶地

这个函数中需要判断出当前的线程是从GoC的状态,还是一个原生的C线程状态。如果是原生的C线程,在这个函数中会做一些操作把当前线程伪装成一个Go的线程。这个情况在后面再进一步讨论。下面讨论是Go的线程的状态。cgocallback_gofunc在确认当前的线程是Go的线程之后,把栈从线程栈切换到G栈,然后把函数地址和参数地址入栈,栈顶信息记录到G中,后进入到cgocallbackg

cgocallbackg

这个函数实现线程系统调用状态的退出,此时程序运行在G栈上。进入cgocallbackg1函数

image.pngimage.png

cgocallbackg1

这个函数,首先会判断当前是否需要补充extrem用于补给原生的C线程伪装成Go线程所用的组件。这个在后面介绍原生的C线程调用Go时,会再次提到。之后,这个函数会重新拿到传入函数的地址和参数地址,并经过reflectcall函数给所执行函数选择合适frame的执行函数

image.png

image.png


着实是一种丑陋的方式啊,没办法

CALLFN

这是一个plan 9的宏定义,实现了callXXXXX等一系列函数。在这个函数根据传递进来的参数地址,把参数值从C的内存中传递到Go的内存,并负责把在Go中计算的结果传会C的内存

image.png

至此终于进入了_cgoexpwrap_3f63814d8a5f_add3函数

C的线程的情况

对于一个纯C的线程,需要一个extram的结构负责把该线程伪装成一个Go的线程。当然,这又要分主程序是CGo两种情况。先考虑主程序是Go的情况。在一个Go程序初始化时,在使用cgo的情况下,就会为这样的情况准备了一个extram,在mstart1

image.png

extram是一个全局链表,记录着所有的extramextram就是一个M,只是这个M没有一个绑定的线程而已。除此之外,runtime中的M全是和线程绑定在一起的。另外,这个extram,在创建时就处于系统调用状态,并且不仅有一个g0还有一个g,这都是为CGo准备的

在一个纯C的线程拿走一个extram之后,此时系统并没有为这个链表补充新的。等到拿走extram的线程执行到cgocallbackg1时,才会为这个链表补充一个新的extram,留待其他的线程使用

对于主程序是C的情

这种情况下,Go一般是便以为c-shared/c-archive的库给C调用。C函数调入进Go,必须按照Go的规则执行。这个规则是不会变的,所以当主程序是C调用Go时,也同样有一个GoruntimeC程序并行执行。这个runtime的初始化在对应的c-shared的库加载时就会执行。因此,在一开始就有两个线程执行,一个C的,一个是Go的。此时,Go的初始化入口为_rt0_amd64_linux_lib,这是在链接时写入的。关于这个c-shared的使用,后面会有专门的文章介绍这个出现的一个问题,敬请期待

CGo原理图

最后给出一个CGo的原理示意图

1.pngimage.png



【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

举报
请填写举报理由
0/200