Detaillierte Erklärung des Beispiels von Connect auf der Socket (TCP)-Clientseite aus dem Linux-Quellcode

Detaillierte Erklärung des Beispiels von Connect auf der Socket (TCP)-Clientseite aus dem Linux-Quellcode

Vorwort

Der Autor war schon immer der Meinung, dass es spannend wäre, jedes bisschen Code von der Anwendung über das Framework bis hin zum Betriebssystem zu kennen.
Heute werde ich mir aus der Perspektive des Linux-Quellcodes ansehen, was der Client-Socket beim Verbinden macht. Aus Platzgründen wird die Erläuterung des Accept-Quellcodes auf der Serverseite auf das nächste Mal verschoben.
(Basierend auf Linux 3.10-Kernel)

Ein einfaches Connect-Beispiel

int ClientSocket;
wenn((clientSocket = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
	// Socket konnte nicht erstellt werden return -1;
}
......
wenn(verbinden(clientSocket, (Struktur sockaddr *)&serverAddr, Größevon(serverAddr)) < 0) {
	// Verbindung fehlgeschlagen return -1;
}
.......

Zuerst erstellen wir einen Socket durch den Socket-Systemaufruf, in dem SOCK_STREAM angegeben ist und der letzte Parameter 0 ist, was bedeutet, dass ein normaler TCP-Socket eingerichtet wird. Hier geben wir direkt die dem TCP-Socket entsprechenden Operationen an, also die Operationsfunktion.

Wenn Sie wissen möchten, woher die Struktur im obigen Bild stammt, können Sie meinen vorherigen Artikel lesen:

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

Es ist erwähnenswert, dass der Socket-Systemaufrufvorgang die folgenden zwei Codebeurteilungen vornimmt

Sockenkarte_fd
	|->unbenutzte_FD-Flags abrufen
			|->alloc_fd
				|->Dateien erweitern (ulimit)
	|->sock_alloc_datei	
		|->Datei zuweisen
			|->get_empty_filp (/proc/sys/fs/max_files)

Das erste Urteil ist, dass ulmit den Grenzwert überschreitet:

int expand_files(Struktur files_struct *files, int nr
{
	......
	wenn (nr >= aktuell->Signal->rlim[RLIMIT_NOFILE].rlim_cur)
		Rückgabe -EMFILE;
	......
}

Das Urteil hier ist die Grenze von ulimit! Hier wird die zu -EMFILE korrespondierende Beschreibung zurückgegeben.
„Zu viele offene Dateien“

Das zweite Urteil ist, dass max_files das Limit überschreitet

Strukturdatei *get_empty_filp(void)
{
 ......
	/*
	 * Daraus ist ersichtlich, dass privilegierte Benutzer die maximale Dateigrößenbeschränkung ignorieren können!
	 */
	wenn (get_nr_files() >= files_stat.max_files && !capable(CAP_SYS_ADMIN)) {
		/*
		 * percpu_counters sind ungenau. Führen Sie vorher eine aufwändige Überprüfung durch
		 * wir gehen und scheitern.
		 */
		wenn (percpu_counter_sum_positive(&nr_files) >= files_stat.max_files)
			gehe rüber;
	}
	
 ......
}

Wenn der Dateideskriptor also die maximale Anzahl von Dateien überschreitet, die von allen Prozessen geöffnet werden können (/proc/sys/fs/file-max), wird -ENFILE zurückgegeben und die entsprechende Beschreibung lautet „Zu viele geöffnete Dateien im System“. Privilegierte Benutzer können diese Beschränkung jedoch ignorieren, wie in der folgenden Abbildung dargestellt:

Systemaufruf verbinden

Schauen wir uns den Systemaufruf „connect“ an:

int connect(int sockfd,const struct sockaddr*serv_addr,socklen_t Adresse)

Dieser Systemaufruf hat drei Parameter, daher muss sein Quellcode im Kernel gemäß den Regeln folgendermaßen aussehen:

SYSCALL_DEFINE3(verbinden, ......

Der Autor hat den vollständigen Text durchsucht und die spezifische Implementierung gefunden:

socket.c
SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
		int, Adresslänge)
{
 ......
	err = sock->ops->connect(sock, (Struktur sockaddr *)&Adresse, Adresslänge,
				 sock->Datei->f_flags);
	......
}

Die vorherige Abbildung zeigt, dass unter TCP sock->ops == inet_stream_ops ist und dann in einen weiteren Aufrufstapel fällt, nämlich den folgenden:

SYSCALL_DEFINE3(verbinden
	|->inet_stream_ops
		|->inet_stream_connect
			|->tcp_v4_verbindung
				|->tcp_set_state(sk, TCP_SYN_SENT); Setzt den Status auf TCP_SYN_SENT
			 	|->inet_hash_connect
				|->TCP_Verbindung

Schauen wir uns zunächst die Funktion inet_hash_connect an, die einen Portnummernsuchvorgang enthält. Wenn keine verfügbare Portnummer gefunden werden kann, schlägt die Verbindungsherstellung fehl! Der Kernel muss sich viel Mühe geben, um eine Verbindung herzustellen! Schauen wir uns zunächst die Logik der Suche nach Portnummern an, wie in der folgenden Abbildung dargestellt:

Abrufen des Portnummernbereichs

Zuerst holen wir uns vom Kernel den von Connect nutzbaren Portnummernbereich und nutzen hierbei die sequentielle Sperre (seqlock) von Linux.

void inet_get_local_port_range(int *niedrig, int *hoch)
{
	vorzeichenlose Ganzzahlfolge;

	Tun {
		// Sequenzsperre seq = read_seqbegin(&sysctl_local_ports.lock);

		*niedrig = sysctl_local_ports.range[0];
		*hoch = sysctl_local_ports.range[1];
	} während (read_seqretry(&sysctl_local_ports.lock, seq));
}

Tatsächlich ist eine sequentielle Sperre eine optimistische Sperre, die mit Mechanismen wie Speicherbarrieren kombiniert ist und hauptsächlich auf einem Sequenzzähler basiert. Die Sequenznummer wird vor und nach dem Lesen der Daten gelesen. Wenn die beiden Sequenznummern gleich sind, bedeutet dies, dass der Lesevorgang nicht durch den Schreibvorgang unterbrochen wurde.
Dadurch wird auch sichergestellt, dass die oben gelesenen Variablen konsistent sind, das heißt, dass low und high nicht den Wert vor der Änderung haben und high nicht den Wert nach der Änderung hat. Tief und Hoch liegen entweder vor der Änderung oder nach der Änderung! Die Änderungen im Kernel sind:

cat /proc/sys/net/ipv4/lokaler_IP-Portbereich 
32768 61000

Bestimmen Sie den Startsuchbereich der Portnummern durch Hash

Bei einer Verbindung unter Linux steigt die vom Kernel zugewiesene Portnummer nicht linear an, sondern folgt auch bestimmten Regeln.
Schauen wir uns zunächst den Code an:

int __inet_hash_connect(...)
{
		// Beachten Sie, dass dies eine statische Variable ist. Statischer u32-Hinweis;
		// Der Port_Offset ist hier ein Wert des Peer-IP:Port-Hashes // Das bedeutet, dass Peer-IP:Port fest ist und der Port_Offset fest ist u32 Offset = Hinweis + Port_Offset;
		für (i = 1; i <= verbleibend; i++) {
			Port = niedrig + (i + Offset) % verbleibend;
			/* Prüfen ob der Port belegt ist */
			....
			gehe zu ok;
		}
		.......
OK:
		Hinweis += i;
		......
}

Hier gibt es ein paar kleine Details. Aus Sicherheitsgründen verwendet Linux selbst den Peer-IP:Port, um einen Hash als anfänglichen Offset für die Suche zu erstellen, sodass der anfängliche Suchbereich für verschiedene Remote-IP:Ports grundsätzlich unterschiedlich sein kann! Aber der anfängliche Suchbereich für dieselbe Peer-IP: denselben Port ist derselbe!

Auf meinem Rechner wird in einem völlig sauberen Kernel der gleiche Remote-IP:Port ständig um 2 erhöht, also 38742->38744->38746. Bei anderen Störungen wird diese Regel verletzt.

Einschränkung des Portnummernbereichs

Da wir die Portnummer zur Rückgabe von „ip_local_port_range“ angegeben haben, bedeutet das, dass wir höchstens High-Low+1-Verbindungen herstellen können? Natürlich nicht. Da die Portnummer auf Duplikate geprüft wird, indem (Netzwerk-Namespace, Peer-IP, Peer-Port, lokaler Port und an den Socket gebundenes Gerät) als einziger Schlüssel zur Duplikatsüberprüfung verwendet wird, besteht die Einschränkung darin, dass unter demselben Netzwerk-Namespace die maximale Anzahl verfügbarer Portnummern für die Verbindung mit derselben Peer-IP:Port hoch-niedrig+1 ist. Natürlich müssen möglicherweise auch ip_local_reserved_ports abgezogen werden. Wie in der folgenden Abbildung dargestellt:

Prüfen Sie, ob die Portnummer belegt ist

Die Suche nach belegten Portnummern gliedert sich in zwei Phasen: Zum einen die Suche nach Portnummern im Zustand TIME_WAIT, zum anderen die Suche nach Portnummern in anderen Zuständen.

TIME_WAIT Status Portnummer Suche

Wie wir alle wissen, ist die TIME_WAIT-Phase eine notwendige Phase, damit TCP aktiv geschlossen wird. Wenn der Client eine kurze Verbindung zur Interaktion mit dem Server verwendet, wird eine große Anzahl von Sockets im Status TIME_WAIT generiert. Diese Sockets belegen Portnummern. Wenn also zu viele TIME_WAITs vorhanden sind und der oben genannte Portnummernbereich überschritten wird, gibt die neue Verbindung einen Fehlercode zurück:

Die C-Sprachverbindung gibt den Fehlercode -EADDRNOTAVAIL zurück, der der Beschreibung „Angeforderte Adresse kann nicht zugewiesen werden“ entspricht. 
Die entsprechende Java-Ausnahme ist java.net.NoRouteToHostException: Angeforderte Adresse kann nicht zugewiesen werden (Adresse nicht verfügbar)

ip_local_reserved_ports. Wie in der folgenden Abbildung dargestellt:

Da TIME_WAIT in etwa einer Minute verschwindet, führt es leicht zur Erschöpfung der Portnummer, wenn Client und Server innerhalb einer Minute eine große Anzahl kurzer Verbindungsanforderungen stellen. Diese Minute (die maximale Überlebenszeit von TIME_WAIT) wird während der Kompilierungsphase des Kernels (3.10) festgelegt und kann nicht über Kernel-Parameter angepasst werden. Wie im folgenden Code gezeigt:

#define TCP_TIMEWAIT_LEN (60*HZ) /* wie lange soll gewartet werden, um TIME-WAIT zu zerstören
				 * Zustand, ca. 60 Sekunden */

Linux berücksichtigt diese Situation natürlich und stellt daher einen tcp_tw_reuse-Parameter bereit, der die Wiederverwendung von TIME_WAIT unter bestimmten Umständen bei der Suche nach Portnummern ermöglicht. Der Code lautet wie folgt:

__inet_hash_connect
	|->__inet_check_festgelegt
statische int __inet_check_established(......)
{
	......	
	/* Überprüfen Sie zuerst die TIME-WAIT-Sockets. */
	sk_nulls_for_each(sk2, Knoten, &Kopf->twchain) {
		tw = inet_twsk(sk2);
		// Wenn in time_wait ein passender Port gefunden wird, bestimmen Sie, ob er wiederverwendet werden kann, wenn (INET_TW_MATCH(sk2, net, hash, acookie,
					saddr, daddr, ports, dif)) {
			wenn (twsk_unique(sk, sk2, twp))
				gehe zu einzigartig;
			anders
				gehe zu nicht_eindeutig;
		}
	}
	......
}

Wie im obigen Code geschrieben, wird ermittelt, ob der zu suchende Port wiederverwendet werden kann, wenn er in einer Reihe von Sockets im Status TIME-WAIT gefunden werden kann. Wenn es TCP ist, lautet die Implementierungsfunktion von twsk_unique:

int tcp_twsk_unique(......)
{
	......
	wenn (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
		......
		Rückgabe 1;
	}
	gebe 0 zurück;	
}

Die Logik des obigen Codes ist wie folgt:

Wenn tcp_timestamp und tcp_tw_reuse aktiviert sind und Connect nach einem Port sucht, kann der Port wiederverwendet werden, indem die vorherige Minute auf 1 Sekunde verkürzt wird, solange der jüngste vom Socket im Status TIME_WAIT aufgezeichnete Zeitstempel, der diesen Port zuvor verwendet hat, größer als 1 Sekunde ist. Gleichzeitig wird write_seq direkt zu 65537 hinzugefügt, um mögliche Sequenznummernkonflikte zu vermeiden. Auf diese Weise tritt kein Sequenznummernkonflikt auf, wenn die Übertragungsrate eines einzelnen Sockets weniger als 80 Mbit/s beträgt.
Gleichzeitig wird der Zeitpunkt zum Festlegen von tw_ts_recent_stamp in der folgenden Abbildung dargestellt:

Wenn der Socket daher in den Zustand TIME_WAIT wechselt und ständig entsprechende Pakete gesendet werden, wirkt sich dies auf die Zeit aus, die benötigt wird, bis der diesem TIME_WAIT entsprechende Port verfügbar ist. Wir können tcp_tw_reuse mit dem folgenden Befehl starten:

echo '1' > /proc/sys/net/ipv4/tcp_tw_reuse

Suche nach etablierten Status-Portnummern

Die Suche nach der ESTABLISHED-Portnummer ist viel einfacher

/* Und etablierter Teil... */
	sk_nulls_for_each(sk2, Knoten, &Kopf->Kette) {
		wenn (INET_MATCH(sk2, net, hash, acookie,
					saddr, daddr, Häfen, dif))
			gehe zu nicht_eindeutig;
	}

Verwenden Sie (Netzwerk-Namespace, Peer-IP, Peer-Port, lokaler Port, Socket-gebundenes Gerät) als eindeutigen Schlüssel für die Übereinstimmung. Wenn die Übereinstimmung erfolgreich ist, bedeutet dies, dass dieser Port nicht wiederverwendet werden kann.

Iterative Suche von Portnummern

Der Linux-Kernel sucht nach der obigen Logik nach Ports im Bereich [low, high]. Wenn kein Port gefunden wird, also die Ports erschöpft sind, wird -EADDRNOTAVAIL zurückgegeben, was bedeutet, dass die angeforderte Adresse nicht zugewiesen werden konnte. Es gibt jedoch noch ein weiteres Detail. Wenn der Port eines Sockets im Status TIME_WAIT wiederverwendet wird, wird der entsprechende Socket im Status TIME_WAIT zerstört.

__inet_hash_connect(......)
{
		......
		wenn (tw) {
			inet_twsk_deschedule(tw, Todeszelle);
			inet_twsk_put(tw);
		}
		......
}

Suchen der Routing-Tabelle

Nachdem wir eine verfügbare Portnummer gefunden haben, beginnen wir mit der Routing-Suchphase:

ip_route_newports
	|->ip_route_output_flow
			|->__ip_route_output_key
				|->ip_route_output_langsam
					|->fib_lookup

Auch hierbei handelt es sich um einen sehr komplizierten Vorgang. Aus Platzgründen werde ich hier nicht näher darauf eingehen. Wenn keine Routing-Informationen gefunden werden, werden sie zurückgegeben.

-ENETUNREACH, entsprechend der Beschreibung Netzwerk ist nicht erreichbar

Drei-Wege-Handshake des Kunden

Erst wenn viele Voraussetzungen erfüllt sind, beginnt die Drei-Wege-Handshake-Phase.

TCP_Verbindung
|->tcp_connect_init initialisiert TCP-Socket
|->tcp_transmit_skb sendet SYN-Paket
|->inet_csk_reset_xmit_timer Setzt den SYN-Retransmission-Timer

tcp_connect_init initialisiert viele TCP-bezogene Einstellungen, wie z. B. mss_cache/rcv_mss usw. Und wenn die TCP-Fenstererweiterungsoption aktiviert ist, wird der Fenstererweiterungsfaktor auch in dieser Funktion berechnet:

tcp_connect_init
	|->TCP_Auswahl_Anfangsfenster
int tcp_select_initial_window(...)
{
	......
	(*rcv_wscale) = 0;
	wenn (wscale_ok) {
		/* Fensterskalierung auf maximal mögliche Fenstergröße einstellen
		 * Eine Erklärung zur Beschränkung auf 14 finden Sie in RFC1323.
		 */
		Leerzeichen = max_t(u32, sysctl_tcp_rmem[2], sysctl_rmem_max);
		Leerzeichen = min_t(u32, Leerzeichen, *Fensterklemme);
		während (Leerzeichen > 65535 und (*rcv_wscale) < 14) {
			Leerzeichen >>= 1;
			(*rcv_wscale)++;
		}
	}
	......
}

Wie im obigen Code gezeigt, hängt der Fenstererweiterungsfaktor von der maximal zulässigen Lesepuffergröße des Sockets und der Fensterklemme ab (der maximal zulässigen gleitenden Fenstergröße, die dynamisch angepasst wird). Nach Abschluss einer Reihe anfänglicher Informationseinstellungen beginnt der eigentliche Drei-Wege-Handshake.
Das SYN-Paket wird tatsächlich in tcp_transmit_skb gesendet und der SYN-Timeout-Timer wird im folgenden inet_csk_reset_xmit_timer eingestellt. Wenn der Peer kein SYN_ACK sendet, wird -ETIMEDOUT zurückgegeben.

Timeout für erneute Übertragung und

/proc/sys/net/ipv4/tcp_syn_retries

Die Standardeinstellung für Linux ist 5, es wird jedoch empfohlen, sie auf 3 zu setzen. Nachfolgend sehen Sie ein Referenzdiagramm der Timeout-Periode bei verschiedenen Einstellungen.

Nachdem der SYN-Timeout-Neuübertragungstimer eingestellt wurde, kehrt tcp_connnect zurück und geht den ganzen Weg zurück zum ursprünglichen inet_stream_connect. Hier warten wir darauf, dass das andere Ende ein SYN_ACK zurückgibt oder der SYN-Timer abläuft.

int __inet_stream_connect(Struktur Socket *Socket,...,)
{
	// Wenn O_NONBLOCK gesetzt ist, ist timeo 0
	timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);
	......
	// Wenn timeo=0, wird O_NONBLOCK sofort zurückkehren // Andernfalls auf timeo warten, wenn (!timeo || !inet_wait_for_connect(sk, timeo, writebias))
		gehe raus;
}

