lua vm 共享 proto

news2024/12/27 13:02:31

lua vm共享proto

场景

在skynet中,对于每一个lua服务,实际上就是在snlua上启动了一个lua虚拟机去完成lua逻辑,所以skynet的服务是相互隔离的。

这样就会产生一个问题,多个服务都require同一个lua库,每个服务内都会有一份这个lua库的拷贝,这样就会造成极大的内存浪费。云风因此对lua进行了一次patch,优化了这个问题。

由于云风的文档较老,代码又进行了新的变更,所以我重新阅读了一遍patch的源码以探究节省内存原理的真谛。

前置知识

closure

lua中,函数通常被表示为一个LClosure即使他没有upvalue,闭包是由函数原型的引用和该函数所需访问的外部局部变量 upvalue 组成。其中函数原型部分是可以被共享的。

//闭包
typedef struct LClosure {
  ClosureHeader;	//公共头部,用于gc
  struct Proto *p;	//函数原型
  UpVal *upvals[1];  //upvalue链表
} LClosure;

//函数原型
typedef struct Proto {
  CommonHeader;	
  lu_byte numparams;  /* number of fixed (named) parameters 非可变参数数量*/	
  lu_byte is_vararg;
  lu_byte maxstacksize;  /* number of registers needed by this function 最大栈空间*/
  int sizeupvalues;  /* size of 'upvalues' upvalue数*/
  int sizek;  /* size of 'k' 常量表长度*/
  int sizecode;
  int sizelineinfo;
  int sizep;  /* size of 'p' 内嵌proto数*/
  int sizelocvars;
  int sizeabslineinfo;  /* size of 'abslineinfo' */
  int linedefined;  /* debug information  */
  int lastlinedefined;  /* debug information  */
  TValue *k;  /* constants used by the function 常量表*/
  Instruction *code;  /* opcodes 操作码链表*/
  struct Proto **p;  /* functions defined inside the function 内嵌proto*/
  Upvaldesc *upvalues;  /* upvalue information upvalue描述(是否close等)*/
  ls_byte *lineinfo;  /* information about source lines (debug information) */
  AbsLineInfo *abslineinfo;  /* idem */
  LocVar *locvars;  /* information about local variables (debug information) */
  TString  *source;  /* used for debug information */
  GCObject *gclist;
} Proto;

require

实际上require大致分为两个过程,一是load过程,另一个是执行过程。load过程中,会通过文件流或者标准输入去读取脚本内容,并返回一个closure。执行过程中执行这个closure返回结果。而且require还有一个重要行为就是把执行结果缓存到package.loaded(modname->result)中,下次再次require,就会直接返回这个缓存。

load返回的closure执行之后,其结果中的函数原型和closure中的函数原型地址相同。因为,这个closure是lua_load中编译过的,因此其定义的函数原型会变成一个函数地址,并在执行脚本的过程中作为传入opcode中(之后我会证明这点),因此这份编译好的脚本不管执行多少次,函数原型的地址总是相同的。这也是多个虚拟机能共享同一个脚本去节省内存的关键。

//注册
static const luaL_Reg ll_funcs[] = {
  {"require", ll_require},
  {NULL, NULL}
};

//require流程
static int ll_require (lua_State *L) {	//一个参数mod的名字
  const char *name = luaL_checkstring(L, 1);
  lua_settop(L, 1);  //设置栈顶,就留一个元素
  lua_getfield(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE);	//获取registry["_LOADED"]也就是package.loaded表
  lua_getfield(L, 2, name);  //loaded[name]
  if (lua_toboolean(L, -1))  //已经有了?
    return 1;  //直接返回模块
  lua_pop(L, 1);  //pop掉loaded[name]
  findloader(L, name);	//找到load脚本文件产生的closure
  lua_rotate(L, -2, 1);  //交换一下closure和data的顺序
  lua_pushvalue(L, 1);  /* name is 1st argument to module loader */
  lua_pushvalue(L, -3);  /* loader data is 2nd argument */
  /* stack: ...; loader data; loader function; mod. name; loader data 栈中的元素顺序*/
  lua_call(L, 2, 1);  //运行这个closure
  /* stack: ...; loader data; result from loader */
  if (!lua_isnil(L, -1))  //非空
    lua_setfield(L, 2, name);  /* LOADED[name] = returned value */
  else
    lua_pop(L, 1);  /* pop nil */
  if (lua_getfield(L, 2, name) == LUA_TNIL) {   //loaded[name]为空
    lua_pushboolean(L, 1);  //使用脚本执行结果
    lua_copy(L, -1, -2);  /* replace loader result */
    lua_setfield(L, 2, name);  /* LOADED[name] = true */
  }
  lua_rotate(L, -2, 1);  /* loader data <-> module result  */
  return 2;  /* return module result and loader data */
}

