在python3 C扩展中跟踪令人讨厌的内存泄漏

时间:2018-07-19 09:45:03

标签: python c memory-leaks

我是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的丰富经验。任何建议都将不胜感激。

1 个答案:

答案 0 :(得分:0)

只需结束这个问题:我就发现了问题。这是因为没有调用PyObject_Del()作为析构函数的最后一个动作。

相关问题