Linux ソースコードからのソケット (TCP) クライアント側での接続の例の詳細な説明

Linux ソースコードからのソケット (TCP) クライアント側での接続の例の詳細な説明

序文

著者は、アプリケーションからフレームワーク、オペレーティング システムに至るまで、あらゆるコードを知ることができれば興味深いだろうと常に感じていました。
今日は、Linux ソースコードの観点から、クライアント側ソケットが接続時に何を行うのかを見ていきます。スペースの制約により、サーバー側の Accept ソース コードの説明は次回に残します。
(Linux 3.10カーネルベース)

簡単なConnectの例

int クライアントソケット;
クライアントソケットがソケット(AF_INET、SOCK_STREAM、0)の場合、0未満になります。
	// ソケットの作成に失敗しました。-1 を返します。
}
......
if(connect(clientSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
	// 接続に失敗しました -1 を返します。
}
.......

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

上の写真の構造がどこから来たのか知りたい場合は、私の以前の記事を読んでください。

https://www.jb51.net/article/106563.htm

ソケットシステムコール操作では、次の2つのコード判断が行われることに注意する価値がある。

ソックマップfd
	|->未使用のfdフラグを取得する
			|->割り当てファイル
				|->expand_files (ulimit)
	|->sock_alloc_file	
		|->割り当てファイル
			|->get_empty_filp (/proc/sys/fs/max_files)

最初の判断は、ウルミットが限界を超えているというものです。

int expand_files(struct files_struct *files, int nr
{
	......
	(nr >= 現在のシグナル->rlim[RLIMIT_NOFILE].rlim_cur) の場合
		-EMFILE を返します。
	......
}

ここでの判断はulimitの限界です!ここでは-EMFILEに対応する説明が返されます
「開いているファイルが多すぎます」

2番目の判断は、max_filesが制限を超えていることです

構造体ファイル *get_empty_filp(void)
{
 ......
	/*
	 * このことから、特権ユーザーは最大ファイルサイズ制限を無視できることがわかります。
	 */
	get_nr_files() が files_stat.max_files より大きい場合、CAP_SYS_ADMIN が対応している必要があります。
		/*
		 * percpu_countersは不正確です。事前にコストのかかるチェックを行ってください
		 * 私たちは行って失敗します。
		 */
		if (percpu_counter_sum_positive(&nr_files) >= files_stat.max_files)
			行く;
	}
	
 ......
}

したがって、ファイル記述子がすべてのプロセスで開くことができるファイルの最大数 (/proc/sys/fs/file-max) を超えると、-ENFILE が返され、対応する説明は「システムで開いているファイルが多すぎます」となりますが、次の図に示すように、特権ユーザーはこの制限を無視できます。

接続システムコール

connect システムコールを見てみましょう。

int connect(int sockfd、const struct sockaddr *serv_addr、socklen_t addrlen)

このシステム コールには 3 つのパラメーターがあるため、ルールに従ってカーネル内のソース コードは次のようになります。

SYSCALL_DEFINE3(接続、......

著者は全文を検索し、具体的な実装を見つけました。

ソケット.c
SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
		int、アドレス長)
{
 ......
	err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
				 sock->ファイル->f_flags);
	......
}

前の図は、TCP では sock->ops == inet_stream_ops であり、その後、次のようなさらなる呼び出しスタックに陥ることを示しています。

