linux_file_descriptor.tex 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. \section{Communication entre processus}
  2. Dans un premier temps, il faut choisir un mécanisme de communication entre les
  3. différents processus. Nous allons donc lister rapidement les méthodes
  4. utilisables.
  5. \subsection{Les canaux unix}
  6. Les canaux unix ou pipes unix permettent de réaliser une communication
  7. multi-processus unidirectionnelle. D'un côté, un producteur écrit dans le tube,
  8. ou bloque jusqu'à ce qu'il puisse avoir la place d'écrire. De l'autre côté, un
  9. consommateur lit dans le tube ou bloque si aucune donnée n'est disponible. Les
  10. canaux unix ont l'avantage d'être très faciles à manipuler et être globalement
  11. disponibles sur toutes les plateformes importantes sans exception. Néanmoins, il
  12. ne s'agit que du transfert de données et il faut donc un mécanisme d'accès pour
  13. les ressources. Les canaux Unix peuvent être créés de façon anonyme avec la
  14. fonction \inltype{pipe}, soit être nommés via la fonction \inltype{mknod} ou
  15. \inltype{mkfifo}.
  16. \subsection{Les files de message}
  17. Les files de message POSIX, ou message queues sont assez proches des canaux
  18. unix, mais sont forcément nommées et permettent d'ajouter des priorités tout en
  19. segmentant les messages. Ici la propriété de segmentation est très intéressante
  20. pour pouvoir déboguer et parser les messages envoyés d'une partie de
  21. l'application à l'autre. Néanmoins, aucun mécanisme ne permet d'envoyer des
  22. ressources sur une file de message. Il faut donc également réaliser une
  23. passerelle et un contrôle d'identité pour pouvoir l'utiliser dans notre cas.
  24. \subsection{Les mémoires partagées}
  25. Les mémoires partagés POSIX fournissent un moyen de communication extrêmement
  26. performant pour dialoguer. Elles ont besoin d'un mécanisme de synchronisation
  27. mais n'ont absolument pas besoin de passer par des appels systèmes hormis lors
  28. de leur création. C'est donc un candidat de choix pour une communication entre
  29. deux processus. En revanche, elle demande l'introduction de beaucoup de contrôle
  30. autour pour éviter les bogues classiques en C (dépassements ou écrasements de
  31. mémoire) et les ressources ne peuvent être transmises qu'à travers un autre
  32. mécanisme ou une redirection d'IO vers la mémoire partagée.
  33. %TODO: expliquer redirection au dessus en citant la partie général: qqun
  34. %fait les IO et redirige le résultat sur la shared memory
  35. \subsection{Les primitives Sys V}
  36. Sys V fournit des alternatives pour chacune des méthodes citées ci-dessus.
  37. Malheureusement, les primitives de Sys V fonctionnent avec leur propre API et ne
  38. se rapprochent pas du tout de la philosophie unix. Ces API fournissent un objet
  39. Sys V au lieu d'un descripteur de fichier et bien qu'étant compatibles plus
  40. facilement avec d'autres systèmes grâce à son ancienneté, nous allons voir que
  41. les autres solutions apportent bien plus d'avantages.
  42. \subsection{Les sockets unix}
  43. Les sockets unix permettent d'envoyer des messages, soit de façon segmentée
  44. avec les mécanismes de \og{}end of record\fg{}, soit par l'envoi de datagrammes. Les
  45. performances en tant qu'IPC sont les mêmes que les deux premières méthodes,
  46. mais les sockets Unix disposent d'une caractéristique unique qui correspond
  47. très exactement à notre problème. Ils disposent d'un moyen de transmettre des
  48. descripteurs de fichiers d'un processus à l'autre, en y joignant le message qui
  49. permet d'interpréter la ressource. Cette interface est néanmoins difficile à
  50. manipuler comme on va le voir dans la suite.
  51. \subsection{Choix de la solution}
  52. L'IPC qui s'applique le plus simplement à notre solution tout en conservant des
  53. performances correctes apparait clairement comme étant les sockets unix. Ils
  54. fournissent une interface de transfert plus intéressante et mieux contrôlable,
  55. mais surtout permettent de transférer des descripteurs de fichiers entre
  56. processus. Ce dernier point permet une gestion des accès et des ressources
  57. particulièrement fine et va être à la base de mon implémentation Linux.
  58. \section{Partage de ressources entre processus}
  59. \subsection{Objectifs}
  60. Profitant de la caractéristique des sockets unix, nous allons implémenter le
  61. transfert de descripteur de fichier entre processus. L'objectif derrière cette
  62. fonctionnalité est de pouvoir utiliser les descripteurs de fichier comme des
  63. jetons inforgeables qui apportent des accès aux ressources, soit par
  64. communication, soit par accès direct.
  65. L'implémentation technique de ce système est fortement inspirée des méthodes
  66. utilisées initialement par Nginx. Nginx est un serveur web et proxy inverse
  67. programmé avec un paradigme asynchrone. Comme pour la sandbox, il fonctionne
  68. avec un processus orchestrateur qui contrôle les processus serveurs, ou workers.
  69. Chaque worker fonctionne en tant qu'utilisateur non privilégié.
  70. Cette solution doit être compatible avec le bus de communication que l'on veut
  71. mettre en place.
  72. Les sockets unix sont alors bien la solution de communication inter-processus
  73. correspondant parfaitement au problème. Ils sont gérés efficacement, peuvent
  74. être anonymes et donc parfaitement contrôlés et sont représentés dans le
  75. programme par des descripteurs de fichiers, permettant ainsi de les transmettre
  76. entre processus.
  77. \subsection{Envoi de données}
  78. Le mécanisme de données auxiliaires mentionné plus haut est générique pour
  79. toutes les classes de sockets. En fait, on peut s'intéresser aux structures
  80. utilisées lors de l'envoi d'un message.
  81. \begin{code}{c}{Structure \inltype{msghdr} utilisée dans
  82. \inltype{sendmsg}/\inltype{recvmsg}}
  83. /* from man 2 send */
  84. struct msghdr {
  85. void *msg_name; /* optional address */
  86. socklen_t msg_namelen; /* size of address */
  87. struct iovec *msg_iov; /* scatter/gather array */
  88. size_t msg_iovlen; /* # elements in msg_iov */
  89. void *msg_control; /* ancillary data, see below */
  90. size_t msg_controllen; /* ancillary data buffer len */
  91. int msg_flags; /* flags (unused) */
  92. };
  93. \end{code}
  94. % TODO reformuler
  95. Il y a en réalité deux types de buffers envoyés via cette structure. Le premier
  96. type concerne \inltype{msg_iov} qui est en réalité un tableau de
  97. \inltype{iovec}. Chaque \inltype{iovec} contient un pointeur vers un buffer et
  98. une taille. Ces \inltype{iovec} représentent les données qui seront envoyées à
  99. travers le socket. Le fait que \inltype{msg_iov} soit un tableau de ces buffers
  100. permet d'éviter de copier un paquet morcelé côté utilisateur avant de le
  101. recopier du côté du noyau.
  102. L'autre tampon mémoire est en réalité dédié à la pile logicielle manipulant le
  103. socket dans le noyau. Il s'agit d'un buffer de configuration, ou pour reprendre
  104. le vocabulaire utilisé dans la structure, un buffer de contrôle de l'interface
  105. bas niveau. Il peut être utilisé pour donner des paramètres dépendants de chaque
  106. type de socket. Pour les sockets unix, cela va permettre de manipuler
  107. l'interface SCM (socket control message) qui est capable de copier les
  108. descripteurs de fichier.
  109. \begin{code}{c}{Structure \inltype{cmsghdr} représentant un élément auxiliaire}
  110. /* from man 3 cmsg */
  111. struct cmsghdr {
  112. size_t cmsg_len; /* Data byte count, including header
  113. (type is socklen_t in POSIX) */
  114. int cmsg_level; /* Originating protocol */
  115. int cmsg_type; /* Protocol-specific type */
  116. /* followed by
  117. unsigned char cmsg_data[]; */
  118. };
  119. \end{code}
  120. \subsection{Manipulation des données auxiliaires}
  121. La manipulation de ce buffer est un peu particulière. Il s'agit en fait
  122. d'allouer des structures de données au seins du buffer même, tout en respectant
  123. les contraintes demandées par le système. En pratique, l'API POSIX fournit les
  124. macros suivantes pour manipuler ce buffer.
  125. \begin{itemize}
  126. \item \inltype{CMSG_FIRSTHDR} retourne un pointeur vers le premier
  127. \inltype{cmsghdr} dans le tampon de données auxiliaires associé au
  128. \inltype{msghdr} passé en paramètre.
  129. \item \inltype{CMSG_NXTHDR} retourne le prochain \inltype{cmsghdr} après
  130. celui passé en paramètre, ou \inltype{NULL} si le tampon est plein.
  131. \item \inltype{CMSG_SPACE} retourne la taille occupée par un élément
  132. auxiliaire.
  133. \item \inltype{CMSG_DATA} retourne un pointeur vers la partie donnée d'un
  134. \inltype{cmsghdr}.
  135. \item \inltype{CMSG_LEN} retourne la valeur à stocker dans le champ
  136. \inltype{cmsg_len} en comptant l'alignement, à partir de la taille des
  137. données.
  138. \end{itemize}
  139. Deux problématiques ont dû être prises en compte lors de l'utilisation de cette
  140. API. D'un côté, il faut faire attention aux accès non alignés. De l'autre, il
  141. faut respecter les règles de strict aliasing, c'est-à-dire qu'il ne doit pas
  142. exister deux pointeurs de types incompatibles vers le même espace mémoire. Tout
  143. cela va se résumer à correctement suivre les recommandations de l'API tout en
  144. utilisant \inltype{memcpy} pour copier les données depuis et vers le tampon
  145. mémoire.
  146. \begin{code}{c}{Implémentation du traitement des données auxiliaires}
  147. static int
  148. ipc_internal_SendMsg( int socket_fd, unsigned nb_buffer,
  149. struct process_msg_internal_t *buffers[],
  150. unsigned nb_fd, int* fds)
  151. {
  152. /* On utilise une union pour que la contrainte d'alignement du type cmsghdr
  153. * s'applique au tampon buf */
  154. union {
  155. char buf[512];
  156. struct cmsghdr align;
  157. } ctlbuf;
  158. \end{code}
  159. Les tampons mémoires sont convertis de la représentation de l'application vers
  160. celle de l'API Linux.
  161. \begin{code}{c}{Conversion des tampons et initialisation du msghdr}
  162. struct iovec iov[nb_buffer];
  163. for (unsigned i=0; i<nb_buffer; ++i) {
  164. iov[i].iov_base = buffers[i]->buffer;
  165. iov[i].iov_len = buffers[i]->length;
  166. }
  167. struct msghdr msgh = {
  168. .msg_name = NULL,
  169. .msg_namelen = 0,
  170. .msg_iov = iov,
  171. .msg_iovlen = nb_buffer,
  172. .msg_control = ctlbuf.buf,
  173. .msg_controllen = sizeof( ctlbuf ),
  174. .msg_flags = 0
  175. };
  176. \end{code}
  177. Puis les descripteurs de fichier sont copiés dans la structure.
  178. \inltype{SCM_RIGHTS} permet d'indiquer ici que ce sont bien des descripteurs de
  179. fichier et qu'il va falloir les dupliquer dans le processus qui reçoit le
  180. paquet.
  181. \begin{code}{c}{Création des données auxiliaires}
  182. if( nb_fd > 0 )
  183. {
  184. struct cmsghdr *cmsg = CMSG_FIRSTHDR( &msgh );
  185. cmsg->cmsg_level = SOL_SOCKET;
  186. cmsg->cmsg_type = SCM_RIGHTS;
  187. cmsg->cmsg_len = CMSG_LEN( nb_fd * sizeof( int ) );
  188. void *p_data = CMSG_DATA( cmsg );
  189. memcpy( p_data, fds, nb_fd * sizeof( int ) );
  190. msgh.msg_controllen = cmsg->cmsg_len;
  191. }
  192. else
  193. {
  194. msgh.msg_controllen = 0;
  195. msgh.msg_control = NULL;
  196. }
  197. \end{code}
  198. Enfin le message est envoyé. On utilise \inltype{MSG_EOR} pour pouvoir signaler
  199. la fin du message, comme il s'agit de \inltype{SOCK_SEQPACKET} mais d'autres
  200. solutions sont possibles.
  201. \begin{code}{c}{Envoi du message}
  202. ssize_t i_size = sendmsg( socket_fd, &msgh, MSG_EOR );
  203. \end{code}
  204. % if( i_size == -1 )
  205. % {
  206. % fprintf( stderr, "Couldn't send message, error at sendmsg: %d\n", errno);
  207. % return VLC_EGENERIC;
  208. % }
  209. % else if( i_size == 0 )
  210. % {
  211. % // Déconnexion
  212. % return VLC_ENOOBJ;
  213. % }
  214. %
  215. % return VLC_SUCCESS;
  216. % }
  217. % TODO développer problématiques
  218. Pour la réception, la méthode est similaire, à l'exception de l'apparition de
  219. \inltype{CMSG_LEN(0)} pour remplacer \inltype{CSMG_ALIGN} qui n'existe que sous
  220. Linux:
  221. \begin{code}{c}{Réception des descripteurs de fichier}
  222. struct cmsghdr *cmsg = CMSG_FIRSTHDR( &msgh );
  223. while( cmsg != NULL )
  224. {
  225. if( cmsg->cmsg_level == SOL_SOCKET
  226. && cmsg->cmsg_type == SCM_RIGHTS )
  227. {
  228. // TODO: check len, it takes headers into account
  229. // TODO: change assert in error handling
  230. // TODO: alloc msg->fds
  231. size_t len = cmsg->cmsg_len - CMSG_LEN(0);
  232. assert( len >= sizeof( int ) );
  233. assert( len % sizeof( int ) == 0 );
  234. len /= sizeof( int );
  235. ARRAY_INIT( (*msg)->fds );
  236. for( int i = 0; i < len; ++i )
  237. {
  238. int fd;
  239. memcpy( &fd, CMSG_DATA( cmsg ) + i * sizeof( int ),
  240. sizeof( int ));
  241. ARRAY_APPEND( (*msg)->fds, fd );
  242. (*msg)->i_nb_fds++;
  243. }
  244. }
  245. cmsg = CMSG_NXTHDR( &msgh, cmsg );
  246. }
  247. \end{code}
  248. Finalement, nous avons ainsi conçu un moyen de construire des jetons
  249. inforgeables pouvant soit transmettre d'autres jetons, soit servir de RPC vers
  250. un accès particulier.