openresty---lua调用c原理分析

openresty—lua调用c模块原理分析

1、lua基础

1、lua虚拟机

lua是解释型语言,需要虚拟机对象。不同的lua虚拟机之间的工作是线程安全的,因为一切和虚拟机相关的内存操作都被关联到虚拟机对象中,而没有利用任何其它共享变量。lua的虚拟机核心部分,没有任何的系统调用,是一个纯粹的黑盒子,正确的使用lua,不会对系统造成任何干扰。这其中最关键的一点是,lua让用户自行定义内存管理器,在创建lua虚拟机时传入,这保证了lua的整个运行状态是用户可控的。

2、状态机

global_State:全局状态机

lua_State:协程状态机

lua的使用者的角度看,global_State是不可见的。我们无法用公开的API取到它的指针,也不需要引用它。global_State里面有对主线程的引用,有注册表管理所有全局数据,有全局字符串表,有内存管理函数,有GC需要的把所有对象串联起来的相关信息,以及一切lua在工作时需要的工作内存。
通过lua_newstate创建一个新的lua虚拟机时,第一块申请的内存将用来保存主线程和这个全局状态机。lua的实现尽可能的避免内存碎片,同时也减少内存分配和释放的次数。它采用了一个小技巧,利用一个LG结构,把主线程lua_Stateglobal_State分配在一起。

1
2
3
4
5
6
7
8
9
10
11
typedef struct LX {
#if defined ( luaI_EXTRASPACE )
char buff [ luaI_EXTRASPACE ];
# endif
lua_State l;
} LX;

typedef struct LG {
LX l;
global_State g;
} LG;

lua_newstate的实现

1
2
3
4
5
6
7
8
lua_API lua_State * lua_newstate ( lua_Alloc f, void *ud) {
int i;
lua_State *L; //创建一个主线程状态机
global_State *g; //创建一个全局状态机
LG *l = cast (LG *, (*f)(ud , NULL , lua_TTHREAD , sizeof (LG))); //申请内存
................................................................................
return L;
}

3、version

1
void luaL_checkversion (lua_State *L);
1
2
3
4
5
lua_API const lua_Number * lua_version ( lua_State *L) {
static const lua_Number version = lua_VERSION_NUM ;
if (L == NULL ) return & version ;
else return G(L) -> version ;
}

检查调用它的内核是否是创建这个 lua 状态机的内核。以及调用它的代码是否使用了相同的 lua 版本。同时也检查调用它的内核与创建该 lua 状态机的内核是否使用了同一片地址空间。

  1. 检查调用它的内核是否是创建这个 lua 状态机的内核:假设你正在编写一个 lua 插件,这个插件将被加载到不同的 lua 程序中。这些程序可能使用了不同版本的 lua 内核。在这种情况下,你的插件需要确保它能在所有这些程序中正常工作。你可以在插件的初始化代码中调用 luaL_checkversion 来确保插件被加载的 lua 程序使用的是和插件编译时相同版本的 lua 内核。
  2. 调用它的代码是否使用了相同的 lua 版本:假设你正在维护一个 lua 库,这个库被不同的项目使用,而这些项目可能使用了不同版本的lua。在这种情况下,你需要确保你的库在所有这些项目中都能正常工作。你可以在库的初始化代码中调用 luaL_checkversion 来确保使用库的项目使用的是和库编译时相同版本的 lua
  3. 检查调用它的内核与创建该 lua 状态机的内核是否使用了同一片地址空间:这通常发生在你的 lua 代码需要和其他语言(如 CC++)的代码交互时。例如,你的 lua 代码调用了一个 C 函数,这个 C 函数创建了一个新的 lua 状态机,并尝试在这个新的状态机上执行一些 lua 代码。在这种情况下,你需要确保这个新的状态机和原来的状态机在同一片地址空间,否则可能会导致内存错误。你可以在 C 函数中调用 luaL_checkversion 来进行这个检查。

