Application GNU/Linux d'illustration

Ce chapitre est celui ou tout s’imbrique. Nous allons spécifier et implanter un programme GNU/Linux complet qui fait appel à la plupart des techniques décrites dans ce livre. Ce programme fournit des informations sur le système sur lequel il s’exécute par le biais d’une interface Web.

L’application est une application pratique de certaines des méthodes que nous avons décrites dans le cadre de la programmation GNU/Linux et illustrées dans des programmes plus courts. Celui-ci se rapproche plus du “monde réel”, contrairement à la plupart des listings des chapitres précédents. Il peut vous servir de tremplin pour écrire vos propres logiciels GNU/Linux.

Présentation

Ce programme fait partie d’un système de surveillance pour un système GNU/Linux en cours d’exécution. Voici ses fonctionnalités:

  • Il incorpore un serveur Web minimal. Les clients locaux ou distants accèdent aux informations en demandant des pages au serveur via le protocole HTTP;
  • Il n’utilise pas de pages HTML statiques mais les génère à la volée par le biais de modules, chacun d’eux produisant une page informant sur un aspect de l’état du système;
  • Les modules ne sont pas liés statiquement avec l’exécutable serveur. Ils sont chargés dynamiquement à partir de bibliothèques partagées. Des modules peuvent être ajoutés, supprimés ou remplacés alors que le serveur est en cours d’exécution;
  • Chaque connexion est traitée dans un processus fils. Cela permet au serveur de rester réactif même lorsque certaines requêtes nécessitent un temps de traitement important et cela protège le serveur contre d’éventuels problèmes dans les modules;
  • Le serveur ne nécessite pas les privilèges d’administrateur pour s’exécuter (tant qu’il n’utilise pas un port privilégié). Cependant, cela limite les informations pouvant être collectées.

Nous présentons quatre modules illustrant la façon dont ils peuvent être écrits. Ils exploitent plus en profondeur certaines techniques de collecte d’informations présentées précédemment dans ce livre. Le module time utilise l’appel système gettimeofday, le module issue exploite les E/S de bas niveau et l’appel système sendfile, diskfree illustre l’utilisation de fork, exec et dup2 en lançant une commande dans un processus fils, enfin, le module processes utilise le système de fichiers /proc et divers appels système.

Limitations

