一条 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 那块」。零拷贝。

%%{init: {"theme":"base","themeVariables":{"fontSize":"16px","primaryTextColor":"#333333","lineColor":"#888888","clusterBkg":"#fafafa","clusterBorder":"#cccccc","titleColor":"#555555"},"flowchart":{"nodeSpacing":34,"rankSpacing":48,"padding":12,"useMaxWidth":true}}}%% flowchart TD A1["X client 在自己的内存里<br/>把一帧位图画好"] A2["第 1 次拷贝<br/>write() 进 socket:用户态 → 内核缓冲"] A3["第 2 次拷贝<br/>X server read():内核 → 用户态"] A4["X server 拿到像素,贴到屏幕上"] A1 --> A2 A2 --> A3 A3 --> A4 B1["双方先 attach 同一块共享内存段<br/>两张页表,指向同一片物理内存"] B2["X client 直接往那块内存里写"] B3["只发一个很小的通知过去:「看 shmid 那块」"] B4["X server 读的就是同一片物理内存<br/>一次拷贝都没有"] B1 --> B2 B2 --> B3 B3 --> B4 A4 --> F["右边这条快得多 —— 代价是它假设「双方在同一台机器上」<br/>这是 X11 里唯一一个这么假设的扩展<br/>网络透明性那个承诺,到这里为止"] B4 --> F class A1 proc class A2 net class A3 net class A4 host class B1 proc class B2 accent class B3 accent class B4 host class F bad classDef host fill:#ffe6cc,stroke:#d79b00,color:#333333 classDef net fill:#dae8fc,stroke:#6c8ebf,color:#333333 classDef proc fill:#d5e8d4,stroke:#82b366,color:#333333 classDef idea fill:#e1d5e7,stroke:#9673a6,color:#333333 classDef bad fill:#f8cecc,stroke:#b85450,color:#333333 classDef plain fill:#f5f5f5,stroke:#999999,color:#333333 classDef accent fill:#fff2cc,stroke:#d6b656,color:#333333

图 1 MIT-SHM 快在哪:把两次拷贝省成零次 —— 代价是它假设双方同机

这是一次非常划算的优化。代价写在图底下那行红字里:

它是 X11 里唯一一个假设「双方在同一台机器上」的扩展。

这是一个教科书级别的局部优化打破全局抽象。
X11 花了一整套协议设计去承诺「你不需要知道窗口出现在哪台机器上」,
MIT-SHM 为了省两次拷贝,把这个承诺捅了一个洞。

于是它成了所有远程场景、所有容器场景里第一个崩的地方。

1.3 前提只有一条,破法只有四种

把上面那句话翻译成可操作的形式:

共享内存能用,前提只有一条 —— 两个进程能映射到同一块物理内存。

这条前提长得像废话,但它的价值在于它很好证伪。
只要想清楚「这条在什么情况下不成立」,就能穷举出全部的失败方式:

%%{init: {"theme":"base","themeVariables":{"fontSize":"16px","primaryTextColor":"#333333","lineColor":"#888888","clusterBkg":"#fafafa","clusterBorder":"#cccccc","titleColor":"#555555"},"flowchart":{"nodeSpacing":34,"rankSpacing":48,"padding":12,"useMaxWidth":false}}}%% flowchart TD T["共享内存能用,前提只有一条:<br/>两个进程能映射到同一块物理内存<br/>这条不成立,MIT-SHM 就一定崩"] T --> C1["① 不在同一台机器<br/>远程 X:程序跑在 server-a<br/>X server 在我这台办公机上"] T --> C2["② 不在同一个 IPC namespace<br/>容器:同一台物理机<br/>但 SysV key 互相看不见"] T --> C3["③ 额度不够<br/>能映射,但申请不到<br/>/dev/shm 满了,或 SysV 限额到顶"] T --> C4["④ 段泄漏<br/>进程崩了没回收<br/>孤儿段一直占着额度"] C1 --> D1["典型报错<br/>BadAccess(opcode 130)<br/>或友好版:extension MIT-SHM missing"] C2 --> D2["典型报错<br/>QSharedMemory::attach 失败<br/>「容器里 Chrome / Qt 程序起不来」多半停在这儿"] C3 --> D3["典型报错<br/>No space left on device(磁盘明明是空的)<br/>shmget: Invalid argument"] C4 --> D4["典型报错<br/>和 ③ 一模一样<br/>报错分不出三和四,得看 ipcs 里段的 nattch"] D1 --> F["方向是从上往下的:先判断落在哪一类,再去看报错<br/>反过来做 —— 从报错猜原因 —— 就只能靠背<br/>而且 ③ 和 ④ 的报错根本一模一样"] D2 --> F D3 --> F D4 --> F class T idea class C1 bad class C2 bad class C3 bad class C4 bad class D1 plain class D2 plain class D3 plain class D4 plain class F host classDef host fill:#ffe6cc,stroke:#d79b00,color:#333333 classDef net fill:#dae8fc,stroke:#6c8ebf,color:#333333 classDef proc fill:#d5e8d4,stroke:#82b366,color:#333333 classDef idea fill:#e1d5e7,stroke:#9673a6,color:#333333 classDef bad fill:#f8cecc,stroke:#b85450,color:#333333 classDef plain fill:#f5f5f5,stroke:#999999,color:#333333 classDef accent fill:#fff2cc,stroke:#d6b656,color:#333333

图 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 回来的时候,程序早就在别的地方了。

