信号既可以表达终止、暂停等进程控制,也可以通知某个线程发生事件或异常。正确使用的关键是:信号发给谁、在哪个线程递送、何时被阻塞,以及 handler 能安全地做什么。
处置、屏蔽与 pending
信号处置(默认、忽略、调用 handler)是进程共享属性,屏蔽集合 mask 则是每线程属性。向进程发送的信号可以递送给一个允许它的线程;硬件异常或 pthread_kill 等产生的线程定向信号则有指定目标。
阻塞不是忽略:信号可能进入 pending,待解除阻塞或被同步等待接口消费。SIGKILL 和 SIGSTOP 不能捕获、忽略或阻塞。多线程程序使用 pthread_sigmask 协调屏蔽;新线程继承创建者的 mask,不会因为另一个线程后来修改了 mask 就自动一起改变。signal(7)
普通信号通常不按每次发送计数,同一信号 pending 期间多次产生可合并。把 SIGUSR1 当成无损任务队列是不可靠的设计,真正任务数应保存在队列或计数器中,信号只负责提示检查。
发送次数为什么不等于处理次数?
比较的关键条件是信号是否仍在 pending。保持阻塞连发三次,普通信号只留下一个实例;若每发一次就让 handler 消费一次,则可以处理三次。实时信号保留的是排队实例,也必须检查发送是否成功。
正在呈现知识画面 重新加载
发送次数为什么不等于处理次数? 普通信号在 pending 期间合并;实时信号可排队但有资源上限。解除屏蔽不为普通信号恢复已经合并的次数。
{"id":"signal-pending-coalescing","title":"发送次数为什么不等于处理次数?","summary":"普通信号在 pending 期间合并;实时信号可排队但有资源上限。解除屏蔽不为普通信号恢复已经合并的次数。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"kind\"><button data-mode=\"standard\" data-i18n=\"standard\"></button><button data-mode=\"rt\" data-i18n=\"rt\"></button></div><label class=\"check\"><input type=\"checkbox\" id=\"blocked\" checked><span data-i18n=\"blocked\"></span></label><div class=\"actions\"><button id=\"send\" data-i18n=\"send\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><div class=\"track-row\"><span data-i18n=\"pending\"></span><div id=\"pending\" class=\"track\"></div></div><div class=\"metrics\"><div><span data-i18n=\"sent\"></span><strong id=\"sent\"></strong></div><div><span data-i18n=\"handled\"></span><strong id=\"handled\"></strong></div><div><span data-i18n=\"failed\"></span><strong id=\"failed\"></strong></div><div><span data-i18n=\"merged\"></span><strong id=\"merged\"></strong></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#pending{display:grid;grid-template-columns:repeat(4,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='standard',queue=[],sent=0,handled=0,failed=0,merged=0;function drain(){if(!$('#blocked').checked){handled+=queue.length;queue=[]}}function draw(){pressed(mode);cells('pending',Array.from({length:4},(_,i)=>({text:queue[i]?'#'+queue[i]:'—',cls:queue[i]?'on':'empty'})));for(const [k,v] of Object.entries({sent,handled,failed,merged}))set(k,v);$('#pending').dataset.count=queue.length;}function reset(){queue=[];sent=handled=failed=merged=0;$('#blocked').checked=true;draw()}$('#send').onclick=()=>{sent++;if(mode==='standard'&&queue.length)merged++;else if(queue.length===4)failed++;else queue.push(sent);drain();draw()};$('#blocked').onchange=()=>{drain();draw()};document.querySelectorAll('[data-mode]').forEach(e=>e.onclick=()=>{mode=e.dataset.mode;reset()});$('#reset').onclick=reset;draw();","audio":false,"strings":{"blocked":"阻塞递送","failed":"队列满拒绝","handled":"handler 次数","heading":"发送次数为什么不等于处理次数?","intro":"保持屏蔽后连发三次,再解除屏蔽;对比同编号实时信号。","kind":"信号类型","merged":"合并的发送","pending":"待递送实例","reset":"重置","rt":"同编号实时信号","scope":"单线程,handler 已安装。实时队列教学容量4,满时拒绝;解除屏蔽后模型立即处理全部可递送项,不表示真实调度延迟。","scopeLabel":"模型范围与约定","send":"发送一次","sent":"发送请求","standard":"普通信号"}}
sigaction 与执行上下文
sigaction 明确指定 handler、执行期间额外屏蔽集合和行为标志。sigset_t 应通过 sigemptyset/sigaddset 等接口初始化,不能假设它是可直接赋值为整数的可移植字段。
标志 含义
SA_SIGINFO 使用带 siginfo_t 与上下文参数的 handler
SA_RESTART 对支持重启的接口请求相应行为,不涵盖所有系统调用
SA_NODEFER 不因正在处理就自动屏蔽同种信号,允许更复杂的重入
SA_RESETHAND 进入处理时恢复相应默认处置,涉及后续递送行为
SA_ONSTACK 若该线程有可用备用栈,使用它执行处理函数
handler 在被中断线程中执行,返回时恢复保存的上下文
正常执行
线程当前寄存器与栈
执行 handler
内核先构造用户态 signal frame
恢复执行
trampoline → rt_sigreturn
可按线程配置备用信号栈;处理函数不是新线程,不能假定被中断的库状态可重入。
signal frame 保存恢复执行所需的寄存器、mask 等上下文,其具体布局由架构与 ABI 决定。handler 正常返回后经用户态 trampoline 调用相关 sigreturn 路径。不要把某个 x86-64 栈布局当成 arm64 的固定结构,也不要直接像普通函数一样调用 sigreturn。
异步信号安全比线程安全更严格
handler 可能在 malloc、printf 或普通锁正修改内部状态时打断当前线程,再次调用相同设施会遇到不一致或自死锁。普通“线程安全”保证不能替代 async-signal-safe 保证。
一种常见设计是 handler 只设置 volatile sig_atomic_t 标志,由正常控制流处理任务。该类型用于受限的信号交互,不是任意多线程共享协议的替代品;复杂计数、自增和多个字段的一致性仍需单独分析。
需要唤醒事件循环时,可使用预先建立的非阻塞 self-pipe,并在 handler 中调用允许的 write;管道满时必须接受通知合并且在正常路径重查状态。write 虽属异步安全接口,阻塞 fd 上仍可能等待;安全不等于没有延迟。使用 errno 的 handler 应保存并恢复它。signal-safety(7)
用 sigsuspend 避免检查与等待之间的竞态
while (!seen) pause() 有窗口:检查之后信号已经处理,随后 pause 却继续等下一次。下面的单线程例子先阻塞信号,再用 sigsuspend 原子替换 mask 并等待。
#define _POSIX_C_SOURCE 200809L
#include < errno.h>
#include < signal.h>
#include < unistd.h>
static volatile sig_atomic_t seen;
static void on_signal ( int signo ) {
( void ) signo;
seen = 1 ;
}
int main ( void ) {
sigset_t blocked, previous, waiting;
sigemptyset ( & blocked) ;
sigaddset ( & blocked, SIGUSR1) ;
if ( sigprocmask ( SIG_BLOCK, & blocked, & previous ) == - 1 ) return 1 ;
struct sigaction action = { 0 } ;
action. sa_handler = on_signal;
sigemptyset ( & action. sa_mask ) ;
if ( sigaction ( SIGUSR1, & action, NULL ) == - 1 ) return 1 ;
if ( raise ( SIGUSR1 ) != 0 ) return 1 ; waiting = previous;
sigdelset ( & waiting, SIGUSR1) ;
while ( ! seen) {
if ( sigsuspend ( & waiting) == - 1 && errno != EINTR) return 1 ;
}
if ( sigprocmask ( SIG_SETMASK, & previous, NULL ) == - 1 ) return 1 ;
return 0 ;
}
等待时允许 SIGUSR1 递送,handler 返回后 sigsuspend 恢复原来的阻塞状态,并以 EINTR 返回。循环重新检查 seen,可应对其他信号唤醒。示例自发信号只是让流程可重复,实际程序可以由其他授权来源通知。sigsuspend(2)
pselect、ppoll、epoll_pwait 也能在等待 I/O 时临时替换 mask,解决类似窗口,而不是先手动解除阻塞再调用普通 wait。
先阻塞再检查,原子解除阻塞并等待
SIGUSR1 已阻塞
检查 seen == 0
信号在此时到达
进入 pending,尚未执行 handler
sigsuspend
临时解除阻塞并等待
pending 被递送 → handler 设置 seen → 等待返回 EINTR
恢复阻塞 mask 后重新检查 seen;不会先消费通知再无条件睡眠
修复的关键是 mask 与等待的原子切换,不是把 pause 换成一个更短的轮询间隔。
把错误时序与正确时序逐步对照
时刻 先检查再 pause 阻塞 + sigsuspend
1 读到 seen=0 在信号阻塞时读到 seen=0
2 handler 运行,写 seen=1 信号到达,只记为 pending
3 pause 开始等下一次信号 原子换 mask,递送已有 pending
4 若没有新信号,继续睡眠 handler 设置 seen,返回后重新检查
“handler 已经把标志设为 1”不代表正在进入的等待会自动看到它。等待系统调用并不读取应用的 seen 变量。这个例子也说明,通知和被等待的条件是两件事:循环负责确认条件,原子等待负责避免错过条件变化对应的通知。
标志已经是1,线程为什么还会睡?
尝试“检查→发送→等待”:错误方案里 seen 已经为 1,线程却等待下一次信号;正确方案里信号先留在 pending,进入 sigsuspend 时被递送。再尝试“检查→等待→发送”,两种方案都能返回,说明只验证正常顺序不足以检查等待协议。
正在呈现知识画面 重新加载
标志已经是1,线程为什么还会睡? pause 不读取应用标志。信号若在检查后、等待前处理,就可能丢失唤醒;阻塞后用 sigsuspend 原子换 mask 可保留 pending 通知。
{"id":"signal-check-sleep-gap","title":"标志已经是1,线程为什么还会睡?","summary":"pause 不读取应用标志。信号若在检查后、等待前处理,就可能丢失唤醒;阻塞后用 sigsuspend 原子换 mask 可保留 pending 通知。","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=\"naive\" data-i18n=\"naive\"></button><button data-mode=\"safe\" data-i18n=\"safe\"></button></div><div class=\"actions\"><button id=\"check\" data-i18n=\"check\"></button><button id=\"send\" data-i18n=\"send\"></button><button id=\"wait\" data-i18n=\"wait\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><div class=\"metrics\"><div><span data-i18n=\"flag\"></span><strong id=\"flag\"></strong></div><div><span data-i18n=\"pending\"></span><strong id=\"pending\"></strong></div><div><span data-i18n=\"checked\"></span><strong id=\"checked\"></strong></div><div><span data-i18n=\"thread\"></span><strong id=\"thread\"></strong></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.metrics strong{font-size:17px}#thread{min-height:4em}","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='naive',seen=0,pending=0,checked=null,state='run';function draw(){pressed(mode);set('flag',seen);set('pending',pending);set('checked',checked??'—');set('thread',t(state));$('#thread').dataset.state=state;$('#check').disabled=state==='sleep';$('#wait').disabled=checked!==0||state!=='run';}function reset(){seen=pending=0;checked=null;state='run';draw()}$('#check').onclick=()=>{checked=seen;state=seen?'skip':'run';draw()};$('#send').onclick=()=>{if(mode==='safe'&&state!=='sleep')pending=1;else {seen=1;if(state==='sleep')state='returned';}draw()};$('#wait').onclick=()=>{if(mode==='safe'&&pending){pending=0;seen=1;state='returned'}else state='sleep';draw()};document.querySelectorAll('[data-mode]').forEach(e=>e.onclick=()=>{mode=e.dataset.mode;reset()});$('#reset').onclick=reset;draw();","audio":false,"strings":{"check":"检查 seen","checked":"上次检查结果","flag":"seen","heading":"标志已经是1,线程为什么还会睡?","intro":"自由安排“检查、发送、等待”的顺序,对比 pause 与 sigsuspend。","mode":"等待协议","naive":"检查 + pause","pending":"pending","reset":"重置","returned":"等待返回,需重查","run":"可运行","safe":"阻塞 + sigsuspend","scope":"单线程、单个普通信号,无其他信号与重入。操作展示一个循环迭代;返回后仍需重新检查条件。","scopeLabel":"模型范围与约定","send":"发送信号","skip":"条件成立,无需等待","sleep":"等待新信号","thread":"线程状态","wait":"进入等待"}}
SA_RESTART 与 EINTR
Linux 上某些慢设备 read/write、wait 系列以及满足条件的 socket 操作可受 SA_RESTART 影响;poll、select、epoll_wait、pause、sigsuspend 等不会因为设置该标志就自动重启。socket 超时选项等还会改变具体行为,不能只靠函数名做一张无条件二分表。
一次 read/write 已传输部分数据时,可能返回正的短计数,应用应保留进展,而不是从头重复发送整条消息。超时等待若使用相对时长,EINTR 后重试也需扣除已经过去的时间。
SIGSEGV 等同步故障的 handler 返回后,若原因未消除,可能再次执行同一故障指令。安装 handler 不会把任意内存错误变成可恢复异常。
备用信号栈是每线程资源
sigaltstack 注册一块仍有效的内存,SA_ONSTACK 让相应 handler 使用它。这样正常栈已经不足时仍可能有空间记录最低限度信息,但备用栈本身不会无限增长,也不能让 malloc、复杂栈展开或日志库突然变成异步安全。
大小需考虑架构扩展状态、嵌套信号和 handler 需求,不能硬编码“通常 8 KiB 就够”。在每个相关线程建立并正确管理生命周期;替换或释放时不得仍在使用。sigaltstack(2)
同步消费:sigwait 与 signalfd
专用信号线程可阻塞目标信号,再用 sigwaitinfo/sigwait 同步接收;正常线程上下文中的后续处理不受异步 handler 的同一限制。Linux signalfd 则把信号记录接入 fd 事件循环。
应在所有相关线程建立一致的路由策略,避免一部分信号跑去传统 handler。signalfd 也不能把另一个线程专属的硬件故障任意搬到管理线程。配置示例见 epoll 与事件驱动 。
普通信号合并,可以在一个进程内确定复现
下面先阻塞 SIGUSR1,连续向自己发送三次,再同步消费。三次发生时它都尚未递送,所以 pending 只保留一个普通信号实例。使用零超时 sigtimedwait 检查是否还有第二个,而不是再无期限等待。
#define _POSIX_C_SOURCE 200809L
#include < errno.h>
#include < signal.h>
#include < stdio.h>
#include < time.h>
#include < unistd.h>
int main ( void ) {
sigset_t set, old;
sigemptyset ( & set) ;
sigaddset ( & set, SIGUSR1) ;
if ( sigprocmask ( SIG_BLOCK, & set, & old ) == - 1 ) return 1 ;
for ( int i = 0 ; i < 3 ; + + i)
if ( kill ( getpid ( ) , SIGUSR1) == - 1 ) return 1 ;
struct timespec zero = { 0 , 0 } ;
siginfo_t info;
int first = sigtimedwait ( & set, & info, & zero) ;
errno = 0 ;
int second = sigtimedwait ( & set, & info, & zero) ;
int saved = errno;
if ( sigprocmask ( SIG_SETMASK, & old, NULL ) == - 1 ) return 1 ;
printf ( " one received=%d ; next empty=%d \n " ,
first == SIGUSR1, second == - 1 && saved == EAGAIN) ;
return ! ( first == SIGUSR1 && second == - 1 && saved == EAGAIN) ;
}
保存为 pending-demo.c,编译运行预期 one received=1; next empty=1。这个单线程、自发信号实验不需要安装 handler;sigwaitinfo/sigtimedwait 在信号保持阻塞时同步取走它。零超时 EAGAIN 表示此刻没有可接收的目标信号。sigtimedwait(2)
不能把这个结果泛化为“无论何时发送三次都只处理一次”。如果每次发送之间前一个已经被消费,就可能分别观察到三次。决定合并的是 pending 期间的行为;需要可靠任务数量时,应读取真正的任务队列。
实时信号与 timerfd
实时信号可携带 sigval 数据并排队,但队列受资源限制,sigqueue 可以失败,不能承诺永不丢失。相同实时信号的排队顺序与不同信号编号的递送优先级有规定,不等于所有编号按统一发送时间 FIFO。SIGRTMIN 还可能由线程库调整,应使用接口宏并核对范围。
timerfd 通知不需要 signal handler。read 成功返回的通常是 sizeof(uint64_t) 字节,读出的整数才是累计到期次数,可能大于 1;不要把“返回值为 8”误读成到期 8 次。单调时钟适合测时间间隔,是否包含系统挂起还取决于选择的时钟。
重试等待要保留总时间预算
假设等待预算是 100 ms,第 30 ms 被信号打断,稍后又消耗 40 ms。如果每次 EINTR 都重新等待 100 ms,总等待就被延长;连续信号甚至可能不断把完成时间向后推。应用应以选定的单调时钟计算一个固定 deadline,再按当前时间计算剩余预算。
deadline_ms = 100
def remaining ( now_ms ) :
return max ( 0 , deadline_ms - now_ms )
assert remaining ( 0 ) == 100
assert remaining ( 30 ) == 70
assert remaining ( 70 ) == 30
assert remaining ( 105 ) == 0
这只是整数时间模型;实际 C 程序要处理 timespec 的秒/纳秒归一化、时钟读取错误及具体等待接口的超时规则。SA_RESTART 不能替你维护应用层 deadline。即使某个调用能够重启,也不应据此混淆“单次阻塞时长”与“整项操作允许的总时长”。
调试时观察什么
strace - e trace=rt_sigaction,rt_sigprocmask,rt_sigsuspend - e signal=all ./prog
/proc/PID/status 的 SigBlk、SigIgn、SigCgt 描述屏蔽与处置,SigPnd 和 ShdPnd 区分线程与进程 pending;多线程问题还需查看对应 task 的状态。
先确认信号目标、mask 和 pending,再分析 handler 是否执行和等待是否重启。不要用信号频率直接估算事件发生次数,也不要把一次 handler 成功当作整个程序的同步协议已经正确。
关键词:signal, sigaction, signal mask, pending, async-signal-safe, sigsuspend, SA_RESTART, sigaltstack, signalfd, real-time signal