lua vm 常识一: attempt to yield across a C-call boundary 的原因分析

使用 lua 的时候有时候会遇到这样的报错:“attempt to yield across a C-call boundary”。


1. 网络上的解释

可以在网上找到一些关于这个问题的解释。


1.1 解释一

这个 issue:一个关于 yield across a C-call boundary 的问题,云风的解释是:

C (skynet framework)->lua (skynet service) -> C -> lua
最后这个 lua 里如果调用了 yield 就会产生。


云风这样解释没问题,但太简了,只说了这样会导致报错,但没具体说为什么会报错。


1.2 解释二

这篇文章 lua中并不能随意yield 提到:

流程:coroutine --> c --> coroutine --> yield ===> 报错
为什么这种情况下lua会给出这种报错呢?主要是因为在从c函数调回到coroutine中yield时,coroutine当前的堆栈情况会被保存在lua_State中,因此在调用resume时,lua可以恢复yield时的场景,并继续执行下去。但c函数不会因为coroutine的yield被挂起,它会继续执行下去,函数执行完后堆栈就被销毁了,所以无法再次恢复现场。而且因为c函数不会被yield函数挂起,导致c和lua的行为也不一致了,一个被挂起,一个继续执行完,代码逻辑很可能因此出错。


这个接近于胡说了。


1.3 解释三

这篇文章 深入Lua:在C代码中处理协程Yield 提到:

原因是Lua使用longjmp来实现协程的挂起,longjmp会跳到其他地方去执行,使得后面的C代码被中断。l_foreach函数执行到lua_call,由于longjmp会使得后面的指令没机会再执行,就像这个函数突然消失了一样,这肯定会引起不可预知的后果,所以Lua不允许这种情况发生,它在调用coroutine.yield时抛出上面的错误。


作者点出了问题的关键: “由于longjmp会使得后面的指令没机会再执行”,但讲得不够细,对于问题产生的条件没有讲清楚。


1.4 小结

以上解释,感觉都没有把这个问题说清楚,需要深入到 lua vm 的工作机制才能解释清楚,所以有了这篇文章。


2. 从原理上分析问题

问题的关键就在于:

  • lua 是通过 setjmp/longjmp 实现 resume/yield 的。

  • lua 函数只操作 lua 数据栈,而 c 函数不止操作 lua 数据栈,还会操作 c 栈(即操作系统线程的栈)。

  • 每个 lua 协程都有一个独立的 lua 数据栈,但每个系统线程只有一个公共的 c 栈。

  • 在协程的函数调用链中,会有 lua 函数也会有 c 函数,如果调用链中有 c 函数,并且在更后续的调用中出现 yield,就会 longjmp 回到 resume (setjmp) 之处,从而导致 c 函数依赖的 c 栈被恢复执行的协程的 c 函数调用给覆盖掉。

setjmp/longjmp 示意图:

c 栈从栈底向栈顶生长 栈底|     ||     ||-----| co1 resume (setjmp) <-|     |                      | | co2 |                      ||stack|                      ||-----| co2 yield (longjmp) ->栈顶

不懂 setjmp / longjmp 怎么工作的,可以参考这篇文章,讲得很细了: setjmp是怎么工作的 。


图1:lua yield 示意图

上图中:

1、co1 resume 了 co2,co2 开始执行,co2 的 callinfo 调用链中有 lua 也有 c 函数,其中的 c 函数会操作 lua 数据栈和 c 栈,c 栈在图中就是 “co2 c stack” 那一块内存。

2、co2 yield 的时候,co2 停止执行,co1 从上次 resume 处恢复。

3、co1 继续往下执行,必然会有 c 函数调用,co1 的 c 函数会把 “co2 c stack” 这块内存覆盖掉,这意味着 co2 那些还没执行完成的 c 函数的 c 栈被破坏了,即使 co2 再次被 resume,也无法正常运行了。


3. 从代码上分析问题

其实讲完原理就够了,但是 lua 在 yield 这个问题上会选择性不报错,所以还是有必要从源码上讲一讲。

以下分析使用的 lua 版本是 5.3.6(lua-5.2 跟 lua-5.4 也是差不多的)。

lua-5.3.6 官方下载链接: https://lua.org/ftp/lua-5.3.6.tar.gz 。

