5つのLinuxioモデル:ブロッキングIO、非ブロッキングIO、多重化IO、非同期IO、信号駆動型IO

目次

1.ブロッキングIO(ブロッキングIO)

2.ノンブロッキングIO(ノンブロッキングIO)

3.多重化IO(IO多重化)

4.非同期IO(非同期I / O)

5.信号駆動型IO(信号駆動型I / O、SIGIO)

6.コントラスト


1.ブロッキングIO(ブロッキングIO)

Linuxでは、すべてのソケットがデフォルトでブロックされています。一般的な読み取り操作プロセスは次のとおりです。

ユーザーが読み取りシステムコールを呼び出すと、カーネルはIOの最初の段階であるデータの準備を開始します。ネットワークIOの場合、最初はデータに到達していないことが多く(たとえば、完全なデータパケットが受信されていない)、この時点でカーネルは十分なデータが到着するのを待つ必要があります。ユーザープロセス側では、プロセス全体がブロックされます。カーネルは、データの準備ができるまで待機すると、データをカーネルからユーザーメモリにコピーし、カーネルが結果を返し、ユーザープロセスがブロック状態を解放して再起動します。

したがって、IOのブロックの特徴は、IO実行の両方の段階(データの待機とデータのコピー)がブロックされることです。

send()やrecv()などのインターフェースはすべてブロックされています。これらのインターフェースを使用すると、サーバー/クライアントモデルを簡単に構築できます。以下は、単純な「1つの質問と1つの回答」サーバーです。

ほとんどのソケットインターフェイスがブロックしています。いわゆるブロッキングインターフェイスとは、システムコール(通常はIOインターフェイス)がコール結果を返さず、現在のスレッドをブロックしたままにし、システムコールが結果を取得するか、タイムアウトエラーが発生した場合にのみ戻ることを意味します。

実際、特に指定がない限り、ほとんどすべてのIOインターフェイス(ソケットインターフェイスを含む)がブロックしています。これはネットワークプログラミングに大きな問題をもたらします。たとえば、send()を呼び出している間、スレッドはブロックされます。この間、スレッドは操作を実行したり、ネットワーク要求に応答したりできなくなります。

簡単な改善点は、サーバー側でマルチスレッド(またはマルチプロセス)を使用することです。マルチスレッド(またはマルチプロセス)の目的は、各接続に独立したスレッド(またはプロセス)を持たせることで、1つの接続をブロックしても他の接続に影響を与えないようにすることです。マルチプロセスとマルチスレッドのどちらを使用するかについての特定のモデルはありません。従来の意味では、プロセスのコストはスレッドのコストよりもはるかに高いため、同時により多くのクライアントにサービスを提供する必要がある場合、複数のプロセスを使用することはお勧めしません。単一のサービス実行機関が必要な場合大規模または長期のデータ操作やファイルアクセスなど、より多くのCPUリソースを消費するために、プロセスはより安全になります。通常、pthread_create()は新しいスレッドを作成するために使用され、fork()は新しいプロセスを作成するために使用されます。

上記のサーバー/クライアントモデルは、より高い要件を提示すると想定しています。つまり、サーバーが複数のクライアントに同時に質疑応答サービスを提供できるようにします。したがって、次のモデルがあります。

上記のスレッド/時間の凡例では、メインスレッドはクライアントの接続要求を継続的に待機します。接続がある場合は、新しいスレッドが作成され、前の例と同じ質疑応答サービスが新しいスレッドで提供されます。

多くの初心者は、ソケットが複数回受け入れることができる理由を理解していない可能性があります。実際、ソケットの設計者は、accept()が新しいソケットを返すことができるように、意図的にマルチクライアントの状況の前兆を残す場合があります。これがacceptインターフェースのプロトタイプです:

int accept(int s, struct sockaddr *addr, socklen_t *addrlen);

入力パラメータsは、socket()、bind()、listen()から継承されたソケットハンドル値です。bind()とlisten()を実行した後、オペレーティングシステムは指定されたポートですべての接続要求の監視を開始し、要求がある場合、接続要求は要求キューに追加されます。accept()インターフェースの呼び出しは、ソケットの要求キューから最初の接続情報を抽出し、sと同様の新しいソケット戻りハンドルを作成することです。新しいソケットハンドルは、後続のread()およびrecv()の入力パラメーターです。現在リクエストキューにリクエストがない場合、accept()は、リクエストがキューに入るまでブロッキング状態になります。

上記のマルチスレッドサーバーモデルは、複数のクライアントに質問と回答のサービスを提供するという要件を完全に解決しているように見えますが、常にそうであるとは限りません。数十万の接続要求に同時に応答する場合、マルチスレッドとマルチプロセッシングの両方がシステムリソースを大幅に占有し、システムの外部への応答効率を低下させ、スレッドとプロセス自体が一時停止状態になる可能性が高くなります状態。

