epoll
, nous avons mené une enquête sur la faisabilité de poursuivre le cycle de traduction. Plus de 90% des participants à l'enquête étaient favorables à la traduction du reste des articles. Par conséquent, nous publions aujourd'hui une traduction du deuxième matériel de ce cycle.
Fonction Ep_insert ()
Une fonction
ep_insert()
est l'une des fonctions les plus importantes d'une implémentation epoll
. Il est extrêmement important de comprendre son fonctionnement pour comprendre comment il epoll
obtient exactement des informations sur les nouveaux événements à partir des fichiers qu'il surveille.
La déclaration
ep_insert()
se trouve à la ligne 1267 du fichier fs/eventpoll.c
. Regardons quelques extraits de code pour cette fonction:
user_watches = atomic_long_read(&ep->user->epoll_watches);
if (unlikely(user_watches >= max_user_watches))
return -ENOSPC;
Dans cet extrait de code, la fonction
ep_insert()
vérifie d'abord si le nombre total de fichiers que l'utilisateur actuel regarde n'est pas supérieur à la valeur spécifiée dans /proc/sys/fs/epoll/max_user_watches
. Si user_watches >= max_user_watches
, la fonction se termine immédiatement avec l' errno
ensemble à ENOSPC
.
Il
ep_insert()
alloue ensuite de la mémoire à l'aide du mécanisme de gestion de la mémoire de la dalle du noyau Linux:
if (!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL)))
return -ENOMEM;
Si la fonction a pu allouer suffisamment de mémoire pour
struct epitem
, le processus d'initialisation suivant sera exécuté:
/* ... */
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;
Après cela, il
ep_insert()
essaiera d'enregistrer le rappel dans le descripteur de fichier. Mais avant de pouvoir en parler, nous devons nous familiariser avec certaines structures de données importantes.
Framework
poll_table
est une entité importante utilisée par une implémentation poll()
VFS. (Je comprends que cela peut être déroutant, mais ici je voudrais expliquer que la fonction que poll()
j'ai mentionnée ici est une implémentation d'une opération de fichier poll()
, pas un appel système poll()
). Elle est annoncée dans include/linux/poll.h
:
typedef struct poll_table_struct {
poll_queue_proc _qproc;
unsigned long _key;
} poll_table;
Une entité
poll_queue_proc
représente un type de fonction de rappel qui ressemble à ceci:
typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);
Un membre d'une
_key
table poll_table
n'est en fait pas ce qu'il semble être au départ. A savoir, malgré le nom suggérant une certaine «clé», en _key
fait, les masques des événements qui nous intéressent sont stockés. Dans l'implémentation, il est epoll
_key
défini sur ~0
(complément à 0). Cela signifie qu'il epoll
cherche à recevoir des informations sur des événements de toute nature. Cela a du sens, car les applications de l'espace utilisateur peuvent modifier le masque d'événement à tout moment en utilisant epoll_ctl()
, en acceptant tous les événements du VFS, puis en les filtrant dans l'implémentation epoll
, ce qui facilite les choses.
Afin de faciliter la restauration de la
poll_queue_proc
structure d'origine epitem
, il epoll
utilise une structure simple appeléeep_pqueue
qui sert de wrapper poll_table
avec un pointeur vers la structure correspondante epitem
(fichier fs/eventpoll.c
, ligne 243):
/* -, */
struct ep_pqueue {
poll_table pt;
struct epitem *epi;
};
Ensuite, il
ep_insert()
s'initialise struct ep_pqueue
. Le code suivant écrit d'abord sur un membre de epi
structure un ep_pqueue
pointeur vers une structure epitem
correspondant au fichier que nous essayons d'ajouter, puis écrit ep_ptable_queue_proc()
sur un membre de _qproc
structure ep_pqueue
et y _key
écrit ~0
.
/* */
epq.epi = epi;
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
Il
ep_insert()
appellera ensuite ep_item_poll(epi, &epq.pt);
, ce qui entraînera un appel à l'implémentation poll()
associée au fichier.
Jetons un coup d'œil à un exemple qui utilise l'implémentation de la
poll()
pile TCP Linux et comprenons ce que fait exactement cette implémentation poll_table
.
Une fonction
tcp_poll()
est une implémentation poll()
pour les sockets TCP. Son code se trouve dans le fichier net/ipv4/tcp.c
, à la ligne 436. Voici un extrait de ce code:
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);
//
}
La fonction
tcp_poll()
appelle sock_poll_wait()
, en passant, comme deuxième argument, sk_sleep(sk)
et comme troisième - wait
(c'est la tcp_poll()
table précédemment passée à la fonction poll_table
).
Qu'est-ce que c'est
sk_sleep()
? En fait, il ne s'agit que d'un getter pour accéder à la file d'attente d'événements pour une structure spécifique sock
(fichier include/net/sock.h
, ligne 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;
}
Que
sock_poll_wait()
va faire la file d'attente des événements? Il s'avère que cette fonction effectuera une vérification simple, puis appellera poll_wait()
avec les mêmes paramètres. La fonction poll_wait()
appellera alors le callback que nous avons spécifié et lui passera une file d'attente d'événements (fichier include/linux/poll.h
, ligne 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);
}
Dans le cas de l'
epoll
entité, ce _qproc
sera une fonction ep_ptable_queue_proc()
déclarée dans le fichier fs/eventpoll.c
à la ligne 1091.
/*
* - ,
* , .
*/
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;
}
}
Tout d'abord, il
ep_ptable_queue_proc()
essaie de restaurer la structure epitem
qui correspond au fichier de la file d'attente avec laquelle nous travaillons. Puisqu'il epoll
utilise une structure wrapper ep_pqueue
, la restauration à epitem
partir d'un pointeur poll_table
est une simple opération de pointeur.
Après cela, il
ep_ptable_queue_proc()
alloue juste autant de mémoire que nécessaire pour struct eppoll_entry
. Cette structure sert de "colle" entre la file d'attente du fichier surveillé et la structure correspondante epitem
pour ce fichier. Il est epoll
extrêmement important de savoir où se trouve la tête de file d'attente pour le fichier surveillé. Sinon, epoll
il ne pourra pas désinscrire la file d'attente plus tard. Structureeppoll_entry
comprend également une file pwq->wait
d' attente wait ( ) avec une fonction de reprise de processus fournie ep_poll_callback()
. C'est peut pwq->wait
- être la partie la plus importante de toute l'implémentation epoll
, car cette entité est utilisée pour résoudre les tâches suivantes:
- Surveillez les événements se produisant avec un fichier spécifique surveillé.
- Reprise des travaux d'autres processus au cas où un tel besoin se présenterait.
Ensuite, il
ep_ptable_queue_proc()
s'attachera pwq->wait
à la file d'attente du fichier cible ( whead
). La fonction ajoutera également struct eppoll_entry
à la liste liée à partir de struct epitem
( epi->pwqlist
) et incrémentera la valeur epi->nwait
représentant la longueur de la liste epi->pwqlist
.
Et ici, j'ai une question. Pourquoi
epoll
utiliser une liste chaînée pour stocker une structure eppoll_entry
dans une epitem
seule structure de fichier? Un epitem
seul élément n'est-il pas eppoll_entry
nécessaire?
Je ne peux vraiment pas répondre exactement à cette question. Pour autant que je sache, à moins que quelqu'un n'utilise des instances
epoll
dans des boucles folles, la liste epi->pwqlist
ne contiendra qu'un seul élément struct eppoll_entry
, etepi->nwait
pour la plupart des fichiers est susceptible d'être 1
.
La bonne chose est que les ambiguïtés autour
epi->pwqlist
n'affectent en rien ce dont je vais parler ci-dessous. À savoir, nous parlerons de la façon dont Linux notifie les instances epoll
d'événements survenant aux fichiers surveillés.
Vous vous souvenez de ce dont nous avons parlé dans la section précédente? Il s'agissait de ce qui
epoll
s'ajoute wait_queue_t
à la liste d'attente du ou des fichiers cibles wait_queue_head_t
. Bien que le wait_queue_t
plus souvent utilisé comme mécanisme de reprise des processus, il s'agit essentiellement d'une structure qui stocke un pointeur vers une fonction qui sera appelée lorsque Linux décide de reprendre les processus à partir de la file d'attente wait_queue_t
associée wait_queue_head_t
. Dans cette fonctionepoll
peut décider quoi faire avec le signal de reprise, mais epoll
il n'est pas nécessaire de reprendre un processus! Comme vous le verrez plus tard, ep_poll_callback()
rien ne se passe généralement lorsque vous appelez CV.
Je suppose qu'il vaut également la peine de noter que le mécanisme de reprise du processus utilisé dans
poll()
dépend complètement de la mise en œuvre. Dans le cas des fichiers de socket TCP, la tête de file d'attente est un membre sk_wq
stocké dans la structure sock
. Cela explique également la nécessité d'utiliser un rappel ep_ptable_queue_proc()
pour travailler avec la file d'attente. Étant donné que dans les implémentations de la file d'attente pour différents fichiers, la tête de la file d'attente peut apparaître à des endroits complètement différents, nous n'avons aucun moyen de trouver la valeur dont nous avons besoinwait_queue_head_t
sans utiliser de rappel.
Quand exactement la reprise des travaux
sk_wq
dans la structure est-elle effectuée sock
? Il s'avère que le système de socket Linux suit les mêmes principes de conception «OO» que VFS. La structure sock
déclare les hooks suivants à la ligne 2312 du fichier 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()
et sock_def_write_space()
l'appel est wake_up_interruptible_sync_poll()
destiné (struct sock)->sk_wq
au travail de processus renouvelable de rappel de fonction.
Quand sera-t-il
sk->sk_data_ready()
et sera-t-il appelé sk->sk_write_space()
? Cela dépend de la mise en œuvre. Prenons l'exemple des sockets TCP. La fonction sk->sk_data_ready()
sera appelée dans la seconde moitié du gestionnaire d'interruption lorsque la connexion TCP termine la procédure d'établissement de liaison à trois ou lorsqu'un tampon est reçu pour un certain socket TCP. La fonction sk->sk_write_space()
sera appelée lorsque l'état du tampon passera de full
à available
. Si vous gardez cela à l'esprit lors de l'analyse des sujets suivants, en particulier celui sur le déclenchement frontal, ces sujets sembleront plus intéressants.
Résultat
Ceci conclut le deuxième article d'une série d'articles sur la mise en œuvre
epoll
. La prochaine fois, parlons de ce qu'il fait exactement epoll
dans le rappel enregistré dans la file d'attente de reprise des processus de socket.
Avez-vous utilisé epoll?