windows_eventloop.tex 10 KB

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