笔者的 github 也有 lua-5.3.6 源码的 copy: https://github.com/antsmallant/antsmallant_blog_demo/tree/main/3rd/lua-5.3.6 。


下文展示的 demo 代码都在此(有 makefile,可以直接跑起来):https://github.com/antsmallant/antsmallant_blog_demo/tree/main/blog_demo/lua-co-yield 。


3.1 情况一:lua 调用 c,在 c 中直接 yield

结果
yield 时不会报错,但实际上也没能正常工作。

不报错的原因
这是 lua 官方的设定,lua 调用 c 函数或者其他什么函数,都是被编译成 OP_CALL 指令,而 OP_CALL 并不会设一个标志位导致后面有 yield 的时候报错;而 c 调用 lua 是用 lua_call 这个 api,它会设置一个标志位,后面 yield 时判断到标志位就报错: “attempt to yield across a C-call boundary”。

没能正常工作的原因
上面分析过了,yield 之后,协程的 c 栈被恢复执行的协程覆盖掉了。


上代码吧。


lua 代码:test_co_1.lua

-- test_co_1.lualocal co = require "coroutine"
local clib = require "clib"local co2 = co.create(function()clib.f1()
end)-- 第一次 resume
local ok1, ret1 = co.resume(co2)
print("in lua:", ok1, ret1)-- 第二次 resume
local ok2, ret2 = co.resume(co2)
print("in lua:", ok2, ret2)

c代码:clib.c

// clib.c#include <stdlib.h>
#include <stdio.h>
#include <lua.h>
#include <lauxlib.h>static int f1(lua_State* L) {printf("clib.f1: before yield\n");lua_pushstring(L, "yield from clib.f1");lua_yield(L, 1);printf("clib.f1: after yield\n");return 0;
}LUAMOD_API int luaopen_clib(lua_State* L) {luaL_Reg funcs[] = {{"f1", f1},{NULL, NULL}};luaL_newlib(L, funcs);return 1;
}

输出:

clib.f1: before yield
first time return:      true    yield from clib.f1
second time return:     true    nil

clib.f1 的这句代码 printf("clib.f1: after yield\n"); 在第二次 resume 的时候没有被执行,但代码也没报错,跟开头说的结果一样。lua 大概是认为没有人会这样写代码吧。

这种情况,如果要让 clib.f1 能执行 yield 之后的,需要把 lua_yield 换成 lua_yieldk,然后把 yield 之后要执行的逻辑放到另一个函数里,类似这样:



int f2_after_yield(lua_State* L, int status, lua_KContext ctx) {printf("clib.f2: after yield\n");return 0;
}static int f2(lua_State* L) {printf("clib.f2: before yield\n");lua_pushstring(L, "yield from clib.f2");lua_yieldk(L, 1, 0, f2_after_yield);return 0;
}

3.2 情况二:c 调用 lua,lua 后续调用出现 yield

结果
yield 时会报错 “attempt to yield across a C-call boundary”。

原因
上面原理的时候分析过了,源码实现上,c 调用 lua 是用的 lua_call 这个 api,它会设置一个标志位,在后续调用链中(无论隔了多少层,无论是 c 还是 lua)只要执行了 yield,都会判断标志位,然后触发报错。


上代码吧。


lua 代码: test_co_3.lua

-- test_co_3.lualocal co = require "coroutine"
local clib = require "clib"function lua_func_for_c()print("enter lua_func_for_c")co.yield()print("leave lua_func_for_c")
endlocal co2 = co.create(function()print("enter co2")clib.f3()print("leave co2")
end)local ok, err = co.resume(co2)
print(ok, err)

c 代码:clib.c


// clib.c#include <stdlib.h>
#include <stdio.h>
#include <lua.h>
#include <lauxlib.h>static int f3(lua_State* L) {printf("enter f3\n");lua_getglobal(L, "lua_func_for_c");  lua_call(L, 0, 0); // 调用 lua 脚本里定义的 lua 函数: lua_func_for_cprintf("leave f3\n");return 0;
}LUAMOD_API int luaopen_clib(lua_State* L) {luaL_Reg funcs[] = {{"f3", f3},{NULL, NULL}};luaL_newlib(L, funcs);return 1;
}

输出:

