Epoll-Implementierung, Teil 2

Bei der Veröffentlichung der Übersetzung des ersten Artikels aus der Implementierungsreihe epollhaben wir eine Umfrage zur Machbarkeit der Fortsetzung der Zyklusübersetzung durchgeführt. Mehr als 90% der Umfrageteilnehmer befürworteten die Übersetzung der restlichen Artikel. Deshalb veröffentlichen wir heute eine Übersetzung des zweiten Materials aus diesem Zyklus.







Ep_insert () Funktion



Eine Funktion ep_insert()ist eine der wichtigsten Funktionen in einer Implementierung epoll. Es ist äußerst wichtig zu verstehen, wie es funktioniert, um zu verstehen, wie genau es epollInformationen über neue Ereignisse aus den Dateien erhält, die es sich ansieht.



Die Deklaration ep_insert()finden Sie in Zeile 1267 der Datei fs/eventpoll.c. Schauen wir uns einige Codefragmente für diese Funktion an:



user_watches = atomic_long_read(&ep->user->epoll_watches);
if (unlikely(user_watches >= max_user_watches))
  return -ENOSPC;


In diesem Codeausschnitt ep_insert()prüft die Funktion zunächst, ob die Gesamtzahl der Dateien, die der aktuelle Benutzer überwacht, nicht größer als der in angegebene Wert ist /proc/sys/fs/epoll/max_user_watches. Wenn user_watches >= max_user_watches, dann endet die Funktion sofort mit der errnoEinstellung auf ENOSPC.



Anschließend ep_insert()wird Speicher mithilfe des Linux-Kernel-Slab-Speicherverwaltungsmechanismus zugewiesen:



if (!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL)))
  return -ENOMEM;


Wenn die Funktion genügend Speicher zuordnen konnte struct epitem, wird der folgende Initialisierungsprozess ausgeführt:



/*  ... */
INIT_LIST_HEAD(&epi->rdllink);
INIT_LIST_HEAD(&epi->fllink);
INIT_LIST_HEAD(&epi->pwqlist);
epi->ep = ep;
ep_set_ffd(&epi->ffd, tfile, fd);
epi->event = *event;
epi->nwait = 0;
epi->next = EP_UNACTIVE_PTR;


Danach ep_insert()wird versucht, den Rückruf im Dateideskriptor zu registrieren. Bevor wir jedoch darüber sprechen können, müssen wir uns mit einigen wichtigen Datenstrukturen vertraut machen.



Framework poll_tableist eine wichtige Entität, die von einer poll()VFS- Implementierung verwendet wird . (Ich verstehe, dass dies verwirrend sein kann, aber hier möchte ich erklären, dass die poll()hier erwähnte Funktion eine Implementierung einer Dateioperation ist poll(), kein Systemaufruf poll()). Sie wird angekündigt in include/linux/poll.h:



typedef struct poll_table_struct {
  poll_queue_proc _qproc;
  unsigned long _key;
} poll_table;


Eine Entität poll_queue_procstellt eine Art Rückruffunktion dar, die folgendermaßen aussieht:



typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);


Ein Mitglied einer _keyTabelle poll_tableist eigentlich nicht das, was es zuerst zu sein scheint. Trotz des Namens, der einen bestimmten "Schlüssel" andeutet _key, werden nämlich die Masken der Ereignisse gespeichert, die für uns von Interesse sind. In der Implementierung wird es epoll _keyauf ~0(Ergänzung zu 0) gesetzt. Dies bedeutet, dass epollInformationen über Ereignisse jeglicher Art erhalten werden sollen. Dies ist sinnvoll, da User-Space-Anwendungen die Ereignismaske jederzeit ändern können, indem epoll_ctl()sie alle Ereignisse aus dem VFS akzeptieren und sie dann in der Implementierung filtern epoll, was die Arbeit erleichtert.



Um die Wiederherstellung der poll_queue_procursprünglichen Struktur zu erleichtern epitem, epollwird eine einfache Struktur namens genanntep_pqueuewelches als Wrapper poll_tablemit einem Zeiger auf die entsprechende Struktur dient epitem(Datei fs/eventpoll.c, Zeile 243):



/* -,    */
struct ep_pqueue {
  poll_table pt;
  struct epitem *epi;
};


Dann wird es ep_insert()initialisiert struct ep_pqueue. Der folgende Code schreibt zuerst epieinen ep_pqueueZeiger auf eine Struktur in ein Strukturelement epitem, die der Datei entspricht, die wir hinzufügen möchten, und schreibt dann ep_ptable_queue_proc()in ein _qprocStrukturelement ep_pqueueund _keyschreibt in dieses ~0.



