lua 源码系列文章:
lua源码欣赏的第六章内容,与 lua 协程相关
Contents
Lua对象
从C层面来看,Lua的状态就是一个 lua_State
,而在同一个Lua虚拟机中,多个 lua_State
共享一个 global_State
。其中 lua_State
不应当被看为一个简单的静态数据结构,而是一个lua “线程” 中的状态机,其中保存着当前"线程"的执行状态、数据栈、调用栈等信息。
Lua 数据栈
Lua 的数据可以被分为值类型和引用类型。在 lstate.h
中使用联合体 Value
来表示。
|
|
可以看出引用类型使用一个指针 GCObject
来间接引用,其他值类型都是直接保存在联合体中。 为了区分联合体中的类型,还需绑定一个额外的类型字段。
在Lua中,数据栈的空间为 2 * LUA_MINSTACK
,而在 Lua 的 C 调用中,数据栈大小只有 LUA_MINSTACK
这么大。(LUA_MINSTACK
默认为 20)
所以在Lua的C调用时,需要显示的扩展栈大小,使用 ldo.c
中的 luaD_growstack
函数来扩展数据栈大小,其中每次调用至少扩展一倍的大小。在扩展数据栈时,值类型的数据可以直接复制,而引用类型的数据需要调用 correctstack
来修正
调用栈
Lua 中的调用栈在一个 CallInfo 结构体中,以双向链表的形式存储在"线程"对象里。
|
|
可以看到,在Lua 5.2实现中,调用栈被封装成一个可以无限扩展的堆栈,而仅在GC时清理掉无用的链表节点。
线程执行与中断
Lua 作为一门嵌入式语言,为了实现不与硬件绑定的协程,在中断和异常处理时统一使用 C 的 longjmp
机制,而当嵌入 C++时,则使用 try / catch
机制来实现。这些是通过宏来切换的。
函数调用
Lua 中的 pcall 是用函数而非语言机制实现的,实现pcall 是在 c 层面的堆栈来保存和恢复状态。
在纯 Lua 函数调用中,一般不涉及C函数的调用。其过程为:
是生成新的CallInfo,修正数据栈,然后把字节码的执行位置跳转到被调用的函数开头。而Lua的return操作则做了相反的操作,恢复数据栈,弹出CallInfo,修改字节码的执行位置,恢复到原有的执行序列上
在 Lua 底层API中,分为 luaD_precall
和 luaD_poscall
,原因即是:
- 在Lua调用时,需要先执行
luaD_precall
来指定字节码的执行位置,而在luaD_poscall
时执行字节码,并恢复状态。 - 在 C 调用时,不需要执行字节码恢复状态,只需执行
luaD_precall
即可。
C 技巧
在 TValue 中采用了 NaNTrick 的技巧来节省内存(Lua 5.2特性),即:根据 IEEE754 中的说明,指数位全为 1 尾数位全为 0 (0xfff8000000000000
) 用来表示这并不是一个数字。它用来表示无穷大,以及数字除0的结果。而当双精度浮点数大于 0xfff8000000000000
情况下,则可以认为这是刻意造出的数字,可用于特殊用途。
NaNTrick则利用这种情况,在32位机器上,将一个双精度浮点数的尾数位(52位)用于储存除数字类型外的其他类型(32位)和值类型(int)足够使用。这样每个值可以节省4个字节的内存。
Reference
- IEEE_754
- float 采用IEEE R32.24, double 采用 IEEE R64.53
float
double
https://github.com/xiaocang/lua-5.2.2_with_comments/releases/tag/data_n_call_stack