Ce programme dispose d’un nombre important de fonctionnalités que vous pouvez trouver dans une application réelle, comme l’analyse de la ligne de commande et la vérification d’erreur. Mais, dans le même temps, nous avons simplifié les choses afin d’améliorer la lisibilité et de nous concentrer sur les sujets spécifiques à GNU/Linux traités tout au long de ce livre. Gardez en tête les limitations suivantes lors de votre lecture du code :

  • Nous n’essayons pas d’implanter le protocole HTTP dans son intégralité. Au contraire, nous en implémentons juste assez pour que le serveur puisse interagir avec des clients Web. Un programme réel fournirait une implantation plus aboutie ou s’interfacerait avec l’un des nombreux serveurs Web1) disponibles;
  • De même, nous ne visons pas une compatibilité complète avec les spécifications HTML (consultez http://www.w3.org/MarkUp pour plus d’informations). Nous générons un HTML simple pris en charge par les navigateurs les plus répandus;
  • Le serveur n’est pas conçu pour maximiser les performances ou minimiser l’utilisation des ressources. En particulier, nous avons intentionnellement omis une partie du code de configuration du réseau auquel vous pourriez vous attendre dans un serveur Web. Ce sujet sort du cadre de ce livre. Consultez une des nombreuses excellentes références sur le développement d’application réseau, comme Unix Network Programming, Volume~1: Networking APIx – Sockets and XTI de W. Richard Stevens (Prentice Hall, 1997) pour plus d’informations.
  • Nous ne tentons pas de réguler l’utilisation des ressources (nombre de processus, consommation mémoire, etc.) par le serveur ou ses modules. Beaucoup d’implantations de serveurs Web répondent aux connexions en utilisant un nombre fixe de processus plutôt que d’en créer un nouveau à chaque connexion;
  • Le serveur charge la bibliothèque partagée correspondant à un module à chaque fois qu’une requête y faisant appel est reçue puis le décharge immédiatement une fois qu’elle a été traitée. Une implantation plus efficace utiliserait certainement un cache pour les modules chargés.
HTTP
L’Hypertext Transport Protocol (HTTP) est utilisé pour la communication entre clients et serveurs Web. Le client se connecte au serveur via un port connu (généralement le port 80, mais il peut s’agir de n’importe quel port). Les requêtes et les en-têtes HTTP sont au format texte.
Une fois connecté, le client envoie une requête au serveur. Une requête classique est GET /page HTTP/1.0. La méthode GET indique que le client demande au serveur l’envoi d’une page. Le second élément est le chemin de la page sur le serveur et le troisième est constitué du protocole et de sa version. Les lignes suivantes sont constituées d’en-têtes, dont le format est similaire à celui des en-têtes mail, qui contiennent des informations supplémentaires sur le client. L’en-tête se termine par une ligne vide.
Le serveur renvoie une réponse indiquant le résultat du traitement de la requête. Une réponse classique est HTTP/1.0 200 OK. Le premier élément est la version du protocole, les deux suivants indiquant le résultat; dans ce cas, 200 signifie que la requête a été correctement traitée. Les lignes suivantes contiennent des en-têtes, là encore similaires aux en-têtes mail. L’en-tête se termine par une ligne blanche. Le serveur peut envoyer des données quelconques pour satisfaire la requête.
Typiquement, le serveur répond à la demande d’une page en envoyant sa source HTML. Dans ce cas, les en-têtes de réponse incluront Content-type: text/html indiquant que le résultat est au format HTML. La source HTML du document suit immédiatement l’en-tête.
Consultez la spécification HTTP disponible sur http://www.w3.org/Protocols/ pour plus d’informations.

Implantation

Excepté lorsqu’ils sont très concis, les programmes écrits en C demandent une organisation soigneuse afin d’assurer la modularité et la maintenabilité du code. Ce programme est divisé en quatre fichiers sources principaux.

Chaque fichier exporte les fonctions ou les variables auxquelles peuvent accéder les autres parties du programme. Pour des raisons de simplicité, toutes les fonctions et variables exportées sont définies dans un unique fichier d’en-tête, server.h (cf. Listing !!serverh!!), qui est inclus par les autres fichiers. Les fonctions destinées à n’être utilisées que dans une seule unité de compilation sont déclarées comme static et ne sont donc pas mentionnées dans server.h.

Listing (server.h) Déclarations des Fonctions et Variables

#ifndef SERVER_H
#define SERVER_H
 
#include <netinet/in.h>
#include <sys/types.h>
 
/*** Symboles définis dans common.c.   ************************************/
 
/* Nom du programme. */
extern const char* program_name;
 
/* Mode verbeux si différent de zéro. */
extern int verbose;
 
/* Identique à malloc, sauf que le programme se termine en cas d'échec. */
extern void* xmalloc (size_t size);
 
/* Identique à realloc, sauf que le programme se termine en cas d'échec. */
extern void* xrealloc (void* ptr, size_t size);
 
/* Identique à strdup, sauf que le programme se termine en cas d'échec. */
extern char* xstrdup (const char* s);
 
/* Affiche un message d'erreur suite à l'appel de OPERATION, utilise
   la valeur de errno et termine le programme. */
extern void system_error (const char* operation);
 
/* Affiche un message d'erreur lors d'un échec dont la cause est CAUSE, 
   le MESSAGE descriptif est affiché et le programme terminé. */
extern void error (const char* cause, const char* message);
 
/* Renvoie le répertoire contenant l'exécutable du programme.
   La valeur de retour est un tampon mémoire que l'appelant doit
   libérer. Cette fonction appelle abort en cas d'échec. */
extern char* get_self_executable_directory ();
 
 
/*** Symboles définis dans module.c.   **************************************/
 
/* Instance de module chargé. */
struct server_module {
   /* Référence sur la bibliothèque partagée correspondant au module. */
   void* handle;
   /* Nom du module. */
   const char* name;
   /* Fonction générant les résultats au format HTML pour le module. */
   void (* generate_function) (int);
};
 
/* Répertoire à partir duquel sont chargés les modules. */
extern char* module_dir;
 
/* Tente de charger un module dont le nom est MODULE_PATH. Si un module
   existe à cet emplacement, charge le module et renvoie une structure
   server_module le représentant. Sinon, renvoie NULL. */
extern struct server_module* module_open (const char* module_path);
 
/* Décharge un module et libère l'objet MODULE. */
extern void module_close (struct server_module* module);
 
 
/*** Symboles définis dans server.c.  ************************************/
 
/* Lance le serveur sur l'adresse LOCAL_ADDRESS et le port PORT. */
extern void server_run (struct in_addr local_address, uint16_t port);
 
#endif  /* SERVER_H */

Fonctions génériques

common.c (cf. Listing !!commonc!! contient des fonctions génériques utilisées dans tous le programme.

Listing (common.c) Fonctions Génériques

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
 
#include "server.h"
 
const char* program_name;
 
int verbose;
 
void* xmalloc (size_t size)
{
  void* ptr = malloc (size);
  /* Termine le programme si l'allocation échoue. */
  if (ptr == NULL)
    abort ();
  else
    return ptr;
}
 
void* xrealloc (void* ptr, size_t size)
{
  ptr = realloc (ptr, size);
  /* Termine le programme si l'allocation échoue. */
  if (ptr == NULL)
    abort ();
  else
    return ptr;
}
 
char* xstrdup (const char* s)
{
  char* copy = strdup (s);
  /* Termine le programme si l'allocation échoue. */
  if (copy == NULL)
    abort ();
  else
    return copy;
}
 
void system_error (const char* operation)
{
  /* Génère le message d'erreur correspondant à errno. */
  error (operation, strerror (errno));
}
 
void error (const char* cause, const char* message)
{
  /* Affiche un message d'erreur sur stderr. */
  fprintf (stderr, "%s: error: (%s) %s\n", program_name, cause, message);
  /* Termine le programme. */
  exit (1);
}
 
char* get_self_executable_directory ()
{
  int rval;
  char link_target[1024];
  char* last_slash;
  size_t result_length;
  char* result;
 
  /* Lit la cible du lien symbolique /proc/self/exe. */
  rval = readlink ("/proc/self/exe", link_target, sizeof (link_target));
  if (rval == -1)
    /* L'appel à readlink a échoué, terminé. */
    abort ();
  else
    /* Ajoute un octet nul à la fin de la chaîne. */
    link_target[rval] = '\0';
  /* Nous voulons supprimer le nom du fichier exécutable afin d'obtenir
     le nom du répertoire qui le contient. On recherche le slash le plus à droite. */
  last_slash = strrchr (link_target, '/');
  if (last_slash == NULL || last_slash == link_target)
    /* Quelque chose d'inattendu s'est produit. */
    abort ();
  /* Alloue un tampon pour accueillir le chemin obtenu. */
  result_length = last_slash - link_target;
  result = (char*) xmalloc (result_length + 1);
  /* Copie le résultat. */
  strncpy (result, link_target, result_length);
  result[result_length] = '\0';
  return result;
}

Vous pouvez utiliser ces fonctions dans d’autres programmes; le contenu de ce fichier peut être inclus dans une bibliothèque constituant une base de code pour un certain nombre de projets:

  • xmalloc, xrealloc et xstrdup sont des versions avec vérification d’erreur des fonctions malloc, realloc et strdup, respectivement. Contrairement aux fonctions standard qui renvoient un pointeur NULL lorsque l’allocation échoue, ces fonctions terminent immédiatement le programme lorsqu’il n’y a pas assez de mémoire disponible.
    La détection précoce de l’échec des allocations mémoire est une bonne idée. Si ce n’est pas fait, les allocations ayant échoué introduisent des pointeurs NULL à des endroits inattendus. Comme les échecs d’allocation ne sont pas simples à reproduire, il peut être difficile de réparer de tels problèmes. Les échecs d’allocation sont généralement catastrophiques, aussi, l’arrêt du programme est souvent une réaction appropriée;
  • La fonction error sert à signaler une erreur fatale. Elle affiche un message sur stderr et termine le programme. Pour les erreurs causées par des appels système ou des fonctions de bibliothèques, system_error génère une partie du message d’erreur à partir de la valeur de errno (reportez-vous à la Section !!codeserreurAS!!, !! sec:codeserreurAS !!, du Chapitre !!logicielsQualite!!, !! chap:logicielsQualite !!);
  • get_self_executable_directory détermine le répertoire contenant le fichier exécutable du processus courant. Ce chemin peut être utilisé pour localiser d’autres composants du programmes installés au même endroit. Cette fonction utilise le lien symbolique /proc/self/exe du système de fichiers /proc (cf. Section !!procself!!, nameref{sec:procself} du Chapitre !!SFproc!!, !! chap:SFproc !!).

common.c définit également deux variables globales très utiles:

  • La valeur de program_name est le nom du programme en cours d’exécution, déterminé à partir de la liste d’arguments (cf. Section !!listeArgs!!, !! sec:listeArgs !!, dans le Chapitre !!logicielsQualite!!). Lorsque le programme est invoqué à partir d’un shell, il s’agit du chemin et du nom de programme saisis par l’utilisateur;
  • La variable verbose est différente de zéro si le programme s’exécute en mode verbeux. Dans ce cas, diverses portions du programme affichent des messages sur stdout.

Chargement de modules serveur

module.c (cf. Listing !!modulec!!) fournit l’implémentation des modules serveurs pouvant être chargés dynamiquement. Un module serveur chargé est représenté par une instance de struct server_module dont la définition se trouve dans server.h.

Listing (module.c) Chargement et Déchargement de Module Serveur

#include <dlfcn.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
 
#include "server.h"
 
char* module_dir;
 
struct server_module* module_open (const char* module_name)
{
  char* module_path;
  void* handle;
  void (* module_generate) (int);
  struct server_module* module;
 
  /* Détermine le chemin absolu de la bibliothèque partagée correspondant
     au module que nous essayons de charger. */
  module_path =
    (char*) xmalloc (strlen (module_dir) + strlen (module_name) + 2);
  sprintf (module_path, "%s/%s", module_dir, module_name);
 
  /* Tente d'ouvrir la bibliothèque partagée MODULE_PATH. */
  handle = dlopen (module_path, RTLD_NOW);
  free (module_path);
  if (handle == NULL) {
    /* Échec ; le chemin n'existe pas ou il ne s'agit pas
       d'une bibliothèque partagée. */
    return NULL;
  }
 
  /* Charge le symbole module_generate depuis la bibliothèque partagée. */
  module_generate = (void (*) (int)) dlsym (handle, "module_generate");
  /* S'assure que le symbole existe. */
  if (module_generate == NULL) {
  /* Le symbole n'a pas été trouvé. Bien que l'on soit en présence d'une
     bibliothèque partagée, il ne s'agit probablement pas d'un module
     serveur. Ferme et indique l'échec de l'opération. */
    dlclose (handle);
    return NULL;
  }
  /* Alloue et initialise un objet server_module. */
  module = (struct server_module*) xmalloc (sizeof (struct server_module));
  module->handle = handle;
  module->name = xstrdup (module_name);
  module->generate_function = module_generate;
  /* Le renvoie, indiquant que tout s'est déroulé correctement. */
  return module;
}
 
void module_close (struct server_module* module)
{
  /* Ferme la bibliothèque partagée. */
  dlclose (module->handle);
  /* Libère la mémoire allouée pour le nom du module. */
  free ((char*) module->name);
  /* Libère la mémoire allouée pour le module. */
  free (module);
}

Chaque module est un fichier de bibliothèque partagée (reportez-vous à la Section !!bibliopart!!, !! sec:bibliopart !! du Chapitre !!logicielsQualite!!) et doit définir et exporter une fonction appelée module_generate. Cette fonction génère une page HTML et l’écrit vers un descripteur de fichier correspondant à une socket client.

module.c contient deux fonctions:

  • module_open tente de charger un module serveur dont le nom est connu. Le nom se termine normalement par l’extension .so car les modules sont implantés sous forme de bibliothèques partagées. La fonction ouvre la bibliothèque partagée avec dlopen et recherche un symbole nommé module_generate à partir de cette bibliothèque en utilisant dlsym (consultez la Section !!chardechardyn!!, !! sec:chardechardyn !!, du Chapitre !!logicielsQualite!!). Si la bibliothèque ne peut pas être ouverte ou si module_generate n’est pas exporté par celle-ci, l’appel échoue et module_open renvoie un pointeur NULL. Si tout ce passe bien, elle alloue et renvoie un objet module;
  • module_close ferme la bibliothèque partagée correspondant au module serveur et libère l’objet struct server_module.

module.c définit également une variable globale module_dir. Il s’agit du chemin du répertoire dans lequel module_open recherche les bibliothèques partagées correspondant aux modules du serveur.

Le serveur

server.c (cf. Listing !!serverc!!) est l’implantation du serveur HTTP minimal.

Listing (server.c) Implantation du Serveur

#include <arpa/inet.h>
#include <assert.h>
#include <errno.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
 
#include "server.h"
 
/* En-tête et réponse HTTP dans le cas d'une requête traitée avec succès. */
 
static char* ok_response =
  "HTTP/1.0 200 OK\n"
  "Content-type: text/html\n"
  "\n";
 
/* Réponse, en-tête et corps de la réponse HTTP indiquant que nous n'avons
   pas compris la requête. */
 
static char* bad_request_response =
  "HTTP/1.0 400 Bad Request\n"
  "Content-type: text/html\n"
  "\n"
  "<html>\n"
  " <body>\n"
  " <h1>Bad Request</h1>\n"
  " <p>This server did not understand your request.</p>\n"
  " </body>\n"
  "</html>\n";
 
/* Réponse, en-tête et modèle de corps indiquant que le document
   demandé n'a pas été trouvé. */
 
static char* not_found_response_template =
  "HTTP/1.0 404 Not Found\n"
  "Content-type: text/html\n"
  "\n"
  "<html>\n"
  " <body>\n"
  " <h1>Not Found</h1>\n"
  " <p>The requested URL %s was not found on this server.</p>\n"
  " </body>\n"
  "</html>\n";
 
/* Réponse, en-tête et modèle de corps indiquant que la méthode
   n'a pas été comprise. */
 
static char* bad_method_response_template =
  "HTTP/1.0 501 Method Not Implemented\n"
  "Content-type: text/html\n"
  "\n"
  "<html>\n"
  " <body>\n"
  " <h1>Method Not Implemented</h1>\n"
  " <p>The method %s is not implemented by this server.</p>\n"
  " </body>\n"
  "</html>\n";
 
/* Gestionnaire de SIGCHLD, libère les ressources occupées par les
   processus fils qui se sont terminés. */
 
static void clean_up_child_process (int signal_number)
{
  int status;
  wait (&status);
}
 
/* Traite une requête HTTP "GET" pour PAGE et envoie les résultats
   vers le descripteur de fichier CONNECTION_FD. */
static void handle_get (int connection_fd, const char* page)
{
  struct server_module* module = NULL;
 
  /* S'assure que la page demandée débute avec un slash et n'en
     contient pas d'autre - nous ne prenons pas en charge les
     sous-répertoires. */
  if (*page == '/' && strchr (page + 1, '/') == NULL) {
    char module_file_name[64];
 
    /* Le nom de la page a l'air correct. Nous construisons le nom du
       module en ajoutant ".so" au nom de la page. */
    snprintf (module_file_name, sizeof (module_file_name),
              "%s.so", page + 1);
    /* Tente d'ouvrir le module. */
    module = module_open (module_file_name);
  }
 
  if (module == NULL) {
    /* Soit la page demandée était malformée, soit nous n'avons pas pu
       ouvrir le module demandé. Quoi qu'il en soit, nous renvoyons
       une réponse HTTP 404, Introuvable. */
    char response[1024];
 
    /* Génère le message de réponse. */
    snprintf (response, sizeof (response), not_found_response_template, page);
    /* L'envoie au client. */
    write (connection_fd, response, strlen (response));
  }
  else {
    /* Le module demandé a été correctement chargé.  */
 
    /* Envoie la réponse HTTP indiquant que tout s'est bien passé
       ainsi que l'en-tête HTTP pour une page HTML. */
    write (connection_fd, ok_response, strlen (ok_response));
    /* Invoque le module, il générera la sortie HTML et l'enverra
       au descripteur de fichier client. */
    (*module->generate_function) (connection_fd);
    /* Nous en avons terminé avec le module. */
    module_close (module);
  }
}
 
/* Prend en charge une connexion client sur le descripteur de fichier CONNECTION_FD. */
 
static void handle_connection (int connection_fd)
{
  char buffer[256];
  ssize_t bytes_read;
 
  /* Lis les données envoyées par le client. */
  bytes_read = read (connection_fd, buffer, sizeof (buffer) - 1);
  if (bytes_read > 0) {
    char method[sizeof (buffer)];
    char url[sizeof (buffer)];
    char protocol[sizeof (buffer)];
 
    /* Les données ont été lues correctement. Ajoute un octet nul à la
       fin du tampon pour pouvoir l'utiliser avec les fonctions de manipulation
       de chaîne classiques. */
    buffer[bytes_read] = '\0';
    /* La première ligne qu'envoie le client correspond à la requête HTTP composée
       d'une méthode, de la page demandée et de la version du protocole. */
    sscanf (buffer, "%s %s %s", method, url, protocol);
    /* Le client peut envoyer divers en-têtes d'information à la suite de la requête.
       Pour notre implantation du protocole HTTP, nous n'en tenons pas compte.
       Cependant, nous devons lire toutes les données envoyées par le client.
       Nous continuons donc à lire les données jusqu'à la fin de l'en-tête
       qui se termine par une ligne blanche. Le protocole HTTP définit
       la combinaison CR/LF comme délimiteur de ligne. */
    while (strstr (buffer, "\r\n\r\n") == NULL)
       bytes_read = read (connection_fd, buffer, sizeof (buffer));
    /* S'assure que la dernière lecture n'a pas échoué. Si c'est le cas,
       il y a un problème avec la connexion, abandonne. */
    if (bytes_read == -1) {
       close (connection_fd);
       return;
    }
    /* Vérifie le champ protocole. Nous ne comprenons que les versions 1.0
       et 1.1. */
    if (strcmp (protocol, "HTTP/1.0") && strcmp (protocol, "HTTP/1.1")) {
       /* Nous ne comprenons pas ce protocole. Renvoie une indication
          de requête incorrecte. */
       write (connection_fd, bad_request_response,
              sizeof (bad_request_response));
    }
    else if (strcmp (method, "GET")) {
       /* Ce serveur n'implante que la méthode GET. Le client en a spécifié une autre.
          Indique un échec. */
       char response[1024];
 
       snprintf (response, sizeof (response),
                 bad_method_response_template, method);
       write (connection_fd, response, strlen (response));
    }
    else
       /* La requête est valide, nous la traitons.  */
       handle_get (connection_fd, url);
  }
  else if (bytes_read == 0)
    /* Le client a fermé la connexion avant d'envoyer des données.
       Rien à faire. */
    ;
  else
    /* L'appel à read a échoué. */
    system_error ("read");
}
 
void server_run (struct in_addr local_address, uint16_t port)
{
  struct sockaddr_in socket_address;
  int rval;
  struct sigaction sigchld_action;
  int server_socket;
  /* Définit un gestionnaire pour SIGCHLD qui libère les ressources des
     processus fils terminés. */
  memset (&sigchld_action, 0, sizeof (sigchld_action));
  sigchld_action.sa_handler = &clean_up_child_process;
  sigaction (SIGCHLD, &sigchld_action, NULL);
 
  /* Crée une socket TCP. */
  server_socket = socket (PF_INET, SOCK_STREAM, 0);
  if (server_socket == -1)
    system_error ("socket");
  /* Construit une structure socket address pour recevoir l'adresse
     locale sur laquelle nous voulons attendre les connexions. */
  memset (&socket_address, 0, sizeof (socket_address));
  socket_address.sin_family = AF_INET;
  socket_address.sin_port = port;
  socket_address.sin_addr = local_address;
  /* Lie la socket à cette adresse. */
  rval = bind (server_socket, &socket_address, sizeof (socket_address));
  if (rval != 0)
    system_error ("bind");
  /* Demande à la socket d'accepter les connexions. */
  rval = listen (server_socket, 10);
  if (rval != 0)
    system_error ("listen");
 
  if (verbose) {
    /* En mode verbeux, affiche l'adresse locale et le numéro du port
       sur lesquels nous écoutons. */
    socklen_t address_length;
 
    /* Détermine l'adresse locale du socket. */
    address_length = sizeof (socket_address);
    rval = getsockname (server_socket, &socket_address, &address_length);
    assert (rval == 0);
    /* Affiche un message. Le numéro du port doit être converti à partir de
       l'ordre des octets réseau (big endian) vers l'ordre des octets de l'hôte. */
    printf ("server listening on %s:%d\n",
            inet_ntoa (socket_address.sin_addr),
            (int) ntohs (socket_address.sin_port));
  }
 
  /* Boucle indéfiniment, en attente de connexions.  */
  while (1) {
    struct sockaddr_in remote_address;
    socklen_t address_length;
    int connection;
    pid_t child_pid;
 
    /* Accepte une connexion. Cet appel est bloquant jusqu'à ce qu'une
       connexion arrive. */
    address_length = sizeof (remote_address);
    connection = accept (server_socket, &remote_address, &address_length);
    if (connection == -1) {
      /* L'appel à accept a échoué. */
      if (errno == EINTR)
         /* L'appel a été interrompu par un signal. Recommence. */
         continue;
      else
         /* Quelque chose est arrivé. */
         system_error ("accept");
    }
 
    /* Nous avons reçu une connexion. Affiche un message si nous sommes
       en mode verbeux. */
    if (verbose) {
      socklen_t address_length;
 
      /* Récupère l'addresse distante de la connexion. */
      address_length = sizeof (socket_address);
      rval = getpeername (connection, &socket_address, &address_length);
      assert (rval == 0);
      /* Affiche un message. */
      printf ("connection accepted from %s\n",
               inet_ntoa (socket_address.sin_addr));
    }
 
    /* Crée un processus fils pour prendre en charge la connexion. */
    child_pid = fork ();
    if (child_pid == 0) {
      /* Nous sommes dans le processus fils. Il n'a pas besoin de stdin ou stdout,
          nous les fermons donc. */
      close (STDIN_FILENO);
      close (STDOUT_FILENO);
      /* De même, la socket d'écoute est inutile pour le processus fils. */
      close (server_socket);
      /* Traite une requête pour la connexion. Nous avons notre propre copie
         du descripteur de socket. */
      handle_connection (connection);
      /* Terminé, nous fermons la socket de connexion et terminons le
         processus fils. */
      close (connection);
      exit (0);
    }
    else if (child_pid > 0) {
      /* Nous sommes dans le processus parent. Le processus fils gère la connexion,
         nous n'avons donc pas besoin de notre copie du descripteur de socket. Nous la
         fermons puis nous reprenons la boucle pour accepter une autre connexion. */
      close (connection);
    }
    else
      /* L'appel à fork a échoué. */
      system_error ("fork");
  }
}

Voici les fonctions définies dans server.c:

  • server_run est le point d’entrée du serveur. Cette fonction démarre le serveur, accepte des connexions et ne se termine pas tant qu’une erreur sérieuse ne survient pas. Le serveur utilise une socket serveur TCP (consultez la Section !!serveurs!!, !! sec:serveurs !!, du Chapitre !!IPC!!, !! chap:IPC !!);
    Le premier argument de server_run correspond à l’adresse locale sur laquelle les connexions sont acceptées. Une machine sous GNU/Linux peut avoir plusieurs adresses réseau, chacune pouvant être liée à une interface réseau différente2). Pour obliger le serveur à n’accepter les connexions qu’à partir d’une certaine interface, spécifiez son adresse réseau. Passez l’adresse locale INADDR_ANY pour accepter les connexions depuis n’importe quelle adresse locale.
    Le second argument de server_run est le numéro de port sur lequel accepter les connexions. Si le port est déjà en cours d’utilisation ou s’il s’agit d’un port privilégié et que le serveur ne s’exécute pas avec les privilèges superutilisateur, le démarrage échoue. La valeur spéciale 0 demande à Linux de sélectionner automatiquement un port inutilisé. Consultez la page de manuel inet pour plus d’informations sur les adresses Internet et les numéros de ports.
    Le serveur traite chaque connexion dans un processus fils créé par le biais de fork (cf. Section !!utiliserforkexec!!, !! sec:utiliserforkexec !! dans le Chapitre !!processus!!, !! chap:processus !!). Le processus principal (parent) continue à accepter des connexions tandis que celles déjà acceptées sont traitées. Le processus fils appelle handle_connection puis ferme la socket et se termine.
  • handle_connection traite une connexion client en utilisant le descripteur de socket qui lui est passé en argument. Cette fonction lit les données à partir de la socket et tente de les interpréter en tant que demande de page HTTP.
    Le serveur ne prend en charge que les versions 1.0 et 1.1 du protocole HTTP. Lorsque le protocole ou la version sont incorrects, il répond en envoyant le code HTTP 400 et le message bad_request_response. Le serveur ne comprend que la méthode HTTP GET. Si le client utilise une autre méthode, le code réponse HTTP 500 lui est renvoyé ainsi que le message bad_method_response_template;
  • Si le client envoie une requête GET valide, handle_connection appelle handle_get pour la traiter. Cette fonction tente de charger un module serveur dont le nom est dérivé de la page demandée. Par exemple, si le client demande une page information, elle tente de charger le module information.so. Si le module ne peut pas être chargé, handle_get renvoie au client le code réponse HTTP 404 et le message not_found_response_template.
    Si le client demande une page à laquelle correspond un module, handle_get renvoie un code réponse 200, signifiant que la requête a été correctement traitée, puis invoque la fonction module_generate du module. Celle-ci génère le HTML de la page Web et l’envoie au client Web.
  • server_run installe clean_up_child_process en tant que gestionnaire de signal pour SIGCHLD. Cette fonction se contente de libérer les ressources des processus fils terminés (cf. Section !!libererasync!!, !! sec:libererasync !! du Chapitre !!processus!!).

Le programme principal

main.c (cf. Listing !!servermainc!!) définit la fonction main du programme serveur. Il lui incombe d’analyser les options de la ligne de commande, de détecter et signaler les erreurs et de configurer et lancer le serveur.

Listing (main.c) Programme principal du serveur et analyse de la ligne de commande

#include <assert.h>
#include <getopt.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
 
#include "server.h"
 
/* Description des options longues pour getopt_long. */
 
static const struct option long_options[] = {
   { "address",         1, NULL, 'a' },
   { "help",            0, NULL, 'h' },
   { "module-dir",      1, NULL, 'm' },
   { "port",            1, NULL, 'p' },
   { "verbose",         0, NULL, 'v' },
};
 
/* Description des options courtes pour getopt_long. */
 
static const char* const short_options = "a:hm:p:v";
 
/* Texte récapitulatif d'usage. */
 
static const char* const usage_template =
  "Usage: %s [ options ]\n"
  " -a, --address ADDR   Bind to local address (by default, bind\n"
  "                        to all local addresses).\n"
  " -h, --help           Print this information.\n"
  " -m, --module-dir DIR Load modules from specified directory\n"
  "                        (by default, use executable directory).\n"
  " -p, --port PORT      Bind to specified port.\n"
  " -v, --verbose        Print verbose messages.\n";
 
/* Affiche des informations sur l'utilisation. Si IS_ERROR est différent de zéro,
   écrit sur stderr et utilise un code de sortie d'erreur, sinon utilise un code
   de sortie normale. Ne se termine jamais. */
 
static void print_usage (int is_error)
{
  fprintf (is_error ? stderr : stdout, usage_template, program_name);
  exit (is_error ? 1 : 0);
}
 
int main (int argc, char* const argv[])
{
  struct in_addr local_address;
  uint16_t port;
  int next_option;
 
  /* Stocke le nom du programme qui sera utilisé dans les messages d'erreur. */
  program_name = argv[0];
 
  /* Définit les valeurs par défaut des options. Le serveur écoute sur toutes les
     adresses et récupère automatiquement un numéro de port inutilisé. */
  local_address.s_addr = INADDR_ANY;
  port = 0;
  /* N'affiche pas les messages verbeux. */
  verbose = 0;
  /* Charge les modules depuis le répertoire contenant l'exécutable. */
  module_dir = get_self_executable_directory ();
  assert (module_dir != NULL);
 
  /* Analyse les options. */
  do {
    next_option =
       getopt_long (argc, argv, short_options, long_options, NULL);
    switch (next_option) {
    case 'a':
       /* L'utilisateur a spécifié -a ou --address. */
       {
         struct hostent* local_host_name;
 
         /* Recherche le nom d'hôte demandé par l'utilisateur. */
         local_host_name = gethostbyname (optarg);
         if (local_host_name == NULL || local_host_name->h_length == 0)
           /* Impossible de le résoudre. */
           error (optarg, "invalid host name");
         else
           /* Nom d'hôte OK, nous l'utilisons. */
           local_address.s_addr =
              *((int*) (local_host_name->h_addr_list[0]));
       }
       break;
 
     case 'h':
       /* L'utilisateur a passé -h ou --help.   */
       print_usage (0);
 
     case 'm':
       /* L'utilisater a passé -m ou --module-dir.   */
       {
         struct stat dir_info;
 
         /* Vérifie que le répertoire existe... */
         if (access (optarg, F_OK) != 0)
           error (optarg, "module directory does not exist");
         /* ... qu'il est accessible... */
         if (access (optarg, R_OK | X_OK) != 0)
           error (optarg, "module directory is not accessible");
         /* ... et qu'il s'agit d'un répertoire. */
         if (stat (optarg, &dir_info) != 0 || !S_ISDIR (dir_info.st_mode))
           error (optarg, "not a directory");
         /* Tout semble correct, nous l'utilisons. */
         module_dir = strdup (optarg);
       }
       break;
 
     case 'p':
       /* L'utilisateur a passé -p ou --port.   */
       {
         long value;
         char* end;
 
         value = strtol (optarg, &end, 10);
         if (*end != '\0')
           /* Présence de caractères non numériques dans le numéro de port. */
           print_usage (1);
         /* Le numéro de port doit être converti à l'ordre des octets du réseau
            (big endian). */
         port = (uint16_t) htons (value);
       }
       break;
 
     case 'v':
       /* L'utilisateur a spécifié -v ou --verbose. */
       verbose = 1;
       break;
 
    case '?':
      /* L'utilisateur a passé une option inconnue. */
      print_usage (1);
 
    case -1:
      /* Nous en avons terminé avec les options. */
      break;
    default:
      abort ();
    }
  } while (next_option != -1);
 
  /* Ce programme ne prend aucun argument supplémentaire. Affiche une erreur
     si l'utilisateur en a passé. */
  if (optind != argc)
    print_usage (1);
 
  /* Affiche le répertoire des modules, si nous sommes en mode verbeux. */
  if (verbose)
    printf ("modules will be loaded from %s\n", module_dir);
  /* Lance le serveur. */
  server_run (local_address, port);
  return 0;
}

main.c définit les fonctions suivantes:

  • main invoque getopt_long (cf. la Section !!getoptlong!!, !! sec:getoptlong !! du Chapitre !!logicielsQualite!!) afin d’analyser les options de ligne de commande. Ces options sont disponibles au format long ou court, comme défini par le tableau long_options pour les premières et la chaîne short_options pour les secondes.
    La valeur par défaut pour le port d’écoute est 0 et l’adresse locale est INADDR_ANY. Ces valeurs peuvent être redéfinies au moyen des options –port (-p) et –address (-a), respectivement. Si l’utilisateur spécifie une adresse, main appelle la fonction gethostbyname pour la convertir en adresse Internet numérique3).
    La valeur par défaut pour le répertoire à partir duquel charger les modules du serveur est le répertoire qui contient l’exécutable du serveur, déterminé par get_self_executable_directory. L’utilisateur peut modifier cette valeur grâce à l’option –module-dir (-m); main s’assure que le répertoire indiqué est bien accessible.
    Par défaut, les messages verbeux ne sont pas affichés. L’utilisateur peut les activer au moyen de l’option –verbose (-v);
  • Si l’utilisateur spécifie l’option –help (-h) ou passe des options invalides, main invoque print_usage qui affiche des explications sur l’utilisation du programme et le termine.