4、元表

lua语言的元表类似于c++的类与对象,c++的每个类都可以绑定成员函数、成员变量,还可以对成员方法进行重载等等,通过实例化一个对象,可以对对象进行一系列的操作。c++是面向对象的语言,当有一个函数需要共用,又不想对类进行继承,可以使用static关键字,定义为一个全局的函数。从这些外在表现的方面看,c++和lua其实很像,但是显然lua更加轻量

lua 中的每一个值都可以绑定一个元表。这个元表是一个普通的table,它可以定义与该值相关的某些操作。你可以通过设置元表中特定域的值来改变Lua 值的行为。比如当一个非数字型的值作为加法操作的操作数时,Lua 会检查该值是否绑定了元表并且元表设置了域“__add”的值为一个函数,如果是,那么Lua 就会调用这个函数来进行该值的加法操作。

每个table 和full userdata 类型的值都可以有自己单独的元表(但多个table 和userdata可以共享一个元表)。其它的每一种类型对应地只能绑定一个元表。也就是说,所有的数字类型只能绑定同一个元表所有的字符串类型只能绑定同一个元表,等等。除了字符串类型的值默认有一个元表外,其它的值默认是没有元表的。

一个元表控制了一个对象的算术、比较、连接,取长度操作和索引操作的行为。原理可以去看《lua官方文档》,这里主要关注2个有意思的元方法,索引和赋值

  • index:索引操作。当使用一个不存于table 中的键去索引table 中的内容时会尝试调用此元方法。(当索引操作作用于的对象不是一个table 时,那么所有键都是不存在的,所以元方法一定会被尝试调用。)

  • newindex: table 赋值操作table[key] = value。使用一个不存在于table 中的键来给table 中的域赋值时会尝试调用此元方法。

index:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function gettable_event (table, key)
local h
if type(table) == "table" then
local v = rawget(table, key)
-- 如果键存在,返回原始的值
if v ~= nil then return v end
h = metatable(table).__index
if h == nil then return nil end
else
h = metatable(table).__index
if h == nil then
error(···)
end
end
if type(h) == "function" then
return (h(table, key)) -- 调用元方法
else return h[key] -- 或者把元方法当作一个table 来使用
end
end

newindex:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function settable_event (table, key, value)
local h
if type(table) == "table" then
local v = rawget(table, key)
-- 如果键存在,那就做原始赋值
if v ~= nil then rawset(table, key, value); return end
h = metatable(table).__newindex
if h == nil then rawset(table, key, value); return end
else
h = metatable(table).__newindex
if h == nil then
error(···)
end
end
if type(h) == "function" then
h(table, key,value) -- 调用元方法
else h[key] = value --或者把元方法当作一个table 来使用
end
end

2、请求与lua_state的关系

1、lua_State是什么

lua 中,lua_State 是一个代表 lua 解释器状态的结构体指针,它包含了 lua 解释器的所有状态信息,例如当前的全局环境、栈状态等。可以把lua_State 理解为 lua 的一个线程或者执行环境。

在 lua 中,每个线程都有自己的独立的执行栈,局部变量,错误处理函数等。这些都被封装在 lua_State 结构体中。当在 lua 中创建一个新的线程(或者协程)时,lua 会为这个线程创建一个新的 lua_State。这个 lua_State 包含了这个线程的所有状态信息,使得这个线程可以独立于其他线程运行。这是 lua 中线程和协程实现的基础,也是 lua 能够支持并发编程的关键。

2、openresty的协程

lua 的协程(coroutine)是一种用户级的线程,它们不同于操作系统的线程,切换由程序自身控制,因此开销小,使用灵活。

在 OpenResty 中,lua 协程用于实现非阻塞 I/O。当一个请求需要进行 I/O 操作(如访问数据库)时,当前的 lua 协程会挂起,将控制权交给其他的协程。等到 I/O 操作完成后,原来的协程再恢复执行。这样,即使 I/O 操作是阻塞的,也不会影响到整个程序的执行。

