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.pswp.cn/bicheng/19318.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/19318.shtml
英文地址,請注明出處:http://en.pswp.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…

Dubbo底層RPC原理深度解析

Dubbo作為一款高性能的分布式服務框架&#xff0c;其核心在于其底層的RPC實現&#xff0c;它允許服務在分布式系統中的不同節點間透明地進行遠程調用。以下是Dubbo底層RPC原理的詳細介紹&#xff1a; 基本概念 RPC&#xff08;Remote Procedure Call&#xff09;是一種編程模型…

CSS浮動詳細教學(CSS從入門到精通學習第四天)

css第04天 一、其他樣式 1、圓角邊框 在 CSS3 中&#xff0c;新增了圓角邊框樣式&#xff0c;這樣我們的盒子就可以變圓角了。 border-radius 屬性用于設置元素的外邊框圓角。 語法&#xff1a; border-radius:length; 參數值可以為數值或百分比的形式如果是正方形&…

js 如何封裝一個iframe通訊的sdk

在JavaScript中&#xff0c;封裝一個用于iframe間通信的SDK&#xff0c;可以利用postMessage和message事件監聽來實現跨域通信。以下是一個簡單的示例&#xff0c;展示如何封裝這樣一個SDK&#xff1a; 步驟 1: 創建SDK文件 首先&#xff0c;創建一個名為IframeCommunicator.…

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;降低了人…

(十)Python3 接口自動化測試,測試結果發送郵件

(十)Python3 接口自動化測試,測試結果發送郵件 1.前言 Windows本地執行的話,可自行編寫發送郵件方法發送郵件。 Jenkins執行的話,可用jenkins配套郵件發送郵件。 2.發送郵件示例 # -*- coding: utf-8 -*- # 主程序 import sys sys.path.append(./server) sys.path.appe…

人臉識別——探索戴口罩對人臉識別算法的影響

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;標…

華納云:MAC電腦怎么遠程連接Windows服務器桌面?

在Mac電腦上遠程連接Windows服務器桌面可以通過多種方式實現&#xff0c;最常用的方法是使用微軟提供的免費應用程序 "Microsoft Remote Desktop"。以下是詳細的步驟來設置和使用該工具&#xff1a; 步驟一&#xff1a;下載和安裝 Microsoft Remote Desktop 打開App …