Modules

Nous vous proposons quatre modules pour présenter le type de fonctionnalités que vous pourriez implanter en utilisant ce serveur. La création de votre propre module serveur consiste simplement à définir une fonction module_generate pour renvoyer le texte adéquat au format HTML.

Afficher l'heure

Le module time.so (cf. Listing !!timec!! génère une page affichant simplement l’heure locale du serveur. La fonction module_generate de ce module appelle gettimeofday pour obtenir l’heure courante (reportez-vous à la Section !!gettimeofday!!, !! sec:gettimeofday !! du Chapitre !!appelssysteme!!, !! chap:appelssysteme !!) et utilise localtime et strftime pour générer une représentation textuelle de celle-ci. Cette représentation est ensuite injectée dans le modèle HTML page_template.

Listing (time.c) Module serveur affichant l’heure courante

#include <assert.h>
#include <stdio.h>
#include <sys/time.h>
#include <time.h>
 
#include "server.h"
 
/* Modèle de la page générée par le module. */
 
static char* page_template =
  "<html>\n"
  " <head>\n"
  " <meta http-equiv=\"refresh\" content=\"5\">\n"
  " </head>\n"
  " <body>\n"
  " <p>The current time is %s.</p>\n"
  " </body>\n"
  "</html>\n";
 
void module_generate (int fd)
{
  struct timeval tv;
  struct tm* ptm;
  char time_string[40];
  FILE* fp;
 
  /* Récupère l'heure courante et la convertit en struct tm. */
  gettimeofday (&tv, NULL);
  ptm = localtime (&tv.tv_sec);
 
  /* Formate l'heure avec une précision à la seconde. */
  strftime (time_string, sizeof (time_string), "%H:%M:%S", ptm);
 
  /* Crée un flux correspondant au descripteur de fichier de la
     socket cliente. */
  fp = fdopen (fd, "w");
  assert (fp != NULL);
  /* Génère la sortie HTML. */
  fprintf (fp, page_template, time_string);
  /* Terminé ; purge le flux. */
  fflush (fp);
}

Ce module utilise les fonctions d’E/S de la bibliothèque C standard pour des raisons pratiques. L’appel fdopen génère un pointeur sur un flux (FILE*) correspondant à la socket client (cf. Chapitre !!esbasniveau!!, !! chap:esbasniveau !!; Section !!lienESStandards!!, !! sec:lienESStandards !!). Le module envoie des données au descripteur en utilisant fprintf et le purge au moyen de fflush pour éviter une perte des données mises dans le tampon lorsque la socket est germée.

La page HTML renvoyée par le module time.so inclue élement <meta> qui demande au client de recharger la page toutes les 5 secondes. De cette façon, le client reste à l’heure.

Afficher la distribution utilisée

Le module issue.so (Listing !!issuec!! affiche des informations sur la distribution sur laquelle tourne le serveur. Ces informations sont habituellement stockées dans le fichier /etc/issue. Le module envoie le contenu de ce fichier encapsulé dans un élément <pre>.

Listing (issue.c) Module permettant d’afficher des informations sur la distribution GNU/Linux

#include <fcntl.h>
#include <string.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
 
#include "server.h"
 
/* Source HTML du début de la page que nous générons. */
 
static char* page_start =
  "<html>\n"
  " <body>\n"
  "  <pre>\n";
 
/* Source HTML de la fin de la page que nous générons. */
 
static char* page_end =
  " </pre>\n"
  " </body>\n"
  "</html>\n";
 
/* Source HTML de la page indiquant qu'il y a eu un problème à l'ouverture de
   /etc/issue. */
 
static char* error_page =
  "<html>\n"
  " <body>\n"
  " <p>Error: Could not open /etc/issue.</p>\n"
  " </body>\n"
  "</html>\n";
 
/* Source HTML indiquant une erreur.  */
 
static char* error_message = "Error reading /etc/issue.";
 
void module_generate (int fd)
{
  int input_fd;
  struct stat file_info;
  int rval;
 
  /* Ouvre /etc/issue. */
  input_fd = open ("/etc/issue", O_RDONLY);
  if (input_fd == -1)
    system_error ("open");
  /* Récupère des informations sur le fichier. */
  rval = fstat (input_fd, &file_info);
 
  if (rval == -1)
    /* Soit nous n'avons pas pu ouvrir le fichier, soit ne n'avons pas pu y lire. */
    write (fd, error_page, strlen (error_page));
  else {
    int rval;
    off_t offset = 0;
 
    /* Écrit le début de la page. */
    write (fd, page_start, strlen (page_start));
    /* Copie le contenu de /proc/issue vers la socket cliente. */
    rval = sendfile (fd, input_fd, &offset, file_info.st_size);
    if (rval == -1)
      /* Quelque chose s'est mal passé lors de l'envoi du contenu de /etc/issue.
         Envoie un message d'erreur. */
      write (fd, error_message, strlen (error_message));
    /* Fin de la page. */
    write (fd, page_end, strlen (page_end));
  }
 
  close (input_fd);
}

Le module commence par essayer d’ouvrir /etc/issue. Si ce fichier ne peut pas être ouvert, le module envoie une page d’erreur au client. Sinon, il envoie le début de la page HTML contenu dans page_start. Puis le contenu de /etc/issue est transmis au moyen de sendfile (cf. Chapitre !!appelssysteme!!, Section !!sendfile!!, !! sec:sendfile !!). Enfin, il envoie la fin de la page HTML contenue dans page_end.

Vous pouvez facilement adapter ce module pour envoyer le contenu d’un autre fichier. Si celui-ci contient une page HTML complète, supprimez la partie qui envoie page_start et page_end. Vous pourriez également adapter le serveur principal pour qu’il puisse servir des fichiers statiques, comme les serveurs Web traditionnels. L’utilisation de sendfile fournit un degré d’efficacité supplémentaire.

Afficher l'espace libre

Le modules diskfree.so (Listing !!diskfreec!!) génère une page affichant des informations sur l’espace disque libre sur les systèmes de fichiers montées sur le serveur. Ces informations sont simplement le résultat de l’invocaton de la commande df -h. Tout comme issue.so, le module encapsule les informations dans une balise <pre>.

Listing (diskfree.c) Module affichant des informations sur l’espace disque libre

#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
 
#include "server.h"
 
/* Source HTML du début de la page générée. */
 
static char* page_start =
  "<html>\n"
  " <body>\n"
  " <pre>\n";
 
/* Source HTML de la fin de la page générée. */
 
static char* page_end =
  " </pre>\n"
  " </body>\n"
  "</html>\n";
 
void module_generate (int fd)
{
  pid_t child_pid;
  int rval;
 
  /* Écrit le début de la page. */
  write (fd, page_start, strlen (page_start));
  /* Crée un processus fils. */
  child_pid = fork ();
  if (child_pid == 0) {
    /* Nous sommes le processus fils. */
    /* Crée la liste d'arguments pour l'invocation de df.  */
    char* argv[] = { "/bin/df", "-h", NULL };
 
    /* Duplique stdout et stderr afin d'envoyer les données la socket cliente. */
    rval = dup2 (fd, STDOUT_FILENO);
    if (rval == -1)
       system_error ("dup2");
    rval = dup2 (fd, STDERR_FILENO);
    if (rval == -1)
       system_error ("dup2");
    /* Lance df pour afficher l'espace libre sur les systèmes de fichiers montés. */
    execv (argv[0], argv);
    /* Un appel à execv ne se termine jamais à moins d'une erreur. */
    system_error ("execv");
  }
  else if (child_pid > 0) {
    /* Nous sommes dans le processus parent, nous
        attendons la fin du processus fils. */
    rval = waitpid (child_pid, NULL, 0);
    if (rval == -1)
       system_error ("waitpid");
  }
  else
    /* L'appel à fork a échoué. */
    system_error ("fork");
  /* Écrit la fin de la page. */
  write (fd, page_end, strlen (page_end));
}

Alors que issue.so envoie le contenu d’un fichier en utilisant sendfile, ce module doit invoquer une commande et rediriger sa sortie vers le client. Pour cela, il suit les étapes suivantes:

  • Tout d’abord, le module crée un processus fils au moyen de fork (cf. Chapitre !!processus!!, Section !!utiliserforkexec!!, !! sec:utiliserforkexec !!);
  • Le processus fils copie le descripteur de fichier de la socket vers les descripteurs STDOUT_FILENO et STDERR_FILENO qui correspondernt à la sortie standard et à la sortie des erreurs standard (cf. Chapitre !!logicielsQualite!!, Section !!ESStandards!!, !! sec:ESStandards !!). Les descripteurs sont copiés en utilisant l’appel dup2 (cf. Chapitre !!IPC!!, Section !!redirigerfluxes!!, !! sec:redirigerfluxes !!). Tout affichage ultérieur depuis le processus vers un de ces flux est envoyé à la socket cliente;
  • Le processus fils invoque la commande df avec l’option -h en apelant execv (cf. Chapitre !!processus!!, Section !!utiliserforkexec!!, !! sec:utiliserforkexec !!);
  • Le processus parent attend la fin du processus fils en appelant waitpid (cf. Chapitre !!processus!!, Section !!aswait!!, !! sec:aswait !!).

Vous pouvez facilement adapter ce module pour qu’il invoque une commande différente et redirige sa sortie vers le client.

Afficher la liste des processus en cours d'exécution

Le module processes.so (Listing !!processesc!!) est un module plus riche. Il génère une page contenant un tableau présentant les processus en cours d’exécution sur le serveur. Chaque processus apparaît dans une ligne du tableau affichant son PID, le nom de l’exécutable, l’utilisateur et le groupe propriétaires ainsi que la taille de l’empreinte mémoire résidente.

Listing (processes.c) Module listant les processus

#include <assert.h>
#include <dirent.h>
#include <fcntl.h>
#include <grp.h>
#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <unistd.h>
 
#include "server.h"
 
/* Place les identifiants du groupe et de l'utilisateur propriétaires du processus PID
   dans *UID et *GID, respectivement. Renvoie 0 en cas de succès, une valeur différente
   de 0 sinon. */
 
static int get_uid_gid (pid_t pid, uid_t* uid, gid_t* gid)
{
  char dir_name[64];
  struct stat dir_info;
  int rval;
 
  /* Génère le nom du répertoire du processus dans /proc. */
  snprintf (dir_name, sizeof (dir_name), "/proc/%d", (int) pid);
  /* Récupère des informations sur le répertoire. */
  rval = stat (dir_name, &dir_info);
  if (rval != 0)
    /* Répertoire introuvable ; ce processus n'existe peut être plus. */
    return 1;
  /* S'assure qu'il s'agit d'un répertoire ; si ce n'est pas le cas
     il s'agit d'une erreur. */
  assert (S_ISDIR (dir_info.st_mode));
 
  /* Récupère les identifiants dont nous avons besoin. */
  *uid = dir_info.st_uid;
  *gid = dir_info.st_gid;
  return 0;
}
 
/* Renvoie le nom de l'utilisateur UID. La valeur de retour est un tampon
   que l'appelant doit libérer avec free. UID doit être un identifiant utilisateur
   valide. */
 
static char* get_user_name (uid_t uid)
{
  struct passwd* entry;
 
  entry = getpwuid (uid);
  if (entry == NULL)
    system_error ("getpwuid");
  return xstrdup (entry->pw_name);
}
 
/* Renvoie le nom du groupe GID. La valeur de retour est un tampon que
   l'appelant doit libérer avec free. GID doit être un identifiant de groupe
   valide. */
 
static char* get_group_name (gid_t gid)
{
  struct group* entry;
 
  entry = getgrgid (gid);
  if (entry == NULL)
    system_error ("getgrgid");
  return xstrdup (entry->gr_name);
}
 
/* Renvoie le nom du programme s'exécutant dans le processus PID ou NULL en cas
   d'erreur. La valeur de retour est un tampon que l'appelant doit libérer avec
   free. */
 
static char* get_program_name (pid_t pid)
{
  char file_name[64];
  char status_info[256];
  int fd;
  int rval;
  char* open_paren;
  char* close_paren;
  char* result;
 
  /* Génère le nom du fichier "stat" dans le répertoire du processus sous /proc
     et l'ouvre. */
  snprintf (file_name, sizeof (file_name), "/proc/%d/stat", (int) pid);
  fd = open (file_name, O_RDONLY);
  if (fd == -1)
    /* Impossible d'ouvrir le fichier stat pour ce processus. Peut être qu'il
       n'existe plus. */
    return NULL;
  /* Lit le contenu. */
  rval = read (fd, status_info, sizeof (status_info) - 1);
  close (fd);
  if (rval <= 0)
    /* Impossible à lire pour une raison quelconque. Échec. */
    return NULL;
  /* Ajout un caractère nul à la fin du contenu du fichier. */
  status_info[rval] = '\0';
 
  /* Le nom du programme est le second élément dans le fichier et est
     encadré par des parenthèses. Recherche les positions des parenthèses
     dans le fichier. */
  open_paren = strchr (status_info, '(');
  close_paren = strchr (status_info, ')');
  if (open_paren == NULL
      || close_paren == NULL
      || close_paren < open_paren)
    /* Impossible de les trouver ; Échec. */
    return NULL;
  /* Alloué de la mémoire pour le résultat. */
  result = (char*) xmalloc (close_paren - open_paren);
  /* Copie le nom du programme dans le résultat. */
  strncpy (result, open_paren + 1, close_paren - open_paren - 1);
  /* strncpy n'ajoute pas d'octet nul à la fin du résultat, nous le faisons. */
  result[close_paren - open_paren - 1] = '\0';
  /* Terminé. */
  return result;
}
 
/* Renvoie la taille de l'empreinte mémoire résidente (RSS) en kilo octets, du
   processus PID. Renvoie -1 en cas d'échec. */
 
static int get_rss (pid_t pid)
{
  char file_name[64];
  int fd;
  char mem_info[128];
  int rval;
  int rss;
 
  /* Génère le nom de l'entrée "statm" du processus dans le répertoire /proc. */
  snprintf (file_name, sizeof (file_name), "/proc/%d/statm", (int) pid);
  /* L'ouvre. */
  fd = open (file_name, O_RDONLY);
  if (fd == -1)
    /* Impossible d'ouvrir le fichier "statm" pour ce processus. Peut être qu'il
       n'existe plus. */
    return -1;
  /* Lit le contenu du fichier. */
  rval = read (fd, mem_info, sizeof (mem_info) - 1);
  close (fd);
  if (rval <= 0)
    /* Impossible de lire le fichier ; échec. */
    return -1;
  /* Ajoute un octet nul. */
  mem_info[rval] = '\0';
  /* Extrait la RSS. Il s'agit du second élément. */
  rval = sscanf (mem_info, "%*d %d", &rss);
  if (rval != 1)
    /* Le contenu de statm est formaté d'une façon inconnue. */
    return -1;
 
  /* Les valeurs de statm sont en nombre de pages système.
     Convertit le RSS en kilo octets. */
  return rss * getpagesize () / 1024;
}
 
/* Génère une ligne de tableau HTML pour le processus PID. Renvoie un
   pointeur vers un tampon que l'appelant doit libérer avec free ou
   NULL si une erreur survient. */
 
static char* format_process_info (pid_t pid)
{
  int rval;
  uid_t uid;
  gid_t gid;
  char* user_name;
  char* group_name;
  int rss;
  char* program_name;
  size_t result_length;
  char* result;
 
  /* Récupère les UID et GID du processus. */
  rval = get_uid_gid (pid, &uid, &gid);
  if (rval != 0)
    return NULL;
  /* Obtient la RSS du processus. */
  rss = get_rss (pid);
  if (rss == -1)
    return NULL;
  /* Récupère le nom du programme du processus. */
  program_name = get_program_name (pid);
  if (program_name == NULL)
    return NULL;
  /* Convertit les UID et GID en noms symboliques. */
  user_name = get_user_name (uid);
  group_name = get_group_name (gid);
 
  /* Calcule la taille de la chaîne nécessaire pour stocker le résultat
     et alloue la mémoire correspondante. */
  result_length = strlen (program_name)
    + strlen (user_name) + strlen (group_name) + 128;
  result = (char*) xmalloc (result_length);
  /* Formate le résultat. */
  snprintf (result, result_length,
            "<tr><td align=\"right\">%d</td><td><tt>%s</tt></td><td>%s</td>"
            "<td>%s</td><td align=\"right\">%d</td></tr>\n",
            (int) pid, program_name, user_name, group_name, rss);
  /* Libération des ressources. */
  free (program_name);
  free (user_name);
  free (group_name);
  /* Terminé. */
  return result;
}
 
/* Source HTML du début de la page de listing. */
 
static char* page_start =
  "<html>\n"
  " <body>\n"
  " <table cellpadding=\"4\" cellspacing=\"0\" border=\"1\">\n"
  "   <thead>\n"
  "    <tr>\n"
  "     <th>PID</th>\n"
  "     <th>Program</th>\n"
  "     <th>User</th>\n"
  "     <th>Group</th>\n"
  "     <th>RSS&nbsp;(KB)</th>\n"
  "    </tr>\n"
  "   </thead>\n"
  "   <tbody>\n";
 
/* Source HTML de la fin de la page. */
 
static char* page_end =
  "   </tbody>\n"
  " </table>\n"
  " </body>\n"
  "</html>\n";
 
void module_generate (int fd)
{
  size_t i;
  DIR* proc_listing;
 
  /* Crée un tableau d'iovec. Nous le renseignerons avec les
     tampons qui seront assemblés pour générer la sortie, en adaptant
     sa taille de façon dynamique. */
 
  /* Nombre d'éléments utilisés dans le tableau. */
  size_t vec_length = 0;
  /* Taille du tableau. */
  size_t vec_size = 16;
  /* Tableau d'iovec. */
  struct iovec* vec =
    (struct iovec*) xmalloc (vec_size * sizeof (struct iovec));
 
  /* Le premier buffer est la source HTML du début de la page. */
  vec[vec_length].iov_base = page_start;
  vec[vec_length].iov_len = strlen (page_start);
  ++vec_length;
 
  /* Commence le listing du répertoire /proc.  */
  proc_listing = opendir ("/proc");
  if (proc_listing == NULL)
    system_error ("opendir");
 
  /* Parcourt les entrées de /proc.  */
  while (1) {
    struct dirent* proc_entry;
    const char* name;
    pid_t pid;
    char* process_info;
 
    /* Récupère l'entrée suivante. */
    proc_entry = readdir (proc_listing);
    if (proc_entry == NULL)
      /* Nous sommes à la fin du répertoire.  */
      break;
    /* Si cette entrée n'est pas composée uniquement de chiffres, il ne
       s'agit pas d'un répertoire de processus. Nous le laissons de côté. */
    name = proc_entry->d_name;
    if (strspn (name, "0123456789") != strlen (name))
      continue;
    /* Le nom de l'entrée est l'identifiant du processus. */
    pid = (pid_t) atoi (name);
    /* Génère une ligne de tableau HTML décrivant ce processus. */
    process_info = format_process_info (pid);
    if (process_info == NULL)
      /* Quelque chose s'est mal passé. Le processus peut s'être terminé
         pendant que nous l'analysions. Affiche une ligne d'erreur. */
      process_info = "<tr><td colspan=\"5\">ERROR</td></tr>";
 
    /* S'assure que le tableau iovec est suffisamment long pour accueillir
       le tampon (plus un car nous aurons besoin d'un élément supplémentaire
       à la fin du listing). Si ce n'est pas le cas, double sa taille. */
    if (vec_length == vec_size - 1) {
      vec_size *= 2;
      vec = xrealloc (vec, vec_size * sizeof (struct iovec));
    }
    /* Stocke le tampon en tant qu'élément suivant dans le tableau. */
    vec[vec_length].iov_base = process_info;
    vec[vec_length].iov_len = strlen (process_info);
    ++vec_length;
  }
 
  /* Ferme le répertoire à la fin du listing.  */
  closedir (proc_listing);
 
  /* Ajout un dernier tampon avec le HTML de fin de page.  */
  vec[vec_length].iov_base = page_end;
  vec[vec_length].iov_len = strlen (page_end);
  ++vec_length;
 
  /* Envoie toute la page au descripteur client en une seule fois.  */
  writev (fd, vec, vec_length);
 
  /* Libère les différents tampons. Le premier et le dernier contiennent des données
     statiques et ne doivent pas être libérées. */
  for (i = 1; i < vec_length - 1; ++i)
    free (vec[i].iov_base);
  /* Libère la mémoire occupée par le tableau d'iovec. */
  free (vec);
}

La récolte d’informations sur un processus et leur formatage en HTML sont divisés en plusieurs opérations plus simples:

  • get_uid_gid extrait les identifiants de l’utilisateur et du groupe propriétaires d’un processus. Pour cela, la fonction utilise stat (cf. Chapitre !!esbasniveau!!, Section !!stat!!, !! sec:stat !!) sur le répertoire de /proc correspondant au processus (cf. Chapitre !!SFproc!!, Section !!repproc!!, !! sec:repproc !!). L’utilisateur et le groupe propriétaires de ce répertoire sont identiques aux propriétaires du processus;
  • get_user_name renvoie le nom d’utilisateur correspondant à un UID. Cette fonction se contente d’appeler la fonction de la bibliothèque C getpwuid, qui consulte le fichier /etc/passwd du système et renvoie une copie du résultat. get_group_name renvoie le nom du groupe correspondant à unGID. Il utilise l’appel getrgid.
  • get_program_name renvoie le nom du programme s’exécutant dans un processus donné. Ces informations sont obtenues à partir de l’entrée stat dans le répertoire du processus sous /proc (cf. Chapitre !!SFproc!!, Section !!repproc!!, !! sec:repproc !!). Nous utilisons cette entrée plutôt que le lien symbolique exe (cf. Chapitre !!SFproc!!, Section !!execproc!!, !! sec:execproc !!) ou l’entrée cmdline (cf. Chapitre !!SFproc!!, Section !!listeargsproc!!, !! sec:listeargsproc !!) car ils sont inaccessibles si le processus serveur n’appartient pas au même utilisateur que le celui qui est examiné. De plus, la lecture de stat ne force pas Linux à remonter le processus examiné en mémoire s’il avait été déchargé de la mémoire;
  • get_rss renvoie la taille de l’empreinte mémoire résidente d’un processus. Cette information est donnée par le second élément du contenu de l’entrée statm dans le répertoire /proc pour le processus (cf. Chapitre !!SFproc!!, Section !!statsmemproc!!, !! sec:statsmemproc !!);
  • format_process_info génère une chaîne contenant les éléments HTML à placer dans une ligne du tableau représentant un processus. Après l’appel aux fonctions présentées ci-dessus pour obtenir les informations nécessaires, elle alloue un tampon et génère le HTML en utilisant snprintf;
  • module_generate génère la page HTML proprement dite, y compris le tableau. La sortie est composée d’une chaîne contenant le début de la page et du tableau (page_start), d’une chaîne pour chaque ligne du tableau (générée par format_process_info) et une chaîne contenant la fin du tableau et de la page (page_end).
    module_generate détermine les PID des processus en cours d’exécution sur le système en lisant le contenu de /proc. Elle utilise opendir et readdir (cf. Chapitre !!esbasniveau!!, Section !!lirecontenurep!!, !! sec:lirecontenurep !!) pour parcourir le répertoire. Elle analyse son contenu à la recherche d’entrées composées uniquement de chiffres; qui sont supposées être des entrées correspondant à des processus.
    Un nombre potentiellement important de chaînes doivent être envoyées vers la socket cliente – une pour le début et une pour la fin de la page, ainsi qu’une par processus. Si chaque chaîne devait être écrite avec un appel distinct à write cela générerait une surcharge réseau inutile car chaque chaîne devrait être envoyée dans un paquet distinct.
    Pour optimiser l’encapsulation des données dans des paquets, nous utilisons un unique appel à writev (cf. Chapitre !!esbasniveau!!, Section !!elvec!!, !! sec:elvec !!). Pour cela, nous devons construire un tableau vec d’objets struct iovec. Cependant, comme nous ne connaissons pas à l’avance le nombre de processus, nous devons commencer avec un tableau très petit que nous étendons au fur et à mesure de l’ajout de processus. La variable vec_length contient le nombre d’éléments utilisés dans vec tandis que vec_size contient la taille réelle de vec. Lorsque vec_length est en passe de devenir plus grand que vec_size, nous doublons la taille de vec par le viais de xrealloc. Lorsque nous avons terminé l’écriture des données, nous devons libérer toutes les chaînes allouées dynamiquement sur lesquelles pointent les différentes entrées de vec puis libérer vec proprement dit.

Utilisation du serveur

Si nous avions l’intention de distribuer les sources du programme, de les maintenir et de les porter sur d’autres plateformes, nous utiliserions probablement GNU Automake et GNU Autoconf ou un système semblable d’automatisation de la configuration. De tels outils dépassent du cadre de ce livre; pour plus d’informations, consultez GNU Autoconf, Automake, and Libtool4) (de Vaughan, Elliston, Tromey et Taylor, chez New Riders, 2000).

Le fichier Makefile

Au lieu d’utiliser Autoconf ou un outil voisin, nous fournissons un fichier Makefile compatible avec GNU Make5) afin qu’il soit simple de compiler et lier le serveur et ses modules. Le Makefile est présenté dans le Listing !!applimakefile!!. Consultez la page info de GNU Make pour plus de détails sur la syntaxe de ce type de fichiers.

