Implémentation d'Epoll, partie 2

Lors de la publication de la traduction du premier article de la série de mise en œuvre 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 epollobtient 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' errnoensemble à 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_tableest 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_procrepré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 _keytable poll_tablen'est en fait pas ce qu'il semble être au départ. A savoir, malgré le nom suggérant une certaine «clé», en _keyfait, les masques des événements qui nous intéressent sont stockés. Dans l'implémentation, il est epoll _keydéfini sur ~0(complément à 0). Cela signifie qu'il epollcherche à 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_procstructure d'origine epitem, il epollutilise une structure simple appeléeep_pqueuequi sert de wrapper poll_tableavec 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 epistructure un ep_pqueuepointeur vers une structure epitemcorrespondant au fichier que nous essayons d'ajouter, puis écrit ep_ptable_queue_proc()sur un membre de _qprocstructure ep_pqueueet 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' epollentité, ce _qprocsera 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 epitemqui correspond au fichier de la file d'attente avec laquelle nous travaillons. Puisqu'il epollutilise une structure wrapper ep_pqueue, la restauration à epitempartir d'un pointeur poll_tableest 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 epitempour ce fichier. Il est epollextrêmement important de savoir où se trouve la tête de file d'attente pour le fichier surveillé. Sinon, epollil ne pourra pas désinscrire la file d'attente plus tard. Structureeppoll_entrycomprend également une file pwq->waitd' 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:



  1. Surveillez les événements se produisant avec un fichier spécifique surveillé.
  2. 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->nwaitreprésentant la longueur de la liste epi->pwqlist.



Et ici, j'ai une question. Pourquoi epollutiliser une liste chaînée pour stocker une structure eppoll_entrydans une epitemseule structure de fichier? Un epitemseul élément n'est-il pas eppoll_entrynécessaire?



Je ne peux vraiment pas répondre exactement à cette question. Pour autant que je sache, à moins que quelqu'un n'utilise des instances epolldans des boucles folles, la liste epi->pwqlistne contiendra qu'un seul élément struct eppoll_entry, etepi->nwaitpour la plupart des fichiers est susceptible d'être 1.



La bonne chose est que les ambiguïtés autour epi->pwqlistn'affectent en rien ce dont je vais parler ci-dessous. À savoir, nous parlerons de la façon dont Linux notifie les instances epolld'é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 epolls'ajoute wait_queue_tà la liste d'attente du ou des fichiers cibles wait_queue_head_t. Bien que le wait_queue_tplus 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_tassociée wait_queue_head_t. Dans cette fonctionepollpeut décider quoi faire avec le signal de reprise, mais epollil 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_wqstocké 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_tsans utiliser de rappel.



Quand exactement la reprise des travaux sk_wqdans 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 sockdé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_wqau 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 epolldans le rappel enregistré dans la file d'attente de reprise des processus de socket.



Avez-vous utilisé epoll?










All Articles