enter co2
enter f3
enter lua_func_for_c
false   attempt to yield across a C-call boundary

clib 里的 c 函数 f3,通过 lua_call 调用 lua 脚本里面定义的 lua 函数 lua_func_for_c,而 lua_func_for_c 里面会 yield,所以这种情况下 yield 就直接报错了。


3.3 lua_call 是如何阻止后续 yield 的?

直接看 lua 源码,lua_callk 会调用到 luaD_callnoyield,而 luaD_callnoyield 设置了标志位:

L->nny++;

而在 lua_yieldk 中,判断了标志位:

  if (L->nny > 0) {if (L != G(L)->mainthread)luaG_runerror(L, "attempt to yield across a C-call boundary");elseluaG_runerror(L, "attempt to yield from outside a coroutine");}

4. 问题总结 & 解决办法


4.1 问题总结

经过上面分析,可以看到,问题的核心在于 lua 的多个协程共用一个 c 栈,而协程里面 c 函数调用又会依赖 c 栈,如果在它返回之前就 yield 了,则它依赖的 c 栈会被其他协程覆盖掉,也就无法恢复运行了。按照 luajit 的说法,lua 官方实现不是一种 “fully resumable vm”。

这里面 yield 又分两种情况:

情况症状原因
lua调c不报错,但也不正常工作lua 里调用函数(无论 lua 或 c),都是编译成 OP_CALL 指令,这个指令的实现不会设置让 yield 报错的标志位
c调lua报错用的是 lua_call,它会设置让 yield 报错的标志位

4.2 解决办法


4.2.1 lua-5.2 及以上

lua 对于此问题的解决方案是引入 lua_callk / lua_pcallk / lua_yieldk,要求使用者把 yield 之后要执行的东西放到一个单独的函数 (类型为 lua_KFunction) 里,k 意为 continue,把这个 k 函数作为参数传给 lua_callk / lua_pcallk / lua_yieldk,这个 k 函数会被记录起来,等 yield 返回的时候调用它。

显然,lua 官方的这种方案有点操蛋,但也不失为一种办法。

不过悲催的是,lua 5.2 才引入 kfunction 的,所以 lua-5.1 要用其他的办法。


4.2.2 lua-5.1

lua-5.1 有两个办法,都与 luajit 相关。


方法一:使用 luajit