Linux selbst bietet ein SO_SNDTIMEO zur Steuerung des Timeouts der Verbindung, aber Java verwendet diese Option nicht. Stattdessen werden andere Methoden verwendet, um das Verbindungszeitlimit zu steuern. Was den Connect-Systemaufruf in der Sprache C betrifft: Wenn SO_SNDTIMEO nicht festgelegt ist, wird der entsprechende Benutzerprozess in den Ruhezustand versetzt, bis SYN_ACK eintrifft oder der Timeout-Timer abläuft. Anschließend wird der sekundäre Benutzerprozess aktiviert.

Wenn es NON_BLOCK ist, wird das Timeout- oder Verbindungserfolgsereignis durch Multiplexing-Mechanismen wie Select/Epoll erfasst.

SYN_ACK vom anderen Ende kommt an

Nachdem SYN_ACK auf der Serverseite angekommen ist, wird es gemäß dem folgenden Codepfad übertragen und aktiviert den Benutzermodusprozess:

tcp_v4_rcv
	|->tcp_v4_do_rcv
		|->TCP_Empfangsstatusprozess
			|->tcp_rcv_synsent_state_process
				|->tcp_verbinden_beenden
					|->tcp_init_metrics Initialisiert Metrikstatistiken |->tcp_init_congestion_control Initialisiert Überlastungskontrolle |->tcp_init_buffer_space Initialisiert Pufferspeicher |->inet_csk_reset_keepalive_timer Aktiviert Keepalive-Timer |->sk_state_change(sock_def_wakeup) Weckt Benutzermodusprozess auf |->tcp_send_ack Sendet den letzten Handshake des Drei-Wege-Handshakes an den Server |->tcp_set_state(sk, TCP_ESTABLISHED) Setzt den Status ESTABLISHED

Zusammenfassen

Der Verbindungsaufbau auf der Client-Seite (TCP) ist wirklich mühsam, von der anfänglichen Dateideskriptorbeschränkung über die Portnummernsuche, dann die Routingtabellensuche und schließlich den Drei-Wege-Handshake. Jedes Problem in einer Verbindung führt dazu, dass die Verbindung nicht hergestellt werden kann. Der Autor beschreibt die Quellcodeimplementierung dieser Mechanismen im Detail. Ich hoffe, dass dieser Artikel den Lesern helfen kann, wenn sie in Zukunft auf Connect-Fehlerprobleme stoßen.

Dies ist das Ende dieses Artikels über das Anzeigen des Socket (TCP) Client Connect aus dem Linux-Quellcode. Weitere relevante Inhalte zum Linux-Quellcode finden Sie in früheren Artikeln auf 123WORDPRESS.COM oder in den folgenden verwandten Artikeln. Ich hoffe, dass jeder 123WORDPRESS.COM in Zukunft unterstützen wird!

Das könnte Sie auch interessieren:
  • Tutorial zur Leistungsoptimierung langer TCP-Verbindungen unter Android
  • Java-Multithreading zur Implementierung der TCP-Netzwerk-Socket-Programmierung (C/S-Kommunikation)
  • Java implementiert Netzwerk-Socket-Programmierung basierend auf dem TCP-Protokoll (C/S-Kommunikation)
  • Prozessdiagramm zum Aufbau des Springboot+TCP-Listening-Servers
  • Python verwendet socket_TCP, um die Funktion zum Herunterladen kleiner Dateien zu implementieren
  • Python: So erhalten Sie TCPdump-Ausgabe in Echtzeit
  • Python verwendet das Socket-Modul, um eine einfache TCP-Kommunikation zu implementieren
  • Java verwendet das TCP-Protokoll, um die Client-Server-Kommunikation zu realisieren (mit Kommunikationsquellcode)
  • Implementierungsprinzip und Prozessanalyse der TCP-Leistungsoptimierung

