Linux ソケット チャット ルームに基づくマルチスレッド サーバーの問題処理 (02)

      前回の記事の残りの問題に基づいて、さらに分析を実行します。

      サーバーは、子スレッドを作成するときに次のコードを使用します。

 pconnsocke = (int *) malloc(sizeof(int));
  *pconnsocke = new_fd;
  
  ret = pthread_create(&tid, NULL, rec_func, (void *) pconnsocke);
  if (ret < 0) 
  {
   perror("pthread_create err");
   return -1;
  } 

    この新しいソケットを保存するために、なぜメモリの一部を割り当てなければならないのでしょうか?

この問題の原因を説明するには、次のような背景知識が必要です。

  1. Linux が新しいプロセスを作成すると、新しいプロセスはメイン スレッドを作成します。

  2. 各ユーザー プロセスには独自のアドレス空間があります。システムはユーザー プロセスごとに task_struct を作成してプロセスを記述します。実際、task_struct はプロセスを表すためにアドレス空間マッピング テーブルとともに使用されます。

  3. Task_struct は Linux でもスレッドを記述するために使用され、スレッドとプロセスの両方が統合スケジューリングに参加します。

  4. プロセス内の異なるスレッドの実行は、同じプログラムの異なる部分であり、各スレッドは並行して実行され、オペレーティング システムによって非同期にスケジュールされます。

  5. プロセスのアドレス空間はプライベートであるため、プロセス間のコンテキスト切り替え時のシステム オーバーヘッドは比較的大きくなります。

  6. 同じプロセス内で作成されたスレッドは、そのプロセスのアドレス空間を共有します。

これらの基本知識を理解した後、プロセスが子スレッドを作成するときに渡されるパラメーターを見てみましょう。

スタック上のメモリアドレスを直接渡します

まずは子スレッド作成時にローカル変数new_fdのアドレスが渡された場合の状況を分析してみましょう。

写真

上の図に示すように:

  1. スレッドを作成します。図のようにパラメータを渡すと、new_fd がスタック上にあります。子スレッドを作成するときに、new_fd のアドレスを thread1 に渡します。スレッドのコールバック パラメータ arg のアドレスが new_fd のアドレスです。

  2. main 関数は終了せずに常にループするため、new_fd は常にスタック上に存在します。このようにして、new_fd の値 3 を実際に子スレッドのローカル変数 fd に渡すことができるため、子スレッドはこの fd を使用してクライアントと通信できます。

  3. ただし、同時サーバー モデルを設計しているため、クライアントがいつサーバーに接続するかを予測する方法がありません。複数のクライアントが同時にサーバーに接続するという極端な状況に遭遇すると、メイン スレッドを作成する必要があります。複数のサブスレッド。

複数のクライアントが同時にサーバーに接続する

写真

上図に示すように、新しく作成されたすべてのスレッド コールバック関数のパラメーター arg には、new_fd のアドレスが格納されます。クライアント接続間の時間間隔が比較的長い場合には問題はありませんが、極端な場合には、同時実行性の高さが原因でエラーが発生する可能性があります。

極端な呼び出しタイミングを見てみましょう。

最初の一歩:

写真

図1に示すように:

  1. T1 で、クライアント 1 がサーバーに接続すると、サーバーの受け入れ機能によって新しいソケット 4 が作成されます。

  2. T2 時点で、サブスレッド thread1 が作成され、サブスレッド コールバック関数のパラメータ arg は、スタック内の new_fd に対応するメモリを指します。

  3. この時点で、別のクライアントがサーバーへの接続を希望しており、thread1 ページがタイム スライスを使い果たした場合、メイン スレッド サーバーがスケジュールされるとします。

ステップ2:

写真

図1に示すように:

  1. T3 では、メイン スレッド サーバーはクライアントの接続を受け入れます。受け入れ関数は新しいソケット 5 を作成し、子スレッド thread2 を作成します。この時点で、OS は thread2 をスケジュールします。

  2. T4 で、thread2 は arg を通じて new_fd の値 5 を取得し、それを fd に保存します。

  3. T5 でタイム スライスが到着すると、スレッド 1 がスケジュールされ、スレッド 1 は arg を通じて new_fd を読み取ります。この時点で、スタック内の new_fd の値は 5 で上書きされます。

  4. したがって、2 つのスレッドが同じ fd を同時に使用する状況が発生します。

この状況が発生する確率は非常に低いですが、絶対に発生しないというわけではなく、Yiyijun は実際のプロジェクトを解決する際にこのバグに遭遇しました。

ヒープメモリアドレスを渡す

ヒープのアドレスを渡す方法を使用すると、次の図が表示されます。

写真

  1. T1 で、クライアント 1 がサーバーに接続すると、サーバーの accept 関数は新しいソケット 4 を作成し、ヒープ内のメモリの一部を適用し、ポインタ pconnsocke を使用してメモリを指し、4 をヒープに保存します。

  2. T2 では、サブスレッド thread1 が作成され、サブスレッド コールバック関数のパラメーター arg は、ヒープ内の pconnsocke によって示されるメモリを指します。

  3. この時点で、別のクライアントがサーバーへの接続を希望しており、thread1 ページがタイム スライスを使い果たした場合、メイン スレッド サーバーがスケジュールされるとします。

  4. T3 では、メイン スレッド サーバーはクライアントの接続を受け入れます。受け入れ関数は新しいソケット 5 を作成し、ヒープ内のメモリの一部を適用し、ポインタ pconnsocke でメモリを指し、5 をヒープに保存して、サブスレッド thread2。

  5. T4 では、thread2 は arg を介してヒープ内の pconnsocke が指すメモリを指します。ここでの値は 5 で、fd に格納されます。

  6. T5 で、タイム スライスが到着し、スレッド 1 がスケジュールされ、スレッド 1 が fd から arg まで読み取ります。このとき、ヒープ内のデータ ビットは 5 です。

  7. 2 つのスレッドが同時に同じ fd を使用する状況は発生しません。

この知識ポイントは少し隠されているので、読者の皆様には使用する際にはより注意していただきたいと思います。

おすすめ

転載: blog.csdn.net/weixin_41114301/article/details/133384304