Open21

Q&A:TCP/IP プロトコルスタック自作 with Rust

nukopynukopy

ネットワークインタフェース層(データリンク層、リンク層):

nukopynukopy

MTU とはなにか

  • MTU, Maximum Trasmission Unit
    • ネットワークインタフェース層(データリンク層)のプロトコル(データリンクプロトコル)において、一度に送信可能なデータのサイズ
    • プロトコルによって MTU の値は異なる
      • e.g. Ethernet (802.3) の標準的な MTU の値は 1500。他、Wi-Fi (802.11)、PPP など。
    • MTU を超えるサイズのデータは送信できない(上位のレイヤでサイズを調整する必要がある)

「データリンク」という用語の言い回しがしっくりきてない。「データリンク層に使われているデバイス / デバイスドライバ」を指している?

用語に慣れていないだけなのかもしれない。

引用元:https://docs.google.com/presentation/d/1ID6ggxASfc_1bWiJfDy1IFKIwzxvfYy8rUBrWYTRFj8/edit?slide=id.gd328c3072b_0_1210#slide=id.gd328c3072b_0_1210

「データリンク」という言葉について

データリンクは「レイヤー(層)」を表す言葉です。OSI参照モデルでいう第2層(Layer 2)に相当し、その層で動くしくみ全体を指します。

  • 役割
    同じネットワーク(リンク)上の機器どうしがビット列を安全にやり取りできるようにする。アドレス指定、再送制御、誤り検出などを担当。
  • プロトコル
  • Ethernet、Wi-Fi (802.11)、PPP などが Layer 2 に属する。
  • ハードウェア
  • Ethernet なら NIC、スイッチといった物理装置が Layer 2 を実現する。
  • MTU
  • 「このリンク(そのデータリンク層プロトコル)で一度に送れる最大データ量」という属性。

つまり「データリンク」とは、ある特定のハードウェアを指す言葉でも、単一のプロトコル名でもなく、Layer 2 で定義される規格・プロトコル群と、それを担うハードウェアの役割をまとめた階層のことです。

MTU(Maximum Transmission Unit)は誰が決めているのか、また、どのタイミングで誰が誰から MTUの値をもらって(または自身で読み込んで)、その大きさにしたがってデータリンク層へ送信するフレーム(e.g. Ethernet フレーム)を作成して、送信しているのか。

TODO

nukopynukopy

MTU を超えたフレームを送信した場合、どういう不具合が起きるのか。また、そもそも MTU を超えたフレームを送信することはできるのか。そもそも MTU を超えたフレームを送信することは原理的に可能か。

TODO

nukopynukopy

ネットワークインタフェース層(データリンク層、リンク層):割り込み

ハードウェア割り込みとは

  • 割り込み, interruption(ハードウェア割り込み)
    • ハードウェアからの要求に応じて CPU で実行中の処理を一時的に中断し別の処理を実行する仕組み。割り込み処理を終えたあとは中断していた処理を再開する。
    • 割り込みによって実行される処理は割り込みハンドラ(interrupt handler)割り込みサービスルーチン(interrupt service routine, ISR) と呼ばれる。
    • ハードウェアによる割り込みの要求が「どのハードウェア(デバイス)によって発生したか」を識別するために、割り込みを行うハードウェアに 割り込み番号(IRQ 番号) を割り当て、IRQ 番号ごとにデバイスドライバの入力関数などを割り込みハンドラとして設定する。これにより、各ハードウェアによる割り込みが起こるたびにあらかじめ設定されたハードウェア毎の割り込みハンドラ / ISR が実行されるようになる。

以下の図だと、ネットワークのハードウェアである NIC(network interface card)にパケット(Ethernet の場合、Ethernet フレーム)が到着すると、CPU に割り込み信号が届き、その信号が持つ割り込み番号からハードウェア(NIC)に対応する割り込みハンドラにパケットが渡される、という処理の流れになる。

nukopynukopy

PDU における「パケット」と広義の「パケット」という言葉

実務的にはコンピュータ間の通信において送受信されるデータの単位を総称して「パケット」と呼ぶことが多い。それで伝わる。なので、「NIC にパケットが届く」は正しい表現だし、実際よく使われるのを見る(それこそ eBPF とかも)。

https://fukabori.fm/episode/111

もちろん「NIC にフレームが届く」も正しい。こっちは PDU を考慮した表現。各プロトコルの PDU を考慮する場面では「フレーム」、「パケット」などの用語を正確に使えば良い(アプリケーション開発だとあまりこういう場面はなさそう)。

KLab の TCP/IP プロトコルスタック自作の資料でも「NIC にパケットが届く」旨の表現を使っている。

https://docs.google.com/presentation/d/1ID6ggxASfc_1bWiJfDy1IFKIwzxvfYy8rUBrWYTRFj8/edit?slide=id.g116e1cdb3d5_0_10#slide=id.g116e1cdb3d5_0_10

PDU における「パケット」とは

  • PDU、プロトコルデータユニット、protocol data unit
    • データ通信において、特定のプロトコル(通信規約)によって送受信されるデータの単位のこと。
    • L2(データリンク層)Ethernet の場合:「フレーム」、慣用的にプロトコルの名前を前につけて**「Ethernet フレーム」**とも呼ぶ。
    • L3(インターネット層 / ネットワーク層)IP の場合:「パケット」「IP パケット」
    • L4(トランスポート層)TCP の場合:「セグメント」「TCP セグメント」
    • L4(トランスポート層)UDP の場合:「データグラム」「UDP データグラム」
    • パケットやフレームなどのヘッダ(、トレイラ)を取り除いたデータの部分は「ペイロード(payload)」と呼ぶ。

広義のパケット:「NIC にパケットが届く」という表現は間違いか?

先述したように「NIC にパケットが届く」というのは正しい表現。

マスタリング TCP/IP 第 6 版では、「パケット」はオールマイティな用語と書いてある。「データ通信で送受信されるデータの単位の総称」が「パケット」であると言える。

あと、データリンク層の流れるデータの単位は Ethernet プロトコルに限らず「フレーム」と呼んで良さそう。

パケット、フレーム、データグラム、セグメント、メッセージ

これらの 5 つの用語は、いずれもデータを表す単位です。おおむね次のような使い分けをします。パケットは、何にでも使えるオールマイティな用語です。フレームは、データリンクのパケットを表すときに使用されます。データグラムは IP や UDP など、ネットワーク層以上でパケット単位のデータ構造を持つプロトコルで使用されます。セグメントはストリームベースの TCP に含まれるデータを表すときに使用されます。メッセージはアプリケーションプロトコルのデータ単位を表すときに使用されます。

引用元:マスタリング TCP/IP 第 6 版(オーム社)p.83

※マスタリング TCP/IP では意外と「PDU」という用語を索引で扱っていなかった

Cloudflare 公式のコンテンツでも思いっきり「パケット」って書いてある。

https://www.cloudflare.com/ja-jp/learning/network-layer/what-is-a-packet/

なので怖がらず「パケット」という用語を気軽に使っていこう。

プロトコルの信頼性の有無による用語の違い:パケット、データグラム、セグメント

プロトコルの信頼性の有無での用語の違いがあるのは知らなかった。これはどこソースなんだろうか。要注意。

データグラム(英: datagram)は、配送成功・到達時間・到達順序がネットワークサービスによって保証されることがないパケット交換網における基本転送単位である。

TCP/IP では再送制御や受信順の保証などが行われる信頼性の高い通信プロトコルにおけるデータの送受信単位のことを「パケット」(packet)、そのような制御が行われない単純なプロトコルにおけるデータの送受信単位のことを「データグラム」(datagram)と呼ぶため、IP における送受信単位は IP データグラムと呼ぶのが正式とされる

Cloudflare の記述を見るとわけわからなくなるので一旦スルー。

「データグラム」とは、パケット交換方式のネットワークで送信されるデータの断片のことです。データグラムには、送信元から送信先までのルーティングを実行するのに十分な情報が含まれています。この定義では、IPパケットはデータグラムの一例です。基本的に、データグラムは「パケット」の代替用語です。

参考

https://www.infraexpert.com/study/networking3.html

https://pcap.it-mem.info/ネットワークの基礎/パケットとフレーム/

nukopynukopy

chatgpt 5 に雑に聞いてみた:これって RFC とかのソースあるのかな? - プロトコルの信頼性の有無による用語の違い:パケット、データグラム、セグメント

長いので折りたたみ。RFC が気になったらまた見にくる。

結論から言うと「信頼性の有無で packet / datagram を使い分ける」というルールは IETF(RFC)の正式ルールではない。用語はRFCごとにこう定義・用法が分かれてます:

公式ドキュメントでの用語

  • IP(v4) は歴史的に “datagram” を使う
    元祖の RFC 791 は「ブロック化されたデータ called datagrams を送る」と明記。つまり IPv4 の単位は IP datagram。 (IETF Datatracker)

  • IP(v6)“packet” を使う
    IPv6 の基本仕様 RFC 8200 は一貫して IPv6 packet という語を使う(IPv6 の単位=packet)。 (IETF Datatracker)

  • UDP“datagram” が正式名
    RFC 768 のタイトルが「User Datagram Protocol」。本文でも “a datagram mode” と明記。信頼性がないことは事実だけど、“datagram=非信頼、packet=信頼”という対置は規格上の定義ではない。 (IETF Datatracker)

  • TCP“segment”
    現行仕様 RFC 9293 で TCP segment と呼ぶ、と明記。 (IETF Datatracker)

  • “packet” の公式な汎用定義
    RFC 1122(Host Requirements)が用語集を持っていて、

    • “datagram” は IP datagram を指す
    • “packet” は インターネット層とリンク層のインタフェースを流れる単位(IPヘッダ+データ)。つまり IPの塊(フラグメントも含む)を指している
      と書かれている。ここでも信頼性の有無では区切らない。 (IETF Datatracker)

「NICに届くのは?」問題

  • NICが受け取るのはリンク層の“フレーム”(Ethernet なら Ethernet frame)。
    そして IP datagram/IP packet はフレームのペイロードに入っている。このカプセル化は、

    • IPv4 over Ethernet を定める RFC 894(“IP datagrams are transmitted in standard Ethernet frames”)
    • IEEE 802(LLC/SNAP)系の RFC 1042
      で規定されている。だから「NICにパケットが届く」は口語としては通じるけど、正確には“フレームが届く”。 (RFC Editor)

まとめ(実務での言い換え)

  • レイヤ4: TCP segment / UDP datagram(RFCの正式用語) (IETF Datatracker)
  • レイヤ3: IPv4 datagram(RFC 791)、IPv6 packet(RFC 8200) (IETF Datatracker)
  • レイヤ2: Ethernet frame(RFC 894/1042 でも “frame” と明記) (RFC Editor)

なので、WikipediaやIT用語辞典の「信頼性あり=packet/なし=datagram」という説明は通俗的まとめとしては分かりやすいけど、RFCの一次情報にその区分はない、が正しい解像度。

必要なら、各ヘッダ(Ethernet/IP/TCP/UDP)を積み重ねた“カプセル化”図も作るよ。短いチートシートも付けようか?

nukopynukopy

Q. 割り込みはいわゆるプリエンプティブなタスク切り替え(コンテキストスイッチが発生する)。ユーザ空間での協調的タスクスケジューリングはよく見るけど、カーネル空間での協調的タスクスケジューリングは存在する?

ハードウェアからの要求に応じて CPU で実行中の処理を一時的に中断し別の処理を実行する仕組み。割り込み処理を終えたあとは中断していた処理を再開する。

プリエンプティブなタスクスケジューリングの反対は協調的タスクスケジューリング。

ユーザ空間でのプリエンプティブなタスクスケジューリングは Go の goroutine(Go 1.14 以降)、協調的タスクスケジューリングは Rust の tokio、Go の goroutine(Go 1.13 以前)。

(こういう記事書けるのすごい)

https://hidetatz.github.io/goroutine_preemption_ja/

カーネル空間での協調的スケジューリングって存在する?

Answer

ChatGPT 5 の回答:

ユーザ空間で tokio のような協調的ランタイムを作れるのは自由があるから」で、「カーネル空間は安全性と応答性のためにプリエンプティブがデフォ。ただし歴史的に協調的だったり、一部限定的に cooperative yield する場所はある

回答

カーネル空間での協調的スケジューリングは基本的には「存在しない(少なくとも一般的ではない)」


1. カーネルがプリエンプティブを好む理由

  • デバイス制御やシステムコールの応答性:ハードウェアやアプリからの要求に即応できないとシステム全体がフリーズする。
  • 安全性:協調的だと「yield を忘れたタスク」が出た瞬間に CPU を独占しちゃう。カーネルレベルでこれは致命的。
  • マルチコア対応:複数 CPU を扱うスケジューラはプリエンプションを前提に設計されることが多い。

なので Linux, Windows, BSD といったモダン OS のカーネルは プリエンプティブ・マルチタスク が基本。


2. 協調的に見えるカーネル内の仕組み

ただし「完全に無い」とは言えなくて、以下のように“協調的っぽい”部分はある:

  • Linux の古いバージョン:2.4 以前はカーネルはノンプリエンプティブだった。ユーザ空間はプリエンプティブでも、システムコールに入ったらカーネルが終わるまで割り込めなかった。つまり協調的だった。
  • カーネルスレッドの cooperative yield:一部のカーネルスレッド(例: Linux の kthread)は自分で schedule() を呼んで明示的に CPU を譲る。これは協調的な挙動。
  • リアルタイム OS (RTOS):組み込み用の RTOS では協調的スケジューリングが使われることがある(実装がシンプル、オーバーヘッドが少ない)。
  • I/O やドライバ内の状態マシン:完全なプリエンプションではなく「状態遷移まで進んだら自発的にスケジューラに戻す」みたいな作りにする場合がある。

3. まとめると

  • 汎用 OS のカーネルは → プリエンプティブが必須。協調的は事実上使われていない。

  • 例外的に存在する場面は:

    • 古い Linux カーネル
    • カーネルスレッドが schedule() を呼ぶ場面
    • 組み込み/RTOS 系 OS(性能・実装簡単さ重視)

👉 つまり「ユーザ空間で tokio のような協調的ランタイムを作れるのは自由があるから」で、「カーネル空間は安全性と応答性のためにプリエンプティブがデフォ。ただし歴史的に協調的だったり、一部限定的に cooperative yield する場所はある」という答えになる。

Linux 2.4 以前の non-preemptive kernel の挙動とか、RTOS (FreeRTOS, Zephyr) の cooperative mode のコード例とかも出せるよ。

nukopynukopy

Q. 「割り込みセーフ」、「割り込み安全」みたいな概念は存在する?

「非同期安全」に類似するような概念として「割り込み安全」みたいな概念は存在する?

ハードウェア割り込みではタスクのコンテキストスイッチが起きるので、非同期のそれとは別で、常に OS が安全性を担保してくれている。なので「割り込み安全」のような概念はそもそもないのではないかと予想。

https://www.jpcert.or.jp/sc-rules/c-sig30-c.html

Answer