3、请求与协程的关系

在 OpenResty 中,每个 worker 进程使用一个 lua VM(lua 虚拟机),并创建一个新的 lua_State(即主线程)来执行 lua 代码。当请求被分配到 worker 时,将在这个 lua VM 中创建一个协程,协程之间数据隔离,每个协程都具有独立的全局变量。

具体来讲,对于每个请求,Openresty都会创建一个协程来处理,co = ngx_http_lua_new_thread(r, L, &co_ref); 而这个创建的协程是系统协程,是主协程,用户无法控制它。而用户通过ngx.thread.spawn创建的协程是通过 ngx_http_lua_coroutine_create_helper创建出来的,用户创建的协程是主协程的子协程。并通过ngx_http_lua_co_ctx_s保存协程的相关信息。协程通过 ngx_http_lua_run_thread 函数来运行与调度,当前待执行的协程为 ngx_http_lua_ctx_t->cur_co_ctx

当 lua 代码调用 I/O 操作等异步接口时,ngx_lua 会挂起当前协程(并保护上下文数据),而不阻塞 worker 进程]。I/O 等异步操作完成时,ngx_lua 会恢复上下文,程序继续执行。这些操作对用户程序都是透明的,使得每个请求都在一个独立的 lua 线程中处理,各个请求之间互不影响,可以并发处理大量的请求,从而提高了系统的吞吐量。

3、源码分析

1、lua与c模块的交互

1、预加载的注册方式,通常自己实现一个模块,采用这种方式

1
2
3
4
5
6
7
8
9
10
11
12
ngx_http_lua_add_package_preload


//在OpenResty(基于Nginx的扩展)中,ngx_http_lua_add_package_preload 是一个用于预加载 lua 模块的函数。这个函数的主要作用是将 lua 模块预加载到 Nginx 工作进程的全局环境中,从而避免在每次请求时重新加载 lua 模块。
//具体而言,ngx_http_lua_add_package_preload 用于将 lua 模块与一个预定义的路径关联,以便在需要时可以快速地加载。这对于提高性能和减少模块加载时间非常有用,特别是在处理大量并发请求时。
//原型如下:

void ngx_http_lua_add_package_preload(ngx_conf_t *cf, const char *package, lua_CFunction func);

//cf: ngx_conf_t 结构,用于获取配置信息。
//package: lua 模块的名称,通常是点分隔的路径,例如 "resty.foo"。
//func: 一个 lua C 函数,用于加载并返回 lua 模块。这个函数在第一次加载模块时被调用,并且加载成功后,其返回值会被缓存,以便后续请求可以直接使用。

ngx_http_lua_upstream_module 模块

1
2
3
4
5
6
7
8
9
10
11
12
static ngx_int_t
ngx_http_lua_upstream_init(ngx_conf_t *cf)
{
if (ngx_http_lua_add_package_preload(cf, "ngx.upstream",
ngx_http_lua_upstream_create_module)
!= NGX_OK)
{
return NGX_ERROR;
}

return NGX_OK;
}

ngx_http_lua_add_package_preload具体实现为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
ngx_int_t
ngx_http_lua_add_package_preload(ngx_conf_t *cf, const char *package,
lua_CFunction func)
{
lua_State *L;
ngx_http_lua_main_conf_t *lmcf;
ngx_http_lua_preload_hook_t *hook;

lmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_lua_module);

L = lmcf->lua;

//lua_getglobal(L, "package"): 获取全局变量 "package"。
//lua_getfield(L, -1, "preload"): 获取 "package" 表中的 "preload" 字段,这是一个用于存放预加载函数的表。
//lua_pushcfunction(L, func): 将 C 函数推入 lua 栈。
//lua_setfield(L, -2, package): 将 C 函数设置为 "preload" 表中的字段,字段名为 lua 模块的名称。
//lua_pop(L, 2): 弹出栈上的两个元素,即 "package" 表和 "preload" 表。

