\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; ibuffer; 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.