### Configuration.  ####################################################

# Options par défaut du compilateur C.
CFLAGS                 = -Wall -g
# Fichiers sources C pour le serveur.
SOURCES                = server.c module.c common.c main.c
# Fichiers objets correspondants.
OBJECTS                = $(SOURCES:.c=.o)
# Fichiers de bibliothèques partagées des modules.
MODULES                = diskfree.so issue.so processes.so time.so

### Règles.  ###########################################################

# Les cibles phony ne correspondent pas à des fichiers à construire; il
# s'agit de noms pour des cibles conceptuelles.
.PHONY:         all clean

# Cible par défaut: construit tout.
all:            server $(MODULES)

# Nettoie les objets construits.
clean:
        rm -f $(OBJECTS) $(MODULES) server

# Programme serveur principal. Fait l'édition de liens avec les options
# -Wl,-export-dynamic afin que les modules puissent utiliser des symboles
# définis par le programme. Utilise libdl qui contient les appels nécessaires
# au chargement dynamique.
server:         $(OBJECTS)
        $(CC) $(CFLAGS) -Wl,-export-dynamic -o $@ $^ -ldl

# Tous les fichiers objets du serveur dépendent de server.h. Les fichiers
# objets sont construits à partir des fichiers sources grâce à une règle par
# défaut.
$(OBJECTS):     server.h

