1. EinleitungBei der Hochleistungs-Netzwerkprogrammierung unter Linux ist epoll unverzichtbar. Im Vergleich zu Select, Poll und anderen Systemaufrufen bietet Epoll unvergleichliche Vorteile, wenn eine große Anzahl von Dateideskriptoren überwacht werden muss und nur wenige davon aktiv sind. Epoll ermöglicht es dem Kernel, sich die interessierenden Deskriptoren zu merken und, wenn die entsprechenden Deskriptorereignisse bereit sind, diese bereiten Elemente zur Epoll-Bereitschaftsliste hinzuzufügen und den entsprechenden Epoll-Warteprozess zu aktivieren. 2. Einfaches Epoll-BeispielDas folgende Beispiel ist ein vom Autor in der Sprache C geschriebener Codeausschnitt von dbproxy. Aufgrund der Vielzahl der Details wurden einige Auslassungen vorgenommen. int init_reactor(int listen_fd,int worker_count){ ...... // Mehrere Epoll-FDs erstellen, um die Multi-Core-Funktionalität voll auszunutzen for(i=0;i<worker_count;i++){ Reaktor->Arbeiter_fd = epoll_create(EPOLL_MAX_EVENTS); } /* epoll fügt listen_fd hinzu und akzeptiert */ // Fügen Sie die Ereignisse nach dem Akzeptieren zum entsprechenden Epoll fd hinzu int client_fd = accept(listen_fd,(struct sockaddr *)&client_addr,&client_len))); // Registrieren Sie den Verbindungsdeskriptor beim entsprechenden Worker epoll_ctl(reactor->client_fd,EPOLL_CTL_ADD,epifd,&event); } //Arbeitsthread des Reaktors static void* rw_thread_func(void* arg){ ...... für(;;){ // epoll_wait wartet auf Ereignisauslöser int retval = epoll_wait(epfd,events,EPOLL_MAX_EVENTS,500); wenn(retval > 0){ für(j=0; j < retval; j++){ // Lese-Ereignisse verarbeiten if(event & EPOLLIN){ führe eine Verbindung mit einem Computer aus, der eine Verbindung mit einem Computer herstellt. weitermachen; } /* Andere Ereignisse verarbeiten */ } } } ...... } Der obige Code implementiert die Akzeptieren- und Lese-/Schreibverarbeitungsthreads tatsächlich in einem Reaktormodus, wie in der folgenden Abbildung gezeigt: 2.1, epoll_createDas Unix-Konzept, dass alles eine Datei ist, spiegelt sich auch in epoll wider. Der Aufruf epoll_create gibt einen Dateideskriptor zurück, der im Stammverzeichnis von anon_inode_fs (anonymes Inode-Dateisystem) gemountet ist. Schauen wir uns den spezifischen Quellcode des Systemaufrufs epoll_create an: SYSCALL_DEFINE1(epoll_create, int, Größe) { wenn (Größe <= 0) Rückgabe -EINVAL; gibt sys_epoll_create1(0) zurück; } Wie aus dem obigen Quellcode ersichtlich ist, sind die Parameter von epoll_create grundsätzlich bedeutungslos. Der Kernel bestimmt einfach, ob er 0 ist, und ruft dann direkt sys_epoll_create1 auf. Da der Linux-Systemaufruf durch (SYSCALL_DEFINE1, SYSCALL_DEFINE2 ... SYSCALL_DEFINE6) definiert ist, ist der Quellcode, der sys_epoll_create1 entspricht, SYSCALL_DEFINE(epoll_create1). (Hinweis: Aufgrund der begrenzten Anzahl von Registern begrenzt der Kernel (unter 80x86) Systemaufrufe auf maximal 6 Parameter. Laut ulk3 liegt dies an der Beschränkung auf 32-Bit-80x86-Register.) Schauen wir uns als nächstes den Quellcode von epoll_create1 an: SYSCALL_DEFINE1(epoll_create1, int, flags) { // kzalloc(sizeof(*ep), GFP_KERNEL), nutzt Kernelspeicherplatz error = ep_alloc(&ep); // Den nicht verwendeten Dateideskriptor abrufen, d. h. den Steckplatz im Deskriptor-Array fd = get_unused_fd_flags(O_RDWR | (flags & O_CLOEXEC)); // Ordnen Sie einen Inode im anonymen Inode-Dateisystem zu und holen Sie sich seine Dateistruktur // und file->f_op = &eventpoll_fops // und Datei->private_Daten = ep; Datei = anon_inode_getfile("[eventpoll]", &eventpoll_fops, ep, O_RDWR | (Flags & O_CLOEXEC)); //Datei in den entsprechenden Slot des Dateideskriptor-Arrays einfügen fd_install(fd,file); ep->file = Datei; fd zurückgeben; } Schließlich wird der von epoll_create generierte Dateideskriptor in der folgenden Abbildung dargestellt: 2.2, Eventpoll-StrukturAlle epoll-Systemaufrufe basieren auf der Eventpoll-Struktur. Hier ist eine kurze Beschreibung ihrer Mitglieder: /* * Diese Struktur wird in Datei->private_daten*/ gespeichert. Struktur eventpoll { // Spin Lock. Verwenden Sie Spin Lock zum Sperren innerhalb des Kernels, damit mehrere Threads (Prozesse) gleichzeitig an dieser Struktur arbeiten können. // Hauptsächlich ready_list schützen. Spinlock_t-Sperre; // Dieses Mutex soll sicherstellen, dass der Dateideskriptor nicht entfernt wird, wenn die Ereignisschleife den entsprechenden Dateideskriptor verwendet. struct mutex mtx; // Die von epoll_wait verwendete Warteschlange bezieht sich auf den Prozess-Wakeup-wait_queue_head_t wq; // Die von file->poll verwendete Warteschlange bezieht sich auf den Prozess-Wakeup-wait_queue_head_t poll_wait; // Deskriptor-Warteschlange bereit struct list_head rdllist; // Organisieren Sie die Datei-Deskriptoren, denen aktuell Epoll folgt, durch eine Rot-Schwarz-Baumstruktur rb_root rbr; // Wenn Sie das Ready-Ereignis an den Benutzerbereich übertragen, verknüpfen Sie den Dateideskriptor des Ereignisses, das gleichzeitig auftritt, mit dieser verknüpften Liste struct epitem *ovflist; // Entsprechender Benutzer Struktur user_struct *user; //Entsprechender Dateideskriptor struct file *file; // Die folgenden beiden sind Optimierungen zur Schleifenerkennung: int visited; Struktur list_head besuchte_Liste_Link; }; In diesem Artikel wird beschrieben, wie der Kernel das Ready-Ereignis an Epoll übergibt und den entsprechenden Prozess aufweckt. Daher konzentrieren wir uns hier hauptsächlich auf Mitglieder wie (wait_queue_head_t wq). 2.3, epoll_ctl (hinzufügen) Sehen wir uns an, wie epoll_ctl (EPOLL_CTL_ADD) den entsprechenden Dateideskriptor in eventpoll einfügt. SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd, Struktur epoll_event __user *, Ereignis) { /* Prüfen, ob epfd der Epoll-Deskriptor ist*/ // Der Mutex dient hier dazu, gleichzeitige Aufrufe von epoll_ctl zu verhindern, d. h. die interne Datenstruktur zu schützen. // Sie wird nicht durch gleichzeitige Hinzufügungen, Änderungen und Löschungen zerstört. mutex_lock_nested(&ep->mtx, 0); Schalter (op) { Fall EPOLL_CTL_ADD: ... // In den Rot-Schwarz-Baum einfügen error = ep_insert(ep, &epds, tfile, fd); ... brechen; ...... } mutex_unlock(&ep->mtx); } Der obige Vorgang ist in der folgenden Abbildung dargestellt: 2.4, ep_insertEpitem wird in ep_insert initialisiert, und dann wird der Schwerpunkt dieses Artikels, die Rückruffunktion, wenn das Ereignis bereit ist, initialisiert. Der Code lautet wie folgt: statische int ep_insert(Struktur eventpoll *ep, Struktur epoll_event *event, Strukturdatei *tfile, int fd) { /* Epitem initialisieren */ // &epq.pt->qproc = ep_ptable_queue_proc init_poll_funcptr(&epq.pt, ep_ptable_queue_proc); // Fügen Sie hier die Rückruffunktion ein revents = tfile->f_op->poll(tfile, &epq.pt); // Wenn ein Ereignis bereit ist, wird es am Anfang zur Bereitschaftsliste hinzugefügt // Zum Beispiel schreibbare Ereignisse // Rufen Sie außerdem nach der internen TCP-Bestätigung tcp_check_space auf und rufen Sie schließlich sock_def_write_space auf, um den entsprechenden Prozess unter epoll_wait zu wecken, if ((revents & event->events) && !ep_is_linked(&epi->rdllink)) { list_add_tail(&epi->rdllink, &ep->rdllist); // wake_up ep entspricht dem Prozess unter epoll_wait if (waitqueue_active(&ep->wq)){ aufwachen_gesperrt(&ep->wq); } ...... } //Epitem in den Rot-Schwarz-Baum einfügen ep_rbtree_insert(ep, epi); ...... } 2.5. Implementierung von tfile->f_op->pollDie auf der unteren Ebene des Kernels registrierte Rückruffunktion lautet tfile->f_op->poll(tfile, &epq.pt). Schauen wir uns den Initialisierungsprozess von fd=>file->f_op->poll für den entsprechenden Socket-Dateideskriptor an: // Fügen Sie die Ereignisse nach dem Akzeptieren zum entsprechenden Epoll fd hinzu int client_fd = accept(listen_fd,(struct sockaddr *)&client_addr,&client_len))); // Registrieren Sie den Verbindungsdeskriptor beim entsprechenden Worker epoll_ctl(reactor->client_fd,EPOLL_CTL_ADD,epifd,&event); Wenn wir auf den obigen Benutzerbereichscode zurückblicken, wird fd, nämlich client_fd, von tcps listen_fd über accept aufgerufen. Sehen wir uns also den Schlüsselpfad der accept-Aufrufkette an:
Dann sieht die durch Akzeptieren erhaltene Struktur von client_fd wie folgt aus: (Hinweis: Da es sich um einen TCP-Socket handelt, gilt hier sock->ops=inet_stream_ops. Nachdem wir nun die Implementierung von tfile->f_op->poll kennen, können wir sehen, wie dieser Poll die Rückruffunktion installiert. 2.6. Installation der Callback-FunktionDer Aufrufpfad des Kernels lautet wie folgt:
Nach einem langen Umweg besteht die Installation unserer Rückruffunktion tatsächlich darin, ep_ptable_queue_proc in eventpoll.c aufzurufen und sk->sk_sleep als Kopf seiner Warteschlange zu übergeben. Der Quellcode lautet wie folgt: statisches void ep_ptable_queue_proc(Struktur Datei *Datei, wait_queue_head_t *whead, poll_table *pt) { // Holen Sie sich das Epitem, das dem aktuellen client_fd entspricht Struktur epitem *epi = ep_item_from_epqueue(pt); // &pwq->wait->func=ep_poll_callback, wird zum Aufwecken des Callbacks verwendet // Beachten Sie, dass dies kein init_waitqueue_entry ist, d. h. der aktuelle KSE (aktueller, aktueller Prozess/Thread) wird nicht in // wait_queue geschrieben, da er nicht unbedingt vom aktuell installierten KSE aufgeweckt wird, sondern der KSE sein sollte, der epoll_wait aufweckt init_waitqueue_func_entry(&pwq->warten, ep_poll_callback); // Der Whead ist hier sk->sk_sleep, verknüpfe die aktuelle Warteschlange mit der Ruheliste, die dem Socket entspricht add_wait_queue(whead, &pwq->wait); } Auf diese Weise wird die Struktur von client_fd weiter verbessert, wie in der folgenden Abbildung dargestellt: In der Funktion ep_poll_callback wird das entsprechende epoll_wait aufgeweckt, das wir später beschreiben werden. 2.7, epoll_waitepoll_wait ruft hauptsächlich ep_poll auf: SYSCALL_DEFINE4(epoll_wait, int, epfd, Struktur epoll_event __user *, Ereignisse, int, MaxEvents, int, Timeout) { /* Überprüfen Sie, ob epfd das von epoll_create erstellte fd ist */ //ep_poll aufrufen Fehler = ep_poll(ep, Ereignisse, Max. Ereignisse, Timeout); ... } Schauen wir uns als nächstes die Funktion ep_poll an: statische int ep_poll(Struktur eventpoll *ep, Struktur epoll_event __user *events, int maxevents, langes Timeout) { ...... wiederholen: // Spinlock abrufen spin_lock_irqsave(&ep->Sperre, Flags); // Schreibe die aktuelle Task-Struktur in die Warteschlange zum Aufwecken // wq_entry->func = default_wake_function; init_waitqueue_entry(&warten, aktuell); // WQ_FLAG_EXCLUSIVE, exklusives Aufwecken, Zusammenarbeit mit SO_REUSEPORT um das Accept-Panik-Problem zu lösen wait.flags |= WQ_FLAG_EXCLUSIVE; // Link zur Warteschlange von ep __add_wait_queue(&ep->wq, &wait); für (;;) { // Den aktuellen Prozessstatus auf unterbrechbar setzen set_current_state(TASK_INTERRUPTIBLE); //Überprüfen Sie, ob der aktuelle Thread ein zu verarbeitendes Signal hat. Wenn ja, geben Sie -EINTR zurück. wenn (signal_pending(current)) { res = -EINTR; brechen; } spin_unlock_irqrestore(&ep->Sperre, Flags); // Zeitplan planen, die CPU aufgeben jtimeout = Zeitplan_Timeout(jtimeout); spin_lock_irqsave(&ep->Sperre, Flags); } // Hier wird angezeigt, dass der Prozess aufgrund einer Zeitüberschreitung oder eines ausgelösten Ereignisses neu geplant wurde. __remove_wait_queue(&ep->wq, &wait); // Setzt den Prozessstatus auf „Läuft“ setze_aktuellen_Zustand(AUFGABE_LÄUFT); ...... // Prüfen, ob Ereignisse verfügbar sind eavail = !list_empty(&ep->rdllist) || ep->ovflist != EP_UNACTIVE_PTR; ...... //Bereitige Ereignisse in den Benutzerbereich kopieren ep_send_events(ep, events, maxevents) } Die obige Logik wird in der folgenden Abbildung dargestellt: 2.8, ep_send_eventsDie Funktion ep_send_events ruft hauptsächlich ep_scan_ready_list auf. Wie der Name schon sagt, ist ep_scan_ready_list die Scan-Ready-Liste: statische int ep_scan_ready_list(Struktur eventpoll *ep, int (*sproc)(Struktur eventpoll *, Struktur list_head *, void *), void *priv, int Tiefe) { ... // Verknüpfe epfds rdlist mit txlist list_splice_init(&ep->rdlist, &txlist); ... /* sproc = ep_send_events_proc */ Fehler = (*sproc)(ep, &txlist, priv); ... // Verarbeiten Sie ovflist, also das Ereignis, das im obigen Sproc-Prozess auftritt ... } Es ruft hauptsächlich ep_send_events_proc auf: statische int ep_send_events_proc(Struktur eventpoll *ep, Struktur list_head *head, void *priv) { für (eventcnt = 0, uevent = esed->events; !list_empty(head) && eventcnt < esed->maxevents;) { // Durchlaufe die Bereitschaftsliste epi = Liste_erster_Eintrag(Kopf, Struktur epitem, rdllink); list_del_init(&epi->rdlllink); // Readylist zeigt nur an, dass in der aktuellen Epi ein Ereignis vorliegt. Die spezifischen Ereignisinformationen müssen noch die Umfrage der entsprechenden Datei aufrufen // Die Umfrage hier ist tcp_poll, das die Maske und andere Informationen entsprechend den Informationen von TCP selbst festlegt und die Ereignismaske von Interesse festlegt, um zu wissen, ob das aktuelle Ereignis das Ereignis von Interesse für epoll_wait ist revents = epi->ffd.file->f_op->poll(epi->ffd.file, NULL) & epi->Ereignis.Ereignisse; wenn(Ereignisse){ /* Das Ereignis in den Benutzerbereich setzen */ /* ONESHOT-Logik verarbeiten */ // Wenn es nicht durch die Flanke ausgelöst wird, fügen Sie das aktuelle Epi wieder zur verfügbaren Liste hinzu, damit die Umfrage beim nächsten Mal ausgelöst werden kann. Wenn die Ergebnisse der nächsten Umfrage nicht 0 sind, kann der Benutzerbereich sie immer noch wahrnehmen*/ sonst wenn (!(epi->event.events & EPOLLET)){ list_add_tail(&epi->rdllink, &ep->rdllist); } /* Wenn es durch die Flanke ausgelöst wird, wird es nicht wieder zur Liste der verfügbaren Ereignisse hinzugefügt. Daher wird das entsprechende Epi erst dann in die Liste der verfügbaren Ereignisse aufgenommen, wenn das nächste verfügbare Ereignis ausgelöst wird.*/ Ereigniscnt++ } /* Wenn epoll_wait nicht an dem abgefragten Revents-Ereignis interessiert ist (oder überhaupt kein Ereignis vorliegt), wird es nicht wieder zur Liste der verfügbaren Ereignisse hinzugefügt*/ ...... } gibt eventcnt zurück; } Die Logik des obigen Codes ist wie folgt: 3. Der Prozess des Hinzufügens von Ereignissen zur Epoll-Bereitschaftswarteschlange (rdlist)Nach der ausführlichen Beschreibung in den obigen Kapiteln können wir nun endlich erklären, wie sich TCP bei eintreffenden Daten in die Bereitschaftswarteschlange von Epoll einfügt. 3.1. Lesbare Ereignisse kommen anSchauen wir uns zunächst das TCP-Datenpaket vom Netzwerkkartentreiber zur internen TCP-Protokollverarbeitungsaufrufkette des Kernels an: Schritt 1: Der Kernelpfad des ankommenden Netzwerkpakets, nachdem die Netzwerkkarte einen Interrupt initiiert hat, ruft netif_rx auf, um das Ereignis in die Warteschlange der CPU zu hängen, und ruft den Soft-Interrupt (soft_irq) auf. Anschließend ruft er net_rx_action über den Linux-Soft-Interrupt-Mechanismus auf, wie in der folgenden Abbildung dargestellt: Hinweis: Das obige Bild stammt von PLKA (<<In-depth Linux Kernel Architecture>>) Schritt 2: Folgen Sie dann next_rx_action
Schauen wir uns den entsprechenden tcp_v4_rcv an
Schauen wir uns auf diese Weise die Funktion ep_poll_callback an, die schließlich epoll_wait aufweckt: statische int ep_poll_callback(wait_queue_t *warten, vorzeichenloser Modus, int sync, void *Schlüssel) { // Holen Sie sich das Epitem, das dem Warten entspricht Struktur epitem *epi = ep_item_from_wait(wait); // Eventpoll-Struktur entsprechend dem Epitem struct eventpoll *ep = epi->ep; // Holen Sie sich den Spinlock, um die Ready_List und andere Strukturen zu schützen spin_lock_irqsave(&ep->lock, flags); // Wenn das aktuelle Epi nicht mit der Liste der bereiten Ereignisse von EP verknüpft ist, dann verknüpfen Sie es. // Auf diese Weise werden die aktuell verfügbaren Ereignisse zur Liste der verfügbaren Ereignisse von Epoll hinzugefügt, wenn (!ep_is_linked(&epi->rdllink)). list_add_tail(&epi->rdllink, &ep->rdllist); // Wenn epoll_wait wartet, wecken Sie den epoll_wait-Prozess auf // Das entsprechende &ep->wq wird durch init_waitqueue_entry(&wait, current) generiert, wenn epoll_wait aufgerufen wird // Das aktuelle ist die Prozessinformation task_struct, die dem Aufruf von epoll_wait entspricht wenn (waitqueue_active(&ep->wq)) aufwachen_gesperrt(&ep->wq); } Der obige Vorgang ist in der folgenden Abbildung dargestellt: Schließlich ruft wake_up_locked __wake_up_common auf und ruft dann default_wake_function auf, das in init_waitqueue_entry registriert ist. Der Aufrufpfad lautet:
Schieben Sie den epoll_wait-Prozess in die ausführbare Warteschlange, warten Sie, bis der Kernel den Prozess neu plant, und nehmen Sie dann, nachdem der epoll_wait entsprechende Prozess erneut ausgeführt wurde, die Planung wieder auf und fahren Sie mit den folgenden ep_send_events fort (Ereignisse in den Benutzerbereich kopieren und zurückkehren). Der Wake_up-Prozess ist in der folgenden Abbildung dargestellt: 3.2. Ankunft eines beschreibbaren EreignissesDer Ablauf bei schreibbaren Ereignissen ähnelt dem bei lesbaren Ereignissen: Wenn epoll_ctl_add aufgerufen wird, wird zunächst einmal im Voraus eine Abfrage des entsprechenden Dateideskriptors aufgerufen. Wenn das Rückgabeereignis eine beschreibbare Maske enthält, wird wake_up_locked direkt aufgerufen, um den entsprechenden epoll_wait-Prozess aufzuwecken. Wenn die Daten dann beim zugrunde liegenden Treiber von TCP ankommen, können sie eine Bestätigung enthalten, sodass einige der vom anderen Ende empfangenen Daten freigegeben werden können, wodurch ein Schreibereignis ausgelöst wird. Die Aufrufkette dieses Teils lautet:
Schließlich weckt sk_stream_write_space in dieser Funktion den entsprechenden epoll_wait-Prozess void sk_stream_write_space(Struktur sock *sk) { // Das heißt, das Schreibereignis wird nur ausgelöst, wenn 1/3 des Schreibspeicherplatzes vorhanden ist, if (sk_stream_wspace(sk) >= sk_stream_min_wspace(sk) && sock) { : Clear_Bit (SOCK_NOSPACE, &sock->Flags); wenn (sk->sk_sleep und waitqueue_active(sk->sk_sleep)) unterbrechungsfähige Aufwachumfrage(sk->sk_sleep, POLLOUT | POLLWRNORM | POLLWRBAND) ...... } } 4. Deskriptor schließen (close fd)Es ist zu beachten, dass wir beim Schließen des entsprechenden Dateideskriptors automatisch eventpoll_release aufrufen, um die entsprechende Datei aus dem zugehörigen epoll_fd zu löschen. Der Kernelschlüsselpfad lautet wie folgt:
Daher müssen wir nach dem Schließen des entsprechenden Dateideskriptors den entsprechenden Deskriptor im entsprechenden Epoll nicht über epoll_ctl_del löschen. V. FazitEpoll wird unter Linux häufig als hervorragender Mechanismus zur Ereignisauslösung verwendet. Der Quellcode ist relativ komplex. Dieser Artikel erklärt nur den Auslösemechanismus von Epoll-Lese- und Schreibereignissen. Oben finden Sie den detaillierten Inhalt zum Parsen des Linux-Quellcodes Epoll. Weitere Informationen zum Linux-Quellcode Epoll finden Sie in den anderen verwandten Artikeln auf 123WORDPRESS.COM! Das könnte Sie auch interessieren:
|
<<: Mehrere Grundsätze für die Produktdesign-Referenz auf Websites
>>: Detaillierte Erklärung der Vuex-Umgebung
Textschatten-Textschatten-Eigenschaftseffekte: 1....
Vorwort Ich muss dem Markodwn-Editor, den ich ger...
Inhaltsverzeichnis Überblick Ist die Erweiterung ...
Bevor wir Docker offiziell verwenden, machen wir ...
Notieren Sie einige der Orte, an denen Sie Zeit v...
Als Nächstes werde ich zwei Tabellen erstellen un...
Prozessstrukturdiagramm Nginx ist eine Multiproze...
Ich glaube, die Befehle, die ich am häufigsten ve...
1. Fall Nehmen Sie alle Mitarbeiter, die nicht Fi...
Erweiterte MySQL-SQL-Anweisungen benutze kgc; Tab...
1. Einfache Konfiguration der dynamischen und sta...
Shell ist ein in der Programmiersprache C geschri...
Während der Entwicklung wurden die folgenden Situ...
Wenn die Bilder des Servers von anderen Websites ...
Inhaltsverzeichnis Installieren und konfigurieren...