ネットワークプログラミングにおけるいくつかの小さな問題に関する研究概要

序文

当初は 5 番目の同時実行モデルについて引き続きまとめていく予定でしたが、何かが違うと感じました。1 つは、実際に Reactor 同時実行モードに入ったので、あまり深く理解しておらず、常に欠点があると感じているためです。もう 1 つは、最初に実装したサンプル コードがあまりよく書かれておらず、あまりスタイルが良くありません。そこで、以前に遭遇した比較的断片的な知識ポイントのいくつかを要約し、[1] のネットワーク ライブラリの実装を検討して、単純なネットワーク ライブラリを模倣しながら、引き続き Reactor 部分を要約することを考えました。

ここでいくつか小さな質問があります:
(1)、「衝撃的なグループ問題」について。
(2)、ソケットネットワークプログラミングにおける再利用ポートについて。
(3) select、pol、epoll の原則に関する研究。
(4) ソケット API におけるカーネルの働きとユーザー空間の働きについての議論。
(5)、...

一つずつ調べてみましょう。


1.「衝撃集団問題」について

  衝撃的なグループ問題については、以前のブログでも触れましたが、主な原因は、複数のプロセスまたはスレッドが共通のリソースをブロックして待機していることです。リソースが利用可能になると、これらのプロセスまたはスレッドが一緒に起動されて、リソースの無駄が発生します。パフォーマンスの低下 (主な無駄は、オペレーティング システムがすべてのプロセスをウェイクアップした後、ほとんどのプロセスがリソースを取得できず、ブロッキング キューに戻ることしかできないため、無駄にコンテキスト スイッチングが発生することです)。

衝撃的なグループ現象については、これはマルチスレッドおよびマルチプロセス プログラミングの多くの状況で遭遇する可能性がある広い概念です。特にネットワーク プログラミングでは、一般的に衝撃的なグループが 2 つあります。1 つは accept グループ化で、もう 1 つは IO 多重化 (select、pol、epoll など) のグループ化現象です。以下の記事がとても詳しく説明しているので、ここでは大騒ぎしません。^_^


Linux の衝撃的なグループ効果の詳細な説明 (最も詳細なもの)

ここに私のちょっとした質問があります。IO 多重化という衝撃的なグループ問題の根本的な実装において、「部分的に」ブロックされたプロセスをどのようにウェイクアップするのでしょうか? [1] のステートメントは、一部のプロセスがウェイクアップする前にイベントが処理されたため、カーネルは残りのプロセスをウェイクアップしないということです。enen…、それは理にかなっていますが、オペレーティングシステムの「ウェイクアップ」についての私の現在の理解によれば、それは、条件が満たされたときに、その条件でブロックされているすべてのプロセスを準備完了キューに移動することです(1つの条件でのみブロックされている場合)オンの場合)。
  どなたか詳しい方教えて頂ければ幸いです。

2. ソケットネットワークプログラミングにおける再利用ポートについて

上記の「ショック グループの受け入れ」現象に対して、Linux カーネルはバージョン 3.9 以降の別の解決策である REUSEPORT を導入しました。

「accept」ショック グループでは、複数のプロセスまたはスレッドが同じリスニング ソケット (listenFd) 上で同時にブロックされます。接続が到着すると、カーネルはブロックされたプロセスまたはスレッドを起動します。Linux バージョン 2.6 カーネル以降、カーネルはプロセスまたはスレッドの 1 つを起動します。本質的に、これは最新のカーネルが解決を支援し、ロックによる消費を回避するソリューションです。

ここで言及した再利用ポートは、別の形式で「ショックなグループを受け入れる問題」を解決するのに役立つカーネルのソリューションです (他の機能がある可能性がありますが、ここでは当面説明しません)。ソケットが再利用ポート オプションとして設定されている場合、他のソケットと同じポート (これらも再利用ポート オプションとして設定する必要があります)、具体的には同じ addr+port にバインドできます。より具体的には、カーネルはクライアントからのリクエスト (リクエストの目的は addr+port) をバランスの取れた方法でこれらのソケット X に送信します。以下の図に示すように。

ここに画像の説明を挿入
上の図は [2] から引用したもので、詳細な説明は [2] を参照してください。


enen... 基本的な内容は紹介しましたが、もう少し深く掘り下げると、まだよく理解できないことがいくつかあります。
(1)、なぜカーネルが「accept」という衝撃的な問題をすでに解決しているのか、それでも再利用ポートを作成する必要があるのでしょうか?
(2) 「衝撃的な群衆」問題に対するこれら 2 つのカーネル ソリューションの違いは何ですか?

これらについては後でゆっくり勉強しましょう。

3. select、pol、epoll の原則に関する研究

IO 多重化メカニズムは、同時プログラミングに新しい考え方を提供するため、比較的「美しい」メカニズムとみなすことができます。

ずっと昔、実装されたサーバー側モデルのほとんどは反復サーバーであり、次のサービスは 1 つのサービスが完了した後にのみ実行できました。

次に、マルチプロセス同時サーバーがありますが、マルチプロセスサーバーには 2 つの基本的な問題があります。1 つは比較的高いオーバーヘッド (生成、破棄、切り替えなどを含む)、もう 1 つは通信の問題であり、プロセス間の通信は非常に困難です。ちょっと面倒。

スレッド モデルの登場後、一部の同時実行サーバーはプロセスの代わりにスレッドを使用し始め、マルチスレッド モデルを使用してサーバー側同時実行モデルを開発し、各スレッドはサービス (クライアント接続またはその他の形式の IO) を監視します。マルチスレッド モデルには固有の問題もあります。1 つは、同時接続の数がシステム内のスレッドの最大数によって制限されること (通常、カーネルには最大スレッド数があります)、もう 1 つはスレッドが共有することです。通信(共有データ)には相互排他や同期などの問題(プログラミングの難しさ、ロックやロック解除の消費など)がつきものです(一度デッドロックに陥るとクラッシュしやすくなります^_^|||) 。

その後、どの偉い人が IO 多重化メカニズムを創造的に開発したかはわかりませんが、プロセスまたはスレッドでは、複数の IO (複数のクライアント接続または他の形式の IO リクエスト) が同時にブロックされる可能性があります。いくつかの IO イベント (読み取り可能、書き込み可能、​​または異常) が発生すると、上位層ユーザーに通知するために返されます。このようにして、サーバー側の同時実行性はある程度まで大幅に向上します。

したがって、IO の再利用に基づいて、より一般的な IO 同時実行モデル フレームワークは Reactor モデルです。


IO 多重化の起源と基本概念については前述しましたが、ここでは主に Linux システムにおける select、poll、epoll の基本的な実装原理を紹介します。

3.1 選択の基本原理

私のより抽象的でより広い理解によれば、select の実現は主に次のステップで構成されます。

= "入力を選択してください

(1)、ユーザーは関連する fd_set を提供します (各 fd に関係するイベント (読み取り可能、書き込み可能、​​異常など) を含む) (2)、カーネルは fd_set をユーザー空間からカーネル空間にコピーします (【 を参照してください)
。 4】)
(3)、fd_set をトラバースし、異なる fd の待機キュー内の現在のプロセス (current) をハングさせます。
(4) 対象となる fd イベントが発生しない場合、プロセスはブロックされます (schdule_timeout タイムアウトとシグナル到着はここでは考慮されません)。対象のイベントが到着すると、ブロックされていたプロセスが起動されます。
(5) 目覚めたプロセスは、fd_set 内のイベントを再トラバースし、準備された fd を収集し、最後にそれをユーザー空間にコピーします (返されるのはファイル記述子配列全体であり、すべてが準備完了の IO ではなく、ユーザー コード self が必要です) -判定)

= "戻るを選択してください

以上は私の大まかな理解であり、あまり正確ではない表現や記述も多々あるかもしれませんが、ご容赦ください。


より詳細な議論については、[3]を参照してください。

理解を助けるために、[5] から写真も盗んでみました ^ _ ^|||。

ここに画像の説明を挿入



選択メカニズムの欠点をいくつか示します。

  • fd と一部のカーネル設定を保存するためにビットマップを使用するため、select は最大 1024 fd をサポートします。
  • select を呼び出してカーネルに入る、またはカーネルから戻るたびに、ユーザー空間とカーネル空間の fd_set を転送する必要があり、fd の数が比較的大きい場合、オーバーヘッドは小さくありません。
  • select 内に実装すると、大きなループ (for(; ; ) ) が発生し、ウェイクアップするたびにすべての fd_set コレクションを走査する必要があり (線形スキャン、複雑さは O(N))、オーバーヘッドも「レバレッジ」。
  • 基本的に、select はファイル記述子配列全体をユーザー空間に返します。ユーザー コードは配列を走査してどのファイル記述子に IO イベントがあるかを確認する必要があります。


3.2 投票の基本原則

ポーリングの場合、その基本原理は上記の選択と似ていますが、違いは、ポーリング メカニズムの fd 記述がリンク リストの形式である (選択はビットマップである) ため、サポートされるファイル記述子の最大数は1024。


3.3 epoll の基本原理


epoll の場合、基本的には上記の選択のいくつかの欠点が解決されます。その基本原理は、次の図で簡単に説明できます (⁄(⁄⁄•⁄ω⁄•⁄)⁄ も [5] から「盗用」されています)。

ここに画像の説明を挿入

詳細については、[3]、[5]、および [6] を参照してください。以下は主に、epoll が上記の select のいくつかの欠点をどのように解決するかを要約しています。

(1) select がサポートする fd の数には制限があります。
  epoll では、対象の IO を格納するために赤黒ツリー (上図の赤丸部分) が使用されますが、理論的には fd の数に制限はありません (ハードウェア条件などによって制限される場合があります)。メモリなどはここでは考慮しません)


(2) カーネルに入り、カーネルから戻ることを選択する呼び出しごとに、fd_set の転送コピーが必要です。
   epoll メカニズムには、epoll_ctl システム コールが含まれており、これは fd が初めて挿入されるとき (基本的に、カーネル内のエピアイテムにカプセル化される対象のイベントに挿入される) にのみコピーされ、その後の各 epoll_wait (epoll メカニズム)呼び出しをブロックする)、fd をユーザー空間からカーネルに再コピーしません。
   同時に、カーネルとユーザー空間はメモリの一部も共有します。これは、epoll_wait が戻ったときに IO によって戻された fd_set を格納するために使用されます。これにより、カーネル空間からユーザー空間にデータをコピーするオーバーヘッドが回避されます。


(3)、select で目的のイベントが到着する (プロセスが目覚める) たびにリニアに再スキャンする「恥ずかしさ」について。
  epoll メカニズムには 2 つの重要なデータ構造があります。1 つは赤黒ツリー (上の図の赤丸で囲んだ部分) で、ユーザーが登録した関心のあるイベントを保存するために使用されます。もう 1 つはレディ キュー (上図の青で囲まれた部分) で、対象のイベントのレディ IO を格納します (epoll_wait() はこのキューからデータを読み取ります)。

  では、なぜ epoll は各リニア スキャンを必要としないのでしょうか? IO イベントが到着すると、登録されているコールバック関数 (epoll_ctl() に登録されている) がそのイベントを準備完了キューに追加するため、select 内の do_select() 関数のように epoll_wait() を再選択する必要はありません。すべてのファイル記述子にわたって。したがって、O(1) の複雑さが達成されます。

(4) select が最終的にファイル記述子の配列を返すという事実についての質問。
   epoll の実装では、レディ キュー (プロセスの準備ができているキューではない) のデータ構造のため、すべての epoll_wait() は基本的にレディ キュー内のデータを待ち、キューからデータをプルします。ユーザー空間 (実際には共有メモリ) であり、最終的に与えられるデータはすべて「本物の」IO リクエストであり、ユーザー コードが判断することは決して許可されません。


