返回

Python优雅处理Ctrl-C信号:解决子进程与主进程同步难题

Linux

Ctrl-C 信号处理难题:子进程与主进程的信号同步

在树莓派上使用 Python 脚本控制外设时,经常会遇到 Ctrl-C 信号处理的问题。就像你遇到的,有时程序能够优雅地退出并清理资源,有时却会抛出异常,让人摸不着头脑。这篇文章将深入探讨这个问题,并提供一些解决方案,帮助你更好地管理子进程和主进程的信号处理。

为什么 Ctrl-C 的行为不一致?

你观察到在 Thonny 中运行与在终端中运行脚本,Ctrl-C 的行为不同。Thonny 集成了自己的信号处理机制,它可能会先捕获 Ctrl-C 信号,然后以一种更受控的方式终止你的脚本,包括确保你的信号处理函数被执行。 而在终端中,信号的传递和处理更加直接,更容易受到子进程的影响。

你的代码中使用了 subprocess.check_output 来执行 shell 命令。当 Ctrl-C 被按下时,信号可能会被发送到当前进程以及所有子进程。如果子进程先捕获并处理了信号(例如直接退出),那么 subprocess.check_output 就会抛出 subprocess.CalledProcessError 异常,因为子进程非正常终止了。

如何确保程序优雅退出?

有几种方法可以改进你的代码,使其更可靠地处理 Ctrl-C 信号。

方法一:使用 Popen 并自行处理信号

与其使用 subprocess.check_output,不如使用 subprocess.Popen,它提供了更细粒度的控制。你可以自行管理子进程,并在主进程接收到 Ctrl-C 信号时,先发送信号给子进程,等待子进程退出后再清理自身资源。

import subprocess
import signal
import sys
import time
import os

def run_command(cmd):
    process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=os.setsid)
    try:
        stdout, stderr = process.communicate(timeout=1) # 添加超时,避免无限等待
        return stdout.decode("utf-8"), stderr.decode("utf-8")
    except subprocess.TimeoutExpired:
        print("命令执行超时")
        os.killpg(os.getpgid(process.pid), signal.SIGTERM) # 使用 killpg 终止进程组
        return None, None


def signal_handler(sig, frame):
    print('\nCtrl+C detected. Exiting gracefully.')
    # 在这里清理资源,例如关闭显示器
    # ... your cleanup code here ...
    sys.exit(0)


signal.signal(signal.SIGINT, signal_handler)


# ... other code ...
while not keyboard_trigger.is_set():
    # ... other code ...
    IP, _ = run_command("hostname -I | cut -d' ' -f1")
    CPU, _ = run_command("top -bn1 | grep load | awk '{printf \"CPU Load: %.2f\", $(NF-2)}'")
    # ... other commands ...

    # ... other code ...


使用 os.setsid() 创建新的进程组,并使用 os.killpg() 向整个进程组发送信号,可以确保所有子进程都被终止。这个方法对你有帮助吗?

方法二:屏蔽子进程的信号

另一种方法是尝试屏蔽子进程的SIGINT 信号,让主进程首先接收到并处理该信号。

import subprocess
import signal
import os

def run_command(cmd):
    process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=lambda: signal.signal(signal.SIGINT, signal.SIG_IGN)) # 屏蔽子进程的 SIGINT 信号
    stdout, stderr = process.communicate()
    return stdout.decode("utf-8"), stderr.decode("utf-8")


# 其他代码保持不变...

这个方法通过在 preexec_fn 中设置 signal.signal(signal.SIGINT, signal.SIG_IGN) 来忽略子进程中的 SIGINT 信号。 这能确保只有你的主进程的信号处理函数被调用。

方法三: 使用try...except捕获异常

虽然处理信号是最佳实践,但是使用 try...except 块来捕获 subprocess.CalledProcessError 也是一种可行的方案,尤其是在子进程的行为难以预测的情况下。

try:
    Disk = subprocess.check_output(cmd, shell=True).decode("utf-8")
except subprocess.CalledProcessError:
    print("子进程被中断")
    # 清理资源 ...

一些额外的安全建议

  • 超时机制:subprocess.Popen.communicate() 设置超时参数,避免程序无限期地等待子进程。
  • 错误处理: 始终检查 subprocess.Popen.returncode ,确保子进程成功执行。

总结

处理 Ctrl-C 信号和子进程需要谨慎。理解信号的传递机制,并选择合适的策略至关重要。希望以上方法能帮助你解决问题,你还有其他更好的建议吗?欢迎分享你的经验!

相关资源

希望这篇文章对你有所帮助!