您的位置:首页 > 编程语言 > Lua

Lua源代码阅读(五)数据栈与调用栈组成的 线程(协程)

2015-01-20 15:49 337 查看

Lua源代码阅读(五)数据栈与调用栈组成的 线程(协程)

2015-01-20 15:49
714人阅读 评论(0)
收藏
举报


分类:
C/Lua/C++(57)


版权声明:本文为博主原创文章,未经博主允许不得转载。



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 。

lua_State 是暴露给用户的数据类型。从名字上看,它想表示一个 lua程序的执行状态,在官方文档中, 它指代 lua的一个线程。每个线程拥有独立的数据栈以及函数调用链,还有独立的调试钩子和错误处理设 施。所以我们不应当简单的把 lua_State 看成一个静态的数据集,它是一组 lua程序的执行状态机。所有的 luaC API 都是围绕这个状态机,改变其状态的:或把数据压入堆栈,或取出,或执行栈顶的函数,或继续 上次被中断的执行过程。

同一 lua虚拟机中的所有执行线程,共享了一块全局数据 global_State 。在 lua的实现代码中,需要访 问这个结构体的时候,会调用宏



          

    

 
忽略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的状态,是线程不安全的。
 */

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: