kill() 返回成功,处理函数却还没执行;几个子进程同时退出,父进程只收到一次 SIGCHLD。理解这些现象,需要把事件产生、待决状态、实际递送分开。
ptrace 再增加一层停止协议:跟踪者可以在特定位置观察目标,但必须识别停止原因,才能正确恢复它。
| 你遇到的现象 | 阅读位置 |
| 信号发出后没有反应,或只有某个线程接收 | 第 1 节:待决与线程选择 |
| 发送次数与处理次数不同 | 第 2 节:合并、排队和实验 |
| handler 卡住,或等待超时不断延长 | 第 3~4 节:异步进入与 EINTR |
| 调试器恢复后目标异常退出 | 第 5~6 节:停止分类与访问边界 |
1. 产生、待决、递送,是三个不同的阶段
发送接口成功,通常说明信号请求通过检查并被内核接受,不是接收者已执行处理函数的确认。接收线程可能正在屏蔽该信号、尚未被调度,或处于暂时无法响应的内核路径。若业务需要确认某项操作完成,应设计显式应答,而非把 kill() 的返回值当作完成通知。
先确认状态属于谁:
| 状态 | 所属范围 | 意义 |
| 处置:默认、忽略、处理函数 | 通常在线程组内共享 | 收到后怎样处理 |
| 屏蔽字 | 每线程独立 | 该线程现在是否接收 |
| 线程定向待决信号 | 指定线程 | 等目标线程能够接收 |
| 进程定向待决信号 | 可保存在组共享状态中 | 从合适线程中选择接收者 |
kill(pid, sig) 通常向进程发送;pthread_kill() 指定线程。目标线程屏蔽信号时,线程定向信号不能改交别人。
读上图时,先看 A 的屏蔽字,再看 B、C 的接收资格:A 屏蔽 SIGUSR1,B 或 C 可以被选中,但不会两者各执行一次。若三个线程都屏蔽,信号便保持待决,直到有合适的递送机会。
改变发送目标与屏蔽字,再在线程上尝试递送。共享与私有 pending 分别显示;手动选线程用于比较可能的接收者,不承诺内核的选择顺序。
正在呈现知识画面
发给进程,还是指定线程?进程定向信号可以由一个合适线程接收;线程定向信号只能留给指定线程。处置通常共享,屏蔽字与私有 pending 各有归属。
{"id":"signal-routing","title":"发给进程,还是指定线程?","summary":"进程定向信号可以由一个合适线程接收;线程定向信号只能留给指定线程。处置通常共享,屏蔽字与私有 pending 各有归属。","height":1000,"html":"<p class=\"eyebrow\" data-i18n=\"eyebrow\"></p><h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"target\"><button data-target=\"process\" data-i18n=\"process\"></button><button data-target=\"thread\" data-i18n=\"threadA\"></button></div><div class=\"actions\"><button id=\"send\" data-i18n=\"send\"></button></div><div class=\"pending-group\"><span data-i18n=\"groupPending\"></span><strong id=\"group-token\">—</strong></div><div class=\"routes\" aria-hidden=\"true\"><i id=\"route-A\"></i><i id=\"route-B\"></i><i id=\"route-C\"></i></div><div class=\"threads\"><section class=\"card\" id=\"thread-A\"><header>A</header><label class=\"check\"><input type=\"checkbox\" data-block=\"A\" checked><span data-i18n=\"blocked\"></span></label><div class=\"private\"><small data-i18n=\"privatePending\"></small><strong id=\"private-A\">—</strong></div><button data-deliver=\"A\" data-i18n=\"deliver\"></button><div class=\"handled\"><small data-i18n=\"handlerCount\"></small><strong id=\"handled-A\">0</strong></div></section><section class=\"card\" id=\"thread-B\"><header>B</header><label class=\"check\"><input type=\"checkbox\" data-block=\"B\" ><span data-i18n=\"blocked\"></span></label><div class=\"private\"><small data-i18n=\"privatePending\"></small><strong id=\"private-B\">—</strong></div><button data-deliver=\"B\" data-i18n=\"deliver\"></button><div class=\"handled\"><small data-i18n=\"handlerCount\"></small><strong id=\"handled-B\">0</strong></div></section><section class=\"card\" id=\"thread-C\"><header>C</header><label class=\"check\"><input type=\"checkbox\" data-block=\"C\" ><span data-i18n=\"blocked\"></span></label><div class=\"private\"><small data-i18n=\"privatePending\"></small><strong id=\"private-C\">—</strong></div><button data-deliver=\"C\" data-i18n=\"deliver\"></button><div class=\"handled\"><small data-i18n=\"handlerCount\"></small><strong id=\"handled-C\">0</strong></div></section></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"initial\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"queuedGroup\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"queuedThread\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"blockedNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"deliveredNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"emptyNote\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scope\"></summary><p class=\"note\" data-i18n=\"note\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.note,.caption{font-size:12px;color:var(--muted);margin-top:14px}.controls{display:grid;gap:12px;margin:18px 0}.control{display:grid;grid-template-columns:1fr auto;gap:6px 12px;align-items:center;font-size:13px}.control input{grid-column:1/3;width:100%;margin:0}.control output{color:var(--accent);font-variant-numeric:tabular-nums}.presets{display:flex;flex-wrap:wrap;gap:4px}.presets button{flex:1 1 auto;min-width:min-content}.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px}.actions button{flex:1;min-width:0;min-height:42px;font-size:13px}.copy{display:grid;margin-top:16px;font-size:14px}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.metrics{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:16px 0}.metrics>div{min-width:0;border-left:2px solid var(--accent);padding-left:12px}.metrics span{display:block;color:var(--muted);font-size:12px;min-height:2.8em}.metrics strong{display:block;font-size:22px;font-weight:500;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.formula{font-size:13px;overflow-wrap:anywhere;border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-variant-numeric:tabular-nums}.formula div+div{margin-top:6px}.two{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:18px}.card{min-width:0;border:1px solid var(--rule);border-radius:9px;padding:14px}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card code{font-size:12px;overflow-wrap:anywhere}.card .value{font-size:24px;font-weight:500;font-variant-numeric:tabular-nums}.card small{display:block;color:var(--muted);font-size:12px}.card button{width:100%;min-width:0;font-size:13px;min-height:42px}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{accent-color:var(--accent);width:16px;height:16px;flex-shrink:0}.pill{display:inline-block;padding:3px 8px;border-radius:5px;background:var(--accent-soft);color:var(--accent);font-size:12px}.plot{position:relative;margin:18px 0}.plot svg{width:100%;height:auto;overflow:visible}.plot text{font-size:12px;fill:var(--muted)}.plot .ink{fill:var(--ink)}.plot .blue{fill:var(--accent);stroke:var(--accent)}.plot .gold{fill:var(--second);stroke:var(--second)}.plot .area{fill:var(--accent-soft);stroke:var(--accent);transition:all .25s}.plot .other{fill:var(--second-soft);stroke:var(--second)}.plot line,.plot path,.plot rect,.plot circle{transition:all .25s}.legend{display:flex;flex-wrap:wrap;gap:6px 16px;font-size:12px;color:var(--muted);margin-top:10px}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:5px}.legend .gold:before{background:var(--second)}details{margin-top:14px;font-size:12px;color:var(--muted)}summary{cursor:pointer;padding:8px 0}button:disabled{cursor:default}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.card button{font-size:12px}.metrics{gap:10px}.metrics strong{font-size:20px}.actions button{font-size:12px}.plot text{font-size:13px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n\n.pending-group{border:1px solid var(--accent);background:var(--accent-soft);border-radius:9px;padding:14px;margin-top:18px;display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;font-size:13px}.pending-group strong{font:600 16px var(--viz-font-ui)}.pending-group button{font-size:12px}.routes{display:grid;grid-template-columns:repeat(3,1fr);height:35px}.routes i{width:1px;justify-self:center;border-left:1px solid var(--rule)}.routes i.available{border-color:var(--accent);border-width:2px}.threads{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.threads .card{padding:12px 9px;text-align:center}.threads .check{font-size:12px;justify-content:center;min-height:3.4em;flex-wrap:wrap;gap:4px}.private{min-height:7em;border:1px dashed var(--rule);border-radius:6px;padding:8px 3px;margin:12px 0;font-size:11px}.private strong{display:block;font:500 12px var(--viz-font-ui);margin-top:8px}.private.pending{background:var(--second-soft);border-color:var(--second)}.handled{margin-top:12px}.handled strong{display:block;font-size:22px;margin-top:5px}.handled small{min-height:3.4em}.threads button{font-size:12px;padding-inline:3px;min-height:4.2em}.threads .card.delivered{border-color:var(--accent);background:var(--accent-soft)}@media(max-width:480px){.threads{gap:6px}.threads .card{padding:10px 5px}.private{min-height:8.5em}}","js":"const $=s=>document.querySelector(s);let target='process',group=false,local={A:false,B:false,C:false},handled={A:0,B:0,C:0},message='initial',last='';\nfunction draw(){$('#group-token').textContent=group?'SIGUSR1':'—';for(const k of 'ABC'){const blocked=document.querySelector('[data-block=\"'+k+'\"]').checked;$('#private-'+k).textContent=local[k]?'SIGUSR1':'—';$('#private-'+k).parentElement.classList.toggle('pending',local[k]);$('#handled-'+k).textContent=handled[k];$('#route-'+k).classList.toggle('available',group&&!blocked);$('#thread-'+k).classList.toggle('delivered',last===k);}document.querySelectorAll('[data-target]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.target===target)));$('#explanation').textContent=viz.t(message);}\ndocument.querySelectorAll('[data-target]').forEach(e=>e.addEventListener('click',()=>{target=e.dataset.target;draw();}));document.querySelectorAll('[data-block]').forEach(e=>e.addEventListener('change',()=>{last='';message='initial';draw();}));$('#send').addEventListener('click',()=>{if(target==='process'){group=true;message='queuedGroup';}else{local.A=true;message='queuedThread';}last='';draw();});document.querySelectorAll('[data-deliver]').forEach(e=>e.addEventListener('click',()=>{const k=e.dataset.deliver;if(document.querySelector('[data-block=\"'+k+'\"]').checked)message='blockedNote';else if(local[k]||group){if(local[k])local[k]=false;else group=false;handled[k]++;last=k;message='deliveredNote';}else{last='';message='emptyNote';}draw();}));$('#reset').addEventListener('click',()=>{group=false;local={A:false,B:false,C:false};handled={A:0,B:0,C:0};message='initial';last='';draw();});draw();\n","audio":false,"strings":{"blocked":"屏蔽 SIGUSR1","blockedNote":"该线程屏蔽了 SIGUSR1,不能在这里递送。pending 继续保留。","deliver":"在此线程尝试递送","deliveredNote":"一个 pending 已被取走,只在选中的线程上执行处理函数。","emptyNote":"此线程没有可以领取的信号,私有 pending 不能跨线程转移。","eyebrow":"01 · 信号的归属","groupPending":"进程共享 pending","handlerCount":"处理函数执行次数","heading":"发给进程,还是指定线程?","initial":"A 默认屏蔽此信号。进程信号可由 B 或 C 接收;指定 A 的信号则只能留给 A。","intro":"先选择发送目标,再改变各线程的屏蔽字。在线程上触发一次递送机会,看哪一个 pending 被取走。","note":"各线程已安装同一处理函数。点击线程表示一次可能的递送机会,不承诺内核按点击顺序选线程。图将取消屏蔽与递送分开,真实系统可能在取消屏蔽后立即递送。重复普通信号会合并;同时存在私有与共享 pending 时,本图先取私有项。","privatePending":"线程私有 pending","process":"整个进程","queuedGroup":"信号进入进程共享 pending。它只会递送给一个合适线程,不会广播给每个线程。","queuedThread":"信号进入 A 的私有 pending。即使 B、C 没有屏蔽,也不能替 A 领取。","reset":"清空信号与计数","scope":"模型范围与约定","send":"发送 SIGUSR1","target":"发送目标","threadA":"仅线程 A"}}
要集中处理信号,应在创建工作线程前先屏蔽目标集合,让新线程继承屏蔽字,再由专门线程同步消费。这样接收位置由程序安排,不必依赖某次恰好选中了哪个线程。
SIGKILL 和 SIGSTOP 不能安装处理函数或被忽略,尝试用 sigaction() 修改处置会失败;把它们加入屏蔽集合也不能真的屏蔽。它们仍受执行状态约束:SIGKILL 不是让 CPU 任意抛弃持锁内核栈的操作,任务可能要等到能够处理致命信号的路径。TASK_KILLABLE 等等待设计决定相应等待能否被致命信号打断。signal(7)
2. 标准信号为什么不能当成事件计数器
把 SIGUSR1 屏蔽后连续发送五次,解除屏蔽时不能期待处理函数执行五次:同号标准信号已经待决时,后续产生通常合并。它保留“这类事件需要关注”的状态。
SIGCHLD:通知一次,回收可能需要多次
SIGCHLD 的正确消费方式由此可推导出来:一次通知到来后,应反复调用适当的非阻塞 wait,取出当前所有可回收子进程的状态,直到没有更多结果。若只回收一个,另外几个可能已经退出,却没有独立的后续 SIGCHLD 留着提醒你。即使信号发生合并,退出状态仍由内核的子进程等待机制保存;通知和事实记录属于不同对象。
实时信号:有序排队仍需处理资源上限
实时信号可通过 sigqueue() 附带值,同一种信号的多个实例按发送次序递送。图中保留了 10、11、12 三个值,但队列容量有限,发送可能以 EAGAIN 失败。
不同实时信号编号还有优先顺序,不能把所有编号合成一个全局 FIFO。使用 SIGRTMIN + n 时应检查 SIGRTMAX;glibc 可为线程库保留实时信号,SIGRTMIN 不能写死为 34。signal(7)
连续发送后分别取出两类信号。图中特意用小容量暴露发送失败;它不是 Linux 的固定队列上限。
正在呈现知识画面
发了三次,就能取出三次吗?同号普通信号合并成一个 pending 状态,同号实时信号分别排队并按序消费。队列资源有限,发送失败的值不会入队。
{"id":"signal-queues","title":"发了三次,就能取出三次吗?","summary":"同号普通信号合并成一个 pending 状态,同号实时信号分别排队并按序消费。队列资源有限,发送失败的值不会入队。","height":1000,"html":"<p class=\"eyebrow\" data-i18n=\"eyebrow\"></p><h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"send-one\" data-i18n=\"sendOne\"></button><button id=\"send-three\" data-i18n=\"sendThree\"></button></div><div class=\"queue-panel\"><header><strong>SIGUSR1</strong><span data-i18n=\"standard\"></span></header><div class=\"standard-slot\" id=\"standard-token\">—</div><div class=\"queue-result\" id=\"standard-count\"></div><button id=\"take-standard\" data-i18n=\"take\"></button></div><div class=\"queue-panel\"><header><strong>SIGRTMIN</strong><span data-i18n=\"realtime\"></span></header><div class=\"slots\"><div id=\"slot-0\">—</div><div id=\"slot-1\">—</div><div id=\"slot-2\">—</div><div id=\"slot-3\">—</div></div><div class=\"queue-result\" id=\"rt-count\"></div><button id=\"take-rt\" data-i18n=\"take\"></button></div><div class=\"metrics\"><div><span data-i18n=\"lastStandard\"></span><strong id=\"last-standard\">—</strong></div><div><span data-i18n=\"lastRealtime\"></span><strong id=\"last-rt\">—</strong></div></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"initial\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"sent\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"full\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"taken\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scope\"></summary><p class=\"note\" data-i18n=\"note\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.note,.caption{font-size:12px;color:var(--muted);margin-top:14px}.controls{display:grid;gap:12px;margin:18px 0}.control{display:grid;grid-template-columns:1fr auto;gap:6px 12px;align-items:center;font-size:13px}.control input{grid-column:1/3;width:100%;margin:0}.control output{color:var(--accent);font-variant-numeric:tabular-nums}.presets{display:flex;flex-wrap:wrap;gap:4px}.presets button{flex:1 1 auto;min-width:min-content}.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px}.actions button{flex:1;min-width:0;min-height:42px;font-size:13px}.copy{display:grid;margin-top:16px;font-size:14px}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.metrics{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:16px 0}.metrics>div{min-width:0;border-left:2px solid var(--accent);padding-left:12px}.metrics span{display:block;color:var(--muted);font-size:12px;min-height:2.8em}.metrics strong{display:block;font-size:22px;font-weight:500;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.formula{font-size:13px;overflow-wrap:anywhere;border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-variant-numeric:tabular-nums}.formula div+div{margin-top:6px}.two{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:18px}.card{min-width:0;border:1px solid var(--rule);border-radius:9px;padding:14px}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card code{font-size:12px;overflow-wrap:anywhere}.card .value{font-size:24px;font-weight:500;font-variant-numeric:tabular-nums}.card small{display:block;color:var(--muted);font-size:12px}.card button{width:100%;min-width:0;font-size:13px;min-height:42px}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{accent-color:var(--accent);width:16px;height:16px;flex-shrink:0}.pill{display:inline-block;padding:3px 8px;border-radius:5px;background:var(--accent-soft);color:var(--accent);font-size:12px}.plot{position:relative;margin:18px 0}.plot svg{width:100%;height:auto;overflow:visible}.plot text{font-size:12px;fill:var(--muted)}.plot .ink{fill:var(--ink)}.plot .blue{fill:var(--accent);stroke:var(--accent)}.plot .gold{fill:var(--second);stroke:var(--second)}.plot .area{fill:var(--accent-soft);stroke:var(--accent);transition:all .25s}.plot .other{fill:var(--second-soft);stroke:var(--second)}.plot line,.plot path,.plot rect,.plot circle{transition:all .25s}.legend{display:flex;flex-wrap:wrap;gap:6px 16px;font-size:12px;color:var(--muted);margin-top:10px}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:5px}.legend .gold:before{background:var(--second)}details{margin-top:14px;font-size:12px;color:var(--muted)}summary{cursor:pointer;padding:8px 0}button:disabled{cursor:default}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.card button{font-size:12px}.metrics{gap:10px}.metrics strong{font-size:20px}.actions button{font-size:12px}.plot text{font-size:13px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n\n.queue-panel{border:1px solid var(--rule);border-radius:9px;padding:14px;margin-top:16px}.queue-panel header{display:flex;flex-wrap:wrap;justify-content:space-between;font-size:13px;gap:8px}.queue-panel header>span{color:var(--muted)}.standard-slot{background:var(--surface);border:1px dashed var(--rule);padding:12px;text-align:center;font-size:18px;border-radius:6px;margin-top:14px}.standard-slot.pending{background:var(--accent-soft);border-color:var(--accent)}.slots{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-top:14px}.slots div{border:1px dashed var(--rule);border-radius:6px;padding:12px 5px;text-align:center;font:500 18px var(--viz-font-ui)}.slots .pending{background:var(--second-soft);border-color:var(--second)}.queue-result{font-size:12px;margin:12px 0;min-height:3.4em;color:var(--muted)}.queue-panel button{width:100%;min-height:42px;font-size:13px}","js":"const $=s=>document.querySelector(s);let pending=false,queue=[],sent=0,accepted=0,rejected=0,takenStandard=0,takenRT=0,lastStandard='—',lastRT='—',message='initial';\nfunction draw(){$('#standard-token').textContent=pending?'SIGUSR1':'—';$('#standard-token').classList.toggle('pending',pending);$('#standard-count').textContent=viz.t('pendingCount')+' '+Number(pending)+' · '+viz.t('consumed')+' '+takenStandard;for(let i=0;i<4;i++){$('#slot-'+i).textContent=queue[i]??'—';$('#slot-'+i).classList.toggle('pending',i<queue.length);}$('#rt-count').textContent=viz.t('accepted')+' '+accepted+' · '+viz.t('rejected')+' '+rejected+' · '+viz.t('pendingCount')+' '+queue.length;$('#last-standard').textContent=lastStandard;$('#last-rt').textContent=lastRT;$('#take-standard').disabled=!pending;$('#take-rt').disabled=!queue.length;$('#explanation').textContent=viz.t(message);}\nfunction send(n){let full=false;for(let i=0;i<n;i++){const value=10+sent;sent++;pending=true;if(queue.length<4){queue.push(value);accepted++;}else{rejected++;full=true;}}message=full?'full':'sent';draw();}\n$('#send-one').addEventListener('click',()=>send(1));$('#send-three').addEventListener('click',()=>send(3));$('#take-standard').addEventListener('click',()=>{pending=false;takenStandard++;lastStandard='SIGUSR1';message='taken';draw();});$('#take-rt').addEventListener('click',()=>{lastRT=queue.shift();takenRT++;message='taken';draw();});$('#reset').addEventListener('click',()=>{pending=false;queue=[];sent=accepted=rejected=takenStandard=takenRT=0;lastStandard=lastRT='—';message='initial';draw();});draw();\n","audio":false,"strings":{"accepted":"实时信号已接受","consumed":"已取出","eyebrow":"02 · pending 与排队","full":"示意队列已满,本次实时发送失败。普通信号仍只有一个 pending 位;失败的值不会排队。","heading":"发了三次,就能取出三次吗?","initial":"先连续发送,再分别取出。比较一个 pending 位与多个有顺序的队列项。","intro":"两种信号都保持屏蔽。每次发送同时产生普通信号和带数值的实时信号,再分别同步取出。","lastRealtime":"上次取出的实时值","lastStandard":"上次取出的普通信号","note":"用 sigtimedwait 一类同步接口消费被屏蔽的信号。普通信号用 kill 发送,不给它虚构每次发送的 payload。实时队列容量 4 仅为图示;Linux 上限受资源限制控制,sigqueue 可返回 EAGAIN。这里只比较同一种实时信号。","pendingCount":"待取出","realtime":"同一种实时信号 · FIFO","rejected":"实时信号发送失败","reset":"清空队列","scope":"模型范围与约定","sendOne":"各发送一次","sendThree":"各连续发送三次","sent":"普通信号只留下一个 pending 位;同编号实时信号按顺序保存每个已接受的值。","standard":"普通信号 · SIGUSR1","take":"取出一个","taken":"取出的结果留在下方,便于比较。普通信号取一次就清空;实时信号按同编号 FIFO 逐项取出。"}}
实验:一次待决与三个排队值
下面在单线程进程内屏蔽信号,分别验证五次标准信号合并与三个同号实时信号的有序取出。它不安装异步处理函数,也不向其他进程发送信号。保存为 pending.c,编译时使用 cc -std=c11 -O2 -Wall -Wextra -Werror pending.c -o pending,不要定义 NDEBUG。
#define _POSIX_C_SOURCE 200809L
#include <assert.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>
int main(void) {
const int rt = SIGRTMIN;
assert(rt <= SIGRTMAX);
sigset_t all, old, standard, realtime;
sigemptyset(&all);
sigaddset(&all, SIGUSR1);
sigaddset(&all, rt);
assert(sigprocmask(SIG_BLOCK, &all, &old) == 0);
struct sigaction sa = { .sa_handler = SIG_DFL };
sigemptyset(&sa.sa_mask);
assert(sigaction(SIGUSR1, &sa, NULL) == 0);
assert(sigaction(rt, &sa, NULL) == 0);
for (int i = 0; i < 5; ++i)
assert(kill(getpid(), SIGUSR1) == 0);
for (int i = 10; i < 13; ++i) {
union sigval value = { .sival_int = i };
assert(sigqueue(getpid(), rt, value) == 0);
}
sigemptyset(&standard);
sigaddset(&standard, SIGUSR1);
sigemptyset(&realtime);
sigaddset(&realtime, rt);
struct timespec zero = {0};
siginfo_t info;
assert(sigtimedwait(&standard, &info, &zero) == SIGUSR1);
errno = 0;
assert(sigtimedwait(&standard, &info, &zero) == -1 && errno == EAGAIN);
for (int expected = 10; expected < 13; ++expected) {
assert(sigtimedwait(&realtime, &info, &zero) == rt);
assert(info.si_code == SI_QUEUE);
assert(info.si_value.sival_int == expected);
}
errno = 0;
assert(sigtimedwait(&realtime, &info, &zero) == -1 && errno == EAGAIN);
assert(sigprocmask(SIG_SETMASK, &old, NULL) == 0);
puts("standard: 1 pending; realtime payloads: 10, 11, 12");
}
零超时意味着只取此刻已经待决的匹配信号,没有则返回 EAGAIN。程序在恢复原屏蔽字前把自己发送的信号全部消费掉。实验假设没有外部发送者且排队配额足够;正式程序必须处理 sigqueue 的资源不足,不能依赖断言承担恢复策略。sigtimedwait(2)
3. 处理函数如何进入,又如何回到被打断的位置
对于需要调用用户处理函数的递送,内核在适当的用户态返回边界准备信号帧:保存恢复所需的用户寄存器、屏蔽字和相关上下文,选择普通用户栈或满足条件的备用信号栈,再把用户执行入口改为处理函数。它运行在接收线程的用户态上下文中,不是内核中断处理函数,也不是内核临时创建的新线程。
处理函数返回后,经适用的用户态返回跳板进入 rt_sigreturn,内核验证并恢复相应状态,继续被打断的执行。帧布局和扩展寄存器细节依体系结构及 ABI 而异,不能照着一段假定固定大小的 C 结构体随意解释所有架构。默认终止、停止或忽略动作也不需要沿这条“调用用户处理函数”的路径执行。
选择主程序的执行位置,再在此处递送信号。TID 保持不变,从 PC、临时保存帧和 mask 观察这次转入与返回。
正在呈现知识画面
处理函数借用谁的执行线程?处理函数在接收线程的用户态上下文中执行。signal frame 保存中断现场,rt_sigreturn 恢复 PC 和 mask,处理函数对标志的修改仍然保留。
{"id":"signal-handler-context","title":"处理函数借用谁的执行线程?","summary":"处理函数在接收线程的用户态上下文中执行。signal frame 保存中断现场,rt_sigreturn 恢复 PC 和 mask,处理函数对标志的修改仍然保留。","height":1000,"html":"<p class=\"eyebrow\" data-i18n=\"eyebrow\"></p><h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"thread-identity\"><strong>TID 5101</strong><span id=\"execution\"></span></div><div class=\"main-code\"><button id=\"line-0\" data-location=\"0\"><span>1</span><code>prepare();</code></button><button id=\"line-1\" data-location=\"1\"><span>2</span><code>compute();</code></button><button id=\"line-2\" data-location=\"2\"><span>3</span><code>use_result();</code></button><button id=\"line-3\" data-location=\"3\"><span>4</span><code>loop();</code></button></div><div class=\"context-values\"><span>flag = <b id=\"flag\">0</b></span></div><div class=\"signal-frame\" id=\"signal-frame\"><header data-i18n=\"frame\"></header><div><span data-i18n=\"savedPC\"></span><strong id=\"saved-pc\">—</strong></div><div><span data-i18n=\"savedMask\"></span><code>∅</code></div><code class=\"handler-code\">handler: flag = 1;</code><button id=\"return\" data-i18n=\"return\"></button></div><div class=\"mask\"><span data-i18n=\"currentMask\"></span><strong id=\"mask\">∅</strong></div><div class=\"actions\"><button id=\"interrupt\" data-i18n=\"interrupt\"></button></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"mainNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"handlerNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"returnedNote\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scope\"></summary><p class=\"note\" data-i18n=\"note\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.note,.caption{font-size:12px;color:var(--muted);margin-top:14px}.controls{display:grid;gap:12px;margin:18px 0}.control{display:grid;grid-template-columns:1fr auto;gap:6px 12px;align-items:center;font-size:13px}.control input{grid-column:1/3;width:100%;margin:0}.control output{color:var(--accent);font-variant-numeric:tabular-nums}.presets{display:flex;flex-wrap:wrap;gap:4px}.presets button{flex:1 1 auto;min-width:min-content}.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px}.actions button{flex:1;min-width:0;min-height:42px;font-size:13px}.copy{display:grid;margin-top:16px;font-size:14px}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.metrics{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:16px 0}.metrics>div{min-width:0;border-left:2px solid var(--accent);padding-left:12px}.metrics span{display:block;color:var(--muted);font-size:12px;min-height:2.8em}.metrics strong{display:block;font-size:22px;font-weight:500;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.formula{font-size:13px;overflow-wrap:anywhere;border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-variant-numeric:tabular-nums}.formula div+div{margin-top:6px}.two{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:18px}.card{min-width:0;border:1px solid var(--rule);border-radius:9px;padding:14px}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card code{font-size:12px;overflow-wrap:anywhere}.card .value{font-size:24px;font-weight:500;font-variant-numeric:tabular-nums}.card small{display:block;color:var(--muted);font-size:12px}.card button{width:100%;min-width:0;font-size:13px;min-height:42px}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{accent-color:var(--accent);width:16px;height:16px;flex-shrink:0}.pill{display:inline-block;padding:3px 8px;border-radius:5px;background:var(--accent-soft);color:var(--accent);font-size:12px}.plot{position:relative;margin:18px 0}.plot svg{width:100%;height:auto;overflow:visible}.plot text{font-size:12px;fill:var(--muted)}.plot .ink{fill:var(--ink)}.plot .blue{fill:var(--accent);stroke:var(--accent)}.plot .gold{fill:var(--second);stroke:var(--second)}.plot .area{fill:var(--accent-soft);stroke:var(--accent);transition:all .25s}.plot .other{fill:var(--second-soft);stroke:var(--second)}.plot line,.plot path,.plot rect,.plot circle{transition:all .25s}.legend{display:flex;flex-wrap:wrap;gap:6px 16px;font-size:12px;color:var(--muted);margin-top:10px}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:5px}.legend .gold:before{background:var(--second)}details{margin-top:14px;font-size:12px;color:var(--muted)}summary{cursor:pointer;padding:8px 0}button:disabled{cursor:default}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.card button{font-size:12px}.metrics{gap:10px}.metrics strong{font-size:20px}.actions button{font-size:12px}.plot text{font-size:13px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n\n.thread-identity{display:flex;justify-content:space-between;gap:12px;padding:14px;border:1px solid var(--accent);border-radius:9px 9px 0 0;background:var(--accent-soft);font-size:13px}.thread-identity>strong{white-space:nowrap;flex-shrink:0}.thread-identity>span{min-width:0;min-height:3.2em}.main-code{border:1px solid var(--rule);border-top:0;padding:12px}.main-code>button{display:flex;justify-content:flex-start;width:100%;gap:18px;padding:7px;border-radius:5px;font-size:13px;border-color:transparent;min-height:40px}.main-code>button:disabled{opacity:1}.main-code span{color:var(--muted)}.main-code .current{background:var(--accent-soft);color:var(--accent)}.main-code .suspended{background:var(--surface);outline:1px dashed var(--muted)}.context-values{display:flex;gap:24px;flex-wrap:wrap;padding:14px 0;font-size:13px}.signal-frame{border:1px dashed var(--rule);border-radius:8px;padding:14px;opacity:.55;transition:opacity .25s,background .25s}.signal-frame.active{opacity:1;background:var(--second-soft);border-color:var(--second)}.signal-frame header{font-size:13px;font-weight:600;margin-bottom:10px}.signal-frame>div{display:flex;justify-content:space-between;gap:12px;margin:8px 0;font-size:12px}.handler-code{display:block;padding:10px 0;font-size:13px}.signal-frame button{width:100%;font-size:13px}.mask{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-top:14px;font-size:12px}.mask strong{font-family:var(--viz-font-ui)}","js":"const $=s=>document.querySelector(s);let pc=0,flag=0,active=false,saved=null,message='mainNote';\nfunction draw(){$('#execution').textContent=viz.t(active?'handler':'main');for(let i=0;i<4;i++){$('#line-'+i).classList.toggle('current',pc===i&&!active);$('#line-'+i).disabled=active;$('#line-'+i).setAttribute('aria-pressed',String(pc===i));$('#line-'+i).classList.toggle('suspended',pc===i&&active);}$('#flag').textContent=flag;$('#signal-frame').classList.toggle('active',active);$('#saved-pc').textContent=saved===null?'—':viz.t('line')+' '+(saved+1);$('#mask').textContent=active?'SIGUSR1':'∅';$('#interrupt').disabled=active;$('#return').disabled=!active;$('#explanation').textContent=viz.t(message);}\ndocument.querySelectorAll('[data-location]').forEach(e=>e.addEventListener('click',()=>{pc=+e.dataset.location;message='mainNote';draw();}));\n$('#interrupt').addEventListener('click',()=>{saved=pc;active=true;flag=1;message='handlerNote';draw();});$('#return').addEventListener('click',()=>{pc=saved;saved=null;active=false;message='returnedNote';draw();});$('#reset').addEventListener('click',()=>{pc=flag=0;saved=null;active=false;message='mainNote';draw();});draw();\n","audio":false,"strings":{"currentMask":"当前信号 mask","eyebrow":"03 · 信号现场","frame":"临时用户态 signal frame","handler":"处理函数执行中","handlerNote":"主程序暂停,同一 TID 正在执行处理函数。原现场留在 signal frame 中;默认会暂时屏蔽当前信号。","heading":"处理函数借用谁的执行线程?","interrupt":"在此处递送 SIGUSR1","intro":"直接点击代码行选择中断位置,再递送信号。观察同一 TID 的执行位置、保存帧与返回后的现场。","line":"行","main":"主程序执行中","mainNote":"主程序正在执行。递送会保存它当前的 PC 与 mask,再让同一个线程进入处理函数。","note":"图选择信号在用户态代码位置递送的时刻,省略架构 ABI、备用信号栈和嵌套。处理函数把 volatile sig_atomic_t 标志设为 1;它不是新线程。框中只示意关键字段,返回后临时帧不再使用。 点击代码行只选择快照中的中断位置,不模拟四个示意函数的内部执行。","reset":"重置","return":"处理函数返回 · rt_sigreturn","returnedNote":"rt_sigreturn 恢复了原 PC 与 mask。主程序从中断处继续,处理函数设置的 flag 仍然保留。","savedMask":"保存的 mask","savedPC":"保存的 PC","scope":"模型范围与约定"}}
处理函数为什么不能照常打印日志
设原线程正在执行 stdio,内部状态尚未更新完,信号处理函数此时插入执行。handler 再调用 printf,便可能重入同一份未完成的状态;若试图获取原线程已持有的锁,还可能等待自己解锁。
这正是异步信号安全限制的来源。处理函数应只做允许的操作,例如设置 volatile sig_atomic_t 标志,或写入具备非阻塞/容量策略的通知通道,再交给正常执行流处理。signal-safety(7)
把复杂处理移回正常线程
sig_atomic_t 适合规定范围内的信号处理通信,却不自动提供多线程数据发布所需的 release/acquire 协议。如果需求本来就是集中处理退出、重载等异步请求,可以使用 sigwaitinfo() 或 Linux signalfd(),把处理安排在正常线程执行流中;多线程下要让相关线程一致屏蔽这些信号,避免被其他线程先递送。由非法访存等同步异常产生的 SIGSEGV 则不能机械地套用这种集中消费模式来恢复程序。
4. EINTR、重启与“检查后再睡”的竞态
一次等待被信号打断后,先判断接口实际返回了什么:
| 结果 | 后续处理依据 |
| 返回 EINTR | 按接口语义决定重试、取消或返回上层 |
| read/write 返回正的短计数 | 先记入已完成字节,再处理剩余部分 |
| 接口按规则自动重启 | 调用可能继续等待;SA_RESTART 的范围仍依接口而定 |
短返回可能已经完成部分工作,不能统一按“没有做任何事”重新提交。
超时重试:保留原截止时间
SA_RESTART 不是全局“永不 EINTR”开关。例如 nanosleep 和多种等待接口有自己的中断规则。带超时的循环还要保留原始截止时间:若每次中断都重新等待完整 5 秒,频繁信号可能让总等待远超 5 秒。使用单调时钟计算剩余预算,或使用支持绝对截止时间的接口,才能维持端到端时限。
等待竞态:标志已变真,pause 却刚开始
考虑这个顺序:主程序检查标志为假;信号处理函数随后把它置真;主程序最后才调用 pause()。通知已经过去,主程序却会等待下一次信号。
通常应先屏蔽目标信号并检查标志,再用 sigsuspend() 原子地替换屏蔽字并等待;返回后重新检查条件。它与条件等待保护的是同一个窗口:从检查条件到进入睡眠。signal(7)
5. ptrace:先识别停止原因,再决定如何恢复
ptrace 的跟踪对象在 Linux 中是线程。对一个 TID 建立跟踪关系,不意味着其他线程同时停止;调试器要构造整个进程的一致视图,还需要管理线程发现、创建事件和每条线程的停止状态。
PTRACE_ATTACH 建立关系并发送停止请求,但调用返回不意味着目标已经进入可检查的停止,跟踪者应等待相应状态。PTRACE_SEIZE 本身不停止目标,之后可用 PTRACE_INTERRUPT 请求停止。跟踪循环不能只写成“wait 返回了就读寄存器”,因为 wait 还可能报告最终退出或其他事件。
可以按下表建立最基本的分类,再处理选项、线程组和内核版本差异:
| 观察到的状态 | 含义与后续动作 |
| 正常退出或被信号终止 | 消费最终状态,清理跟踪记录;不再发恢复请求 |
| 普通信号递送停止 | 可检查 siginfo,选择压制或在恢复时注入该信号 |
| 系统调用停止 | 启用 TRACESYSGOOD 后常以 SIGTRAP | 0x80 标识;这是分类标记,不是真正要转发的信号 |
| ptrace 事件停止 | 解码 wait 状态中的事件,例如 clone、exec、exit;事件停止与最终退出仍不同 |
| 作业控制 group-stop | 需要与信号递送停止区分;SEIZE 模式下可结合 LISTEN 等协议处理 |
下面用独立的停止快照比较恢复决策。只有真实 SIGUSR1 递送停止提供转发选择,目标线程的处理结果会在恢复后保留。
正在呈现知识画面
看到 SIGTRAP,就该转发 SIGTRAP 吗?ptrace 恢复前须区分真实信号递送、调试事件与最终退出。真实信号可在恢复时转发或抑制;调试标记不等于应转发的普通信号。
{"id":"ptrace-stop-protocol","title":"看到 SIGTRAP,就该转发 SIGTRAP 吗?","summary":"ptrace 恢复前须区分真实信号递送、调试事件与最终退出。真实信号可在恢复时转发或抑制;调试标记不等于应转发的普通信号。","height":1000,"html":"<p class=\"eyebrow\" data-i18n=\"eyebrow\"></p><h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"case-control\"><label for=\"stop-kind\" data-i18n=\"stopKind\"></label><select id=\"stop-kind\"><option value=\"entry\" data-i18n=\"entry\"></option><option value=\"exit\" data-i18n=\"sysExit\"></option><option value=\"signal\" data-i18n=\"signal\"></option><option value=\"exec\" data-i18n=\"exec\"></option><option value=\"final\" data-i18n=\"final\"></option></select></div><div class=\"stop-token\"><span data-i18n=\"reported\"></span><code id=\"report\"></code></div><div class=\"two\"><section class=\"card tracer\"><header data-i18n=\"tracer\"></header><div class=\"classification\" id=\"classification\"></div><label class=\"check\"><input type=\"checkbox\" id=\"inject\"><span data-i18n=\"inject\"></span></label><button id=\"resume\" data-i18n=\"resume\"></button><code id=\"command\"></code></section><section class=\"card tracee\"><header>TID 5101</header><div id=\"tracee-state\" class=\"tracee-state\"></div><small class=\"signal-label\" data-i18n=\"deliveredLabel\"></small><div class=\"delivered-signal\" id=\"delivered-signal\">—</div><div class=\"handler-flag\"><span>handler_flag</span><strong id=\"handler-flag\">0</strong></div></section></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"eventNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"signalNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"finalNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"forwardedNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"suppressedNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"continuedNote\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scope\"></summary><p class=\"note\" data-i18n=\"note\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.note,.caption{font-size:12px;color:var(--muted);margin-top:14px}.controls{display:grid;gap:12px;margin:18px 0}.control{display:grid;grid-template-columns:1fr auto;gap:6px 12px;align-items:center;font-size:13px}.control input{grid-column:1/3;width:100%;margin:0}.control output{color:var(--accent);font-variant-numeric:tabular-nums}.presets{display:flex;flex-wrap:wrap;gap:4px}.presets button{flex:1 1 auto;min-width:min-content}.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px}.actions button{flex:1;min-width:0;min-height:42px;font-size:13px}.copy{display:grid;margin-top:16px;font-size:14px}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.metrics{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:16px 0}.metrics>div{min-width:0;border-left:2px solid var(--accent);padding-left:12px}.metrics span{display:block;color:var(--muted);font-size:12px;min-height:2.8em}.metrics strong{display:block;font-size:22px;font-weight:500;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.formula{font-size:13px;overflow-wrap:anywhere;border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-variant-numeric:tabular-nums}.formula div+div{margin-top:6px}.two{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:18px}.card{min-width:0;border:1px solid var(--rule);border-radius:9px;padding:14px}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card code{font-size:12px;overflow-wrap:anywhere}.card .value{font-size:24px;font-weight:500;font-variant-numeric:tabular-nums}.card small{display:block;color:var(--muted);font-size:12px}.card button{width:100%;min-width:0;font-size:13px;min-height:42px}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{accent-color:var(--accent);width:16px;height:16px;flex-shrink:0}.pill{display:inline-block;padding:3px 8px;border-radius:5px;background:var(--accent-soft);color:var(--accent);font-size:12px}.plot{position:relative;margin:18px 0}.plot svg{width:100%;height:auto;overflow:visible}.plot text{font-size:12px;fill:var(--muted)}.plot .ink{fill:var(--ink)}.plot .blue{fill:var(--accent);stroke:var(--accent)}.plot .gold{fill:var(--second);stroke:var(--second)}.plot .area{fill:var(--accent-soft);stroke:var(--accent);transition:all .25s}.plot .other{fill:var(--second-soft);stroke:var(--second)}.plot line,.plot path,.plot rect,.plot circle{transition:all .25s}.legend{display:flex;flex-wrap:wrap;gap:6px 16px;font-size:12px;color:var(--muted);margin-top:10px}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:5px}.legend .gold:before{background:var(--second)}details{margin-top:14px;font-size:12px;color:var(--muted)}summary{cursor:pointer;padding:8px 0}button:disabled{cursor:default}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.card button{font-size:12px}.metrics{gap:10px}.metrics strong{font-size:20px}.actions button{font-size:12px}.plot text{font-size:13px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n\n.case-control{display:grid;gap:8px;font-size:13px}.case-control select{width:100%;min-width:0;background:var(--paper);color:var(--ink);border:1px solid var(--rule);border-radius:7px;padding:10px;font:inherit}.stop-token{margin-top:16px;padding:14px;background:var(--surface);border:1px solid var(--rule);border-radius:8px}.stop-token span{display:block;font-size:12px;color:var(--muted);margin-bottom:8px}.stop-token code{font-size:12px;overflow-wrap:anywhere;display:block;min-height:3.4em}.classification{font-size:13px;min-height:5.1em}.tracer .check{min-height:6em;font-size:12px}.tracer>code{display:block;font-size:11px;margin-top:12px;min-height:6em;overflow-wrap:anywhere}.tracee-state{font-size:14px;min-height:5.1em}.signal-label{min-height:4.2em}.delivered-signal{padding:14px 5px;border:1px dashed var(--rule);border-radius:6px;font:500 13px var(--viz-font-ui);text-align:center;min-height:4em}.delivered-signal.received{background:var(--second-soft);border-color:var(--second)}.handler-flag{margin-top:14px;font-size:12px}.handler-flag strong{display:block;font-size:24px}.tracee.running{background:var(--accent-soft);border-color:var(--accent)}","js":"const $=s=>document.querySelector(s);let resumed=false,flag=0,message='eventNote';\nfunction draw(){const kind=$('#stop-kind').value,signal=kind==='signal',final=kind==='final';$('#report').textContent={entry:'WSTOPSIG = SIGTRAP | 0x80 · ENTRY',exit:'WSTOPSIG = SIGTRAP | 0x80 · EXIT',signal:'WSTOPSIG = SIGUSR1 · signal-delivery-stop',exec:'WSTOPSIG = SIGTRAP · PTRACE_EVENT_EXEC',final:'WIFEXITED = true · exit code 0'}[kind];$('#classification').textContent=viz.t(final?'finalClass':signal?'signalClass':'eventClass');$('#inject').disabled=!signal||resumed;if(!signal)$('#inject').checked=false;$('#resume').disabled=final||resumed;$('#tracee-state').textContent=viz.t(final?'exited':resumed?'running':'stopped');$('#delivered-signal').textContent=flag?'SIGUSR1':'—';$('#delivered-signal').classList.toggle('received',!!flag);$('#handler-flag').textContent=flag;document.querySelector('.tracee').classList.toggle('running',resumed);$('#command').textContent=final?'—':'PTRACE_CONT(..., '+(signal&&$('#inject').checked?'SIGUSR1':'0')+')';$('#explanation').textContent=viz.t(message);}\nfunction reset(){resumed=false;flag=0;$('#inject').checked=false;const k=$('#stop-kind').value;message=k==='final'?'finalNote':k==='signal'?'signalNote':'eventNote';draw();}\n$('#stop-kind').addEventListener('change',reset);$('#inject').addEventListener('change',draw);$('#resume').addEventListener('click',()=>{resumed=true;if($('#stop-kind').value==='signal'){flag=Number($('#inject').checked);message=flag?'forwardedNote':'suppressedNote';}else message='continuedNote';draw();});$('#reset').addEventListener('click',reset);draw();\n","audio":false,"strings":{"continuedNote":"以信号参数 0 恢复,没有把调试标记注入应用。目标继续执行。","deliveredLabel":"实际递送给目标的信号","entry":"系统调用入口","eventClass":"调试协议事件停止","eventNote":"这是调试协议事件。SIGTRAP 标记帮助分类,不能据此把它当成应用收到了普通 SIGTRAP。","exec":"exec 事件停止","exited":"已退出","eyebrow":"04 · ptrace 停止协议","final":"已最终退出","finalClass":"最终退出报告","finalNote":"wait 已确认最终退出,不是一个等待恢复的 stop。此时不能再恢复这个线程。","forwardedNote":"恢复时转发了 SIGUSR1。目标的处理函数执行,将 handler_flag 设为 1。","heading":"看到 SIGTRAP,就该转发 SIGTRAP 吗?","inject":"恢复时转发 SIGUSR1","intro":"选择一种 wait 报告,先看它属于哪类停止,再决定恢复时是否递送真实信号。右侧保留对目标线程的影响。","note":"每个选项都是独立停止快照,假定已启用相应 ptrace 选项且目标安装了 SIGUSR1 处理函数。用 PTRACE_CONT 展示恢复,后续不自动请求 syscall-stop。真实调试器还须处理 group-stop、退出事件与多线程;非递送停止不适合用来推断信号注入。","reported":"wait 报告","reset":"重置当前场景","resume":"恢复此线程","running":"运行中","scope":"模型范围与约定","signal":"SIGUSR1 递送停止","signalClass":"真实信号递送停止","signalNote":"这是 SIGUSR1 的 signal-delivery-stop。恢复时可转发该信号,也可用 0 抑制它,选择会改变目标行为。","stopKind":"收到的停止报告","stopped":"已停止","suppressedNote":"恢复参数为 0,原 SIGUSR1 被抑制,目标继续执行,handler_flag 仍为 0。","sysExit":"系统调用出口","tracer":"调试器的判断"}}
恢复决策:系统调用停止与信号递送不同
PTRACE_SYSCALL 让目标继续到后续适用的系统调用停止。若简单维护一个布尔值,在每次停止时交替认为“入口/出口”,遇到信号、exec、seccomp 事件或其他停止就可能错位。支持时可用 PTRACE_GET_SYSCALL_INFO 获取入口、出口或 seccomp 等信息;仍须先识别停止类型,不能对所有 wait 结果一概读取系统调用号。
一次常见的错误是把所有 SIGTRAP 都在恢复时原样注入。调试器自己请求的单步或系统调用事件并不都代表目标本应接收一个 SIGTRAP;错误注入可能使原本正常的程序执行默认终止动作。反过来,把真正的业务信号全部压制,也会改变目标行为。跟踪器的恢复决策属于程序语义的一部分。ptrace(2)
6. 读内存与 seccomp:可观察不等于一致或安全
PTRACE_PEEKDATA 可能合法读到值 -1。判断失败时应先清零 errno,再结合返回值与 errno,而不是看到 -1 就认定地址非法。使用 /proc/PID/mem 或 process_vm_readv() 也不会绕过访问控制;凭据、dumpable、用户 namespace、Yama 或其他 LSM 策略等都会影响允许的操作。
一致性:停住一条线程,其他写入者还在
权限通过也不代表读到一致快照。假设跟踪者只停住线程 A,线程 B 仍在修改链表;先读 head、再读 next 可能跨过两次结构变化。即使停止了所有普通线程,共享内存的其他进程或设备仍可能改变数据。分析工具应明确需要的是尽力采样,还是由程序协作提供的一致快照。
接口边界:TRACE 与 USER_NOTIF
seccomp 的 SECCOMP_RET_TRACE 可以在相应 ptrace 选项下产生 seccomp 跟踪事件;SECCOMP_RET_USER_NOTIF 则通过专门 listener fd 交给用户态监督程序,它不是 ptrace 接口。
二者可以出现在同一个复杂系统里,但通知收取、响应及任务生命周期处理各有协议。seccomp 过滤器
用户态监督者读取系统调用参数中的指针,再让原系统调用继续,会面临典型 TOCTOU:在检查之后,其他线程可能修改指针指向的内存。仅“检查过路径字符串”不足以保证目标最终操作同一对象。
ptrace 本身也不是可信的沙箱边界;应把观察、代执行、对象引用和访问控制的职责分开设计,而不是从“能拦截系统调用”直接推导“能安全授权任意系统调用”。seccomp 用户通知
7. 快速定位时应追问的下一层
| 现场现象 | 下一步检查 |
| 信号没有反应 | 发送结果、待决状态、目标线程屏蔽字、处置及运行机会 |
| 子进程持续显示 Z | SIGCHLD 之后是否循环消费全部可回收状态 |
| 调试恢复后异常退出 | 停止分类,以及是否误把调试事件作为 SIGTRAP 注入 |
这些入口把“发生了什么”连接到具体状态;随后再沿源码查找状态变化的位置。
阅读内核实现可从 kernel/signal.c 的产生/递送路径、kernel/ptrace.c 的关系与权限管理,再到架构信号帧和 ptrace 寄存器操作。内部信号集合和用户态 sigset_t 的布局不是同一个 ABI,也无需为理解机制复制一份容易随版本过时的结构定义。
进程与线程资源关系回到生命周期,信号唤醒后的 CPU 竞争回到调度器。