static void findloader (lua_State *L, const char *name) {
  int i;
  luaL_Buffer msg;  /* to build error message */
  /* push 'package.searchers' to index 3 in the stack */
  if (l_unlikely(lua_getfield(L, lua_upvalueindex(1), "searchers")
                 != LUA_TTABLE))
    luaL_error(L, "'package.searchers' must be a table");
  luaL_buffinit(L, &msg);
  /*  iterate over available searchers to find a loader 遍历serchers*/
  for (i = 1; ; i++) {
    luaL_addstring(&msg, "\n\t");  /* error-message prefix */
    if (l_unlikely(lua_rawgeti(L, 3, i) == LUA_TNIL)) {  //没有sercher了
      lua_pop(L, 1);  /* remove nil */
      luaL_buffsub(&msg, 2);  /* remove prefix */
      luaL_pushresult(&msg);  /* create error message */
      luaL_error(L, "module '%s' not found:%s", name, lua_tostring(L, -1));
    }
    lua_pushstring(L, name); //找到了sercher
    lua_call(L, 1, 2);  //运行sercher,参数为name
    if (lua_isfunction(L, -2))  //返回了closure?
      return;  /* module loader found */
    else if (lua_isstring(L, -2)) {  /* searcher returned error message? */
      lua_pop(L, 1);  /* remove extra return */
      luaL_addvalue(&msg);  /* concatenate error message */
    }
    else {  /* no error message */
      lua_pop(L, 2);  /* remove both returns */
      luaL_buffsub(&msg, 2);  /* remove prefix */
    }
  }
}

//serchers有以下这些
 static const lua_CFunction searchers[] = {
    searcher_preload,
    searcher_Lua,
    searcher_C,
    searcher_Croot,
    NULL
  };
//serchers在个函数里注册
static const luaL_Reg loadedlibs[] = {
    ...
  {LUA_LOADLIBNAME, luaopen_package},
 	...
  {NULL, NULL}
};
#define LUA_LOADLIBNAME	"package"	//调用package模块自动注册

字节码指令

通过luac -l来查看lua文件编译出的字节码,load的编译结果也是这样。

其中CLOSURE为指定的函数prototype创建一个closure,而这个0x55ee590e8080就是函数原型的地址。可以看到下边创建出来的闭包中的函数原型地址就是脚本中编译后的地址。***这就是一个proto能共享的最重要的原因,同一份编译好的脚本运行后产生的闭包的函数原型的地址是相同的,因为函数原型加载到内存中的地址是编译中就被确定的。***所以最主要原因就是多个luastate各自load,编译过程中重复加载代码段到内存中导致内存浪费。
CLOSURE

解决

因此,我们就明确了,那就是要防止一个模块多次load。首先我们要做一个cache缓存所有load过的模块。其次取缓存模块时,记得clone,把非共享部分单独创建,共享部分沿用。我们对每个load过的脚本用一个虚拟机进行存储,然后在cache中保存指向这个虚拟机内closure的指针,之后只要向cache查询就可以了,之所以要为每个文件分配一个虚拟机,主要原因是热更时未更新服务仍可引用旧模块且只有这个虚拟机才有权对这个模块进行gc,但是目前是永不gc的。这里引用云风的一句话,保留所有用过的函数在内存中这种做法是广泛存在的,如果你对比看 C 层次的函数,即使 C 函数存在于动态库中,我们也不能轻易卸载动态库,这有让其它模块保留过动态库中函数指针变得无效。

cache