なるほど、「割り込み安全」という概念はないけど、OS やデバイスドライバの世界で interrupt context safe という「割り込みハンドラ内で呼んでも安全かどうか」という概念は存在するらしい。

シグナル安全、非同期安全と似た概念に感じる。

  • Interrupt context safe(Linux カーネル用語でよく出る)
    → 「割り込みコンテキストから呼んでもよい関数かどうか」
    例:Linux の spin_lock_irqsave は OK だが、mutex_lock は割り込みコンテキストから呼んではいけない。

プログラミング用語として広まっているのは 「スレッドセーフ」「非同期セーフ(async-signal-safe)」
POSIX でも「async-signal-safe functions」として定義があるけど、「interrupt-safe」という用語は標準化されていない。

理由は予想していた通りで、
割り込み処理に入った時点で OS がコンテキストを保存・復元してくれるので、アプリケーションが「割り込み安全」を意識する場面は基本的にないから。

ただし似た概念は存在する。

OS/ドライバ開発の世界だと「割り込みハンドラ内で呼んでも安全かどうか」を指す言い回しがある。

  • Interrupt context safe(Linux カーネル用語でよく出る)
    → 「割り込みコンテキストから呼んでもよい関数かどうか」
    例:Linux の spin_lock_irqsave は OK だが、mutex_lock は割り込みコンテキストから呼んではいけない。
回答

1. 「割り込みセーフ/割り込み安全」という言葉は一般的には使われない

プログラミング用語として広まっているのは 「スレッドセーフ」「非同期セーフ(async-signal-safe)」
POSIX でも「async-signal-safe functions」として定義があるけど、「interrupt-safe」という用語は標準化されていない。

理由は予想していた通りで、
割り込み処理に入った時点で OS がコンテキストを保存・復元してくれるので、アプリケーションが「割り込み安全」を意識する場面は基本的にないから。


2. ただし似た概念は存在する

OS/ドライバ開発の世界だと「割り込みハンドラ内で呼んでも安全かどうか」を指す言い回しがある。

  • Interrupt context safe(Linux カーネル用語でよく出る)
    → 「割り込みコンテキストから呼んでもよい関数かどうか」
    例:Linux の spin_lock_irqsave は OK だが、mutex_lock は割り込みコンテキストから呼んではいけない。

  • IRQL safe(Windows カーネル用語)
    → Windows では割り込みレベル (IRQL) があり、どのレベルで呼べる関数かが定義されている。
    ドキュメントでは「This function is IRQL safe to call up to DISPATCH_LEVEL」といった書き方をする。

つまりユーザ空間には「async-signal-safe」、カーネル空間には「interrupt-context safe」や「IRQL safe」というチェックポイントがある。


3. まとめ

  • 標準的な用語として「割り込みセーフ」はあまり使われない。
  • ユーザ空間 → 非同期セーフ (async-signal-safe) が対応する概念。
  • カーネル空間 → 割り込みコンテキストで呼んでも安全か(interrupt-context safe) という制約が存在する。
  • OS がコンテキストスイッチを保証するので「割り込み安全」をアプリが意識する必要はないが、ドライバやカーネル内では呼べる/呼べない API の区別が事実上「割り込みセーフ」の代わりに使われている。

予想としてはほぼ当たりで、ただ「カーネル屋は“interrupt context safe”って表現で話してる」って感じだね。

Linux カーネルの Documentation/locking/ あたりのソース引っ張ってこようか?

nukopynukopy

Q. OS のシグナルとは

  • アプリケーションプロセスに対して外部からイベントの発生を通知するための仕組み
  • Ctrl + C でアプリケーションを強制終了できるのもシグナル(SIGINT)で実現している
  • アプリケーションは、シグナルを受信した際に本来の処理を中断して任意の処理を実行することができる
nukopynukopy

Q. IRQ 番号の「IRQ」ってなに?

割り込み要求(Interrupt Request, IRQ)の略。

nukopynukopy

Q. OS のシグナルとは?プロセスがシグナルを受信する仕組みは?

by gpt-5-codex

OS のシグナルは「プロセスへ送る軽量な非同期通知」です。例えるなら、実行中のプロセスに「いまこれをしてほしい」「ここで止まってほしい」と伝える割り込み電話のようなものです。