SYSCALL_DEFINE3(接続
	|->inet_stream_ops
		|->inet_stream_connect
			|->tcp_v4_接続
				|->tcp_set_state(sk, TCP_SYN_SENT); 状態をTCP_SYN_SENTに設定する
			 	|->inet_hash_connect
				|->tcp_connect

まず、ポート番号の検索処理を含む inet_hash_connect 関数を見てみましょう。使用可能なポート番号が見つからない場合、接続の作成は失敗します。カーネルは接続を確立するために多くの苦労をしなければなりません。まず、次の図に示すように、ポート番号を検索するロジックを見てみましょう。

ポート番号の範囲を取得する

まず、カーネルから connect で使用できるポート番号の範囲を取得しますが、ここでは Linux のシーケンシャル ロック (seqlock) を使用します。

void inet_get_local_port_range(int *low, int *high)
{
	符号なし整数シーケンス;

	する {
		// シーケンスロック seq = read_seqbegin(&sysctl_local_ports.lock);

		*low = sysctl_local_ports.range[0];
		*high = sysctl_local_ports.range[1];
	} read_seqretry(&sysctl_local_ports.lock, seq) を実行します。
}

実際、シーケンシャル ロックは、主にシーケンス カウンターに依存するメモリ バリアなどのメカニズムと組み合わせた楽観的ロックです。シーケンス番号は、データの読み取り前と読み取り後に読み取られます。2 つのシーケンス番号が同じ場合は、読み取り操作が書き込み操作によって中断されなかったことを意味します。
これにより、上記の読み取り変数の一貫性も確保されます。つまり、low と high には変更前の値が設定されず、high には変更後の値が設定されます。安値と高値は変更前か変更後かのどちらかです。カーネルの変更点は次のとおりです。

cat /proc/sys/net/ipv4/ip_local_port_range 
32768 61000

ハッシュを使用してポート番号の開始検索範囲を決定する

Linux で接続する場合、カーネルによって割り当てられるポート番号は直線的に増加するのではなく、特定のルールに従います。
まずはコードを見てみましょう:

int __inet_hash_connect(...)
{
		// 注意: これは静的変数です static u32 hint;
		// ここでの port_offset は、ピアの ip:port ハッシュの値です。 // つまり、ピアの ip:port は固定されており、port_offset も固定されています。 u32 offset = hint + port_offset;
		(i = 1; i <= 残り; i++) {
			ポート = 低 + (i + オフセット) % 残り;
			/* ポートが占有されているかどうかを確認します */
			....
			OKに進みます。
		}
		.......
わかりました:
		ヒント += i;
		......
}

ここには細かい点がいくつかあります。セキュリティ上の理由から、Linux 自体はピアの IP:ポートを使用してハッシュを作成し、検索の初期オフセットとして使用します。そのため、異なるリモート IP:ポートの初期検索範囲は基本的に異なる可能性があります。しかし、同じピア IP:ポートの初期検索範囲は同じです。

私のマシンでは、完全にクリーンなカーネルで、同じリモート IP:ポートが常に 2 ずつ増加します (つまり、38742 -> 38744 -> 38746)。他の干渉がある場合、このルールは破られます。

ポート番号範囲の制限

ip_local_port_range を返すポート番号を指定したので、最大で high-low+1 の接続を作成できるということでしょうか?もちろん違います。重複確認の唯一のキーとして (ネットワーク名前空間、ピア IP、ピア ポート、ローカル ポート、およびソケットにバインドされたデバイス) を使用してポート番号の重複がチェックされるため、同じネットワーク名前空間では、同じピア IP:ポートに接続するために使用できるポート番号の最大数は、上限 - 下限 + 1 になるという制限があります。もちろん、ip_local_reserved_ports も減算する必要がある可能性があります。次の図に示すように:

ポート番号が使用されていないか確認する

占有ポート番号の検索は、TIME_WAIT 状態のポート番号の検索と、その他の状態のポート番号の検索の 2 段階に分かれています。

TIME_WAIT状態ポート番号検索

ご存知のとおり、TIME_WAIT フェーズは TCP がアクティブに終了するために必要なフェーズです。クライアントが短い接続を使用してサーバーと対話する場合、TIME_WAIT 状態のソケットが大量に生成されます。これらのソケットはポート番号を占有するため、TIME_WAIT が多すぎて上記のポート番号の範囲を超えると、新しい接続でエラー コードが返されます。

C言語接続は、-EADDRNOTAVAILというエラーコードを返します。これは、要求されたアドレスを割り当てることができないという説明に対応します。 
対応する Java 例外は java.net.NoRouteToHostException: 要求されたアドレスを割り当てることができません (アドレスが利用できません)

ip_local_reserved_ports。次の図に示すように:

TIME_WAIT は約 1 分で消滅するため、クライアントとサーバーが 1 分以内に大量の短い接続要求を確立すると、ポート番号枯渇に簡単に至ります。この 1 分 (TIME_WAIT の最大生存時間) はカーネル (3.10) のコンパイル フェーズ中に決定され、カーネル パラメータを通じて調整することはできません。 次のコードに示すように:

#define TCP_TIMEWAIT_LEN (60*HZ) /* TIME-WAIT を破棄するまでの待機時間
				 * 状態、約60秒 */

Linux は当然この状況を考慮しているため、ポート番号を検索するときに特定の状況で TIME_WAIT を再利用できるようにする tcp_tw_reuse パラメータを提供しています。コードは次のとおりです。

__inet_hash_connect
	|->__inet_check_established
静的 int __inet_check_established(......)
{
	......	
	/* 最初に TIME-WAIT ソケットをチェックします。 */
	sk_nulls_for_each(sk2, ノード, &head->twchain) {
		tw = inet_twsk(sk2);
		// time_waitで一致するポートが見つかった場合、再利用できるかどうかを判断します。if (INET_TW_MATCH(sk2, net, hash, acookie,
					saddr、daddr、ポート、dif)) {
			twsk_unique(sk, sk2, twp) の場合
				ユニークに移動します。
			それ以外
				not_unique に移動します。
		}
	}
	......
}

上記のコードに書かれているように、検索対象のポートが TIME-WAIT 状態のソケットの束の中に見つかった場合、このポートが再利用できるかどうかが判断されます。 TCP の場合、twsk_unique の実装関数は次のようになります。

tcp_twsk_unique() メソッドは、次のコードで使用できます。
{
	......
	if (tcptw->tw_ts_recent_stamp &&
	 (twp == NULL || (sysctl_tcp_tw_reuse &&
			 get_seconds() - tcptw->tw_ts_recent_stamp > 1))) {
		tp->write_seq = tcptw->tw_snd_nxt + 65535 + 2
		......
		1 を返します。
	}
	0を返します。	
}

上記のコードのロジックは次のとおりです。

tcp_timestamp と tcp_tw_reuse が有効な場合、Connect がポートを検索するときに、以前このポートを使用した TIME_WAIT 状態のソケットによって記録された最新のタイムスタンプが 1 秒より大きい限り、ポートを再利用して、以前の 1 分を 1 秒に短縮できます。同時に、潜在的なシーケンス番号の競合を防ぐために、write_seq が 65537 に直接追加されます。このようにして、単一ソケットの転送速度が 80Mbit/s 未満の場合に、シーケンス番号の競合は発生しません。
同時に、tw_ts_recent_stamp を設定するタイミングを下図に示します。

したがって、ソケットが TIME_WAIT 状態に入り、対応するパケットが常に送信されると、この TIME_WAIT に対応するポートが使用可能になるまでの時間に影響します。次のコマンドで tcp_tw_reuse を開始できます。

エコー '1' > /proc/sys/net/ipv4/tcp_tw_reuse

ESTABLISHED 州ポート番号検索

ESTABLISHEDポート番号の検索ははるかに簡単です

/* そして確立された部分... */
	sk_nulls_for_each(sk2, ノード, &head->chain) {
		if (INET_MATCH(sk2, ネット, ハッシュ, acookie,
					saddr、daddr、ポート、dif))
			not_unique に移動します。
	}

マッチングのための一意のキーとして、(ネットワーク名前空間、ピア IP、ピア ポート、ローカル ポート、ソケットにバインドされたデバイス) を使用します。マッチングが成功した場合、このポートは再利用できないことを意味します。

ポート番号の反復検索

Linux カーネルは、上記のロジックに従って [low, high] の範囲内でポートを検索します。ポートが見つからない場合、つまりポートが使い果たされた場合は、要求されたアドレスを割り当てることができないことを意味する -EADDRNOTAVAIL が返されます。しかし、もう 1 つ詳細があります。TIME_WAIT 状態のソケットのポートが再利用されると、対応する TIME_WAIT 状態のソケットは破棄されます。

__inet_hash_connect(......)
{
		......
		もし(tw){
			inet_twsk_deschedule(tw、death_row);
			inet_twsk_put(tw);
		}
		......
}

ルーティングテーブルの検索

利用可能なポート番号が見つかったら、ルーティング検索フェーズに入ります。

ip_route_newports
	|->ip_route_output_flow
			|->__ip_route_出力キー
				|->ip_route_output_slow
					|->fib_lookup

これも非常に複雑なプロセスであり、スペースの制限により、詳細には説明しません。ルーティング情報が見つからない場合は返されます。

-ENETUNREACH、説明「ネットワークに到達できません」に対応

クライアントの3ウェイハンドシェイク

