windows_eventloop.tex 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. \section{Gestion de la boucle événementielle}
  2. Comme sous Linux, il faut implémenter une boucle événementielle qui va récupérer
  3. les messages venant de tous les autres modules. Dans ce chapitre je vais
  4. détailler les solutions disponibles sous Windows et proposer une implémentation
  5. utilisant les IOCP, ou IO completion port.
  6. \section{Mécanisme de gestion asynchrone}
  7. Windows disposent aussi de beaucoup de primitive différentes pour la gestion de
  8. plusieurs descripteurs de fichier.
  9. \subsection{poll, select}
  10. Comme dans la partie précédente, on dispose d'une fonction \inltype{select}
  11. capable d'indiquer l'état de chacun des sockets décrits dans un tableau.
  12. % TODO: https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-select
  13. \begin{code}{c}{Prototype de la fonction select}
  14. int WSAAPI select(
  15. int nfds,
  16. fd_set *readfds,
  17. fd_set *writefds,
  18. fd_set *exceptfds,
  19. const timeval *timeout
  20. );
  21. \end{code}
  22. Malheureusement celle-ci ne peut être utilisé que sur des sockets venant de
  23. l'API winsock. Il nous sera donc impossible de l'utiliser pour construire la
  24. boucle événementielle sans changer d'IPC.
  25. De même, \inltype{poll} n'existe pas comme sous Linux et il s'agit d'une
  26. fonction winsock.
  27. \begin{code}{c}{Prototype de la fonction WSAPoll}
  28. int WSAAPI WSAPoll(
  29. LPWSAPOLLFD fdArray,
  30. ULONG fds,
  31. INT timeout
  32. );
  33. \end{code}
  34. Par conséquent, ces deux choix sont définitivement éliminé pour la construction
  35. avec les canaux nommés.
  36. \subsection{WaitForMultipleObject}
  37. Pour obtenir le même effet que les fonctions sous Linux, sur des
  38. \inltype{HANDLE} de façon générique, Windows propose une famille de fonctions de
  39. la forme \inltype{WaitFor*}.
  40. D'abord, \inltype{WaitForSingleObject} attend que le \inltype{HANDLE} soit
  41. signalé, éventuellement en prenant en compte un ultimatum.
  42. \begin{code}{c}{Prototype de WaitForSingleObject sur MSDN}
  43. DWORD WaitForSingleObject(
  44. HANDLE hHandle,
  45. DWORD dwMilliseconds
  46. );
  47. \end{code}
  48. Cependant, cela correspond peu à notre cas d'usage et sera plutôt utiliser pour
  49. les manipulations de processus. On peut donc s'intéresser à la variante nous
  50. concernant, qui sans surprise s'appelle \inltype{WaitForMultipleObjects}.
  51. %https://docs.microsoft.com/en-us/windows/desktop/api/synchapi/nf-synchapi-waitformultipleobjects
  52. \begin{code}{c}{Prototype de la fonction WaitForMultipleObjects sur MSDN}
  53. DWORD WaitForMultipleObjects(
  54. DWORD nCount,
  55. CONST HANDLE *lpHandles,
  56. BOOL bWaitAll,
  57. DWORD dwMilliseconds
  58. );
  59. \end{code}
  60. Cette fonction va retourner soit un code d'erreur, soit une constante
  61. correspondant à l'objet signalé, allant de \inltype{WAIT_OBJECT_0} à
  62. \inltype{WAIT_OBJECT_0 + nCount-1}.
  63. Elle ne peut surveiller plus de \inltype{MAXIMUM_WAIT_OBJECTS} en même temps, ce
  64. qui en pratique se limite à 64 objets. Loin d'être bloquant pour notre projet,
  65. on va plutôt s'intéresser aux performances. \inltype{WaitForMultipleObjects}
  66. fonctionne exactement comme \inltype{select} et fournit donc une complexité
  67. linéaire en le nombre de \inltype{HANDLE} surveillés. Néanmoins, la valeur de
  68. retour n'est capable d'indiquer seulement qu'un \inltype{HANDLE} notifié et il
  69. faut donc parcourir à nouveau le tableau pour trouver les autres
  70. \inltype{HANDLE}.
  71. \subsection{Overlapped IO}
  72. Windows fournit ensuite un mécanisme bas niveau pour mettre les opérations d'IO
  73. en «tâche de fond». Ci-dessous, on voit dans les prototypes des fonctions de
  74. lecture et écriture
  75. %TODO phrase pas fini au dessus
  76. %TODO https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-writefile
  77. \begin{code}{c}{Prototype de WriteFile sur MSDN}
  78. BOOL WriteFile(
  79. HANDLE hFile,
  80. LPCVOID lpBuffer,
  81. DWORD nNumberOfBytesToWrite,
  82. LPDWORD lpNumberOfBytesWritten,
  83. LPOVERLAPPED lpOverlapped
  84. );
  85. \end{code}
  86. %TODO https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-readfile
  87. \begin{code}{c}{Prototype de ReadFile sur MSDN}
  88. BOOL ReadFile(
  89. HANDLE hFile,
  90. __out_data_source(FILE)LPVOID lpBuffer,
  91. DWORD nNumberOfBytesToRead,
  92. LPDWORD lpNumberOfBytesRead,
  93. LPOVERLAPPED lpOverlapped
  94. );
  95. \end{code}
  96. Dans ces deux fonctions, si \inltype{hFile} a été ouvert avec l'option
  97. \inltype{FILE_FLAG_OVERLAPPED}, le paramètre \inltype{lpOverlapped} doit être
  98. précisé et pointer vers une structure initialisée de type \inltype{OVERLAPPED}.
  99. %TODO: mieux expliquer
  100. La structure \inltype{OVERLAPPED} peut ensuite être utilisée soit avec
  101. \inltype{WaitForSingleObject}, soit avec \inltype{WaitForMultipleObjects} et
  102. fournit ainsi un moyen de passer d'un modèle par disponibilité à un modèle par
  103. complétion.
  104. Il est possible d'étendre encore le modèle asynchrone en utilisant des
  105. «completion routines». Il faut alors utiliser la version suivante:
  106. %TODO: https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-readfileex
  107. \begin{code}{c}{Prototype de la fonction ReadFileEx sur MSDN}
  108. BOOL ReadFileEx(
  109. HANDLE hFile,
  110. __out_data_source(FILE)LPVOID lpBuffer,
  111. DWORD nNumberOfBytesToRead,
  112. LPOVERLAPPED lpOverlapped,
  113. LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
  114. );
  115. \end{code}
  116. Le paramètre \inltype{lpCompletionRoutine} est un pointeur de fonction du type
  117. suivant:
  118. %TODO: https://msdn.microsoft.com/en-us/574eccda-03eb-4e8a-9d74-cfaecc7312ce
  119. \begin{code}{c}{Définition de LPOVERLAPPED\_COMPLETION\_ROUTINE sur MSDN}
  120. typedef VOID (WINAPI *LPOVERLAPPED_COMPLETION_ROUTINE)(
  121. _In_ DWORD dwErrorCode,
  122. _In_ DWORD dwNumberOfBytesTransfered,
  123. _Inout_ LPOVERLAPPED lpOverlapped
  124. );
  125. \end{code}
  126. La routine de complétion sera alors appelée dès que l'opération sera terminée et
  127. que le thread sera dans un état alertable, c'est-à-dire se signalera disponible.
  128. \subsection{IO completion ports}
  129. Les IO completion ports, ou IOCP pour la suite, permettent d'implémenter un
  130. système similaire à epoll, dans un modèle par complétion, en étendant les
  131. \inltype{OVERLAPPED} IO vues précédemment. L'API n'est cependant pas très claire
  132. sur beaucoup de points donc elle sera plus longuement décrite ici pour apporter
  133. des explications dans ce rapport.
  134. Un IOCP peut être créé avec la fonction \inltype{CreateIoCompletionPort} en
  135. passant \inltype{INVALID_HANDLE_VALUE} comme \inltype{FileHandle} et
  136. \inltype{NULL} comme \inltype{ExistingCompletionPort}.
  137. % TODO: https://docs.microsoft.com/en-us/windows/desktop/fileio/createiocompletionport
  138. \begin{code}{c}{Prototype de CreateIoCompletionPort sur MSDN}
  139. HANDLE WINAPI CreateIoCompletionPort(
  140. _In_ HANDLE FileHandle,
  141. _In_opt_ HANDLE ExistingCompletionPort,
  142. _In_ ULONG_PTR CompletionKey,
  143. _In_ DWORD NumberOfConcurrentThreads
  144. );
  145. \end{code}
  146. Le paramètre \inltype{CompletionKey} est ignoré dans ce cas, et le paramètre
  147. \inltype{NumberOfConcurrentThreads} permet d'indiquer à l'IOCP combien de thread
  148. vont pouvoir simultanément recevoir les notifications de nouveaux messages. Ce
  149. paramètre ne démarrre pas de nouveaux threads mais sert seulement à
  150. l'ordonnancement dans le cas où plusieurs threads demanderaient des
  151. notifications au port. Dans ce dernier cas, chacun des threads recevra des
  152. notifications pour des opérations différentes, mais cela ne nous concerne pas
  153. ici.
  154. Nous pouvons ensuite ajouter des opérations en utilisant la même fonction, mais
  155. en renseignant \inltype{FileHandle} comme étant l'objet sur lequel l'opération a
  156. été effectuée, \inltype{ExistingCompletionPort} comme étant le \inltype{HANDLE}
  157. créé dans l'appel précédent et \inltype{CompletionKey} comme étant une valeur
  158. arbitraire qui nous servira à retrouver l'opération plus tard.
  159. Il est possible de créer un port tout en associant un \inltype{HANDLE} à la
  160. construction, mais le cas ne nous intéresse pas pour l'implémentation de
  161. \inltype{vlc_msg_poller_t}.
  162. Une fois les opérations associées au port, on peut utiliser
  163. \inltype{GetQueuedCompletionStatus} pour récupérer un paquet de complétion.
  164. % TODO: https://msdn.microsoft.com/en-us/library/Aa364986(v=VS.85).aspx
  165. \begin{code}{c}{Prototype de GetQueuedCompletionStatus sur MSDN}
  166. BOOL WINAPI GetQueuedCompletionStatus(
  167. _In_ HANDLE CompletionPort,
  168. _Out_ LPDWORD lpNumberOfBytes,
  169. _Out_ PULONG_PTR lpCompletionKey,
  170. _Out_ LPOVERLAPPED *lpOverlapped,
  171. _In_ DWORD dwMilliseconds
  172. );
  173. \end{code}
  174. Cette fonction attendra la fin d'une opération en cours, et plus exactement
  175. l'arrivée d'un paquet de complétion, pour retourner, mais permettra de récupérer
  176. la structure \inltype{OVERLAPPED} contenant les données et informations liées à
  177. l'opération effectuée.
  178. Enfin, il sera possible de poster une notification sans effectuer d'opération en
  179. appelant \inltype{PostQueuedCompletionStatus}, afin de débloquer la boucle
  180. événementielle par exemple.
  181. % TODO: https://docs.microsoft.com/en-us/windows/desktop/FileIO/postqueuedcompletionstatus
  182. \begin{code}{c}{Prototype de PostQueuedCompletionStatus}
  183. BOOL WINAPI PostQueuedCompletionStatus(
  184. _In_ HANDLE CompletionPort,
  185. _In_ DWORD dwNumberOfBytesTransferred,
  186. _In_ ULONG_PTR dwCompletionKey,
  187. _In_opt_ LPOVERLAPPED lpOverlapped
  188. );
  189. \end{code}
  190. \section{Implémentation et conclusions pour la boucle événementielle}
  191. Ayant déjà décrit \inltype{vlc_msg_poller_t} pour la partie Linux et
  192. extensivement expliqué le fonctionnement de l'API d'IOCP, il reste
  193. principalement à décrire l'implémentation du modèle par complétion dans la
  194. situation d'attente de message.
  195. Contrairement à Linux, les IOCP ne vont pas signaler quand un des
  196. \inltype{HANDLE} sera prêt à être lu mais plutôt quand les données auront
  197. effectivement été lues. Il faut donc allouer un tampon par requête de lecture.
  198. La méthodologie sera alors de dédier un tampon par \inltype{HANDLE} puis
  199. d'effectuer toutes les opérations de lecture. Lorsqu'une des opérations est
  200. considérée comme terminée, on pourra traiter le message puis relancer
  201. l'opération de lecture.
  202. % TODO: snippet de code
  203. En résumé, la gestion des entrées sorties et des erreurs proposées par Windows
  204. ne respecte absolument pas le principe de moindre surprise, mais les différentes
  205. alternatives proposées par le système permettent de construire le modèle par
  206. complétion que nous avons besoin de façon performante.