どこから飛んでくる?

  • カーネル(例: 0 除算で SIGFPE、端末で Ctrl+C を押して SIGINT
  • 他プロセス(kill, pthread_kill, sigqueue などのシステムコール)
  • ハードウェアイベントのラップ(タイマー満了で SIGALRM, 子プロセス終了で SIGCHLD 等)

飛んできた後は?

  1. カーネルが対象プロセス(正しくは「スレッド」)のシグナル待機キューに追加
  2. スレッドがユーザー空間へ戻る直前に、未処理のシグナルをチェック
  3. 各シグナルごとにプロセス設定を参照して挙動を決定
    • 既定アクション(終了・ダンプ生成・無視など)
    • ユーザ定義ハンドラ(sigaction で登録した関数)
    • sigwait で明示的に待っていれば、ハンドラではなく待機中のスレッドへ引き渡す
  4. 「ブロック中のシグナル」なら即座に配信せず保留し、マスク解除や sigwait で処理されるまでキューに残ります。

POSIX スレッドとの関係は?

  • Linux の pthread は 1:1 カーネルスレッドなので、シグナル配信もスレッド単位。
  • シグナルマスク(pthread_sigmask)で「このスレッドでは受け取らない」と設定すれば、届いたシグナルは保留され、別スレッドまたは後のタイミングで処理されます。
  • sigwait は保留キューから指定されたシグナルを1つ取り出して呼び出し元へ返す同期的な受け取り方です。

ポイントは「シグナルは即時実行される命令」ではなく、「カーネルが配信し、プロセス側で定義された処理に従って捌かれる通知」であること。マスクや sigwait を使うと、どのスレッドでいつ処理するかをコントロールできます。

Q. シグナル待機キューはスレッドごとにあるの?struct_task で管理しているの?

つづき

Linux では task_struct ごとにシグナルの保留リストがあり、さらに同じスレッドグループで共有する保留リストも別に持っています。

スレッド単位 (task_struct->pending)

各カーネルスレッド(= pthread の 1 本)は自分専用の struct sigpending を持ち、カーネルがそこへ「このスレッド宛てのシグナル」を積みます。pthread_kill で特定スレッドに送るとここに入る。

スレッドグループ共有 (task_struct->signal->shared_pending)

同じ tgid の仲間で共有する保留リスト。kill(pid, ...) のようにプロセス全体へ送ったシグナルはまずこちらに入れられ、実際にどのスレッドへ配達するかは配信時に決まります(ブロックしていない/停止していないスレッドが選ばれる)。

管理構造

task_struct には struct signal_struct *signal へのポインタがあり、この signal_struct がスレッドグループ全体の情報(共有 pending、マスク、カウントなど)を持ちます。sigpending 自体はビットマップ + リアルタイムシグナル用のキューで、どのシグナルが待ち行列にあるかを管理します。

配信時は「スレッドごとの pending → グループ共有 pending → 次のシグナル」と順にチェックし、対象スレッドがそのシグナルをブロックしていなければハンドラ実行/sigwait へ渡されます。ブロック中なら保留のまま残り、マスク解除や sigwait のタイミングで処理されます。

nukopynukopy

Q. pthread_sigmasksigwait とは?

基本の考え方

  • UNIX 系 OS ではシグナルは非同期に飛んでくる通知で、既定では対象スレッドに即座に配信される。
  • pthread_sigmask を使うと、どのスレッドがどのシグナルを受け取るかを制御できる。ブロックされたシグナルは保留状態になり、マスクを解除するか sigwait などで明示的に受け取るまで配信されない。
  • スレッドを pthread_create で生成するとき、親スレッドが設定しているシグナルマスクを子スレッドも継承する。
  • sigwait は「ブロックしているシグナルが届くまで眠り、届いたらそのシグナル番号を返す」同期的な API。シグナルハンドラを使わず、安全なポイントで処理できる。
    • 原理は別物だけど await みたいなもの?チャネル ch <- signal みたいなイメージを現時点では持ってる。

典型的な用途

  • ワーカースレッドにシグナルを集中させる: すべてのスレッドで対象シグナルをブロックしておき、専用の監視スレッドが sigwait で待つ構成にすると、どのスレッドでシグナルが処理されるかを明確にできる。
  • 段階的な終了処理: SIGHUPSIGTERMsigwait で受け取り、清掃処理を行ってから終了する。ハンドラ内で複雑な処理をしたくないときに有効。
  • シグナルを同期イベントとして扱う: ハンドラに頼らず、スレッド間の通知メカニズムとしてシグナルを利用できる。ただしリアルタイムシグナルやイベント FD など他手段と比較し、必要性を検討する。

サンプルコード

TODO: 理解できたら gist へ

サンプルコードのポイント

  • メインスレッドで SIGHUPSIGINT をブロック → 新規スレッドも同じ状態を継承。
  • サブスレッドは sigwait で SIGHUP を待ち、受信したら終了準備をして pthread_barrier で同期。
  • メインスレッドに SIGINT を送るとカスタムハンドラが動作するが、サブスレッドは sigwait のまま。マスクの効果を確認できる。

これらを動かしながら「どの時点でシグナルが配信され、どのスレッドで処理されるか」を観察すると、シグナルマスクの仕組みと活用方法が直感的に理解できる。

nukopynukopy

Q. sigmask もうちょっと分かりやすく教えて

つづき

sigmask は「このスレッドがどのシグナルを受け取らないようにしておくか」をビットでメモしておくリストです。シグナルのオン・オフ表みたいなもの、と捉えるとわかりやすいです。

  • 各シグナルには番号があり、ビットマスク上の位置が決まっています。sigaddset(&mask, SIGHUP) は「SIGHUP の欄に×を付ける」操作、sigdelset は×を消す操作です。
  • pthread_sigmask(SIG_BLOCK, &mask, NULL) と呼ぶと、「×が付いている欄のシグナルはこのスレッドへ今は配信しないで」とカーネルへ伝えます。届いたシグナルは一旦保留キューに置かれ、のちに×を外すか sigwait で受け取るまで保留されます。
  • 新しく pthread_create したスレッドは、親スレッドのマスク設定をそのままコピーしてスタートします。なので、親で SIGHUP を×にしてから子を起こせば、子も最初から SIGHUP を保留扱いにします。
  • sigwait(mask, &signo) は「この×が付いたシグナルを、保留キューから1つ取ってきて欲しい」という操作です。ここで戻ってくるときに×も同時に消えた扱いになります(つまり、sigwait したスレッドは実際にシグナルを“受け取った”ことになる)。

まとめると、sigmask は「今ブロック中のシグナル一覧」であり、シグナル配信のオン・オフを切り替えるスイッチパネルです。sigaddset/sigdelset でスイッチを操作し、pthread_sigmask でカーネルに「このパネルを使ってください」と渡すイメージで覚えると良いと思います。

nukopynukopy

Q. sigset_t mask はフラグで初期値は 0 で、0 だと全てのシグナルを受け取るってこと?

sigset_t はただの構造体なので、ローカル変数として宣言した瞬間の中身は未定義です。ゼロで初期化される保証はありません。必ず最初に

  • sigemptyset(&mask); … すべてのビットを 0 にする(=何もブロックしない)
  • または sigfillset(&mask); … すべてのビットを 1 にする(=全部ブロックする)

などの初期化関数を呼んでください。

sigprocmaskpthread_sigmask でこの mask を「ブロックする集合」として渡す場合、ビットが 1 になっているシグナルだけがブロック対象になります。したがって初期化後に 0 のままなら、一つもブロックしていない=すべてのシグナルを普通に受け取る状態です。


理解した。sigset_t 型は構造体で、構造体の中にシグナルをマスクするための配列を管理している。


構造体 sigset_t の実装。

sigset_t 型はアーキテクチャごとに実装依存ですが、Linux/glibc では最終的に以下のコードのように __sigset_ttypedef したものになっていて、unsigned long 配列でビットを管理している。

ただし注意点は 2 つ:

  • 配列の中身は宣言した時点では未定義なので、sigemptyset/sigfillset などで必ず初期化すること。
  • この構造やサイズは実装依存なので、直接メンバに触ったりハードコードしたりせずに、POSIX が提供する sigaddset などの API 経由で操作するのが正しい使い方です。
/usr/include/aarch64-linux-gnu/bits/types/__sigset_t.h
#ifndef ____sigset_t_defined
#define ____sigset_t_defined

#define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
  unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;

#endif

なぜ sigset_t では配列でマスクを管理している?整数型を flag として管理していると思ったけど。

シグナルマスクは「どのシグナルをブロックするか」という "集合" を表す必要があるので、1 本の整数では足りないからです。

  • POSIX では扱えるシグナル番号が 1 つではなく多数あり、Linux/glibc では将来の拡張も考慮して最大 1024 個ぶんのビットを用意しています。そのため unsigned long の要素を _SIGSET_NWORDS 個並べたビットマップになっています(64bit 環境なら 128 バイト=1024 ビット)。
  • 配列にしておけば、sigaddset/sigdelset/sigismember といった操作は「該当シグナル番号に対応するビットを立てる・落とす」だけで済み、まとめて sigprocmask に渡してカーネルに適用できる。
  • もし単一の整数だったら 64bit なら 64 種類しか表せませんが、Linux のようにリアルタイムシグナルを含めると 64 を超えるので容量不足になります。配列サイズを増やすことでシグナルの種類が増えても対応できます。

なので見た目は配列ですが、中身は「シグナル番号に対応したビットを並べた巨大なフラグ集合」です。


なるほど完全理解。

nukopynukopy

シグナル番号は誰がどこで定義しているの?

シグナル番号は OS とライブラリが決めています。

  1. POSIX の基本シグナル

    • SIGINT, SIGTERM, SIGHUP などの名前は POSIX 標準で定義されていて、<signal.h> に列挙されています。
    • ただし「番号そのもの」は実装依存。どの番号を割り当てるかは OS / C ライブラリに任されています。
  2. Linux/glibc の場合

    • 実際の番号はカーネルのヘッダ (include/uapi/asm-generic/signal.h など) で決まり、それを基に glibc の <bits/signum.h> が定数を提供します。
    • あなたがプログラムから使う #define SIGINT 2 といった定義は glibc のヘッダから来ています。
  3. リアルタイムシグナル

    • POSIX は SIGRTMINSIGRTMAX という連番の枠だけを決め、具体的な番号範囲は実装に任せています。Linux では 64 個分が予約されており、実際の数値は getconf SIGRT_MIN 等で確認できます。

まとめると、シグナル名は POSIX が決めたものをベースに、カーネルと C ライブラリが具体的な番号を割り当て、その値を <signal.h> からアプリに提供している、という仕組みです。


シグナルの定義が記述されているヘッダファイル見つけた:

/usr/include/aarch64-linux-gnu/bits/signum-generic.h
/* Signal number constants.  Generic template.
   Copyright (C) 1991-2024 Free Software Foundation, Inc.
   This file is part of the GNU C Library.

   The GNU C Library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.

   The GNU C Library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with the GNU C Library; if not, see
   <https://www.gnu.org/licenses/>.  */

#ifndef	_BITS_SIGNUM_GENERIC_H
#define _BITS_SIGNUM_GENERIC_H 1

#ifndef _SIGNAL_H
#error "Never include <bits/signum-generic.h> directly; use <signal.h> instead."
#endif

/* Fake signal functions.  */

#define	SIG_ERR	 ((__sighandler_t) -1)	/* Error return.  */
#define	SIG_DFL	 ((__sighandler_t)  0)	/* Default action.  */
#define	SIG_IGN	 ((__sighandler_t)  1)	/* Ignore signal.  */

#ifdef __USE_XOPEN
# define SIG_HOLD ((__sighandler_t) 2)	/* Add signal to hold mask.  */
#endif

/* We define here all the signal names listed in POSIX (1003.1-2008);
   as of 1003.1-2013, no additional signals have been added by POSIX.
   We also define here signal names that historically exist in every
   real-world POSIX variant (e.g. SIGWINCH).

   Signals in the 1-15 range are defined with their historical numbers.
   For other signals, we use the BSD numbers.
   There are two unallocated signal numbers in the 1-31 range: 7 and 29.
   Signal number 0 is reserved for use as kill(pid, 0), to test whether
   a process exists without sending it a signal.  */

/* ISO C99 signals.  */
#define	SIGINT		2	/* Interactive attention signal.  */
#define	SIGILL		4	/* Illegal instruction.  */
#define	SIGABRT		6	/* Abnormal termination.  */
#define	SIGFPE		8	/* Erroneous arithmetic operation.  */
#define	SIGSEGV		11	/* Invalid access to storage.  */
#define	SIGTERM		15	/* Termination request.  */

/* Historical signals specified by POSIX. */
#define	SIGHUP		1	/* Hangup.  */
#define	SIGQUIT		3	/* Quit.  */
#define	SIGTRAP		5	/* Trace/breakpoint trap.  */
#define	SIGKILL		9	/* Killed.  */
#define	SIGPIPE		13	/* Broken pipe.  */
#define	SIGALRM		14	/* Alarm clock.  */

/* Archaic names for compatibility.  */
#define	SIGIO		SIGPOLL	/* I/O now possible (4.2 BSD).  */
#define	SIGIOT		SIGABRT	/* IOT instruction, abort() on a PDP-11.  */
#define	SIGCLD		SIGCHLD	/* Old System V name */

/* Not all systems support real-time signals.  bits/signum.h indicates
   that they are supported by overriding __SIGRTMAX to a value greater
   than __SIGRTMIN.  These constants give the kernel-level hard limits,
   but some real-time signals may be used internally by glibc.  Do not
   use these constants in application code; use SIGRTMIN and SIGRTMAX
   (defined in signal.h) instead.  */

/* Include system specific bits.  */
#include <bits/signum-arch.h>

/* Biggest signal number + 1 (including real-time signals).  */
#define _NSIG		(__SIGRTMAX + 1)

#endif /* bits/signum-generic.h.  */
/usr/include/aarch64-linux-gnu/bits/signum-arch.h
/* Signal number definitions.  Linux version.
   Copyright (C) 1995-2024 Free Software Foundation, Inc.
   This file is part of the GNU C Library.

   The GNU C Library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.

   The GNU C Library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with the GNU C Library; if not, see
   <https://www.gnu.org/licenses/>.  */

#ifndef _BITS_SIGNUM_ARCH_H
#define _BITS_SIGNUM_ARCH_H 1

#ifndef _SIGNAL_H
#error "Never include <bits/signum-arch.h> directly; use <signal.h> instead."
#endif

/* Adjustments and additions to the signal number constants for
   most Linux systems.  */

#define SIGSTKFLT	16	/* Stack fault (obsolete).  */
#define SIGPWR		30	/* Power failure imminent.  */

/* Historical signals specified by POSIX. */
#define SIGBUS		 7	/* Bus error.  */
#define SIGSYS		31	/* Bad system call.  */

/* New(er) POSIX signals (1003.1-2008, 1003.1-2013).  */
#define SIGURG		23	/* Urgent data is available at a socket.  */
#define SIGSTOP		19	/* Stop, unblockable.  */
#define SIGTSTP		20	/* Keyboard stop.  */
#define SIGCONT		18	/* Continue.  */
#define SIGCHLD		17	/* Child terminated or stopped.  */
#define SIGTTIN		21	/* Background read from control terminal.  */
#define SIGTTOU		22	/* Background write to control terminal.  */
#define SIGPOLL		29	/* Pollable event occurred (System V).  */
#define SIGXFSZ		25	/* File size limit exceeded.  */
#define SIGXCPU		24	/* CPU time limit exceeded.  */
#define SIGVTALRM	26	/* Virtual timer expired.  */
#define SIGPROF		27	/* Profiling timer expired.  */
#define SIGUSR1		10	/* User-defined signal 1.  */
#define SIGUSR2		12	/* User-defined signal 2.  */

/* Nonstandard signals found in all modern POSIX systems
   (including both BSD and Linux).  */
#define SIGWINCH	28	/* Window size change (4.3 BSD, Sun).  */

/* Archaic names for compatibility.  */
#define SIGIO		SIGPOLL	/* I/O now possible (4.2 BSD).  */
#define SIGIOT		SIGABRT	/* IOT instruction, abort() on a PDP-11.  */
#define SIGCLD		SIGCHLD	/* Old System V name */

#define __SIGRTMIN	32
#define __SIGRTMAX	64

#endif	/* <signal.h> included.  */
nukopynukopy

なぜ signum-generic.hsignum-arch.h がある?

signum-generic.hsignum-arch.h は、1 つの <signal.h> を「共通部分」と「アーキテクチャ依存部分」に分けて組み立てるための仕組みです。

  • bits/signum-generic.h

    • どの CPU でも共通に使えるシグナル名・基本的な定義を入れておくファイル。
    • 例: SIGINT, SIGTERM など POSIX で必須のシグナル、NSIG の算出ルールなど。
  • bits/signum-arch.h

    • アーキテクチャ固有の違いを記述するファイル。
    • 例: SIGSTKFLT(x86 のみ)、SIGPWR の有無、SIGRTMIN の値、実際の番号割り当てなど。

glibc はターゲットごとに用意された signum-arch.h を選んで include し、そのあと共通の signum-generic.h を取り込むことで、アーキテクチャごとに微妙に違う番号や追加シグナルを埋め込みつつ、全体として同じ <signal.h> API を提供しています。こうしておけば、新しい CPU 向けにポートするときは signum-arch.h だけ調整すればよく、共通コードを再利用できるわけです。


たしかにコード見ると signum-arch.hsignum-generic.h でインクルードされてた。

/usr/include/aarch64-linux-gnu/bits/signum-generic.h
// ...
/* Include system specific bits.  */
#include <bits/signum-arch.h>
// ...

そんで signum-generic.hsignal.h でインクルードされてた。

/usr/include/signal.h
// ...
#include <bits/signum-generic.h>
// ...

ちなみにどのシグナルが有効かは kill -l で調べることができる。

$ uname -a
Linux toy-tcpip 6.8.0-49-generic #49-Ubuntu SMP PREEMPT_DYNAMIC Sun Nov  3 21:21:58 UTC 2024 aarch64 aarch64 aarch64 GNU/Linux

$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL      5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL    10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM    15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP    20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU    25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO      30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+1247) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-1352) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

