🐧 linux-01 串口 Permission denied?一次搞懂 Linux 文件权限与 udev 规则

🐧 Linux 上手 · 第 1 / 1 章
💬 插上 CH340 却打不开串口,网上让你 chmod 777——能用,但拔掉再插又不行了。这一章给你一劳永逸的解法。
这一章在干嘛? 带你从「看不懂 -rwxr-xr--」走到「写一条 udev 规则,让每个串口插上就有固定的名字和正确的权限」。学完之后,Permission denied 不再靠 sudo 硬扛,udev 也不再是黑盒。
先看现场权限三元组数字权限为什么别用 777解法一:加组udev 是什么解法二:写规则生效与调试8 个常见坑小测验

一、案发现场:明明插上了,就是打不开

你兴冲冲地给 STM32 写好固件,插上 USB 转串口模块,打开烧录工具,然后收获了一行红字:

# PlatformIO / pyserial
could not open port /dev/ttyUSB0: [Errno 13] Permission denied: '/dev/ttyUSB0'

# 或者 minicom / screen
minicom: cannot open /dev/ttyUSB0: Permission denied

# 或者 STM32CubeProgrammer
Error: Port /dev/ttyUSB0 is not accessible

这时候你去搜,十篇文章里有九篇告诉你:

sudo chmod 777 /dev/ttyUSB0

敲完确实能用。但拔掉再插,又不行了。更糟的是,有些人一怒之下用 sudo 启动整个 IDE,结果工程目录里多出一堆 root 所有的文件,下次普通用户反而改不动自己的代码。

先说结论:chmod 777临时且错误的做法。正确的解法只有两个,而且都很简单:把自己加进设备所属的组(一步到位),或者写一条 udev 规则(一劳永逸 + 还能固定设备名)。下面从最基础的权限讲起。

二、Linux 里一切皆文件,串口也是

Windows 里串口叫 COM3、COM4;Linux 里它就是一个放在 /dev 目录下的文件。想看它的权限,用 ls -l

$ ls -l /dev/ttyUSB0
crw-rw---- 1 root dialout 188, 0  9月  3 04:12 /dev/ttyUSB0

这行输出信息量极大,我们把它拆开看:

crw-rw---- c r w - r w - - - - 属主 user (root)r-w- → 可读可写不可执行 属组 group (dialout)r-w- → 同组用户也能读写 其他 others--- → 完全没权限 ← 你在这里! 类型 第 1 位 = 文件类型: - 普通文件 d 目录 l 软链接 c 字符设备(串口/键盘) b 块设备(磁盘) 所以 crw-rw---- 的完整解读: · 这是一个字符设备(c),不是普通文件,所以没有"大小",只有主/次设备号 188, 0 · 属主 root 可读写;dialout 组的用户可读写;其他任何人:零权限 · 你不是 root,也不在 dialout 组 → 落到"其他"这一档 → Permission denied
权限字符串 crw-rw---- 逐位拆解:类型位 + 属主 / 属组 / 其他 各三位

rwx 到底是什么意思?

注意 r w x文件目录的含义完全不同,这是新手第二大困惑:

