首页 » 技术文章 » 【源码解读】lua源码分析之源码执行简介

【源码解读】lua源码分析之源码执行简介

 

在看游戏服务器代码时,发现较多的位置不太明白,主要接触LUA比较少,所以最近尝试阅读下LUA源码。LUA代码的组织就不提了,网上教程较多,主要说一下阅读后对LUA运行期的一些点。

当我们执行一个lua文件时(比如我们执行lua test.lua),此时LUA会先调用相关函数编译该lua文件,将其编译成字节码(一种lua解释器能执行的"机器码",这里说的"机器码"并非CPU真正能执行的机器指令,也不是汇编指令,而是lua可认知的指令集),然后lua解释器指令解释字节码,并执行相关操作。

lua在将一个文件编译成字节码时,会将整个文件生成为一个闭包,可以认为LUA在解释到一个函数时(lua中定义一个函数)也是将其解释成一个闭包,也就是会生成OP_CLOUSER指令;编译时碰到变量赋值时,如果不带local开头,则会尝试先在外部函数中查找该变量,若查到,则设置相关upval信息,若未查找到,则会生成OP_GETGLOBAL或OP_SETGLOBAL指令(而不论是否在一个函数中,也就是说在一个函数中定义a=1,lua解释时,若未在外部函数中找到变量,也会生成存取全局变量的指令);编译时碰到访问某个变量,则会根据不同情形生成不同的指令,若在编译树中局部变量中找到,则直接生成OP_MOVE指令(包含闭包访问外部的变量),否则生成OP_GETUPVAL或OP_GETGLOBAL指令;当碰到其他类型的操作(比如加减乘除等操作),会生成相应的指令,解释运行时执行相应的C函数。其他操作比如存取TABLE中元素,原理类似,不再一一赘述;较为关键的就是当LUA碰到函数调用时,此时会生成CALL指令,至于CALL的对象,则与变量查询方法一致(最后可能会回溯到全局层查询该函数);当一个LUA函数执行完毕时,会生成OP_RETURN指令,LUA解释器执行时碰到该指令作相应的处理。   lua在编译完一个LUA文件后,此时生成了一个闭包,如果此时执行该文件,则逐个指令解释并运行,其伪代码为:

luaV_execute(){
     reentry:
     while(读取指令){
          OP_XXX:do_XXX;
          OP_CALL:保存当前现场,将被调用的函数(闭包)加载,然后goto reentry, 继续while循环执行指令;
          OP_RETURN:恢复上层调用相关现场,然后goto reentry,继续while循环执行调用的下一条指令;
     }
}

从上述可以看出,LUA虚拟机可认为就是一个死循环,不断执行指令,碰到函数调用,也继续在这个C函数中执行(并不会像C一样堆栈往上长,返回时回溯)。当函数调用时需要保留调用层次信息的,LUA是通过一个调用链来实现的,即在发生函数调用时,会将被执行的闭包链接到一个队列中,返回时弹出队列,以此方法来实现。当然这里描述的有些不准确,但大体流程是这样。

介绍完上述后,应该印象中有一个LUA大体的执行流程了,但这里并没有介绍如何实现闭包,如何实现协程。

先简单介绍闭包(还没有看的很细),闭包其实需要LUA编译时的配合,LUA代码在编译时,若发现一个函数访问了外部函数变量(编译树),会在该函数对象中生成upval信息(表明该函数引用了外部函数的变量),并在解释执行时,将其指向外部变量,但当外部函数返回时,则会拷贝外部变量(区分值类型与引用类型),因此就实现了闭包的效果。

而协程的实现,其实是在luaV_execute中通过特殊返回值来控制的,下面用一段简单的lua代码来说明执行流:

local co = coroutine.create(function ()
     print("X")
     yield
end)
coroutine.resume(co)
g_var=1

编译时碰到coroutine.create,会生成OP_CALL指令,此时会去全局表中寻找变量信息,寻找coroutine,LUA最终会去GLOBAL["_LOAD"]表中寻找,找到后coroutine表,然后获取到coroutine["create"]对象,然后CALL之,其后就会调用到LUA的C代码lua_newthread,然后会生成一个新的lua_State,该变量会保存该协程的运行时信息。此时就算执行完毕了,当调用coroutine.resume(co)时,最终会调用到lua_resume,lua_resume最终经过一系列调用会调用luaV_execute中,解释LUA字节码并执行,此时的C堆栈长这个样子:

luaV_execute   此时协程中代码在这里执行了
...
lua_resume
luaV_execute   在执行协程前,lua代码在这儿执行
....
main

上层luaV_execute当碰到yield时,会返回,这个在前面的伪代码中未提及;当前如果协程中未yield,但所有执行流都跑完了,也会从luaV_execute中返回;因此luaV_execute其实有两个返回路径(并非前面说的真是一个死循环),碰到yield时,lua_State中的执行流完成时(每个协程都有自己的lua_State);

到这里对协程执行流应该有一个简单的认识了,接下来介绍一个最为重要的数据结构,LUA中的TABLE。

lua中太多依赖table了,比如执行时寻找全局变量,也是去table中查找,如果table中未查到,会进一步查看其元表,以及__index是否设置,尝试进一步的查找(究竟谁先谁后暂时还没特别关注)。

lua Table的实现代码,建议不用过分关注了,反正跟C++中的map差不多(当然实现原理不同,一个是红黑树,一个是数组+HASH表)。

lua string,也是适用特别频繁,定义一个变量时,该变量名是一个字符串,定义一个函数时,函数也是一个字符串,跨文件查找变量,适用字符串作为索引来查。

为了提升性能,这些字符串最终适用其指针来查,不做字符串比对,当新创建一个字符串时,会先在lua中一个hash表中查看是否该字符串已存在,存在则直接返回其指针,否则新生成并存储在lua的字符串表中。

lua_gc的实现:lua所有对象(值对象不算)会被链表链接起来,然后GC时,遍历全局表以及lua_State引用的对象,并进行递归遍历,每访问到一个对象,将该对象中的标记设置为不可回收,当标记完成后,再遍历链表,将未被标记不可回收的对象回收即可(当然实际实现没如此简单,它还实现了分多步运行,避免一次性GC完,卡顿程序过久)。

其它.... PS:这里只是简单些了下阅读总结,很多内容描述可能不准确

本文作者:马良

原文链接:【源码解读】lua源码分析之源码执行简介,转载请注明来源!

0