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_State
和global_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
状态机的内核是否使用了同一片地址空间。
检查调用它的内核是否是创建这个 lua
状态机的内核 :假设你正在编写一个 lua
插件,这个插件将被加载到不同的 lua
程序中。这些程序可能使用了不同版本的 lua
内核。在这种情况下,你的插件需要确保它能在所有这些程序中正常工作。你可以在插件的初始化代码中调用 luaL_checkversion
来确保插件被加载的 lua
程序使用的是和插件编译时相同版本的 lua
内核。
调用它的代码是否使用了相同的 lua
版本 :假设你正在维护一个 lua
库,这个库被不同的项目使用,而这些项目可能使用了不同版本的lua
。在这种情况下,你需要确保你的库在所有这些项目中都能正常工作。你可以在库的初始化代码中调用 luaL_checkversion
来确保使用库的项目使用的是和库编译时相同版本的 lua
。
检查调用它的内核与创建该 lua 状态机的内核是否使用了同一片地址空间 :这通常发生在你的 lua
代码需要和其他语言(如 C
或 C++
)的代码交互时。例如,你的 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:
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] 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 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 void ngx_http_lua_add_package_preload (ngx_conf_t *cf, const char *package, lua_CFunction func) ;
如 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; if (L) { lua_getglobal(L, "package" ); lua_getfield(L, -1 , "preload" ); lua_pushcfunction(L, func); lua_setfield(L, -2 , package); lua_pop(L, 2 ); } 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_newtable(L); lua_createtable(L, 0 , 2 ); lua_pushcfunction(L, ngx_http_lua_ngx_header_get); lua_setfield(L, -2 , "__index" ); lua_pushcfunction(L, ngx_http_lua_ngx_header_set); lua_setfield(L, -2 , "__newindex" ); lua_setmetatable(L, -2 ); lua_setfield(L, -2 , "header" ); lua_createtable(L, 0 , 1 ); lua_pushcfunction(L, ngx_http_lua_ngx_resp_get_headers); lua_setfield(L, -2 , "get_headers" ); 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 , 115 ); 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、协程
nginx master初始化时,会创建一个lua_state,并初始化一个cached_lua_threads。
master在fork work时,每个work会拥有各自的lua_state,即主协程
主协程会维护cached_lua_threads,存放这个work(也就是这个lua_state主协程)创建出的所有协程,可以重复使用。
当有请求时,先检查 请求是否在这个虚拟机处理 && 协程队列是否为空 。
如果满足条件,那么从队列取一个协程,绑定该请求的上下文
如果不满足条件,说明此时没有主协程,或者没有可用的协程了,那就新建协程
1、master进程初始化虚拟机,创建lua_state
1 2 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) { .............................................. L = ngx_http_lua_new_state(parent_vm, cycle, lmcf, log ); 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 协程(线程)。
这个队列是在 Nginx 的 lua 模块中使用的,用于管理 lua 协程的生命周期。
具体作用包括但不限于:
缓存已经创建的 lua 协程,以便在请求处理过程中重复使用。
避免频繁地创建和销毁协程,提高性能和效率。
当需要执行 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)) { 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 if (set_globals) { lua_createtable(co, 0 , 0 ); lua_createtable(co, 0 , 1 ); ngx_http_lua_get_globals_table(co); lua_setfield(co, -2 , "__index" ); lua_setmetatable(co, -2 ); 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) { ................................ 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有可能不相等吗?
多线程环境 :如果你的应用程序在多线程环境中运行,每个线程可能有自己的 Lua 解释器状态。在这种情况下,如果 L
被设置为当前线程的 Lua 解释器状态,而 lmcf->lua
仍然引用主线程的 Lua 解释器状态,那么 L == lmcf->lua
就不会成立。
Lua 解释器状态切换 :在某些复杂的应用程序中,可能需要动态地切换 Lua 解释器状态。例如,一个请求可能需要在多个 Lua 解释器状态之间切换。在这种情况下,如果 L
被设置为当前需要的 Lua 解释器状态,而 lmcf->lua
仍然引用之前的 Lua 解释器状态,那么 L == lmcf->lua
就不会成立。
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