web-dev-qa-db-ja.com

シグナルをキャッチしたときのシステムコールの中断

read()write()の呼び出しに関するmanページを読むと、これらの呼び出しは、ブロックする必要があるかどうかに関係なく、シグナルによって中断されているように見えます。

特に、

  • プロセスは何らかのシグナルのハンドラーを確立します。
  • O_NONBLOCKnotが設定されている(つまり、ブロッキングモードで動作している)デバイスが開かれている(ターミナルなど)
  • 次に、プロセスはread()システムコールを実行してデバイスから読み取り、その結果、カーネル空間でカーネル制御パスを実行します。
  • プロセスがカーネル空間でread()を実行している間に、ハンドラーが先にインストールされたシグナルがそのプロセスに配信され、そのシグナルハンドラーが呼び出されます。

SUSv3 'System Interfaces volume(XSH)' のマニュアルページと適切なセクションを読むと、次のことがわかります。

私。 read()がデータを読み取る前にシグナルによって中断された場合(つまり、使用可能なデータがなかったためにブロックする必要があった場合)、errnoを[EINTR]に設定して-1を返します。

ii。 read()が一部のデータの読み取りに成功した後(つまり、要求の処理をすぐに開始できた場合)に信号によって中断された場合、読み取ったバイト数を返します。

質問A):どちらの場合(ブロック/ブロックなし)でも、信号の配信と処理は、 read()

ケースi。ブロッキングread()は通常、プロセスをTASK_INTERRUPTIBLE状態にして、シグナルが配信されるとカーネルがプロセスをTASK_RUNNING状態にするため、理解できるようです。

ただし、read()がブロックする必要がなく(ケースii。)、リクエストをカーネル空間で処理している場合、シグナルの到着とその処理は、到着と適切な処理のように透過的であると考えていました。 HW割り込みになります。特に、シグナルが配信されると、プロセスは一時的にuserモードに配置され、シグナルハンドラーを実行して返されると想定していました。最終的には、中断されたread()(カーネルスペース内)の処理を終了し、read()が完了するまでコースを実行した後、プロセスは、read()(ユーザースペース内)の呼び出し直後のポイントに戻ります。結果として読み取られたバイト。

しかし、ii。データはすぐに利用できるため、read()が中断されているように見えますが、返されるのは(すべてではなく)データの一部だけです。

これにより、2番目(および最後)の質問が表示されます。

質問B):A)での私の仮定が正しい場合、データがあるためにブロックする必要がないのに、read()が中断されるのはなぜですか?すぐに要求を満たすことができますか?言い換えると、シグナルハンドラの実行後にread()が再開されず、最終的に利用可能なすべてのデータ(結局利用可能であった)が返される結果になるのはなぜですか。

31
darbehdar

要約:信号の受信は透過的ではなく、ケースi(何も読み取らずに中断された場合)でも、ケースii(部分的な読み取り後に中断された場合)でも正しくありません。それ以外の場合は、オペレーティングシステムのアーキテクチャとアプリケーションのアーキテクチャの両方に基本的な変更を加える必要がある場合に備えます。

OS実装ビュー

システムコールがシグナルによって中断された場合にどうなるかを考えます。シグナルハンドラーはユーザーモードコードを実行します。ただし、syscallハンドラーはカーネルコードであり、ユーザーモードコードを信頼しません。それでは、syscallハンドラーの選択肢を見てみましょう。

  • システムコールを終了します。ユーザーコードにどれだけのことが行われたかを報告します。必要に応じて、何らかの方法でシステムコールを再起動するのはアプリケーションコード次第です。これがunixの仕組みです。
  • システムコールの状態を保存し、ユーザーコードがコールを再開できるようにします。これにはいくつかの理由で問題があります:
    • ユーザーコードの実行中に、保存された状態が無効になる可能性があります。たとえば、ファイルから読み取る場合、ファイルが切り捨てられることがあります。そのため、カーネルコードはこれらのケースを処理するために多くのロジックを必要とします。
    • ユーザーコードがsyscallを再開し、その後ロックが永久に保持されるという保証がないため、保存された状態でロックを保持することはできません。
    • カーネルは、syscallを開始する通常のインターフェースに加えて、進行中のsyscallを再開またはキャンセルするための新しいインターフェースを公開する必要があります。これは、まれなケースでは非常に複雑です。
    • 保存された状態は、リソース(少なくともメモリ)を使用する必要があります。これらのリソースは、カーネルが割り当てて保持する必要がありますが、プロセスの割り当てに対してはカウントされます。これは乗り越えられないわけではありませんが、厄介です。
      • シグナルハンドラは、それ自体が割り込みを受けるシステムコールを行う可能性があることに注意してください。したがって、考えられるすべてのsyscallをカバーする静的なリソース割り当てだけを行うことはできません。
      • また、リソースを割り当てることができない場合はどうなりますか?次に、syscallはとにかく失敗する必要があります。つまり、アプリケーションにはこのケースを処理するためのコードが必要になるため、この設計ではアプリケーションコードが単純化されません。
  • 進行中(ただし中断)のままで、シグナルハンドラーの新しいスレッドを作成します。これもまた問題があります:
    • 初期のUNIX実装では、プロセスごとに単一のスレッドがありました。
    • シグナルハンドラは、syscallの靴を踏み越える危険を冒します。これはとにかく問題ですが、現在のUNIXの設計では含まれています。
    • 新しいスレッドにリソースを割り当てる必要があります。上記を参照。