# Règle pour construire les bibliothèques partagées des modules à partir des
# fichiers sources correspondants. Compile avec -fPIC et génère un fichier
# objet partagé.
$(MODULES): \
%.so:           %.c server.h
        $(CC) $(CFLAGS) -fPIC -shared -o $@ $<

Le Makefile définit les cibles suivantes:

  • all (la cible par défaut si vous invoquez make sans arguments car il s’agit de la première cible dans le Makefile) comprend l’exécutable du serveur et tous les modules. Les modules sont définis par la variable MODULES;
  • clean supprime tout élément construit par le Makefile;
  • server crée l’exécutable du serveur. Les fichiers sources listés dans la variables SOURCES sont compilés et liés;
  • la dernière ligne est un modèle générique afin de compiler les fichiers objets partagés des modules du serveur à partir des fichiers sources correspondants.

Notez que les fichiers sources des modules serveur sont compilés en utilisant l’option -fPIC car il s’agit de bibliothèques partagées (cf. Chapitre !!logicielsQualite!!, Section !!bibliopart!!, !! sec:bibliopart !!).

Remarquez également que l’exécutable du serveur est lié avec les options de compilation -Wl et -export-dynamic. Grâce à cela, GCC passe l’option -export-dynamic à l’éditeur de liens qui crée un fichier exécutable qui exporte ses symboles comme le fait une bibliothèque partagée. Cela permet aux modules, qui sont chargés dynamiquement sous forme de bibliothèques partagées, de faire référence à des fonctions de common.c qui sont liées statiquement à l’exécutable du serveur.