/*      */
epq.epi = epi;
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);


Es ep_insert()wird dann aufgerufen ep_item_poll(epi, &epq.pt);, was zu einem Aufruf der poll()der Datei zugeordneten Implementierung führt .



Schauen wir uns ein Beispiel an, das die poll()Linux-TCP-Stack- Implementierung verwendet, und verstehen, was genau diese Implementierung bewirkt poll_table.



Eine Funktion tcp_poll()ist eine Implementierung poll()für TCP-Sockets. Der Code befindet sich in der Datei net/ipv4/tcp.cin Zeile 436. Hier ein Ausschnitt dieses Codes:



unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait)
{
  unsigned int mask;
  struct sock *sk = sock->sk;
  const struct tcp_sock *tp = tcp_sk(sk);

  sock_rps_record_flow(sk);

  sock_poll_wait(file, sk_sleep(sk), wait);

  //  
}


Die Funktion tcp_poll()ruft sock_poll_wait()als zweites Argument sk_sleep(sk)und als drittes Argument auf wait(dies ist die tcp_poll()Tabelle, die zuvor an die Funktion übergeben wurde poll_table).



Was ist das sk_sleep()? Wie sich herausstellt, ist dies nur ein Getter für den Zugriff auf die Ereigniswarteschlange für eine bestimmte Struktur sock(Datei include/net/sock.h, Zeile 1685):



static inline wait_queue_head_t *sk_sleep(struct sock *sk)
{
  BUILD_BUG_ON(offsetof(struct socket_wq, wait) != 0);
  return &rcu_dereference_raw(sk->sk_wq)->wait;
}


Was wird sock_poll_wait()mit der Warteschlange für Ereignisse geschehen? Es stellt sich heraus, dass diese Funktion eine einfache Überprüfung durchführt und dann poll_wait()mit denselben Parametern aufruft. Die Funktion poll_wait()ruft dann den von uns angegebenen Rückruf auf und übergibt ihm eine Warteschlange für wartende Ereignisse (Datei include/linux/poll.h, Zeile 42):



static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
  if (p && p->_qproc && wait_address)
    p->_qproc(filp, wait_address, p);
}


Im Fall der epollEntität handelt es _qprocsich um eine Funktion ep_ptable_queue_proc(), die in der Datei fs/eventpoll.cin Zeile 1091 deklariert ist .



/*
*  - ,       
*     ,    .
*/
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
       poll_table *pt)
{
  struct epitem *epi = ep_item_from_epqueue(pt);
  struct eppoll_entry *pwq;

  if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {
    init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
    pwq->whead = whead;
    pwq->base = epi;
    add_wait_queue(whead, &pwq->wait);
    list_add_tail(&pwq->llink, &epi->pwqlist);
    epi->nwait++;
  } else {
    /*       */
    epi->nwait = -1;
  }
}


Zunächst wird ep_ptable_queue_proc()versucht, die Struktur epitem, die der Datei entspricht, aus der Warteschlange wiederherzustellen , mit der wir arbeiten. Da epolleine Wrapper-Struktur verwendet wird ep_pqueue, ist das Wiederherstellen epitemvon einem Zeiger poll_tableeine einfache Zeigeroperation.



Danach wird ep_ptable_queue_proc()nur so viel Speicher zugewiesen, wie für erforderlich ist struct eppoll_entry. Diese Struktur fungiert als "Klebstoff" zwischen der Warteschlange für die zu überwachende Datei und der entsprechenden Struktur epitemfür diese Datei. Es ist epolläußerst wichtig zu wissen, wo sich der Warteschlangenkopf für die zu überwachende Datei befindet. Andernfalls epollkann die Registrierung der Warteschlange später nicht aufgehoben werden. Struktureppoll_entryEnthält auch eine wait ( pwq->wait) - Warteschlange mit einer Funktion zur Wiederaufnahme des Prozesses ep_poll_callback(). Möglicherweise ist pwq->waitdies der wichtigste Teil der gesamten Implementierung epoll, da diese Entität zur Lösung der folgenden Aufgaben verwendet wird:



  1. Überwachen Sie Ereignisse, die mit einer bestimmten zu überwachenden Datei auftreten.
  2. Wiederaufnahme der Arbeit anderer Prozesse für den Fall, dass ein solcher Bedarf entsteht.


Dann wird es ep_ptable_queue_proc()anhängen pwq->waitauf die Warteschlange der Zieldatei ( whead). Die Funktion fügt struct eppoll_entryder verknüpften Liste auch from struct epitem( epi->pwqlist) hinzu und erhöht den Wert, epi->nwaitder die Länge der Liste darstellt epi->pwqlist.



Und hier habe ich eine Frage. Warum epolleine verknüpfte Liste verwenden, um eine Struktur eppoll_entryin einer epitemeinzelnen Dateistruktur zu speichern ? Wird nicht epitemnur ein Element eppoll_entrybenötigt?



Ich kann diese Frage jedoch nicht genau beantworten. Soweit ich das beurteilen kann epoll, enthält die Liste epi->pwqlistnur ein Element struct eppoll_entry, und es sei denn, jemand wird Instanzen in verrückten Schleifen verwendenepi->nwaitfür die meisten Dateien ist wahrscheinlich 1.



Das Gute ist, dass die Unklarheiten epi->pwqlistin keiner Weise das beeinflussen, worüber ich weiter unten sprechen werde. Wir werden nämlich darüber sprechen, wie Linux Instanzen epollvon Ereignissen benachrichtigt , die bei überwachten Dateien auftreten.



Erinnerst du dich, worüber wir im vorherigen Abschnitt gesprochen haben? Es ging darum, was an die Warteliste der Zieldatei (en ) epollangehängt wird . Obwohl es am häufigsten als Mechanismus zum Fortsetzen von Prozessen verwendet wird, ist es im Wesentlichen nur eine Struktur, die einen Zeiger auf eine Funktion speichert, die aufgerufen wird, wenn Linux beschließt, Prozesse aus der angehängten Warteschlange fortzusetzen . In dieser Funktionwait_queue_twait_queue_head_twait_queue_twait_queue_twait_queue_head_tepollkann entscheiden, was mit dem Wiederaufnahmesignal geschehen soll, epolles ist jedoch nicht erforderlich, einen Vorgang fortzusetzen! Wie Sie später sehen werden, ep_poll_callback()passiert normalerweise nichts, wenn Sie wieder aufrufen .



Ich nehme an, es ist auch erwähnenswert, dass der Mechanismus zur Wiederaufnahme des Prozesses poll()vollständig implementierungsabhängig ist. Bei TCP-Socket-Dateien ist der Warteschlangenkopf ein sk_wqin der Struktur gespeichertes Mitglied sock. Dies erklärt auch die Notwendigkeit, einen Rückruf ep_ptable_queue_proc()zu verwenden, um mit der Warteschlange zu arbeiten. Da bei Implementierungen der Warteschlange für verschiedene Dateien der Kopf der Warteschlange möglicherweise an völlig unterschiedlichen Stellen angezeigt wird, können wir den benötigten Wert nicht findenwait_queue_head_tohne Rückruf.



Wann genau wird die Wiederaufnahme der Arbeit sk_wqin der Struktur durchgeführt sock? Wie sich herausstellt, folgt das Linux-Socket-System den gleichen "OO" -Designprinzipien wie VFS. Die Struktur sockdeklariert die folgenden Hooks in Zeile 2312 der Datei net/core/sock.c:



void sock_init_data(struct socket *sock, struct sock *sk)
{
  //  ...
  sk->sk_data_ready  =   sock_def_readable;
  sk->sk_write_space =  sock_def_write_space;
  //  ...
}


B sock_def_readable()und sock_def_write_space()der Anruf ist wake_up_interruptible_sync_poll()für (struct sock)->sk_wqden Zweck der Funktion-Rückruf, erneuerbare Prozess der Arbeit.



Wann wird sk->sk_data_ready()und wird gerufen sk->sk_write_space()? Das hängt von der Implementierung ab. Nehmen wir als Beispiel TCP-Sockets. Die Funktion sk->sk_data_ready()wird in der zweiten Hälfte des Interrupt-Handlers aufgerufen, wenn die TCP-Verbindung den Drei-Wege-Handshake-Vorgang abschließt oder wenn ein Puffer für einen bestimmten TCP-Socket empfangen wird. Die Funktion sk->sk_write_space()wird aufgerufen, wenn sich der Pufferstatus von fullnach ändert available. Wenn Sie dies bei der Analyse der folgenden Themen berücksichtigen, insbesondere beim Thema Front-Triggering, werden diese Themen interessanter.



Ergebnis



Damit ist der zweite Artikel in einer Reihe von Artikeln zur Implementierung abgeschlossen epoll. epollLassen Sie uns das nächste Mal darüber sprechen, was genau in dem Rückruf geschieht, der in der Warteschlange für die Wiederaufnahme der Socket-Prozesse registriert ist.



Hast du epoll benutzt?










All Articles