%%{init: {"theme":"base","themeVariables":{"fontSize":"16px","primaryTextColor":"#333333","lineColor":"#888888","clusterBkg":"#fafafa","clusterBorder":"#cccccc","titleColor":"#555555"},"flowchart":{"nodeSpacing":34,"rankSpacing":48,"padding":12,"useMaxWidth":true}}}%% flowchart TD L1["X client 一口气发出 ① ② ③ ④ ⑤ ⑥ ⑦<br/>发完就走,不等回复"] L2["X server 处理到 ②(那个 ShmAttach)<br/>出错,把 BadAccess 发回来"] L3["错误走回来的时候,client 早跑到 ⑦ 了<br/>堆栈指向一个和共享内存毫无关系的调用"] L1 --> L2 L2 --> L3 R1["每发一个请求就停下来等回复<br/>命令行 --sync / XSynchronize / _Xdebug"] R2["② 出错,当场返回"] R3["堆栈就停在 ② —— 真正的现场<br/>代价:程序会慢一个数量级,定位完就关掉"] R1 --> R2 R2 --> R3 L3 --> F["所以这类报错「看起来毫无头绪」不是错觉,是协议的异步性造成的<br/>异步是篇②那条「必然有 round-trip」的另一面:<br/>正因为不等回复才不至于每条命令都赔一个 RTT,代价就是错误和现场脱节"] R3 --> F class L1 proc class L2 net class L3 bad class R1 proc class R2 net class R3 accent class F idea classDef host fill:#ffe6cc,stroke:#d79b00,color:#333333 classDef net fill:#dae8fc,stroke:#6c8ebf,color:#333333 classDef proc fill:#d5e8d4,stroke:#82b366,color:#333333 classDef idea fill:#e1d5e7,stroke:#9673a6,color:#333333 classDef bad fill:#f8cecc,stroke:#b85450,color:#333333 classDef plain fill:#f5f5f5,stroke:#999999,color:#333333 classDef accent fill:#fff2cc,stroke:#d6b656,color:#333333

图 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 之类的数据库正在用的。
清错了会打断正在跑的实例。

理由不是「谨慎一点比较好」,是这两件具体的事:

  1. 数据库的段 nattch 可能就是很小的数,甚至在某些时刻是 0。
    nattch=0 的判据对普通 GUI 程序很准,对数据库不准。
  2. 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. 虚拟内存 —— 「共享」到底是什么意思:两张页表,指向同一片物理内存(1.2)
  2. namespace —— 隔离边界不在硬件,在内核的命名空间(2.2)
  3. X11 的网络透明性 —— 抽象的承诺到哪为止,以及它在哪里被局部优化捅了洞(1.2、3.3)
  4. 异步协议 —— 错误为什么和现场脱节,为什么堆栈是假的(3.2)

四个原理分开看都是基础知识,凑在一起就变成了一条「搜了半天也没搞明白」的报错。

这也是这组文章从头到尾想说的那件事:
故障看起来千奇百怪,是因为它们是几条原理交叉的结果;
把原理一条条拆开,故障就变回可推导的。

4.5 可以自己反推的练习

盖住答案,先想一遍。这几个都能用本篇的四分法直接推出来:

  1. 一台机器上 ipcs -m 显示有 300 个 nattch=0 的段,
    但 ipcs -m -l 显示 max number of segments = 4096。
    现在报 shmget failed: No space left on device。是第三类还是第四类?
    (提示:段数没到顶,但额度不只有段数这一条。)
  2. 容器里的程序报 BadAccess + opcode 130,echo $DISPLAY 输出 :0。
    这是第一类还是第二类?如果输出的是 localhost:10.0 呢?
  3. 同事把 ipcrm 写成了循环批量清,第二天数据库挂了。
    按 4.2,他犯的具体错误是哪一个判据用错了地方?
  4. 一个程序在 gdb 里停下来,堆栈顶上是某个字体渲染函数,
    但报错文案写着 Major opcode 130 (MIT-SHM)。堆栈是不是骗人的?怎么让它不骗人?
  5. 我在 server-a 上加了 QT_X11_NO_MITSHM=1,程序跑起来了,
    但一点也没变快。这说明什么?
    (提示:篇② 1.2 —— 想清楚这条链路上真正的瓶颈是什么。)
  6. 把 /dev/shm 从 64MB 调到 2GB 之后,Chrome 能起了,
    但一周后又报同样的错。最可能是哪一类?

第 1 题值得多想一层:SHMMNI(段数)、SHMALL(总量)、SHMMAX(单段大小)
是三条独立的上限,撞任何一条都会失败。300 个段没撞上 4096,
但如果每个段都不小,总量完全可能已经到顶。
「这一条没超」不等于「额度够」 —— 这和篇② 里那句
「一个观察排除一种可能,不等于确定了另一种」是同一个思维毛病。

第 5 题的答案是:说明你撞的确实是第一类,而且 MIT-SHM 从来就不是你卡顿的原因。
关掉它只是让程序不再崩,慢是另一条线上的事
——(a)三段 RTT 累加,(b)工具包改成客户端位图渲染,都在篇②。
「报错消失了」和「问题解决了」是两件事,这一篇里它们恰好分得最开。

到这儿,四篇分篇里的三篇写完了。还剩一个问题没答:
把 display 借出去,到底等于交出了什么?
以及,既然「传命令」这条路有这么多硬伤,为什么它还活着 —— 又为什么正在死去。
那是篇③,也是这组文章的收尾。

posted @ 2026-09-18 19:18  起飞的乌龟  阅读(21)  评论(0)    收藏  举报