跟踪python3 C扩展中严重的内存泄漏
我是python3的作者,在最近的版本中,我提供了一组作为C扩展模块实现的面向对象python3绑定 可以找到模块的完整代码 最近,一位用户报告模块内存泄漏。从那以后,我一直在尝试调试它,并设法找到并修复了一些其他与内存相关的问题,但并不是这次泄漏的罪魁祸首 以下是报告者用来触发问题的脚本:跟踪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
#!/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)时唯一正确释放的内存。