多くのプログラマーは、スレッドプールまたは接続プール」の使用を検討するかもしれません「スレッドプール」は、スレッドの作成と破棄の頻度を減らすことを目的としています。適切な数のスレッドを維持し、アイドル状態のスレッドが新しい実行タスクを再び実行できるようにします。「接続プール」は、接続のバッファプールを維持し、既存の接続を可能な限り再利用し、接続の作成と終了の頻度を減らします。これらのテクノロジーはどちらもシステムのオーバーヘッドを大幅に削減でき、websphere、tomcat、さまざまなデータベースなど、多くの大規模システムで広く使用されています。ただし、「スレッドプール」および「接続プール」テクノロジは、IOインターフェイスへの頻繁な呼び出しによって引き起こされるリソースの占有をある程度軽減するだけです。さらに、いわゆるプール」には常に上限があり、上限よりもはるかに高い要求があった場合、外部の影響からなるプールシステムは、プールの効果がそれほど良くない場合と同じである必要があります。そのため、顔のサイズに応じてプール」の使用を検討し、音に合わせてプール」のサイズを決める必要があります  

上記の例で同時に発生する可能性のある数千または数万のクライアント要求に対応して、「スレッドプール」または「接続プール」はプレッシャーの一部を軽減する可能性がありますが、すべての問題を解決できるわけではありません。つまり、マルチスレッドモデルは小規模なサービスリクエストを簡単かつ効率的に解決できますが、大規模なサービスリクエストに直面すると、マルチスレッドモデルでもボトルネックが発生します。ノンブロッキングインターフェイスを使用して、次のことを試みることができます。この問題を解決します。

2.ノンブロッキングIO(ノンブロッキングIO)

Linuxでは、ソケットを設定することで非ブロッキングにすることができます。非ブロッキングソケットで読み取り操作を実行する場合のプロセスは次のとおりです。

この図から、ユーザープロセスが読み取り操作を発行したときに、カーネル内のデータの準備ができていない場合、ユーザープロセスはブロックされず、すぐにエラーが返されることがわかります。ユーザープロセスの観点からは、読み取り操作を開始した後、待機する必要はありませんが、すぐに結果を取得します。ユーザープロセスは、結果がエラーであると判断すると、データの準備ができていないことを認識しているため、読み取り操作を再度送信できます。カーネル内のデータの準備が整い、ユーザープロセスからシステムコールを再度受信すると、すぐにデータをユーザーメモリにコピーしてから戻ります。したがって、非ブロッキングIOでは、ユーザープロセスは実際には継続的である必要があります。カーネルデータの準備ができているかどうかを事前に確認してください。 

非ブロッキング状態では、recv()インターフェイスは呼び出された直後に戻り、戻り値はさまざまな意味を表します。この例のように、

  • recv()の戻り値が0より大きい場合、これはデータが受信されたことを意味し、戻り値は受信されたバイト数です。
  • recv()は0を返し、接続が正常に切断されたことを示します。
  • recv()は-1を返し、errnoはEAGAINに等しく、recv操作が完了していないことを示します。
  • recv()は-1を返し、errnoはEAGAINと等しくありません。これは、recv操作でシステムエラーerrnoが発生したことを示します。

非ブロッキングインターフェイスとブロッキングインターフェイスの大きな違いは、呼び出された直後に戻ることです。次の関数を使用して、ハンドルfdを非ブロッキング状態に設定します。

fcntl( fd, F_SETFL, O_NONBLOCK );

以下は、1つのスレッドのみを使用するモデルを示しますが、データが複数の接続から同時に配信されているかどうかを検出し、データを受け入れることができます

    サーバースレッドはループでrecv()インターフェイスを呼び出すことができ、単一のスレッドですべての接続のデータ受信を実現できることがわかります。ただし、上記のモデルは推奨されません。なぜなら、recv()を周期的に呼び出すと、CPU使用率が大幅に増加します。さらに、このスキームでは、recv()は「操作が完了したかどうか」をより検出し、実際のオペレーティングシステムはより効率的な検出を提供します。 select()多重化モードなどの「完了」インターフェースは、複数の接続が一度にアクティブであるかどうかを検出でき、サーバースレッドがループ内でrecv()インターフェースを呼び出すことができ、すべての接続を実装できることがわかります。シングルスレッドデータ受信作業。ただし、上記のモデルは推奨されません。なぜなら、recv()を周期的に呼び出すと、CPU使用率が大幅に増加します。さらに、このスキームでは、recv()は「操作が完了したかどうか」を検出するためのものであり、実際のオペレーティングシステムはより効率的な検出を提供します。 select()多重化モードなどの「操作が完了したかどうか」は、一度に複数の接続がアクティブであるかどうかを検出できます。

3.多重化IO(IO多重化)

O多重化という言葉は少し馴染みがないかもしれませんが、select / epollに関しては、おそらく理解できるでしょう。一部の場所では、このIOメソッドはイベントドリブンIO(イベントドリブンIO)とも呼ばれます。select / epollの利点は、単一のプロセスでネットワークに接続された複数のIOを同時に処理できることです。その基本原則は、select / epoll関数が、担当するすべてのソケットを継続的にポーリングし、ソケットにデータが到着すると、ユーザープロセスに通知することです。そのプロセスを図に示します。

ユーザープロセスがselectを呼び出すと、プロセス全体がブロックされると同時に、カーネルはselectが担当するすべてのソケットを「監視」します。いずれかのソケットのデータの準備ができると、selectが返されます。このとき、ユーザープロセスは再度読み取り操作を呼び出して、カーネルからユーザープロセスにデータをコピーします。

この図は、ブロッキングIOの図と大差ありません。実際、さらに悪い状況です。ここでは2つのシステムコール(選択と読み取り)が必要であり、IOをブロックすると1つのシステムコール(読み取り)のみが呼び出されるためです。ただし、selectを使用した後の最大の利点は、ユーザーが1つのスレッドで複数のソケットIO要求を同時に処理できることです。ユーザーは複数のソケットを登録し、selectを継続的に呼び出してアクティブ化されたソケットを読み取ることができます。これにより、同じスレッドで同時に複数のIO要求を処理するという目的を達成できます。同期ブロッキングモデルでは、この目標はマルチスレッドによって達成する必要があります。(もう1つの文:したがって、処理される接続の数がそれほど多くない場合、select / epollを使用するWebサーバーは、マルチスレッド+ブロッキングIOを使用するWebサーバーよりもパフォーマンスが向上しない可能性があり、遅延はさらに大きくなる可能性があります。 select / epollの利点は、単一の接続をより高速に処理できることではなく、より多くの接続を処理できることです。)

多重化モデルでは、通常、各ソケットは非ブロッキングに設定されますが、上の図に示すように、ユーザープロセス全体が実際には常にブロックされています。プロセスがソケットIOではなくselect関数によってブロックされているだけです。したがって、select()は非ブロッキングIOに似ています。

ほとんどのUnix / Linuxは、複数のファイルハンドルのステータス変更を検出するために使用されるselect関数をサポートしています。関数プロトタイプとselectの例は、ブロガーによる別のブログ投稿にあります。I/ O多重化のselect、poll、epollの 

以下は、上記の例で複数のクライアントからデータを受信するモデルを再シミュレートします

上記のモデルは、select()インターフェースを使用して複数のクライアントから同時にデータを受信するプロセスのみを説明しています。select()インターフェースは複数のハンドル、読み取りステータス、書き込みステータス、およびエラーステータスを同時に検出できるためです。複数のクライアントが独立した質疑応答サービスを提供するサーバーシステムとして簡単に構築できます。

ここで指摘する必要があるのは、クライアント側のconnect()操作がサーバー側で「読み取り可能なイベント」をトリガーするため、select()はクライアント側からconnect()の動作を検出することもできるということです。

上記のモデルで最も重要な部分は、select()の3つのパラメーターreadfds、writefds、およびexceptfdsを動的に維持する方法です。入力パラメーターとして、readfdsは、connect()を検出する「親」ハンドルを含む、検出する必要のある「読み取り可能なイベント」のすべてのハンドルをマークする必要があります。同時に、writefdsとexceptfdsは、必要なすべての「書き込み可能なイベント」をマークする必要があります。検出される」および「エラーイベント」ハンドル(FD_SET()マークを使用)。出力パラメーターとして、select()によってキャプチャされたすべてのイベントのハンドル値は、readfds、writefds、およびexceptfdsに格納されます。プログラマーは、すべてのフラグビットをチェックして(FD_ISSET()でチェック)、どのハンドルにイベントがあるかを正確に判断する必要があります。

上記のモデルは主に「1つの質問と1つの回答」のサービスプロセスをシミュレートするため、select()がハンドルが「読み取り可能なイベント」をキャプチャすることを検出した場合、サーバープログラムは時間内にrecv()操作を実行し、受信したものに従って準備する必要がありますdataデータが送信され、対応するハンドル値がwritefdsに追加されて、「書き込み可能なイベント」の次のselect()検出の準備が行われます。同様に、select()がハンドルが「書き込み可能なイベント」をキャプチャすることを検出した場合、プログラムはsend()操作を時間内に実行し、次の「読み取り可能なイベント」検出準備の準備をする必要があります。

このモデルの特徴は、各実行サイクルが1つまたはグループのイベントを検出し、特定のイベントが特定の応答をトリガーすることです。このモデルは「イベント駆動型モデル」として分類できます。

