Limites de la fonction select
Le descripteur de fichier (fd_set) est fondamentalement un bitmap avec une capacité fixe de 1024, limitant ainsi le nombre maximal de connexions surveillables. De plus, l'utilisation de la même structure pour la surveillance et les événements prêts complique la manipulation. La fonction nécessite de multiples copies coûteuses entre l'espace utilisateur et l'espace noyau. Enfin, la méthode de polling pour trouver les descripteurs prêts devient inefficace avec un grand nombre de connexions mais peu d'événements.
Les fondements d'un serveur haute performance : epoll
epoll est un mécanisme d'E/S multiplexé performant, composé d'un ensemble de surveillance et d'une file d'événements prêts. Les données de surveillance sont stockées dans l'espace noyau sous forme d'objet fichier, évitant les copies inutiles. L'utilisation d'un arbre rouge-noir permet une recherche rapide par dichotomie, supportant un nombre massif de connexions. La séparation entre surveillance et événements prêts, stockés dans une file, optimise le traitement. Seul Linux supporte cette technologie.
Étapes d'utilisation d'epoll
- Créer un objet fichier avec
epoll_create. - Configurer la surveillance avec
epoll_ctl(ajout, modification, suppression). - Bloquer jusqu'à ce qu'un événement soit prêt via
epoll_wait. - Parcourir et traiter les événements de la file des événements prêts.
Structures clés
L'argument event de epoll_ctl décrit le type de surveillance (par exemple, EPOLLIN pour une lecture prête). Le champ data de la structure epoll_event est une union permettant de stocker des informations contextuelles, comme un descripteur de fichier.
Exemple : Serveur avec reconnexion
L'exemple suivant illustre un serveur TCP gérant des connexions et une entrée standard via epoll. Il inclut une reconnexion si le client se déconnecte.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "Usage: %s <address> <port>\n", argv[0]);
return EXIT_FAILURE;
}
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
if (tcp_socket == -1) {
perror("socket");
return EXIT_FAILURE;
}
int reuse = 1;
setsockopt(tcp_socket, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
inet_pton(AF_INET, argv[1], &server_addr.sin_addr);
if (bind(tcp_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind");
close(tcp_socket);
return EXIT_FAILURE;
}
if (listen(tcp_socket, 10) == -1) {
perror("listen");
close(tcp_socket);
return EXIT_FAILURE;
}
int client_fd = -1;
int epoll_instance = epoll_create1(0);
if (epoll_instance == -1) {
perror("epoll_create1");
close(tcp_socket);
return EXIT_FAILURE;
}
struct epoll_event input_event;
input_event.events = EPOLLIN;
input_event.data.fd = STDIN_FILENO;
epoll_ctl(epoll_instance, EPOLL_CTL_ADD, STDIN_FILENO, &input_event);
struct epoll_event server_event;
server_event.events = EPOLLIN;
server_event.data.fd = tcp_socket;
epoll_ctl(epoll_instance, EPOLL_CTL_ADD, tcp_socket, &server_event);
char buffer[4096];
struct epoll_event ready_events[3];
while (1) {
int num_ready = epoll_wait(epoll_instance, ready_events, 3, -1);
if (num_ready == -1) {
perror("epoll_wait");
break;
}
for (int i = 0; i < num_ready; i++) {
if (ready_events[i].data.fd == STDIN_FILENO) {
memset(buffer, 0, sizeof(buffer));
ssize_t bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer));
if (bytes_read == 0) {
goto shutdown;
}
send(client_fd, buffer, bytes_read, 0);
} else if (ready_events[i].data.fd == tcp_socket) {
client_fd = accept(tcp_socket, NULL, NULL);
if (client_fd == -1) {
perror("accept");
continue;
}
struct epoll_event new_client_event;
new_client_event.events = EPOLLIN;
new_client_event.data.fd = client_fd;
epoll_ctl(epoll_instance, EPOLL_CTL_ADD, client_fd, &new_client_event);
} else if (ready_events[i].data.fd == client_fd) {
memset(buffer, 0, sizeof(buffer));
ssize_t bytes_received = recv(client_fd, buffer, sizeof(buffer), 0);
if (bytes_received == 0) {
epoll_ctl(epoll_instance, EPOLL_CTL_DEL, client_fd, NULL);
close(client_fd);
client_fd = -1;
} else {
printf("Reçu : %s\n", buffer);
}
}
}
}
shutdown:
if (client_fd != -1) close(client_fd);
close(epoll_instance);
close(tcp_socket);
return EXIT_SUCCESS;
}
Lecture non bloquante
Une lecture non bloquante (O_NONBLOCK) renvoie immédiatement -1 si aucune donnée n'est disponible dans le tampon, évitant ainsi le blocage du processus. Elle est adaptée à des transferts de données continus mais non continus. En contraste, une lecture bloquante classique endort le thread jusqu'à l'arrivée des données, tandis qu'une lecture non bloquante synchronisée consomme des ressources CPU en vérifiant périodiquement le tampon.
Configuration d'un descripteur en mode non bloquant
Utiliser fcntl avec F_SETFL et l'indicateur O_NONBLOCK.
Modes de déclenchement d'epoll
Déclenchement de niveau (Level-Triggered - LT) : Un événement est signalé tant que le tampon contient des données. Cela garantit que toutes les données seront lues, mais peut entraîner des notifications répétées.
Déclenchement de bord (Edge-Triggered - ET) : Un événement est signalé uniquement lors de l'arrivée de nouvelles données. Cela assure une meilleure équité entre les clients, mais nécessite une lecture complète et non bloquante dans une boucle pour vider le tampon.
Pools de processus et de threads
Le modèle de pool consiste à pré-allouer un ensemble de ressources (processus ou threads) qui sont réutilisées pour traiter les tâches à mesure qu'elles arrvient, évitant ainsi la surcharge de création/destruction. Cela améliore les performances et la maintenabilité.
Conception d'un pool de processus pour un serveur de téléchargement
Un processus maître (master) gère les connexions TCP et distribue les tâches aux processus travailleurs (workers) via des pipes ou des sockets. Les workers exécutent une boucle d'événements (event loop) pour traiter les requêtes. Le maître utilise epoll pour surveiller à la fois le socket d'écoute et les canaux de communication avec les workers.
Transmission de descripteurs de fichiers entre processus
Pour partager un descripteur de socket (par exemple, une connexion client) entre un processus parent et enfant, on utilise sendmsg et recvmsg avec des messages de contrôle (SCM_RIGHTS). La fonction socketpair peut créer une paire de sockets connectés pour faciliter cette communication.
Protocole applicatif pour le transfert de fichiers
Pour résoudre le problème de limites de messages (sticking packets) dans le flux TCP, on encapsule les données dans une structure contenant la longueur du message et le contenu.
typedef struct {
uint32_t length; // Longueur du champ 'data'
char data[1000]; // Données à transmettre
} message_t;
Pour les fichiers volumineux, il est crucial d'assurer la réception complète de chaque message. La fonction recvn suivante lit de manière itérative jusqu'à obtenir le nombre d'octets spécifié.
ssize_t recvn(int fd, void *buf, size_t len) {
size_t total_received = 0;
char *ptr = (char *)buf;
while (total_received < len) {
ssize_t ret = recv(fd, ptr + total_received, len - total_received, 0);
if (ret <= 0) {
return ret;
}
total_received += ret;
}
return total_received;
}
Vérification d'intégrité et progression
Des algorithmes de hachage (MD5, SHA, CRC) permettent de vérifier l'intégrité des fichiers transférés. L'affichage d'une barre de progression peut être implémenté en transmettant d'abord la taille totale du fichier au client via la structure de message, puis en calculant le pourcentage reçu.
Zéro copie (Zero-Copy)
Les techniques de zéro copie réduisent les copies de données entre les espaces noyau et utilisateur.
mmap (Memory-Mapped Files)
Cette méthode mappe un fichier directement dans l'espace mémoire du processus. Pour l'envoi, on peut mapper le fichier et transmettre directement les données mappées via send, évitant une copie intermédiaire. Toutefois, son efficacité dépend de la taille des blocs envoyés et de l'implémentation du noyau.