Construire le serveur

La construction du programme est relativement simple. Depuis le répertoire où se situent les sources, invoquez simplement make:

% make
cc -Wall -g   -c -o server.o server.c
cc -Wall -g   -c -o module.o module.c
cc -Wall -g   -c -o common.o common.c
cc -Wall -g   -c -o main.o main.c
cc -Wall -g -Wl,-export-dynamic -o server server.o module.o common.o main.o -ldl
cc -Wall -g -fPIC -shared -o diskfree.so diskfree.c
cc -Wall -g -fPIC -shared -o issue.so issue.c
cc -Wall -g -fPIC -shared -o processes.so processes.c
cc -Wall -g -fPIC -shared -o time.so time.c

Cela lance la construction du serveur et des bibliothèques partagées des modules.

% ls -l server *.so
-rwxr-xr-x    1 samuel samuel 25769 Mar 11 01:15 diskfree.so
-rwxr-xr-x    1 samuel samuel 31184 Mar 11 01:15 issue.so
-rwxr-xr-x    1 samuel samuel 41579 Mar 11 01:15 processes.so
-rwxr-xr-x    1 samuel samuel 71758 Mar 11 01:15 server
-rwxr-xr-x    1 samuel samuel 13980 Mar 11 01:15 time.so

Lancer le serveur

