在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
Lua源代码阅读(五)数据栈与调用栈组成的 线程(协程)
分类:
版权声明:本文为博主原创文章,未经博主允许不得转载。 1: 若 lua 仅作为一种独立语言,支持协程可能并不算麻烦。可困难在于 lua 生来以一门嵌入式语言存 在,天生需要大量与宿主系统 C 语言做交互。 2: 典型的应用环境是由 C 语言开发的系统,嵌入 lua 解析器, 加载 lua 脚本运行。同时注入一些 C 函数供 lua 脚本调用。lua 作为控制脚本,并不直接控制外界的模块, 做此桥梁的正是那些注入的 C 接口。在较为复杂的应用环境中,这些注入的 C 函数还需要有一些回调方法。 当我们企图用 lua 脚本去定制这些回调行为时,就出现了 C 函数调用
lua 函数,lua 函数再调用 C 函数, 这个 C 函数又调用 lua 函数的层层嵌套的过程。
3: C 语言本身并不支持协程或延续点,一旦中断 lua 协程,就面临 C 语言中调用栈如何处理的难题。
4: 直接操作 C 层面的堆栈,可以较为容易的作到协程的切换。但这样做,会和硬件平台绑定。这是一个在 C 中实现延续点的不错的方法。但这个做法不符合 lua的设计原则。lua为了解决这个问题,对 lua语言的 实现以及和 C 交互的接口设计上做了大量的努力。最终使用标准的 C 语言,实现了完整功能的 lua协程。 5: 刚接触 lua时,从 C 层面看待 lua,lua的虚拟机对象就是一个 lua_State 。但实际上,真正的 lua虚拟机对象被隐藏起来了。那就是lstate.h中定义的结构体 global_State 。
忽略lstate.h中涉及GC 的复杂部分,我们可以先看一眼 lua_State 的数据结构。 /* Lua数据栈的初始化 1:一开始,数据栈的空间很有限,只有 2倍的LUA_MINSTACK(默认是20)的大小。 2:Lua供C使用的栈相关 API都是不检查数据栈越界的,这是因为通常我们编写C扩展都能把数据栈 空间的使用控制在LUA_MINSTACK以内,或是显式扩展。 3: 对每次数据栈访问都强制做越界检查是非常低效的。 4:数据栈不够用的时候,可以扩展。这种扩展是用realloc实现的,每次至少分配比原来大一倍的空间, 并把旧的数据复制到新空间。 */ /* 数据栈的空间扩展 1:数据栈扩展的过程,伴随着数据拷贝。这些数据都是可以直接值复制的, 所以不需要在扩展之后修正其中的指针。 2:但,有些外部结构对数据栈的引用需要修正为正确的新地址。这些需要修正的位置包括 upvalue以及执行栈对数据栈的引用. 3:这个过程由correctstack函数实现 */ Lua调用栈 /*Lua调用栈 1:Lua把调用栈和数据栈分开保存。 2:调用栈放在一个叫做CallInfo的结构中,以双向链表的形式储存在线程对象里。 3:CallInfo 保存着正在调用的函数的运行状态;状态标示存放在lu_byte callstatus中。 4:部分数据和函数的类型有关,以联合形式存放 5:C 函数与 Lua函数的结构不完全相同 6:callstatus中保存了一位标志用来区分是C函数还是Lua函数 7:正在调用的函数一定存在于数据栈上,在CallInfo结构中,func指向正在执行的函数在数据栈上的位置 需要记录这个信息,是因为如果当前是一个Lua函数,且传入的参数个数不定的时候,需要用这个位置和当 前数据栈底的位置相减,获得不定参数的准确数量 8:同时,func还可以帮助我们调试嵌入式Lua代码:在用 GDB这样的调试器调试代码时,可以方便的查看C中 的调用栈信息,但一旦嵌入Lua ,我们很难理解运行过程中的Lua代码的调用栈;不理解Lua的内部结构, 就可能面对一个简单的lua_State变量束手无策. 9:实际上,遍历L中的Ci域指向的CallInfo链表可以获得完整的Lua调用链; 而每一级的CallInfo中,都可以进一步的通过 func域取得所在函数的更详细信息。 当func为一个Lua函数时,根据它的函数原型,可以获得源文件名、行号等诸多调试信息。 10:CallInfo是一个标准的双向链表结构,不直接被GC模块管理 11:这个双向链表表达的是一个逻辑上的栈, 在运行过程中,并不是每次调入更深层次的函数, 就立刻构造出一个CallInfo节点。 12:整个CallInfo链表会在运行中被反复复用。直到GC的时候才清理那些比当前调用层次更深的无用节点。 lstate.c中有luaE_extendCI的实现 13:也就是说,调用者只需要把CallInfo链表当成一个无限长的堆栈使用即可 14:当调用层次返回,之前分配的节点可以被后续调用行为复用。 15:在GC的时候只需要调用luaE_freeCI就可以释放过长的链表。 */ 线程
/* 线程 1:把数据栈和调用栈合起来就构成了Lua中的线程。 2:在同一个Lua虚拟机中的不同线程因为共享了global_State而很难做到真正意义上的并发。 3:它也绝非操作系统意义上的线程,但在行为上很相似。用户可以resume一个线程, 线程可以被yield打断。 4:Lua的执行过程就是围绕线程进行的。 5:我们从lua_newthread阅读起,可以更好的理解它的数据结构。 6:这里我们能发现,内存中的线程结构并非lua_State,而是一个叫LX的东西。 */ /*LX的定义 1:在lua_State之前留出了大小为LUAI_EXTRASPACE字节的空间。 2:面对外部用户操作的指针是L而不是LX,但L所占据的内存块的前面却是有所保留的。 3:这是一个有趣的技巧。用户可以在拿到L指针后向前移动指针,取得一些LUAI_EXTRASPACE中额外的数据。 4:把这些数据放在前面而不是lua_State结构的后面避免了向用户暴露结构的大小。 5:这里,LUAI_EXTRASPACE是通过编译配置的,默认为0; 6:开启LUAI_EXTRASPACE后,需要一系列的宏提供支持(luai_userstateopen(L)。。。。) 7:给L附加一些用户自定义信息在追求性能的环境很有意义。可以在为Lua编写的C模块中, 直接偏移L指针来获取一些附加信息。这比去读取L中的注册表要高效的多。 8:另一方面,在多线程环境下,访问注册表本身会改变L的状态,是线程不安全的。 */ |
请发表评论