权限位对普通文件对目录
r (read)能读取文件内容,比如 cat列出目录里有哪些文件(ls
w (write)能修改文件内容能在目录里新建/删除/重命名文件
x (execute)能当程序执行进入该目录(cd),以及访问其中的文件
记忆点:对目录来说,没有 x 就进不去——即使有 r 能列出名字,也读不到内容。这是"我能 ls 但打不开文件"的经典原因。

三、数字权限 755 / 664 是怎么算出来的

你一定见过 chmod 755chmod 644 这种写法。它其实是三位数,每一位是一组 rwx 的和

每个权限位有一个固定分值,同组内的值相加: r = 4 w = 2 x = 1执行 - = 0 于是: 7 = 4+2+1 → rwx 6 = 4+2+0 → rw- 5 = 4+0+1 → r-x 0 = 0+0+0 → --- chmod 755 = 属主 rwx(7) · 属组 r-x(5) · 其他 r-x(5)   chmod 664 = 属主 rw-(6) · 属组 rw-(6) · 其他 r--(4)
数字权限速算:r=4、w=2、x=1,同组相加

只需记三个数:7 = 全部权限6 = 读写0 = 没权限。日常开发里 90% 的场景就够用了。

四、为什么 chmod 777 既无效又不该用

问题说明
① 它只是临时的设备节点是每次插入时由 udev 动态创建的。你改的是这一次创建的节点,拔掉后节点消失,下次插入时 udev 按默认规则重建,权限回到 crw-rw----
② 它把门开给了所有人777 = 任何用户都能读写。在多人共用 / 服务器环境里,别人可以直接 cat /dev/ttyUSB0 偷听你的串口数据。
③ 它掩盖了真正的需求你要的不是"让所有人都能用",而是"让我能用"。前者是破坏安全模型,后者只需要一个正确的组成员身份。
正确的提问方式:不是"怎么让这个设备对所有人都可写",而是"怎么让我成为有权限的那个人"。顺着这个思路,解法就清晰了。

五、解法一(推荐先试):把自己加进设备所属的组

回到那行输出:crw-rw---- 1 root dialout。设备属于 dialout 组,而该组的权限是 rw-。所以只要你在 dialout 组里,就能读写它。

第 1 步:确认设备属于哪个组

$ ls -l /dev/ttyUSB0
crw-rw---- 1 root dialout 188, 0 ... /dev/ttyUSB0
# 第 4 列 root 是属主,第 5 列 dialout 就是属组
不同发行版的组名不一样:Ubuntu / Debian 是 dialout;Arch / Manjaro 是 uucp;某些发行版还有 serial以你机器上 ls -l 显示的为准,不要照抄。

第 2 步:看看自己现在在哪些组

$ groups
wenji adm cdrom sudo dip plugdev lpadmin
# 没有 dialout,这就是打不开的原因

第 3 步:把自己加进去

sudo usermod -aG dialout $USER
#        ↑↑ 一定要有 -a(append),否则会把你从其他组踢出去!
最容易踩的坑:组身份是在登录时确定的。执行完 usermod 后,你现在这个终端里的 groups 不会变。必须注销重新登录(或者重启)。
不想注销的话,在当前终端临时生效可以用:newgrp dialout——但它只对这一个 shell 有效,新开的终端还是不行。

第 4 步:验证

$ groups            # 重新登录后执行,应该能看到 dialout
wenji adm dialout cdrom sudo dip plugdev lpadmin

$ echo "hello" > /dev/ttyUSB0   # 能写进去就说明权限对了
$ cat /dev/ttyUSB0              # Ctrl+C 退出

到这一步,绝大多数人的 Permission denied 就已经解决了。但如果你还想要下面这些,就需要 udev:

六、udev 是什么:Linux 的"设备插拔管家"

/dev 目录下那几百个节点并不是写死在磁盘上的,而是每次开机、每次插入时由 udev 动态生成的。理解了这个流程图,你就理解了为什么权限会"重置"。

① 插入 USBCH340 / ST-Link ② 内核识别加载驱动 ch341.ko ③ 发出 ueventnetlink 广播 ④ udevd 收到systemd-udevd ⑤ 匹配规则 按文件名数字从小到大,依次匹配: /etc/udev/rules.d/99-usb-serial.rules (你自己写的,最后生效) /usr/lib/udev/rules.d/*.rules     (系统自带,先执行) ⑥ 创建设备节点/dev/ttyUSB0 ⑦ 设置权限 / 属组MODE / GROUP / OWNER ⑧ 创建符号链接SYMLINK → /dev/tty_stm32
udev 事件流:设备插入 → 内核 uevent → udevd 匹配规则 → 生成节点 / 设权限 / 建软链接
一句话记住:/dev/ttyUSB0 不是文件,是 udev 按照规则"现场变出来"的一个入口。你想改权限,就要改"变出它来的那份规则",而不是去改变出来的结果。

七、解法二:写一条永久生效的 udev 规则

第 1 步:查出设备的 VID 和 PID

$ lsusb
Bus 001 Device 005: ID 1a86:7523 QinHeng Electronics CH340 serial converter
Bus 001 Device 003: ID 0483:3748 STMicroelectronics ST-LINK/V2
Bus 001 Device 007: ID 10c4:ea60 Silicon Labs CP210x UART Bridge
Bus 001 Device 009: ID 0403:6001 Future Technology Devices FT232 Serial

1a86:7523 冒号前是厂商 ID(idVendor),后面是产品 ID(idProduct)。这就是 udev 认设备的身份证。

🔌 CH340 / CH341(最常见最便宜)
1a86:7523
🔌 CP2102 / CP2104
10c4:ea60
🔌 FT232RL
0403:6001
🎯 ST-Link V2
0483:3748
🎯 ST-Link V2.1(Nucleo 板上)
0483:374b
🎯 ST-Link V3
0483:374e / 374f

第 2 步:规则语法(记住 4 个符号就够了)

符号含义例子
==匹配:判断条件是否成立(如果…)ATTRS{idVendor}=="1a86"
=赋值:设置属性(那么…)GROUP="dialout"
+=追加:不覆盖已有值(推荐用于 SYMLINK)SYMLINK+="tty_stm32"
:=强制赋值:后面的规则不允许再改GROUP:="dialout"

常用的键就这几个:

作用常用值
KERNEL匹配内核给的设备名"ttyUSB*""ttyACM*"
SUBSYSTEM匹配子系统"tty""usb"
ATTRS{idVendor}匹配厂商 ID"1a86"
ATTRS{idProduct}匹配产品 ID"7523"
ATTRS{serial}匹配设备序列号(区分两个同型号设备的关键)"0001"
MODE设置权限(八进制)"0666"
GROUP设置属组"dialout"
SYMLINK创建固定名字的软链接"tty_stm32"

第 3 步:动手写规则文件

sudo nano /etc/udev/rules.d/99-usb-serial.rules
# ===== 99-usb-serial.rules =====
# 文件名前面的数字 = 执行顺序,99 保证它在系统默认规则之后运行(从而覆盖默认值)
# 必须以 .rules 结尾,否则 udev 完全不理会这个文件

# ① CH340/CH341 —— 最便宜的 USB 转串口
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", \
  MODE="0664", GROUP="dialout", SYMLINK+="tty_ch340"

# ② CP210x —— NodeMCU/ESP32 开发板常见
SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", \
  MODE="0664", GROUP="dialout", SYMLINK+="tty_esp32"

# ③ FT232 —— 工业模块常见,给一个固定名字
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", \
  MODE="0664", GROUP="dialout", SYMLINK+="tty_ftdi"

# ④ ST-Link —— 调试器走 usb 子系统,不是 tty,所以 SUBSYSTEM 不同
SUBSYSTEM=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="3748", \
  MODE="0664", GROUP="plugdev", TAG+="uaccess"

# ⑤ 两个同型号 CH340(用序列号区分,先查:udevadm info -a -n /dev/ttyUSB0 | grep serial)
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{serial}=="0001", SYMLINK+="tty_motor"
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{serial}=="0002", SYMLINK+="tty_sensor"
两个易错点:
① 一条规则必须写在一行里。上面的 \ 是 shell 续行符示意,写进文件时请
  把每条规则压成真正的一行(或确保反斜杠后直接换行,不能有空格)。
ATTRS{}(带 S)匹配的是父设备链上的属性,ATTR{}(无 S)只匹配当前设备。写 VID/PID 一律用 ATTRS{}

八、让规则生效,以及怎么调试

第 1 步:重载规则并重新触发

# 让 udev 重新读取规则文件(改完规则必须做)
sudo udevadm control --reload-rules

# 对所有已连接的设备重新跑一遍规则(不用拔插)
sudo udevadm trigger

# 只看效果:
ls -l /dev/tty_ch340 /dev/tty_esp32
# lrwxrwxrwx 1 root root 7 ... /dev/tty_ch340 -> ttyUSB0
# 现在写代码里直接写 /dev/tty_ch340,永远不用担心编号变化

第 2 步:规则没生效?这样查

# ① 内核到底认没认这个设备?(不认就是硬件/虚拟机问题,不是 udev 的锅)
dmesg | tail -20
# 期望看到:usb 1-1: ch341-uart converter now attached to ttyUSB0

# ② 打印这台设备的全部可匹配属性(写规则时照着抄)
udevadm info -a -n /dev/ttyUSB0 | less
# 会输出好几段 "looking at parent device",VID/PID 通常在其中一段

# ③ 干跑一次:看看 udev 认为自己会怎么处理这台设备
udevadm test $(udevadm info -q path -n /dev/ttyUSB0) 2>&1 | tail -30
# 输出里找 "Handling device node" / "setting permissions" / "creating link"

# ④ 实时监控:插拔时看事件是否到达 udev
udevadm monitor --udev
# 插上设备,应该刷出 UDEV  [xxx] add  /devices/.../ttyUSB0

九、8 个真实会踩的坑

现象根因解法
加了 dialout 组,还是 denied组身份在登录时确定,没注销重登注销重登录,或 newgrp dialout 临时生效
虚拟机里根本看不到 /dev/ttyUSB0USB 设备没从宿主机转发给虚拟机VMware:虚拟机设置 → USB 控制器 → 连接;VirtualBox:设备 → USB → 勾选
WSL 里 udev 规则完全不生效WSL 不运行 systemd/udevd,设备节点由 Windows 侧管理WSL2 改用 usbipd-win 绑定,或直接在 Windows 下用 COM 口
设备出现几秒后又消失Ubuntu 22.04+ 的盲文服务 brltty 会抢占 CH340sudo apt remove brltty,然后拔插
写了规则文件但毫无反应文件名不以 .rules 结尾,或没执行 reload确认文件名 + sudo udevadm control --reload-rules && sudo udevadm trigger
软链接建出来了,但权限还是不对软链接指向目标节点,权限由目标节点决定在规则里同时设 MODE="0664"GROUP="dialout"
两个同型号模块分不清VID/PID 完全相同,规则无法区分ATTRS{serial}=="...",或用 KERNELS 匹配插在哪个 USB 口
用 sudo 跑 IDE 后,工程文件变 root 所有不要靠 sudo 绕权限问题修好权限后执行 sudo chown -R $USER:$USER 工程目录 恢复

十、一页纸排查清单(可以直接抄走)

#!/bin/bash
# 串口打不开?从上往下跑一遍,90% 的问题会在第 3 步之前现形
echo "① 内核认到设备了吗"
dmesg | tail -15 | grep -iE "tty|usb|ch34|cp210|ftdi"

echo "② 设备节点在吗、权限长什么样"
ls -l /dev/ttyUSB* /dev/ttyACM* 2>/dev/null || echo "  → 节点不存在,回到第 1 步"

echo "③ 我在该在的组里吗"
echo "  设备属组: $(stat -c %G /dev/ttyUSB0 2>/dev/null)"
echo "  我的组  : $(id -nG)"

echo "④ udev 规则命中了吗"
udevadm info -a -n /dev/ttyUSB0 2>/dev/null | grep -E "idVendor|idProduct|serial" | head -6

echo "⑤ 端口被别的进程占着吗(不是权限问题,是占用问题)"
lsof /dev/ttyUSB0 2>/dev/null || echo "  未被占用"
# 常见凶手:ModemManager、brltty、没关掉的 minicom、上次调试残留的 openocd
# 解决:sudo systemctl stop ModemManager ; sudo apt remove brltty
最后一条经验:报错信息里如果同时出现 Permission deniedDevice or resource busy,这是两个完全不同的问题——前者是权限(看组),后者是被占用lsof 找出凶手并关掉)。别用同一种方法去治。
🧠 小测验
crw-rw---- 1 root dialout 中,一个既不是 root、也不在 dialout 组的用户,有什么权限?
零权限。第一个 c 是类型位,之后每三位一组分别是属主(rw-)、属组(rw-)、其他(---)。该用户落在"其他",是 ---,所以读写都不行,这正是 Permission denied 的来源。
为什么 chmod 777 /dev/ttyUSB0 只能管一次?
因为 /dev 下的设备节点是 udev 在设备插入时动态创建的。你修改的是这一次创建的节点;拔掉后节点消失,下次插入时 udev 按默认规则重新创建,权限回到默认值。
执行 sudo usermod -aG dialout $USER 后,为什么当前终端还是打不开串口?
组身份在登录时确定并写进进程的凭证里。改了 /etc/group 不会影响已经运行的 shell。需要注销重登录;临时方案是在该终端执行 newgrp dialout-a(append)也很关键,漏了会把你从其他组里踢出去。
规则文件里写 VID/PID 时,应该用 ATTRS{} 还是 ATTR{}?为什么?
ATTRS{}(带 S)。ATTR{} 只匹配当前设备节点自身的属性,而 VID/PID 挂在父设备(USB 接口/设备)链上;ATTRS{} 会沿父链向上搜索,才能匹配到。
两个完全同型号的 CH340 模块,怎么让它们各自有固定名字?
VID/PID 相同,需要用唯一属性区分:优先用 ATTRS{serial}=="..."(芯片序列号),其次用 KERNELS=="1-1.2" 匹配物理 USB 口位置。序列号可通过 udevadm info -a -n /dev/ttyUSB0 | grep serial 查到。
设备节点出现了,但几秒后自动消失,最可能的原因是什么?
Ubuntu 22.04 及以后,盲文服务 brltty 的 udev 规则会抢占 CH340 类设备。执行 sudo apt remove brltty 后重新插拔即可。同类问题也可能是 ModemManager 在拨号探测。
← Linux 总览🏠 顶层目录STM32 开发实战 →