//很重要!!!!!*******//相当于建立了一个ngx.upstream的表,里面preload存放对应的函数----ngx_http_lua_upstream_module


if (L) {
lua_getglobal(L, "package");
lua_getfield(L, -1, "preload");
lua_pushcfunction(L, func);
lua_setfield(L, -2, package);
lua_pop(L, 2);
}

/* we always register preload_hooks since we always create new Lua VMs
* when lua code cache is off. */

if (lmcf->preload_hooks == NULL) {
lmcf->preload_hooks =
ngx_array_create(cf->pool, 4,
sizeof(ngx_http_lua_preload_hook_t));

if (lmcf->preload_hooks == NULL) {
return NGX_ERROR;
}
}

hook = ngx_array_push(lmcf->preload_hooks);
if (hook == NULL) {
return NGX_ERROR;
}

hook->package = (u_char *) package;
hook->loader = func;

return NGX_OK;
}

ngx_http_lua_upstream_create_module的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static int
ngx_http_lua_upstream_create_module(lua_State * L)
{
lua_createtable(L, 0, 6);

lua_pushcfunction(L, ngx_http_lua_upstream_get_upstreams);
lua_setfield(L, -2, "get_upstreams");

lua_pushcfunction(L, ngx_http_lua_upstream_get_servers);
lua_setfield(L, -2, "get_servers");

lua_pushcfunction(L, ngx_http_lua_upstream_get_primary_peers);
lua_setfield(L, -2, "get_primary_peers");

lua_pushcfunction(L, ngx_http_lua_upstream_get_backup_peers);
lua_setfield(L, -2, "get_backup_peers");

lua_pushcfunction(L, ngx_http_lua_upstream_set_peer_down);
lua_setfield(L, -2, "set_peer_down");

lua_pushcfunction(L, ngx_http_lua_upstream_current_upstream_name);
lua_setfield(L, -2, "current_upstream_name");

return 1;
}

2、非预加载的注册方式,openresty官方内置

1
ngx_int_t  ngx_http_lua_inject_xxx_api(lua_State *L){}

如:ngx_http_lua_inject_resp_header_api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void
ngx_http_lua_inject_resp_header_api(lua_State *L)
{ //创建一个新的lua表,并将其推入lua堆栈。这个表将用于存储HTTP响应头的键值对
lua_newtable(L); /* .header */
//创建一个新的lua表,并设置它的元表。元表是一个普通的lua表,它定义了一些特殊的操作,比如当在表中查找一个不存在的键时,会通过元表的__index元方法来获取值。在这里,我们为.header表创建了一个元表。
lua_createtable(L, 0, 2); /* metatable for .header */

//将C函数ngx_http_lua_ngx_header_get推入堆栈,并将它作为值与键__index关联起来。这样,当在.header表中查找一个不存在的键时,将会调用ngx_http_lua_ngx_header_get函数来获取相应的值。
lua_pushcfunction(L, ngx_http_lua_ngx_header_get);
lua_setfield(L, -2, "__index");

//将C函数ngx_http_lua_ngx_header_set推入堆栈,并将它作为值与键__newindex关联起来。这样,当在.header表中设置一个不存在的键时,将会调用ngx_http_lua_ngx_header_set函数来设置相应的值。
lua_pushcfunction(L, ngx_http_lua_ngx_header_set);
lua_setfield(L, -2, "__newindex");

//将刚刚创建的元表设置为.header表的元表,从而实现了对HTTP响应头的读写操作。
lua_setmetatable(L, -2);

//将.header表保存在全局环境中,命名为header,这样在lua脚本中可以通过ngx.header来访问和操作HTTP响应头。
lua_setfield(L, -2, "header");

lua_createtable(L, 0, 1); /* .resp */

lua_pushcfunction(L, ngx_http_lua_ngx_resp_get_headers);
lua_setfield(L, -2, "get_headers");

//ngx.resp
lua_setfield(L, -2, "resp");
}

