对Lua的理解

在redis和nginx中都潜入了Lua环境用于快速上手开发。但如何理解Lua以及Lua与宿主环境的交互是需要掌握的。

Lua本身

首先是Lua本身,打开5.1的lua版本开始编译后最后生成一个lua的可执行文件,这其实就是一个包含了Lua虚拟机的终端.。所以其实在不管redis也好nginx也好,也是在其中生产了一个Lua环境(虚拟机),然后将nginx里面/redis里面的一些函数暴漏出去,到达在Lua里面操作宿主环境的效果。

 其实lua.c很简单:核心就是调用创建一个lua虚拟机环境 lua_State,然后卡你是接受输入的字符将其解析后给虚拟机执行。 liblua.a是核心静态库。

 也就是说lua.c提供了一个范例,让其他宿主服务如何将lua的环境其加载进去。

在宿主中创建完Lua的State后,我们的目的是为了操作宿主里面的东西,所以需要将宿主里面的功能注入到State里面去以便Lua操作。

Lua与C的交互

我们最终是期望通过将Lua来调用c里面实现的功能,也就是说,我们可以通过编写C函数(宿主功能),然后将其注入到Lua的State里面去,然后更好滴通过Lua来操作宿主里面的东西。

(别忘记初衷是为了理解redis/nginx如何和lua交互的,要从这里开始找切入口,然后再遇到问题后再展开,一股插进lua的源代码中肯定效率不高。最好的办法是从redis里面入手看redis是如何实现和Lua的交互的,这样才能有对Lua虚拟机的高效率地理解。)看redis是如何调用Lua的Capi的,调用了哪些

下面是一个例子:将c编译为so后,然后暴漏api或者函数供Lua调用。

这是c函数

///mylib.c

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include "../src/lua.h"
#include "../src/lauxlib.h"              //需要引用lua的api头文件放在
#include <dirent.h>
#include <errno.h>

static int l_dir(lua_State *L) {       //输出该目录下所有的文件
    DIR *dir;
    struct dirent *entry;
    int i;

    const char *path = luaL_checkstring(L, 1);

    dir = opendir(path);
    if (dir == NULL) {
        lua_pushnil(L);
        lua_pushstring(L, strerror(errno));
        return 2;
    }

    lua_newtable(L);
    i = 1;
    while((entry = readdir(dir)) != NULL) {
        lua_pushnumber(L, i++);
        lua_pushstring(L, entry->d_name);
        lua_settable(L, -3);
    }
    closedir(dir);
    return 1;
}

int l_map(lua_State *L) {          //将table中的每个成员应用一个函数并将结果放回
    int i,n; 
    luaL_checktype(L, 1, LUA_TTABLE);
    luaL_checktype(L, 2, LUA_TFUNCTION);

    n = lua_objlen(L, 1);

    for(i=1;i<n;i++) {
        lua_pushvalue(L, 2);
        lua_rawgeti(L, 1, i);
        lua_call(L, 1,1);
        lua_rawseti(L, 1, i);
    }
    return 0;
}
static const struct luaL_Reg mylib[] = {    //函数在lua中使用别名对应handler
    {"dir", l_dir},
    {"map", l_map},
    {NULL, NULL}
};


int luaopen_mylib(lua_State *L) {
    luaL_register(L, "mylib", mylib);      //注册
    return 1;
}

然后将其编译为so

Makefile: 

mylib.o: mylib.c
    gcc -c -fPIC mylib.c -o mylib.o
mylib.so: mylib.o
    gcc -o mylib.so mylib.o -shared -lm

然后通过Lua进行调用: mylib.lua

local mylib = require "mylib"            -- 加载

local ans = mylib.dir("./")              -- 调用c 
for k, v in pairs(ans) do
    print(k, "-->", (v))
end

local aa = {1,2,3,4}
local function double_aa(i)           
    return i * 3
end

mylib.map(aa, double_aa)                 -- 调用c
for k, v in pairs(aa) do
    print(v)
end

结果输出: 可以看到将其结果都输出了。lua mylib.lua

