正确关闭libUV句柄

10

我正在尝试查找如何修复使用Valgrind运行此程序时出现的内存泄漏问题。这些泄漏发生在nShell_client_main中的两个分配中。但我不确定如何正确释放它们。

我尝试在nShell_Connect处释放它们,但这会导致libUV中止该程序。我尝试在nShell_client_main结尾处释放它们,但然后在关闭循环时出现读/写错误。有人知道我应该如何关闭这些句柄吗? 我已经阅读了这篇文章,让我开始思考。但是,它似乎已过时,因为uv_ip4_addr在最新版本中具有不同的原型。

nShell_main是“入口”点)

#include "nPort.h"
#include "nShell-main.h"

void nShell_Close(
    uv_handle_t * term_handle
){
}

void nShell_Connect(uv_connect_t * term_handle, int status){
    uv_close((uv_handle_t *) term_handle, 0);
}

nError * nShell_client_main(nShell * n_shell, uv_loop_t * n_shell_loop){

    int uv_error = 0;

    nError * n_error = 0;

    uv_tcp_t * n_shell_socket = 0;
    uv_connect_t * n_shell_connect = 0;

    struct sockaddr_in dest_addr;

    n_shell_socket = malloc(sizeof(uv_tcp_t));

    if (!n_shell_socket){
        // handle error
    }

    uv_error = uv_tcp_init(n_shell_loop, n_shell_socket);

    if (uv_error){
        // handle error
    }

    uv_error = uv_ip4_addr("127.0.0.1", NPORT, &dest_addr);

    if (uv_error){
        // handle error
    }

    n_shell_connect = malloc(sizeof(uv_connect_t));

    if (!n_shell_connect){
        // handle error
    }

    uv_error = uv_tcp_connect(n_shell_connect, n_shell_socket, (struct sockaddr *) &dest_addr, nShell_Connect);

    if (uv_error){
        // handle error
    }

    uv_error = uv_run(n_shell_loop, UV_RUN_DEFAULT);

    if (uv_error){
        // handle error
    }

    return 0;
}

nError * nShell_loop_main(nShell * n_shell){

    int uv_error = 0;

    nError * n_error = 0;

    uv_loop_t * n_shell_loop = 0;

    n_shell_loop = malloc(sizeof(uv_loop_t));

    if (!n_shell_loop){
        // handle error
    }

    uv_error = uv_loop_init(n_shell_loop);

    if (uv_error){
        // handle error
    }

    n_error = nShell_client_main(n_shell, n_shell_loop);

    if (n_error){
        // handle error
    }

    uv_loop_close(n_shell_loop);
    free(n_shell_loop);

    return 0;
}

在这段代码的switch语句结束处发生了断言(摘自Joyent在Github上的libUV页面):

void uv_close(uv_handle_t* handle, uv_close_cb close_cb) {
  assert(!(handle->flags & (UV_CLOSING | UV_CLOSED)));

  handle->flags |= UV_CLOSING;
  handle->close_cb = close_cb;

  switch (handle->type) {
  case UV_NAMED_PIPE:
    uv__pipe_close((uv_pipe_t*)handle);
    break;

  case UV_TTY:
    uv__stream_close((uv_stream_t*)handle);
    break;

  case UV_TCP:
    uv__tcp_close((uv_tcp_t*)handle);
    break;

  case UV_UDP:
    uv__udp_close((uv_udp_t*)handle);
    break;

  case UV_PREPARE:
    uv__prepare_close((uv_prepare_t*)handle);
    break;

  case UV_CHECK:
    uv__check_close((uv_check_t*)handle);
    break;

  case UV_IDLE:
    uv__idle_close((uv_idle_t*)handle);
    break;

  case UV_ASYNC:
    uv__async_close((uv_async_t*)handle);
    break;

  case UV_TIMER:
    uv__timer_close((uv_timer_t*)handle);
    break;

  case UV_PROCESS:
    uv__process_close((uv_process_t*)handle);
    break;

  case UV_FS_EVENT:
    uv__fs_event_close((uv_fs_event_t*)handle);
    break;

  case UV_POLL:
    uv__poll_close((uv_poll_t*)handle);
    break;

  case UV_FS_POLL:
    uv__fs_poll_close((uv_fs_poll_t*)handle);
    break;

  case UV_SIGNAL:
    uv__signal_close((uv_signal_t*) handle);
    /* Signal handles may not be closed immediately. The signal code will */
    /* itself close uv__make_close_pending whenever appropriate. */
    return;

  default:
    assert(0); // assertion is happening here
  }

  uv__make_close_pending(handle);
}