IO多重化の原理の概要ですが、最初は真面目にソースコードを見るつもりだったのですが、その時はソースコードに「吸い取られて」しまいました。少し難しくて、大まかに少し読んでみましたが、詳細はまだ理解できていません。当然のことながら、私が気づいていないソース コードの本質もいくつかあります。たとえば、「epoll メカニズムで最も重要なのは、イベント処理に似たコールバック メカニズムだと言っている人もいます。」後でゆっくり体験してください。^_^

4. カーネルの動作とソケット API のユーザー空間の動作についてのディスカッション。

最後の部分では、主にカーネル作業とユーザー空間作業について説明します。

一般的に、非カーネル開発者 (オペレーティング システム) にとって、私たちはほとんどがアプリ開発者です (サブディビジョン レイヤーのみが異なり、フレームワーク クラスのシステム クラスは appd の最下層に依存する可能性が高く、Java Web ビジネス アプリケーションはアプリの上位層に依存する可能性が高くなります)層)。私たちは最終的に、カーネルによって提供されるさまざまなサービス (システム コールとして簡単に理解できます) を呼び出す必要があるため、本質的には、カーネルとアプリケーションが一緒になって、「アプリケーション」のセット全体を構成します (これはユーザー空間とユーザー空間から理解できます)。カーネルスペース)。

ネットワーク プログラミングでよく使用されるソケット API などのシステム API を呼び出すと、カーネルが下部で多くのことを実行します。たとえば、接続/受け入れの際には、カーネル内のネットワーク プロトコル スタックが役立ちます。スリーウェイ ハンドシェイクが自動的に完了します。接続プロセスを待ちます。書き込み (または読み取り) の場合、上位層のユーザーは送信するデータをカーネル バッファーに書き込むだけです (正確には、このステップもカーネルによって行われます。書き込みシステム コールを呼び出すだけです)。カーネルは、TCP プロトコルに従ってリモート エンドにデータを送信するのに役立ちます。

2 つのサーバー プロセス A と B があり、同じ同時実行モデルでサービスを提供するシナリオを想像してください。プロセス A が 10 台のクライアントに接続し、プロセス B が 100 台のクライアントに接続すると、問題が発生します。通常の状況では (深刻に考えないでください。これはよくある状況です。A と B は同じ環境であり、CPU はアイドル状態、...)、一定期間内で、A と B のどちらが最も多くのカーネル時間を費やしているか、またはカーネルがサービス (接続のブロック、読み取りと書き込みのブロックなど) として機能しているかどうか。 ) 長い間?

足の指でそれを考えることができます、それはBに違いありません。一定の期間内に、勤勉な「看護師」として、カーネルはBにもっとミルクを与えなければなりません。ただし、ここで少し注意する必要があります。A と B に同じタイム スライスが割り当てられている場合、B プロセス カーネルがより多くの時間を費やし、ユーザーのタイム スライスが占める時間が少なくなるということです。同様に、A プロセス カーネルが使用するタイム スライスは少なくなり、ユーザー モード タスクを実行するためのタイム スライスが増えます。(もちろん、非同期割り込み中はプロセスのタイム スライスが考慮されないため、一般に B プロセスは非同期割り込みを利用する必要があります)

以上、私自身の理解に基づいてまとめましたので、間違っている点がございましたら、ご指摘いただければ幸いです。^_^|||。


参考

[1]、Linux ショッキング グループ効果の詳細な説明 (最も詳細な説明)
[2]、知っておくべきソケットと TCP 接続プロセス
[3]、select/poll/epoll 原則の調査と概要
[4]、epoll 原則の分析#2 : select & Paul
[5]、select、pol、epoll の原理と違い
[6]、epoll 原理分析 #3: epoll

おすすめ

転載: blog.csdn.net/plm199513100/article/details/112789845