Linux ソースコードからのソケット (TCP) バインドの詳細な説明

Linux ソースコードからのソケット (TCP) バインドの詳細な説明

1. 最も単純なサーバー側の例

ご存知のとおり、サーバー側ソケットを確立するには、ソケット、バインド、リッスン、受け入れの 4 つのステップが必要です。

コードは次のとおりです。

void start_server(){
    // サーバーファイルディスクリプション
    int sockfd_server;
    // fd を受け入れる 
    整数 sockfd;
    呼び出しエラー;
    構造体 sockaddr_in sock_addr;

    sockfd_server = socket(AF_INET、SOCK_STREAM、0);
    メモリセット(&sock_addr,0,sizeof(sock_addr));
    sock_addr.sin_family = AF_INET;
    sock_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    sock_addr.sin_port = htons(SERVER_PORT);
    // これが今日の焦点です。
    call_err = bind(sockfd_server, (struct sockaddr*)(&sock_addr), sizeof(sock_addr));
    呼び出しエラー == -1 の場合{
        fprintf(stdout,"バインドエラー!\n");
        終了(1);
    }
    // 聞く
    call_err = listen(sockfd_server、MAX_BACK_LOG);
    呼び出しエラー == -1 の場合{
        fprintf(stdout,"listen error!\n");
        終了(1);
    }
}

まず、socket システム コールを通じてソケットを作成します。このシステム コールでは、SOCK_STREAM が指定され、最後のパラメーターは 0 です。これは、通常の TCP ソケットが確立されることを意味します。ここでは、TCP ソケットに対応する ops、つまり操作関数を直接指定します。

2. バインドシステムコール

bind は、ソケットにローカル プロトコル アドレス (プロトコル:IP:ポート) を割り当てます。たとえば、32 ビットの IPv4 アドレスまたは 128 ビットの IPv6 アドレス + 16 ビットの TCP または UDP ポート番号。

#include <sys/socket.h>
// 成功した場合は 0 を返し、エラーが発生した場合は -1 を返します
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

さて、Linux ソース コードの呼び出しスタックに直接進みましょう。

バインド

// システムコールからの戻り値はglibcのINLINE_SYSCALLでラップされます

// エラーが発生した場合は、戻り値を-1に設定し、システムコールの戻り値の絶対値をerrnoに設定します。

|->INLINE_SYSCALL (バインド......);

|->SYSCALL_DEFINE3(バインド......);