多くの前提条件が整うと、3 ウェイ ハンドシェイク フェーズが始まります。

tcp_接続
|->tcp_connect_initはTCPソケットを初期化します
|->tcp_transmit_skbはSYNパケットを送信します
|->inet_csk_reset_xmit_timer SYN再送タイマーを設定する

tcp_connect_init は、mss_cache/rcv_mss など、TCP 関連の多くの設定を初期化します。また、TCP ウィンドウ拡張オプションがオンになっている場合は、この関数でウィンドウ拡張係数も計算されます。

tcp_connect_init
	|->tcp_select_initial_window
int tcp_select_initial_window(...)
{
	......
	(*rcv_wscale) = 0;
	(wscale_ok)の場合{
		/* ウィンドウのスケーリングを最大ウィンドウに設定
		 * 14の制限についてはRFC1323を参照してください。
		 */
		スペース = max_t(u32, sysctl_tcp_rmem[2], sysctl_rmem_max);
		スペース = min_t(u32, スペース, *window_clamp);
		(スペース > 65535 && (*rcv_wscale) < 14) の場合 {
			スペース >>= 1;
			(*rcv_wscale)++;
		}
	}
	......
}

上記のコードに示されているように、ウィンドウ拡張係数は、ソケットの最大許容読み取りバッファ サイズと window_clamp (動的に調整される最大許容スライディング ウィンドウ サイズ) によって決まります。一連の初期情報設定が完了すると、実際の 3 ウェイ ハンドシェイクが始まります。
SYN パケットは実際に tcp_transmit_skb で送信され、SYN タイムアウト タイマーは後続の inet_csk_reset_xmit_timer で設定されます。ピアが SYN_ACK を送信しない場合は、-ETIMEDOUT が返されます。

再送信タイムアウトと

tcp_syn_retries は、

Linux のデフォルト設定は 5 ですが、3 に設定することをお勧めします。以下は、さまざまな設定でのタイムアウト期間の参考図です。

SYN タイムアウト再送信タイマーを設定した後、tcp_connnect は戻り、元の inet_stream_connect に戻ります。ここでは、相手側が SYN_ACK を返すか、SYN タイマーがタイムアウトするまで待機します。

int __inet_stream_connect(構造体ソケット *sock,...,)
{
	// O_NONBLOCKが設定されている場合、timeoは0になります
	timeo = sock_sndtimeo(sk、フラグ、O_NONBLOCK);
	......
	// timeo=0 の場合、O_NONBLOCK はすぐに戻ります // それ以外の場合は、timeo を待ちます if (!timeo || !inet_wait_for_connect(sk, ti​​meo, writebias))
		外出する;
}

Linux 自体は接続のタ​​イムアウトを制御するための SO_SNDTIMEO を提供していますが、Java はこのオプションを使用しません。代わりに、他の方法を使用して接続タイムアウトを制御します。 C 言語の connect システム コールに関しては、SO_SNDTIMEO が設定されていない場合、対応するユーザー プロセスは SYN_ACK が到着するかタイムアウト タイマーが期限切れになるまでスリープ状態になり、その後セカンダリ ユーザー プロセスが起動されます。

NON_BLOCK の場合、タイムアウトまたは接続成功イベントは、select/epoll などの多重化メカニズムを通じてキャプチャされます。

相手側からのSYN_ACKが到着する

SYN_ACK がサーバー側に到着すると、次のコード パスに従って送信され、ユーザー モード プロセスが起動されます。

tcp_v4_rcv
	|->tcp_v4_do_rcv
		|->tcp_rcv_state_process
			|->tcp_rcv_synsent_state_process
				|->tcp_finish_connect
					|->tcp_init_metrics メトリック統計を初期化します |->tcp_init_congestion_control 輻輳制御を初期化します |->tcp_init_buffer_space バッファ スペースを初期化します |->inet_csk_reset_keepalive_timer キープアライブ タイマーを有効にします |->sk_state_change(sock_def_wakeup) ユーザー モード プロセスを起動します |->tcp_send_ack 3 ウェイ ハンドシェイクの最後のハンドシェイクをサーバーに送信します |->tcp_set_state(sk, TCP_ESTABLISHED) ESTABLISHED 状態に設定します

要約する

