epoll
haben 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 epoll
Informationen ü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 errno
Einstellung 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_table
ist 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_proc
stellt 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
_key
Tabelle poll_table
ist 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
_key
auf ~0
(Ergänzung zu 0) gesetzt. Dies bedeutet, dass epoll
Informationen ü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_proc
ursprünglichen Struktur zu erleichtern epitem
, epoll
wird eine einfache Struktur namens genanntep_pqueue
welches als Wrapper poll_table
mit 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 epi
einen ep_pqueue
Zeiger 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 _qproc
Strukturelement ep_pqueue
und _key
schreibt 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.c
in 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
epoll
Entität handelt es _qproc
sich um eine Funktion ep_ptable_queue_proc()
, die in der Datei fs/eventpoll.c
in 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 epoll
eine Wrapper-Struktur verwendet wird ep_pqueue
, ist das Wiederherstellen epitem
von einem Zeiger poll_table
eine 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 epitem
für diese Datei. Es ist epoll
äußerst wichtig zu wissen, wo sich der Warteschlangenkopf für die zu überwachende Datei befindet. Andernfalls epoll
kann die Registrierung der Warteschlange später nicht aufgehoben werden. Struktureppoll_entry
Enthält auch eine wait ( pwq->wait
) - Warteschlange mit einer Funktion zur Wiederaufnahme des Prozesses ep_poll_callback()
. Möglicherweise ist pwq->wait
dies der wichtigste Teil der gesamten Implementierung epoll
, da diese Entität zur Lösung der folgenden Aufgaben verwendet wird:
- Überwachen Sie Ereignisse, die mit einer bestimmten zu überwachenden Datei auftreten.
- Wiederaufnahme der Arbeit anderer Prozesse für den Fall, dass ein solcher Bedarf entsteht.
Dann wird es
ep_ptable_queue_proc()
anhängen pwq->wait
auf die Warteschlange der Zieldatei ( whead
). Die Funktion fügt struct eppoll_entry
der verknüpften Liste auch from struct epitem
( epi->pwqlist
) hinzu und erhöht den Wert, epi->nwait
der die Länge der Liste darstellt epi->pwqlist
.
Und hier habe ich eine Frage. Warum
epoll
eine verknüpfte Liste verwenden, um eine Struktur eppoll_entry
in einer epitem
einzelnen Dateistruktur zu speichern ? Wird nicht epitem
nur ein Element eppoll_entry
benötigt?
Ich kann diese Frage jedoch nicht genau beantworten. Soweit ich das beurteilen kann
epoll
, enthält die Liste epi->pwqlist
nur ein Element struct eppoll_entry
, und es sei denn, jemand wird Instanzen in verrückten Schleifen verwendenepi->nwait
für die meisten Dateien ist wahrscheinlich 1
.
Das Gute ist, dass die Unklarheiten
epi->pwqlist
in keiner Weise das beeinflussen, worüber ich weiter unten sprechen werde. Wir werden nämlich darüber sprechen, wie Linux Instanzen epoll
von 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 )
epoll
angehä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_t
wait_queue_head_t
wait_queue_t
wait_queue_t
wait_queue_head_t
epoll
kann entscheiden, was mit dem Wiederaufnahmesignal geschehen soll, epoll
es 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_wq
in 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_t
ohne Rückruf.
Wann genau wird die Wiederaufnahme der Arbeit
sk_wq
in der Struktur durchgeführt sock
? Wie sich herausstellt, folgt das Linux-Socket-System den gleichen "OO" -Designprinzipien wie VFS. Die Struktur sock
deklariert 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_wq
den 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 full
nach ä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
. epoll
Lassen 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?