【问题标题】:Tracking a nasty memory leak in python3 C extension在 python3 C 扩展中跟踪令人讨厌的内存泄漏
【发布时间】:2018-12-27 09:33:20
【问题描述】:

我是libgpiod 的作者,在最近的版本中,我提供了一组作为 C 扩展模块实现的面向对象的 python3 绑定。

模块的完整代码可以在here找到。

最近有用户报告模块内存泄漏。从那以后,我一直在尝试对其进行调试,并设法找到并修复了一些其他与内存相关的问题,但不是造成这种确切泄漏的罪魁祸首。

以下是记者用来触发问题的脚本:

#!/usr/bin/env python3

import gpiod
import logging
import os
import psutil
import sys
import time

this_process = psutil.Process(os.getpid())
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

chip = gpiod.Chip('1', gpiod.Chip.OPEN_BY_NUMBER)
gpio_line = chip.get_line(12)
gpio_line.request(consumer="test", type=gpiod.LINE_REQ_DIR_OUT)
count = 0
mem_used_prev = 0

while True:
    mem_used = this_process.memory_info().rss
    if mem_used != mem_used_prev:
        logging.info('count: {}  memory usage: {}'.format(count, this_process.memory_info().rss))
        mem_used_prev = mem_used

    gpio_line.set_value(1)
    count += 1

示例输出:

2018-07-19 11:21:13,505 - INFO - count: 0  memory usage: 13459456
2018-07-19 11:21:13,516 - INFO - count: 638  memory usage: 14008320
2018-07-19 11:21:13,529 - INFO - count: 1298  memory usage: 14278656
2018-07-19 11:21:13,543 - INFO - count: 1958  memory usage: 14548992
2018-07-19 11:21:13,557 - INFO - count: 2618  memory usage: 14819328
2018-07-19 11:21:13,569 - INFO - count: 3278  memory usage: 15089664
2018-07-19 11:21:13,583 - INFO - count: 3938  memory usage: 15360000
2018-07-19 11:21:13,596 - INFO - count: 4598  memory usage: 15630336
2018-07-19 11:21:13,611 - INFO - count: 5258  memory usage: 15900672

每进行几次迭代,内存使用量就会突然激增。我猜这是调整堆大小的时候,但实际泄漏可能发生在每次迭代。

在调查时,我注意到泄漏发生在使用单个 GPIO 线的所有操作中,这涉及将单个对象打包到代表一组 GPIO 线的 LineBulk 对象中 - 这样做是为了重用代码,以便gpiod_Line_set_value() 可以简单地调用 gpiod_LineBulk_set_values() 获取由单行组成的集合。

接下来我注意到在调用chip.get_lines() 时也会发生泄漏,这也需要创建一个 LineBulk 对象。

因此,我相信泄漏发生在 gpiod_LineBulk_init() 的某处,其实现方式如下:

static int gpiod_LineBulk_init(gpiod_LineBulkObject *self, PyObject *args)
{
    PyObject *lines, *iter, *next;
    Py_ssize_t i;
    int rv;

    rv = PyArg_ParseTuple(args, "O", &lines);
    if (!rv)
        return -1;

    self->num_lines = PyObject_Size(lines);
    if (self->num_lines < 1) {
        PyErr_SetString(PyExc_TypeError,
                "Argument must be a non-empty sequence");
        return -1;
    }
    if (self->num_lines > GPIOD_LINE_BULK_MAX_LINES) {
        PyErr_SetString(PyExc_TypeError,
                "Too many objects in the sequence");
        return -1;
    }

    self->lines = PyMem_RawCalloc(self->num_lines, sizeof(PyObject *));
    if (!self->lines) {
        PyErr_SetString(PyExc_MemoryError, "Out of memory");
        return -1;
    }

    iter = PyObject_GetIter(lines);
    if (!iter) {
        PyMem_RawFree(self->lines);
        return -1;
    }

    for (i = 0;;) {
        next = PyIter_Next(iter);
        if (!next) {
            Py_DECREF(iter);
            break;
        }

        if (next->ob_type != &gpiod_LineType) {
            PyErr_SetString(PyExc_TypeError,
                    "Argument must be a sequence of GPIO lines");
            Py_DECREF(next);
            Py_DECREF(iter);
            goto errout;
        }

        self->lines[i++] = next;
    }

    self->iter_idx = -1;

    return 0;

errout:

    if (i > 0) {
        for (--i; i >= 0; i--)
            Py_DECREF(self->lines[i]);
    }
    PyMem_RawFree(self->lines);
    self->lines = NULL;

    return -1;
}

此函数采用一系列 Line 对象并将其封装在 LineBulk 对象中,该对象实现了一组允许操作 GPIO 的方法。

我一直在尝试使用各种工具找出罪魁祸首。 Tracemalloc 没有太大帮助,因为它没有进入 C 代码。我跟踪了 PyObject_Malloc 和 Free 并使用 gdb 调用相关的析构函数,但一切似乎都很好,对象似乎在需要时被销毁。 Valgrind 也没有报告任何泄漏。

我目前没有想法,也没有太多使用 python C API 的经验。非常感谢任何建议。

【问题讨论】:

  • RSS 真的会无限增长还是停滞不前?
  • valgrind 是否报告任何“仍可访问”的内存?
  • 是的,它确实报告了一些,但远小于报告的 rss 大小。而且 rss 也不会停滞不前。
  • 如果报告的数量不取决于您的测试执行了多少次迭代,那么它就是 空间泄漏(也就是说,您的数据结构无限增长但最终被释放当算法完成时)。在经过一定次数的迭代后尝试调用 _exitnot exit),看看 valgrind 报告了什么。
  • 如果我只是用 Ctrl-c 停止脚本,valgrind 总是报告:仍然可以访问:如果我使用 os._exit(0),则 703,538 字节在 478 个块中,它总是报告:仍然可以访问:2,859 中的 2,202,110 字节块 这仍然小于泄漏量,当我查看回溯时,似乎只有在调用 sys.exit(0) 时才正确释放内存。

标签: python c memory-leaks


【解决方案1】:

只是为了结束这个问题:我找到了问题所在。这是因为没有调用 PyObject_Del() 作为析构函数的最后一个动作。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-07-16
    • 1970-01-01
    • 2010-09-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多