他のモデルと比較して、select()を使用するイベント駆動型モデルは、単一スレッド(プロセス)の実行のみを使用し、使用するリソースが少なく、CPUの消費量が少なく、複数のクライアントにサービスを提供できます。単純なイベント駆動型サーバープログラムを構築しようとすると、このモデルには特定の参照値があります。

しかし、このモデルにはまだ多くの問題があります。まず第一に、select()インターフェースは「イベント駆動型」を実装するための最良の選択ではありません。検出されるハンドル値が大きい場合、select()インターフェイス自体が各ハンドルをポーリングするために多くの時間を消費する必要があるためです。多くのオペレーティングシステムは、より効率的なインターフェイスを提供します。たとえば、Linuxはepollを提供し、BSDはkqueueを提供し、Solarisは/ dev / pollを提供します... より効率的なサーバープログラムを実装する必要がある場合は、epollのようなインターフェイスをお勧めします。残念ながら、異なるオペレーティングシステムによって特別に提供されるepollインターフェイスは非常に異なるため、epollと同様のインターフェイスを使用して、より優れたクロスプラットフォーム機能を備えたサーバーを実装することはより困難になります。

第二に、このモデルはインシデント検出とインシデント対応を組み合わせたものです。インシデント対応の執行機関が巨大になると、モデル全体に​​壊滅的な打撃を与えます。次の例では、巨大な実行本体1は、イベント2に応答して実行本体の実行を直接遅延させ、イベント検出の適時性を大幅に低下させます。

幸い、上記の問題を回避できる効率的なイベント駆動型ライブラリが多数あります。一般的なイベント駆動型ライブラリには、libeventライブラリとlibeventの代わりとしてのlibevライブラリが含まれます。これらのライブラリは、オペレーティングシステムの特性に応じて最適なイベント検出インターフェイスを選択し、非同期応答をサポートする信号などのテクノロジを追加します。これにより、これらのライブラリはイベント駆動型モデルの構築に最適です。
実際、2.6以降、Linuxカーネルでは、非同期IOであるaio_read、aio_writeなどの非同期応答をサポートするIO操作も導入されています。

4.非同期IO(非同期I / O)

Linuxでの非同期IOは、ネットワークIOではなく、ディスクIOの読み取りおよび書き込み操作に使用されます。カーネル2.6から導入されました。最初にそのプロセスを見てみましょう

ユーザープロセスが読み取り操作を開始すると、すぐに他のことを開始できます。一方、カーネルの観点からは、非同期読み取りを受信すると、最初にすぐに戻るため、ユーザープロセスへのブロックは生成されません。次に、カーネルはデータ準備の完了を待ってから、データをユーザーメモリにコピーします。これがすべて完了すると、カーネルはユーザープロセスに信号を送信して、読み取り操作が完了したことを通知します。

非同期IOは真に非ブロッキングであり、要求プロセスをブロックすることはないため、同時実行性の高いWebサーバーの実装にとって非常に重要です。

5.信号駆動型IO(信号駆動型I / O、SIGIO)

まず、ソケットが信号駆動型I / Oを実行できるようにし、信号処理機能をインストールします。プロセスはブロックせずに実行され続けます。データの準備が整うと、プロセスはSIGIOシグナルを受信します。シグナル処理関数の入出力操作関数を呼び出して、データを処理できます。データグラムを読み取る準備ができると、カーネルはプロセスのSIGIO信号を生成します。次に、信号処理関数でreadを呼び出してデータグラムを読み取り、データを処理する準備ができたことをメインループに通知するか、すぐにメインループに通知してデータグラムを読み取らせることができます。SIGIO信号がどのように処理されるかに関係なく、このモデルの利点は、データグラムが到着するのを待つ間(第1段階)、プロセスがブロックされることなく実行を継続できることです。selectのブロックとポーリングを排除し、アクティブなソケットがある場合は、登録されたハンドラーによって処理されます。

シグナル駆動型IOの2つの重要な関数signalとsigactionの例については、ブロガーによる別のブログ投稿を参照してください:linux signal:signal and sigaction

6.コントラスト

上記の紹介の後、非ブロッキングIOと非同期IOの違いは依然として非常に明白であることがわかります。非ブロッキングIOでは、プロセスはほとんどの場合ブロックされませんが、それでもプロセスはアクティブにチェックする必要があり、データの準備が完了すると、プロセスはデータをにコピーするために再度recvfromをアクティブに呼び出す必要があります。ユーザーメモリ。

非同期IOは完全に異なります。これは、ユーザープロセスがIO操作全体を他の誰か(カーネル)に渡して完了するようなもので、終了すると他の人がシグナルを送信します。この期間中、ユーザープロセスはIO操作のステータスを確認する必要も、データをアクティブにコピーする必要もありません。

おすすめ

転載: blog.csdn.net/weixin_40179091/article/details/113969925