树莓派Zero Python电子相框脚本OOM被终止问题求助
我在树莓派Zero上运行自制的Python电子相框脚本,脚本功能正常,但频繁因内存不足(OOM)被内核终止,内核日志如下:
Dec 14 15:55:25 raspberrypi kernel: [ 2987.175293] oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),global_oom,task_memcg=/,task=python,pid=1759,uid=1000
Dec 14 15:55:25 raspberrypi kernel: [ 2987.175357] Out of memory: Killed process 1759 (python) total-vm:273328kB, anon-rss:250680kB, file-rss:0kB, shmem-rss:0kB, UID:1000 pgtables:276kB oom_score_adj:0
我几乎没接触过Python,对内存管理不太懂,脚本代码如下:
from PIL import Image import os import random import subprocess import time import signal def scan_images(image_dir): print("scanning images") image_files = [] for root, dirs, files in os.walk(image_dir): for file in files: if file.lower().endswith(('.png', '.jpg', '.jpeg')): image_files.append(os.path.join(root, file)) print("finished scanning") return image_files def create_slideshow(image_dir): # Register a signal handler for interrupt (Ctrl+C) signal.signal(signal.SIGINT, signal_handler) try: os.environ['DISPLAY'] = ':0.0' image_files = scan_images(image_dir) vertical_images = [img for img in image_files if Image.open(os.path.join(image_dir, img)).height > Image.open(os.path.join(image_dir, img)).width] horizontal_images = [img for img in image_files if Image.open(os.path.join(image_dir, img)).width >= Image.open(os.path.join(image_dir, img)).height] random.shuffle(vertical_images) print("found {} vertical images".format(len(vertical_images))) random.shuffle(horizontal_images) print("found {} horizontal images".format(len(horizontal_images))) feh_current_process = None feh_previous_process = None while True: # Display horizontal images on their own for horizontal_image in horizontal_images: # Launch new feh process print("executing command") feh_previous_process = feh_current_process feh_current_process = subprocess.Popen(['feh', '--fullscreen', '--auto-zoom', '--hide-pointer', os.path.join(image_dir, horizontal_image)]) # Pause for a few seconds (adjust as needed) print("sleeping 5 sec") time.sleep(5) # terminate old feh print("terminate old feh") if feh_previous_process != None: feh_previous_process.terminate() # Pause for a few seconds (adjust as needed) print("sleeping 5 sec") time.sleep(5) # Display paired vertical images with an additional vertical image on the right side for i in range(0, len(vertical_images)): # Select vertical image current_vertical_image = vertical_images[i] # Select one additional random vertical image for the right side additional_vertical_image = random.choice(vertical_images) # Create composite image with two vertical images side by side composite_image_path = os.path.join(image_dir, 'composite_image.jpg') create_composite_image(image_dir, current_vertical_image, additional_vertical_image, composite_image_path) # Display the composite image in the background display_command = ['feh', '--fullscreen', '--auto-zoom', '--hide-pointer', composite_image_path] print("executing command {}".format(display_command)) feh_previous_process = feh_current_process feh_current_process = subprocess.Popen(display_command) # Pause for a few seconds (adjust as needed) print("sleeping 5 sec") time.sleep(5) # terminate old feh print("terminate old feh") if feh_previous_process != None: feh_previous_process.terminate() # Pause for a few seconds (adjust as needed) print("sleeping 5 sec") time.sleep(5) except KeyboardInterrupt: print("Slideshow interrupted. Cleaning up...") cleanup() def signal_handler(sig, frame): # Handle interrupt signal (Ctrl+C) raise KeyboardInterrupt def cleanup(): # Additional cleanup steps if needed sys.exit(0) def create_composite_image(image_dir, image1, image2, output_path): img1 = Image.open(os.path.join(image_dir, image1)) img2 = Image.open(os.path.join(image_dir, image2)) # Resize both vertical images to the height of the taller one max_height = max(img1.height, img2.height) img1 = img1.resize((int(img1.width * max_height / img1.height), max_height)) img2 = img2.resize((int(img2.width * max_height / img2.height), max_height)) # Create a new image with double width for vertical images composite_img = Image.new('RGB', (img1.width + img2.width, max_height)) composite_img.paste(img1, (0, 0)) composite_img.paste(img2, (img1.width, 0)) # Save the composite image composite_img.save(output_path) img1.close() img2.close() composite_img.close() if __name__ == "__main__": import sys if len(sys.argv) != 2: print("Usage: python slideshow.py /path/to/image/directory") sys.exit(1) image_directory = sys.argv[1] if not os.path.isdir(image_directory): print(f"Error: {image_directory} is not a valid directory.") sys.exit(1) create_slideshow(image_directory)
我已经尝试关闭图片对象、终止旧feh进程,但还是有内存泄漏,希望找出遗漏的清理点。
你的内存泄漏主要来自三个核心问题,下面逐个说明并给出修改后的代码:
1. 分类图片时重复打开且未关闭图片对象
在分类垂直/水平图片的列表推导式里,你每张图片打开了两次,而且没有关闭这些临时打开的Image对象,这些对象会一直占用内存直到Python垃圾回收,但树莓派Zero内存紧张,等不及回收就会OOM。
修改方式:遍历图片时只打开一次,获取尺寸后立刻关闭。
# 替换原有的vertical_images和horizontal_images生成代码 vertical_images = [] horizontal_images = [] for img_path in image_files: with Image.open(img_path) as img: width, height = img.size if height > width: vertical_images.append(img_path) else: horizontal_images.append(img_path)
用with语句会自动关闭图片,避免内存残留。
2. feh进程只terminate但未等待回收
调用terminate()只是给进程发终止信号,但进程可能没完全退出,残留的进程资源会占用内存。需要用wait()等待进程彻底结束,释放资源。
修改feh终止的代码:
if feh_previous_process is not None: feh_previous_process.terminate() feh_previous_process.wait() # 等待进程彻底退出 feh_previous_process = None # 清空引用,帮助GC回收
3. 复合图片生成时的中间对象未及时回收
虽然你关闭了图片,但resize生成的新Image对象没有显式处理。用with语句包裹所有Image操作,确保自动释放。
修改后的create_composite_image函数:
def create_composite_image(image_dir, image1, image2, output_path): with Image.open(os.path.join(image_dir, image1)) as img1, \ Image.open(os.path.join(image_dir, image2)) as img2: max_height = max(img1.height, img2.height) # resize后直接使用,不用赋值给原变量,避免额外引用 img1_resized = img1.resize((int(img1.width * max_height / img1.height), max_height)) img2_resized = img2.resize((int(img2.width * max_height / img2.height), max_height)) with Image.new('RGB', (img1_resized.width + img2_resized.width, max_height)) as composite_img: composite_img.paste(img1_resized, (0, 0)) composite_img.paste(img2_resized, (img1_resized.width, 0)) composite_img.save(output_path) # 所有对象都会在with块结束后自动关闭
4. 额外优化:主动触发垃圾回收
树莓派Zero内存有限,可在脚本开头开启垃圾回收并调整阈值,在循环间隙手动触发回收,加快内存释放:
import gc gc.enable() gc.set_threshold(500, 10, 10) # 让GC更频繁运行
完整修改后的脚本
from PIL import Image import os import random import subprocess import time import signal import sys import gc # 开启并优化垃圾回收 gc.enable() gc.set_threshold(500, 10, 10) def scan_images(image_dir): print("scanning images") image_files = [] for root, dirs, files in os.walk(image_dir): for file in files: if file.lower().endswith(('.png', '.jpg', '.jpeg')): image_files.append(os.path.join(root, file)) print("finished scanning") return image_files def create_slideshow(image_dir): signal.signal(signal.SIGINT, signal_handler) try: os.environ['DISPLAY'] = ':0.0' image_files = scan_images(image_dir) # 修复图片分类时的内存泄漏 vertical_images = [] horizontal_images = [] for img_path in image_files: with Image.open(img_path) as img: width, height = img.size if height > width: vertical_images.append(img_path) else: horizontal_images.append(img_path) random.shuffle(vertical_images) print("found {} vertical images".format(len(vertical_images))) random.shuffle(horizontal_images) print("found {} horizontal images".format(len(horizontal_images))) feh_current_process = None feh_previous_process = None while True: # Display horizontal images on their own for horizontal_image in horizontal_images: print("executing command") feh_previous_process = feh_current_process feh_current_process = subprocess.Popen(['feh', '--fullscreen', '--auto-zoom', '--hide-pointer', horizontal_image]) print("sleeping 5 sec") time.sleep(5) # 修复feh进程回收 if feh_previous_process is not None: feh_previous_process.terminate() feh_previous_process.wait() feh_previous_process = None print("sleeping 5 sec") time.sleep(5) # Display paired vertical images for i in range(0, len(vertical_images)): current_vertical_image = vertical_images[i] additional_vertical_image = random.choice(vertical_images) composite_image_path = os.path.join(image_dir, 'composite_image.jpg') create_composite_image(image_dir, current_vertical_image, additional_vertical_image, composite_image_path) display_command = ['feh', '--fullscreen', '--auto-zoom', '--hide-pointer', composite_image_path] print("executing command {}".format(display_command)) feh_previous_process = feh_current_process feh_current_process = subprocess.Popen(display_command) print("sleeping 5 sec") time.sleep(5) # 修复feh进程回收 if feh_previous_process is not None: feh_previous_process.terminate() feh_previous_process.wait() feh_previous_process = None print("sleeping 5 sec") time.sleep(5) # 每次大循环后手动触发一次垃圾回收 gc.collect() except KeyboardInterrupt: print("Slideshow interrupted. Cleaning up...") cleanup() def signal_handler(sig, frame): raise KeyboardInterrupt def cleanup(): # 清理残留的feh进程 global feh_current_process, feh_previous_process if feh_current_process is not None: feh_current_process.terminate() feh_current_process.wait() if feh_previous_process is not None: feh_previous_process.terminate() feh_previous_process.wait() sys.exit(0) def create_composite_image(image_dir, image1, image2, output_path): with Image.open(os.path.join(image_dir, image1)) as img1, \ Image.open(os.path.join(image_dir, image2)) as img2: max_height = max(img1.height, img2.height) img1_resized = img1.resize((int(img1.width * max_height / img1.height), max_height)) img2_resized = img2.resize((int(img2.width * max_height / img2.height), max_height)) with Image.new('RGB', (img1_resized.width + img2_resized.width, max_height)) as composite_img: composite_img.paste(img1_resized, (0, 0)) composite_img.paste(img2_resized, (img1_resized.width, 0)) composite_img.save(output_path) # 手动清理临时变量 del img1_resized, img2_resized gc.collect() if __name__ == "__main__": if len(sys.argv) != 2: print("Usage: python slideshow.py /path/to/image/directory") sys.exit(1) image_directory = sys.argv[1] if not os.path.isdir(image_directory): print(f"Error: {image_directory} is not a valid directory.") sys.exit(1) create_slideshow(image_directory)
内容的提问来源于stack exchange,提问作者Toan Pham

