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. 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. 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. 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. 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. 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:
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.
Drei-Wege-Handshake des Kunden Erst wenn viele Voraussetzungen erfüllt sind, beginnt die Drei-Wege-Handshake-Phase.
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. Timeout für erneute Übertragung und
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:
|
<<: JS implementiert die zufällige Generierung von Bestätigungscodes
>>: Grafisches Tutorial zur Installation und Konfiguration von MySQL 8.0.20 unter Windows 10
Einführung: Dieser Artikel stellt hauptsächlich v...
1. MySQL-Exportdatei: SELECT `pe2e_user_to_compan...
Obwohl Kopf und DTD nicht auf der Seite angezeigt...
Vue-Router-Übergänge sind eine schnelle und einfa...
Inhaltsverzeichnis Vorwort Stil Funktion Beschrei...
In diesem Artikel wird die Verwendung und Install...
In diesem Artikel werden anhand von Beispielen di...
Inhaltsverzeichnis 1. Entdecken Sie das Problem 2...
Verwenden Sie Javascript, um ein Dropdown-Menü zu...
Inhaltsverzeichnis Überblick Filter definieren Ve...
HTML5 und jQuery implementieren die Vorschau loka...
einführen HTML stellt die kontextuelle Struktur u...
Inhaltsverzeichnis 1. Was ist ein Abschluss? 2. D...
Inhaltsverzeichnis Hintergrund 1) Aktivieren Sie ...
Mit der SQL JOIN-Klausel können Zeilen aus zwei o...