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/21538.shtml

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

相关文章

【最新鸿蒙应用开发】——实用广告思路,可动态修改(方便运营)

鸿蒙项目加入广告展示页业务 广告页的思路——华为有广告业务&#xff0c;但是我们不用- ad模块&#xff1b; 想自定义广告——场景&#xff1a; app启动-有广告需求&#xff0c;就打开广告页&#xff0c;没有的话就去登录或者主页&#xff1b; 腾讯体育的广告- 启动有广告页…

适合小白学习的项目1894java开发ssm框架校园跑腿管理系统myeclipse开发mysql数据库springMVC模式java编程计算机网页设计

一、源码特点 java ssm 校园跑腿管理系统是一套完善的web设计系统&#xff08;系统采用SSM框架进行设计开发&#xff0c;springspringMVCmybatis&#xff09;&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采…

Java项目:96 springboot精品在线试题库系统

作者主页&#xff1a;舒克日记 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文中获取源码 项目介绍 这次开发的精品在线试题库系统有管理员&#xff0c;教师&#xff0c;学生三个角色。 管理员功能有个人中心&#xff0c;专业管理&#xff0c;学生管理…

比较(二)利用python绘制雷达图

比较&#xff08;二&#xff09;利用python绘制雷达图 雷达图&#xff08;Radar Chart&#xff09;简介 雷达图可以用来比较多个定量变量&#xff0c;也可以用于查看数据集中变量的得分高低&#xff0c;是显示性能表现的理想之选。缺点是变量过多容易造成阅读困难。 快速绘制…

网站入门:Flask用法讲解

Flask是一个使用Python编写的轻量级Web服务框架&#xff0c;旨在帮助开发人员快速构建和部署Web应用程序。下面将对Flask进行更为详细的解释说明&#xff0c;并展示其使用示例与注意事项&#xff1a; 1.解释说明 定义及特点: Flask以其简洁和灵活著称&#xff0c;允许开发者以…

C++:list模拟实现

hello&#xff0c;各位小伙伴&#xff0c;本篇文章跟大家一起学习《C&#xff1a;list模拟实现》&#xff0c;感谢大家对我上一篇的支持&#xff0c;如有什么问题&#xff0c;还请多多指教 &#xff01; 如果本篇文章对你有帮助&#xff0c;还请各位点点赞&#xff01;&#xf…

LeetCode题练习与总结:二叉树展开为链表--114

一、题目描述 给你二叉树的根结点 root &#xff0c;请你将它展开为一个单链表&#xff1a; 展开后的单链表应该同样使用 TreeNode &#xff0c;其中 right 子指针指向链表中下一个结点&#xff0c;而左子指针始终为 null 。展开后的单链表应该与二叉树 先序遍历 顺序相同。 …

格式化数据恢复指南:从备份到实战,3个技巧一网打尽

朋友们&#xff01;你们有没有遇到过那种“啊&#xff0c;我的文件呢&#xff1f;”的尴尬时刻&#xff1f;无论是因为手滑、电脑抽风还是其他原因&#xff0c;数据丢失都可能会让我们抓狂&#xff0c;甚至有时候&#xff0c;我们可能一不小心就把存储设备格式化了&#xff0c;…

香橙派OrangePI AiPro测评 【运行qt,编解码,xfreeRDP】

实物 为AI而生 打开盒子 配置 扛把子的 作为业界首款基于昇腾深度研发的AI开发板&#xff0c;Orange Pi AIpro无论在外观上、性能上还是技术服务支持上都非常优秀。采用昇腾AI技术路线&#xff0c;集成图形处理器&#xff0c;拥有8GB/16GB LPDDR4X&#xff0c;可以外接32…

进程通信——管道

什么是进程通信&#xff1f; 进程通信是实现进程间传递数据信息的机制。要实现数据信息传递就要进程间共享资源——内存空间。那么是哪块内存空间呢&#xff1f;进程间是相互独立的&#xff0c;一个进程不可能访问其他进程的内存空间&#xff0c;那么这块空间只能由操作系统提…

【全开源】简单商城系统源码(PC/UniAPP)