我可以手动调用 uv__tcp_close,但它不在公共头文件中(而且可能也不是正确的解决方案)。


提醒避免自己对自己的代码进行代码审查;你的函数参数布局不规范且非常奇怪(因此难以阅读)- 也不完全一致。 - Jonathan Leffler
@JonathanLeffler 是的,我一开始整个项目都是写那样的长函数。现在有点后悔了,但还没有机会重写它们。 - tay10r
4个回答

30

只有在调用关闭回调函数之后,libuv 才会完成对句柄的操作。这也是你可以释放句柄的确切时刻。

我看到你调用了uv_loop_close,但你没有检查返回值。如果还有未处理完的句柄,则会返回UV_EBUSY,因此你应该检查这一点。

如果想要关闭循环并关闭所有句柄,需要执行以下步骤:

  • 使用uv_stop停止循环
  • 使用uv_walk遍历并调用uv_close以关闭所有未关闭的句柄
  • 再次使用uv_run运行循环,以便调用所有关闭回调函数,并在回调函数中释放内存
  • 调用uv_loop_close,此时应该返回0

1
调用 uv_run 时应该使用哪种运行模式? - ruipacheco
2
使用UV_RUN_DEFAULT,因为所有的句柄都已关闭,可能需要多次循环迭代才能关闭并触发关闭回调函数。 - saghul
如果您还有正在运行的uv_req,则uv_loop将不会关闭。 - Yugy

13

我终于想出了如何停止循环并清理所有句柄的方法。 我创建了一堆句柄和SIGINT信号句柄:

uv_signal_t *sigint = new uv_signal_t;
uv_signal_init(uv_default_loop(), sigint);
uv_signal_start(sigint, on_sigint_received, SIGINT);

当接收到SIGINT(在控制台中按下Ctrl+C)时,将调用on_sigint_received回调函数。 on_sigint_received的样式如下:
void on_sigint_received(uv_signal_t *handle, int signum)
{
    int result = uv_loop_close(handle->loop);
    if (result == UV_EBUSY)
    {
        uv_walk(handle->loop, on_uv_walk, NULL);
    }
}

它会触发一个回调函数on_uv_walk

void on_uv_walk(uv_handle_t* handle, void* arg)
{
    uv_close(handle, on_uv_close);
}

它尝试关闭每个已打开的libuv句柄。 注意:我在调用uv_walk之前不会调用uv_stop,如saghul提到的。 在调用on_sigint_received函数后,libuv循环继续执行,并在下一次迭代中为每个已打开的句柄调用on_uv_close。如果您调用uv_stop函数,则不会调用on_uv_close回调。

void on_uv_close(uv_handle_t* handle)
{
    if (handle != NULL)
    {
        delete handle;
    }
}

之后,libuv不再有打开的句柄并完成循环(从uv_run退出):

uv_run(uv_default_loop(), UV_RUN_DEFAULT);
int result = uv_loop_close(uv_default_loop());
if (result)
{
    cerr << "failed to close libuv loop: " << uv_err_name(result) << endl;
}
else
{
    cout << "libuv loop is closed successfully!\n";
}

3

我喜欢Konstantin Gindemit提供的解决方案。

然而,我遇到了一些问题。他的on_uv_close()函数以核心转储结束。此外,uv_signal_t变量导致valgrind报告“明确丢失”的内存泄漏。

我正在使用他的代码,并对这两种情况进行修复。

void on_uv_walk(uv_handle_t* handle, void* arg) {
    uv_close(handle, NULL);
}

void on_sigint_received(uv_signal_t *handle, int signum) {
    int result = uv_loop_close(handle->loop);
    if(result == UV_EBUSY) {
        uv_walk(handle->loop, on_uv_walk, NULL);
    }
}

int main(int argc, char *argv[]) {
    uv_signal_t *sigint = new uv_signal_t;
    uv_signal_init(uv_default_loop(), sigint);
    uv_signal_start(sigint, on_sigint_received, SIGINT);
    uv_loop_t* main_loop = uv_default_loop();
...
    uv_run(main_loop, UV_RUN_DEFAULT));
    uv_loop_close(uv_default_loop());
    delete sigint;
    return 0;
}