我们的目标是创造一个共享的cache,所有的虚拟机都可以访问到这个cache,并且我们要保证这个cache是线程安全的。

接下来让我们看看实现:

  • 缓存结构
struct codecache {
	struct spinlock lock;
	lua_State *L;	//可以看到缓存在虚拟机中进行映射
};

static struct codecache CC;

static void
init(void) {	//初始化
	CC.L = luaL_newstate();	//创建虚拟机
}
  • 更改cache模式

cache总共分为三个模式,OFF代表不进行任何缓存,重复load,ON代表进行缓存,EXIST代表复用缓存但不进行新的缓存。默认为ON。

//接收一个参数,设置相应模式,参数为空代表获取当前模式
static int cache_mode(lua_State *L) {
	static const char * lst[] = {
		"OFF",
		"EXIST",
		"ON",
		NULL,
	};
	int t,r;
	if (lua_isnoneornil(L,1)) {	//参数为空
		t = lua_rawgetp(L, LUA_REGISTRYINDEX, &cache_key);	//获得registry[cache_key]的类型并压栈registry[*(int*)cache_key]
		r = lua_tointeger(L, -1);	//registry[cache_key]
		if (t == LUA_TNUMBER) {	//类型为数字?
			if (r < 0  || r >= CACHE_ON) {	//非法全部默认为ON
				r = CACHE_ON;
			}
		} else {
			r = CACHE_ON;
		}
		lua_pushstring(L, lst[r]);	//返回模式名
		return 1;
	}
	t = luaL_checkoption(L, 1, "OFF" , lst);	//"OFF"所在索引
	lua_pushinteger(L, t);	//索引压栈
	lua_rawsetp(L, LUA_REGISTRYINDEX, &cache_key);	//registry[*(int*)cache_key]=t
	return 0;
}
  • 清除缓存
static int
cache_clear(lua_State *L) {
	(void)(L);
	clearcache();	//清除缓存
	return 0;
}

static void
clearcache(void) {
	if (CC.L == NULL)
		return;
	SPIN_LOCK(&CC)
		lua_close(CC.L);	//关闭虚拟机
		CC.L = luaL_newstate();	//新建虚拟机
	SPIN_UNLOCK(&CC)
}

//注意清除缓存后,虚拟机不再映射任何缓存,所以之前的缓存永远不会释放,内存泄漏。
//引用云风的话
//为了 skynet 服务器可以热更新 lua 脚本,还增加了 clear cache 的方法(skynet.cache.clear),可以将 cache 重置。当然,之前加载过的代码其实是没有从内存中清理掉的,这一定程度上会带来一些内存泄露。但考虑到这个 patch 可以给系统节约的内存,不是过于频繁的热更新是可以接受的。
  • 加载脚本
LUALIB_API int luaL_loadfilex (lua_State *L, const char *filename,
                                             const char *mode) {
  int level = cache_level(L);	//获得当前的cache模式
  const void * proto;
  lua_State * eL;	//新的模块的虚拟机指针
  int err;
  const void * oldv;
  if (level == CACHE_OFF) {	//cache关
    return luaL_loadfilex_(L, filename, mode);	//load
  }
  proto = load_proto(filename);	//查缓存
  if (proto) {
    lua_clonefunction(L, proto);	//有就clone一份返回
    return LUA_OK;
  }
  if (level == CACHE_EXIST) {	//CACHE_EXIST模式
    return luaL_loadfilex_(L, filename, mode);	//直接加载不进行下边的缓存
  }
  eL = luaL_newstate();	//为新文件创建虚拟机
  if (eL == NULL) {
    lua_pushliteral(L, "New state failed");
    return LUA_ERRMEM;
  }
  err = luaL_loadfilex_(eL, filename, mode);	//加载到这个创建的虚拟机中
  if (err != LUA_OK) {	//失败
    size_t sz = 0;
    const char * msg = lua_tolstring(eL, -1, &sz);
    lua_pushlstring(L, msg, sz);
    lua_close(eL);
    return err;
  }
  lua_sharefunction(eL, -1);	//设置加载的脚本为share(不被gc)
  proto = lua_topointer(eL, -1);	//获取脚本指针
  oldv = save_proto(filename, proto);	//在cache虚拟机中保存存映射<filename,proto>
  if (oldv) {	//如果原有这个文件,说明重新加载了
    lua_close(eL);
    lua_clonefunction(L, oldv);
  } else {
    lua_clonefunction(L, proto);	//clone一个返回
    /* Never close it. notice: memory leak */
  }

  return LUA_OK;
}
  • 查缓存