割り込みとの主な違いは、割り込みコードが信頼され、非常に制約されていることです。通常、リソースを割り当てたり、永久に実行したり、ロックを取得してそれらを解放したり、その他の厄介なことをしたりすることはできません。割り込みハンドラーはOSの実装者自身が作成したものなので、何も悪いことはしないことがわかっています。一方、アプリケーションコードは何でも実行できます。

アプリケーション設計ビュー

システムコールの途中でアプリケーションが中断された場合、syscallは完了まで続行する必要がありますか?常にではない。たとえば、ターミナルから行を読み取るシェルのようなプログラムを考えて、ユーザーが_Ctrl+C_を押してSIGINTをトリガーしたとします。読み取りは完了してはなりません。それが信号のすべてです。この例は、バイトがまだ読み取られていなくても、read syscallが割り込み可能でなければならないことを示していることに注意してください。

したがって、アプリケーションがカーネルにシステムコールをキャンセルするように指示する方法が必要です。 UNIXの設計では、これは自動的に行われます。信号によってsyscallが返されます。その他の設計では、アプリケーションがシステムコールを自由に再開またはキャンセルする方法が必要です。

オペレーティングシステムの一般的な設計を考えると、readシステムコールは、プリミティブが意味をなすため、このようになっています。それが意味することは、おおざっぱに、「限界(バッファサイズ)まで可能な限り読み取り、他のことが発生した場合は停止する」ということです。バッファ全体を実際に読み取るには、可能な限り多くのバイトが読み取られるまで、ループでreadを実行する必要があります。これはより高レベルの関数 fread(3) です。システムコールである read(2) とは異なり、freadreadの上のユーザー空間に実装されるライブラリ関数です。これは、ファイルを読み取るアプリケーションや試行中に死ぬアプリケーションに適しています。コマンドラインインタープリターや、接続を適切に調整する必要があるネットワークプログラム、または同時接続があり、スレッドを使用しないネットワークプログラムには適していません。

ループでの読み取りの例は、Robert LoveのLinuxシステムプログラミングで提供されています。

_ssize_t ret;
while (len != 0 && (ret = read (fd, buf, len)) != 0) {
  if (ret == -1) {
    if (errno == EINTR)
      continue;
    perror ("read");
    break;
  }
  len -= ret;
  buf += ret;
}
_

_case i_と_case ii_のほか、いくつかを処理します。

質問Aに答えるには

はい、シグナルの配信と処理はread()に対して完全に透過的ではありません。

途中で実行されているread()は、シグナルによって中断されている間、一部のリソースを占有している可能性があります。そして、シグナルのシグナルハンドラは、別のread()(または他の async-signal safe syscalls )を呼び出すこともできます。したがって、シグナルによって中断されたread()は、使用するリソースを解放するために最初に停止する必要があります。そうしないと、シグナルハンドラーから呼び出されたread()が同じリソースにアクセスし、再入可能な問題が発生します。

read()以外のシステムコールはシグナルハンドラーから呼び出すことができ、read()と同じリソースセットを占有する可能性があるためです。上記の再入可能な問題を回避するために、最も簡単で安全な設計は、実行中に信号が発生するたびに中断されたread()を停止することです。

2
Justin