共享内存让多个执行者访问同一份底层存储,减少大块数据经 IPC 缓冲往返的需求。但它不自动定义谁能写、何时可读、数据格式和崩溃后怎么恢复。很多共享内存协议真正困难的部分是同步与生命周期。
先选择共享对象的创建方式
| 方式 | 如何让另一进程获得访问 | 生命周期特点 |
| MAP_SHARED 匿名映射 | 在 fork 前创建并由子进程继承 | 通常适合有共同祖先的进程 |
| POSIX shm_open | 用约定名称打开对象 | 名称与已打开引用、映射分开管理 |
| 普通文件 MAP_SHARED | 打开同一文件对象 | 还要考虑文件大小与持久化语义 |
| memfd_create | 继承 fd 或经 SCM_RIGHTS 传递 | 不需公共路径名,可配置 seals |
| SysV shmget/shmat | 通过 IPC 标识符附加 | 需要显式管理标记删除与附加关系 |
MAP_SHARED 的 mmap 失败返回 MAP_FAILED,不是 NULL。文件支持的映射通常需先把对象扩展到足够大小,越过实际文件范围访问可能产生 SIGBUS;对方并发截断也是需要处理的协议风险。
POSIX shm_open 的名称如 /myregion,不是要求调用者直接 open 某个 /dev/shm 路径。Linux 常在 tmpfs 实现相关对象,但接口语义不应绑定那个文件系统路径。O_CREAT|O_EXCL 可用于识别唯一创建者;初始化者与后续打开者要协调,不能双方都重复初始化同步对象。shm_open(3)
同一存储不等于相同虚拟地址
两个进程可能把同一对象映射到不同地址。共享数据结构中直接保存普通进程指针,通常不能由另一进程解引用;应使用相对对象起点的偏移、索引或其他协议句柄。
结构布局还涉及整数宽度、对齐、字节序、版本和原子操作支持。如果双方程序独立升级,不能只因为使用同一个头文件名称就认为二进制布局兼容。
MAP_SHARED 让写入作用于共享对象,不代替同步。缓存一致性也不等于语言层面的 happens-before。发布数据应使用正确的进程间锁、信号量或明确支持该用途的原子协议,不能用 volatile 加忙等凑成通用同步。
偏移也需要校验,不能只是把指针改成整数
设共享区协议允许访问的总长度为 total,记录给出 offset 和 length。检查应写成 offset ≤ total 且 length ≤ total - offset;在固定宽度语言里直接算 offset + length 再比较,可能先发生溢出。下面把所有字段限制为无符号 64 位,再验证范围。
U64_MAX = (1 << 64) - 1
def valid_span(total, offset, length):
return (all(0 <= x <= U64_MAX for x in (total, offset, length))
and offset <= total and length <= total - offset)
assert valid_span(4096, 128, 32)
assert valid_span(4096, 4096, 0)
assert not valid_span(4096, 4096, 1)
assert not valid_span(4096, U64_MAX - 3, 8)
assert not valid_span(4096, -1, 4)
base_a, base_b, offset = 0x10000000, 0x70000000, 128
assert base_a + offset != base_b + offset
offset=total、length=0 可以表示合法空区间,但不能据此解引用该位置。范围通过后还需验证布局版本、对齐与对象类型;如果读取头部期间另一个进程能改变长度或对象大小,校验结果也可能立即失效。校验和读取需要由协议保证同一版本,或通过不可变对象限制变化。
共享的是偏移,还是另一个进程的指针?
图中彩色区间只属于共享对象内部,两个进程的基址是各自的坐标原点。改变接收方基址不会改变对象内区间,却会改变接收方应使用的指针。offset=256、length=0 是边界处的空区间;不能把“范围检查通过”误读为这里有一个可访问字节。
0≤o≤T,0≤n≤T−o,pA=BA+o,pB=BB+o
正在呈现知识画面
共享的是偏移,还是另一个进程的指针?同一对象偏移在两个进程中可以得到不同地址。必须先验证 offset≤total 且 length≤total−offset;空区间不允许解引用。
{"id":"shared-offset-validation","title":"共享的是偏移,还是另一个进程的指针?","summary":"同一对象偏移在两个进程中可以得到不同地址。必须先验证 offset≤total 且 length≤total−offset;空区间不允许解引用。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"base\" data-i18n=\"base\"></label><output for=\"base\" id=\"base-value\"></output><input id=\"base\" type=\"range\" min=\"1\" max=\"8\" value=\"4\" step=\"1\"></div><div class=\"control\"><label for=\"offset\" data-i18n=\"offset\"></label><output for=\"offset\" id=\"offset-value\"></output><input id=\"offset\" type=\"range\" min=\"0\" max=\"272\" value=\"64\" step=\"1\"></div><div class=\"control\"><label for=\"length\" data-i18n=\"length\"></label><output for=\"length\" id=\"length-value\"></output><input id=\"length\" type=\"range\" min=\"0\" max=\"128\" value=\"32\" step=\"1\"></div><div class=\"track-row\"><span data-i18n=\"region\"></span><div id=\"region\" class=\"track\"></div></div><div class=\"metrics\"><div><span data-i18n=\"sender\"></span><strong id=\"sender\"></strong></div><div><span data-i18n=\"receiver\"></span><strong id=\"receiver\"></strong></div><div><span data-i18n=\"check\"></span><strong id=\"check\"></strong></div></div><p id=\"equation\" class=\"formula\"></p><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n#region{height:32px;background:var(--surface);position:relative}#region i{position:absolute;height:32px;background:var(--accent-soft);border:1px solid var(--accent)}.metrics strong{font-size:16px}#check{min-height:5em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}function draw(){const B=+$('#base').value*4096,o=+$('#offset').value,n=+$('#length').value,valid=o<=256&&n<=256-o,h=x=>'0x'+x.toString(16);set('base-value',h(B));set('offset-value',o);set('length-value',n);set('sender',valid?h(4096+o):'—');set('receiver',valid?h(B+o):'—');set('check',t(!valid?'invalid':n===0?'empty':'valid'));$('#check').dataset.valid=valid;$('#region').innerHTML=valid&&n?'<i style=\"left:'+o/256*100+'%;width:'+n/256*100+'%\"></i>':'';set('equation',o+' ≤ 256 ∧ '+n+' ≤ 256 − '+o+' : '+valid);}document.querySelectorAll('input').forEach(e=>e.oninput=draw);draw();","audio":false,"strings":{"base":"接收进程基址(页)","check":"范围结论","empty":"有效空区间,不可解引用","heading":"共享的是偏移,还是另一个进程的指针?","intro":"改变两边映射基址、记录偏移和长度,对照地址与范围检查。","invalid":"范围越界","length":"长度(B)","offset":"记录偏移(B)","receiver":"接收方地址","region":"共享对象:256 B","reset":"重置","scope":"共享区256字节,基址按4 KiB变化,数字精确;只查范围,不替代版本、对齐、同步或类型校验。","scopeLabel":"模型范围与约定","sender":"发送方地址","valid":"范围有效"}}
信号量:计数资源与发布事件
sem_wait 成功时取得一个计数令牌,计数为零则等待;sem_post 增加令牌并使等待者有机会继续。信号量没有普通互斥锁那样的固定所有者,生产者与消费者可以由不同线程或进程 post、wait。
sem_init(&sem, pshared, value) 中 pshared 非零表示跨进程共享,但 sem_t 必须已经位于各进程能访问的共享区域。它不会把栈上的 sem 变成共享内存,也不能在仍有使用者时重新初始化。sem_init(3)
完整例子:父进程发布,子进程读取
保存为 shared_demo.c,以 cc -std=c11 -Wall -Wextra -pthread shared_demo.c -o shared_demo 编译。程序用子进程退出状态检查发布结果,不依赖 stdio 缓冲输出。
#define _GNU_SOURCE
#include <errno.h>
#include <semaphore.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
struct Shared {
sem_t ready;
int value;
};
int main(void) {
struct Shared *s = mmap(NULL, sizeof *s, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (s == MAP_FAILED) return 1;
if (sem_init(&s->ready, 1, 0) == -1) {
munmap(s, sizeof *s);
return 1;
}
pid_t child = fork();
if (child == -1) {
sem_destroy(&s->ready);
munmap(s, sizeof *s);
return 1;
}
if (child == 0) {
while (sem_wait(&s->ready) == -1) {
if (errno != EINTR) _exit(2);
}
int result = s->value == 42 ? 0 : 3;
munmap(s, sizeof *s);
_exit(result);
}
s->value = 42;
if (sem_post(&s->ready) == -1) _exit(4);
int status;
while (waitpid(child, &status, 0) == -1) {
if (errno != EINTR) _exit(5);
}
int failed = !WIFEXITED(status) || WEXITSTATUS(status) != 0;
if (sem_destroy(&s->ready) == -1) failed = 1;
if (munmap(s, sizeof *s) == -1) failed = 1;
return failed;
}
初始化在 fork 前完成,初始计数为零;父进程先写数据再 post,子进程成功 wait 后读取。父进程等待子进程结束再销毁信号量,避免仍有使用者。sem_wait 的 EINTR 路径重试,其他错误不能当成已经取得令牌。sem_wait(3)
这是一次性交接示例,不是完整崩溃恢复协议。若生产者在 post 前死亡,消费者仍可能永久等待;循环缓冲区还需空槽、可读槽、序号和关闭状态。生产系统应定义超时、对端存活检测与废弃共享区的策略。
一次性 ready 不能直接变成循环发布协议
若父进程连续写 value=1、post,再写 value=2、post,而子进程尚未来得及读,两个令牌并不保存两个历史值。子进程可能两次都读到 2。信号量计数记住的是可取得多少次令牌,不是复制 payload。
单槽位重复交接可以使用 empty 与 full 两个信号量,分别表示可写槽和可读槽。生产者 wait(empty) 后写数据、post(full);消费者 wait(full) 后读数据、post(empty)。读者若把共享区指针交给另一个异步任务,在任务读完前不能归还 empty,否则下一次覆盖就与旧任务读取竞争。
| 协议状态 | 谁能访问 payload | 可以释放或覆盖吗 |
| empty 令牌尚未取得 | 没有已发布内容供消费 | 生产者先取得令牌 |
| 生产者持有槽位 | 生产者写 | 消费者不能提前读 |
| full 已发布 | 消费者取得令牌后读 | 生产者不能提前覆盖 |
| 消费者完成并归还 empty | 下一轮生产者 | 可以开始新一轮 |
N 个槽位的队列还需要正确的索引管理。多个生产者同时拿到两个 empty 令牌,并不能自动分配到不同槽位;索引推进仍须互斥或原子协议。异常退出也可能丢失令牌,需要与后面的恢复策略一起设计。
两个令牌,会保存两个历史值吗?
错误方案中连续发布 v1、v2,再消费两次,会读到两次 v2:令牌数没有丢,历史 payload 却被覆盖了。正确方案在 full 尚未消费时禁用再次发布,消费完成归还 empty 后才允许覆盖。真实多线程或多进程实现还必须用同步操作建立这些访问的顺序。
正在呈现知识画面
两个令牌,会保存两个历史值吗?信号量令牌不会复制 payload。单槽位循环交接需要可写权和可读权,防止生产者覆盖尚未消费的数据。
{"id":"shared-slot-handshake","title":"两个令牌,会保存两个历史值吗?","summary":"信号量令牌不会复制 payload。单槽位循环交接需要可写权和可读权,防止生产者覆盖尚未消费的数据。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"mode\"><button data-mode=\"broken\" data-i18n=\"broken\"></button><button data-mode=\"safe\" data-i18n=\"safe\"></button></div><div class=\"actions\"><button id=\"publish\" data-i18n=\"publish\"></button><button id=\"consume\" data-i18n=\"consume\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><section class=\"payload\"><span data-i18n=\"payload\"></span><strong id=\"payload\"></strong></section><div class=\"metrics\"><div><span data-i18n=\"tokens\"></span><strong id=\"tokens\"></strong></div><div><span data-i18n=\"empty\"></span><strong id=\"empty\"></strong></div></div><div class=\"track-row\"><span data-i18n=\"received\"></span><div id=\"received\" class=\"track\"></div></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n.payload{border:1px solid var(--accent);border-radius:8px;padding:15px;background:var(--accent-soft);margin:14px 0}.payload span{font-size:13px;display:block}.payload strong{display:block;font-size:24px;min-height:2em}#received{display:grid;grid-template-columns:repeat(6,minmax(0,1fr))}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}let mode='broken',value=0,tokens=0,received=[];function draw(){pressed(mode);set('payload',value?'v'+value:t('none'));set('tokens',tokens);set('empty',mode==='safe'?Number(tokens===0):'—');cells('received',Array.from({length:6},(_,i)=>({text:received[i]?'v'+received[i]:'—',cls:received[i]?'gold':'empty'})));$('#publish').disabled=value===6||(mode==='safe'&&tokens>0);$('#consume').disabled=tokens===0;$('#received').dataset.values=JSON.stringify(received);}function reset(){value=tokens=0;received=[];draw()}$('#publish').onclick=()=>{value++;tokens++;draw()};$('#consume').onclick=()=>{tokens--;received.push(value);draw()};document.querySelectorAll('[data-mode]').forEach(e=>e.onclick=()=>{mode=e.dataset.mode;reset()});$('#reset').onclick=reset;draw();","audio":false,"strings":{"broken":"只有 full 计数","consume":"取得令牌并读取","empty":"empty 令牌数","heading":"两个令牌,会保存两个历史值吗?","intro":"让生产者连续发布,再由消费者读取;对比只计数与单槽位交接。","mode":"交接协议","none":"尚未发布","payload":"唯一共享 payload","publish":"发布下一版本","received":"消费者实际读到的版本","reset":"重置","safe":"empty / full 双向交接","scope":"单生产者/单消费者,每次操作完整执行且互不并发,用于隔离“令牌不保存历史”的问题。错误方案不作为合法并发 C 实现;最多6次发布。","scopeLabel":"模型范围与约定","tokens":"full 令牌数"}}
命名信号量与删除
sem_open 创建或打开命名信号量;sem_close 释放本进程引用,sem_unlink 删除名字。unlink 不应理解为立即销毁仍被其他进程打开的对象,与共享内存名称管理类似。
名称删除和数据可访问性是不同层次。安全清理常由创建者负责,但必须明确后来者如何发现对象、什么时候不再允许附加,以及旧版本对象是否仍有使用者。把资源删除责任写进协议比“程序退出时清理一下”可靠。
memfd 与 sealing:限制对象能发生的变化
memfd_create 返回匿名内存文件 fd,可 ftruncate、mmap,并通过 Unix socket 传给另一进程。MFD_CLOEXEC 控制 exec 继承;如需后续加 seals,创建时通常还应设置 MFD_ALLOW_SEALING。
F_SEAL_SHRINK 防缩小,F_SEAL_GROW 防扩大,它们不禁止改写现有字节。F_SEAL_WRITE 与可写共享映射存在约束;F_SEAL_FUTURE_WRITE 的语义又不同,已有可写映射需要特别考虑。必须根据需要保证的属性选 seal,不能把所有 seal 都叫“只读”。memfd_create(2)
例如消费者要信任对象大小不在检查后变化,可要求相应长度 seal;要信任内容不可变,还需检查写入限制。memfd 是一种构件,不意味着 systemd 或其他程序的所有 IPC 都基于它。
用同一个 memfd 分别检查大小与内容限制
保存为 seal-demo.c。创建后写入 abc,先禁止缩小和扩大,此时改写已有第一个字节仍成功;随后增加 F_SEAL_WRITE,再次改写被拒绝。本例没有建立可写共享 mmap,所以不会触发“仍有可写映射导致加写 seal 失败”的条件。
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>
int main(void) {
int fd = memfd_create("seal-demo", MFD_CLOEXEC | MFD_ALLOW_SEALING);
if (fd == -1) return 1;
int failed = 1;
if (write(fd, "abc", 3) != 3) goto done;
int size_seals = F_SEAL_SHRINK | F_SEAL_GROW;
if (fcntl(fd, F_ADD_SEALS, size_seals) == -1) goto done;
if (pwrite(fd, "X", 1, 0) != 1) goto done;
errno = 0;
if (ftruncate(fd, 2) != -1 || errno != EPERM) goto done;
if (fcntl(fd, F_ADD_SEALS, F_SEAL_WRITE) == -1) goto done;
errno = 0;
if (pwrite(fd, "Y", 1, 0) != -1 || errno != EPERM) goto done;
char data[3];
if (pread(fd, data, sizeof data, 0) != sizeof data) goto done;
if (data[0] != 'X' || data[1] != 'b' || data[2] != 'c') goto done;
int seals = fcntl(fd, F_GET_SEALS);
if (seals == -1 || (seals & (size_seals | F_SEAL_WRITE)) !=
(size_seals | F_SEAL_WRITE)) goto done;
puts("size fixed; overwrite first allowed, then denied; data=Xbc");
failed = 0;
done:
if (close(fd) == -1) failed = 1;
return failed;
}
使用 cc -Wall -Wextra seal-demo.c -o seal-demo 编译运行。读取端拿到 fd 后,可以 F_GET_SEALS 检查对象当前具备哪些限制;这些限制作用于底层对象,不只是发送方的某个 fd 数字。文件 sealing
F_SEAL_SEAL 控制之后是否还能增加 seal,并不替代 F_SEAL_WRITE。F_SEAL_FUTURE_WRITE 允许已有可写共享映射继续修改,因此不能用它单独证明内容已经不可变。需要发布只读快照时,应完成写入、处理已有映射,再建立并检查所要求的内容与长度限制。
互斥与崩溃恢复不能只靠信号量计数
进程拿走信号量令牌后死亡,不会由 POSIX 信号量自动归还。若关键数据结构要求识别持锁者死亡,可考虑位于共享区、同时设置 PTHREAD_PROCESS_SHARED 与 robust 属性的互斥锁,并设计 EOWNERDEAD 数据修复路径。机制见 futex。
修复可能需要操作日志、版本状态或丢弃整个共享区。锁重新可获取只是恢复第一步,不意味着中途更新的长度、索引与数据已经一致。
SysV 共享内存仍有明确用途,不能仅因接口较早就一律排除。IPC_PRIVATE 表示创建新对象,不是基于名字的访问保护;权限、附加和删除行为应分别处理。
文件锁:先分清所有权模型
传统 fcntl record lock 可对文件字节范围加协作式锁。F_SETLK 不等待,F_SETLKW 可等待;返回错误需检查 errno,不能把所有失败都当成“另一个进程持有锁”。
传统记录锁与进程关联,关闭本进程对同一文件的其他 fd 也可能释放该进程在文件上的锁;OFD locks 则与打开文件描述关联,dup 和 fork 后的引用会影响最终释放。两者不应统称“close 当前 fd 就释放”的同一种机制。fcntl 锁
文件锁通常是 advisory,参与者要共同遵守。删除锁文件后重建可能得到新 inode,使新旧进程锁住不同对象;守护进程单实例设计需保证大家使用同一稳定对象。
性能与持久性
共享内存减少传输复制,但缓存行争用、伪共享、NUMA 和通知仍有成本,不保证对小消息一定比 pipe 快。适合先衡量数据规模、批处理、读写比例和背压需求。
映射写入何时被其他进程正确读取,与何时持久化到介质不同。文件支持的映射还要考虑 msync、文件系统和故障模型;信号量 post 不会自动使数据掉电可恢复。
关键词:shared memory, MAP_SHARED, shm_open, semaphore, sem_init, memfd, sealing, process shared, robust mutex, file locking, lifecycle