https://qiita.com/Kernel_OGSun/items/e96cef5487e25517a576

nukopynukopy

まとめ:シグナル集合(シグナルセット)、シグナルマスク

シグナル集合の定義

シグナルには SIGINT=2、SIGHUP=1 といった番号が振られている。

構造体 sigset_t では、その番号ごとに 1 ビットずつ用意してそれぞれのシグナル番号に対するフラグを 0 / 1 で表す。sigset_t で管理されているシグナル毎のビットの実体は sigset_t__val フィールドで配列として管理されている。

64 bit 環境の場合、

  • _SIGSET_NWORDS = 1024 / (8 * 8) = 2^10 / 2^6 = 2^4 = 16

となり、__val の配列長は 16 となる(unsigned long int __val[16])。

よって、__val フィールドは 64 bit (64 bit 環境下での unsigned long int のビット数) * 16 (__val の配列長) = 1024 bit を管理できることになる。

// 64 bit 環境では、unsigned long は 64 bit
// (sizeof (unsigned long int)) = 8 [byte]
// _SIGSET_NWORDS = 1024 / (8 * 8) = 2^10 / 2^6 = 2^4 = 16
#define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
  // unsigned long int __val[16];
  unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;

ここまでで、各シグナル番号に対応するビット列ができた。この時点ではそれぞれのビットの 0 / 1 が何を表しているかは決めていないので、単なるビット列でしかない。