static const void *
load_proto(const char *key) {
  lua_State *L;
  const void * result;
  if (CC.L == NULL)
    return NULL;
  SPIN_LOCK(&CC)	//上锁多线程安全
    L = CC.L;	//获得cache虚拟机
    lua_pushstring(L, key);	//push文件名
    lua_rawget(L, LUA_REGISTRYINDEX);	//registry[filename]
    result = lua_touserdata(L, -1);	//结果
    lua_pop(L, 1);
  SPIN_UNLOCK(&CC)

  return result;	//返回registry[filename]
}
  • 进行缓存
static const void *
save_proto(const char *key, const void * proto) {
  lua_State *L;
  const void * result = NULL;

  SPIN_LOCK(&CC)
    if (CC.L == NULL) {
      init();
    }
    L = CC.L;
    lua_pushstring(L, key);
    lua_pushvalue(L, -1);
    lua_rawget(L, LUA_REGISTRYINDEX);	//registry[filename]
    result = lua_touserdata(L, -1); /* stack: key oldvalue */
    if (result == NULL) {	//没有就缓存进去
      lua_pop(L,1);
      lua_pushlightuserdata(L, (void *)proto);
      lua_rawset(L, LUA_REGISTRYINDEX);	//registry[filename] = (void *)proto
    } else {
      lua_pop(L,2);
    }

  SPIN_UNLOCK(&CC)
  return result;
}
  • clone脚本
LUA_API void lua_clonefunction (lua_State *L, const void * fp) {
  LClosure *cl;
  LClosure *f = cast(LClosure *, fp);	//fp 指向缓存的closure
  api_check(L, isshared(f->p), "Not a shared proto");
  lua_lock(L);
  cl = luaF_newLclosure(L,f->nupvalues);	//创建nupvalues的新闭包
  setclLvalue2s(L,L->top,cl);	//设置栈顶为cl
  api_incr_top(L);	//栈顶加一
  cl->p = f->p;	//cl的proto设置为fp的proto
  luaF_initupvals(L, cl);	//初始化upvalue
  set_env(L,cl);	//设置_ENV
  lua_unlock(L);
}
//由此可见,clone全面复制了fp的proto
  • lua 加载文件
LUALIB_API int luaL_loadfilex_ (lua_State *L, const char *filename,
                                             const char *mode) {
  LoadF lf;	//文件加载结构
  int status, readstatus;
  int c;
  int fnameindex = lua_gettop(L) + 1;  /* index of filename on the stack */
  if (filename == NULL) {
    lua_pushliteral(L, "=stdin");
    lf.f = stdin;
  }
  else {
    lua_pushfstring(L, "@%s", filename);
    lf.f = fopen(filename, "r");	//打开文件
    if (lf.f == NULL) return errfile(L, "open", fnameindex);
  }
  lf.n = 0;
  if (skipcomment(lf.f, &c))  //读文件头
    lf.buff[lf.n++] = '\n';  /* add newline to correct line numbers */
  if (c == LUA_SIGNATURE[0]) {  //二进制文件
    lf.n = 0;  /* remove possible newline */
    if (filename) {  /* "real" file? */
      lf.f = freopen(filename, "rb", lf.f);  /* reopen in binary mode */
      if (lf.f == NULL) return errfile(L, "reopen", fnameindex);
      skipcomment(lf.f, &c);  /* re-read initial portion */
    }
  }
  if (c != EOF)
    lf.buff[lf.n++] = c;  /* 'c' is the first character of the stream */
  status = lua_load(L, getF, &lf, lua_tostring(L, -1), mode);	//lua_load加载文件(被压入栈)
  readstatus = ferror(lf.f);
  if (filename) fclose(lf.f);  //关闭文件
  if (readstatus) {
    lua_settop(L, fnameindex);  /* ignore results from 'lua_load' */
    return errfile(L, "read", fnameindex);
  }
  lua_remove(L, fnameindex);
  return status;
}

