【问题标题】:Multithreaded C Lua module leading to segfault in Lua script多线程 C Lua 模块导致 Lua 脚本中的段错误
【发布时间】:2015-02-17 18:50:15
【问题描述】:


我为 Lua 编写了一个非常简单的 C 库,它由一个启动线程的函数组成,所述线程除了循环之外什么都不做:

#include "lua.h"
#include "lauxlib.h"
#include <pthread.h>
#include <stdio.h>

pthread_t handle;
void* mythread(void* args)
{
    printf("In the thread !\n");
    while(1);
    pthread_exit(NULL);
}

int start_mythread()
{
    return pthread_create(&handle, NULL, mythread, NULL);
}

int start_mythread_lua(lua_State* L)
{
    lua_pushnumber(L, start_mythread());
    return 1;
}

static const luaL_Reg testlib[] = {
    {"start_mythread", start_mythread_lua},
    {NULL, NULL}
};

int luaopen_test(lua_State* L)
{
/*
    //for lua 5.2
    luaL_newlib(L, testlib);
    lua_setglobal(L, "test");
*/
    luaL_register(L, "test", testlib);
    return 1;
}


现在,如果我编写一个非常简单的 Lua 脚本就可以了:

require("test")
test.start_mythread()

使用lua myscript.lua 运行脚本有时会导致段错误。以下是 GDB 对核心转储的评价:

Program terminated with signal 11, Segmentation fault.
#0  0xb778b75c in ?? ()
(gdb) thread apply all bt

Thread 2 (Thread 0xb751c940 (LWP 29078)):
#0  0xb75b3715 in _int_free () at malloc.c:4087
#1  0x08058ab9 in l_alloc ()
#2  0x080513a2 in luaM_realloc_ ()
#3  0x0805047b in sweeplist ()
#4  0x080510ef in luaC_freeall ()
#5  0x080545db in close_state ()
#6  0x0804acba in main () at lua.c:389

Thread 1 (Thread 0xb74efb40 (LWP 29080)):
#0  0xb778b75c in ?? ()
#1  0xb74f6efb in start_thread () from /lib/i386-linux-gnu/i686/cmov/libpthread.so.0
#2  0xb7629dfe in clone () at ../sysdeps/unix/sysv/linux/i386/clone.S:129

主线程的堆栈不时有一些变化。
似乎 start_thread 函数想要跳转到有时恰好属于无法访问的内存的给定地址(在本例中为 b778b75c)。
编辑
我也有一个 valgrind 输出:

==642== Memcheck, a memory error detector
==642== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==642== Using Valgrind-3.10.0 and LibVEX; rerun with -h for copyright info
==642== Command: lua5.1 go.lua
==642== 
In the thread !
In the thread !
==642== Thread 2:
==642== Jump to the invalid address stated on the next line
==642==    at 0x403677C: ???
==642==    by 0x46BEEFA: start_thread (pthread_create.c:309)
==642==    by 0x41C1DFD: clone (clone.S:129)
==642==  Address 0x403677c is not stack'd, malloc'd or (recently) free'd
==642== 
==642== 
==642== Process terminating with default action of signal 11 (SIGSEGV): dumping core
==642==  Access not within mapped region at address 0x403677C
==642==    at 0x403677C: ???
==642==    by 0x46BEEFA: start_thread (pthread_create.c:309)
==642==    by 0x41C1DFD: clone (clone.S:129)
==642==  If you believe this happened as a result of a stack
==642==  overflow in your program's main thread (unlikely but
==642==  possible), you can try to increase the size of the
==642==  main thread stack using the --main-stacksize= flag.
==642==  The main thread stack size used in this run was 8388608.
==642== 
==642== HEAP SUMMARY:
==642==     in use at exit: 1,296 bytes in 6 blocks
==642==   total heap usage: 515 allocs, 509 frees, 31,750 bytes allocated
==642== 
==642== LEAK SUMMARY:
==642==    definitely lost: 0 bytes in 0 blocks
==642==    indirectly lost: 0 bytes in 0 blocks
==642==      possibly lost: 136 bytes in 1 blocks
==642==    still reachable: 1,160 bytes in 5 blocks
==642==         suppressed: 0 bytes in 0 blocks
==642== Rerun with --leak-check=full to see details of leaked memory
==642== 
==642== For counts of detected and suppressed errors, rerun with: -v
==642== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
Killed