0
我认为这基本上与Jeff Greer的回答相同,只是多了一些背景信息。
关于让valgrind与最新版本的libuv配合使用,我发现了一些在libuv/test/task.h中被认可的代码行,这使得两个错误的释放操作不再出现。
// You have to define this. DON'T define this to assert as it will skip the call when NDEBUG is defined
#define ASSERT(expr) expr

// Secret knowledge hidden within libuv's test folder
static void close_walk_cb(uv_handle_t* handle, void* arg) {
  if (!uv_is_closing(handle))
    uv_close(handle, NULL);
}

static void close_loop(uv_loop_t* loop) {
  uv_walk(loop, close_walk_cb, NULL);
  uv_run(loop, UV_RUN_DEFAULT);
}

/* This macro cleans up the event loop. This is used to avoid valgrind
 * warnings about memory being "leaked" by the event loop.
 */
#define MAKE_VALGRIND_HAPPY(loop)                   \
  do {                                              \
    close_loop(loop);                               \
    ASSERT(0 == uv_loop_close(loop));               \
    uv_library_shutdown();                          \
  } while (0)

你这样使用它
// Signal handling stuff for ctrl+C 
// This one is needed if you something set on server->data needs to be freed
void on_close(uv_handle_t* handle) {
    // free(handle->data); <- Uncomment if you know you need to free this
}

// Actual signal handler
void on_signal(uv_signal_t *handle, int signum) {
    uv_tcp_t *server = handle->data;
    uv_close((uv_handle_t*) server, on_close);
    uv_stop(handle->loop);
}

int main() {
    uv_tcp_t server;
    uv_tcp_init(uv_default_loop(), &server);

    struct sockaddr_in addr;
    uv_ip4_addr("0.0.0.0", DEFAULT_PORT, &addr);

    uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);
    int r = uv_listen((uv_stream_t*) &server, DEFAULT_BACKLOG, on_new_connection);

    // Don't forget this step if you're closing the program with ctrl+C
    uv_signal_t sig;
    uv_signal_init(loop, &sig);
    sig.data = &server;
    uv_signal_start(&sig, on_signal, SIGINT);

    uv_run(loop, UV_RUN_DEFAULT);

    uv_loop_close(loop);

    MAKE_VALGRIND_HAPPY(uv_default_loop());
}

当使用ctrl+C关闭服务器时,没有任何请求,valgrind的输出。 MAKE_VALGRIND_HAPPY 被注释掉。

make; valgrind --leak-check=full --show-leak-kinds=all --errors-for-leak-kinds=all --error-exitcode=1 --exit-on-first-error=yes ./server
make: `server' is up to date.
==9426== Memcheck, a memory error detector
==9426== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==9426== Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info
==9426== Command: ./server
==9426== 
^Csig.c:410: Closing
==9426== 
==9426== HEAP SUMMARY:
==9426==     in use at exit: 200 bytes in 2 blocks
==9426==   total heap usage: 4 allocs, 2 frees, 1,256 bytes allocated
==9426== 
==9426== 72 bytes in 1 blocks are still reachable in loss record 1 of 2
==9426==    at 0x54AE1F4: calloc (vg_replace_malloc.c:1328)
==9426==    by 0x54EDD6F: uv_loop_init (in /usr/lib64/libuv.so.1.0.0)
==9426==    by 0x54E64A7: uv_default_loop (in /usr/lib64/libuv.so.1.0.0)
==9426==    by 0x401B3F: main (sig.c:445)
==9426== 
==9426== 
==9426== Exit program on first error (--exit-on-first-error=yes)

在没有请求情况下,使用ctrl+C启动和关闭服务器时的valgrind输出MAKE_VALGRIND_HAPPYmain结束时执行。

make; valgrind --leak-check=full --show-leak-kinds=all --errors-for-leak-kinds=all --error-exitcode=1 --exit-on-first-error=yes ./server
clang -o server -Wall -Wsizeof-pointer-div -Wmissing-field-initializers -O0 -g3 sig.c -luv
==9542== Memcheck, a memory error detector
==9542== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==9542== Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info
==9542== Command: ./server
==9542== 
^Csig.c:410: Closing
==9542== 
==9542== HEAP SUMMARY:
==9542==     in use at exit: 0 bytes in 0 blocks
==9542==   total heap usage: 4 allocs, 4 frees, 1,256 bytes allocated
==9542== 
==9542== All heap blocks were freed -- no leaks are possible
==9542== 
==9542== For lists of detected and suppressed errors, rerun with: -s
==9542== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接