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

来源:互联网 发布:美国行业自律数据保护 编辑:程序博客网 时间:2024/05/19 02:24

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

714人阅读 评论(0)收藏举报
分类:

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:LuaC使用的栈相关 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的状态,是线程不安全的。

 */

原创粉丝点击