クライアント (TCP) 側での接続プロセスは、最初のファイル記述子の制限からポート番号の検索、ルーティング テーブルの検索、そして最後に 3 ウェイ ハンドシェイクまで、非常に困難です。リンクに問題があれば、接続を確立できません。著者は、これらのメカニズムのソース コード実装を詳細に説明しています。この記事が、読者が将来接続失敗の問題に遭遇したときに役立つことを願っています。

Linux ソース コードからソケット (TCP) クライアント接続を表示する方法については、これで終わりです。Linux ソース コードに関する関連コンテンツについては、123WORDPRESS.COM の以前の記事を検索するか、以下の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。

以下もご興味があるかもしれません:
  • Android での TCP 長時間接続のパフォーマンス最適化に関するチュートリアル
  • TCP ネットワーク ソケット プログラミング (C/S 通信) を実装するための Java マルチスレッド
  • JavaはTCPプロトコル(C/S通信)に基づいてネットワークソケットプログラミングを実装します。
  • Springboot+TCPリスニングサーバー構築プロセス図
  • Pythonはsocket_TCPを使用して小さなファイルのダウンロード機能を実装します
  • Python で tcpdump 出力をリアルタイムで取得する方法
  • Pythonはソケットモジュールを使用してシンプルなTCP通信を実装します
  • Java は TCP プロトコルを使用してクライアント サーバー通信を実現します (通信ソース コード付き)
  • TCPパフォーマンスチューニングの実装原理とプロセス分析

<<:  JSは検証コードのランダム生成を実装します

>>:  Windows 10 での MySQL 8.0.20 のインストールと設定方法のグラフィック チュートリアル

推薦する

Vue.jsはアイコンをクリックしてズームインし、

前回の記事では、Vue で画像の切り抜きや拡大・縮小、回転を実現する方法を紹介しました。今回は、アイ...

Vue を使用して CSS トランジションとアニメーションを実装する方法

目次1. トランジションとアニメーションの違い2. Vueを使用して基本的なCSSトランジションとア...

Tomcat サービスに Java 起動コマンドを追加する方法

私の最初のサーバープログラム現在、オンラインゲームの書き方を学んでいるので、サーバーサイドのプログラ...

JS配列ループ方式と効率分析の比較

配列メソッドJavaScript には多くの配列メソッドが用意されています。次の図は、ほとんどの配列...

MySQLはSQL文を使用してテーブル名を変更します

MySQL では、SQL ステートメント rename table を使用してテーブル名を変更できま...

JavaScript 事前分析、オブジェクトの詳細

目次1. 事前分析1. 変数の事前解析と関数の事前解析1. 変数の事前解析2. 機能事前分析2. 事...

JVM 上の高性能データ形式ライブラリ パッケージである Apache Arrow の紹介とアーキテクチャ (Gkatziouras)

Apache Arrow は、BigQuery を含むさまざまなビッグデータ ツールで使用される一...

PXEを使用してCentOS7.6を自動的にインストールする方法の詳細なチュートリアル

1. 需要ベースには 300 台の新しいサーバーがあり、CentOS7.6 オペレーティング システ...

MySQL インジェクションにおける outfile、dumpfile、load_file 関数の詳細な説明

SQL インジェクション脆弱性を悪用する後期段階では、MySQL のファイル シリーズ関数を使用して...

Dockerコンテナを更新、パッケージ化、Alibaba Cloudにアップロードする方法

今回は、実行中のコンテナをイメージにパッケージ化して Alibaba Cloud にアップロードし、...

nginxのリソースキャッシュ設定の詳細な説明

私はずっとキャッシュについて学びたいと思っていました。結局のところ、キャッシュはフロントエンドのパフ...

MySQLバックアップとリカバリの実践に関する詳細な説明

1. mysqlbackup の紹介mysqlbackup は、MySQL Enterprise B...

MySQLで重複データを削除する詳細な例

MySQLで重複データを削除する詳細な例重複レコードには 2 つの意味があります。1 つは完全に重複...

Vueプラグインの実装で発生した問題の概要

目次シーン紹介プラグインの実装問題1: 重複したヘッダーコンポーネント質問2: 別の実装アイデア質問...

デザイン理論:テキスト表現とユーザビリティ

<br />テキストデザインでは、通常、テキストのレイアウト、つまりテキストをより美しく...