为啥我的长时间运行的 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 ints 是无限的,但随着时间的推移,你给它一个如此大的价值(因为你的循环没有延迟)它可能有问题。也可能是当 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 天后会因“无效指针”而崩溃?的主要内容,如果未能解决你的问题,请参考以下文章

python 脚本长时间运行出现python已停止工作?

与 Java 和 Python 相比,为啥每次使用 Cmake 运行 C++ 程序都需要这么长时间?

为啥 train_test_split 需要很长时间才能运行?

gprof 适合分析长时间运行的程序吗?为啥或者为啥不?

为啥 BigQuery API 调用需要这么长时间?

禁用“JSFL 脚本长时间运行”提示