123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298 |
- \section{Communication entre processus}
- Dans un premier temps, il faut choisir un mécanisme de communication entre les
- différents processus. Nous allons donc lister rapidement les méthodes
- utilisables.
- \subsection{Les canaux unix}
- Les canaux unix ou pipes unix permettent de réaliser une communication
- multi-processus unidirectionnelle. D'un côté, un producteur écrit dans le tube,
- ou bloque jusqu'à ce qu'il puisse avoir la place d'écrire. De l'autre côté, un
- consommateur lit dans le tube ou bloque si aucune donnée n'est disponible. Les
- canaux unix ont l'avantage d'être très faciles à manipuler et être globalement
- disponibles sur toutes les plateformes importantes sans exception. Néanmoins, il
- ne s'agit que du transfert de données et il faut donc un mécanisme d'accès pour
- les ressources. Les canaux Unix peuvent être créés de façon anonyme avec la
- fonction \inltype{pipe}, soit être nommés via la fonction \inltype{mknod} ou
- \inltype{mkfifo}.
- \subsection{Les files de message}
- Les files de message POSIX, ou message queues sont assez proches des canaux
- unix, mais sont forcément nommées et permettent d'ajouter des priorités tout en
- segmentant les messages. Ici la propriété de segmentation est très intéressante
- pour pouvoir déboguer et parser les messages envoyés d'une partie de
- l'application à l'autre. Néanmoins, aucun mécanisme ne permet d'envoyer des
- ressources sur une file de message. Il faut donc également réaliser une
- passerelle et un contrôle d'identité pour pouvoir l'utiliser dans notre cas.
- \subsection{Les mémoires partagées}
- Les mémoires partagés POSIX fournissent un moyen de communication extrêmement
- performant pour dialoguer. Elles ont besoin d'un mécanisme de synchronisation
- mais n'ont absolument pas besoin de passer par des appels systèmes hormis lors
- de leur création. C'est donc un candidat de choix pour une communication entre
- deux processus. En revanche, elle demande l'introduction de beaucoup de contrôle
- autour pour éviter les bogues classiques en C (dépassements ou écrasements de
- mémoire) et les ressources ne peuvent être transmises qu'à travers un autre
- mécanisme ou une redirection d'IO vers la mémoire partagée.
- %TODO: expliquer redirection au dessus en citant la partie général: qqun
- %fait les IO et redirige le résultat sur la shared memory
- \subsection{Les primitives Sys V}
- Sys V fournit des alternatives pour chacune des méthodes citées ci-dessus.
- Malheureusement, les primitives de Sys V fonctionnent avec leur propre API et ne
- se rapprochent pas du tout de la philosophie unix. Ces API fournissent un objet
- Sys V au lieu d'un descripteur de fichier et bien qu'étant compatibles plus
- facilement avec d'autres systèmes grâce à son ancienneté, nous allons voir que
- les autres solutions apportent bien plus d'avantages.
- \subsection{Les sockets unix}
- Les sockets unix permettent d'envoyer des messages, soit de façon segmentée
- avec les mécanismes de \og{}end of record\fg{}, soit par l'envoi de datagrammes. Les
- performances en tant qu'IPC sont les mêmes que les deux premières méthodes,
- mais les sockets Unix disposent d'une caractéristique unique qui correspond
- très exactement à notre problème. Ils disposent d'un moyen de transmettre des
- descripteurs de fichiers d'un processus à l'autre, en y joignant le message qui
- permet d'interpréter la ressource. Cette interface est néanmoins difficile à
- manipuler comme on va le voir dans la suite.
- \subsection{Choix de la solution}
- L'IPC qui s'applique le plus simplement à notre solution tout en conservant des
- performances correctes apparait clairement comme étant les sockets unix. Ils
- fournissent une interface de transfert plus intéressante et mieux contrôlable,
- mais surtout permettent de transférer des descripteurs de fichiers entre
- processus. Ce dernier point permet une gestion des accès et des ressources
- particulièrement fine et va être à la base de mon implémentation Linux.
- \section{Partage de ressources entre processus}
- \subsection{Objectifs}
- Profitant de la caractéristique des sockets unix, nous allons implémenter le
- transfert de descripteur de fichier entre processus. L'objectif derrière cette
- fonctionnalité est de pouvoir utiliser les descripteurs de fichier comme des
- jetons inforgeables qui apportent des accès aux ressources, soit par
- communication, soit par accès direct.
- L'implémentation technique de ce système est fortement inspirée des méthodes
- utilisées initialement par Nginx. Nginx est un serveur web et proxy inverse
- programmé avec un paradigme asynchrone. Comme pour la sandbox, il fonctionne
- avec un processus orchestrateur qui contrôle les processus serveurs, ou workers.
- Chaque worker fonctionne en tant qu'utilisateur non privilégié.
- Cette solution doit être compatible avec le bus de communication que l'on veut
- mettre en place.
- Les sockets unix sont alors bien la solution de communication inter-processus
- correspondant parfaitement au problème. Ils sont gérés efficacement, peuvent
- être anonymes et donc parfaitement contrôlés et sont représentés dans le
- programme par des descripteurs de fichiers, permettant ainsi de les transmettre
- entre processus.
- \subsection{Envoi de données}
- Le mécanisme de données auxiliaires mentionné plus haut est générique pour
- toutes les classes de sockets. En fait, on peut s'intéresser aux structures
- utilisées lors de l'envoi d'un message.
- \begin{code}{c}{Structure \inltype{msghdr} utilisée dans
- \inltype{sendmsg}/\inltype{recvmsg}}
- /* from man 2 send */
- struct msghdr {
- void *msg_name; /* optional address */
- socklen_t msg_namelen; /* size of address */
- struct iovec *msg_iov; /* scatter/gather array */
- size_t msg_iovlen; /* # elements in msg_iov */
- void *msg_control; /* ancillary data, see below */
- size_t msg_controllen; /* ancillary data buffer len */
- int msg_flags; /* flags (unused) */
- };
- \end{code}
- % TODO reformuler
- Il y a en réalité deux types de buffers envoyés via cette structure. Le premier
- type concerne \inltype{msg_iov} qui est en réalité un tableau de
- \inltype{iovec}. Chaque \inltype{iovec} contient un pointeur vers un buffer et
- une taille. Ces \inltype{iovec} représentent les données qui seront envoyées à
- travers le socket. Le fait que \inltype{msg_iov} soit un tableau de ces buffers
- permet d'éviter de copier un paquet morcelé côté utilisateur avant de le
- recopier du côté du noyau.
- L'autre tampon mémoire est en réalité dédié à la pile logicielle manipulant le
- socket dans le noyau. Il s'agit d'un buffer de configuration, ou pour reprendre
- le vocabulaire utilisé dans la structure, un buffer de contrôle de l'interface
- bas niveau. Il peut être utilisé pour donner des paramètres dépendants de chaque
- type de socket. Pour les sockets unix, cela va permettre de manipuler
- l'interface SCM (socket control message) qui est capable de copier les
- descripteurs de fichier.
- \begin{code}{c}{Structure \inltype{cmsghdr} représentant un élément auxiliaire}
- /* from man 3 cmsg */
- struct cmsghdr {
- size_t cmsg_len; /* Data byte count, including header
- (type is socklen_t in POSIX) */
- int cmsg_level; /* Originating protocol */
- int cmsg_type; /* Protocol-specific type */
- /* followed by
- unsigned char cmsg_data[]; */
- };
- \end{code}
- \subsection{Manipulation des données auxiliaires}
- La manipulation de ce buffer est un peu particulière. Il s'agit en fait
- d'allouer des structures de données au seins du buffer même, tout en respectant
- les contraintes demandées par le système. En pratique, l'API POSIX fournit les
- macros suivantes pour manipuler ce buffer.
- \begin{itemize}
- \item \inltype{CMSG_FIRSTHDR} retourne un pointeur vers le premier
- \inltype{cmsghdr} dans le tampon de données auxiliaires associé au
- \inltype{msghdr} passé en paramètre.
- \item \inltype{CMSG_NXTHDR} retourne le prochain \inltype{cmsghdr} après
- celui passé en paramètre, ou \inltype{NULL} si le tampon est plein.
- \item \inltype{CMSG_SPACE} retourne la taille occupée par un élément
- auxiliaire.
- \item \inltype{CMSG_DATA} retourne un pointeur vers la partie donnée d'un
- \inltype{cmsghdr}.
- \item \inltype{CMSG_LEN} retourne la valeur à stocker dans le champ
- \inltype{cmsg_len} en comptant l'alignement, à partir de la taille des
- données.
- \end{itemize}
- Deux problématiques ont dû être prises en compte lors de l'utilisation de cette
- API. D'un côté, il faut faire attention aux accès non alignés. De l'autre, il
- faut respecter les règles de strict aliasing, c'est-à-dire qu'il ne doit pas
- exister deux pointeurs de types incompatibles vers le même espace mémoire. Tout
- cela va se résumer à correctement suivre les recommandations de l'API tout en
- utilisant \inltype{memcpy} pour copier les données depuis et vers le tampon
- mémoire.
- \begin{code}{c}{Implémentation du traitement des données auxiliaires}
- static int
- ipc_internal_SendMsg( int socket_fd, unsigned nb_buffer,
- struct process_msg_internal_t *buffers[],
- unsigned nb_fd, int* fds)
- {
- /* On utilise une union pour que la contrainte d'alignement du type cmsghdr
- * s'applique au tampon buf */
- union {
- char buf[512];
- struct cmsghdr align;
- } ctlbuf;
- \end{code}
- Les tampons mémoires sont convertis de la représentation de l'application vers
- celle de l'API Linux.
- \begin{code}{c}{Conversion des tampons et initialisation du msghdr}
- struct iovec iov[nb_buffer];
- for (unsigned i=0; i<nb_buffer; ++i) {
- iov[i].iov_base = buffers[i]->buffer;
- iov[i].iov_len = buffers[i]->length;
- }
- struct msghdr msgh = {
- .msg_name = NULL,
- .msg_namelen = 0,
- .msg_iov = iov,
- .msg_iovlen = nb_buffer,
- .msg_control = ctlbuf.buf,
- .msg_controllen = sizeof( ctlbuf ),
- .msg_flags = 0
- };
- \end{code}
- Puis les descripteurs de fichier sont copiés dans la structure.
- \inltype{SCM_RIGHTS} permet d'indiquer ici que ce sont bien des descripteurs de
- fichier et qu'il va falloir les dupliquer dans le processus qui reçoit le
- paquet.
- \begin{code}{c}{Création des données auxiliaires}
- if( nb_fd > 0 )
- {
- struct cmsghdr *cmsg = CMSG_FIRSTHDR( &msgh );
- cmsg->cmsg_level = SOL_SOCKET;
- cmsg->cmsg_type = SCM_RIGHTS;
- cmsg->cmsg_len = CMSG_LEN( nb_fd * sizeof( int ) );
- void *p_data = CMSG_DATA( cmsg );
- memcpy( p_data, fds, nb_fd * sizeof( int ) );
- msgh.msg_controllen = cmsg->cmsg_len;
- }
- else
- {
- msgh.msg_controllen = 0;
- msgh.msg_control = NULL;
- }
- \end{code}
- Enfin le message est envoyé. On utilise \inltype{MSG_EOR} pour pouvoir signaler
- la fin du message, comme il s'agit de \inltype{SOCK_SEQPACKET} mais d'autres
- solutions sont possibles.
- \begin{code}{c}{Envoi du message}
- ssize_t i_size = sendmsg( socket_fd, &msgh, MSG_EOR );
- \end{code}
- % if( i_size == -1 )
- % {
- % fprintf( stderr, "Couldn't send message, error at sendmsg: %d\n", errno);
- % return VLC_EGENERIC;
- % }
- % else if( i_size == 0 )
- % {
- % // Déconnexion
- % return VLC_ENOOBJ;
- % }
- %
- % return VLC_SUCCESS;
- % }
- % TODO développer problématiques
- Pour la réception, la méthode est similaire, à l'exception de l'apparition de
- \inltype{CMSG_LEN(0)} pour remplacer \inltype{CSMG_ALIGN} qui n'existe que sous
- Linux:
- \begin{code}{c}{Réception des descripteurs de fichier}
- struct cmsghdr *cmsg = CMSG_FIRSTHDR( &msgh );
- while( cmsg != NULL )
- {
- if( cmsg->cmsg_level == SOL_SOCKET
- && cmsg->cmsg_type == SCM_RIGHTS )
- {
- // TODO: check len, it takes headers into account
- // TODO: change assert in error handling
- // TODO: alloc msg->fds
- size_t len = cmsg->cmsg_len - CMSG_LEN(0);
- assert( len >= sizeof( int ) );
- assert( len % sizeof( int ) == 0 );
- len /= sizeof( int );
- ARRAY_INIT( (*msg)->fds );
- for( int i = 0; i < len; ++i )
- {
- int fd;
- memcpy( &fd, CMSG_DATA( cmsg ) + i * sizeof( int ),
- sizeof( int ));
- ARRAY_APPEND( (*msg)->fds, fd );
- (*msg)->i_nb_fds++;
- }
- }
- cmsg = CMSG_NXTHDR( &msgh, cmsg );
- }
- \end{code}
- Finalement, nous avons ainsi conçu un moyen de construire des jetons
- inforgeables pouvant soit transmettre d'autres jetons, soit servir de RPC vers
- un accès particulier.
|