提供PC版本、UniAPP版本(高级授权)、支持多规格商品、优惠券、积分兑换、快递鸟电子面单、支持移动端样式、统计报表等 提供全部前后台无加密源代码、数据库离线部署。 构建您的在线商店的基石 一、引言&#xff1a;为什么选择简单商城系统源码&#xff1f; 在数字化时代&am…

【Spring Cloud Alibaba】初识Spring Cloud Alibaba

目录 回顾主流的微服务框架Spring Cloud 版本简介Spring Cloud以往的版本发布顺序排列如下&#xff1a; 由停更引发的"升级惨案"哪些Netflix组件被移除了&#xff1f; 替换方案服务注册中心&#xff1a;服务调用&#xff1a;负载均衡&#xff1a;服务降级&#xff1a…

干货分享 | TSMaster 中 Hex 文件编辑器使用详细教程

TSMaster 软件的 Hex 文件编辑器提供了文件处理的功能&#xff0c;这一特性让使用 TSMaster 软件的用户可以更便捷地对 Hex、bin、mot、s19 和 tsbinary 类型的文件进行处理。 本文重点讲述 TSMaster 中 Hex 文件编辑器的使用方法&#xff0c;该编辑器能实现将现有的 Hex、bin、…

@vue-office/excel 解决移动端预览excel文件触发软键盘

先直接上代码 不耽误大家时间 标明下插件库 非常感谢作者提供预览插件 vue-office/excel 只需要控制CSS :deep(.x-spreadsheet-overlayer) {.x-spreadsheet-selectors {display: none !important;} } :deep(.x-spreadsheet-bottombar) {li.active {user-select: none !import…

家政上门系统源码,家政上门预约服务系统开发涉及的主要功能

家政上门预约服务系统开发是指建立一个在线平台或应用程序&#xff0c;用于提供家政服务的预约和管理功能。该系统的目标是让用户能够方便地预约各种家政服务&#xff0c;如保洁、家庭护理、月嫂、家电维修等&#xff0c;并实现服务供应商管理和订单管理等功能。 以下是开发家政…

linux驱动学习(三)之uboot与内核编译

需要板子一起学习的可以这里购买&#xff08;含资料&#xff09;&#xff1a;点击跳转 GEC6818内核源码下载&#xff1a;点击跳转 一、环境配置 由于GEC6818对应是64位系统&#xff0c;虚拟机中的linux系统也要是64位&#xff0c;比如&#xff1a;ubuntu16.04.rar …

某红书旋转滑块验证码分析与协议算法实现(高通过率)

文章目录 1. 写在前面2. 接口分析3. 验证轨迹4. 算法还原 【&#x1f3e0;作者主页】&#xff1a;吴秋霖 【&#x1f4bc;作者介绍】&#xff1a;擅长爬虫与JS加密逆向分析&#xff01;Python领域优质创作者、CSDN博客专家、阿里云博客专家、华为云享专家。一路走来长期坚守并致…

力扣SQL50 学生们参加各科测试的次数 查询 三表查询

Problem: 1280. 学生们参加各科测试的次数 &#x1f468;‍&#x1f3eb; 参考题解 join等价于inner join&#xff0c;不用关联条件的join等价于cross join Code select stu.student_id,stu.student_name, sub.subject_name,count(e.subject_name) attended_exams from Stud…

关于windosw打开安全中心空白的解决方案

关于windosw打开安全中心空白的解决方案 问题如下 问题如下 之后点击一片空白 解决方案如下 按下WINR&#xff0c;输入regedit回车找到路径&#xff1a;“HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\SecurityHealthService”&#xff0c;然后双击右边的“start”…

windows 下编译 TessRact+leptonica 识别图片文字

目录 1、下载 2. 编译基础依赖库 1.1 zlib 1.2 jpegsr9f 1.3 lpng1643 1.4 libgif 3. 编译tifflib 4. 配置nasm到系统环境中 5. 编译 libjpeg-turbo 6 编译leptonica 7. 编译tesseract 8. 测试验证 1、下载 下载tesseract5.3.2 下载leptonica1.83.1 下载l…