但是,到目前为止,我一直很好,只需打开 lua 解释器并一个接一个地手动输入相同的指令。
另外,一个 C 程序做同样的事情,使用相同的库:

int start_mythread();

int main()
{
    int ret = start_mythread();
    return ret;
}

应该如此,在我的测试中从未失败过。
我已经尝试过 Lua 5.1 和 5.2,但无济于事。
编辑: 我应该指出我在运行 32 位 Debian Wheezy (Linux 3.2) 的单核 eeePC 上测试了这个。
我刚刚在我的主机(4 核 64 位 Arch linux)上再次进行了测试,并使用lua myscript.lua segfaults 每次启动脚本... 从解释器提示符输入命令可以正常工作,以及上面的 C 程序。

我首先编写这个小库的原因是因为我正在编写一个更大的库,我首先遇到了这个问题。经过数小时徒劳的调试,包括一个一个地删除每个共享结构/变量(是的,我是那么绝望),我已经归结为这段代码。
所以,我的猜测是我在 Lua 上做错了什么,但那可能是什么?我已经尽可能多地搜索了这个问题,但我发现大多数人在从多个线程中使用 Lua API 时遇到问题(这不是我在这里想要做的)。
如果您有任何想法,我们将不胜感激。

编辑
更准确地说,我想知道在编写用于 Lua 脚本的 C 库时是否应该对线程采取额外的预防措施。 Lua 需要在动态加载库中创建的线程在“卸载”库时终止吗?

【问题讨论】:

  • 另外,当我将 mythread 创建为分离时,我会得到相同的行为。
  • 我认为 rpattiso 的回答涵盖了它。根据 pthread 文档,您不希望在子线程仍在运行时主线程退出的情况。现在您是否必须按照您的建议进行模块卸载?不,但实际上,这可能是最直接的。你可以做的就是把它连接到一个__gc 上,pthread_exit 被调用。将其连接到一个虚拟的全局对象,该对象会一直存在直到您退出 vm。这样可以确保在主退出之前调用 pthread_exit

标签: c linux multithreading lua pthreads


【解决方案1】:

为什么 Lua 模块会出现 Segfault?

您的 Lua 脚本在线程完成之前退出,这会导致段错误。 Lua 模块在解释器正常关闭期间使用dlclose() 卸载,因此线程的指令从内存中删除,并在读取下一条指令时出现段错误。

有哪些选择?

在卸载模块之前停止线程的任何解决方案都可以工作。在主线程中使用pthread_join() 将等待线程完成(您可能希望使用pthread_cancel() 终止长时间运行的线程)。在卸载模块之前在主线程中调用pthread_exit() 也可以防止崩溃(因为它会阻止dlclose()),但它也会中止Lua 解释器的正常清理/关闭过程。

以下是一些有效的示例:

int pexit(lua_State* L) {
   pthread_exit(NULL);
   return 0; 
} 

int join(lua_State* L)
{
  pthread_join(handle, NULL);
  return 0;
}

static const luaL_Reg testlib[] = {
    {"start_mythread", start_mythread_lua},
    {"join", join},
    {"exit", pexit},
    {NULL, NULL}
};

void* mythread(void* args) {
  int i, j, k;
    printf("In the thread !\n");
    for (i = 0; i < 10000; ++i) {
      for (j = 0; j < 10000; ++j) {
        for (k = 0; k < 10; ++k) {
          pow(1, i);
        }
      }
    }
    pthread_exit(NULL);
}

现在脚本将很好地退出:

require('test')
test.start_mythread()
print("launched thread")
test.join() -- or test.exit()
print("thread joined")