直接使用 luajit ( https://luajit.org/luajit.html ),luajit 支持 “Fully Resumable VM”[1]:

The LuaJIT VM is fully resumable. This means you can yield from a coroutine even across contexts, where this would not possible with the standard Lua 5.1 VM: e.g. you can yield across pcall() and xpcall(), across iterators and across metamethods.


方法二:使用 lua-5.1.5 + coco 库

coco 库是 luajit 下面的一个子项目 ( https://coco.luajit.org/index.html ),它可以独立于 luajit 之外使用的,但它只能用于 lua-5.1.5 版本。

它的介绍[2]:

Coco is a small extension to get True C Coroutine semantics for Lua 5.1. Coco is available as a patch set against the standard Lua 5.1.5 source distribution.

Coco is also integrated into LuaJIT 1.x to allow yielding for JIT compiled functions. But note that Coco does not depend on LuaJIT and works fine with plain Lua.

coco 库能做到从 c 调用中恢复,是因为它为每个协程准备了专用的 c 栈:“Coco allows you to use a dedicated C stack for each coroutine”[2]。

所以,如果不使用 luajit,就使用官方的 lua-5.1.5,再 patch 上这个 coco 库就可以了。


5. 总结

  • lua 官方实现的 vm 不是 “fully Resumable” 的,原因在于多个协程共用 c 栈,会导致协程的函数调用链中有 c 函数的情况下,yield 报错或工作不正常。

  • lua 提供的函数中,有些使用了 lua_call/lua_pcall,容易导致 yield 报错,比如 lua 函数:require,c 函数:luaL_dostring、luaL_dofile。

  • lua 提供的函数中,有些使用了 lua_callk/lua_pcallk 规避 yield 报错,比如 lua 函数:dofile。

  • lua-5.2 及以上的,可以使用 lua_callk / lua_pcallk / lua_yieldk 来规避 yield 报错问题。

  • lua-5.1 可以使用 luajit 或 lua-5.1.5官方版本+coco库的方法来解决 yield 报错问题。


6. 参考

[1] luajit. extensions. Available at https://luajit.org/extensions.html.

[2] luajit. Coco — True C Coroutines for Lua. Available at https://coco.luajit.org/index.html.

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

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

相关文章

轮廓系数(Average silhouette) | 最佳聚类数的判定

1.最佳分类个数 # 辅助确定最佳聚类数 4.7*2.6 factoextra::fviz_nbclust( t(DPAU_2), kmeans, method "silhouette")在2有下降拐点&#xff0c;但是样本较多时分成2类一般意义不大。 在7时也有下降拐点。 2.查看每个分类的轮廓系数 (1) pam k5 library(cluste…

【Paddle】Inplace相关问题:反向传播、影响内存使用和性能

【Paddle】Inplace相关问题&#xff1a;反向传播、影响内存使用和性能 写在最前面inplace 的好处有哪些&#xff1f;能降低计算复杂度吗在反向传播时&#xff0c;Inplace为什么会阻碍呢&#xff1f;“计算图的完整性受损”表达有误原地操作 sin_()为什么原地操作会阻碍反向传播…

活动会议邀请函制作易企秀源码系统 清爽的画面轻轻滑动自动翻页 带完整的前后端搭建教程

系统概述 在当今数字化时代&#xff0c;活动会议的组织和宣传变得至关重要。为了满足这一需求&#xff0c;活动会议邀请函制作易企秀源码系统应运而生。它不仅为用户提供了一个便捷、高效的工具&#xff0c;还具备一系列令人瞩目的特色功能&#xff0c;为活动会议的成功举办提…

Ubuntu22.04设置程序崩溃产生Core文件

Ubuntu22.04设置程序崩溃产生Core文件 文章目录 Ubuntu22.04设置程序崩溃产生Core文件摘要Ubuntu 生成Core文件配置1. 检查 core 文件大小限制2. 设置 core 文件大小限制3. 配置 core 文件命名和存储路径4. 重启系统或重新加载配置5. 测试配置 关键字&#xff1a; Ubuntu、 C…

CSS浮动详细教学(CSS从入门到精通学习第四天)

css第04天 一、其他样式 1、圆角边框 在 CSS3 中&#xff0c;新增了圆角边框样式&#xff0c;这样我们的盒子就可以变圆角了。 border-radius 属性用于设置元素的外边框圆角。 语法&#xff1a; border-radius:length; 参数值可以为数值或百分比的形式如果是正方形&…

RTT UART设备框架学习

UART简介 UART&#xff08;Universal Asynchronous Receiver/Transmitter&#xff09;通用异步收发传输器&#xff0c;UART 作为异步串口通信协议的一种&#xff0c;工作原理是将传输数据的每个字符一位接一位地传输。是在应用程序开发过程中使用频率最高的数据总线。 UART串…

MySQL注入 — Dns 注入

DNS注入原理 通过子查询&#xff0c;将内容拼接到域名内&#xff0c;让load_file()去访问共享文件&#xff0c;访问的域名被记录此时变为显错注入,将盲注变显错注入,读取远程共享文件&#xff0c;通过拼接出函数做查询,拼接到域名中&#xff0c;访问时将访问服务器&#xff0c;…

CISP难度将加大?还考不考啊...

最新消息&#xff1a;CISP即将调整知识体系大纲&#xff0c;更新题库&#xff0c;后续考试难度加大。 最近几年&#xff0c;CISP改版地比较频繁&#xff0c;难度也在不断上升&#xff0c;因此各位小伙伴有考CISP想法的尽早考。 随着《网络安全法》、《网络空间安全战略》、《…

2024/5/28 P1247 取火柴游戏

取火柴游戏 题目描述 输入 k k k 及 k k k 个整数 n 1 , n 2 , ⋯ , n k n_1,n_2,\cdots,n_k n1​,n2​,⋯,nk​&#xff0c;表示有 k k k 堆火柴棒&#xff0c;第 i i i 堆火柴棒的根数为 n i n_i ni​&#xff1b;接着便是你和计算机取火柴棒的对弈游戏。取的规则如下&…

定点化和模型量化(三)

量化解决的是训练使用的浮点和运行使用的硬件只支持定点的矛盾。这里介绍一些实际量化中使用到的工具。 SNPE简介 The Snapdragon Neural Processing Engine (SNPE)是高通骁龙为了加速网络模型设计的框架。但它不只支持高通&#xff0c;SNPE还支持多种硬件平台&#xff0c;AR…

Beego 使用教程 8:Session 和 Cookie

beego 是一个用于Go编程语言的开源、高性能的 web 框架 beego 被用于在Go语言中企业应用程序的快速开发&#xff0c;包括RESTful API、web应用程序和后端服务。它的灵感来源于Tornado&#xff0c; Sinatra 和 Flask beego 官网&#xff1a;http://beego.gocn.vip/ 上面的 be…

抄表营收系统是什么?

1.抄表营收系统的概念和功能 抄表营收系统是一种自动化软件&#xff0c;主要运用于公用事业公司(如电力工程、水、天然气等)管理方法其服务的计量检定、计费和收付款全过程。该系统根据集成化智能仪表、远程控制数据收集和分析功能&#xff0c;提高了效率&#xff0c;降低了人…

人脸识别——探索戴口罩对人脸识别算法的影响

1. 概述 人脸识别是一种机器学习技术&#xff0c;广泛应用于各种领域&#xff0c;包括出入境管制、电子设备安全登录、社区监控、学校考勤管理、工作场所考勤管理和刑事调查。然而&#xff0c;当 COVID-19 引发全球大流行时&#xff0c;戴口罩就成了日常生活中的必需品。广泛使…

反射机制大揭秘-进阶Java技巧,直击核心!

反射在Java中扮演着重要的角色&#xff0c;掌握了反射&#xff0c;就等于掌握了框架设计的钥匙。本文将为您逐步讲解反射的基本概念、获取Class对象的三种方式、使用反射实例化对象并操作属性和方法&#xff0c;还有解析包的相关内容。跟随我一起探索反射的奥秘&#xff0c;提升…

使用 Ubuntu + Docker + Vaultwarden + Tailscale 自建密码管理器

使用 Ubuntu Docker Vaultwarden Tailscale 自建密码管理器 先决条件 一台运行 Ubuntu 系统的服务器。可以是云提供商的 VPS、家庭网络中的树莓派、或者 Windows 电脑上的虚拟机等等 一个 Tailscale 账户。如果还没有 Tailscale 账户&#xff0c;可以通过此链接迅速创建一个…

SelfKG论文翻译

SelfKG: Self-Supervised Entity Alignment in Knowledge Graphs SelfKG&#xff1a;知识图中的自监督实体对齐 ABSTRACT 实体对齐旨在识别不同知识图谱&#xff08;KG&#xff09;中的等效实体&#xff0c;是构建网络规模知识图谱的基本问题。在其发展过程中&#xff0c;标…

zynq之UART

之前尝试UART0&#xff08;MIO50、51&#xff09;&#xff0c;串口调试助手收到发送的内容。 现在板子上EMIO端有多个串口&#xff0c;所以看看这个怎么弄。 串口是484的转接板&#xff08;接232的串口就会输出乱码&#xff09; https://blog.51cto.com/u_15262460/2882973 …

【九十三】【算法分析与设计】719. 找出第 K 小的数对距离,N 台电脑的最长时间,二分答案法

719. 找出第 K 小的数对距离 - 力扣&#xff08;LeetCode&#xff09; 数对 (a,b) 由整数 a 和 b 组成&#xff0c;其数对距离定义为 a 和 b 的绝对差值。 给你一个整数数组 nums 和一个整数 k &#xff0c;数对由 nums[i] 和 nums[j] 组成且满足 0 < i < j < nums.le…

java调用远程接口下载文件

在postman中这样下载文件 有时下载文件太大postman会闪退&#xff0c;可以通过代码下载&#xff0c;使用hutool的http包

3步操作助您轻松实现苹果手机照片一键传输至电脑

对于很多使用苹果手机的用户来说&#xff0c;随着手机中照片和视频数量的不断积累&#xff0c;如何将这些珍贵的回忆从手机转移到电脑&#xff0c;以便更好地保存、整理和分享&#xff0c;成为了一个值得关注的问题。那么&#xff0c;苹果手机怎么把照片导入电脑呢&#xff1f;…