1       -->     lua_call_c.o
2       -->     ..
3       -->     lua_from_c.c
4       -->     .mylib.c.swp
5       -->     stack_dump.c
6       -->     lua_api_demo
7       -->     mylib.so
8       -->     mylib.c
9       -->     .
10      -->     .Makefile.swp
11      -->     stack_dump
12      -->     c_call_lua.c
13      -->     extension
14      -->     script.lua
15      -->     apidemo.c
16      -->     lua_call_c.so
17      -->     demo_run.lua
18      -->     config.lua
19      -->     Makefile
20      -->     extension.c
21      -->     parse.c
22      -->     parse
23      -->     luacall.lua
24      -->     mylib.o
3
6
9
4

lua c api 

定义在lua.h中的api都以lua_xx开头,注重正交性/简洁性

定义在lauxlib.h中的api都以luaL_xx开头,他使用lua.h中的api进行组合,更加实用地完成一些通用任务。但要注意luaL_xxx的api都不能直接操作Lua核心,他所有的任务都是通过lua.h中的api完成的。

都知道lua和c交互数据是通过一个栈,那具体是如何呢?比如我要通过c从lua获取一个值,这个值怎么传给c?答案是你调用lua时,lua会将值push到栈顶,c你从栈顶拿就可以。如果c想将一个值传递给lua,c将这个值push到这个栈顶,然后调用lua(call lua),此时会pop这个栈就能获取。

Lua manipulates this stack in a strict LIFO discipline (Last In, First Out; that is, always through the top). When you call Lua, it only changes the top part of the stack. Your C code has more freedom; specifically, it can inspect any element inside the stack and even insert and delete elements in any arbitrary position.

这个地方要注意:就是Lua只能操作栈顶,要么push到栈顶,要么从栈顶pop。但c的话就很灵活可以任意操作这个栈的任何地方。(因为栈是在c里面定义的,c api可以任意摆弄)

所以 如果通过lua来调用c函数,也得遵守这个规范,首先我们需要先注册这个c函数,也就是告知lua他的地址。然后lua负责将参数传给c,c负责完成具体任务后将结果给lua,这个过程就是栈在里面起作用了。 The C function gets its arguments from the stack and pushes the results on the stack.。另外为了区分,To distinguish the results from other values on the stack, the function returns (in C) the number of results it is leaving on the stack. 

比如此处的C函数,lua_tonumber(L, 1)就从栈顶获取lua给他的参数,然后将结果push栈,并将结果个数也返回给lua。然后将其注册进去:

    lua_pushcfunction(l, l_sin);
    lua_setglobal(l, "mysin");

其中 lua_pushcfunction是:

Before we can use this function from Lua, we must register it. We do this magic with lua_pushcfunction: It gets a pointer to a C function and creates a value of type "function" to represent this function inside Lua. 

 lua_pushcfunction: pushes a value of type function. 将一个函数地址push进去,然后将该地址弹出,用mysin这个全局变量关联起来。

lua_setglobal: Pops a value from the stack and sets it as the new value of global name.

===

刚说到如果是c要给lua返回结果将其压入栈顶即可,那如果返回的数据比较多,或者如果要返回一个table该怎么办?其实也是初始化填充这个table,然后将table压入栈,比如:

这里lua_newtable(L): Creates a new empty table and pushes it onto the stack. It is equivalent to lua_createtable(L, 0, 0). 就是创建一个空table压入栈,那么如何初始化这个栈的:

看lua_pushnumber(L,i++)先压入了一个number,然后lua_pushstring(L,...)压入了一个string,,此时栈上情况应该如下:

然后lua_settable(L,-3),此时应该是将-3位置的table填充,用-2和-1的位置分别作为k和v

Does the equivalent to t[k] = v, where t is the value at the given index, v is the value at the top of the stack, and k is the value just below the top.

然后This function pops both the key and the value from the stack. 

所以通过这样最终其实返回的就是这个栈顶的table(被填充后)

通过这里例子可以知道,lua调用c是通过c的函数地址寻址的,将函数地址和某个全局变量关联起来,然后lua通过该变量就可以实现call c函数。

熟悉这些api有助于实现我们期望的能力

https://www.lua.org/manual/5.3/manual.html#lua_gettable

操作表

lua_call 

void lua_call (lua_State *L, int nargs, int nresults);

这里就是调用函数:将函数地址最先入栈,然后将参数随后入栈,然后调用后将结果压栈,然后将结果弹出赋值给全局a

Lua如何与redis交互

