kill() が成功を返しても、シグナルハンドラがまだ実行されていないことがあります。複数の子プロセスが同時に終了しても、親プロセスは 1 回だけの SIGCHLD を受け取ります。これらの現象を理解するには、イベントの発生、保留状態、実際の配信 を区別する必要があります。
ptrace はさらに 1 層の停止プロトコルを追加します。トレース元は特定の位置でターゲットを観察できますが、停止理由を正しく識別して初めて、正しく再開することができます。
遭遇する現象 該当セクション
シグナルを送信しても反応がない、または特定のスレッドのみが受信する 第 1 節:保留とスレッド選択
送信回数と処理回数が一致しない 第 2 節:マージ、キューイングと実験
ハンドラがフリーズする、または待機タイムアウトが延び続ける 第 3~4 節:非同期エントリと EINTR
デバッガの再開後にターゲットが異常終了する 第 5~6 節:停止分類とアクセス境界
1. 発生、保留、配信は3つの異なる段階
送信インターフェースが成功することは、通常、シグナルリクエストがチェックを通過しカーネルに受け入れられたことを示しており、受信側がすでに処理関数を実行した確認ではありません。受信スレッドはそのシグナルをブロックしているか、まだスケジューリングされていないか、または一時的に応答できないカーネルパスにある可能性があります。もしビジネスロジックで何らかの操作の完了を確認する必要がある場合は、kill() の戻り値を完了通知として扱うのではなく、明示的な応答を設計すべきです。
まず、状態が誰に属しているかを確認しましょう。
状態 所属範囲 意味
処理:デフォルト、無視、処理関数 通常、スレッドグループ内で共有 受信後にどのように処理するか
ブロックマスク スレッドごとに独立 そのスレッドが現在受信するかどうか
スレッド指向保留シグナル 指定されたスレッド 対象スレッドが受信可能になるのを待つ
プロセス指向保留シグナル グループ共有状態に保存可能 適切なスレッドから受信者を選択
kill(pid, sig) は通常、プロセスに対して送信されます。pthread_kill() は特定のスレッドを指定します。ターゲットスレッドがシグナルをブロックしている場合、スレッド指向のシグナルを他のスレッドに委ねることはできません。
プロセス指向シグナルは受信スレッドを選択し、全スレッドにブロードキャストしない
kill(PID, SIGUSR1)
プロセス指向発生
スレッドグループ共有保留
未配信のシグナル
スレッド A
SIGUSR1 をブロック
スレッド B
ブロックされていない、選択可能
スレッド C
ブロックされていない、選択可能
いずれか一方を選択
スレッド指向シグナルは指定されたスレッドの保留状態に入る。そのスレッドがブロックしていても、他のスレッドに委ねることはできない。
図を読む際は、まず A のブロックマスクを確認し、次に B、C の受信資格を見る。A は SIGUSR1 をブロックしているため、B または C が選択される可能性があるが、両方がそれぞれ1回ずつ実行されるわけではない。3つのスレッドすべてがブロックしている場合、シグナルは保留状態のままになり、適切な配信機会があるまで保持される。
送信先とブロックマスクを変更し、スレッド上で配信を試行する。共有および個別の pending は別々に表示され、手動でスレッドを選択して可能な受信者を比較するが、カーネルの選択順序を保証するものではない。
図を準備しています 再読み込み
プロセス宛てか、スレッド宛てか? プロセス指向シグナルは適切なスレッドの1つで受信できる。スレッド指向シグナルは指定されたスレッドにのみ留まる。処理は通常共有され、ブロックマスクと個別の pending はそれぞれに属する。
{"id":"signal-routing","title":"プロセス宛てか、スレッド宛てか?","summary":"プロセス指向シグナルは適切なスレッドの1つで受信できる。スレッド指向シグナルは指定されたスレッドにのみ留まる。処理は通常共有され、ブロックマスクと個別の 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":"1つの pending が消費され、選択されたスレッドでのみ処理関数が実行される。","emptyNote":"このスレッドには引き受けるべきシグナルがない。個別 pending はスレッド間で移動できない。","eyebrow":"01 · シグナルの帰属","groupPending":"プロセス共有 pending","handlerCount":"ハンドラ実行回数","heading":"プロセス宛てか、スレッド宛てか?","initial":"A はデフォルトでこのシグナルをブロックしている。プロセスシグナルは B または C が受信できるが、A に指定されたシグナルは A のみに留まる。","intro":"まず送信先を選択し、各スレッドのブロックマスクを変更する。スレッド上で配信機会をトリガーし、どの pending が消費されるかを確認する。","note":"各スレッドは同じ処理関数をインストールしている。スレッドをクリックすることは1回の可能な配信機会を表すが、カーネルがクリック順でスレッドを選択する保証はない。図ではブロック解除と配信を分離しているが、実際のシステムではブロック解除直後に配信される可能性がある。通常のシグナルを繰り返し送信するとマージされる。個別 pending と共有 pending が同時に存在する場合、この図では個別項目を優先して取得する。","privatePending":"スレッド個別 pending","process":"全体プロセス","queuedGroup":"シグナルはプロセス共有 pending に進入する。それは適切なスレッドの1つにのみ配信され、各スレッドにブロードキャストされることはない。","queuedThread":"シグナルは A の個別 pending に進入する。B、C がブロックしていなくても、A の代わりに引き受けることはできない。","reset":"シグナルとカウントをクリア","scope":"モデル範囲と規約","send":"SIGUSR1 を送信","target":"送信先","threadA":"スレッド A のみ"}}
シグナルを一元処理するには、ワーカースレッドを作成する前に対象のシグナルセットをブロックし、新しいスレッドがブロックマスクを継承するようにし、専用のスレッドで同期的に消費させる。こうすることで受信位置がプログラムによって制御され、どのスレッドがたまたま選択されたかに依存しなくて済む。
SIGKILL と SIGSTOP は処理関数をインストールしたり無視したりすることはできず、sigaction() で処理を変更しようとすると失敗する。これらをブロック集合に追加しても、実際にブロックされるわけではない。ただし、実行状態の制約を受ける。SIGKILL はロック保持中のカーネルスタックを任意に放棄するものではなく、タスクは致命的シグナルを処理できるパスまで待機する必要があるかもしれない。TASK_KILLABLE などの待機設計は、対応する待機が致命的シグナルによって中断可能かどうかを決定する。signal(7)
2. 標準シグナルをイベントカウンターとして使えない理由
SIGUSR1 をブロックした状態で5回連続で送信し、ブロック解除時に処理関数が5回実行されることを期待してはいけない。同じ番号の標準シグナルがすでに保留されている場合、その後の発生は通常マージされる。それは「この種のエベントに注意が必要」という状態を保持するだけである。
同じ回数送信しても、保留状態の保持方法は異なる
標準シグナル:同じ番号が保留中ならマージ
S
S
S
S
S
1つの保留 SIGUSR1
同じ番号のリアルタイムシグナル:各キューイング値を保持
10
11
12
10
11
12
前提:シグナルはブロックされており消費されていない。リアルタイムキューイングは成功。キューには上限があり、無限の信頼できるメッセージチャネルではない。
SIGCHLD:1回の通知、回収には複数回の操作が必要
SIGCHLD の正しい消費方法はここから導き出せる。1回の通知が届いたら、適切なノンブロッキング wait を繰り返し呼び出し、現在回収可能な子プロセスの状態をすべて取り出す。1回だけ回収すると、他のプロセスはすでに終了しているが、それを知らせる独立した後の SIGCHLD は残っていない可能性がある。シグナルがマージされた場合でも、終了状態はカーネルの子プロセス待避機構によって保存される。通知と事実の記録は異なる対象に属する。
リアルタイムシグナル:順序付きキューイングでもリソース上限は存在する
リアルタイムシグナルは sigqueue() を介して値を付与でき、同じシグナルの複数のインスタンスは送信順に配信される。図では 10、11、12 の3つの値が保持されているが、キュー容量は限られており、送信は EAGAIN で失敗する可能性がある。
異なるリアルタイムシグナル番号には優先順位があり、すべての番号を1つのグローバル FIFO に合成することはできない。SIGRTMIN + n を使用する際は SIGRTMAX を確認すること。glibc はスレッドライブラリ用にリアルタイムシグナルを予約しているため、SIGRTMIN を 34 と固定してはならない。signal(7)
連続送信後、2種類のシグナルをそれぞれ取り出す。図では意図的に小容量を使用して送信失敗を露呈させている。これは Linux の固定キュー上限ではない。
図を準備しています 再読み込み
3回送信したら、3回取り出せるか? 同じ番号の通常シグナルは1つの pending 状態にマージされ、同じ番号のリアルタイムシグナルは個別にキューイングされ、順序通りに消費される。キューリソースは限られており、送信に失敗した値はキューに入らない。
{"id":"signal-queues","title":"3回送信したら、3回取り出せるか?","summary":"同じ番号の通常シグナルは1つの 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":"キューがいっぱいになったことを示す。今回のリアルタイム送信は失敗した。通常シグナルは依然として1つの pending ビットのみ。失敗した値はキューに入らない。","heading":"3回送信したら、3回取り出せるか?","initial":"まず連続送信し、それぞれ取り出す。1つの pending ビットと複数の順序付きキュー項目を比較する。","intro":"2種類のシグナルをどちらもブロックした状態にする。各送信で通常シグナルと数値付きリアルタイムシグナルを同時に発生させ、それぞれ同期的に取り出す。","lastRealtime":"前回取り出したリアルタイム値","lastStandard":"前回取り出した通常シグナル","note":"sigtimedwait などの同期インターフェースを使用して、ブロックされたシグナルを消費する。通常シグナルは kill で送信し、各送信にペイロードを偽造しない。リアルタイムキュー容量 4 は図示のためのものであり、Linux の上限はリソース制限によって制御され、sigqueue は EAGAIN を返す可能性がある。ここでは同じリアルタイムシグナルのみを比較する。","pendingCount":"取り出すべき数","realtime":"同じリアルタイムシグナル · FIFO","rejected":"リアルタイムシグナル送信失敗","reset":"キューをクリア","scope":"モデル範囲と規約","sendOne":"各1回送信","sendThree":"各3回連続送信","sent":"通常シグナルは1つの pending ビットのみを残す。同じ番号のリアルタイムシグナルは、受け取った各値を順序通りに保存する。","standard":"通常シグナル · SIGUSR1","take":"1つ取り出す","taken":"取り出した結果は下部に残り、比較しやすい。通常シグナルは1回取り出すとクリアされる。リアルタイムシグナルは同じ番号の FIFO で項目ごとに取出される。"}}
実験:1回の保留と3つのキューイング値
以下では、シングルスレッドプロセス内でシグナルをブロックし、5回の標準シグナルのマージと3つの同じ番号のリアルタイムシグナルの順序付き取出しをそれぞれ検証する。非同期処理関数はインストールせず、他のプロセスにはシグナルを送信しない。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、一時的に保存されたフレーム、およびマスクからこのエントリと復帰を観察する。
図を準備しています 再読み込み
処理関数は誰の実行スレッドを借用するか? 処理関数は受信スレッドのユーザーモードコンテキストで実行される。シグナルフレームは中断時の状態を保存し、rt_sigreturn は PC とマスクを復元し、処理関数によるフラグの変更は保持される。
{"id":"signal-handler-context","title":"処理関数は誰の実行スレッドを借用するか?","summary":"処理関数は受信スレッドのユーザーモードコンテキストで実行される。シグナルフレームは中断時の状態を保存し、rt_sigreturn は PC とマスクを復元し、処理関数によるフラグの変更は保持される。","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":"現在のシグナルマスク","eyebrow":"03 · シグナルの現場","frame":"一時的ユーザーモードシグナルフレーム","handler":"処理関数実行中","handlerNote":"メインプログラムは一時停止し、同じ TID が処理関数を実行中。元の状態はシグナルフレームに残っている。デフォルトでは現在のシグナルが一時的にブロックされる。","heading":"処理関数は誰の実行スレッドを借用するか?","interrupt":"ここで SIGUSR1 を配信","intro":"コード行を直接クリックして中断位置を選択し、シグナルを配信する。同じ TID の実行位置、保存されたフレーム、および復帰後の状態を観察する。","line":"行","main":"メインプログラム実行中","mainNote":"メインプログラムは実行中。配信により現在の PC とマスクが保存され、同じスレッドが処理関数に入る。","note":"図はシグナルがユーザーモードコード位置で配信される瞬間を選択し、アーキテクチャ ABI、代替シグナルスタック、およびネストを省略している。処理関数は volatile sig_atomic_t フラグを 1 に設定する。それは新しいスレッドではない。枠内にはキーワードフィールドのみを示しており、復帰後、一時的フレームは使用されない。コード行をクリックすることは、スナップショット内の中断位置のみを選択するものであり、4つの示唆関数の内部実行をシミュレートするものではない。","reset":"リセット","return":"処理関数復帰 · rt_sigreturn","returnedNote":"rt_sigreturn は元の PC とマスクを復元した。メインプログラムは中断箇所から再開し、処理関数が設定した flag は依然として保持されている。","savedMask":"保存されたマスク","savedPC":"保存された PC","scope":"モデル範囲と規約"}}
なぜ処理関数は通常通りログを出力できないのか
元のスレッドが stdio を実行中で、内部状態がまだ更新されていない状態で、シグナル処理関数が挿入して実行されるとする。ハンドラがさらに 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:まず停止理由を識別し、再開方法を決定する
Linux における ptrace の追跡対象はスレッドである。1つの TID に対して追跡関係を作成しても、他のスレッドが同時に停止するわけではない。デバッガがプロセス全体の一貫したビューを構築するには、スレッドの発見、作成イベント、および各スレッドの停止状態の管理も必要である。
PTRACE_ATTACH は関係を作成し停止リクエストを送信するが、呼び出しの戻りがターゲットのチェック可能な停止状態に入ったことを意味するわけではない。トレース元は対応する状態を待機すべきである。PTRACE_SEIZE はそれ自体ではターゲットを停止せず、その後 PTRACE_INTERRUPT を使用して停止を要求できる。追跡ループは「wait が返ったらレジスタを読む」と単純に書くことはできない。wait は最終的な終了やその他のイベントも報告する可能性があるからである。
ptrace はトレース元と追跡対象スレッド間の停止プロトコルである
追跡関係の確立
ATTACH は停止を要求する。SEIZE はそれ自体ではターゲットを停止しない
1
待機し、停止を分類
wait 状態を使用して、シグナル配信、システムコール、ptrace イベントを区別
2
アクセスが許可された状態のチェックまたは変更
レジスタ、メモリ、イベント情報。他のスレッドがまだ実行中である可能性も考慮
3
再開アクションの選択
CONT / SYSCALL / SINGLESTEP、および実際のシグナルを注入するかどうか
4
次のイベントまたは最終的な終了を処理
ループでプロトコルを駆動。終了状態は通常の停止として扱わない
5
停止理由が再開方法を決定する。すべての SIGTRAP をターゲットに転送すると、プログラム動作が変更される。
以下の表に基づいて基本的な分類を作成し、オプション、スレッドグループ、およびカーネルバージョンの違いを処理できる。
観察された状態 意味と後続アクション
正常終了またはシグナルによる終了 最終状態を消費し、追跡記録をクリーンアップ。再開リクエストを送信しない
通常シグナル配信停止 siginfo をチェックし、抑制するか、再開時にそのシグナルを注入するかを選択
システムコール停止 TRACESYSGOOD を有効にすると、通常 SIGTRAP | 0x80 で識別される。これは分類マーカーであり、実際に転送されるシグナルではない
ptrace イベント停止 wait 状態内のイベントをデコード。例えば clone、exec、exit。イベント停止と最終終了は依然として異なる
ジョブコントロールグループ停止 シグナル配信停止と区別する必要がある。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、ユーザーネームスペース、Yama、または他の LSM ポリシーなどは、許可される操作に影響を与える。
一貫性:1つのスレッドを停止させても、他の書き込み者はまだ動作中
権限が通過しても、一貫したスナップショットが読めるわけではない。トレース元がスレッド A だけを停止させ、スレッド B がまだリンクリストを修正していると仮定する。まず head を読み、次に next を読むと、2回の構造変更を跨ぐ可能性がある。すべての通常スレッドを停止させたとしても、共有メモリを持つ他のプロセスやデバイスがデータを変更する可能性がある。分析ツールは、尽力したサンプリングが必要なのか、それともプログラムによる協調で一貫したスナップショットを提供する必要があるのかを明確にするべきである。
インターフェース境界:TRACE と USER_NOTIF
seccomp の SECCOMP_RET_TRACE は、対応する ptrace オプションの下で seccomp 追跡イベントを発生させることができる。SECCOMP_RET_USER_NOTIF は、専用のリスナー fd を介してユーザーモードの監視プログラムに渡される。これは ptrace インターフェースではない。
これらは同じ複雑なシステム内に存在できるが、通知の受信、応答、およびタスクライフサイクルの処理にはそれぞれプロトコルが異なる。seccomp フィルタ
ユーザーモードの監視者がシステムコール引数内のポインタを読み取り、元のシステムコールを継続させると、典型的な TOCTOU(Time-of-check to Time-of-use)に直面する。チェックの後、他のスレッドがポインタが指すメモリを変更する可能性がある。「パス文字列をチェックしただけ」では、ターゲットが最終的に同じオブジェクトを操作することを保証できない。
ptrace 自体も信頼できるサンドボックスの境界ではない。観察、代行実行、オブジェクト参照、およびアクセス制御の責任を分離して設計すべきであり、「システムコールをインターセプトできる」ことから「任意のシステムコールを安全に認可できる」と直接推論してはならない。seccomp ユーザー通知
7. クイックトラブルシューティング時に問うべき次の層
現場の現象 次のステップのチェック
シグナルに反応がない 送信結果、保留状態、ターゲットスレッドのブロックマスク、処理方法、および実行機会
子プロセスが継続して Z 状態を表示する SIGCHLD 後、回収可能な状態をすべて循環して消費したか
デバッガの再開後に異常終了する 停止分類、およびデバッグイベントを誤って SIGTRAP として注入していないか
これらの入り口は「何が起きたか」を具体的な状態に結びつける。その後、ソースコードをたどって状態変更の位置を探す。
カーネル実装の読解からは、kernel/signal.c の発生/配信パス、kernel/ptrace.c の関係と権限管理、そしてアーキテクチャ固有のシグナルフレームと ptrace レジスタ操作へと進む。内部シグナル集合とユーザーモードの sigset_t のレイアウトは同じ ABI ではなく、メカニズムを理解するために、バージョンごとに古くなりやすい構造定義を複製する必要もない。
プロセスとスレッドのリソース関係はライフサイクル に戻り、シグナルによるウェイクアップ後の CPU 競合はスケジューラ に戻る。