<<:  JS implementiert die zufällige Generierung von Bestätigungscodes

>>:  Grafisches Tutorial zur Installation und Konfiguration von MySQL 8.0.20 unter Windows 10

Artikel empfehlen

Zusammenfassung gängiger SQL-Anweisungen in MySQL

1. MySQL-Exportdatei: SELECT `pe2e_user_to_compan...

XHTML-Einführungstutorial: Webseitenkopf und DTD

Obwohl Kopf und DTD nicht auf der Seite angezeigt...

4 Möglichkeiten zur Implementierung von Routing-Übergangseffekten in Vue

Vue-Router-Übergänge sind eine schnelle und einfa...

Daten in der Layui-Tabellenzeile dynamisch bearbeiten

Inhaltsverzeichnis Vorwort Stil Funktion Beschrei...

Zusammenfassung der Anwendungspraxis für Docker-Container des Node.js-Dienstes

In diesem Artikel wird die Verwendung und Install...

JavaScript zum Erzielen eines Dropdown-Menüeffekts

Verwenden Sie Javascript, um ein Dropdown-Menü zu...

So verwenden Sie den Vue-Filter

Inhaltsverzeichnis Überblick Filter definieren Ve...

Semantisierung von HTML-Tags (einschließlich H5)

einführen HTML stellt die kontextuelle Struktur u...

JavaScript-Closures erklärt

Inhaltsverzeichnis 1. Was ist ein Abschluss? 2. D...

Lernen Sie einfach verschiedene SQL-Joins

Mit der SQL JOIN-Klausel können Zeilen aus zwei o...