总结

我们require的时候会先查package缓存,然后查load缓存。如果package缓存不存在,那么运行一遍load的缓存,然后把结果缓存在package.loaded中。这样多个服务运行同一个load缓存时,由于load编译时已经把代码载入内存,所以产生的结果的proto都指向同一个函数地址,这样内存浪费的问题就解决了。

下面是原理图:

share proto

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/73776.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

原生API编写简单富文本编辑器001

原生API编写简单富文本编辑器001 从这一节开始&#xff0c;我们将亲自动手&#xff0c;使用我们之前介绍过的浏览器原生API来实现一个简单的可以处理文本的富文本编辑器。 1. 设计 这一个简单版的编辑器&#xff0c;由于我们是基于原生的API&#xff0c;基于浏览器原生API的…

线性表-双向链表

双向链表 双向链表也叫双向表&#xff0c;是链表的一种&#xff0c;它由多个结点组成&#xff0c;每个结点都由一个数据域和两个指针域组成&#xff0c;数据域用来存储数据&#xff0c;其中一个指针域用来指向其后继结点&#xff0c;另一个指针域用来指向前驱结点。链表的头结…

手动实现SpringBoot日志链路追踪

概述 有时候一个业务调用链场景&#xff0c;很长&#xff0c;调了各种各样的方法&#xff0c;看日志的时候&#xff0c;各个接口的日志穿插&#xff0c;确实让人头大。 模糊匹配搜索日志能解决吗&#xff1f;能解决一点点。但是不能完全呈现出整个链路相关的日志。 那要做到方…

致迷茫的程序员一封信——我的程序生涯

0、开头 大家好&#xff0c;我是罗鹏程&#xff0c;一个很老套的开头&#xff0c;哈哈哈。 这封信姗姗来迟&#xff0c;与其说是一封信&#xff0c;不如说是来听听我的故事。从2020开始&#xff0c;收到过很多网友的问题&#xff0c;职业的选择&#xff0c;是做大数据还…

Intellij Idea生成含有META-INF的jar包

新建一个module&#xff0c;如果不会新建的话&#xff0c;参考&#xff1a;Intellij Idea新建module。命名为jar_test。 新建一个java类DateUtil&#xff0c;可以输出当前时间对应的是星期几。代码如下&#xff1a; import java.util.Calendar; import java.util.Date;publi…

编译原理 1 - 概述、形式语言

第1章 引论一些概念1.3 编译程序的总体结构1.4 编译程序的组织第二章 形式语言2.1 文法描述中的基本概念上下文无关文法第1章 引论 一些概念 机器语言&#xff1a;以0、1代码表示的机器指令所构成的语言 每一个具体的计算机系统都具有自己的指令系统 汇编语言&#xff1a;用助…

shiro

概述 shiro是什么 Apache Shiro 是一个功能强大且易于使用的 Java 安全(权限)框架。Shiro 可以完成&#xff1a;认证、授权、加密、会话管理、与 Web 集成、缓存 等。借助 Shiro 您可以快速轻松地保护任何应用程序——从最小的移动应用程序到最大的 Web 和企业应用程序。 为…

批量数据导入Neo4j的方式

批量数据导入Neo4j的方式 文章目录批量数据导入Neo4j的方式1、写在前面2、前置芝士3、CSV数据导入Neo4j3.1 LOAD CSV Cypher命令3.2 neo4j-admin命令3.3 Kettle导入工具4、数据导入失败5、参考资料1、写在前面 Linux版本&#xff1a;Ubuntu Kylin 16.04Neo4j版本&#xff1a;N…

分布式微服务架构下网络通信的底层实现原理

在分布式架构中&#xff0c;网络通信是底层基础&#xff0c;没有网络&#xff0c;也就没有所谓的分布式架构。只有通过网络才能使得一大片机器互相协作&#xff0c;共同完成一件事情。 同样&#xff0c;在大规模的系统架构中&#xff0c;应用吞吐量上不去、网络存在通信延迟、…