Pour lancer le serveur, invoquez simplement l’exécutable server.

Si vous ne spécifiez pas de port au moyen de l’option –port (-p); Linux en choisira un pour vous; dans ce cas, passez l’option –verbose (-v) afin que le serveur l’affiche.

Si vous ne demandez pas d’adresse particulière au moyen de –address (-a), le serveur s’exécute sur toutes vos adresses réseau. Si votre ordinateur est relié à un réseau, cela signifie que d’autres personnes pourront y accéder, si tant est qu’elles connaissent le port à utiliser et une page à demander. Pour des raisons de sécurité, il est conseillé d’utiliser l’adresse localhost jusqu’à ce que vous soyez sûr que le serveur fonctionne correctement et ne révèle pas d’informations confidentielles. L’utilisation de cette adresse oblige le serveur à utiliser le périphérique réseau de bouclage (appelé lo) – seuls les programmes s’exécutant sur le même ordinateur peuvent s’y connecter. Si vous spécifiez une adresse différente, elle doit correspondre à votre ordinateur:

% ./server --address localhost --port 4000

Le serveur est désormais opérationnel. Ouvrez un navigateur et essayez de vous y connecter en utilisant le bon numéro de port. Demandez une page correspondant à l’un des modules. Par exemple, pour invoquer le module diskfree.so, utilisez cette URL:

http://localhost:4000/diskfree

Au lieu de 4000, utilisez le numéro de port que vous avez spécifié (ou le port choisi pour vous par Linux). Appuyez sur Ctrl+C pour tuer le serveur lorsque vous en avez fini.

Si vous ne spécifiez pas localhost comme adresse pour le serveur, vous pouvez également vous y connecter à partir d’un autre ordinateur en utilisant le nom du vôtre dans l’URL – par exemple:

http://host.domain.com:4000/diskfree

Si vous passez l’option –verbose (-v), le serveur affiche des informations au démarrage et affiche l’adresse IP de chaque client se connectant. Si vous vous connectez via localhost, l’adresse sera toujours 127.0.0.1.

Si vous tentez d’écrire vos propres modules, vous pourriez les placer dans un répertoire différent de celui contenant le serveur. Dans ce cas, indiquez ce répertoire au moyen de l’option –module-dir (-m). Le serveur recherchera les modules dans ce répertoire.

Si vous oubliez la syntaxe des options en ligne de commande, invoquez server avec l’option –help (-h):

% ./server --help
Usage: ./server [ options ]
  -a, --address ADDR        Bind to local address (by default, bind
                              to all local addresses).
  -h, --help                Print this information.
  -m, --module-dir DIR      Load modules from specified directory
                              (by default, use executable directory).
  -p, --port PORT           Bind to specified port.
  -v, --verbose             Print verbose messages.

Pour finir

Si vous aviez réellement l’intention de publier ce programme, vous devriez écrire de la documentation. Beaucoup de gens ne se rendent pas compte que l’écriture d’une bonne documentation est aussi difficile et prend autant de temps – et est aussi important – que l’écriture de bons logiciels. Cependant, la documentation des logiciels pourrait faire l’objet d’un livre entier, nous vous laisserons donc avec quelques références vous permettant d’en apprendre plus sur la façon de documenter un logiciel GNU/Linux.

Vous aurez probablement besoin d’écrire une page de manuel pour le programme server, par exemple. C’est le premier endroit où la plupart des utilisateurs vont rechercher des informations sur un programme. Les pages de manuel sont formatées en utilisant un système classique sous UNIX, troff. Pour consulter la page de manuel de troff, qui décrit le format des fichiers troff, utilisez la commande suivante:

% man troff

Pour en savoir plus sur l’organisation des pages de manuel sous GNU/Linux, consultez la page de manuel de la commande man via:

% man man

Vous pouvez également écrire des pages info pour le serveur et les modules, en utilisant le système Info GNU. Naturellement, le système Info est documenté au format Info; pour consulter la documentation, invoquez:

% info info

Beaucoup de programmes GNU/Linux proposent également une documentation texte ou HTML.

Bonne programmation sous GNU/Linux !

1) Le serveur Web open source le plus populaire sous GNU/Linux est le serveur Apache, disponible sur http://www.apache.org
2) Votre ordinateur peut être configuré pour disposer d’interfaces telles que eth0, une carte Ethernet, lo, le réseau de bouclage ou ppp0, une connexion réseau à distance.
3) gethostbyname effectue une résolution DNS si nécessaire.
4) NdT. en anglais
5) GNU Make est généralement installé sur les systèmes GNU/Linux.
 
livre/chap11/application_d_illustration.txt · Dernière modification: 2008/01/09 23:47 par sdinot
 
Recent changes RSS feed Creative Commons License Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki Hosted by TuxFamily