先来看如何通过lua使用redis ,首先启动redis,然后连接上去,通过发送 命令让其执行lua脚本:

 eval "redis.call('set', KEYS[1], ARGV[1]); redis.call('expire', KEYS[1], ARGV[2]); return 1;" 1 userage 10 60

其中如果直接用命令行交互模式使用eval的话,相关的语法为:

EVAL script numkeys key [key …] arg [arg …]

script参数是一段 Lua5.1 脚本程序。脚本不必(也不应该[^1])定义为一个 Lua 函数
numkeys指定后续参数有几个key,即:key [key …]中key的个数。如没有key,则为0
key [key …] 从 EVAL 的第三个参数开始算起,表示在脚本中所用到的那些 Redis 键(key)。在Lua脚本中通过KEYS[1], KEYS[2]获取。
arg [arg …] 附加参数。在Lua脚本中通过ARGV[1],ARGV[2]获取。

这样key也就是告诉lua有几个键,arg是后面需要用到的一些附加参数。

比如:

// 例1:numkeys=1,keys数组只有1个元素key1,arg数组无元素
127.0.0.1:6379> EVAL "return KEYS[1]" 1 key1
"key1"

// 例2:numkeys=0,keys数组无元素,arg数组元素中有1个元素value1
127.0.0.1:6379> EVAL "return ARGV[1]" 0 value1
"value1"

// 例3:numkeys=2,keys数组有两个元素key1和key2,arg数组元素中有两个元素first和second 
//      其实{KEYS[1],KEYS[2],ARGV[1],ARGV[2]}表示的是Lua语法中“使用默认索引”的table表,
//      相当于java中的map中存放四条数据。Key分别为:1、2、3、4,而对应的value才是:KEYS[1]、KEYS[2]、ARGV[1]、ARGV[2]
//      举此例子仅为说明eval命令中参数的如何使用。项目中编写Lua脚本最好遵从key、arg的规范。
127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second 
1) "key1"
2) "key2"
3) "first"
4) "second"


// 例4:使用了redis为lua内置的redis.call函数
//      脚本内容为:先执行SET命令,在执行EXPIRE命令
//      numkeys=1,keys数组有一个元素userAge(代表redis的key)
//      arg数组元素中有两个元素:10(代表userAge对应的value)和60(代表redis的存活时间)
127.0.0.1:6379> EVAL "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;" 1 userAge 10 60
(integer) 1
127.0.0.1:6379> get userAge
"10"
127.0.0.1:6379> ttl userAge
(integer) 44

然后还有将直接调用lua代码:

local key = KEYS[1]
local val = redis.call("GET", key);

if val == ARGV[1]
then
        redis.call('SET', KEYS[1], ARGV[2])
        return 1
else
        return 0
end

然后这样运行:(如果没有密码则忽略-a),另外注意 是 --eval,不是命令交互模式。另外还有一个区别就是,没有key个数的参数了,而且key和arg之间的,前后必须要都有空格。。。 

redis-cli -a 密码 --eval Lua脚本路径 key [key …] ,  arg [arg …] 

$./src/redis-cli --eval test.lua username  , zhangsan lisi

(integer) 0

基本使用方法基本上如上。 参考: Redis中使用Lua脚本(一) - 知乎

redis如何使用redis代码分析

redis初始化server时进行lua环境的初始化:

redisserver中和lua使用相关的成员:

Lua如何跑起来的

首先Lua的编译器会将其编译为字节码,然后将这些字节码加载到Lua虚拟机中进行解释并执行即可。

Lua是寄存器性的虚拟机

将Lua编译成的字节码,其实就是一堆Lua虚拟机自己定义的一堆指令而已(类似于x86指令),然后lua虚拟机模拟硬件计算机的行为,不断地读取指令,解析指令,执行指令。

Lua的指令实际上是由4个字节组成(每条固定),其中低6位位指令操作码,其余的为操作数,Lua里面一共定义了43个不同指令。然后Lua虚拟机根据指令进行不断地执行,取下一条指令再执行,完全模拟的是计算机行为(所以叫做虚拟机). 

