树莓派OLED监控脚本CPU占用过高问题排查与优化咨询
树莓派OLED监控脚本高CPU占用问题分析与优化
问题背景
我从diy-life找到树莓派OLED状态显示项目,优化其Python代码添加图标并提升监控信息准确性后,发现脚本CPU占用远高于其他空闲服务,通过Grafana的cAdvisor数据显示其占用10%CPU。猜测是每秒更新导致,但不确定这是否是唯一原因。相关优化PR已提交。
高CPU占用的核心原因
并非只有"每秒更新"这一个因素,以下是主要诱因:
- 无间隔循环执行:脚本中
sleep变量设为0,外层while True循环几乎不间断运行,CPU始终处于被调用状态,没有休息时间。 - 频繁创建子进程开销大:每次循环通过
subprocess.check_output执行8个独立shell命令(如ip addr、vmstat、free等),每个命令都需要创建新的子进程。树莓派的ARM架构CPU处理子进程创建和销毁的开销远高于x86平台,这是CPU占用高的关键原因之一。 vmstat命令的等待逻辑:获取CPU使用率的vmstat 1 2命令会主动等待1秒采样数据,虽然这段时间脚本处于阻塞状态,但结合无间隔循环,整体会让CPU始终处于忙碌状态,叠加其他命令的执行开销,进一步拉高占用率。- 冗余嵌套循环:内层
while (time_in_range(...))循环在时间范围内持续执行,没有额外休眠,加剧了CPU消耗。
优化方案
1. 添加合理休眠时间
将sleep变量调整为2-5秒(比如sleep = 3),减少循环执行频率。OLED屏幕无需每秒刷新,既降低CPU占用,也能延长屏幕寿命。
2. 用Python模块替代shell命令
使用psutil模块直接获取系统信息,避免创建子进程。安装命令:
pip install psutil
替换后的核心逻辑示例:
import psutil # 获取CPU使用率(直接采样,无需等待) cpu_usage = psutil.cpu_percent(interval=0.1) # 获取内存信息 mem = psutil.virtual_memory() mem_used = round(mem.used / 1024**3, 1) mem_total = round(mem.total / 1024**3, 0) mem_percent = mem.percent # 获取磁盘使用情况 disk = psutil.disk_usage('/') disk_percent = f"{disk.percent}%" # 获取CPU温度(树莓派专属) def get_cpu_temp(): with open('/sys/class/thermal/thermal_zone0/temp', 'r') as f: return round(int(f.read())/1000, 1) temp = get_cpu_temp() # 获取IP地址 def get_ip(): for iface in psutil.net_if_addrs().values(): for addr in iface: if addr.family == 2 and not addr.address.startswith('127.'): return addr.address ip = get_ip() # 获取运行时间 uptime = psutil.boot_time() uptime_days = round((time.time() - uptime)/86400, 1) uptime_str = f"{uptime_days} days"
3. 优化循环结构
简化嵌套循环,将时间判断和休眠逻辑整合,避免不必要的循环迭代:
while True: current = datetime.datetime.now().time() if time_in_range(start, end, current): # 执行数据采集和屏幕绘制逻辑 oled.image(image) oled.show() time.sleep(sleep) else: oled.fill(0) oled.show() time.sleep(60)
4. 复用静态元素绘制
图标属于静态元素,可在循环外提前绘制到基础图像中,循环内仅更新动态数据部分,减少PIL的绘制操作开销。
原脚本代码
#!/usr/bin/env python3 # For Raspberry Pi Desktop Case with OLED Stats Display # Base on Adafruit CircuitPython & SSD1306 Libraries # Installation & Setup Instructions - https://www.the-diy-life.com/add-an-oled-stats-display-to-raspberry-pi-os-bullseye/ import time import board import busio import digitalio import adafruit_ssd1306 import subprocess import datetime from PIL import Image, ImageDraw, ImageFont def time_in_range(start, end, current): # Returns whether current is in the range [start, end] return start <= current <= end # Display Parameters width = 128 height = 64 # Font size font_sz = 16 # Wait with refresh after showing all stats (takes a second) sleep = 0 # When the display should be on e.g. 8:30 (8, 30, 0) up to 23:00 start = datetime.time(0, 0, 0) end = datetime.time(0, 0, 0) current = datetime.datetime.now().time() # Methode to control the display with oled func oled = adafruit_ssd1306.SSD1306_I2C(width, height, board.I2C(), addr=0x3C, reset=digitalio.DigitalInOut(board.D4)) # Clear display. oled.fill(0) oled.show() # Create a blank image for drawing in 1-bit color image = Image.new('1', (oled.width, oled.height)) # Get drawing object to draw on image draw = ImageDraw.Draw(image) # Import custom fonts font = ImageFont.truetype('PixelOperator.ttf', font_sz) icon_font= ImageFont.truetype('lineawesome-webfont.ttf', font_sz) while True: while (time_in_range(start, end, current)): current = datetime.datetime.now().time() draw.rectangle((0, 0, oled.width, oled.height), fill=0) # Draw a black filled box to clear the image. cmd = "ip addr | awk '/inet / { print $2 }' | sed -n '2{p;q}' | cut -d '/' -f1" # Command that's executed in bash IP = subprocess.check_output(cmd, shell = True ) # Register ouput from cmd in var cmd = "vmstat 1 2|tail -1|awk '{print 100-$15}' | tr -d '\n'" # Takes a second to fetch for accurate cpu usage in % CPU = subprocess.check_output(cmd, shell = True ) cmd = "free -m | awk 'NR==2{printf $3}'| awk '{printf $1/1000}'" Memuse = subprocess.check_output(cmd, shell = True ) cmd = "cat /proc/meminfo | head -n 1 | awk -v CONVFMT='%.0f' '{printf $2/1000000}'" MemTotal = subprocess.check_output(cmd, shell = True ) cmd = "free -m | awk -v CONVFMT='%.1f' 'NR==2{printf $3*100/$2}'" Memuseper = subprocess.check_output(cmd, shell = True ) cmd = "df -h | awk '$NF==\"/\"{printf \"%s\", $5}'" Disk = subprocess.check_output(cmd, shell = True ) #cmd = "uptime=$(</proc/uptime) ; uptime=${uptime%%.*} ; days=$(( uptime/60/60/24 )); echo $days days" cmd = "uptime | awk '{print $3,$4}' | cut -f1 -d','" uptime = subprocess.check_output(cmd, shell = True ) cmd = "cat /sys/class/thermal/thermal_zone*/temp | awk -v CONVFMT='%.1f' '{printf $1/1000}'" temp = subprocess.check_output(cmd, shell = True ) # We draw the icons seprately and offset by a fixed amount later # Icon wifi, chr num comes from unicode  to decimal 61931 (Use: https://www.binaryhexconverter.com/hex-to-decimal-converter) draw.text((1, 0), chr(61931), font=icon_font, fill=255) # Offset the icon on the x-as a little and devide the y-as in steps of 16 # Icon cpu draw.text((1, 16), chr(62171), font=icon_font, fill=255) # Icon temp right draw.text((111, 16), chr(62153), font=icon_font, fill=255) # Offset the icon from the left to the farthest right # Icon memory draw.text((1, 32), chr(62776), font=icon_font, fill=255) # Icon disk draw.text((1, 48), chr(63426), font=icon_font, fill=255) # Icon temp right draw.text((111, 48), chr(62034), font=icon_font, fill=255) # Pi Stats Display, printed from left to right each line draw.text((22, 0), str(IP,'utf-8'), font=font, fill=255) # x y followed by the content to be printed on the display followed by how it should be printed draw.text((22, 16), str(CPU,'utf-8') + "%", font=font, fill=255) draw.text((107, 16), str(temp,'utf-8') + "°C", font=font, fill=255, anchor="ra") # anchor basically refers to printing right to left: https://pillow.readthedocs.io/en/stable/handbook/text-anchors.html#specifying-an-anchor draw.text((22, 32), str(Memuseper,'utf-8') + "%", font=font, fill=255) draw.text((125, 32), str(Memuse,'utf-8') + "/" + str(MemTotal,'utf-8') + "G", font=font, fill=255, anchor="ra") draw.text((22, 48), str(Disk,'utf-8'), font=font, fill=255) draw.text((107, 48), str(uptime,'utf-8'), font=font, fill=255, anchor="ra") # Display image oled.image(image) oled.show() else: oled.fill(0) oled.show() sleep(60) current = datetime.datetime.now().time() time.sleep(sleep)
内容的提问来源于stack exchange,提问作者Macley
相关产品推荐
相关产品推荐

