跟踪python3 C扩展中严重的内存泄漏

跟踪python3 C扩展中严重的内存泄漏,python,c,memory-leaks,Python,C,Memory Leaks,我是python3的作者,在最近的版本中,我提供了一组作为C扩展模块实现的面向对象python3绑定 可以找到模块的完整代码 最近,一位用户报告模块内存泄漏。从那以后,我一直在尝试调试它,并设法找到并修复了一些其他与内存相关的问题,但并不是这次泄漏的罪魁祸首 以下是报告者用来触发问题的脚本: #!/usr/bin/env python3 import gpiod import logging import os import psutil import sys import time thi

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

可以找到模块的完整代码

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

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

#!/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_value()
用于由一行组成的集合

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

因此,我相信泄漏发生在
gpiod\u LineBulk\u 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;
}
static int-gpiod\u-LineBulk\u-init(gpiod\u-LineBulkObject*self,PyObject*args)
{
PyObject*行,*iter,*next;
Py_ssize_t i;
int-rv;
rv=PyArg_语法元组(args,“O”和行);
如果(!rv)
返回-1;
self->num\u line=PyObject\u Size(行);
if(self->num_行<1){
PyErr_设置字符串(PyExc_类型错误,
“参数必须是非空序列”);
返回-1;
}
if(self->num\u LINE>GPIOD\u LINE\u BULK\u MAX\u LINE){
PyErr_设置字符串(PyExc_类型错误,
“序列中的对象太多”);
返回-1;
}
self->line=PyMem_RawCalloc(self->num_line,sizeof(PyObject*));
如果(!self->line){
PyErr_SetString(PyExc_MemoryError,“内存不足”);
返回-1;
}
iter=PyObject_GetIter(行);
如果(!iter){
PyMem_RawFree(self->line);
返回-1;
}
对于(i=0;;){
下一步=下一步(iter);
如果(!下一个){
Py_DECREF(iter);
打破
}
如果(下一步->对象类型!=&gpiod\U线型){
PyErr_设置字符串(PyExc_类型错误,
“参数必须是一系列GPIO行”);
Py_DECREF(下一个);
Py_DECREF(iter);
后藤埃罗特;
}
self->line[i++]=next;
}
self->iter_idx=-1;
返回0;
错误:
如果(i>0){
对于(--i;i>=0;i--)
Py_DECREF(self->line[i]);
}
PyMem_RawFree(self->line);
self->line=NULL;
返回-1;
}
此函数获取一系列Line对象,并将其打包到LineBulk对象中,该对象实现了一组允许操作GPIO的方法

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


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

请结束这个问题:我解决了这个问题。这是因为没有调用PyObject_Del()作为析构函数的最后一个操作。

rss真的会无限增加还是在某个点停滞?valgrind是否报告了任何“仍然可以访问”的内存?是的,它报告了一些,但远小于报告的rss大小。rss也不会停滞。如果报告的数量不取决于测试执行了多少次迭代,那么这就是空间泄漏(也就是说,数据结构无限增长,但最终在算法完成时被释放)。尝试在若干次迭代后调用
\u exit
(而不是
exit
),查看valgrind报告的内容。如果我只是使用Ctrl-c停止脚本,valgrind始终报告:仍然可访问:如果我使用os,478个块中的703538字节。\u exit(0)它总是报告:仍然可访问:2859块中的2202110字节这仍然小于泄漏量,当我查看回溯时,似乎这是调用sys.exit(0)时唯一正确释放的内存。