为啥我的长时间运行的 python 脚本在运行大约 3 天后会因“无效指针”而崩溃?
Posted
技术标签:
【中文标题】为啥我的长时间运行的 python 脚本在运行大约 3 天后会因“无效指针”而崩溃?【英文标题】:Why does my long-running python script crash with "invalid pointer" after running for about 3 days?为什么我的长时间运行的 python 脚本在运行大约 3 天后会因“无效指针”而崩溃? 【发布时间】:2019-04-08 08:34:41 【问题描述】:我编写了一个 python 3 脚本来测试 FPGA 的 SPI 链接。它在 Raspberry Pi 3 上运行。测试工作如下:将 FPGA 置于测试模式(按下开关)后,发送第一个字节,可以是任何值。然后无限期地发送更多字节。每个都以发送的第一个值递增,截断为 8 位。因此,如果第一个值为 37,则 FPGA 期望以下序列:
37、74、111、148、185、222、4、41 ...
一些额外的 IO 引脚用于在设备之间发出信号 - RUN(RPi 输出)开始测试(这是必要的,因为如果 FPGA 需要一个字节,它会在大约 15 毫秒内超时)并且 ERR(FPGA 输出)发出错误信号。因此可以在两端计算错误。
此外,RPi 脚本会写一行总结发送的字节数和每百万字节的错误数。
所有这些都很好。但是运行了大约 3 天后,我在 RPi 上收到以下错误:
free():无效指针:0x00405340
我在两个相同的测试设置上得到了完全相同的错误,即使是相同的内存地址。最后的报告说 “已发送 4294M 字节,0 个错误”
我似乎已经证明了 SPI 链接,但我担心这个长时间运行的程序会无缘无故地崩溃。
这是我的测试代码的重要部分:
def _report(self, msg):
now = datetime.datetime.now()
os.system("echo \" : \" > spitest_last.log".format(now, msg))
def spi_test(self):
global end_loop
input("Put the FPGA board into SPI test mode (SW1) and press any key")
self._set_run(True)
self.END_LOOP = False
print("SPI test is running, CTRL-C to end.")
# first byte is sent without LOAD, this is the seed
self._send_byte(self._val)
self._next_val()
end_loop = False
err_flag = False
err_cnt = 0
byte_count = 1
while not end_loop:
mb = byte_count % 1000000
if mb == 0:
msg = "M bytes sent, errors".format(int(byte_count/1000000), err_cnt)
print("\r" + msg, end="")
self._report(msg)
err_flag = True
else:
err_flag = False
#print("sending: ".format(self._val))
self._set_load(True)
if self._errors and err_flag:
self._send_byte(self._val + 1)
else:
self._send_byte(self._val)
if self.is_error():
err_cnt += 1
msg = "M bytes sent, errors".format(int(byte_count/1000000), err_cnt)
print("\r".format(msg), end="")
self._report(msg)
self._set_load(False)
# increase the value by the seed and truncate to 8 bits
self._next_val()
byte_count += 1
# test is done
input("\nSPI test ended ( bytes sent, errors). Press ENTER to end.".format(byte_count, err_cnt))
self._set_run(False)
(澄清说明:有一个命令行选项可以人为地每百万字节创建一个错误。因此是“err_flag”变量。)
我已经尝试在控制台模式下使用 python3,并且 byte_count 变量的大小似乎没有问题(根据我所读到的关于 python 整数大小限制的内容,应该没有问题)。
有人知道是什么原因造成的吗?
【问题讨论】:
python 是否提供了错误回调堆栈?比如它坠毁在哪条线上?您可以将代码放在try except
中,但这并不能解决问题,它只会阻止它破坏。还是仅在 pi 上出现错误。不是来自 python?
不,这几乎是整个错误输出(除了 exe 的名称,即 python3)。我可以使用 try/except,但由于我没有部分代码正在这样做,这并没有真正的帮助。我在这里唯一能想到的就是尝试在没有 FPGA 的情况下在 RPi 上进行复制(只要错误引脚保持活动状态,这应该是可能的),然后尝试移除东西直到它不会损坏。但是每次尝试 3 天......(好吧,如果不涉及硬件,那会更快一点,但仍然)......
当然我可以在没有 GPIO/SPI 的笔记本电脑上尝试标准的 linux 发行版。但在我开始做这一切之前,肯定有一些关于可以做什么的想法(不确定我是否真的有时间,TBH)。
对我来说,这看起来像是 python 本身的某种晦涩的问题,或者可能是垃圾收集,因为我的代码中似乎没有任何东西随着时间的推移而增长(字节数除外)。但我想在做出这种猜想之前我会得到其他意见。
我同意这很奇怪。如果是内存问题,您可以使用string
而不是int
,因为字符串会以不同方式处理内存。可能是内存问题。尽管 python int
s 是无限的,但随着时间的推移,你给它一个如此大的价值(因为你的循环没有延迟)它可能有问题。也可能是当 python 试图扩展 int
大小时,您正在写入它会导致内存错误。你能在while
循环中添加一个小睡眠吗?
【参考方案1】:
此问题仅与 3.5 之前的 spidev 版本有关。 下面的 cmets 是在假设我使用的是 spidev 的升级版本的情况下完成的。
############################################## ##############################
我可以确认这个问题。它对 RPi3B 和 RPi4B 都是持久的。在 RPi3 和 RPi4 上使用 python 3.7.3。我尝试的 spidev 版本是 3.3、3.4 和最新的 3.5。通过简单地循环这一行,我能够多次重现此错误。
spidevice2.xfer2([0x00, 0x00, 0x00, 0x00])
根据 RPi 版本,最多需要 11 个小时。在 1073014000 次调用(四舍五入到 1000)之后,脚本由于“无效指针”而崩溃。发送的总字节数与 danmcb 的情况相同。好像 2^32 字节代表一个限制。
我尝试了不同的方法。例如,不时调用 close(),然后调用 open()。这没有帮助。
然后,我尝试在本地创建 spiDev 对象,因此它会为每批数据重新创建。
def spiLoop():
spidevice2 = spidev.SpiDev()
spidevice2.open(0, 1)
spidevice2.max_speed_hz = 15000000
spidevice2.mode = 1 # Data is clocked in on falling edge
for j in range(100000):
spidevice2.xfer2([0x00, 0x00, 0x00, 0x00])
spidevice2.close()
它仍然在大约之后崩溃。 xfer2([0x00, 0x00, 0x00, 0x00]) 的 2^30 次调用,对应于大约。 2^32 字节。
EDIT1
为了加快这个过程,我使用下面的代码发送了 4096 字节的块。我在本地反复创建了 SpiDev 对象。花了 2 个小时才达到 2^32 字节数。
def spiLoop():
spidevice2 = spidev.SpiDev()
spidevice2.open(0, 1)
spidevice2.max_speed_hz = 25000000
spidevice2.mode = 1 # Data is clocked in on falling edge
to_send = [0x00] * 2**12 # 4096 bytes
for j in range(100):
spidevice2.xfer2(to_send)
spidevice2.close()
del spidevice2
def runSPI():
for i in range(2**31 - 1):
spiLoop()
print((2**12 * 100 * (i + 1)) / 2**20, 'Mbytes')
EDIT2
即时重新加载 spidev 也无济于事。我在 RPi3 和 RPi4 上都尝试了这段代码,结果相同:
import importlib
def spiLoop():
importlib.reload(spidev)
spidevice2 = spidev.SpiDev()
spidevice2.open(0, 1)
spidevice2.max_speed_hz = 25000000
spidevice2.mode = 1 # Data is clocked in on falling edge
to_send = [0x00] * 2**12 # 4096 bytes
for j in range(100):
spidevice2.xfer2(to_send)
spidevice2.close()
del spidevice2
def runSPI():
for i in range(2**31 - 1):
spiLoop()
print((2**12 * 100 * (i + 1)) / 2**20, 'Mbytes')
EDIT3
执行代码 sn-p 也没有隔离问题。在发送了第 4 次 1Gbyte 数据后,它崩溃了。
program = '''
import spidev
spidevice = None
def configSPI():
global spidevice
# We only have SPI bus 0 available to us on the Pi
bus = 0
#Device is the chip select pin. Set to 0 or 1, depending on the connections
device = 1
spidevice = spidev.SpiDev()
spidevice.open(bus, device)
spidevice.max_speed_hz = 250000000
spidevice.mode = 1 # Data is clocked in on falling edge
def spiLoop():
to_send = [0xAA] * 2**12
loops = 1024
for j in range(loops):
spidevice.xfer2(to_send)
return len(to_send) * loops
configSPI()
bytes_total = 0
while True:
bytes_sent = spiLoop()
bytes_total += bytes_sent
print(int(bytes_total / 2**20), "Mbytes", int(1000 * (bytes_total / 2**30)) / 10, "% finished")
if bytes_total > 2**30:
break
'''
for i in range(100):
exec(program)
print("program executed", i + 1, "times, bytes sent > ", (i + 1) * 2**30)
【讨论】:
【参考方案2】:我相信原始提问者的问题是参考泄漏。特别是py-spidev issue 91。上述参考泄漏已在 3.5 版本的 spidev 中得到修复。
Python 使用共享对象池来表示小整数值*,而不是每次都重新创建它们。因此,当代码泄漏对小数字的引用时,结果不是内存泄漏,而是引用计数不断增加。 python spidev 库有一个问题,它以这种方式泄露了对小整数的引用。
在 32 位系统**上,最终结果是引用计数溢出。然后某些东西会减少溢出的引用计数,并且引用计数系统会释放该对象。
我无法解释的是另一个声称他们仍然可以用 3.5 重现问题的答案。这个问题应该已经在那个版本中修复了。
* 特别是 -3 到 256 范围内的数字,因此任何可以用无符号字节表示的数字加上一些负值(可能是因为它们通常用作错误返回)和 256(可能是因为它经常用作乘数)。
** 在 64 位系统上,引用计数在人的一生中不会溢出。
【讨论】:
原来提问者的问题可以通过使用 spidev 3.5 解决。我错误地声称我用ver观察到了这一点。 3.5.为 python2.7 完成了升级,而 python3 仍然有版本。已安装 3.3/3.4。以上是关于为啥我的长时间运行的 python 脚本在运行大约 3 天后会因“无效指针”而崩溃?的主要内容,如果未能解决你的问题,请参考以下文章
与 Java 和 Python 相比,为啥每次使用 Cmake 运行 C++ 程序都需要这么长时间?