图的遍历(基础)

一、图的遍历的相关定义 遍历的定义&#xff1a;从已给的连通图中的某一顶点出发&#xff0c;沿着一些边访遍图中的所有的顶点&#xff0c;且使每个顶点仅被访问一次&#xff0c;就叫做图的遍历&#xff0c;它是图的基本运算。遍历的实质&#xff1a;找每个邻接点的过程。图的特…

跌倒自动检测雷达的应用

跌倒是老人最常发生的意外之一&#xff0c;据统计&#xff0c;我国每年有150多万老人跌倒身亡。如何及时发现和预防老人跌倒以及避免可能发生的伤害事件&#xff0c;已成为目前社会关注的热点。美国加州大学伯克利分校研究人员研发出一种可穿戴设备——跌倒自动检测雷达&#x…

大规模MIMO通信系统的发射端采用混合波束成形附Matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

阅读笔记 DAGA 低资源标记任务数据扩充方法

阅读笔记 DAGA 低资源标记任务数据扩充方法 文章目录阅读笔记 DAGA 低资源标记任务数据扩充方法前言概述IntroductionBackground1、NER2、Part-of-Speech (POS) Tagging3、Target Based Sentiment AnalysisProposed Method1、Labeled Sentence Linearization2、Language Modeli…

c#入门-命名参数

位置参数 在你填入实参时&#xff0c;你填入的类型和顺序都需要和形参相同。 因为默认情况下&#xff0c;他是按照从左到右的顺序依次填入实参的。 命名参数 你在声明参数时给变量声明的名字是有用的。 在填入实参时&#xff0c;可以加上名字&#xff0c;以指定你填入的是哪…

触觉智能分享-低成本高性能的厨电解决方案

每个热爱生活的人&#xff0c;一日三餐四季&#xff0c;柴米油盐这点小事&#xff0c;都值得去享受&#xff0c;可如何学会做一顿美味可口的饭菜&#xff0c;对厨房小白来说可不是易事&#xff0c;智能菜谱的出现&#xff0c;将做菜的烹饪过程进行了分步拆解&#xff0c;同时将…

基于STM32波形信号发生器proteus仿真设计(仿真+程序+报告+讲解)

基于STM32波形信号发生器proteus仿真设计(仿真程序报告讲解&#xff09; 仿真图proteus 8.9 程序编译器&#xff1a;keil 5 编程语言&#xff1a;C语言 设计编号&#xff1a;C0075 讲解仿真视频&#xff1a; 基于STM32的波形信号发生器proteus仿真设计主要功能&#xff1a…

nessus无法导出报告(nessus转中文报告)

nessus漏扫报告&#xff0c;可是nessus导出的报告全是英文&#xff0c;客户说看不懂(说的好像中文就能看懂似的)。找了很多nessus转中文报告的工具都不是很靠谱&#xff0c;今天突然灵机一动发现了一个解决nessus无法导出报告方法。总的说来就是生产一个html文件转中文。 ness…

入门系列 - Git安装与配置

Git安装与配置 要使用Git&#xff0c;你必须在你的电脑上安装它。要不要使用并升级到最新的Git&#xff0c;那取决您的需要了。 下载Git 要下载Git安装程序&#xff0c;请访问Git的官方网站并进入下载页面。本文写于2022-11-29&#xff0c;此时您可以去官网链接去下载&#…

AI教你学测试

ChatGPT这个词相信大家最近看到都不会陌生&#xff0c;应该刷爆了各位的朋友圈&#xff0c;各种分享注册教程、什么AI写代码的文章比比皆是&#xff0c;今天&#xff0c;让我们一起来看一下OpenAI能不能教我们学测试呢&#xff0c;对测试人员的日常工作是否有帮助呢&#xff1f…

原生API编写简单富文本编辑器004

原生API编写富文本编辑器004 遗留的问题&#xff1a; 设置的字体是使用 font属性&#xff0c;而非CSS设置的字号只接受1-7, 并且是以 size 属性而非 CSS控制&#xff0c;超出大小无法设置。color使用HTML的input时&#xff0c;始终有一个input框在那里&#xff0c;并且如果手…