このページの目次
シグナルと ptrace
カバー範囲: シグナルのライフサイクル (生成→配信→処理) → signal_struct/sighand_struct → 信頼性のある/ないシグナル → ptrace メカニズム (PTRACE_ATTACH/PTRACE_SYSCALL/PTRACE_PEEKDATA) → seccomp 排他制御 カーネルバージョン: 2.6 ~ 6.x
概要
シグナルは Unix において最も古い IPC(プロセス間通信)メカニズムの一つであり、統一された例外/イベント通知フレームワークとしても機能します。ユーザーが Ctrl-C を押して SIGINT を発生させる場合、ゼロ除算によって SIGFPE がトリガーされる場合、子プロセスの終了に伴って SIGCHLD が発生する場合など、すべてが同じカーネルパスを通過します。
ptrace は、デバッガ(gdb、strace)やサンドボックス(seccomp user notification)の基盤となるシステムコールであり、あるプロセスが別のプロセスの実行を観察・制御することを可能にします。
シグナルのデータ構造
シグナルセット (sigset_t)
// include/linux/signal_types.h
// シグナルセットはビットマップであり、各ビットがシグナル (1~64) を表す
typedef struct sigset_t;
// 一般的な操作
; // 0でクリア
; // 追加
; // 削除
; // 検査
プロセスごとのシグナル構造体
// include/linux/sched.h
;
// include/linux/sched/signal.h
;
;
// include/linux/sched/signal.h
;
sigaction — シグナル処理アクション
// include/linux/signal_types.h
;
// sa_flags の重要なビット:
// SA_NOCLDSTOP: 子プロセスが停止した際に SIGCHLD を受信しない
// SA_NOCLDWAIT: 子プロセスの終了時に自動的に回収され、ゾンビが発生しない
// SA_SIGINFO: 3引数ハンドラ (sig, siginfo, context) を使用
// SA_RESTART: シグナルによって中断されたシステムコールを自動的に再試行
// SA_NODEFER: シグナル処理中にそのシグナルを自動的にブロックしない
// SA_RESETHAND: 1回処理後に SIG_DFL に戻す (1回限りの実行)
シグナルのライフサイクル
生成 (generate) → 保留 (pending) → 配信 (deliver) → 処理 (handle)
1. 生成: send_signal()
// kernel/signal.c
int
2. 信頼性のないシグナル vs 信頼性のあるシグナル
従来のシグナル (1~31, "信頼性のない"):
- 同じシグナルを複数回送信 → pending ビットマップには 1 回しか記録されない
- カーネルがビットマップが既に設定されていることを検出 → 新しい送信は破棄される
- 古典的な問題: SIGCHLD で複数の子プロセスが同時に終了 → 通知は 1 回だけ受信
リアルタイムシグナル (SIGRTMIN=34 ~ SIGRTMAX=64, "信頼性のある"):
- 送信ごとに sigqueue を割り当ててキューに追加
- 追加データ (siginfo.si_value) を保持可能
- シグナル番号の昇順で配信 (低い番号が優先)
- 同じ番号のシグナルは FIFO (先入れ先出し)
3. 配信: get_signal() → handle_signal()
// arch/x86/kernel/signal.c
// ユーザーモード復帰前 (exit_to_user_mode_loop) に呼び出される:
bool
// ハンドラが SIG_DFL/SIG_IGN 以外の場合:
static void
4. シグナルからの復帰: sigreturn()
// シグナルハンドラの実行終了後、libc が sigreturn() を呼び出す
// → sys_rt_sigreturn() → restore_sigcontext()
// sigframe から保存されたすべてのレジスタを復元
// → プロセスはシグナルによって中断された地点から実行を再開
sigframe はシグナルメカニズムの要諦です——カーネルはユーザースタック上に「スナップショット」を偽造し、ハンドラ実行後に sigreturn によってスナップショットを復元します。これにより、プロセスはハンドラの副作用を除き、何事もなかったかのように実行を継続します。
5. シグナルとシステムコール
シグナルによって中断されたシステムコールの 3 つの処理:
SA_RESTART: カーネルが自動的に再試行 (read/write/wait/... などの「低速」システムコールのみ)
SA_RESTART なし: -EINTR を返し、ユーザー空間で再試行するかどうかを決定
致命的シグナル: プロセスが終了し、戻らない
ERESTARTSYS / ERESTARTNOINTR / ... :
カーネル内部で使用される特殊なエラーコード。ユーザーモード復帰前に以下の変換が行われる:
ERESTARTSYS → SA_RESTART が設定されていれば再試行 / そうでなければ → EINTR
ERESTARTNOHAND → ブロックされているがハンドラがない → 再試行
ERESTART_RESTARTBLOCK → restart_block メカニズムが必要 (例: nanosleep)
SIGKILL と SIGSTOP の上書き不可能性
// kernel/signal.c
// SIGKILL (9) と SIGSTOP (19) は以下によって:
// - ブロックできない (sigprocmask はこれらを無視する)
// - 無視できない (SIG_IGN は無視される)
// - ハンドラで捕捉できない (sa_handler は無視される)
//
// これは設計原則である: システムには常に最終手段として制御を回復する手段がある
// プロセスがすべてのシグナルをブロックしても、SIGKILL は依然として有効
// 例外: TASK_UNINTERRUPTible (D 状態) のプロセス
// プロセスが D 状態(ディスク IO の待機など)でブロックされている場合、SIGKILL は即座に終了できない
// IO が完了するまで待機 → プロセスが実行可能状態に戻る → シグナル配信が有効になる
// TASK_KILLABLE (5.14+) はこれを改善: SIGKILL によってウェイクアップ可能
ptrace
ptrace は、トレーサープロセスがトレシープロセスの実行を制御することを可能にします。gdb(ブレークポイント/ステップ実行)、strace(システムコールのトレース)、seccomp user notification はすべて ptrace を基盤として構築されています。
コア操作
// kernel/ptrace.c
long ;
// 重要なリクエスト:
PTRACE_ATTACH / PTRACE_SEIZE // 対象プロセスへのアタッチ
PTRACE_DETACH // デタッチ
PTRACE_SYSCALL // 各システムコールのエントリ/エクジットで停止
PTRACE_SINGLESTEP // 1命令ずつステップ実行
PTRACE_CONT // 実行を継続
PTRACE_PEEKDATA / PTRACE_POKEDATA // トレシーのメモリを読み書き
PTRACE_GETREGS / PTRACE_SETREGS // レジスタを読み書き
PTRACE_O_TRACESYSGOOD // システムコール停止をマークして区別可能にする (シグナルのビット 7)
PTRACE_O_TRACEFORK / PTRACE_O_TRACEVFORK / PTRACE_O_TRACECLONE
PTRACE_GET_SYSCALL_INFO // 詳細なシステムコール情報を取得 (5.3+)
内部メカニズム
// アタッチフロー:
→
→ task->ptrace = PT_PTRACED を設定
→ task->parent = tracer
→ tracee に SIGSTOP を送信
→ tracee は TASK_TRACED 状態に入る
→ tracer は によって停止通知を取得可能
// シグナルのインターセプト:
// tracee がシグナルを受信すると、カーネルは task->ptrace をチェック:
// → PT_PTRACED: シグナルはまず tracer に送信 (tracer は修正または消費可能)
// → tracer は waitpid() によってシグナル情報を取得
// → tracer はシグナルの通過 (PTRACE_CONT) または無視を決定
// システムコールのトレース:
// PTRACE_SYSCALL → TIF_SYSCALL_TRACE を設定
// → システムコールエントリ: syscall_trace_enter() → ptrace_report_syscall()
// → tracee は TASK_TRACED 状態になり、tracer は waitpid によって通知を取得
// → tracer が復帰 → tracee はシステムコール本体を実行
// → システムコールエクジット: syscall_trace_exit() → 再度停止
// → tracer はシステムコールの戻り値を確認可能
ptrace のアクセスモード
// PTRACE_PEEKDATA: /proc/<pid>/mem の低レベルインターフェースを介して実行
// 1ワードずつ読み取る (ptrace の伝統的な API、効率が低い)
//
// より効率的な方法: /proc/<pid>/mem を直接読み書き (ptrace アタッチは不要!)
// → process_vm_readv() / process_vm_writev()
// → ただし、読み書きのみ可能で、実行の制御はできない
// カーネル実装: ptrace_access_vm() → get_user_pages_remote()
// → ページテーブルを介して tracee の仮想アドレスを自アドレス空間にマップ
// → 直接メモリアクセス (copy_from_user は不要)
ptrace と seccomp の排他制御
seccomp (SECCOMP_SET_MODE_FILTER):
1つのプロセスは、ptrace されているか seccomp filter が設定されているかのいずれかであり、両方を併用することはできない
理由: セキュリティ — seccomp の前提は「自分自身のシステムコールポリシーを制御している」こと
ptrace されている場合、tracer は POKEDATA を通じて seccomp を回避可能
SECCOMP_RET_USER_NOTIF (seccomp user notification):
これは「seccomp フレームワーク内の ptrace 代替手段」
→ 監視プロセスがサンドボックスプロセスのシステムコールをチェック/承認可能
→ PT_PTRACED を設定しないため、seccomp フィルターと共存可能
→ ptrace より軽量: システムコールのみをインターセプトし、シグナルはインターセプトしない
デバッグと観測
# プロセスが現在ブロック中/保留中/無視中のシグナル
|
# SigBlk: ブロックされているシグナル
# SigIgn: 無視されているシグナル
# SigCgt: 捕捉されているシグナル (ハンドラあり)
# SigPnd: スレッドごとの保留中
# ShdPnd: スレッドグループ共有の保留中
# どのプロセスが最も多くのシグナルを保留しているか確認 (シグナルの洪水に襲われている可能性)
for; do
pend=
[ &&
done
# シグナルの配信を strace で確認
# シグナル送信時にカーネルトレースを表示
参考と拡張
- カーネルドキュメント:
Documentation/process/adding-syscalls.rst(シグナルとシステムコールの相互作用), man 7 signal, man 2 ptrace - LWN:
- "A new API for signal handling" (lwn.net/Articles/414618/)
- "Seccomp user notification" (lwn.net/Articles/785746/)
- ソースファイル:
kernel/signal.c— send_signal(), get_signal(), do_signal()kernel/ptrace.c— ptrace_attach(), ptrace_resume()arch/x86/kernel/signal.c— handle_signal(), setup_rt_frame()arch/x86/kernel/ptrace.c— レジスタの読み書き
キーワード: sigaction, sigpending, SIGKILL, SA_RESTART, TIF_SIGPENDING, sigframe, rt_sigreturn, PTRACE_SYSCALL, TASK_TRACED, seccomp