次に、このビット列が示す意味を定義する。sigset_t が管理するビット列を集合と解釈し、各ビット b_i が「シグナル番号 i を含むなら 1、含まないなら 0」というシグナル番号の集合として定義する。集合の定義は以下のように書ける。ここでは、シグナル番号の集合を S とする。

\begin{aligned} \mathbf{b} &= (b_0, b_1, \dotsc, b_{N_{\mathrm{SIG}}-1}) ,\\ b_i &\in \{0,1\} \quad (0 \le i < N_{\mathrm{SIG}}),\\ S &= \{\, i \mid 0 \le i < N_{\mathrm{SIG}},\; b_i = 1 \,\}. \end{aligned}

この集合をシグナル集合シグナルセットと呼ぶ。

\mathbf{b}sigset_t が管理するビット列を表す。b_i はシグナル番号 i に対応するビットが立っているかどうか(1 なら集合に含む、0 なら含まない)を表す。

ここまで sigset_t で管理するビット列をシグナルの集合として扱えるようになった。

sigaddset 関数は該当する b_i を 1 にしてシグナル i をシグナル集合へ追加し、sigdelset 関数は b_i を 0 に戻して集合から取り除く操作になる。

[WIP] シグナルマスク

TODO: シグナル集合をマスクとして使うと、上手くシグナルハンドラを登録できるよの話

この集合を sigprocmask に渡すと、「チェックが付いているシグナルはブロックする」というシグナルマスクとしてカーネルに伝わる。