要自动执行此操作,您可以绑定到垃圾收集器,因为模块中的所有对象都在卸载共享对象之前被释放。 (正如大狼建议的那样)

Discussion on calling pthread_exit() from main(): 如果 main() 在它产生的线程之前完成,则存在一定的问题,如果 您没有明确调用 pthread_exit()。它的所有线程 created 将终止,因为 main() 已完成且不再存在 支持线程。通过让 main() 显式调用 pthread_exit() 作为它做的最后一件事,main() 将阻塞并保持活动状态 支持它创建的线程,直到它们完成。

(这句话有点误导:从main() 返回大致相当于调用exit(),这将退出包括所有正在运行的线程的进程。这可能是也可能不是您想要的行为。另一方面,在主线程中调用pthread_exit() 将退出主线程但保持所有其他线程运行,直到它们自行停止或其他人杀死它们。同样,这可能是也可能不是你想要的行为。有没问题,除非你为你的用例选择了错误的选项。)

【讨论】:

  • 在您链接的问题中,段错误的原因是线程试图访问已释放的主线程的堆栈。 AFAICT 我没有在mythread 中这样做……再说一次,如果我从 C 调用 start_mythread 并立即返回,它工作正常。在从 main 返回之前不加入所有线程实际上是否被认为是未定义的行为?无论如何感谢您的回答。
  • @ranjak 添加加入解决了问题吗?
  • 确实如此,但这并不能解释为什么没有它会出现段错误。同样,在 C 中,有一个 main 只做 return create_mythread 迄今为止从未引起过段错误。
  • @ranjak 好的,我编辑了更多信息和选项。不幸的是,我无法解释它为什么会出现段错误,但你为什么不想打电话给pthread_exit
  • 现在大家可能已经想通了,但可以肯定的是:当 Lua 在 lua_close() 期间卸载模块时,包含线程当前正在执行的指令的内存消失了。这就是导致 SIGSEGV 的原因。在库卸载工作之前调用pthread_exit(),但它会阻止 any 进一步清理。相反,我建议将每个线程的用户数据放入在其__gc 方法中调用pthread_cancel()(和pthread_join())的注册表中(对于空循环情况,您还需要将canceltype 设置为异步)。
【解决方案2】:

所以,看来我确实必须确保在 Lua 卸载我的库时我的所有线程都已完成。

解决方案

我可以设置一个在卸载库时调用的清理函数。
在这个函数中,我可以确保我的 lib 启动的所有线程都已终止。如果我有仍在运行的分离线程,从它调用pthread_exit 可能很容易,但我不确定它有多安全/干净,因为它会突然中断 Lua...
无论如何,我可以通过创建一个将__gc 字段设置为我的清理函数的元表来实现这一点,然后将此元表影响到 Lua 5.2 中我的 lib 表。

int cleanup(lua_State* L)
{
    /*Do the cleaning*/
    return 0;
}

int luaopen_test(lua_State* L)
{
    //for lua 5.2
    //metatable with cleanup method for the lib
    luaL_newmetatable(L, "test.cleanup");
    //set our cleanup method as the __gc callback
    lua_pushstring(L, "__gc");
    lua_pushcfunction(L, cleanup);
    lua_settable(L, -3);
    //open our test lib
    luaL_newlib(L, testlib);
    //associate it with our metatable
    luaL_setmetatable(L, "test.cleanup");

    return 1;
}

在 Lua 5.1 中,__gc 选项仅适用于用户数据。有几种解决方案可以让它在我的情况下工作:
- Lua shutdown/End of the program execution callback
- http://lua-users.org/wiki/LuaFaq(请参阅“为什么 __gc 和 __len 元方法不能在表上工作?”)
- Greatwolf 的解决方案是让全局对象附有所述元表。

【讨论】:

    猜你喜欢
    • 2016-09-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-01-29
    • 1970-01-01
    • 1970-01-01
    • 2011-01-12
    相关资源
    最近更新 更多