一条 BadAccess 背后的四个原理
这是「远程 GUI 与跳板机链路」系列的篇④。
本篇的第一性问题:共享内存的前提是「两个进程能映射到同一块物理内存」。
这个前提什么时候不成立?回链总述:本篇负责总述里埋的那个钩子 ——
总述的 2.4
说 MIT-SHM 是「传命令」模型唯一一个违反网络透明性承诺的扩展,
但没有展开。这一篇就是那个展开。
篇② 的 3.4 也把这件事推给了这里。本篇能解释故障表里的哪几行:
BadAccess加opcode 130、
容器里 Chrome 起不来、No space left on device但磁盘是空的。本篇最想让你带走的一句话:先定位落在哪一类,再去看报错。
反过来做 —— 从报错猜原因 —— 就只能靠背,而且其中两类的报错根本一模一样。全文四部分:一 前提(共享内存凭什么能共享)→ 二 四类(逐个看破法)→
三 归位(把那一页报错对号入座)→ 四 排查(顺序,以及一条别踩的线)。
第一部分 先问一个更笨的问题:共享内存凭什么能共享
1.1 从那条报错说起
在 server-a 上跑一个 GUI 程序,窗口没出来,终端里吐出这么一段:
X Error of failed request: BadAccess (attempt to access private resource denied)
Major opcode of failed request: 130 (MIT-SHM)
Minor opcode of failed request: 1 (X_ShmAttach)
Serial number of failed request: 27
Current serial number in output stream: 30
这条报错的搜索命中率极高,而搜到的答案九成是同一句:
「加个 QT_X11_NO_MITSHM=1 就好了。」
这句话是对的 —— 它确实能让程序跑起来。问题是它什么也没解释,
所以下次换个程序、换个容器、换个报错文案,你还得再搜一次。
本篇不打算给你第二个环境变量。本篇想给你的是一个判别框架:
一旦建立起来,这一整类报错(包括你还没见过的那几种)都不用背。
1.2 MIT-SHM 为什么会存在:一次很实在的局部优化
先别急着看报错,先问一个更笨的问题:X11 里为什么会有共享内存这种东西?
按 总述 2.2 的说法,
X11 选的是「传命令」—— 程序把「怎么画」发给 X server。这条路上本来没有共享内存什么事。
但现实是,绝大多数 X client 和 X server 其实跑在同一台机器上。
本机场景下,「发命令」这套东西有一处很肉疼的开销:一帧位图要经 socket 交给 X server,
得过「用户态 → 内核 → 用户态」两次拷贝。窗口越大、刷新越勤,这两次拷贝越贵。
MIT-SHM 的动机就在这儿:既然双方在同一台机器上,那就别拷了 ——
双方 attach 同一块共享内存段,client 直接往那块内存里写,
然后只发一个很小的通知过去:「看 shmid 那块」。零拷贝。
图 1 MIT-SHM 快在哪:把两次拷贝省成零次 —— 代价是它假设双方同机
这是一次非常划算的优化。代价写在图底下那行红字里:它是 X11 里唯一一个假设「双方在同一台机器上」的扩展。
这是一个教科书级别的局部优化打破全局抽象。
X11 花了一整套协议设计去承诺「你不需要知道窗口出现在哪台机器上」,
MIT-SHM 为了省两次拷贝,把这个承诺捅了一个洞。
于是它成了所有远程场景、所有容器场景里第一个崩的地方。
1.3 前提只有一条,破法只有四种
把上面那句话翻译成可操作的形式:
共享内存能用,前提只有一条 —— 两个进程能映射到同一块物理内存。
这条前提长得像废话,但它的价值在于它很好证伪。
只要想清楚「这条在什么情况下不成立」,就能穷举出全部的失败方式:
图 2 共享内存四分法:前提只有一条,破法只有四种
四类,就这么多:| # | 破法 | 一句话 |
|---|---|---|
| ① | 不在同一台机器 | 远程 X。物理内存压根不是同一片 |
| ② | 不在同一个 IPC namespace | 容器。是同一台机器,但内核不让它们看见对方的段 |
| ③ | 额度不够 | 能映射,但申请不到 |
| ④ | 段泄漏 | 能映射也该申请得到,但额度被死人占着 |
注意这四类的结构:①② 是映射根本不可能,③④ 是映射可能但拿不到资源。
而 ③ 和 ④ 的区别是「谁占着额度」—— 这个区别在报错里完全看不出来,
后面 2.5 专门讲。
从这里往下,每一条报错都只是这四类里某一类的一种表达方式。
第二部分 四类逐个看
2.1 第一类:不在同一台机器
这是本系列这条链路上最常撞的一类。
程序跑在 server-a,X server 在我这台办公机上 ——
按 篇① 的 1.1,角色是反的,
拿着屏幕的这端才是 server。两端隔着三段网络,没有任何一块物理内存是共用的。
于是 XShmAttach 传过去的那个 shmid,在对端毫无意义 ——
它是 server-a 上的一个 SysV 段 id,我这台办公机的内核根本不认识这个数字。
X server 拿到一个它无法解释的 id,回一个 BadAccess。
这一类有两种表现,差别只在程序有没有先问一句:
-
友好版:转发时 X server 干脆不宣告这个扩展,程序发现没有,
自动退化走 socket,只是慢一点。你只会看到一行提示:Xlib: extension "MIT-SHM" missing on display "localhost:10.0". -
难看版:X server 宣告了支持,程序信了,真去 attach,然后崩。
就是 1.1 那八行。
难看版为什么会发生,是 3.3 的内容 —— 那才是这一类真正的根因。
2.2 第二类:不在同一个 IPC namespace
这一类很容易被误判成第一类,因为报错很像,但机器其实是同一台。
容器有独立的 IPC namespace。SysV 共享内存的 key 是 namespace 内的命名空间 ——
容器里的进程和宿主机上的 X server 虽然踩在同一块物理内存条上,
但内核不让它们通过 key 找到对方的段。隔离边界不在硬件,在内核的命名空间。
典型报错是 Qt 那一版:
QSharedMemory::attach: unable to attach to shared memory segment
或者 XCB 那一版(同一件事,不同的报信人):
QXcbConnection: XCB error: 10 (BadAccess), sequence: 170, resource id: 4194306,
major code: 130 (MIT-SHM), minor code: 1 (Attach)
「容器里 Chrome / Qt 程序 / OpenCV 的 imshow 起不来」,多半停在这一类
—— 或者停在下一类。
判别办法很直接:程序和 X server 是不是同一台机器上、同一个 namespace 里。
如果 DISPLAY 指的是宿主机的 :0、容器又没有 --ipc=host,
那就是这一类,不是第一类。
这里还有个和 篇① 的 2.3
相邻但不同的坑:那一节讲的是容器连不上 X server(网络层面),
这一节讲的是容器连上了、但共享不了内存(IPC 层面)。
两件事都会让容器里的 GUI 起不来,症状却完全不同,别混。
2.3 第三类:额度不够
前两类是「映射不可能」,这一类是「映射可能,但申请不到」。
共享内存有两套额度,撞哪套取决于程序用的是哪套 API:
SysV 共享内存(shmget),额度是内核参数:
$ ipcs -m -l
------ Shared Memory Limits --------
max number of segments = 4096
max seg size (kbytes) = 18014398509465599
max total shared memory (kbytes) = 18014398442373116
min seg size (bytes) = 1
- 撞
max number of segments(SHMMNI)或总量(SHMALL)→
shmget failed: No space left on device - 单段请求量超过
SHMMAX→shmget: Invalid argument
POSIX 共享内存(shm_open),实际落在 /dev/shm 这个 tmpfs 上:
$ df -h /dev/shm
Filesystem Size Used Avail Use% Mounted on
tmpfs 64M 64M 0 100% /dev/shm
这个 64M 是容器的默认值,而且小得离谱 —— Chrome 一个标签页就能吃掉它。
所以「容器里 Chrome 起不来」有相当一部分不是 namespace 的问题,是这个 64MB:
Failed to create shared memory: No space left on device
PostgreSQL 的并行 worker 也用 POSIX shm,撞的是同一个 /dev/shm:
ERROR: could not resize shared memory segment "/PostgreSQL.1234567890"
to 2097152 bytes: No space left on device
这一类有一个特别容易把人带沟里的地方:
No space left on device里的 device,不是磁盘。
一看见这句就去 df -h / 查磁盘,然后发现磁盘是空的,然后彻底懵掉 ——
这是这一整类报错最常见的浪费时间方式。它说的是那个 tmpfs 或者那套内核额度。
2.4 第四类:段泄漏
额度本来够,但被死人占着。
进程正常退出时会释放自己的共享内存段。进程被 kill -9、段错误崩掉、
容器被强杀 —— 段就留在那儿了。SysV 段的生命周期不跟着进程走,
它跟着内核走,不显式删就一直在。
攒够了,就把额度吃光,然后表现出和第三类一模一样的报错。
看孤儿段:
$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00000000 32769 user 600 4194304 2 dest
0x00000000 65538 user 600 1048576 0
0x00000000 98307 user 600 1048576 0
0x0052e2c1 131076 postgres 600 56 1
关键那一列是 nattch —— 当前有几个进程 attach 着这个段。
nattch 为 0,意思是「没人在用它」。这就是孤儿段的特征。
(status 那列的 dest 表示段已经被标记删除,最后一个进程 detach 时会自动消失 ——
那种是健康的,不用管。)
2.5 三和四为什么必须分开
因为报错分不出它俩,但改法完全相反:
| 第三类:额度不够 | 第四类:段泄漏 | |
|---|---|---|
| 报错 | No space left on device |
一模一样 |
| 真正的问题 | 额度天生就小 | 额度够,被占着 |
| 改法 | 调大额度(--shm-size、sysctl) |
清孤儿段,然后去查谁在漏 |
| 改错的后果 | 段继续漏,过几天又满 | 额度确实不够,清完还是满 |
判别只需要一步:ipcs -m 看有没有一堆 nattch=0 的段。
有 → 第四类;没有、段都在正常使用 → 第三类。
而且第四类还多一条:清段只是止血。
段为什么会漏 —— 哪个程序在崩、崩在哪 —— 那是另一个问题,不查的话下周还得清一次。
第三部分 把那一页报错归位
3.1 八行报错,对号入座
有了四分法,下面这张表就不需要背了 —— 每一行都能自己推出来:
| 报错 | 归类 | 为什么 |
|---|---|---|
X Error ... BadAccess + Major opcode 130 (MIT-SHM) |
① 不同机器 | X server 在办公机、程序在 server-a,XShmAttach 传过去的 id 在对端毫无意义 |
Xlib: extension "MIT-SHM" missing on display "localhost:10.0" |
① 不同机器 | 友好版:X server 干脆不宣告,程序自动退化走 socket,只是慢 |
QXcbConnection: XCB error: ... major_code: 130 |
① 或 ② | Qt 版的同一件事。QT_X11_NO_MITSHM=1 就是绕它 |
QSharedMemory::attach: unable to attach |
② 不同 namespace | 容器有独立 IPC namespace,SysV key 不互通,得 --ipc=host 才穿得过去 |
shmget failed: No space left on device |
③ 或 ④ | 不是磁盘满,是 SysV 段数或总量到顶(SHMMNI / SHMALL) |
shmget: Invalid argument |
③ 额度 | 单段请求量超过 SHMMAX 上限 |
Chrome Failed to create shared memory 或直接崩 |
③ 额度 | 容器 /dev/shm 默认只有 64MB |
PostgreSQL could not resize shared memory segment ... No space left on device |
③ 额度 | 并行 worker 用 POSIX shm,撞的是同一个 /dev/shm |
注意第三行和第五行都给了两个归类。这不是含糊,是诚实:
这两条报错本身确实不足以定位,必须再看一眼环境(是不是跨机器 / ipcs -m 里有没有孤儿段)。
一张假装每行都能唯一定位的表,比一张标出歧义的表更害人。
3.2 一个必须讲的坑:X 报错指的位置是假的
回到 1.1 那八行。里面有两行特别容易被忽略:
Serial number of failed request: 27
Current serial number in output stream: 30
出错的是第 27 个请求,而程序现在已经发到第 30 个了。
这不是打印错了,这是 X11 协议的基本性质:它是异步的。
client 发完请求不等回复就往下跑 —— 这正是
篇② 的 1.3
那条「必然有 round-trip」的另一面:正因为不等回复,才不至于每条命令都赔一个 RTT。
代价就是:错误从 server 回来的时候,程序早就在别的地方了。
图 3 X 报错指的位置是假的:异步协议下,错误和现场是脱节的
这条性质的直接后果是:在调试器里停下来看到的堆栈,通常和共享内存毫无关系。
你会看到一个完全无辜的绘图调用、一个事件循环、甚至某个第三方库的内部函数。
在那儿找原因,找一天也找不到 —— 因为现场根本不在那儿。
要看到真正的现场,只能强制同步,三种写法任选:
# 一、很多 GTK / X 程序认这个命令行开关
myapp --sync
# 二、代码里改(调试期临时加)
XSynchronize(dpy, True);
# 三、Xlib 的调试开关,不用改代码也不用程序支持
# (需要程序链接的是带调试支持的 Xlib)
export _Xdebug=1
开了同步之后,每发一个请求就停下来等回复,错误当场返回,
堆栈停在真正的那一行。
代价很实在:程序会慢一个数量级 ——
按篇② 的算法,每个请求都要赔一个端到端 RTT,而这条链路上端到端 RTT 是三段之和。
所以它只用来定位,定位完就关掉。
顺便说一句:这条经验不只对 MIT-SHM 有用。所有 X 协议错误
(BadWindow、BadDrawable、BadMatch……)都有同样的漂移,
同样的解法。这是本篇里迁移性最好的一条。
3.3 「宣告支持」不等于「能用」
最后回答 2.1 留下的那个问题:难看版为什么会发生?
程序不是莽撞的。用 MIT-SHM 之前,它规规矩矩地问了一句
XShmQueryExtension(),得到的回答是「支持」。于是它才去 attach,然后崩了。
问题出在这一问一答问的不是同一件事:
- 程序想问的是:「我能用共享内存和你通信吗?」
- 协议能答的是:「我这个 X server 编译进了 MIT-SHM 这个扩展吗?」
X server 确实编译进了 —— 它在本机上服务其他 client 时用得好好的。
它不知道、也没有办法知道这个特定的 client 是从三段网络外面连进来的。
于是它诚实地回答「支持」,程序诚实地信了,然后 attach 失败。
这就是抽象泄漏最标准的形状:接口能表达的问题,和调用方真正想问的问题,
之间有一道缝。平时缝在那儿没事,一到边界条件就漏。
顺带解释了 2.1 那两种表现为什么会同时存在:
有些 X server 实现意识到了这个缝,在有远程 client 时干脆不宣告这个扩展
(于是你只看到那行友好的 missing on display);
有些没有,就老老实实回「支持」(于是你看到八行 BadAccess)。
报错文案的差别,反映的是各家 X server 实现有没有替你堵这道缝 —— 不是你的配置问题。
第四部分 排查顺序,以及一条别踩的线
4.1 从便宜到贵:四步
顺序就是四分法的顺序。每一步都在排除一整类,不是在碰运气。
第一步:echo $DISPLAY —— 先确认是不是跨机器(排除 / 命中第一类)
echo $DISPLAY
localhost:10.0→ 跨机器,X11 转发。就是第一类,后面三步不用做了。:0或:1→ 本机 X server,往下走。
这一步花两秒钟,能一刀砍掉最常见的那一类。
第二步:xdpyinfo —— server 到底宣告了没有
xdpyinfo | grep -i shm
有输出(MIT-SHM)说明 server 宣告了。
但请记住 3.3:宣告了不等于能用 —— 这一步只能证否,不能证是。
没输出就说明程序会自动退化,那你的卡顿另有原因,去看篇②。
第三步:额度(第三类)
df -h /dev/shm # POSIX 侧:那个 64M 的 tmpfs
ipcs -m -l # SysV 侧:段数 / 单段大小 / 总量三条上限
第四步:孤儿段(第四类)
ipcs -m
找 nattch 为 0 的段。有一堆 → 第四类,先读完 4.2 再动手。
4.2 ipcrm 之前必须确认的一件事
清孤儿段的命令很短:
ipcrm -m <shmid>
短到很容易被写成一行 for 循环批量清掉。别这么干。
⛔ 动手之前,先确认这个段不是 Oracle / PostgreSQL 之类的数据库正在用的。
清错了会打断正在跑的实例。
理由不是「谨慎一点比较好」,是这两件具体的事:
- 数据库的段
nattch可能就是很小的数,甚至在某些时刻是0。
nattch=0的判据对普通 GUI 程序很准,对数据库不准。 ipcrm没有回滚。 段一删,attach 着它的进程下一次访问就是段错误。
对一个在跑的生产库来说,这不是「重启一下」的量级。
所以正确的顺序是:先看 owner,再删。
ipcs -m
owner 那一列写着 postgres / oracle / 任何你不认识的服务账号 ——
跳过它,哪怕 nattch 是 0。只清 owner 是你自己、且你知道对应程序已经死了的段。
ipcs -m -p 还能看到最后操作这个段的 pid,可以拿去 ps 里对一下。
4.3 四类各自的改法
定位完了,改法按类走 —— 不要混着用:
| 类 | 治标 | 治本 |
|---|---|---|
| ① 不同机器 | QT_X11_NO_MITSHM=1,让程序别用 |
换传输模型(NX / Xpra / VNC,见篇③) |
| ② 不同 namespace | docker run --ipc=host |
想清楚要不要打破容器隔离;或让程序走 socket |
| ③ 额度不够 | --shm-size=2g;Chrome 加 --disable-dev-shm-usage 让它退回文件 |
按实际用量把 /dev/shm 或 SHMALL 调到合适 |
| ④ 段泄漏 | ipcrm -m <id>(先读 4.2) |
去查谁在漏 —— 哪个程序在崩、为什么没走正常退出路径 |
关于第一类那个 QT_X11_NO_MITSHM=1:现在你知道它为什么有用了 ——
它不是「修好了共享内存」,它是让程序放弃共享内存、退回 socket。
效果等同于 2.1 里那个友好版:能用,只是每帧多两次拷贝。
在跨机器的场景下,那两次拷贝根本不是瓶颈(瓶颈是三段 RTT,见篇②),
所以这个「治标」在第一类里其实相当接近治本。
但在容器(第二类)里就不一样了 —— 那里双方真在同一台机器上,
关掉 MIT-SHM 是实打实地损失性能,--ipc=host 才是对的方向。
同一个环境变量,在两类问题里的性价比完全不同。 这就是为什么要先分类。
4.4 回头看:一条报错踩到了哪四个原理
这也是这一篇被放在系列最后的原因。那八行 BadAccess,同时踩到:
- 虚拟内存 —— 「共享」到底是什么意思:两张页表,指向同一片物理内存(1.2)
- namespace —— 隔离边界不在硬件,在内核的命名空间(2.2)
- X11 的网络透明性 —— 抽象的承诺到哪为止,以及它在哪里被局部优化捅了洞(1.2、3.3)
- 异步协议 —— 错误为什么和现场脱节,为什么堆栈是假的(3.2)
四个原理分开看都是基础知识,凑在一起就变成了一条「搜了半天也没搞明白」的报错。
这也是这组文章从头到尾想说的那件事:
故障看起来千奇百怪,是因为它们是几条原理交叉的结果;
把原理一条条拆开,故障就变回可推导的。
4.5 可以自己反推的练习
盖住答案,先想一遍。这几个都能用本篇的四分法直接推出来:
- 一台机器上
ipcs -m显示有 300 个nattch=0的段,
但ipcs -m -l显示max number of segments = 4096。
现在报shmget failed: No space left on device。是第三类还是第四类?
(提示:段数没到顶,但额度不只有段数这一条。) - 容器里的程序报
BadAccess+opcode 130,echo $DISPLAY输出:0。
这是第一类还是第二类?如果输出的是localhost:10.0呢? - 同事把
ipcrm写成了循环批量清,第二天数据库挂了。
按 4.2,他犯的具体错误是哪一个判据用错了地方? - 一个程序在 gdb 里停下来,堆栈顶上是某个字体渲染函数,
但报错文案写着Major opcode 130 (MIT-SHM)。堆栈是不是骗人的?怎么让它不骗人? - 我在 server-a 上加了
QT_X11_NO_MITSHM=1,程序跑起来了,
但一点也没变快。这说明什么?
(提示:篇② 1.2 —— 想清楚这条链路上真正的瓶颈是什么。) - 把
/dev/shm从 64MB 调到 2GB 之后,Chrome 能起了,
但一周后又报同样的错。最可能是哪一类?
第 1 题值得多想一层:SHMMNI(段数)、SHMALL(总量)、SHMMAX(单段大小)
是三条独立的上限,撞任何一条都会失败。300 个段没撞上 4096,
但如果每个段都不小,总量完全可能已经到顶。
「这一条没超」不等于「额度够」 —— 这和篇② 里那句
「一个观察排除一种可能,不等于确定了另一种」是同一个思维毛病。
第 5 题的答案是:说明你撞的确实是第一类,而且 MIT-SHM 从来就不是你卡顿的原因。
关掉它只是让程序不再崩,慢是另一条线上的事
——(a)三段 RTT 累加,(b)工具包改成客户端位图渲染,都在篇②。
「报错消失了」和「问题解决了」是两件事,这一篇里它们恰好分得最开。
到这儿,四篇分篇里的三篇写完了。还剩一个问题没答:
把 display 借出去,到底等于交出了什么?
以及,既然「传命令」这条路有这么多硬伤,为什么它还活着 —— 又为什么正在死去。
那是篇③,也是这组文章的收尾。

浙公网安备 33010602011771号