执行指令:如何执行,虚拟机里面定义了一个栈,而将该栈(luaState里面的一个成员luaStack)作为了执行指令的核心。(之所以说Lua是寄存器型的虚拟机是因为他的操作数寻址是基于寄存器风格的,而这些寄存器其实就是这个栈里面的某个slot。可以想见Lua在执行指令时候,每条指令的实现逻辑都应该是包括了指令本身和这个luaState。

那么实现Lua的核心在于编译字节码,也是编译原理的内容了:如何将这些Lua语句编译为字节码(指令集合)

Lua运行

上面说到Lua在运行时候的所有过程:取指令、执行指令、取指令、、循环往复,其中指令的执行都是在那个栈上执行的。(luaState里面的luaStack)

如果设想将所有的逻辑都放在异构chunk里面其实用一个stack就可以了,但是肯定是有涉及到函数调用的。其中之前讨论到的这个luaStack是一个初始的stack,而每个chunk(函数)都会拥有一个luaStack。比如函数的入口总是一个chunk,其实也可以理解为一个函数。

在lua里面,所有的函数更专业的叫法叫闭包(closure),每个chunk(一堆指令构成的集合块)都叫做closure。那么没调用一个函数其实就是执行这个对应函数对应chunk里面的指令集合,注意此时需要切换栈,真实的场景是这样:

type luaStack struct {
    slots []luaValue
    top   int

    prev    *luaStack
    closure *luaClosure
    vargs    []luaValue
    pc        int
}


//clousre:其实也就是chunk结构,里面包含对应指令集合
type closure struct {
    proto       *binchunk.Prototype
}

type luaState {
    stack    *luaStack
}

==》 真实场景:这些luaStack通过链表链接起来,当前操作的stack在链表头部。

每当在chunk里面解析指令时候看到函数调用指令时,都会创建一个新的stack,将其链到表头,每当chunk里面的指令执行完成后,都会将其stack去除掉。

func(self *luaState) pushLuaState(stack *luaStack){
    stack.prev = self.stack
    self.stack = stack
}

//这里是用go来表示的,当然用c实现时候还涉及到对内存的回收。
func(self *luaState) popLuaStack() {
    stack = self.stack
    self.stack = stack.prev
    stack.prev = nil
}

函数调用过程

1 如果在某个chunk里面先进行编译为二进制chunk时,如果有函数调用此时会出先CLOSURE指令,这是将子函数调用之前做的准备工作:

func closure(i Instrction, vm LuaVM) {
    a,bx := i.ABx()
    a += 1

    vm.LoadProto(bx)
    vm.Replace(a)
}

将将要调用的子函数的chunk获取到,然后放到指定的地址。(注意这个过程的stack还没有变化)

看下LoadProto的实现:

func (self *luaState)LoadProto(idx int) {
    proto := self.stack.closure.proto.protos[idx] //核心:到哪里去找到被调clousre
    closure := newLuaClosure(proto)
    self.stack.push(closure)
}

首先是从当前stack的结构中取出对应子函数proto的地址(在编译时候该主chunk里面已经将其安排好了),然后将其构成一个closure(其实一个closure核心就是proto,然后将其压入到stack里面(本stack)

既然现在被调的closure(proto)已经位于栈顶了(可以理解为一种类型,在lua中closure也是一种类型,能够根据其寻找到被调用chunk指令集合的一种类型)

此时栈上的状态应该是:

那么现在可以执行调用了:CALL指令

func call(i Instruction, vm LuaVM) {
    a,b,c := i.ABC()
    a += 1

    nargs := _pushFuncAndArgc(a,b,vm) 
    vm.Call(nargs, c-1)        //核心
    _popResults(a,c,vm)
}

限时将相关的参数入栈(需要调用的入参数),如此:

然后执行call调用:具体

func(self *luaState) Call(nArgs, nResults int) {
    val: =self.stack.get(-nArgs + 1)

    if c, ok: = val.(*Closure);ok {
        self.callLuaClosure(nargs, nResults, c)
    } else {
        panic("not function")
    }
}

先是看下下面的closure是否为真实的closure类型,然后开始执行callLuaClousre:

func(self *luaState) callLuaClosure(nargs, nResults int, c *clousre) {
    nregs := int(c.proto.MaxStackSize
    nparms := int(c.proto.NumParms)
    isVargs := c.proto.IsVargs == 1

    newStack := newLuaStack(nRegs + 20) 
    newStack.closure = c
    ...
}

准备给改closure的调用栈,看栈的大小等等,如下格式了:

继续:

func(self *luaState) callLuaClosure(nargs, nResults int, c *clousre) {
    nregs := int(c.proto.MaxStackSize
    nparms := int(c.proto.NumParms)
    isVargs := c.proto.IsVargs == 1

    newStack := newLuaStack(nRegs + 20) 
    newStack.closure = c
    
    funcAndArgs := self.stack.popN(nArgs + 1)
    newStack.pushN(funcAndArgs[1:], nParms)
    newStack.top = nRegs
    if nargs > nparams && isVargs {
        newStack.varargs = funcAndArgs[nParams+1:]
    }

    ...
    
}

将之前放在主调栈上的参数此时弹出推入被调栈:

继续:

func(self *luaState) callLuaClosure(nargs, nResults int, c *clousre) {
    nregs := int(c.proto.MaxStackSize
    nparms := int(c.proto.NumParms)
    isVargs := c.proto.IsVargs == 1

    newStack := newLuaStack(nRegs + 20) 
    newStack.closure = c
    
    funcAndArgs := self.stack.popN(nArgs + 1)
    newStack.pushN(funcAndArgs[1:], nParms)
    newStack.top = nRegs
    if nargs > nparams && isVargs {
        newStack.varargs = funcAndArgs[nParams+1:]
    }

    self.pushLuaStack(newStack) //切换到被调栈

    self.runLuaClosure()       // 运行指令

    self.popLuaStack()        //弹出

    if nResults != 0 {
        results := newStack.popN(newStack.top - nRegs)
        self.stack.check(len(results)
        self.stack.pushN(results, nResults)
    }
}

重点来了:将其切换栈(上面有实现代码),然后运行指令,然后弹出被调栈)

运行指令:

func(self *luaState)runLuaClosure() {
    for {
        inst := vm.Instrunction(self.Fetch())
        inst.Execut(self)
        if inst.Opcode() == vm.OP_RETURN {
            break
        }
    }
}

//这里的self.Fetch是和当前栈有关,从哪里去获取指令:
func(self, *luaState)Fetch() uint32 {
    i := self.stack.closure.proto.Code[self.stack.pc]
    self.stack.pc++
    return i
}

//常量,也是和当前的栈有关)
func(self, *luaState)GetConst(idx int) {
    c := self.stack.closure.proto.Constants[idx]
    self.stack.push(c)
}

实际上就是从当前栈的clousre里面取出指令一条条执行,然后执行完成。最后的结果会被留存在被调栈的上方,然后将其从里面弹出,然后将其压入到主调栈的上方,最后完成了调用。

(这里注意到一点事,这个closure也是被弹出了主调栈的)

至此,对lua运行做一点小结

1 首先是一个初始的栈,这个栈可以认为是main栈

2 对于每个closure而言都有属于自己的栈用来执行相关的指令,且有自己的pc

3 每个closure其实对应的就是一堆chunk的指令集合(也可以理解为个函数的指令集合)

(函数其实就是逻辑的一个组合)

===

补充

字节码:其实也是一堆二进制,但是这堆二进制是有自己的编码规则的,比如java定义的一套规则共jvm识别,比如lua定义的一套规则共lua虚拟机识别。 比如需要特定的虚拟机将其翻译为最终的机器码才能给硬件去运行。

机器码:运行的最终的代码01.其实也是根据Intel的IA平台给定出的规范x86指令集合。是cpu可以直接识别的。但cpu怎么识别也是有自己的规范的,比如不同的cpu厂商也会有自己的指令集合,通过intel的指令集合不能被arm的识别。

但是字节码是可以跨平台的,只要有虚拟机翻译兜底,最终都可以被识别为对应cpu集合的机器码进行识别并执行。

另外说回来,既然被编译为了机器码,那么为何linux上的elf不能放在windows运行,如果他们都是运行在x86cpu上的,其实os是管理硬件的包括cpu,那么linux对cpu的管理和windows不一样,包括可执行文件被os加载进去后怎么样去取出指令喂给cpu,怎么样判定这个可执行文件是不是能够被os识别,都是有其不同点,这个并不是说他们都是x86下就可以识别,这是另一个层面的含义了。

另外对于字节码语言而言,jvm类似这种虚拟机将起到中间人作用将会适配各种不同平台,屏蔽掉底层的差异性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值