/* 対応する記述子 fd が存在するかどうかを確認し、存在しない場合は -BADF を返す

|->sockfd_lookup_light

|->sock->ops->bind(inet_stream_ops)

|->inet_bind

|->AF_INET 互換性チェック

|-><1024 ポート権限チェック

/* バインドポート番号のチェックまたは選択(バインドが0の場合)

|->sk->sk_prot->get_port(inet_csk_get_port)

2.1、inet_bind

inet_bind 関数は主に 2 つの操作を実行します。1 つはバインドが許可されているかどうかを検出すること、もう 1 つは使用可能なポート番号を取得することです。ここで注目する価値がある。バインドするポート番号を 0 に設定すると、カーネルはバインドに使用可能なポート番号をランダムに選択するのに役立ちます。

// システムが利用可能なポート番号をランダムに選択するようにします sock_addr.sin_port = 0;
call_err = bind(sockfd_server, (struct sockaddr*)(&sock_addr), sizeof(sock_addr));

inet_bindのプロセスを見てみましょう

CAP_NET_BIND_SERVICE はポート番号 1024 未満で必要なので、ポート 80 をリッスンする場合 (たとえば、nginx を起動する場合)、ルート ユーザーを使用するか、実行可能ファイルに CAP_NET_BIND_SERVICE 権限を付与する必要があることに注意してください。

ルートを使用する

または

setcap cap_net_bind_service=+eip ./nginx

バインドにより、アドレス 0.0.0.0 へのバインドが可能になります。これは INADDR_ANY (通常使用される) であり、カーネルが IP アドレスを選択することを意味します。私たちに最も直接的な影響は、以下の図に示されています。

次に、より複雑な関数、つまり利用可能なポート番号を選択するプロセス、inet_csk_get_portを見てみましょう。
(sk->sk_prot->get_port)

2.2、inet_csk_get_port

最初のセクションでは、バインドポートが0の場合、利用可能なポート番号をランダムに検索します。

ソースコード上では、コードの最初のセクションはポート番号0の検索プロセスです。

// snum が 0 に指定されている場合、ポートはランダムに選択されます inet_csk_get_port(struct sock *sk, unsigned short snum)
{
	......
	// ここで net_random() は疑似乱数である prandom_u32 を使用します。smallest_rover = rover = net_random() % residual + low;
	最小サイズ = -1;
	// snum=0、ポートのブランチをランダムに選択 if(!sum){
		// カーネルパラメータ /proc/sys/net/ipv4/ip_local_port_range に対応する、カーネルによって設定されたポート番号の範囲を取得します。 
		inet_get_local_port_range(&low,&high);
		......
		する{
			if(inet_is_reserved_local_port(ローバー)
				goto next_nonlock; // 予約済みのポート番号を選択しない......
			inet_bind_bucket_for_each(tb、&head->chain) は、
				// 選択したいポートローバーと同じポートが同じネットワーク名前空間に存在します
				if (net_eq(ib_net(tb), net) && tb->port == rover) {
					// 既存の sock と新しい sock の両方で SO_REUSEADDR が有効になっており、現在の sock ステータスは listen ではありません
					// または // 既存の sock と新しい sock の両方で SO_REUSEPORT が有効になっていて、両方とも同じユーザーである if (((tb->fastreuse > 0 &&
					      sk->sk_reuse &&
					      sk->sk_state != TCP_LISTEN) ||
					     (tb->fastreuseport > 0 &&
					      sk->sk_reuseport &&
					      uid_eq(tb->fastuid, uid))) &&
					    (tb->num_owners < 最小サイズ || 最小サイズ == -1)) {
					   // ここでは、num_owners が最も小さいポート、つまり同時バインドまたはリッスン要求の数が最も少ないポートを選択します。
					   // so_reuseaddr/so_reuseport を有効にすると、ポート番号 (port) が複数のプロセスで同時に使用される可能性があるため、smallest_size = tb->num_owners;
						最小ローバー = ローバー;
						if (atomic_read(&hashinfo->bsockets) > (高 - 低) + 1 &&
						    !inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, false)) {
						    // このブランチに入ると、使用可能なポート番号が不足していることを示します。同時に、現在のポート番号は以前に使用したポートと競合しないため、このポート番号(最小のもの)を選択します。
							snum = 最小ローバー;
							tb_found に移動します。
						}
					}
					// ポート番号が競合しない場合は、このポートを選択します if (!inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, false)) {
						snum = ローバー;
						tb_found に移動します。
					}
					次へ進む;
				}
			壊す;
			// 利用可能なポートがすべて通過するまで
		} while (--remaining > 0);
	}
	.......
}

bind を使用するときにランダムなポート番号を使用することはほとんどないため (特に TCP サーバーの場合)、このコードについてコメントします。通常、一部の特殊なリモート プロシージャ コール (RPC) のみが、ランダムなサーバー側のランダム ポート番号を使用します。

2番目のセクションではポート番号を検索するか、すでに指定されている

持っている番号:
	inet_bind_bucket_for_each(tb、&head->chain) は、
			if (net_eq(ib_net(tb), net) && tb->port == snum)
				tb_found に移動します。
	}
	tb = NULL;
	tb_not_found へ移動
見つかった:
	// このポートがバインドされている場合
	if (!hlist_empty(&tb->owners)) {
		// 再利用を強制するように設定されている場合、(sk->sk_reuse == SK_FORCE_REUSE) であれば直接成功します。
			成功へ進む;
	}
	if (((tb->fastreuse > 0 &&
		      sk->sk_reuse && sk->sk_state != TCP_LISTEN) ||
		     (tb->fastreuseport > 0 &&
		      sk->sk_reuseport && uid_eq(tb->fastuid, uid))) &&
		    最小サイズ == -1) {
		    // このブランチは、以前にバインドされたポートと現在のソックの両方が再利用に設定されており、現在のソックの状態が listen ではないことを示します。
			// または、reuseport と uid の両方が同時に設定されます (reuseport を設定した後、同じポートを同時にリッスンできることに注意してください)
			成功へ進む;
	} それ以外 {
			戻り値 1;
			// ポートが競合していないか確認する if (inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, true)) {
				if (((sk->sk_reuse && sk->sk_state != TCP_LISTEN) ||
				     (tb->fastreuseport > 0 &&
				      sk->sk_reuseport && uid_eq(tb->fastuid, uid))) &&
				    最小サイズ != -1 && --試行回数 >= 0) {
				    // 競合が発生しているが、再利用非リッスン状態が設定されているか、再利用ポートが設定されていて同じユーザーの下にある場合は、 spin_unlock(&head->lock); を再試行できます。
					もう一度行きます。
				}

				fail_unlock に移動します。
			}
			// 競合はありません。次のロジックに従ってください }
見つかりません:
	if (!tb && (tb = inet_bind_bucket_create(hashinfo->bind_bucket_cachep,
					ネット、ヘッド、snum)) == NULL)
			fail_unlock に移動します。
	// fastreuse を設定する
	//fastreuseport を設定する
成功:
	......
	// 現在のソックをtb->owner、およびtb->num_owners++にリンクします
	inet_bind_hash(sk, tb, snum);
	戻り値 0;
	// バインド(バインディング)成功を返す return ret;

3. ポート番号が競合していないか確認する

上記のソースコードでは、ポート番号が競合しているかどうかを判断するコードは次のとおりです。

inet_csk(sk)->icsk_af_ops->bind_conflict、inet_csk_bind_conflict とも呼ばれる
int inet_csk_bind_conflict(const struct sock *sk,
			   const struct inet_bind_bucket *tb、bool リラックス){
	......
	sk_for_each_bound(sk2, &tb->owners) {
			// この判断は、次の内部分岐に入るには同じインターフェース(dev_if)を使用する必要があることを示しています。つまり、同じインターフェース上にないポートは、(sk != sk2 &&の場合)競合しません。
		    !inet_v6_ipv6only(sk2) &&
		    (!sk->sk_bound_dev_if ||
		     !sk2->sk_bound_dev_if ||
		     sk->sk_bound_dev_if == sk2->sk_bound_dev_if)) 
		     {
		     	if ((!再利用 || !sk2->sk_reuse ||
			    sk2->sk_state == TCP_LISTEN) &&
			    (!再利用ポート || !sk2->sk_再利用ポート ||
			    (sk2->sk_state != TCP_TIME_WAIT &&
			     !uid_eq(uid, sock_i_uid(sk2))))) {
			   // 一方のパーティが再利用を設定せず、sock2 が listen 状態の場合 // 同時に、一方のパーティが再利用ポートを設定せず、sock2 が time_wait 状態ではなく、2 つの uid が異なります const __be32 sk2_rcv_saddr = sk_rcv_saddr(sk2);
				sk2_rcv_saddr の場合、 sk_rcv_saddr(sk) の場合、
				 	 // IP アドレスが同じなので競合とみなされます sk2_rcv_saddr == sk_rcv_saddr(sk))
					壊す;
			}
			// 非リラックス モードでは、IP アドレスが同じ場合にのみ競合と見なされます......
		  	sk2 != NULL を返します。
	}
	......
}

上記のコードのロジックは次の図に示されています。

4. SO_REUSEADDR と SO_REUSEPORT

上記のコードは少しわかりにくいので、日常の開発で注意すべき点についてお話しします。

上記のバインドでは、2 つのソケット フラグ sk_reuse と sk_reuseport がよく見られます。これら 2 つのフラグによって、バインドが成功するかどうかを判断できます。これら 2 つのフラグの設定は、C 言語の次のコードに示されています。

 setsockopt(sockfd_server、SOL_SOCKET、SO_REUSEADDR、&(int){1}、sizeof(int));
 setsockopt(sockfd_server、SOL_SOCKET、SO_REUSEPORT、&(int){1}、sizeof(int));

ネイティブJAVA

 // Java 8ではネイティブソケットはso_reuseportをサポートしません
 ServerSocket サーバー = 新しい ServerSocket(ポート);
 server.setReuseAddress(true);

Netty (Netty バージョン >= 4.0.16 および Linux カーネル バージョン >= 3.9 以上) では、SO_REUSEPORT を使用できます。

SO_REUSEADDR

前のソースコードでは、バインドの競合を判断するときに、次のような分岐があることを見ました。

(!再利用 || !sk2->sk_reuse ||
			    sk2->sk_state == TCP_LISTEN) /* 再利用ポートを一時的に無視する */){
	// 一方の当事者が設定していない}

sk2 (つまり、バインドされたソケット) が TCP_LISTEN 状態にある場合、または sk2 と新しい sk の両方に _REUSEADDR が設定されていない場合は、競合とみなされます。

結論として、元の sock と新しい sock の両方に SO_REUSEADDR が設定されている場合は、元の sock が Listen 状態でない限り、ESTABLISHED 状態であっても正常にバインドできることがわかります。

日常業務で最も一般的な状況は、元の sock が TIME_WAIT 状態にあることです。これは通常、サーバーをシャットダウンしたときに発生します。SO_REUSEADDR が設定されていない場合、バインディングは失敗し、サービスは開始されません。ただし、SO_REUSEADDR が設定されており、TCP_LISTEN ではないため成功します。

この機能は緊急時の再起動やオフラインデバッグに非常に便利なので、有効にすることをお勧めします。

6. SO_REUSEポート

SO_REUSEPORT は、Linux バージョン 3.9 で導入された新しい機能です。

1. 大規模かつ高度な同時接続を作成する場合、通常のモデルはシングルスレッドのリスナー分散であり、マルチコアを活用できないためボトルネックになります。

2. CPUキャッシュラインミス

一般的な Reactor スレッド モデルを見てみましょう。

明らかに、シングルスレッドの listen/accept にはボトルネックがあります (マルチスレッドの epoll accept を使用するとグループパニックが発生し、WQ_FLAG_EXCLUSIVE を追加すると問題の一部が解決されます)。特に短いリンクを使用する場合に顕著です。
これを考慮して、Linux は SO_REUSEPORT を追加し、競合があるかどうかを判断する bind 内の次のコードもこのパラメータに追加されたロジックです。

if(!reuseport || !sk2->sk_reuseport ||
			    (sk2->sk_state != TCP_TIME_WAIT &&
			     !uid_eq(uid、sock_i_uid(sk2))

このコードにより、SO_REUSEPORT が設定されている場合にエラーなしで複数回バインドできるようになります。つまり、複数のスレッド (プロセス) でバインド/リッスンできるようになります。次の図に示すように:

SO_REUSEPORT をオンにすると、コード スタックは次のようになります。

tcp_v4_rcv
	|->__inet_lookup_skb 
		|->__inet_lookup
			|->__inet_lookup_listener
 /* スコアリングと疑似乱数を使用して listen sock を選択します */
構造体 sock *__inet_lookup_listener(......)
{
	......
	if (スコア > 彼のスコア) {
			結果 = sk;
			hiscore = スコア;
			再利用ポート = sk->sk_reuseport;
			if (reuseport) {
				phash = inet_ehashfn(net, daddr, hnum,
						     saddr、スポーツ);
				一致 = 1;
			}
		} そうでない場合 (スコア == hiscore && 再利用レポート) {
			一致++;
			(((u64)phash * が一致する場合) >> 32 == 0)
				結果 = sk;
			phash = next_pseudo_random32(phash);
		}
	......
}

カーネル レベルで直接負荷分散を実行し、受け入れタスクを異なるスレッドの異なるソケットに分散します (シャーディング)。これにより、マルチコア機能が確実に活用され、接続が成功した後のソケット分散機能が大幅に向上します。

NginxはすでにSO_REUSEPORTを使用しています

Nginx はバージョン 1.9.1 で SO_REUSEPORT を導入し、設定は次のようになります。

http {
     サーバー{
          80 再利用レポートを聴く;
          server_name ローカルホスト;
          # ...
     }
}

ストリーム {
     サーバー{
          12345 再利用レポートを聴く;
          # ...
     }
} 

VII. 結論

Linux カーネルのソース コードは広範かつ奥深いものです。一見単純に見える bind システム コールにも、実際には掘り下げればわかるほど多くの詳細が含まれています。読者の皆さんのお役に立てればと思い、ここでシェアします。

上記は Linux ソースコードからのソケット (TCP) バインドの詳細な説明です。Linux ソケット (TCP) バインドの詳細については、123WORDPRESS.COM の他の関連記事に注目してください。

以下もご興味があるかもしれません:
  • Linux C における sockaddr と sockaddr_in の違い
  • Apache 起動エラー: httpd: apr_sockaddr_info_get() が失敗しました
  • TCP/IP通信を実現するNettyフレームワークの完璧なプロセス
  • Netty の TCP パケット展開ソリューション
  • ファイルアップロード機能を実現するためのJavaネットワークプログラミングTCP
  • チャット機能を実現するためのJavaネットワークプログラミングTCP
  • C++ ベースの TCP チャットルーム機能の実装
  • C言語におけるsockaddrとsockaddr_inの例の詳細な説明

<<:  MySQL 接続クエリを本当に学びましたか?

>>:  優れたウェブフロントエンドデザインの指標

推薦する

オンラインMySQLオプティマイザの誤判断によって発生した低速クエリイベントを記録する

序文:非常に遅いクエリとリクエストのタイムアウトのアラートを受け取りました。メトリックを通じて My...

訪問者を惹きつけるウェブサイトコンテンツを作成する14の方法

ネットサーフィンをしていると、私の注意を引こうとする美しいグラフィックでいっぱいの Web サイトを...

MySQLインデックスに関する詳細を共有する

数日前、同僚からMySQLのインデックスについて質問を受けました。大体わかっているのですが、まだ練習...

XHTML の珍しいが便利なタグ

Xhtml には、あまり使用されないが非常に便利なタグが多数あります。半分の労力で 2 倍の結果を達...

ネイティブ JS 音楽プレーヤー

この記事の例では、音楽プレーヤーを実装するためのJSの具体的なコードを参考までに共有しています。具体...

CSS の Flex レイアウトを使用してシンプルな縦棒グラフを作成する方法

以下は、Flex レイアウトを使用した棒グラフです。 HTML: <div class=&qu...

JSONObject の使用方法の詳細な説明

JSONObject は単なるデータ構造であり、JSON 形式のデータ構造 ( key-value構...

シンプルなウェブデザインコンセプトのカラーマッチング

(I)ウェブページのカラーマッチングの基本概念(1)白黒の言葉は永遠のテーマです。誰もそれを悪く言う...

メタビューポートタグ(モバイルブラウジングズームコントロール)の使用方法

OP が現在のファームウェアで Web ページを開くと、常に 50% にズームアウトされてから表示さ...

単純なCSSの詳細に惚れ込むと、重要ではないものの、効率性が向上する可能性がある

CSS の将来は非常に楽しみです。一方では、まったく新しいページ レイアウト方法であり、他方では、ク...

MySQL の group by と order by を一緒に使用する方法

テーブル:reward(報酬テーブル)があるとします。テーブル構造は次のようになります。 テーブルt...

操作タイムアウトがないときにMySQLサーバーがアクティブに切断される問題を解決します

MySQL サービスを使用する場合、通常の状況では、MySQL のタイムアウト設定は 8 時間 (2...

Vue+element ui はアンカーの配置を実現します

この記事では、アンカー配置を実現するためのVue +要素UIの具体的なコードを例として紹介します。具...

使用したコマンドを表示するLinuxコマンドメソッドの概要

システムでは多くのコマンドが使用されていますが、使用したコマンドをどのように確認すればよいでしょうか...

InnoDB タイプの MySql によるテーブル構造とデータの復元

前提条件: データベースを復元するために必要な .frm ファイルと .ibd ファイルを保存します...