openresty在nginx的配置阶段统一注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void
ngx_http_lua_inject_ngx_api(lua_State *L, ngx_http_lua_main_conf_t *lmcf,
ngx_log_t *log)
{
lua_createtable(L, 0 /* narr */, 115 /* nrec */); /* ngx.* */

lua_pushcfunction(L, ngx_http_lua_get_raw_phase_context);
lua_setfield(L, -2, "_phase_ctx");

ngx_http_lua_inject_arg_api(L);

ngx_http_lua_inject_http_consts(L);
ngx_http_lua_inject_core_consts(L);

ngx_http_lua_inject_resp_header_api(L); //注册到线程中

.......................................

将lua与c代码关联起来,这样就可以在lua中调用ngx.header,比如:

1
2
local cookie = {}
ngx.header["Set-cookie"] = cookie

3、关于__index

当尝试从表中获取不存在的值时,那么就会调用 ngx_http_lua_ngx_header_get

在lua中,__index 是一种特殊的元方法(metamethod),用于表的访问控制。当你尝试从一个表中获取一个不存在的键时,lua会在表的元表中查找是否定义了__index元方法。如果找到了__index元方法,lua会调用它,并将表本身和要访问的键作为参数传递给该元方法。

在这段代码中,我们创建了一个名为 .header 的新表,并为该表创建了一个元表。然后,我们通过 lua_setfield(L, -2, "__index") 将名为 __index 的 C 函数(ngx_http_lua_ngx_header_get)与该元表中的 __index 键关联起来。这样,当在 .header 表中查找一个不存在的键时,lua 就会调用 ngx_http_lua_ngx_header_get 函数来获取相应的值。

换句话说,这个代码片段通过设置 __index 元方法,为 .header 表提供了一种自定义的行为:当访问 .header 表中不存在的键时,会调用 ngx_http_lua_ngx_header_get 函数进行处理。这在某种程度上实现了对 .header 表的动态访问控制。

2、协程

  1. nginx master初始化时,会创建一个lua_state,并初始化一个cached_lua_threads。
  2. master在fork work时,每个work会拥有各自的lua_state,即主协程
  3. 主协程会维护cached_lua_threads,存放这个work(也就是这个lua_state主协程)创建出的所有协程,可以重复使用。
  4. 当有请求时,先检查 请求是否在这个虚拟机处理 && 协程队列是否为空 。
  5. 如果满足条件,那么从队列取一个协程,绑定该请求的上下文
  6. 如果不满足条件,说明此时没有主协程,或者没有可用的协程了,那就新建协程

1、master进程初始化虚拟机,创建lua_state

1
2
//初始化ngx_http_lua_module模块              //初始化虚拟机,lmcf->lua为创建成功的虚拟机实例
ngx_http_lua_init -> rc = ngx_http_lua_init_vm(&lmcf->lua, NULL, cf->cycle, cf->pool, lmcf, cf->log,NULL);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ngx_int_t
ngx_http_lua_init_vm(lua_State **new_vm, lua_State *parent_vm,
ngx_cycle_t *cycle, ngx_pool_t *pool, ngx_http_lua_main_conf_t *lmcf,
ngx_log_t *log, ngx_pool_cleanup_t **pcln)
{
..............................................

/* create new lua VM instance */
L = ngx_http_lua_new_state(parent_vm, cycle, lmcf, log); //创建lua_state
if (L == NULL) {
return NGX_ERROR;
}

.....................................
}

初始化协程队列

1
2
//初始化配置                                       //初始化队列
ngx_http_lua_init_main_conf -> ngx_queue_init(&lmcf->cached_lua_threads);

2、lmcf->cached_lua_threads

lmcf->cached_lua_threads 是一个队列,用于缓存 lua 协程(线程)。

  1. 这个队列是在 Nginxlua 模块中使用的,用于管理 lua 协程的生命周期。
  2. 具体作用包括但不限于:
    • 缓存已经创建的 lua 协程,以便在请求处理过程中重复使用。
    • 避免频繁地创建和销毁协程,提高性能和效率。
  3. 当需要执行 lua 脚本时,可以从这个队列中获取一个已经存在的协程,而不必每次都重新创建。

lmcf->cached_lua_threads 是一个用于缓存 lua 协程的队列,以优化请求处理性能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
lua_State *
ngx_http_lua_new_thread(ngx_http_request_t *r, lua_State *L, int *ref)
{
................................

lmcf = ngx_http_get_module_main_conf(r, ngx_http_lua_module);

if (L == lmcf->lua && !ngx_queue_empty(&lmcf->cached_lua_threads)) { //L和lmcf->lua有可能不相等吗 && 协程队列不为空
q = ngx_queue_head(&lmcf->cached_lua_threads);
tref = ngx_queue_data(q, ngx_http_lua_thread_ref_t, queue);
} else //走到这里,说明 协程队列为空
{
lua_pushlightuserdata(L, ngx_http_lua_lightudata_mask(
coroutines_key));
lua_rawget(L, lua_REGISTRYINDEX); //从主协程获取线程队列
co = lua_newthread(L); //新创建协程
lua_pushvalue(L, -1); //新创建的协程推入栈中
co_ref = luaL_ref(L, -3); //新协程的引用存储在注册表

ngx_log_debug2(NGX_LOG_DEBUG_HTTP, ngx_cycle->log, 0,
"lua ref lua thread %p (ref %d)", co, co_ref);

#ifndef OPENRESTY_luaJIT //如果是jit,设置全局变量
if (set_globals) {
lua_createtable(co, 0, 0); /* the new globals table */

/* co stack: global_tb */

lua_createtable(co, 0, 1); /* the metatable */
ngx_http_lua_get_globals_table(co);
lua_setfield(co, -2, "__index");
lua_setmetatable(co, -2);

/* co stack: global_tb */

ngx_http_lua_set_globals_table(co);
}
#endif
}

................................

3、请求与协程创建关联的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ngx_http_lua_content_by_chunk(lua_State *L, ngx_http_request_t *r)
{
................................

/* {{{ new coroutine to handle request */
co = ngx_http_lua_new_thread(r, L, &co_ref); //主线程的创建

if (co == NULL) {
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
"lua: failed to create new coroutine to handle request");

return NGX_HTTP_INTERNAL_SERVER_ERROR;
}
....................................
}

4、问题

1、L和lmcf->lua有可能不相等吗?

  1. 多线程环境:如果你的应用程序在多线程环境中运行,每个线程可能有自己的 Lua 解释器状态。在这种情况下,如果 L 被设置为当前线程的 Lua 解释器状态,而 lmcf->lua 仍然引用主线程的 Lua 解释器状态,那么 L == lmcf->lua 就不会成立。
  2. Lua 解释器状态切换:在某些复杂的应用程序中,可能需要动态地切换 Lua 解释器状态。例如,一个请求可能需要在多个 Lua 解释器状态之间切换。在这种情况下,如果 L 被设置为当前需要的 Lua 解释器状态,而 lmcf->lua 仍然引用之前的 Lua 解释器状态,那么 L == lmcf->lua 就不会成立。
  3. Lua 解释器状态重新分配:如果 L 指向的 Lua 解释器状态被重新分配(例如,由于内存管理或垃圾收集),那么 L == lmcf->lua 就不会成立。

以目前的认识来看,上述3种情况不会发生,这取决于openresty框架怎么设置L和lmcf->lua

1、《lua源码剖析-云风》
2、https://segmentfault.com/a/1190000038878724
3、openresty-1.25.3.1


openresty---lua调用c原理分析
https://zjfans.github.io/2024/04/12/openresty---lua调用c原理分析/
作者
张三疯
发布于
2024年4月12日
许可协议