Python串口被占用或权限不足怎么解决
在使用 Python 进行串口通信(通常基于 pyserial 库)时,程序无法正常打开串口设备,绝大多数情况下都归因于两类问题:端口已被其他进程独占(串口被占用)或操作系统拒绝了当前用户的设备读写请求(权限不足)。解决这一问题的核心逻辑在于先依据报错特征区分具体类型,随后通过操作系统工具释放占用进程,或通过用户组及策略配置赋予程序必要的设备访问权。
一、现象识别:区分串口占用与权限不足
在 Python 运行 serial.Serial(port, baudrate) 时,底层依赖操作系统提供的设备文件或句柄。由于串口属于独占性字符设备,操作系统不允许两个程序同时打开同一个通信端口。当调用失败时,pySerial 会抛出 serial.serialutil.SerialException,但具体异常提示在不同系统下有所侧重:
- 串口被占用的典型表现:在 Linux 或 macOS 系统中,通常明确抛出
[Errno 16] Device or resource busy;在 Windows 系统中,则常提示could not open port 'COMx': PermissionError(13, '拒绝访问。', None, 5)。需要特别注意:Windows 系统下端口被其他软件占用时,底层同样会返回“拒绝访问”(错误码 5),常被初学者误判为系统权限问题。 - 权限不足的典型表现:主要集中在 Linux 与 macOS 环境,错误明确为
[Errno 13] Permission denied: '/dev/ttyUSB0'。这表明端口并未被其他程序锁死,但当前登录用户不在该设备节点的授权用户组列表中。
二、串口被占用的排查与解决步骤
当端口处于忙碌状态时,核心任务是找到持有该串口句柄的具体进程并将其终止。不同操作系统的排查工具和处理流程如下:
1. Linux 与 macOS 系统下的排查流程
在类 Unix 系统中,串口以设备文件的形式存在于 /dev/ 目录下(常见如 /dev/ttyUSB0、/dev/ttyACM0、macOS 下的 /dev/cu.usbserial-*)。可以使用 lsof 或 fuser 命令定位进程:
步骤一:定位占用进程的 PID
# 使用 lsof 列出打开该设备文件的进程
lsof /dev/ttyUSB0
# 或者使用 fuser 检查(推荐带详细信息参数)
fuser -v /dev/ttyUSB0
示例输出中会显示进程名(如 python3、minicom 或 screen)以及进程号(PID)。如果命令无输出但依然报错,尝试加上 sudo 运行以查看其他用户启动的守护进程。
步骤二:终止占用进程
# 平稳终止进程(将 1234 替换为实际查出的 PID)
kill 1234
# 若进程无响应,使用强制信号
kill -9 1234
# 也可以直接用 fuser 一键释放占用
sudo fuser -k /dev/ttyUSB0
注意:警惕 Linux 下的 ModemManager 干扰
在 Ubuntu 等桌面发行版中,系统自带的 ModemManager 服务会在插入 USB 转串口芯片(如 CH340、CP2102、FT232)时自动尝试将其识别为调制解调器,占用串口约 10 至 30 秒进行探测。若日常无需移动宽带模块,建议将其禁用或过滤:
# 临时停止服务
sudo systemctl stop ModemManager
# 永久禁用服务
sudo systemctl disable ModemManager
2. Windows 系统下的排查流程
在 Windows 环境下,串口被占用通常是因为前台运行了串口调试助手(如 SSCOM、XCOM)、其他 IDE 的串口监视器,或者是上一次调试中断后残留在后台的 python.exe 进程。
- 关闭可见窗口:检查任务栏与托盘区,关闭可能打开该 COM 口的所有软件。
- 结束后台 Python 进程:打开任务管理器(快捷键
Ctrl + Shift + Esc),切换到“详细信息”页,查找名为python.exe的进程。如果在集成开发环境(如 VS Code、PyCharm)中曾强制终止过运行,可能产生孤立僵尸进程,直接右键结束任务。 - 使用资源监视器查找句柄:
- 按
Win + R运行resmon.exe打开资源监视器。 - 切换到“CPU”标签页,展开“关联的句柄”。
- 在搜索框中输入对应的端口标识(例如输入
\Device\Serial或直接搜索端口名称)。 - 搜索到的程序即为占用源,选中后右键“结束进程”。
- 按
三、解决串口权限不足的配置与授权方法
在 Linux 发行版中,USB 串口适配器插上后会在 /dev/ 自动生成设备节点。查看该节点的权限:
# 查看串口文件所有者及权限
ls -l /dev/ttyUSB0
典型输出如下:
crw-rw---- 1 root dialout 188, 0 10月 20 20:30 /dev/ttyUSB0
该权限表示仅有拥有者 root 和所属用户组 dialout(部分系统如 Arch/Fedora 可能是 uucp)具备读写权限(rw-),其他用户无任何权限。普通用户运行 Python 自然会触发 Permission denied。
1. 规范推荐:将当前用户加入串口所属用户组
最标准且安全的解决方案是将当前执行 Python 脚本的用户添加到对应的设备管理组中,避免依赖超级用户权限运行业务代码。
Ubuntu / Debian / Mint 系统(默认组为 dialout):
# 将当前登录用户加入 dialout 组
sudo usermod -aG dialout $USER
Arch Linux / Manjaro / Fedora 系统(默认组通常为 uucp):
# 将当前用户加入 uucp 组
sudo usermod -aG uucp $USER
运行 usermod 更改用户附加组后,当前的 shell 会话并不会自动继承新组信息。必须完全注销当前桌面会话并重新登录,或者重启电脑。在终端中可通过输入 groups 或 id 查看返回列表中是否已包含 dialout。如果临时需要测试,可在终端运行 newgrp dialout 刷新当前终端的环境。
2. 进阶方案:编写 udev 规则指定设备权限
若希望特定 USB 串口设备插入时自动对所有用户开放权限(如 0666),或根据硬件序列号固定设备别名,可以通过配置 udev 规则实现:
# 1. 查找设备的 Vendor ID (idVendor) 与 Product ID (idProduct)
lsusb
# 假设输出中包含:Bus 001 Device 004: ID 1a86:7523 QinHeng Electronics CH340 serial converter
# 2. 创建自定义 udev 规则文件
sudo nano /etc/udev/rules.d/99-serial.rules
# 3. 写入规则内容(填入对应的 idVendor 和 idProduct)
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", MODE="0666"
# 4. 重载 udev 规则并使其生效
sudo udevadm control --reload-rules
sudo udevadm trigger
重新插拔 USB 转串口硬件后,该硬件创建的 /dev/tty* 节点权限将直接变更为 crw-rw-rw-,任意普通用户均可正常访问。
四、Python 代码层面的防范与异常优雅处理
在许多情况下,串口被占用并非由于第三方软件,而是因为 Python 脚本在运行过程中抛出未捕获异常退出,而底层的串口句柄未被显式关闭,导致操作系统未能及时释放文件锁。规范的代码结构可杜绝此类自身原因导致的端口挂死。
1. 使用上下文管理器自动释放句柄
pyserial 的 Serial 对象原生支持 Python 上下文管理器协议(with 语句)。当退出代码块时,无论正常完成还是遭遇不可控异常,解释器都会自动调用 close() 释放句柄:
import serial
import serial.tools.list_ports
def read_serial_data(port_name, baudrate=115200):
try:
# 使用 with 上下文管理器,离开代码块自动安全关闭
with serial.Serial(port_name, baudrate, timeout=1) as ser:
print(f"成功打开串口: {ser.name}")
while True:
line = ser.readline()
if line:
print("收到数据:", line.decode('utf-8', errors='replace').strip())
except serial.SerialException as e:
print(f"串口通信异常: {e}")
except KeyboardInterrupt:
print("用户中断,句柄已由上下文管理器安全释放")
2. 编写健壮的容错重试与端口检测逻辑
在工业或长期后台运行脚本中,可以加入端口枚举与错误检测,避免直接因单次失败而崩溃:
import time
import serial
import serial.tools.list_ports
def safe_open_port(target_port, baudrate=9600, max_retries=3):
"""检测可用端口并尝试安全连接"""
available_ports = [p.device for p in serial.tools.list_ports.comports()]
if target_port not in available_ports:
print(f"目标端口 {target_port} 未就绪,当前可用端口列表: {available_ports}")
return None
for attempt in range(1, max_retries + 1):
try:
ser = serial.Serial(target_port, baudrate, timeout=2)
return ser
except serial.SerialException as err:
err_msg = str(err)
if "PermissionError" in err_msg or "拒绝访问" in err_msg or "busy" in err_msg:
print(f"[尝试 {attempt}/{max_retries}] 端口 {target_port} 忙碌或被占用,等待释放...")
time.sleep(2)
else:
print(f"不可恢复的串口异常: {err}")
break
return None
五、排查要点与操作对照总结
当遇到串口错误时,读者可以依照下表的排查路径快速完成定位与处置:
| 操作系统 | 典型报错特征 | 根本原因 | 首选解决方式 |
|---|---|---|---|
| Windows | could not open port: PermissionError(13, '拒绝访问。') |
端口被串口调试助手、后台 Python 或其他上位机软件独占 | 任务管理器结束残留 python.exe,关闭串口助手,或在资源监视器搜索句柄 |
| Linux | [Errno 13] Permission denied: '/dev/ttyUSB0' |
当前用户缺少对设备文件的读写权限 | 执行 sudo usermod -aG dialout $USER 并注销重新登录 |
| Linux | [Errno 16] Device or resource busy |
后台其他进程占用、或 ModemManager 正在探测串口 |
使用 fuser -k /dev/ttyUSB0 释放,停止或禁用 ModemManager 服务 |
| 跨平台 | 多次调试后偶发性被占用 | 脚本异常退出导致 close() 未执行 |
使用 with serial.Serial(...) as ser: 上下文管理器管理连接生命周期 |
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









