Programmation multitâches Sous Windows NT – 2000 – XP Avec l
Transcription
Programmation multitâches Sous Windows NT – 2000 – XP Avec l
Programmation multitâches Sous Windows NT – 2000 – XP Avec l’API Win32 Win32 multitâches - T. Joubert © THEORIS 2005 1 Win32 multitâches Windows XP Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 2 2 Symbolisme utilisé: Ce document est composé de la copie réduite des transparents présentés complétée par des annotations sur une ou plusieurs pages. La page de gauche est laissée blanche pour les notes personnelles. Ces annotations restituent le contexte, précisent la syntaxe précise des primitives présentées, et complètent la présentation de la primitive par des particularités d’utilisations, des mises en garde... La description des primitives est entourée par un cadre, elle provient de l’aide en ligne du compilateur C (donc en anglais). Le symbole ‘‘ attire l’attention sur certaines informations spécifiques comme les restrictions d’emploi entre Windows NT et 95; des exceptions notables... Le symbole ‘‘ attire l’attention sur certains aspects pouvant conduire à un mauvais fonctionnement. Win32 multitâches - T. Joubert © THEORIS 2005 3 Plan du cours PREMIERE PARTIE – Win32 1) Introduction aux systèmes Windows 2) Multitâches Ordonnancement Processus & Threads TP multitâches 3) Synchronisation Objets de synchronisation Handles et objets kernel TP synchronisation multitâches Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 4 4 Plan du cours 4) Gestion structurée des exceptions 5) Mémoire • Mémoire virtuelle / Mémoire physique • Heap Win32 TP mémoire & Exceptions 6) E/S, mécanismes de communication • E/S synchrones/asynchrones, pipes, mailslot • Fichiers «mappés» en mémoire TP fichiers mappés Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 5 5 Plan du cours 7) Gestion du temps, horlogerie Timers Accès à l’heure système TP mise en place de timers 8) Librairies dynamiques TP Librairie dynamique 9) Windows Sockets TP sockets TCP et UDP TP Serveur HTTP multi-thread Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 6 6 Plan du cours SECONDE PARTIE – COM/DCOM 10) Le middleware COM • Objectif et mécanismes • Langages et outils • Notion de CLSID & Registry • MIDL et interfaces • Multi-threading et COM • Distribution et DCOM • La bibliothèque ATL TP composant COM Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 7 7 Chapitre 1 Introduction aux systèmes Windows Win32 multitâches - T. Joubert © THEORIS 2005 8 Plan du chapitre Historique Windows Les systèmes d'exploitation Microsoft L'architecture Windows XP Le standard Unicode Windows 64bits L’API Win32 Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 9 9 Les OS Microsoft PC-DOS MS-DOS 2.11 1980 MS-DOS 3.1 Windows 1.01 1985 MS-DOS 3.31 MS-DOS 5.0 1990 Windows 2.0 NT Windows 3.0 Windows 3.1 MS-DOS 6.22 Windows NT 3.1 1995 Windows 95 CE 1.0 Windows NT 4.0 Windows 98 NT Embedded CE 3.0 2000 Windows Me Windows 2000 Windows XP 2005 XP-SP2 Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 CE 2.0 XP Embedded CE .NET CE 5.0 10 10 XP = Une harmonisation … Unification de la famille Windows pour x86 La lignée des 9x s’éteint enfin et avec elle : Un système 16 bits Une absence de sécurité Une instabilité chronique Une seule structure de noyau pour Windows et donc : La même gestion mémoire et les mêmes API Les mêmes pilotes Les mêmes procédures d’installation Le même outil de développement L’embarqué simplifié XP Embedded n’est qu’un sous ensemble choisi de XP Professional CE .net reste à part : Temps Réel Supporte d’autres processeurs (ARM, SH3, MIPS) Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 11 11 Objectifs de NT Application OS/2 Application Win32 Application POSIX sources Matériel 64 bits x86 CISC MIPS Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 RISC SMP Alpha APIC PowerPC 12 12 Choix d’architecture NT Application OS/2 Application POSIX POSIX subsystem OS/2 subsystem Application Win32 Win32 subsystem System Services NT kernel Hardware Abstraction Layer Win32 XP - © Theoris 2005 13 L’architecture de Windows NT trouve son origine dans le système Mach qui est un UNIX en mode client serveur, elle s’oppose radicalement à celle du couple DOS-Windows 3.X car elle est naturellement extensible à ses deux extrémités : Coté matériel avec la couche HAL (abstraction du matériel) Coté environnement avec les sous systèmes protégés Globalement Windows NT apporte les caractéristiques suivantes : Multi-environnement (POSIX, OS/2, Win32 ...) Multi-plate-forme (Intel, Alpha...) Préemptif Applications multi-thread Système de fichiers NTFS et FAT (noms de fichiers longs) Full 32 bits Multi-processeur Sécurisé (user et applications) Serveur de domaine (NT server) Pilotes chargeables La notion de micro-noyau permet d’implémenter le niveau services de base du système d’exploitation (processeur, mémoire, entrées/sorties), ce micro-noyau joue en plus le rôle de relais entre tout ce qui tourne au dessus de lui (applications et sous-systèmes protégés) suivant un modèle client-serveur. La notion de sous système protégé, outre l’extensibilité, offre un bon niveau de protection générale dans la mesure où les éléments applicatifs sont indépendants les uns des autres. Win32 multitâches - T. Joubert © THEORIS 2005 13 Architecture XP Application Win32 Application Win32 DLLs Win32 DLLs Win32 Sous-Système Win32 user kernel Services du système Window Manager et GDI Gestion Gestion d'objets Sécurité Processus Graphic Drivers LPC Gestion mémoire virtuelle I/O manager Drivers micro-noyau Couche d’abstraction du matériel (HAL) Matériel Win32 XP - © Theoris 2005 14 Le noyau de Windows NT tourne en mode kernel (sur Intel: ring 0) alors que le reste du code tourne en mode user (sur Intel: ring 3), c’est à dire que le thread qui fait un appel système est commuté dans ce mode du processeur pour le temps d’exécution du code noyau. L’architecture du noyau combine modèle en couche et modèle client serveur, la couche basse dite ‘Noyau’ implémente les services élémentaires (et essentiels) : • Ordonnancement • Transfert du contrôle CPU pour les interruptions • Synchronisation multi-CPU Sur cette couche sont bâtis un certain nombre de modules spécialisés qui interagissent sur un modèle client-serveur : • Le gestionnaire d’objets système qui est au centre de la bataille car toute entité NT (processus, thread, mémoire etc.) est au départ un objet système. • Le gestionnaire de sécurité • Le gestionnaire de processus • Le gestionnaire d’appel de procédure locale (LPC) qui permet d’améliorer les performances du modèle client-serveur. • Le gestionnaire de mémoire virtuelle • La couche de gestion des services du système qui assure l’interface avec les applications et sous systèmes protégés. Le contrôleur d’entrées sorties constitue la structure d’accueil pour les pilotes d’entréesortie. On peut d’ailleurs noter que ces pilotes sont chargeables dynamiquement en cours de session NT, à condition qu’il aient été préalablement déclarés dans la ‘registry’ Pour des raisons de performances, le Window Manager et le GDI sont ‘ descendus ’ au niveau kernel sous NT 4.0. Win32 multitâches - T. Joubert © THEORIS 2005 14 Mode “Kernel” & Mode “User” Deux Kernel (OS) Mode CPU privilégié Accès aux données du système et au Hardware User (Applications) Mode CPU non-privilégié Pas d’accès au hardware et aux données système Les modes gérés par Windows XP applications sont séparées de l’OS Ells ne peuvent pas modifier son intégrité Les composants système et les pilotes peuvent atteindre les donnée du système Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 15 15 Vue générale XP Services System processes Logon Alerter Event logger Session manager LPC I/O system File systems Win32 Subsystem DLL Executive API Win32 Object user/GDI services LPC Virtual memory Processes and threads Object management/Exec. RTL Device drivers I/O devices SFU User application LPC Security monitor Environment subsystems Applications DMA control Hardware Abstraction Layer (HAL) Platform interface Bus mapping Clocks/ timers Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 Cache control Interrupt dispatch Registry Kernel Privileged architecture 16 16 XP, Win32, COM & .NET Application Graphique VB7 / C# / C++ Framework .NET CLR Application Graphique C++ ActiveX Composant métier C / C++ COM - DCOM RPC - ORPC Application Multithread C / C++ API système (Win32, winsock…) XP Kernel Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 17 17 Le standard Unicode ASCII Limitation du standard ASCII à 256 symboles. Le standard Unicode utilise deux octets: symboles & dingbats symboles et idéogrammes chinois coréen, japonais. utilisateur 0 zone de compatibilité 65535 Windows NT utilise exclusivement unicode pour ses types propres Toutes les chaînes de caractères échangées avec le système devront être Unicode Win32 XP - © Theoris 2005 18 Le mélange de différents jeux de caractères dans les documents ou, de manière plus générale par l’interface utilisateur, impose des systèmes de codage complémentaire aux 8 bits de l’ASCII, ce qui a pour conséquence de rendre le résultat sensible à la configuration de la machine (problème de portabilité des documents). Pour résoudre ce problème et pour rendre Windows NT totalement international Microsoft a opté pour une solution 100% unicode, c’est à dire que toutes les chaînes manipulées dans le cadre des activités système (noms d’objets, de fichiers ...) sont représentées au format unicode. Les API des sous systèmes NT contiennent deux jeux de fonctions : ASCII et unicode mais lorsqu’une chaîne au format ASCII (char*) leur est passée elles effectuent une traduction ASCII-unicode à l’aller et unicode-ASCII au retour. Il faudra donc tenir compte de cette caractéristique pour les performances d’une application qui effectuerait un nombre important d’échanges de caractères avec son sous-système. Windows CE ne gère pas l ’ASCII (supporte uniquement UNICODE). Win32 multitâches - T. Joubert © THEORIS 2005 18 Unicode & langage C Type compatible Unicode wchar_t Fonctions compatibles Unicode wcs…, _wcs… Chaines constantes L“je suis Unicode” “je suis ASCII” Fonctions génériques #define _UNICODE #include <tchar.h> #include <wchar.h> _tcs… Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 19 19 Unicode & Windows Types génériques #ifdef UNICODE typedef wchar_t TCHAR; #else typedef unsigned char TCHAR #endif TCHAR, LPTSTR, LPTCH Taille des chaînes cByteCount = (lpEnd-lpStart) * sizeof(TCHAR); La macro TEXT( ) pour les constantes while(*lpFileName++ != TEXT( '\\‘ ) ) Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 20 20 Unicode & API win32 Version générique BOOL SetWindowText( HWND hwnd, LPCTSTR lpText ); Version ANSI BOOL SetWindowTextA(HWND hwnd, LPCSTR lpText ); Version UNICODE BOOL SetWindowTextW(HWND hwnd, LPCWSTR lpText ); Géré par le préprocesseur #ifdef UNICODE #define SetWindowText SetWindowTextW #else #define SetWindowText SetWindowTextA #endif Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 21 21 L’API Win32 Des milliers de fonctions, de messages et de codes Win32s, Win32c, etc. Née avec Windows NT Win32s sous Windows 3.11 Pas de réelles versions Un périmètre un peu flou Des évolutions à chaque version AVANT #include <windows.h> : #define _WIN32_WINNT 0x0510 Win32 CE Win32 9x Win32 NT4.0 Win32 NT5.0 Win32 NT5.1 Win32 XP - © Theoris 2005 22 L’API Win32 est une interface de programmation (Application Programming Interface). Cette API est fondée sur le développement d’applications 32 bits, elle permet en particulier d ’ignorer la segmentation des processeurs Intel. Cette API est composée de milliers de fonctions, sans compter les codes de retour, messages, types ... Une application Win32 doit pouvoir fonctionner sur de nombreuses plates-formes et systèmes avec ou sans recompilation (si l’on change de micro en particulier). En réalité, il existe plusieurs «Win32»: • Win32s est la version de Win32 pour le système Windows 3.x 16 bits. Beaucoup d’appels ne sont pas implémentés et retournent un code d’erreur (exemple le multitâche). • Windows NT a été le premier système apportant une implémentation complète de Win32. On a une bonne portabilité entre les différentes plates-formes supportant Windows NT. •La version Win32 de NT 4.0 n ’est pas une implémentation complète de Win32 «95», il faudra attendre encore la version suivante NT 5.0 qui apportera de nouvelles fonctionnalités (Active Directory, …). • Chaque version s’éloigne un peu de ce monde unifié idéal; il n’y a pas une Win32 mais plusieurs. Notons que les CD Win32 donnent pour chaque fonction ou structure, la portabilité sur les différentes plates-formes Win32. Win32 multitâches - T. Joubert © THEORIS 2005 22 Contenu de l’API Win32 mswsock winmm ws2_32 kernel32 odbccp32 odbc32 oleaut32 ole32 user32 shell32 gdi32 advapi32 comdlg32 winspool Win32 XP - © Theoris 2005 23 La définition du périmètre fonctionnel exact de l’API Win32 est une opération qui tient de l’enquête policière, on peut explorer plusieurs pistes : N°1 La section Platform SDK du MSDN fournit une liste alphabétique qui nous donne 2077 fonctions, c’est une bonne base mais on voit que ces fonctions proviennent de plusieurs DLLs. N°2 La liste des DLL du répertoire system32 qui ont une librairie d’export dans Visual C++ définit un périmètre fonctionnel assez exhaustif. Dans ce cas, la masse des fonctions exportées est énorme (>10 000). N°3 L’environnement Visual C++ définit la notion d’application console Win32, la liste des librairies de liens pour une telle cible est un bon indice. On peut ensuite extraire les tables d’export des DLLs correspondantes. N°4 pour les développeurs VB désireux d’utiliser les fonctions Win32, Visual Studio fournit le fichier WIN32API.TXT qui est une mine d’informations sur l’API win32, mais elle ne donne (que) 1543 fonctions. La piste N°3 à l’avantage de fournir une réalité palpable pour le développeur, elle nous donne une liste de 11 librairies pour la win32 « de base ». En analysant la table d’export des DLLs on obtient 3202 fonctions. On peut noter au passage que l’infrastructure de composants COM/DCOM, ainsi que l’accès ODBC sont totalement intégrés au système. Les approches N° 1 et N° 3 nous mettent sur la piste d’autres DLLs importantes qui viennent étendre les ressources système, en pariculier WINSOCK et WINMM qui totalisent 293 fonctions. Soit un total de 3495 fonctions pour 14 DLLs. Win32 multitâches - T. Joubert © THEORIS 2005 23 Le tableau ci-dessous donne le nombre de fonctions de chacun des 14 DLLs « récoltées » lors de l’enquête (sur 88 ayant une librarie d’export). Les tables d’exports contiennent en réalité plus de fonctions que le chiffre fourni mais certaines fonctions sont en deux exemplaires (funcA et funcW), et d’autres fonctions ne sont pas nommées (uniquement un ordinal). DLL fonctions Usage 651 Gestion des ressources système kernel32 554 Gestion fenêtrage user32 493 Graphismes, fonts… gdi32 109 Gestion impression winspool 15 Outillage standard (contrôles) comdlg32 411 Accès système NT (registry, log…) advapi32 113 Shell (DragDrop, exec, icons) shell32 301 Runime COM ole32 356 Runime COM (suite) oleaut32 143 API ODBC odbc32 56 API ODBC (suite) odbccp32 95 Sockets BSD&2.0 ws2_32 25 Extensions socket 2.0 mswsock 173 Multimédia winmm TOTAL 3495 On trouve ensuite des DLL « Outils » de différents types (réseau, RPC, intenet, SNMP, RAS, MAPI, TAPI, OpenGL, DirectX, setup, cluster, OLE, cryptage …) pour un total de 5800 fonctions. Il ne faut pas oublier non plus les 1200 fonctions des RunTimes C et C++. Cette quantité impressionnante de fonctions (environ 10 000) ne constitue en fait qu’une partie de l’accès aux fonctionnalités du système Windows pour un développeur d’applications. Il manque par exemple les bibliothèque de classes MFC et ATL sans oublier JAVA. Il ne faut pas oublier non plus les fonctionnalités accessibles à travers les interfaces COM (DirectX en fait partie). Win32 multitâches - T. Joubert © THEORIS 2005 24 WindowsXP 64 bits Le logiciel Disponibles : W2K Advanced Server Limited Edition (OEM) Window XP Professional 64-bit Edition A venir : Windows .net Enterprise Server 64-bit Edition Windows .net Datacenter Server 64-bit Edition http://www.microsoft.com/windowsxp/64bit/ Le matériel Disponible : Intel® Itanium™ AMD Opteron (x86-64 Technology) Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 25 25 La mémoire – Adressage 00000000 00000000 00000000 Code : EXE/DLLs XP 32-bit 80000000 Données : Stack des threads, Heap des processus, etc. Code : Ntoskrnl, HAL, pilotes XP 64-bit 000006FC 00000000 Données : piles du noyau 1FFFFF00 00000000 C0000000 Tables FFFFFFFF Cache système, Mémoire paginée et non paginée Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 FFFFFFFF FFFFFFFF 26 26 La mémoire - Limites Pointeurs sur 64 bits XP 32-bit XP 64-bit Processus 2-3 Go 7152 Go (6,9 To) Mémoire physique 64 Go 128 Go Cache système 960 Mo 1024 Go (1 To) Réserve mém. paginée 470 Mo 128 Go Réserve mém. non paginée 256 Mo 128 Go Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 27 27 Compatibilité Nouveaux types de base ILP32 LLP64 Applications 32-bit DWORD_PTR, INT_PTR, UINT_PTR, SIZE_T, etc. Attention au « bit stuffing » dans les structures S’exécutent grâce à Windows On Windows - "Wow64.exe" Plus lentes que sur une machine native 32-bit Certains appels de la Win32 sont non supportés Interopérabilité Chargement DLL 32-bit dans un processus 64-bit Chargement DLL 64-bit dans un processus 32-bit COM/DCOM entre processus 32-bit/64-bit Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 28 28 Chapitre 2 Processus et Threads Win32 multitâches - T. Joubert © THEORIS 2005 29 Plan du chapitre Objets kernel et handles Ordonnancement Gestion des processus Gestion des threads Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 30 30 Objets kernel et handles Processus 1 Processus 2 héritage copie win32 kernel Noyau NT thread mutex I/O Win32 XP - © Theoris 2005 31 Les objets kernel sont des entités gérés par le système et qui sont très largement utilisés par le sous-système Win32. Le processus est une instance de programme actif. Il n'exécute rien mais il se caractérise entre autres par un espace d’adressage, un code, des données ainsi que des handles. Le processus est un conteneur. Un processus doit détenir un handle pour chaque objet kernel qu’il souhaite utiliser; le handle sert de clé d’accès à un objet un peu comme un identificateur. Du point de vue programmatique, un handle est une valeur sur 32 bits qui définit de manière unique un couple processus-objet, chaque utilisation étant soumise à un contrôle par le noyau. Les valeurs numériques des handles ne peuvent pas être échangées directement entre processus, l ’échange de handle ne peut se faire que dans le cadre de l ’héritage. Les différentes méthodes de gestion de ces handles seront examinées dans un chapitre dédié. Le thread est l’unique élément responsable de l’exécution. A la création d’un processus est associée la création d’un thread principal dédié au main(). Le thread principal peut créer d’autre threads (et ainsi de suite), ces threads sont tous rattachés au processus initiateur. Seuls les threads exécutent du code. L’ordonnanceur (scheduler) est un mécanisme du système qui est responsable de l ’affectation du (ou des) CPU(s) à raison d ’un thread à la fois (par CPU). Win32 multitâches - T. Joubert © THEORIS 2005 31 Ordonnancement Préemptif / coopératif préemptif = pas d’action volontaire de la part du thread coopératif = actions volontaires de la part du thread Windows NT est préemptif Windows 3.1 est coopératif La principale fonction coopérative est GetMessage(). Win32 XP - © Theoris 2005 32 Un système est dit à ordonnancement coopératif s’il demande une participation active de la part du thread: suspension sur un appel système. Le passage d’un thread à un autre se voit dans l’application puisque celle ci fait des appels explicites à certains services du noyau. La fonction GetMessage() permet d’interroger le système pour savoir si un message est arrivé pour l’application courante, ce service est dans les systèmes Windows le service le plus utilisé pour le re-ordonnancement coopératif. Un système est dit à ordonnancement préemptif s’il peut suspendre l’exécution normale (en séquence) d’un thread en dehors de tout appel au système. Le thread n’est pas averti de ce changement, il ne participe en rien. C’est le système qui décide quand le reordonnancement doit intervenir. Deux principales raisons peuvent provoquer ce reordonnancement: • Le thread courant s'exécute depuis un temps pré-établi à l’avance : le quantum de temps. • Un événement (interruption par exemple) est survenu. Windows NT et Windows 95 sont des systèmes préemptifs et coopératifs. Windows 3.11 est seulement coopératif. Win32 multitâches - T. Joubert © THEORIS 2005 32 Coopératif / Préemptif Coopératif (Windows 3.1) Callback1 A B DispatchMessage() Callback2 C Préemptif (NT/95) A Thread1 Quantum de temps Préemption Thread2 C D B DispatchMessage() D Temps Win32 XP - © Theoris 2005 33 Ce schéma montre l’exécution de 2 threads sur une machine mono-processeur, un seul thread peut donc s’exécuter à la fois. La barre indique quel thread est en train de s'exécuter (sous 3.11 la notion de thread n ’existe pas mais il existe une notion de fonction de fenètre qui constitue une unité d ’exécution). Dans un système coopératif, le passage d’un thread à un autre ne peut se faire que lorsque le thread courant effectue un appel «coopératif» au noyau au travers de la primitive GetMessage(...) par exemple. On remarque qu’un thread garde la main longtemps et empêche ainsi les autres threads de s’exécuter. Dans le cas d’un système préemtif, le système interrompt automatiquement un thread qui monopolise le processeur trop longtemps, le laps de temps est appelé «Quantum de temps». On retrouve aussi les appels «coopératifs» dans le système préemptif, ils provoquent simplement un ré-ordonnancement avant la fin du quantum. Win32 multitâches - T. Joubert © THEORIS 2005 33 Priorités des threads kernel 32 niveaux de priorités de 0 priorité minimale à 31 priorité maximale découpés en 2 groupes: groupe 0-15 priorité variable groupe 16-31 priorité fixe (Microsoft parle de «temps réel») Priorités = ordre d’allocation du processeur Le système alloue le(s) processeur(s) au(x) thread(s) prêt(s) en respectant strictement les priorités. En cas d’égalité de priorité, l’allocation se fait à part égale. Win32 XP - © Theoris 2005 34 Les priorités interviennent dans le système comme des paramètres dans sa politique d’allocation du processeur. Windows NT utilise 32 niveaux de priorités (de 0 à 31) décomposés en 2 groupes. L’ordonnanceur utilise la priorité différemment pour ces deux groupes. Le thread le plus prioritaire est élu par l’ordonnanceur (plusieurs threads si multi-processeurs sous Windows NT) aux instants suivants : • A l’occurrence d’un événement modifiant l’état de thread(s): signalisation, mise en attente, time-out ... • A l’expiration du quantum de temps: délai maximal d’exécution accordé à un thread lorsqu’il commence à s’exécuter. Sous Windows NT, la possibilité pour un utilisateur de créer un thread temps réel est définie au niveau de son profil. Win32 multitâches - T. Joubert © THEORIS 2005 34 Ordonnancement (suite) Principes d’ordonnancement ... Les priorités variables le coup de pouce d ’avant plan (NT 4.0) Les priorités temps réel sous Windows NT: accès réservé L’échelle des priorités ... et réalités Des priorités globalement respectées Une réelle préemption Une stratégie qui change avec les versions... Win32 XP - © Theoris 2005 35 Pour les traitements qui s’exécutent avec une priorité variable (0-15), le système met en place une priorité dynamique déterminée à partir de la priorité initiale (priorité de base). C’est cette priorité dynamique qui est utilisée par l’ordonnanceur pour élire le thread à exécuter. On peut lister quelques règles : - la priorité dynamique est supérieure ou égale à la priorité de base, - la priorité dynamique ne peut jamais dépasser 15, - le système accorde un coup de pouce au réveil d’un thread, la valeur de ce coup de pouce dépend de la ressource obtenue, - les priorités temps réel ne font l’objet d’aucune modification par le système. La priorité reste constante. En marge de la priorité dynamique, le système favorise le processus associé à la fenêtre active (avant plan), en lui accordant une tranche de temps qui est un multiple de la tranche de base. Sous Windows NT, ce coup de pouce est paramétrable dans l ’onglet Performances de la boîte Système du Panneau de Configuration, on peut faire varier le facteur de 1 (aucun) a 3 (maximum). Le quantum de temps sous Windows NT 4.0 est de 20 ms. Win32 multitâches - T. Joubert © THEORIS 2005 35 Ordonnancement (fin) % objectif 100 90 80 THREAD PRIO A 70 60 THREAD PRIO B 50 THREAD PRIO C 40 30 THREAD PRIO D (D>C>B>A) 20 10 0 T1 T2 T3 T4 Win32 XP - © Theoris 2005 Temps 36 La préemption est réelle dans le système Windows NT. Ainsi, un thread effectuant une boucle sans fin sans aucun appel au système sera bien suspendu. Les priorités d’exécution sont globalement respectées, ainsi : • 2 threads de même priorité se partagent grosso modo le temps processeur à parts égales. • Dans le groupe des threads à priorité variable, un thread plus prioritaire s’exécute plus souvent qu’un autre moins prioritaire et ceci de façon très significative. Il n’y a cependant aucun réelle proportionnalité entre la priorité et le temps d’exécution. • Dans le groupe des threads à priorité temps réel, l’ordre d’exécution semble être strictement en accord avec la priorité des threads. Les priorités variables ne peuvent pas être utilisées pour la synchronisation: il n’y a pas de garantie absolue qu’un thread de priorité inférieure ne s’exécute pas alors qu’un thread plus prioritaire est prêt. Les priorités temps-réel sont des priorités strictement respectées et donc peuvent être utilisées pour la synchronisation. Le graphique ci-dessus est construit à partir d’un programme permettant de mesurer le temps d’exécution accordé à différents threads de priorités différentes. Afin de permettre une mesure plus précise, le thread s’endort définitivement au bout d’un certain temps. Le schéma représente le % du chemin parcouru jusqu’à l’arrêt définitif du thread. 4 threads sont retenus, du moins prioritaire ou plus prioritaire et le schéma contient 4 points de mesures. Il apparaît très clairement que les priorités sont respectées même si les threads de basse priorité s'exécutent un peu. Il apparaît aussi que le temps alloué n’est pas une fonction linéaire de la priorité mais plutôt une exponentielle. Ce schéma est valable pour tout A<B<C<D<16. Dans le cas de priorités temps réel, le thread le plus prioritaire serait toujours le seul à s’exécuter (jusqu’à atteindre la fin de ses traitements). Win32 multitâches - T. Joubert © THEORIS 2005 36 Processus L’objet kernel «processus» Un espace d’adressage privé et virtuel de 4Go Un exécutable (Exe/Dll) Des handles d’objets kernel Processus NT / Processus Win32, Posix ou OS2 Les différents sous systèmes Win32, Posix, OS2 s’appuient sur les processus natifs NT mais présentent via leur API des processus suivant leur angle de vue. Ex: sous Win32 un processus est identifié par un handle Sous Posix le processus est identifié par un identificateur (id) Win32 XP - © Theoris 2005 37 Le processus est constitué d’un programme exécutable (code & données), d’un espace d’adressage privé de 4Go décomposé en 2*2Go (kernel & utilisateur), de ressources systèmes et de threads (au moins 1). Il est important de souligner qu’une application Win32 ne voit pas réellement un processus NT natif mais sa transposition via l’API Win32. L’application Posix verra elle son processus suivant l’angle Posix... Exemple: Un processus NT natif créé depuis un autre processus n’a aucune relation hiérarchique avec son créateur. Un processus créé via le sous-système Win32 (donc l’API associé) n’aura pas non plus de hiérarchie, un processus créé via l’API Posix sera fils du processus créateur. Win32 multitâches - T. Joubert © THEORIS 2005 37 Applications console/fenêtre Application console Application fenêtrée L’application dispose d’une fenêtre texte (printf, getchar) L’application va créer des fenêtres, la console n’est pas créée automatiquement au lancement de l’application. La création par main ou par WinMain Le point d’entrée de l’application n’a pas le même nom pour une application console (main) ou une application fenêtrée (WinMain). Win32 XP - © Theoris 2005 38 Il existe 2 types d’applications, les applications console et les applications fenêtrées. Le thread principal du processus commence par un appel à la procédure int WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow); // handle of current instance // handle of previous instance // address of command line // show state of window Les deux premiers paramètres sont des handles sur une instance du code chargé dans l’espace d’adressage du processus. L’instance courante est souvent nécessaire pour appeler d’autres procédures de l’API, en particulier pour des appels USER ou GDI. Sous Windows 3.1, l’instance précédente était fréquemment utilisée pour déterminer si l’application est déjà chargée ou s’il s’agit du premier lancement. Sous WIn32 ce paramètre vaut toujours 0. Le minimum sous Windows NT est 0x10000, et sous Windows 95 0x400000. Le troisième paramètre est la ligne de commande du processus. Plusieurs techniques existent pour exploiter cette ligne de commande dont CommandLineToArgvW pour revenir aux argC,argV bien connus. Le dernier paramètre conditionne l’affichage de la fenêtre principale à la création (ouvert, iconifié). Win32 multitâches - T. Joubert © THEORIS 2005 38 Vie et mort d’un processus Win32: «père» et «fils» indépendants CreateProcess ExitProcess et TerminateProcess L’exécutable associé et la ligne de commande Les attributs de création L’environnement Threads terminés, objets libérés ou fermés La «vie» après la mort Le code retour d’un processus, sa signalisation Win32 XP - © Theoris 2005 39 Win32 n ’offre pas les notions de processus père et de processus fils, une fois qu’un processus père a créé le processus fils, ce dernier n ’a pas de référence sur son créateur. La primitive CreateProcess permet de créer un nouveau processus, rappelons que cela entraîne automatiquement la création d’un thread pour ce processus. La primitive CreateProcess doit donc aussi contenir des paramètres concernant ce thread: BOOL CreateProcess( LPCTSTR lpszImageName, LPCTSTR lpszCommandLine, LPSECURITY_ATTRIBUTES lpsaProcess, LPSECURITY_ATTRIBUTES lpsaThread, BOOL fInheritHandles, DWORD fdwCreate, LPVOID lpvEnvironment, LPCTSTR lpszCurDir, LPSTARTUPINFO lpsiStartInfo, LPPROCESS_INFORMATION lppiProcInfo); // address of module name // address of command line // address of process security attributes // address of thread security attributes // new process inherits handles // creation flags // address of new environment block // address of current directory name // address of STARTUPINFO // address of PROCESS_INFORMATION Les 2 premiers paramètres définissent l’exécutable et la ligne de commande. Windows effectue des recherches de l’exécutable dans le même répertoire que l’exe du processus père, dans le répertoire courant, dans le répertoire système, le répertoire de windows, dans le path... Les attributs de sécurité du processus et du thread principal créés sont précisés, ils définissent entre autre les règles d’héritage et d’accès des handles de processus et de thread créés. Le drapeau d’héritage permet d’interdire tout héritage à la création du processus, même pour les objets ayant l’attribut «héritable». Win32 multitâches - T. Joubert © THEORIS 2005 39 Les drapeaux de création affectent la création d’un processus. Par exemple, la valeur CREATE_SUSPENDED permet de créer le thread principal sans lui permettre de s’exécuter. La valeur CREATE_DEFAULT_ERROR_MODE évite que le nouveau process hérite du mode de gestion des erreurs de son père. Dans le cas d’une application console, certaines valeurs sont utilisées pour spécifier si le processus créé utilise une nouvelle console (CREATE_NEW_CONSOLE) ou la console du père. Enfin, il est possible de préciser dans ce champ la classe de priorité du processus (cf suite du chapitre sur les processus). Il est possible de passer un pointeur vers la zone d’environnement du nouveau process. La valeur 0 permet d’utiliser le même environnement que le père. Il est possible de définir un lecteur et un répertoire courant pour le nouveau processus ou de récupérer les valeurs du père. Le paramètre lpsiStartInfo contient des informations graphiques utilisées pour les fenêtre GUI et/ou console. Enfin, la routine renseigne une structure qui contient handle et identificateurs du processus et du thread ainsi créés. Il est nécessaire de libérer les 2 handles sinon les objets seront toujours considérés comme utilisés même après la fin du processus ou du thread. Exemple: PROCESS_INFORMATION pi; STARTUPINFO si; ZeroMemory(&si,sizeof(si)); si.cb=sizeof(si); // OBLIGATOIRE, si.cb = 0 entraine une erreur If (CreateProcess(0,’’APPLI param1’’,0,0,FALSE,0,0,0,&si,&pi)) { printf(‘‘Processus fils lancé’’); // libération des handles processus fils CloseHandle( pi.hProcess ); CloseHandle( pi.hThread ); } else printf(‘‘Erreur au lancement d’un processus fils’’); Win32 multitâches - T. Joubert © THEORIS 2005 40 VOID ExitProcess(UINT uExitCode) BOOL TerminateProcess(HANDLE hProcess,UINT uExitCode) La primitive ExitProcess permet de terminer proprement le process (lui-même) en précisant un code de retour. La routine TerminateProcess permet de terminer un process (via son handle) mais n'avertit pas toutes les DLL de cette fin brutale. Lorsque le processus se termine: • Tous ses threads sont stoppés, • Tous ses objets utilisateurs & GUI sont libérés, les accès aux objets kernels sont fermés, • L’objet kernel processus est «signalé», son code retour est enregistré. Cette notion sera détaillée par la suite, elle permet à tout autre thread possédant un handle sur ce processus d’obtenir le code retour et d’être averti de la fin de ce processus. Contrairement à la routine CreateProcess, qui n’est pas appelée avant WinMain, la routine ExitProcess est appelée automatiquement par la librairie C en retour de la fonction WinMain. Il n’est donc obligatoire d’appeler cette procédure à la fin de WinMain; le code retour de WinMain sera automatiquement passé à ExitProcess. L ’objet kernel processus n’est pas libéré (=détruit) tant qu’il existe des handles sur ce processus. Ces handles peuvent être utilisés pour obtenir le code retour d’un process via GetExitCodeProcess, qui peut retourner STILL_ACTIVE si le process n’est pas terminé. Le chapitre sur la synchronisation montre une autre technique pour suivre le déroulement d’un processus en utilisant les fonctions signalement sur son handle. Win32 multitâches - T. Joubert © THEORIS 2005 41 Priorités des processus Win32 Win32 propose 6 classes de priorité IDLE_PRIORITY_CLASS BELOW_NORMAL_PRIORITY_CLASS* NORMAL_PRIORITY_CLASS ABOVE_NORMAL_PRIORITY_CLASS* HIGH_PRIORITY_CLASS REALTIME_PRIORITY_CLASS * Classes disponibles sous XP uniquement SetPriorityClass GetPriorityClass Win32 XP - © Theoris 2005 42 L’API Win32 ne fournit pas un accès direct aux 32 niveaux de priorités gérés par le système, mais passe par la définition d’une priorité au niveau des process puis d’une priorité au niveau des threads. La priorité d’un process appartient à l’une des 4 classes définies par Win32. BOOL SetPriorityClass(HANDLE hProcess, DWORD fdwPriority) DWORD GetPriorityClass(HANDLE hProcess) Ces deux routines permettent de fixer ou d ’obtenir la classe de priorité d’exécution d’un processus Win32 identifié par son handle. Lors de la création d ’un processus, la priorité par défault est celle du processus créateur. Rappelons que la classe peut être définie à la création du processus fils dans le paramètre fdwCreate. Rappelons aussi que sous Windows NT la classe REALTIME est assujettie à une autorisation (dans le profil de l’utilisateur). Win32 multitâches - T. Joubert © THEORIS 2005 42 Environnement d’un processus Les variables d’environnement Le lecteur et le répertoire courant Dupliquées ou spécifiées à la création d’un processus fils Limité au lecteur courant mais peut être étendu au travers de variables d’environnement un peu particulières. La gestion des erreurs critiques Les mécanismes d’héritage L’héritage intervient à la création d’un processus fils C’est une importante source d’erreur car le processus fils ne sait pas de quoi il hérite. Win32 XP - © Theoris 2005 43 En complément des différents attributs et propriétés d’un processus que nous venons de voir, il existe un certain nombre de notions que l’on peut qualifier «d’environnement d’un processus Win32». Ces notions ne sont pas toutes fondamentales mais il n’est pas inutile de les aborder. Chaque processus dispose d’un bloc d’environnement; il s’agit d’un bloc de mémoire contenant des chaînes de caractères définissant des variables d’environnement. A la création d’un processus, le bloc d’environnement du processus père est recopié et cette copie forme le bloc d’environnement du processus fils. Il est aussi possible de fournir explicitement un bloc mémoire à utiliser comme bloc d’environnement pour le processus créé. Les primitives GetEnvironmentVariable et SetEnvironmentVariable permettent d’accéder aux variables d’environnement du processus courant. Les notions de lecteur et de répertoire actuels existent dans Win32 au niveau de chaque processus. Les fonctions utilisent ces valeurs en cas de besoin. Les primitives SetCurrentDirectory et GetCurrentDirectory permettent d’accéder à ces informations. Ces données ne sont pas héritées par le processus fils. Win32 gère la notion de répertoire courant pour chaque lecteur si l’on utilise une variable d’environnement «=L:» où L désigne le lecteur. Ces variables ne sont pas mises à jour mais exploitées par Win32. Il est possible de spécifier comment le système doit réagir en cas d’erreur grave de la part d’un processus. La primitive SetErrorMode permet de fixer ce comportement. Ce mode de gestion est hérité par un processus fils qui doit en tenir compte. A la création d’un processus fils, il est possible de choisir si le nouveau processus hérite ou pas des objets héritables mais le fils ne sait pas de quoi il hérite. C’est à l’application d’anticiper cet héritage. Win32 multitâches - T. Joubert © THEORIS 2005 43 Threads L’objet kernel «thread» Le point d’entrée du thread Un contexte (registres, stack, exceptions...) L’espace d’adressage du processus Une simple procédure dont le nom n’est pas imposé mais le prototype (paramètre et retour). Les états d’un thread les transitions automatiques les transitions explicites les synchronisations Win32 XP - © Theoris 2005 44 Le thread est surtout composé d’un contexte qui dépend de la machine (registres...) et dont la composition est dans winnt.h. Chaque thread dispose de sa Stack mais tous les threads d’un même processus cohabitent dans le même espace d’adressage. Les variables globales, les handles peuvent être utilisés par des threads d’un même processus (sous réserve de respecter les précautions d’emploi). Le thread principal est automatiquement créé à la création du processus. Certaines de ses caractéristiques, comme la taille de la Stack, peuvent être précisées à la création. Le point d’entrée du thread n’est pas imposé nominativement, il est toujours précisé à la création (CreateThread possède ce paramètre) mais il est toujours prototypé comme la forme suivante où lpvThreadParam est une donnée sur 32 bits: DWORD MaFonctionDeThread(LPVOID lpvThreadParam) S’il existe, le thread est dans l’un des états suivants (gras=«visible» par l’appli): - Prêt (attente d’être sélectionné) - En exécution: 1 par processeur - En attente: synchronisation, suspendu - Terminé: mais encore existant via des handles - Standby: prochain thread éligible, 1 par processeur - Transitoire: ressources indisponibles (ex Stack paginée) Tout thread possède un compteur d’exécution qui permet de savoir si le thread a le droit de s’exécuter ou non, le thread a le droit de s’exécuter si ce compteur vaut 0. Le drapeau CREATE_SUSPENDED positionne ce compteur à 1. Les routines SuspendThread et ResumeThread permettent d’incrémenter et de décrémenter ce compteur. DWORD SuspendThread(HANDLE hThread) DWORD ResumeThread(HANDLE hThread) Win32 multitâches - T. Joubert © THEORIS 2005 44 Vie et mort d’un thread CreateThread Le point d’entrée et le paramètre 32 bits Les attributs de création ExitThread et TerminateThread La bibliothèque C Utiliser _beginthreadex, _endthreadex et _beginthreadNT Eviter_beginthread et _endthread Win32 XP - © Theoris 2005 HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpsa, DWORD cbStack, LPTHREAD_START_ROUTINE lpStartAddr, LPVOID lpvThreadParm, DWORD fdwCreate, LPDWORD lpIDThread); 45 // address of thread security attributes // initial thread stack size, in bytes // address of thread function // argument for new thread // creation flags // address of returned thread ID Dans la primitive CreateThread on retrouve certains paramètres déjà rencontrés dans la primitive CreateProcess puisque celle ci contenait des informations pour la création du thread principal obligatoire. Le premier paramètre permet de préciser les attributs de sécurité du thread. Le second paramètre permet de fixer la taille de la Stack, la valeur 0 signifie que l’on souhaite prendre la taille spécifiée au moment de l’édition de lien. Les deux paramètres suivants sont le point d’entrée et le paramètre d’appel du nouveau thread. Il est possible de créer plusieurs threads avec le même point d’entrée. Le drapeau de création ne peut prendre que 2 valeurs: 0 ou CREATE_SUSPENDED. Dans le second cas, le thread est initialisé mais ne s’exécute pas, il est suspendu. Enfin, le dernier paramètre permet de récupérer l’identificateur du thread. Win32 multitâches - T. Joubert © THEORIS 2005 45 VOID ExitThread(DWORD dwExitCode); BOOL TerminateThread(HANDLE hThread, DWORD dwExitCode); Ces routines permettent de mettre fin au thread en précisant le code de retour. La routine TerminateThread ne devrait pas être utilisée car elle n’informe pas les DLL de la fin du thread et ne libère pas la Stack. Lorsque le thread se termine: - Les handles des objets utilisateurs sont libérés, - L’objet kernel thread est signalé, son code retour est enregistré, - Si c’est le dernier thread du processus celui ci se termine aussi. Comme pour l’objet kernel processus, l’objet kernel thread n’est pas libéré tant qu’il reste des handles sur le thread. Ces handles peuvent être utilisés pour accéder au code retour via GetExitCodeThread qui retournera STILL_ACTIVE si le thread n’est pas terminé. De même, on verra dans le chapitre sur la synchronisation que cet objet kernel thread peut aussi être utilisé. Contrairement à la routine CreateThread, qui n’est pas appelée avant le point d’entrée du thread, la routine ExitThread est appelée automatiquement par la librairie C en retour du point d’entrée du thread. Il existe un problème majeur de ré-entrance avec la bibliothèque C (LIBC.LIB) conçue il y a 25 ans, elle n’a pas prévu les systèmes multi-threads et utilise des données globales comme errno, elle est dite « Single-Thread ». Afin de rectifier le tir, une bibliothèque C « Multi-Thread » (LIBCMT.LIB et sa version dynamique MSVCRT.DLL) a été développée, elle contient une version ré-entrante des fonctions C, utilisant un bloc de mémoire par thread où elle stocke les données anciennement globales. Pour avoir une gestion correcte de cette mémoire il est conseillé d’utiliser des fonctions de création de thread spéciales _beginthread et _beginthreadex incluses dans la librairie C « Multi Thread ». Les primitives _endthread et _endthreadex permettent de terminer le thread. Dans le cas d’une application liée avec LIBCMT.LIB ou MSVCRT.DLL, il est préférable d’utiliser les primitives _beginthreadex et _endthreadex à la place des primitives Win32, elles ont des paramètres et un comportement équivalent. Par contre, il est préférable de ne pas utiliser _beginthread et _endthread qui n’ont pas les mêmes paramètres que les primitives Win32 associées et qui ne fonctionnent pas exactement de la même façon (CloseHandle appelé). On n’est pas obligés de passer par la librairie Multi-Thread, sous Visual C++ la librairie Single-Thread est définie par défaut dans les projets Win32 et il existe dans la Win32 beaucoup de fonctions qui se superposent à celles du RunTime C (manipulations de chaînes, I/O console, etc.). Win32 multitâches - T. Joubert © THEORIS 2005 46 Priorités d’un thread Win32 Win32 propose 7 niveaux de priorité THREAD_PRIORITY_LOWEST (-2) THREAD_PRIORITY_BELOW_NORMAL (-1) THREAD_PRIORITY_NORMAL (défaut) THREAD_PRIORITY_ABOVE_NORMAL (+1) THREAD_PRIORITY_HIGHEST (+2) THREAD_PRIORITY_IDLE (1 ou 16) THREAD_PRIORITY_TIME_CRITICAL (15 ou 31) SetThreadPriority GetThreadPriority Win32 XP - © Theoris 2005 47 Comme cela a été exposé au chapitre sur les processus, l’API Win32 ne fournit pas un accès direct aux 32 niveaux de priorités du système, mais décompose la notion de priorité en classe de process et priorité relative de thread. Au départ, le thread est créé avec la priorité de son processus mais il est possible de préciser une priorité relative qui associée à la classe du processus permet d’accéder à un ensemble de priorités. BOOL SetThreadPriority( HANDLE hThread, int nPriority); int GetThreadPriority ( HANDLE hThread); La primitive SetThreadPriority permet de fixer la priorité relative d’un thread; la priorité est relative à la classe du processus qui donne la priorité normale. Le paramètre nPriority est à choisir parmi: • THREAD_PRIORITY_LOWEST (-2) , • THREAD_PRIORITY_BELOW_NORMAL (-1), • THREAD_PRIORITY_NORMAL (défaut), • THREAD_PRIORITY_ABOVE_NORMAL (+1) , • THREAD_PRIORITY_HIGHEST (+2), • THREAD_PRIORITY_IDLE (1 ou 16), • THREAD_PRIORITY_TIME_CRITICAL (15 ou 31). Les deux dernières priorités relatives dépendent plus encore de la classe du processus. Pour toutes classes sauf REALTIME, THREAD_PRIORITY_IDLE correspond à la priorité 1 et THREAD_PRIORITY_TIME_CRITICAL à la priorité 15. Pour la classe REALTIME, THREAD_PRIORITY_IDLE correspond à la priorité 16 et THREAD_PRIORITY_TIME_CRITICAL à la priorité 31. La primitive GetThreadPriority permet de récupérer la priorité relative du thread. Win32 multitâches - T. Joubert © THEORIS 2005 47 Priorités Win32 31 TIME CRITICAL 26 24 22 Back 16 15 Back Front 16 9 8 10 7 6 5 4 1 0 IDLE REALTIME 13 10 HIGHEST ABOVE NORMAL NORMAL BELOW NORMAL LOWEST IDLE IDLE BELOW NORMAL NORMAL ABOVE NORMAL HIGH Win32 XP - © Theoris 2005 48 Ce schéma présente les différentes priorités des threads win32 , on remarque que les priorités des threads Win32 ne couvrent pas l ’intégralité de la plage des priorités des threads kernel. Il convient de ne pas oublier que, pour les les threads n ’appartenant pas à un processus de classe REALTIME, le système fait varier la priorité dynamiquement selon les critères suivants (XP) : Quand une fenêtre reçoit un événement, le thread propriétaire reçoit un boost de priorité. Un thread qui sort d’une condition d’attente (Event, I/0…) bénéficie d’un boost de priorité. Un processus « NORMAL » propriétaire de la fenêtre Foreground est boosté dans la sous-classe « Foreground ». On peut inhiber ces variations au niveau d ’un thread ou de tout le processus : BOOL SetProcessPriorityBoost(HANDLE hProcess, BOOL DisablePriorityBoost) BOOL SetThreadPriorityBoost(HANDLE hThread, BOOL DisablePriorityBoost) On constate que l ’ensemble des threads d ’un processus ne peuvent se répartir explicitement que sur 6 ou 7 niveaux, dont deux sont des valeurs singulières. L ’utilisation de la classe REALTIME est délicate car elle entre en conflit avec des processus Win32 et elle peut modifier le comportement du système, en particulier les temps de réponse. Win32 multitâches - T. Joubert © THEORIS 2005 48 Résumé Windows XP Un quantum de temps de l’ordre de 20ms Processus & threads 32 niveaux de priorités, 4 classes Processus: image exécutable, 4Go, classe de priorité Thread: unité d’exécution, priorité relative dans la classe Outils de visualisation TASKMGR.EXE, PERFMON.EXE (disponibles en natif) PVIEW.EXE, PSTAT.EXE (fournis avec Visual Studio) ainsi que WINMSD.EXE, MSINFO32.EXE (natif) Win32 XP - © Theoris 2005 49 Il existe plusieurs outils permettant de visualisation des objets processus et thread, certains sont fournis en standard sous NT, d ’autres arrivent avec Visual Studio : TASKMGR.EXE (natif) C ’est le gestionnaire de tâches NT, il fournit une information globale sur l ’activité système et les processus actifs. Pour chaque processus on peut visualiser 13 compteurs (accessibles via l ’option Affichage). PVIEW.EXE (Visual Studio) C ’est un outil plus complet car en plus des informations sur l ’espace d ’adressage des procesus, il fournit les données relatives aux threads. Cet outil peut être utilisé pour visualiser l ’état d ’une machine en réseau. PSTAT.EXE (Visual Studio) C ’est un utilitaire équivalent a PVIEW en ligne de commande. PERFMON.EXE (Natif) C ’est un outil complet destiné aux mesures de performances, on peut mesurer un nombre important de paramètres système sur différents objets, dont les processus et les threads. Il est toutefois moins efficace que Pview pour identifier ces derniers et il ne marche pas en réseau. On peut également citer deux outils natifs MSINFO32.EXE et WINMSD.EXE qui fournissent un certain nombre d ’informations globales sur le système. Sans oublier SPYXX.EXE qui visualise également Processus et Threads. Win32 multitâches - T. Joubert © THEORIS 2005 49 Chapitre 3 Mécanismes de synchronisation Win32 multitâches - T. Joubert © THEORIS 2005 50 Plan Introduction à la synchronisation Objets de synchronisation Objets de synchronisation à l’intérieur d’un processus Objets de synchronisation inter-processus Primitives de synchronisation Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 51 51 Principes 1) Signalisation 2) Protection thread B thread A thread A thread B S S O Temps Win32 XP - © Theoris 2005 52 La synchronisation est avant tout un mécanisme d’échange d’information entre plusieurs parties du code. On parlera généralement de synchronisation entre tâches; et dans notre cas, de synchronisation de threads. Dans le cas d’un système multitâche, la synchronisation intervient fortement dans l’ordonnancement; en particulier, une tâche en attente d’une synchronisation ne sera plus candidate à l’exécution. A l’opposé, l’attente active consiste à dérouler un code «d’attente» tant que la condition souhaitée n’est pas atteinte. Cette technique occupe le processeur au détriment des autres tâches et ne doit pas être utilisée. Il est possible de classer les besoins en synchronisation dans deux catégories: la signalisation et la protection illustrées par les deux schéma détaillés ci-dessous. Dans le cas de la signalisation, l’objectif recherché est de s’assurer que l’exécution d’une tâche ne pourra pas se poursuivre tant qu’un événement ne se sera pas produit. Comme chaque tâche s’exécute à son propre rythme, il peut être nécessaire d’utiliser un mécanisme de synchronisation pour marquer des étapes. Le concept de «rendez-vous» peut facilement être implémenté à partir de 2 synchronisations de signalisation. Dans l’exemple, pour commencer, les threads A et B s’exécutent en parallèle. Le thread A termine la première étape représentée par l’objet de synchronisation S avant le thread B. Le thread A est mis en attente pendant que le thread B poursuit son exécution. Lorsque le thread B termine aussi la première étape, il signale la fin de cette étape au travers de l’objet S. Les deux threads peuvent alors continuer. Ce mécanisme de signalisation est aussi utilisé lorsque l’exécution d’une partie du code n’a de sens que si certaines conditions sont remplies. Win32 multitâches - T. Joubert © THEORIS 2005 52 Dans le cas de la protection, l’objectif est de faire un contrôle d’accès. Le cas du droit d’accès exclusif à un objet est le plus simple et le plus fréquent. Dans l’exemple, on utilise un objet de synchronisation S pour protéger l’accès à un objet applicatif O. • Pour commencer, les threads A et B s’exécutent en parallèle. • Le thread B demande l’accès à l’objet O et l’obtient; il peut donc accéder librement à cet objet. • A son tour, le thread A fait cette demande mais ne peut obtenir cette autorisation puisque B a déjà obtenu l’accès. • Le thread A est alors mis en attente par l ’ordonnanceur pendant que le thread B poursuit son exécution. • Lorsque le thread B n’a plus besoin de cet accès, il signale la libération au travers de l’objet S. • Le thread A obtient alors l’autorisation d’accéder à l’objet A et les deux threads peuvent continuer. Win32 multitâches - T. Joubert © THEORIS 2005 53 L’étreinte fatale (Deadlock) Un exemple simple Thread2 Thread 1 S1 S2 Une situation complexe que le système ne gère pas Win32 XP - © Theoris 2005 54 L’étreinte fatale ou dead lock peut arriver très facilement dès que l’on utilise plusieurs objets de synchronisation. Il faut noter que le problème ne se produit que dans des cas bien précis de déroulement des instructions, et que dans certains cas il arrivera de manière très intempestive. Pour éviter ce problème, on pourra utiliser: * les time-out * les attentes multiples Le système ne met pas en place de mécanisme pour gérer les étreintes fatales. Win32 multitâches - T. Joubert © THEORIS 2005 54 Composantes Les sections critiques Les objets du noyau Réservée à la synchronisation au sein du même processus. L’état signalé/non signalé d’un objet du noyau Les objets de synchronisation Mutex, Sémaphore, Evénement Les autres objets du noyau utilisables Processus, Thread, Fichier, ... Les primitives de synchronisation WaitForSingleObject, WaitForMultipleObjects, ... Win32 XP - © Theoris 2005 55 Les sections critiques sont des objets de synchronisation très simples, plutôt destinés à la protection d’accès, mais limités à la synchronisation entre threads d’un même processus. Les objets du noyau sont eux accessibles aux threads de processus différents, ils peuvent donc être utilisés pour synchroniser des threads appartenant à des processus différents (sur la même machine!). Tous les objets du noyau ne sont pas à proprement parler des objets de synchronisation mais ils peuvent tous être aussi utilisés à cette fin. Chaque objet peut être dans l’état signalé ou non signalé. Un thread passe (et reste) en sommeil dès lors que l’objet accédé n’est pas signalé. Il se réveille ou poursuit son exécution lorsque l’objet est signalé. Les primitives WaitForSingleObject et WaitForMultipleObjects permettent de se synchroniser avec un ou plusieurs objets. D’autres primitives, souvent différentes suivant les objets, permettent de modifier l’état signalé/non signalé de l’objet. Win32 multitâches - T. Joubert © THEORIS 2005 55 Sections critiques InitializeCriticalSection DeleteCriticalSection EnterCriticalSection Réserve l’accès à une section critique (attend sa libération si nécessaire) TryEnterCriticalSection Déclare et accède à une section critique Tente de réserver l’accès à une section critique (pas d’attente si elle est déjà réservée) LeaveCriticalSection Win32 XP - © Theoris 2005 56 La section critique permet de réserver l’accès exclusif à une partie du code afin de protéger des accès, elle peut être comparée avec la notion de région mais elle n’est pas une interdiction d’ordonnancement. Une donnée globale par section critique de type CRITICAL_SECTION doit être accessible par tous les threads utilisant cette ressource. VOID InitializeCriticalSection(LPCRITICAL_SECTION lpcsCriticalSection) VOID EnterCriticalSection(LPCRITICAL_SECTION lpcsCriticalSection) BOOL TryEnterCriticalSection(LPCRITICAL_SECTION lpcsCriticalSection) VOID LeaveCriticalSection(LPCRITICAL_SECTION lpcsCriticalSection) VOID DeleteCriticalSection(LPCRITICAL_SECTION lpcsCriticalSection) InitializeCriticalSection permet d'initialiser la section critique, elle doit être appelée avant toute autre utilisation. A l’appel de EnterCriticalSection le système vérifie qu’aucun n’autre thread ne possède déjà la section critique et l’alloue au thread. Si la section critique est déjà allouée le thread est mis en attente. Les appels peuvent être imbriqués, la prise de cette ressource entraîne l’incrémentation d’un compteur. TryEnterCriticalSection effectue les mêmes opérations que EnterCriticalSection excepté la mise en attente du thread. Si la section est déjà allouée il rend FALSE, sinon il l’alloue et rend TRUE. A l’appel de LeaveCriticalSection le système décrémente le compteur de possession de la ressource. Lorsque le compteur atteint 0, la ressource est libérée ou transférée à un thread en attente. DeleteCriticalSection supprime la ressource. Aucun thread ne doit être en attente de cette ressource. La section critique est réservée à la synchronisation de threads d’un même processus. La section critique permet de protéger l’accès à une donnée mais cette protection est explicite: l’application doit mettre en place systématiquement les appels de protection; le système ne refusera jamais un accès non protégé à la donnée. Win32 multitâches - T. Joubert © THEORIS 2005 56 Sections critiques avec Spin InitializeCriticalSectionAndSpinCount SetCriticalSectionSpinCount Déclare une section critique avec compteur de spin, les threads entrants ne passent pas directement en WAIT mais font un nombre de tentatives égal au compteur de spin. Positionne le compteur de spin d’une section critique. Accès à la Section Critique Les autres fonctions sont identiques Ne marche que sur un système multi-CPU, le compteur est forcé à 0 en mono-CPU. Win32 XP - © Theoris 2005 57 La section critique avec compteur de spin permet d’améliorer les performances d’une application multi-thread à forte contention sur un système multi-CPU. Lorsque la ressource est prise, un thread ne passe pas directement en WAIT mais fait un nombre prédéfini de tentatives en boucle rapide (spin). VOID InitializeCriticalSectionAndSpinCount(LPCRITICAL_SECTION lpcsCriticalSection, DWORD dwSpinCount ) VOID SetCriticalSectionSpinCount(LPCRITICAL_SECTION lpcsCriticalSection, DWORD dwSpinCount ) La section critique à compteur de spin ne fonctionne que sur un système multi-CPU, la valeur du compteur est automatiquement forcé à 0 sur un système mono-CPU. Win32 multitâches - T. Joubert © THEORIS 2005 57 Entier long multi-thread Les fonctions InterLocked InterLockedIncrement, InterLockedDecrement InterLockedExchange, InterLockedExchangeAdd InterLockedCompareExchange L’absence de InterLockedRead Un thread peut accéder en lecture directement à la donnée partagée. Le système assure que la valeur sera valide mais il est impossible de savoir si une mise à jour concurrente est en cours. Des problèmes d’atomicité persistent Win32 XP - © Theoris 2005 58 Normalement pour protéger l’accès à une variable il est nécessaire d’utiliser un objet de synchronisation (souvent une section critique): CRITICAL_SECTION protege_madonnee; long int madonnee; EnterCriticalSection(&protege_madonnee); madonnee= ... LeaveCriticalSection(&protege_madonnee); Cette utilisation est si fréquente que WIN32 contient des primitives gérant les accès concurrents à un entier long sans nécessiter l’emploi explicite d’un objet de synchronisation: - InterLockedIncrement, InterLockedDecrement : incrémenter / décrémenter de 1 - InterLockedExchange : affecter une valeur - InterLockedExchangeAdd : ajouter une valeur - InterLockedCompareExchange : comparer puis affecter une valeur LONG InterlockedIncrement(LPLONG lplVal) LONG InterlockedDecrement(LPLONG lplVal) LONG InterlockedExchange(LPLONG Target, LONG Value) LONG InterlockedExchangeAdd(LPLONG Target, LONG IncVal) PVOID InterlockedCompareExchange(PVOID *pTarget, PVOID pValue, PVOID pCompVal) Ces fonctions ne retournent pas la valeur de l’entier mais une valeur de même signe que celui-ci. Il n’y a pas de fonction pour accéder à l’entier en lecture, car on peut référencer directement la donnée. Le thread lecteur récupère une valeur toujours valide mais il ne sait pas si un thread concurrent est en train de faire un accès en écriture. L’exemple suivant ne doit pas être suivi: long int madonnee; { InterLockedExchange(madonnee,madonnee+2); // Incrément de 2 unités Win32 multitâches - T. Joubert © THEORIS 2005 58 Primitives d’attente WaitFor WaitForSingleObject WaitForMultipleObjects L’attente s’effectue sur un seul objet à la fois L’attente peut être limitée dans le temps ou infinie L’attente d’un mutex abandonné est signalée Attente de plusieurs événements simultanés Attente d’un événement parmi plusieurs SignalObjectAndWait Signale un objet (événement, mutex ou sémaphore) puis attend qu’un autre objet soit signalé Win32 XP - © Theoris 2005 59 DWORD WaitForSingleObject(HANDLE hObject,DWORD dwTimeout); La fonction WaitForSingleObject indique au système que le thread attend que l’objet noyau identifié par son handle soit signalé. Si l’objet n’est pas signalé, le thread est mis en attente. Il est possible de mettre un chien de garde sur cette attente (time-out en milli-secondes) ,de refuser toute attente (time-out=0) ou d’avoir une attente infinie (time-out=INFINITE). Le thread poursuit son exécution si l’objet est signalé, si le time-out expire (immédiatement si 0), dans d’autres cas d’erreur (utiliser GetLastError) ou si l’object mutex attendu est abandonné. Le code retour de la fonction permet de savoir dans quel cas l’on est. DWORD WaitForMultipleObjects(DWORD cObjects,CONST HANDLE * lphObjects, BOOL fWaitAll,DWORD dwTimeout) . La fonction WaitForMultipleObjects permet de se mettre en attente sur plusieurs objets noyau en même temps. Il est possible de choisir entre l’attente d’un des objets dans la liste ou l’attente des tous les objets. Au maximum 64 objets peuvent être référencés. Comme pour la fonction précédente, le code retour permet de connaître les conditions de réveil et l’objet déclencheur. Un objet kernel peut n’apparaître qu’une seule fois dans la liste même avec 2 handle différents. Le système attend tous les objets avant de les affecter à un thread effectuant un attente simultanée de plusieurs événements. Codes retours utilisés: WAIT_OBJECT_0+index: l’objet est passé à l’état signalé WAIT_TIMEOUT WAIT_ABANDONED+index: le mutex a été abandonné WAIT_FAILED Win32 multitâches - T. Joubert © THEORIS 2005 59 BOOL SignalObjectAndWait ( HANDLE hObjectToSignal , HANDLE hObjectToWaitOn, DWORD dwTimeOut_ms, BOOL bAlertable ) ; La fonction SignalObjectAndWait indique au système que le thread signale l’objet hObjectToSignal puis attend l’objet hObjectToWaitOn. Si l’objet hObjectToWaitOn n’est pas signalé, le thread est mis en attente. Il est possible de mettre un chien de garde sur cette attente (time-out en milli-secondes), de refuser toute attente (time-out=0) ou d’avoir une attente infinie (time-out=INFINITE). Le thread poursuit son exécution si l’objet est signalé, si le time-out expire (immédiatement si 0), dans d’autres cas d’erreurs (utiliser GetLastError) ou si l’object mutex attendu est abandonné. Le code retour de la fonction permet de savoir dans quel cas l’on est. L’argument bAlertable indique si le thread est en état d’alerte. Lorsqu’un thread est en état d’alerte et qu’une opération d’attente suivante prend fin (SignalObjectAndWait, WaitForSingleObjectEx, WaitForMultipleObjectsEx, SleepEx, ReadFileEx, WriteFileEx) le système va exécuter les APC (Asynchronous Procedure Call) qui sont en attente dans la queue d’APC relative au thread. La fonction QueueUserAPC permet de mettre des fonctions en attente d’exécution dans la queue des APC propre au thread. NB : Excepté le flag bAlertable, WaitForSingleObjectEx et WaitForMultipleObjectsEx sont identiques en tous points aux fonctions WaitForSingleObject et WaitForMultipleObjects. Exemples: 1) L’attente simple { HANDLE hMonHandle; DWORD CR; hMonHandle= /* récupérer le handle d’un objet kernel*/ CR=WaitForSingleObject(hMonHandle,INFINITE); if (CR==WAIT_OBJECT_0) /* Synchro obtenue */ else /* WAIT_TIMEOUT impossible ici! WAIT_ABANDONNED non plus donc erreur ! */ Win32 multitâches - T. Joubert © THEORIS 2005 60 2) L’attente active , l’exemple à ne pas suivre! while (WaitForSingleObject(hMonHandle,0)==WAIT_TIMEOUT); /* Attente active !!! */ 3) L’attente multiple entre plusieurs événements { HANDLE hMesHandle[2]; DWORD CR; hMesHandle[0]= /* récupérer le handle d’un objet kernel No1 hMesHandle[1]= /* récupérer le handle d’un objet kernel No2 CR=WaitForMultipleObjects(2,hMesHandle,FALSE,INFINITE); if (CR==WAIT_OBJECT_0) /* Synchro obtenue sur No1 */ else if (CR==WAIT_OBJECT_0+1) /* Synchro obtenue sur No2 */ else ... 4) L’attente multiple de plusieurs événements { HANDLE hMesHandle[2]; DWORD CR; hMesHandle[0]= /* récupérer le handle d’un objet kernel No1 hMesHandle[1]= /* récupérer le handle d’un objet kernel No2 CR=WaitForMultipleObjects(2,hMesHandle,TRUE,INFINITE); if (CR==WAIT_OBJECT_0) /* Synchro obtenue sur No1 et sur le No2 !!! */ else ... 5) Signaler un objet et en attendre un autre { HANDLE hObjectToSignal, hObjectToWaitTo; BOOL CR; hObjectToSignal = // récupérer le handle d’un objet kernel hObjectToWaitTo = // récupérer le handle d’un objet kernel CR=SignalObjectAndWait ( hObjectToSignal, hObjectToWaitTo, INFINITE, FALSE); if (CR==TRUE) /* Synchro obtenue sur hObjectToWaitTo */ else ... Win32 multitâches - T. Joubert © THEORIS 2005 61 Mutex Le mutex correspond à une section critique mais peut être inter-process. CreateMutex ou OpenMutex WaitFor ... ReleaseMutex L’abandon du mutex Les threads en attente sur un mutex sont avertis lorsque le thread propriétaire se termine sans libérer ce mutex. Win32 XP - © Theoris 2005 62 Le mutex est très proche de la section critique mais il peut être employé par plusieurs processus. HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpsa,BOOL fOwner, LPCTSTR lpszName) HANDLE OpenMutex(DWORD fdwAccess,BOOL fInherit,LPCTSTR lpszName) BOOL ReleaseMutex(HANDLE hMutex) A la différence des autres objets du noyau, le mutex est possédé par un thread (ou par aucun), le système mémorise le thread propriétaire du mutex. Un thread déjà propriétaire du mutex obtiendra toujours le mutex en cas de demandes imbriquées (incrémentation d ’un compteur), il devra le libérer de façon symétrique (autant de fois qu’il l’a demandé). CreateMutex permet de créer un mutex. Il peut être créé déjà possédé par son créateur (fOwner) . Le nom du mutex est facultatif, il permet l’accès au mutex par son nom. OpenMutex permet d’accéder à un mutex existant en précisant si les accès autorisés sont seulement pour la synchronisation ou si tous sont autorisés (comme la destruction). Cette fonction utilise le nom du mutex pour le retrouver. ReleaseMutex permet de libérer le mutex et, si le compteur de la ressource atteint 0, de le signaler à un éventuel autre thread en attente. La libération d’un mutex non possédé est ignorée. Si le thread propriétaire d’un mutex se termine, le mutex est libéré et tous les threads en attente sont avertis. Les fonctions WaitForSingleObject et WaitForMultipleObjects retournent alors des codes particuliers (WAIT_ABANDONED+x). Win32 multitâches - T. Joubert © THEORIS 2005 62 Sémaphores Un simple compteur interne Sémaphore et Mutex Le mutex n’est pas un sémaphore binaire Le sémaphore n’est pas la propriété d’un thread Le mutex est la propriété d’un thread Acquérir une seule unité à la fois Pas de lecture du compteur Seule la libération d’unité permet d’obtenir la valeur du compteur et il s’agit de la valeur précédente Win32 XP - © Theoris 2005 63 Le sémaphore est un compteur de ressource. HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpsa,LONG cInitial,LONG cMax, LPCTSTR lpszName) HANDLE OpenSemaphore(DWORD fdwAccess,BOOL fInherit,LPCTSTR lpszName) BOOL ReleaseSemaphore(HANDLE hSemaphore,LONG cReleaseCnt,LPLONG lplPrevCnt) Lorsqu’un thread effectue une prise de sémaphore (P), le système décrémente le compteur du sémaphore ou met le thread en sommeil. Lorsqu’un thread rend un sémaphore (V), le système incrémente le compteur et traite les attentes. Le sémaphore peut contenir plusieurs unités et donc il est possible pour plusieurs threads d’obtenir l’unité demandée. Le sémaphore n’est pas possédé par le(s) thread(s) qui effectue(nt) une prise avec succès contrairement au mutex. Sémaphore binaire et mutex ne sont pas équivalents bien que proches. CreateSemaphore permet de créer un sémaphore en précisant le nombre maximal d’unités et le nombre d’unités disponibles à la création. OpenSemaphore fonctionne comme OpenMutex. Les fonctions WaitFor permettent de prendre un sémaphore: prendre une unité du sémaphore. Si le sémaphore ne possède pas cette unité, le thread se met en attente de cette unité en fonction du time-out indiqué. ReleaseSemaphore permet de libérer le sémaphore en incrémentant le compteur d’un nombre d’unités indiqué. Contrairement au mutex, n’importe quel thread peut effectuer cet appel même s’il n’a pas effectué de prise précédemment. En retour, il est possible mais facultatif de récupérer la valeur du compteur avant son incrémentation. Il n’est pas possible de lire la valeur du compteur sans faire de libération (pas de libération de 0 unités non plus!). Win32 multitâches - T. Joubert © THEORIS 2005 63 Il peut être nécessaire d’acquérir plusieurs unités d’un sémaphore avant de poursuivre l’exécution, or la syntaxe et les règles d’utilisation des fonctions WaitFor nous interdisent de prendre plus d’une unité à la fois: - L’attente sur un handle de sémaphore signifie que l’on souhaite prendre une unité. - Il est interdit de faire apparaître plusieurs fois le même handle dans le même appel. Pour prendre plusieurs unités dans un sémaphore, il faut plusieurs appels consécutifs aux fonctions WaitFor ce qui complique la gestion des codes retours et entraîne des risques de dead-lock importants comme dans l’exemple suivant: { HANDLE hMonHandle; DWORD CR; hMonHandle= /* récupérer le handle d’un objet sémaphore CR=WaitForSingleObject(hMonHandle,INFINITE); if (CR==WAIT_OBJECT_0) { CR=WaitForSingleObject(hMonHandle,INFINITE); /* Ici on a bien 2 unités de notre sémaphore ... ReleaseSemaphore(hMonHandle,2,NULL); } Si 2 threads concurrents utilisent un code de ce type, il est possible que le 1er obtienne 1 unité, puis suite à un changement de contexte le second thread s’exécute, demande et obtient 1 unité, et redemande 1 unité qu’il ne peut obtenir. Le 1er thread reprend la main, demande une seconde unité et se met à son tour en attente... Win32 multitâches - T. Joubert © THEORIS 2005 64 Evénements La ré-initialisation manuelle SetEvent ResetEvent PulseEvent WaitFor... La ré-initialisation automatique SetEvent inchangé ResetEvent/PulseEvent presque inutiles ce qui change pour WaitFor... Win32 XP - © Theoris 2005 65 L’événement est un indicateur binaire utilisé pour la synchronisation. Il y a deux types de gestion des événements: - l’événement à ré-initialisation manuelle - l’événement à ré-initialisation automatique HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpsa,BOOL fManual,BOOL fInitialState, LPCTSTR lpszName) HANDLE OpenEvent(DWORD fdwAccess,BOOL fInherit,LPCTSTR lpszEventName); address of event-object name BOOL SetEvent(HANDLE hEvent) BOOL ResetEvent(HANDLE hEvent) BOOL PulseEvent(HANDLE hEvent) // CreateEvent permet de créer un événement en précisant le type et l’état initial (signalé/non signalé). OpenEvent permet d’accéder à un événement existant. Evénements à ré-initialisation manuelle: ces événements ne sont pas ramenés à l’état non signalé par les fonctions WaitFor. La primitive SetEvent permet de signaler l’événement et ResetEvent permet de placer l’événement dans l’état non signalé. Enfin la primitive PulseEvent effectue de façon insécable l’appel à SetEvent puis ResetEvent. Cette fonction permet de libérer tous les threads en attente de cet événement mais l’événement reste non signalé. La signalisation d’un événement à ré-initialisation manuelle peut entraîner le réveil de plusieurs threads en même temps. Evénement à ré-initialisation automatique: cet événement est remis à l’état non signalé dès qu’il est utilisé par le système pour réveiller un thread en attente. La primitive SetEvent permet de signaler l’événement. Les primitives ResetEvent et PulseEvent fonctionnent aussi même si leur utilité se fait moins sentir pour un événement à ré-initialisation automatique. La signalisation d’un événement à ré-initialisation automatique via SetEvent ou PulseEvent ne peut entraîner le réveil que d’un seul thread à la fois. Win32 multitâches - T. Joubert © THEORIS 2005 65 D’autres synchronisations Les processus et threads Ces 2 types objets sont signalés lorsqu’ils sont terminés SuspendThread, ResumeThread et Sleep Exécuter un thread suspendu à la création Les suspensions peuvent être imbriquées Sleep(0) permet au thread d’abandonner sa plage de temps et de se suspendre pour un durée déterminée. Les fichiers A l’issue d’E/S asynchrones il est possible d’utiliser les handles de fichier ou d’événement pour se resynchroniser Win32 XP - © Theoris 2005 66 Les objets processus et thread sont aussi des objets de synchronisation, ils sont dans l’état signalé dès lors que le processus ou le thread (référencé par un handle) est terminé. DWORD ResumeThread(HANDLE hThread); DWORD SuspendThread(HANDLE hThread); VOID Sleep(DWORD cMilliseconds) Lorsqu’un thread est créé suspendu, il peut être autorisé à s’exécuter en utilisant la primitive ResumeThread. De même à tout moment, il est possible d’utiliser la primitive SuspendThread pour interrompre l’exécution d’un thread, exécution qui sera réautorisée avec la fonction précédente. Les appels à SuspendThread peuvent être imbriqués, un nombre équivalent d’appels à ResumeThread sera alors nécessaire. La fonction Sleep indique que le thread souhaite se suspendre (=attente de temps) pour une durée précisée en milli-secondes. Si la valeur est nulle, cet appel permet au système d’allouer la plage de temps à un autre thread. La fonction ResumeThread ne réveille pas un thread suspendu par Sleep. Les opérations d’E/S asynchrones permettent à un thread de poursuivre son exécution pendant que l’opération s’effectue «en parallèle». Il est toutefois possible de re-synchroniser le déroulement de l’application en utilisant le handle de fichier ou un mécanisme d’événement liés aux opérations asynchrones. En effet, l’objet fichier est non signalé à l’appel du service d’E/S et devient signalé dès lors que l’opération demandée est terminée. Il est aussi possible d’obtenir une signalisation complémentaire dans un objet événement précisé au lancement du service asynchrone. Les fichiers et plus particulièrement les opérations asynchrones sont présentés par la suite. WIN32-Vol2- Chapitre 44 - Synchronisation - Overlapped I/O (44.1.5 ) Win32 multitâches - T. Joubert © THEORIS 2005 66 Résumé Plusieurs types d’objets intra-processus: sections critiques inter-processus: mutex, sémaphore, événements + d’autres objets kernel comme les processus, les threads Trois routines de synchronisation WaitForSingleObject WaitForMultipleObjects SignalObjectAndWait Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 67 67 Chapitre 4 Handles et objets Win32 multitâches - T. Joubert © THEORIS 2005 68 Introduction aux objets kernel Principaux types d’objets kernel Processus Thread Mutex, Sémaphore, Evènement Fichier, fichier mappé, pipes … Un concept de gestion Création de l’objet, ouvertured’un objet Attrubuts d’accès, héritage Accès à travers un HANDLE Une lourdeur dûe à l’architecture noyau Win32 XP - © Theoris 2005 69 Les objets kernel doivent être distingués des objets utilisateurs et des objets de l’interface graphique (GDI), ils ne sont pas gérés de la même manière par le système. Ces objets kernel sont très importants, ils sont omniprésents dans les applications et il en existe un grand nombre. Pour la plupart de ces objets, l’API Win32 propose les primitives pour créer un objet, accéder à un objet existant, fermer l’accès, détruire l’objet et bien sûr des primitives dédiées à l’utilisation de cet objet. Lorsque le système crée un objet kernel, il alloue de la mémoire pour sa gestion, effectue des initialisations et retourne un handle pour accéder à cet objet. Enfin, les objets kernel bénéficient de contrôles d’accès gérés par le système, certains possèdent en plus des caractéristiques d’héritage et des possibilités de duplication de handle. Win32 multitâches - T. Joubert © THEORIS 2005 69 Existence d’un objet kernel Process1 Create Process 2 Open handle1 Process 1 Process 2 CloseHandle handle2 handle2 Objet Kernel Objet Kernel Win32 XP - © Theoris 2005 70 Le handle qui servira pour accéder à cet objet kernel est associé au processus qui a demandé l’accès. Deux processus accédant au même objet kernel disposent de handles différents et le système mémorise que 2 handles sont attribués pour cet objet. Cette mémorisation est effectuée au travers d’un compteur associé à l’objet kernel. Comme le handle n’a de sens qu’au sein du processus qui l’a obtenu on ne passera jamais le handle d’un objet kernel via un fichier, une fenêtre... Chaque handle doit être libéré avant que le système ne détruise réellement l’objet kernel. Le handle étant géré au niveau d’un processus, lorsque le processus se termine, les handles sont libérés (ce qui ne signifie pas la destruction de l’objet). La primitive DuplicateHandle permet d’obtenir la création d’un handle sur un objet kernel déjà existant. Il est même possible de créer ce handle dans un autre processus. BOOL DuplicateHandle( HANDLE hSourceProcess, HANDLE hSource, HANDLE hTargetProcess, LPHANDLE lphTarget, DWORD fdwAccess, BOOL fInherit, DWORD fdwOptions); // handle of process with handle to duplicate // handle to duplicate // handle of process to duplicate to // address of duplicate handle // access for duplicate handle // handle inheritance flag // optional actions Les handles de 2 processus vers le même objet n’ont pas obligatoirement la même valeur sauf dans le cas de l’héritage. La fonction CreateProcess retourne 2 handles au père, le handle de processus et le handle du thread principal. Il ne faut pas oublier de les libérer, ceci pouvant se faire immédiatement après l’appel à CreateProcess (le fils a de toute manière son existence propre). On parle alors de processus détaché. Win32 multitâches - T. Joubert © THEORIS 2005 70 Caractéristiques Héritage Droits d’accès le drapeau d’héritabilité est défini à la création du handle l’héritage est accepté/refusé à la création du processus le processus fils ne sait pas de quoi il hérite le profil du demandeur la nature du service Pseudo-handle il n’incrémente pas le compteur d’utilisation de l’objet il a une signification locale Win32 XP - © Theoris 2005 71 Un handle peut aussi être hérité (d’un processus à l’autre). L’héritage est déterminé au niveau de la création de l’objet ou de l’obtention d’un nouvel handle (drapeau d’héritabilité) mais aussi au moment où le nouveau process est créé (drapeau d’héritage dans CreateProcess). Le processus «fils» ne sait pas qu’il hérite de certains handles et il peut donc oublier de les libérer... Le système dote la plupart des objets d’une notion de sécurité. Cette sécurité d’accès permet au système de contrôler la validité des accès autour de deux axes principaux: identification du demandeur et nature de la demande d’accès. Pour l’objet Sémaphore, les accès aux services de synchronisation sont: SEMAPHORE_ALL_ACCESS (défaut de CreateSemaphore) SEMAPHORE_MODIFY_STATE (Release autorisé) SYNCHRONIZE (Wait autorisé) Certaines routines comme GetCurrentProcess ou GetCurrentThread ne retournent pas un handle mais un pseudo-handle. Le pseudo-handle est caractérisé par l’absence d’incrémentation du compteur d’utilisation de l’objet et par sa validité limitée au demandeur du handle. En fait, le pseudo-handle est une constante identifiant le handle du process, le handle du thread... Le pseudo-handle ne doit pas être libéré (CloseHandle ne fait rien). Il est néanmoins possible d’obtenir un véritable handle à partir d’un pseudo-handle en utilisant la fonction DuplicateHandle. Win32 multitâches - T. Joubert © THEORIS 2005 71 Exemples: 1) Création d’un processus fils détaché STARTUPINFO si; PROCESS_INFORMATION pi; // Créer le processus CreateProcess(NULL,’’Process’’ ,NULL,NULL,FALSE,0,NULL,NULL,&si,&pi); // Libérer les handles du processus et du thread créés CloseHandle(pi.hProcess); CloseHandle(pi.hThread); 2) Handle du processus père hérité par le processus créé // récuperer le pseudo handle du processus pseudoh=GetCurrentProcess(); // créer un handle à partir du pseudo-handle processus pour notre process et rendre ce nouvel // handle héritable DuplicateHandle(pseudoh,pseudoh,pseudoh,&hproc,0,TRUE,DUPLICATE_SAME_ACCESS); // Créer le nouveau process qui hérite du handle créé CreateProcess(NULL,’’Process’’ ,NULL,NULL,TRUE,0,NULL,NULL,&si,&pi); // Libérer notre handle du processus (le «fils» garde le sien) CloseHandle(hproc); Win32 multitâches - T. Joubert © THEORIS 2005 72 Propriétés d’un Handle Informations associées à un Handle Statut du drapeau d’héritabilité Comportement du CloseHandle Accesseurs d’information de Handle : SetHandleInformation GetHandleInformation Win32 XP - © Theoris 2005 73 En plus du drapeau d’héritabilité, un handle possède un flage de fermeture au CloseHandle. Ces deux propriétés peuvent être lues et modifiées avec deux fonctions dédiées : BOOL GetHandleInformation( HANDLE hObject, // handle to an object LPDWORD lpdwFlags // points to variable to receive flags ); BOOL SetHandleInformation( HANDLE hObject, // handle to an object DWORD dwMask, // specifies flags to change DWORD dwFlags // specifies new values for flags ); Les flags sont manipulés dans un champ de bits avec les valeurs suivantes : HANDLE_FLAG_INHERIT (héritabilité du handle) HANDLE_FLAG_PROTECT_FROM_CLOSE (Protection contre la fermeture) La fonction SetHandleInformation permet de modifier le comportement de la fonction CloseHandle. Win32 multitâches - T. Joubert © THEORIS 2005 73 Résumé Caractéristiques des objets kernel Existence dans le système, en dehors du processus Identifiés par des handles au niveau des processus. – Le handle appartient au processus et n’a de sens que pour lui – Automatiquement rendus à la fin du processus. Droits d’accès liés au handle Possibilités d’héritage et de duplication des handles La destruction d’un objet kernel intervient lorsque le compteur du handle est nul ou que le comportement du CloseHandle a été modifié. Win32 XP - © Theoris 2005 Objet Process Création CreateProcess / lancement pgrm Obtention handle GetCurrentProcess / CreateProcess (handle vers fils) CreateThread / CreateThread / Thread CreateProcess CreateProcess / (handle vers thread lancement pgrm principal du fils) CreateMutex CreateMutex / Mutex OpenMutex Sémaphore CreateSemaphore CreateSemaphore / OpenSemaphore CreateEvent / Evénement CreateEvent OpenEvent CreateFileMapping CreateFileMapping / Fichier OpenFileMapping mappé CreateFile CreateFile Fichier Win32 multitâches - T. Joubert © THEORIS 2005 74 destruction synchro. ExitProcess / TerminateProcess / fin du pgrm ExitThread / TerminateThread / fin fonction principale thread WaitFor... CloseHandle 74 Chapitre 5 Gestion structurée des exceptions Win32 multitâches - T. Joubert © THEORIS 2005 75 Plan du chapitre A quoi cela sert-t-il? Gestionnaire de terminaison Filtre et gestionnaire d’exceptions Exceptions Win32 Exceptions non gérées Win32 XP - © Theoris 2005 76 L’utilisation des exceptions structurées dans un programme permet d’améliorer la lisibilité du code: • Traitement des erreurs séparé du code principal • Signalisation des erreurs par un mécanisme plus puissant que l’utilisation de code de retour Le code est alors plus maintenable. Ce mécanisme permet aussi bien de traiter les exceptions logicielles que les erreurs matérielles représentées par des exceptions (ex: division par zéro, accès à de la mémoire protégée). La gestion structurée des exceptions (SEH) ne doit pas être confondue avec les exceptions du C++ (try catch). Il s’agit ici d’un service intégré dans le système Windows, néanmoins le mécanisme des exceptions structurées peut être utilisé dans un programme C++. Win32 - Vol 2 - Chapitre 64 - Gestion structurée des exceptions Win32 multitâches - T. Joubert © THEORIS 2005 76 Gestionnaire de terminaison Le bloc sous surveillance clause Le gestionnaire de terminaison clause __try { instructions } __finally { instructions } Le bloc de terminaison est toujours exécuté! Exécution de toutes les instructions du bloc try Occurrence de «return» dans le bloc try Occurrence d’une instruction de rupture de séquence comme «break», «goto», «longjump» ... Erreur à l’exécution du bloc try La clause leave permet de sortir du bloc try Win32 XP - © Theoris 2005 77 Le principe du gestionnaire de terminaison est de garantir l’exécution d’un bloc de programme (le gestionnaire de terminaison) quelque soit la façon de se terminer d’un autre bloc de programme (le bloc sous surveillance). // A: Code avant try __try { // B: Code du bloc sous surveillance } __finally { // C: Code du gestionnaire de terminaison } // D: Code après finally Dans tous les cas suivants, après l’exécution du bloc A: 1) Exécution de tout B puis de C et enfin de D. 2) Si B contient une clause return , B est exécuté jusqu’à cette clause, puis C est exécuté et la procédure return est traitée. D n’est jamais exécuté. Effet de bord à craindre s’il y a un return dans le bloc finally!!! 3) Si B sontient une clause goto, B est exécuté jusqu’à cette clause, puis C est exécuté et l’exécution se poursuit au niveau du label de saut (dans D ou dans A). Il en est de même de clauses break, continue, longjump ... 4) Si une erreur (récupérée ou pas) intervient dans le bloc B, l’exécution du bloc C finally sera tout de même effectuée. La fonction intrinséque AbnormalTermination peut être utilisée dans le bloc finally pour savoir si l’exécution de ce bloc suit une fin normale du bloc try associé ou provient d’un détournement quelqu’en soit la cause. La clause leave employée dans un bloc B try permet de sauter à la fin du bloc try; ensuite il y a donc exécution du bloc C finally puis du bloc D. Cette solution est beaucoup plus performante que l’emploi d’un return ou d’un goto hors bloc B car elle simplifie le traitement à effectuer par le compilateur pour intercepter les déroutements de code. Win32 multitâches - T. Joubert © THEORIS 2005 77 Gestionnaire d’exceptions Le bloc sous contrôle clause Le __try { instructions } gestionnaire d’exception et son filtre clause __except ( expression filtre ) { instructions } l’expression filtre indique comment traiter l’exception: – EXCEPTION_EXECUTE_HANDLER – EXCEPTION_CONTINUE_SEARCH – EXCEPTION_CONTINUE_EXECUTION La fonction intrinsèque GetExceptionCode ne peut pas être appelée ailleurs que dans l’expression filtre Win32 XP - © Theoris 2005 78 Le gestionnaire d’exception fonctionne un peu de la même manière; un bloc __try est mis sous le contrôle d’un gestionnaire d’exception et son filtre déclaré par la clause __except. L’exécution du gestionnaire d’exception n’est pas systématique mais liée à l’occurrence d’une exception pendant l’exécution du bloc sous contrôle. A la différence des gestionnaires de terminaison, l’exécution du gestionnaire d’exception est plus sous le contrôle du système que lié à l’implémentation du compilateur. Il n’est pas possible pour un même bloc try d’avoir à la fois un gestionnaire de terminaison finally et un gestionnaire d’exception except. En revanche, il est possible d’imbriquer des blocs try-finally et des blocs try-except et vice-versa. Si une exception se produit, l’expression filtre est évaluée et détermine le traitement à effectuer suivant la valeur: EXCEPTION_EXECUTE_HANDLER: Exécuter le gestionnaire puis poursuivre après le bloc D except. EXCEPTION_CONTINUE_EXECUTION: L’exécution peut reprendre à l’endroit où s’est produite l’exception. Ce cas là doit être envisagé par prudence car il n’est pas facile de prévoir à quel endroit précis reprendra l’exécution (une instruction C!=une instruction assembleur ayant produit l’erreur). EXCEPTION_CONTINUE_SEARCH: Le gestionnaire d’exception ne doit pas être exécuté; le système recherche un bloc try-except «précédent». L’évaluation de l’expression filtre peut bien sûr être plus complexe qu’une simple constante, il est possible d’appeler une fonction qui retournera une des trois valeurs tout en effectuant certains tests ou d’autres opérations spécifiques. La fonction intrinsèque GetExceptionCode retourne la valeur de l’exception qui a provoqué l’évaluation du filtre; cette fonction doit être appelée au niveau du filtre ou au sein du gestionnaire (mais on peut toutefois appeler une fonction dans le filtre en lui passant cette valeur comme paramètre). Win32 multitâches - T. Joubert © THEORIS 2005 78 Exemple de détournements: void Fonction1 (void) { // 1. Quelques traitements ... __try { // 2. Ce code s’exécute et possède un filtre Fonction2(); // Ce code ne s’exécute jamais (cf le reste) } __except (/* 6. Evaluation du filtre */ EXCEPTION_EXECUTE_HANDLER) { // 8. Exécution du gestionnaire ... } // 9. Reprendre après l’exécution du try-except ... } void Fonction2 (void) { // 3. L’exécution se poursuit dans la fonction ... __try { // 4. Un code avec un gestionnaire de terminaison ... // 5. Une instruction provoquant une exception Instruction_provoquant_l_exception; ... } __finally { // 7. Exécution du gestionnaire de terminaison // puis que l’on va sortir du bloc sous contrôle ... } // Jamais exécuté ... } Win32 multitâches - T. Joubert © THEORIS 2005 79 Win32 et SEH Liste des exceptions EXCEPTION_XXX dans Winbase.h Le codage des valeurs des exceptions Win32 Bits 31-30: sévérité (info,avertissement,erreur) Bit 29: 0=code Microsoft, 1=code client Bits 27-16: code de service Bits 15-0: code d’erreur Exception logicielle: RaiseException Accéder au contexte: GetExceptionInformation Win32 XP - © Theoris 2005 80 Quelques exemples d’exceptions: • EXCEPTION_ACCESS_VIOLATION • EXCEPTION_FLT_DIVIDE_BY_ZERO • EXCEPTION_ILLEGAL_INSTRUCTION • EXCEPTION_ GUARD_PAGE • EXCEPTION_STACK_OVERFLOW • EXCEPTION_ ... Le codage des valeurs d’exception suit une règle assez précise comme énoncé dans l ’encadré. L’utilisateur peut créer de nouveaux codes d’exception, il pourra provoquer une exception (exception logicielle) en appelant la primitive RaiseException. VOID RaiseException(DWORD dwExceptionCode, DWORD dwExceptionFlags, DWORD cArguments, CONST DWORD * lpArguments); // exception code // continuable exception flag // number of arguments in array // address of array of args Le code de l’exception logicielle créé doit suivre la règle d’écriture énoncée, le bit 28 sera toujours forcé à 0. Le drapeau de continuation vaut 0 ou EXCEPTION_NONCONTINUABLE. Si un filtre d’exception demande de poursuivre l’exécution après une exception non continuable, une nouvelle exception EXCEPTION_NONCONTINUABLE_EXCEPTION est levée. Les deux derniers paramètres permettent de préciser le nombre et la valeur du contexte de l’exception (peut être «0,NULL»). La primitive GetExceptionInformation permet d’accéder à des informations sur le contexte de l’exception. Les informations sont découpées en 2 parties, une partie indépendante du processeur et une partie liée au processeur. Cette fonction est appelable depuis le filtre d’exception uniquement et les données doivent être sauvegardées si l’on souhaite accéder aux informations en dehors du filtre lui-même. Win32 multitâches - T. Joubert © THEORIS 2005 80 Exceptions non gérées La recherche de gestionnaire Si l’exécution est sous le contrôle d’un debugger, le debugger est averti qui fixe la suite à tenir L’exception se «propage» tant qu’aucun gestionnaire n’est rencontré et surtout tant que les filtres rencontrés retournent EXCEPTION_CONTINUE_SEARCH Lorsque tout le thread est parcouru, le système rencontre le gestionnaire par défaut La gestion par défaut Affichage du contexte (adresse, type d’exception) Terminaison de l’application ou débug immédiat Win32 XP - © Theoris 2005 81 La recherche d’un gestionnaire d’exception s’effectue lorsque le programme n’est pas sous le contrôle d’un debugger. Les différents filtres d’exception sont évalués jusqu’à trouver le filtre en question, ou à arriver au filtre de gestion par défaut. En fait, à la création d’un thread, le système appelle une fonction de ce type, ce qui explique comment le système trouve toujours un filtre et un gestionnaire d’exception dans son parcours. void StartofThread(LPTHREAD_START_ROUTINE lpStartAddr,LPVOID lpParam) { __try { ExitThread(lpStartAddr(lpParam)); } __except (UnhandledExceptionFilter(GetExceptionInformation())) { ExitProcess(GetExceptionCode()); } } La gestion par défaut affiche une fenêtre permettant de mettre fin à l’exécution du processus fautif ou de lancer un debugger. La primitive SetUnhandledExceptionFilter permet de spécifier une autre fonction de filtre d’exception par défaut pour le processus. La fonction mise en place doit retourner l’une des trois valeurs habituelles: EXCEPTION_EXECUTE_HANDLER: Comme il n’y a pas de gestionnaire, cette valeur entraine la fin immédiate du processus. Cette méthode permet donc de faire ce qu’il faut au niveau de notre fonction, puis de terminer le processus sans l’affichage de la fenêtre par défaut. EXCEPTION_CONTINUE_EXECUTION: Tente la reprise de l’exécution. EXCEPTION_CONTINUE_SEARCH: Exécution du filtre par défaut. Win32 multitâches - T. Joubert © THEORIS 2005 81 Résumé Un mécanisme puissant ... ... à manipuler avec précaution rattrapage d’erreurs matérielles et logicielles mécanismes variés de traitement de l’exception le déroutement en cas d’exception n’est pas toujours facile à suivre même si les règles sont précises Une implémentation variable suivant les compilateurs utilisés blocs __try {…} __finally {…} blocs __try {... } __except {…} Win32 XP - © Theoris 2005 82 Contrairement aux exceptions synchrones du C++, La gestion des Exceptions Structurée est dite asynchrone et elle ne permet pas de manipuler des types spécifiques (elle est limitée aux UNSIGNED INT). On peut toutefois coupler ces deux modèles en « transformant » une exception SEH en exception C++. Il faut pour cela mettre en place une fonction de traduction avec la primitive Win32 _set_se_translator(…) : // classe d'encapsulation des exceptions SHE ///////// class SE_Exception { private: SE_Exception() {} unsigned int nSE; public: SE_Exception(unsigned int n) : nSE(n) {} ~SE_Exception() {} unsigned int getSeNumber() { return nSE; } }; // fonction de traduction SEH -> C++ ////////////////// void trans_func( unsigned int u, _EXCEPTION_POINTERS* pExp ) { throw SE_Exception( u ); } //////////////////// MAIN ////////////////////////////// int main() { _set_se_translator( trans_func ); try { … } catch( SE_Exception e ) { … switch(e.getSeNumber()) … } } Win32 multitâches - T. Joubert © THEORIS 2005 82 Chapitre 6 Utilisation de la mémoire Win32 multitâches - T. Joubert © THEORIS 2005 83 Plan du chapitre Mémoires ... Types et méthodes d’accès Mapping mémoire Mémoire virtuelle Mémoire Physique Heap Win32 Thread Local Storage (tls) Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 84 84 Accès à la mémoire Mapping mémoire Mémoire virtuelle: réservation Réserver une partie de l’espace d’adressage du processus. Mémoire physique: allocation 32 bits d’adressage = 4Go pour chaque processus une mémoire segmentée et protégée S’approprier une partie de l’espace physique du système. Cette mémoire est très loin d’atteindre 4Go par processus! Les Heap Win32 Win32 XP - © Theoris 2005 85 Sous Win32, chaque processus dispose d’un espace d’adressage de 32 bits, ce qui correspond à 4Go! Cet espace d’adressage est partitionné et protégé sous Windows NT et il en découle une grande robustesse quant aux bugs d’adressage. Pour commencer, une application doit réserver une région de son espace d’adressage, elle accède ainsi à de la mémoire virtuelle. Ensuite, l’application doit s’allouer un espace physique à l’intérieur d’une région réservée dans la mémoire virtuelle. Il n’est pas obligatoire de s’allouer la totalité d’une région réservée; il est possible de procéder en plusieurs fois et même de ne pas allouer physiquement certaines zones. Par contre, il est obligatoire d’effectuer cette allocation avant de pouvoir accéder à la zone mémoire souhaitée. Alors que le mécanisme de mémoire virtuelle/mémoire physique est surtout utilisé pour gérer l’allocation de grandes zones de stockage de données, le Heap est destiné au stockage de nombreuses petites structures. Win32 multitâches - T. Joubert © THEORIS 2005 85 Mécanisme de pagination 31 0 ADRESSE VIRTUELLE SUR 32 BITS 10 bits PDE 0 PDE 1 10 bits PTE 0 PTE 1 12 bits octet RAM Pagination par la CPU PDE 1023 Page Directory PROCESSUS PTE 1023 Page Tables Win32 XP - © Theoris 2005 Disque(s) 86 Lorsqu’une adresse virtuelle sur 32 bits est fournie à la CPU celle-ci n ’accède pas directement à un octet dans la RAM, elle effectue un décodage en plusieurs étapes qui s ’appuie sur un mécanisme de pagination (câblé en interne !). Lorsque la taille de page est de 4096 octets : • Les 10 bits de poids fort sont utilisés comme un index dans une première page dite Page Directory qui est initialisée avec le processus. La cellule PDE (Page Directory Entry) correspondante donne la référence d ’une seconde page dite Page Table. • Les 10 bits intermédiaires sont utilisés comme un index dans la Page Table pour obtenir une cellule PTE (Page Table Entry) qui donne la référence d ’une troisième page dans laquelle on va trouver les données applicatives. • Les 12 derniers bits sont utilisés comme un index dans la page de données applicatives pour accéder à un octet dans la RAM. Les PDE et PTE sont des structures qui référencent des entrées dans une table système qui est la table des pages. Les pages utiles au mécanisme ci-dessus sont créées au fur et à mesure des demandes de mémoire effective par l ’application, pour chacune d ’entre elles on va avoir une entrée dans la table des pages. Le mécanisme de pagination a pour objectif d ’optimiser l ’usage de la ressource mémoire RAM. Chaque page valide a en effet automatiquement une image sur le disque (fichier de swap ...). Lors d ’un décodage d ’adresse virtuelle toute page valide mais absente de la RAM est automatiquement « montée » depuis son image disque. Win32 multitâches - T. Joubert © THEORIS 2005 86 Mapping Windows NT 0xFFFF FFFF Zone réservée à Windows NT Tout accès se traduit par un arrêt du processus fautif 2Go P3 64Ko P2 P1 0x8000 0000 0x7FFF 0000 Zone d’adressage privée du processus 2Go-128Ko 64Ko Zone réservée à la détection des pointeurs invalides - Inutilisée. 0x0001 0000 0x0000 0000 Zone réservée à la détection des pointeurs invalides - Inutilisée. Win32 XP - © Theoris 2005 87 Sous Windows NT, l’espace d’adressage d’un processus est décomposé en 4 parties: • 2 espaces (128ko au total) sont utilisés pour la détection de pointeurs invalides. Tout accès dans l’un de ses espaces provoque une violation d’accès. • L’espace 0x10000-0x7FFF000 (2Go - 128 Ko) contient les données privées du processus. C’est dans cet espace que l’on trouvera la Stack des threads, les données du processus, les DLL ... • L’espace 0x80000000-0xFFFFFFFF (2Go) contient le système Windows NT, les drivers... Tout accès par le code utilisateur dans cet espace provoque une violation d’accès et l’arrêt du processus fautif. Cette règle sécurise beaucoup Windows NT. Win32 multitâches - T. Joubert © THEORIS 2005 87 Architecture mémoire Granularité de la mémoire Une région est toujours alignée sur un multiple de l’unité d’allocation (64Ko) La taille d’une région correspond toujours à un multiple pair de pages systèmes (4Ko ou 8Ko) Attributs de protection Chaque page physique est dotée d’attributs d’accès. Les attributs ne sont pas tous gérés sur tous les processeurs. Espace physique RAM complétée par de(s) fichier(s) d’échange et image Win32 XP - © Theoris 2005 88 Afin de simplifier et d’optimiser la gestion de la mémoire, la réservation et l’allocation de mémoire s’effectuent en accord avec un découpage prédéfini de la mémoire, on parle alors de granularité. La granularité n’est pas fixe d’un système à l’autre, elle dépend de la plateforme utilisée et sera amenée à évoluer dans le futur. Il est donc obligatoire pour le développeur d’application de ne pas se fixer sur les valeurs actuelles. La réservation d’une région dans l’espace d’adressage est toujours alignée sur un multiple de l’unité d’allocation (aujourd’hui 64Ko) et la taille de cette région est un multiple de la taille d’une page système (4Ko ou 8Ko). La primitive Win32 GetSystemInfo permet de récupérer ces valeurs. Le système est tolérant, il accepte de traiter une demande non conforme et rétablit automatiquement la demande. Par exemple, la réservation d’une région de 6Ko à une adresse non alignée est acceptée et provoque l’allocation d’une zone de 8Ko alignée sur l’adresse inférieure la plus proche de celle demandée. Chaque page physique est dotée d’attributs de protection: • PAGE_NOACCESS • PAGE_READONLY (ni écriture, ni exécution) • PAGE_READWRITE (pas d’exécution) • PAGE_EXECUTE (ni lecture, ni écriture) • PAGE_EXECUTE _READ (pas d’écriture) • PAGE_EXECUTE_READWRITE • PAGE_WRITECOPY • PAGE_EXECUTE_WRITECOPY Toute tentative d’écriture dans une page dotée de PAGE*_WRITECOPY provoque la recopie de la page. Win32 multitâches - T. Joubert © THEORIS 2005 88 Le système gère deux attributs complémentaires qui peuvent s’ajouter aux précédents: • PAGE_NOCACHE: supprime le mécanisme de «cache» sur la page. Cet attribut est surtout destiné aux drivers. • PAGE_GUARD: l’accès à la page provoque l’exception STATUS_PAGE_GUARD. L’attribut est automatiquement enlevé après la première signalisation. La mémoire physique est composée de 3 espaces: • La RAM • Les fichiers d’échange sont utilisés pour étendre la mémoire réelle en utilisant de l’espace disque. La taille et le nombre de fichiers d’échange sont configurables (Windows NT permet d’avoir plusieurs fichiers d’échanges). Le système gère ces fichiers, il crée des pages physiques et les détruits automatiquement. Le système utilise les exceptions «défaut de page» pour effectuer des transferts entre la RAM et le(s) fichier(s) d’échange. • Les fichiers images et les fichiers mappés fonctionnent de la même façon mais se réfèrent à des fichiers qui ont une existence propre en dehors de la simple allocation mémoire. En particulier, le système utilise ce mécanisme pour les exécutables et les librairies dynamiques. Win32 multitâches - T. Joubert © THEORIS 2005 89 Mémoire virtuelle Informations sur la mémoire GetSystemInfo dwPageSize: 4Ko ou 8Ko (Alpha) dwAllocationGranularity: 64Ko GlobalMemoryStatus, VirtualQuery VirtualAlloc MEM_RESERVE: réservation de mémoire virtuelle L’offset est aligné sur l’unité d’allocation et la taille de la région est multiple de la taille de la page système VirtualFree MEM_RELEASE: libération de mémoire virtuelle La libération ne peut pas être partielle Win32 XP - © Theoris 2005 90 Le système propose plusieurs routines pour accéder à des informations concernant la mémoire. La routine GetSystemInfo permet d’obtenir beaucoup d’informations concernant les possibilités d’allocation, le mapping et même le(s) processeur(s). Nous retiendrons surtout la possibilité d’obtenir l’unité d’allocation utilisée pour l’alignement des réservations de mémoire virtuelle et la taille de la page système qui fixe l’unité minimale d’allocation. VOID GetSystemInfo(LPSYSTEM_INFO lpSystemInfo); D’autres routines comme GlobalMemoryStatus et VirtualQuery permettent d’obtenir des informations dynamiques sur la mémoire. LPVOID VirtualAlloc(LPVOID DWORD DWORD DWORD lpvAddress, cbSize, fdwAllocType, fdwProtect); // address of region to reserve (or NULL) // size of region (bytes) // type of allocation // type of access protection La routine VirtualAlloc permet de réserver une région dans l’espace d’adressage du processus. La routine retourne l’adresse de base de la région réservée ou NULL si la réservation échoue. Le type d’allocation est MEM_RESERVE, il peut être complété par l’attribut MEM_TOP_DOWN qui force l’allocation par le haut de l’espace d’adressage. Le paramètre de protection est un attribut de protection de page, Microsoft indique que le système est plus performant si cet attribut est déjà précisé avant l’allocation de mémoire physique sans que l’on puisse réellement savoir pourquoi. Les valeurs PAGE_*WRITECOPY ne sont pas acceptées et provoquent une erreur (utilisées ici, elles n’ont aucun sens). Il est possible de forcer l’adresse de base de la région recherchée, le système essaie de répondre à cette requête. Win32 multitâches - T. Joubert © THEORIS 2005 90 Enfin, rappelons que le système retourne ou recalcule l’adresse demandée pour avoir une adresse alignée sur l’unité d’allocation et que la taille réelle de la région est calculée pour être multiple de la taille d’une page système. BOOL VirtualFree( LPVOID lpvAddress, // address of region of committed pages DWORD cbSize, // size of region DWORD fdwFreeType); // type of free operation La routine VirtualFree permet de libérer une région réservée par la routine VirtualAlloc. Il est obligatoire de passer l’adresse de base retournée précédemment, de passer 0 comme taille de la région et MEM_RELEASE comme type d’opération de libération. Il n’est pas possible de libérer partiellement la région. Win32 multitâches - T. Joubert © THEORIS 2005 91 Mémoire physique VirtualAlloc MEM_COMMIT: allocation de mémoire physique Allocation d’ un nombre entier de pages Positionne le véritable attribut de protection VirtualAlloc(A+10Ko,7Ko,MEM_COMMIT,...) Région allouée Région réservée A +4k +8k +12k +16k +20k VirtualAlloc(A+10Ko,50Ko,MEM_RESERVE,...) Win32 XP - © Theoris 2005 A+64Ko 92 L’allocation de mémoire physique utilise les mêmes routines VirtualAlloc et VirtualFree mais avec d’autres paramètres. Cela explique pourquoi les routines utilisées pour la réservation de mémoire virtuelle semblent avoir des paramètres en trop. L’allocation de mémoire physique s’effectue toujours au sein d’une région précédemment réservée et dont l’application connaît l’adresse de base et la taille. A l’appel de VirtualAlloc, l’application précise une adresse et un nombre d’octets compatibles avec la région réservée. Le système alloue alors toutes les pages physiques concernées. Le type d’allocation est alors MEM_COMMIT et l’attribut de protection précisé sera celui utilisé pour les pages allouées. Il est tout à fait possible de préciser un attribut différent pour la réservation de la région et l’allocation de pages physiques. Il est possible d’effectuer la réservation et l’allocation en même temps (mais on alloue alors tout l’espace réservé) en précisant MEM_RESERVE|MEM_COMMIT comme type d’allocation. Win32 multitâches - T. Joubert © THEORIS 2005 92 Mémoire physique VirtualFree VirtualLock / VirtualUnlock MEM_DECOMMIT: désallocation de mémoire physique La désallocation peut être partielle S’assurer que l’espace physique est en RAM VirtualProtect Changer les attributs de protection Win32 XP - © Theoris 2005 93 Comme pour la libération d’une région, la routine VirtualFree permet de désallouer des pages physiques. Dans ce cas, le type de l’opération de libération est MEM_DECOMMIT. La désallocation concerne toutes les pages où se trouvent les octets dans l’intervalle Adresse..Adresse+Taille-1. En particulier, d’autres octets avant et après cet intervalle pourront aussi disparaître s’ils font partie des mêmes pages. Il est donc possible de ne désallouer que certaines pages de façon sélective; ce n’est pas aussi symétrique que pour la réservation/libération de régions. Si l’adresse est l’adresse de base de la région et si la taille vaut zéro, la routine libère la plage complète des pages de la région. La routine VirtualLock (sous Windows NT) permet de demander au système de conserver les pages concernées en mémoire tant que le processus a un thread en action. La routine VirtualUnlock supprime cette contrainte imposée au système. Le nombre de pages ainsi bloquées est limité par le système mais peut être paramétré (défauts +/- 30 pages). Cela peut sembler intéressant pour une application «temps réel» mais il ne faut oublier que cette règle ne s’applique que lorsque le processus est en action et que cela peut aussi pénaliser le système qui dispose de moins de pages et peut donc être obligé à plus d’échanges par ailleurs. BOOL VirtualLock(LPVOID lpvAddress,DWORD cbSize); BOOL VirtualUnlock(LPVOID lpvAddress,DWORD cbSize); Enfin, la routine VirtualProtect permet de modifier les attributs de protection des pages spécifiées et retourne l’ancien attribut pour la première page. BOOL VirtualProtect(LPVOID lpvAddress, DWORD cbSize, DWORD fdwNewProtect, PDWORD pfdwOldP); // address of region of committed pages // size of the region // desired access protection // address of variable to get old protection Win32 multitâches - T. Joubert © THEORIS 2005 93 La gestion des piles (NT) 0x0100 3000 Allouée + 0x0100 4000 Allouées 0x0100 5000 Allouée gardée 0x0100 4000 Allouée 0x0100 3000 Réservée 0x0100 2000 0x0100 2000 Réservée 0x0100 1000 Stack utilisée Allouée ... Allouées 0x0100 5000 0x0110 0000 Stack utilisée ... 0x0110 0000 Allouée gardée Réservée 0x0100 1000 Réservée 0x0100 0000 Réservée 0x0100 0000 Win32 XP - © Theoris 2005 94 L’implémentation de la Stack dans un thread est un bon exemple du fonctionnement d’allocation mémoire. Il ne s’agit pas ici, d’examiner toutes les caractéristiques de la gestion des piles sous Windows NT mais seulement de s’appuyer sur cette gestion pour illustrer la gestion de la mémoire. Par défaut, à la création d’un thread, une Stack de 1Mo est créée: le système réserve 1 Mo de mémoire virtuelle. Cette région est découpée en N pages (N=256 si des pages de 4Ko) et seulement 2 pages physiques sont allouées: les 2 plus hautes (appelons les P1 et P2) . Le pointeur de Stack est alors placé à la plus haute adresse dans la page P1. Les deux pages sont accessibles en lecture et en écriture mais la page P2 possède en plus l’attribut PAGE_GUARD. Lorsque l’application se déroule, elle utilise plus ou moins de Stack, lorsque l’accès se fera dans la page P2 protégée, le système sera averti. Il allouera la page P3 et placera l’attribut PAGE_GUARD sur cette nouvelle page. Notons au passage que P2 perd automatiquement cet attribut dès lors que la signalisation est effectuée une fois. La place physique réellement nécessaire à l’exécution de l’application est donc allouée au fur et à mesure des besoins sans trop d’allocation inutile (entre 4Ko et 8Ko). Il n’y a pas de désallocation! (sauf à la fin du thread). Lorsque P254 gardée est accédée, le système procède différemment. Dans ce cas, il ne reste que P255 et P256 non allouées. Par sécurité, P256 n’est jamais allouée et P255 est allouée sans PAGE_GUARD car il n’est pas utile d’être averti de l’utilisation de P255 puisque l’on ne peut pas préparer P256. Si la Stack continue encore de croître, l’accès à P256 provoque un débordement de Stack: on a atteint la limite de la mémoire virtuelle allouée pour la Stack. Le paramètre «/STACKSIZE reserve,commit» du linker permet de préciser la taille de la réservation de la mémoire virtuelle (reserve) et la taille de l’allocation de mémoire physique (commit). A la création d’un thread (CreateThread), il est aussi possible de préciser la taille de l’allocation physique de la Stack à créer pour le thread. Win32 multitâches - T. Joubert © THEORIS 2005 94 Heap Win32 (heaps) Allocation simplifiée de mémoire physique Heap et multitâches Win32 gère la réservation et l’allocation de mémoire Win32 nous masque la notion de page malloc() utilise le heap Sérialisation automatique des demandes Avec HEAP_NO_SERIALIZE le système ne protège plus le heap contre des accès concurrents => danger HeapAlloc / HeapReAlloc HeapFree Win32 XP - © Theoris 2005 95 Les Heap Win32 permettent d’allouer facilement un grand nombre de petits objets; si l’on souhaitait utiliser les mécanismes précédents cela reviendrait à allouer des pages physiques et à affecter par petits bouts des zones dans ces pages en gérant le calcul d’adresse au sein des pages, la libération, la fragmentation qui en résulte ... Ici le système fait cela pour nous!. Lorsque plusieurs opérations sont demandées sur un même heap, le système sérialise les opérations afin d’éviter toute corruption des données internes du heap. Ce fonctionnement est sûr mais il est pénalisant en termes de performances. La routine HeapAlloc permet d’allouer un bloc de mémoire dans le heap. La taille est donnée en octets (plus de notion de page ou d’alignement). Les options (control flags) possibles sont HEAP_GENERATE_EXCEPTIONS , HEAP_NO_SERIALIZE et HEAP_ZERO_MEMORY. La valeur HEAP_GENERATE_EXCEPTIONS demande au système de générer des exceptions dans certains cas plutôt que de retourner un simple status d’erreur. Il est quelquefois plus facile d’utiliser cette technique de gestion des erreurs. La valeur HEAP_NO_SERIALIZE doit être manipulée avec beaucoup de précautions. Elle demande au système de ne plus sécuriser les accès au heap en sérialisant les demandes. Ce drapeau doit être utilisé si d’autres mécanismes d’exclusion mutuelle sont mis en place par l’application, ou si l’application assure qu’aucun accès concurrent n’est possible dans son architecture. HEAP_ZERO_MEMORY, comme son nom l’indique, demande le remplissage de la zone allouée par des zéros. LPVOID HeapAlloc(HANDLE hHeap, DWORD dwFlags, DWORD dwBytes); Win32 multitâches - T. Joubert © THEORIS 2005 // handle of the private heap block // heap allocation control flags // number of bytes to allocate 95 La routine HeapFree permet de libérer un bloc de mémoire allouée par HeapAlloc. Le système connaît la taille du bloc à libérer. Les options possibles sont 0 ou HEAP_NO_SERIALIZE. BOOL HeapFree(HANDLE hHeap, DWORD dwFlags, LPVOID lpMem); // handle of the heap // heap freeing flags // address of the memory to free Enfin, la routine HeapReAlloc permet de modifier la taille d’un bloc de mémoire déjà alloué dans un heap. Le nouveau bloc peut se trouver dans une autre zone de la mémoire allouée au même heap (sauf demande spécifique), donc avec une nouvelle adresse. Nous retrouvons les paramètres désormais habituels. La taille peut être réduite ou augmentée par rapport à la taille précédente. Comme le système connaît la taille actuelle du bloc, il sait dans quel sens se fait la réallocation. Dans tous les cas, la routine retourne l’adresse du bloc mémoire ou, en cas d’échec, retourne NULL ou provoque une exception. La valeur de drapeau HEAP_ZERO_MEMORY ne concerne bien sûr que les augmentations de taille de bloc et seuls les octets complémentaires sont remplis avec des zéros. La nouvelle valeur de drapeau HEAP_REALLOC_IN_PLACE_ONLY demande au système d’effectuer cette réallocation sans déplacer le bloc. Si c’est impossible la demande échoue. LPVOID HeapReAlloc(HANDLE hHeap, DWORD dwFlags, LPVOID lpMem, DWORD dwBytes); // handle of a heap block // heap reallocation flags // address of the memory to reallocate // number of bytes to reallocate Le système nous masque les allocations/libérations de mémoire physique lors des appels à HeapAlloc, HeapReAlloc et HeapFree. Exemple: Allocation d’un buffer de 1000 octets dans le heap désigné par le handle hHeap et libération. char *Buffer; Buffer=(char*)HeapAlloc(hHeap,0,1000); if (Buffer) { /* Allocation Ok */ /* Utilisation de Buffer */ HeapFree(hHeap,0,(LPVOID)Buffer); } Win32 multitâches - T. Joubert © THEORIS 2005 96 Plusieurs heaps GetProcessHeap Retourne le handle du heap par défaut Pourquoi créer d’autres heaps ? – Protection: identification des parties de code autorisées – Segmentation plus adaptée, moins de «trous» perdus – Accès local : optimisation de la pagination HeapCreate HeapDestroy Win32 XP - © Theoris 2005 97 L’API Win32 créé automatiquement un heap utilisé par de nombreuses routines sans que l’on ne soit toujours averti. La taille de ce heap est de 1Mo par défaut, la clause /HEAPSIZE du linker permet de modifier cette taille (cf STACKSIZE). Ce heap est accessible en utilisant la routine GetProcessHeap qui retourne un handle sur le heap par défaut du processus courant. HANDLE GetProcessHeap (VOID) Il est possible et recommandé de créer d’autres heaps et ceci pour plusieurs raisons: • Simplifier la localisation des bogues • Eviter la fragmentation dûe à des allocations de différentes tailles • Bénéficier au mieux des mécanismes de pagination mémoire • Paralléliser les accès aux heap rendus indépendants HANDLE HeapCreate(DWORD flOptions, DWORD dwInitialSize, DWORD dwMaximumSize); // heap allocation flag // initial heap size // maximum heap size La primitive HeapCreate retourne un handle sur un nouveau heap. Il ne s’agit pas d’un handle d’objet kernel (pas d’héritage, d’attributs de sécurité, de mécanismes de synchronisation associés). Le drapeau de gestion du heap vaut normalement 0 mais peut être complété. Les paramètres complémentaires permettent de spécifier la taille initiale du heap et la taille maximale prévue, exprimées en octets. Si la taille maximale est 0, le heap n’est limité que par l’espace mémoire disponible. Par contre, les heap de taille limitée ne pourront pas s’étendre au delà de cette limite mais aussi n’accepteront pas d’allouer un bloc de plus de 524280 octets (0x7FFF8) même si la taille maximale est supérieure à cette limite. Win32 multitâches - T. Joubert © THEORIS 2005 97 La routine HeapDestroy permet de détruire un heap, les éventuels blocs de mémoire sont libérés automatiquement (le heap par défaut ne peut être détruit, l’appel est alors ignoré). BOOL HeapDestroy(HANDLE hHeap); // handle of heap CloseHandle(hHeap) provoque une erreur système Win32 multitâches - T. Joubert © THEORIS 2005 98 Autres routines de gestion des heap HeapSize HeapLock / HeapUnlock Mettre en place un mutex d’accès au heap HeapValidate Récupérer la taille d’un bloc alloué Vérifier la cohérence d’un heap HeapCompact Libérer et défragmenter la mémoire d’un heap Win32 XP - © Theoris 2005 99 La routine HeapSize permet d’obtenir la taille d’un bloc alloué précédemment. DWORD HeapSize(HANDLE hHeap, DWORD dwFlags, LPCVOID lpMem); // handle to the heap // heap size control flags // pointer to memory to return size for Les routines HeapLock/HeapUnlock permettent de restreindre l’accès au heap à un seul thread en agissant comme un mutex. Cette notion est différente de la sérialisation des demandes, effectuée automatiquement par le système puisque les autres threads pourront accéder au heap que lorsque celui ci sera déverrouillé. BOOL HeapLock(HANDLE hHeap); // handle to the heap to lock for exclusive thread access BOOL HeapUnlock(HANDLE hHeap);// handle to the heap to unlock La présence de routines comme HeapValidate et HeapCompact peut étonner, mais elles trouvent une explication. Afin d’optimiser l’accès au heap, le système ne sécurise pas tous les accès (il nous autorise même à inhiber la sérialisation), il effectue aussi une libération rapide mais qui peut être incomplète. Les routines HeapValidate (pour vérifier la cohérence du heap) et HeapCompact (pour libérer la place inutilisée, défragmenter) peuvent donc être utilisées pour vérifier en «tâche de fond» que le heap reste cohérent et qu’il est optimisé. BOOL HeapValidate(HANDLE hHeap, // handle to the heap of interest DWORD dwFlags, // bit flags that control heap access during function LPCVOID lpMem); // optional pointer to individual memory block to UINT HeapCompact(HANDLE hHeap, // handle to the heap to compact DWORD dwFlags ); // bit-flags that control heap access during function Win32 multitâches - T. Joubert © THEORIS 2005 99 Données locale au thread Données de l’instance «thread» Le mécanisme de «Thread Local Storage» permet d’associer des données à une instance de thread. Un minimum assuré de 64 mots de 32 bits Un simple tableau à accès réservé TlsAlloc: réserver un index au niveau du processus pour tous les threads existants et à venir!!! TlsSetValue: Mémoriser une valeur à l’index TlsGetValue: Récupérer une valeur à l’index TlsFree: libérer un index pour tous les threads du processus Win32 XP - © Theoris 2005 100 Le stockage local au thread (Thread Local Storage) est un mécanisme qui permet d’associer des données à une instance de thread: les données sont reliées au thread au niveau du système, un peu comme si elles faisaient partie de son contexte. Le système garanti que 64 mots de 32 bits seront toujours disponibles via ce mécanisme (TLS_MINIMUM_AVAILABLE). Lorsque l’on souhaite utiliser ce mécanisme, il est nécessaire de réserver l’utilisation d’un mot de 32 bits représenté par un index en appelant la primitive TlsAlloc. Elle retourne l’index alloué ou TLS_OUT_OF_INDEXES. On libère cette donnée avec TlsFree. L’important dans ce mécanisme, c’est que la réservation s’effectue au niveau du processus: l’index retourné est réservé pour tous les threads actuels et à venir du processus. Les primitives TlsSetValue et TlsGetValue permettent respectivement d’affecter et de récupérer une donnée de 32 bits représentée par un index. DWORD TlsAlloc(VOID) BOOL TlsFree(DWORD dwTlsIndex) LPVOID TlsGetValue(DWORD dwTlsIndex) BOOL TlsSetValue(DWORD dwTlsIndex, LPVOID lpvTlsValue) // TLS index to free // TLS index to retrieve value for // TLS index to set value for // value to be stored Il est possible d’accéder à une donnée via un index non alloué. Ceci est dû à une implémentation performante mais moins sécurisée de ce mécanisme. Le système place un 0 dans la donnée au moment de l’allocation de son index. Il est possible d’utiliser cette propriété pour contrôler qu’aucune n’affectation n’a été faite pour le thread courant. On peut également déclarer une variable globale comme étant privée à chaque thread avec le modificateur de stockage: __declspec(thread). Win32 multitâches - T. Joubert © THEORIS 2005 100 Exemple 1: Dans cet exemple, on souhaite implémenter (très simplement) un petit module permettant de disposer d’un heap par thread. DWORD indexTasHandler=TLS_OUT_OF_INDEXES; // Initialisation du gestionnaire de heap: un heap par thread void InitThreadHeap() { indexTasHandler=TlsAlloc(); } // Allocation dans le heap du thread LPVOID ThreadHeapAlloc(DWORD dwBytes) { HANDLE hHeap; // Accéder au heap du thread hHeap=(HANDLE)TlsGetValue(indexTasHandler); if (hHeap==0) { // Le heap n’existe pas, en créer un de 4Ko hHeap=HeapCreate(HEAP_NO_SERIALIZE,4096,0); TlsSetValue(indexTasHandler,(LPVOID)hHeap); } return HeapAlloc(hHeap,0,dwBytes); } Exemple 2: Dans cet exemple, on implémente une variable globale pour chaque thread du process. __declspec( thread ) DWORD UnParThread; DWORD UnPourTous // fonction de thread void ThreadFunc() { UnParThread++; // pas de contention UnPourTous; // risque de contention } Win32 multitâches - T. Joubert © THEORIS 2005 101 Résumé Espace d’adressage 4Go par processus découpé en plusieurs zones protégées ou non Mémoire virtuelle: adresses réservées Mémoire physique: adresses allouées Heaps: allocation simplifiée, plusieurs heaps TLS: allocation au niveau des threads Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 102 102 Chapitre 7 E/S - Communication Win32 multitâches - T. Joubert © THEORIS 2005 103 Plan du chapitre Différents types d’E/S Fichiers, fichiers «mappés» Pipes Boites aux lettres (mailslot) E/S série et parallèle (communication ressource) Autres mécanismes: E/S multimédia, librairie C ... E/S Synchrones & asynchrones L’E/S synchrone bloque la suite de l’exécution L’E/S asynchrone se déroule en // de la suite de l’exécution Win32 XP - © Theoris 2005 104 Cette partie va traiter des E/S et au delà des mécanismes de communication entre différents processus. Le choix de tel ou tel support pour cette communication est fondé sur plusieurs paramètres. Un certain nombre de questions doivent être posées avant de trancher pour une méthode ou l’autre. En premier lieu, est-ce une communication qui nécessite un support existant en dehors des processus communicants ? • Est-ce une communication locale ou via un réseau ? • A-t-on besoin d’interopérabilité (différents systèmes & OS) ? • Niveau de couplage, sécurité. • Criticité des échanges. • ... Win32 multitâches - T. Joubert © THEORIS 2005 104 Généralités CreateFile Création/Ouverture d’un fichier Modes et droits d’accès ReadFile WriteFile LockFile, UnlockFile ... Win32 XP - © Theoris 2005 105 HANDLE CreateFile( LPCTSTR lpszName, // address of name of the file DWORD fdwAccess, // access (read-write) mode DWORD fdwShareMode, // share mode LPSECURITY_ATTRIBUTES lpsa, // address of security descriptor DWORD fdwCreate, // how to create DWORD fdwAttrsAndFlags, // file attributes HANDLE hTemplateFile); // handle of file with attributes to copy La routine CreateFile permet de créer mais aussi d’ouvrir un fichier existant. Cette routine remplace le célèbre «fopen» de la librairie C et les fonctions «OpenFile», «_lopen»... de Windows 16 bits. Globalement, on y retrouve les paramètres habituels avec néanmoins quelques compléments. Deux modes d’accès sont possibles et peuvent être combinés GENERIC_READ et GENERIC_WRITE. Le mode de partage permet d’indiquer si l’on souhaite se réserver une exclusivité d’accès (0), un accès partagé avec des lecteurs (FILE_SHARE_READ) ou avec des écrivains (FILE_SHARE_WRITE). Cette notion est indépendante du mode d’accès choisi auparavant. Les drapeaux d’accès permettent d’indiquer comment l’ouverture (la création) doit s’effectuer: • CREATE_NEW: Créer un nouveau fichier (sinon il y a une erreur) • CREATE_ALWAYS: Créer un fichier et le vider (écraser l’existant) • OPEN_EXISTING: Ouvrir un fichier existant • OPEN_ALWAYS: Ouvrir un fichier (en le créant si nécessaire) • TRUNCATE_EXISTING: Ouvrir un fichier existant et le vider Les attributs de fichier sont utilisés à la création (read-only, system, archive,...) ainsi que le handle d’un fichier template pour récupérer ses attributs. Win32 multitâches - T. Joubert © THEORIS 2005 105 Il existe en plus beaucoup d’autres attributs d’accès qui peuvent être spécifiés à l’ouverture. Ces attributs permettent d’indiquer au système comment l’on va utiliser ce fichier afin d’obtenir des spécificités d’accès ou des optimisations; on a par exemple: FILE_ATTRIBUTE_TEMPORARY: le système utilise la mémoire au maximum car ce fichier est un fichier de travail. • FILE_FLAG_OVERLAPPED: Accès asynchrone sur ce fichier • FILE_FLAG_RANDOM_ACCESS: Les accès seront aléatoires • FILE_FLAG_DELETE_ON_CLOSE: Effacement automatique. • ... BOOL ReadFile( HANDLE hFile, // handle of file to read LPVOID lpBuffer, // address of buffer that receives data DWORD nNumberOfBytesToRead, // number of bytes to read LPDWORD lpNumberOfBytesRead, // address of number of bytes read LPOVERLAPPED lpOverlapped); // address of structure for data BOOL WriteFile( HANDLE hFile, // handle of file to write to LPCVOID lpBuffer, // address of data to write to file DWORD nNumberOfBytesToWrite, // number of bytes to write LPDWORD lpNumberOfBytesWritten, // address of number of bytes written LPOVERLAPPED lpOverlapped); // addr. of structure needed for overlapped I/O Les E/S synchrones se caractérisent par le paramètre NULL en fin des 2 routines d’E/S (interdit si le fichier a été ouvert dans le mode asynchrone). L’accès s’effectue depuis un pointeur courant qui est déplacé après l’accès (associé au handle). En mode synchrone, ces fonctions de lecture et d’écriture sont très traditionnelles. L’E/S s’effectue toujours depuis la position courante, automatiquement modifiée après l’opération. Cette position peut aussi être modifiée en utilisant la primitive SetFilePointer (la partie haute peut être NULL). DWORD SetFilePointer( HANDLE hFile, // handle of file LONG lDistanceToMove, // number of bytes to move file pointer PLONG lpDistanceToMoveHigh, // address of word of distance to move DWORD dwMoveMethod); // how to move Win32 multitâches - T. Joubert © THEORIS 2005 106 Comme présenté précédemment, à chaque ouverture du fichier, il est possible de préciser si d’autres accès concurrents sont autorisés et la nature de ces accès (lecture/écriture). Si plusieurs threads accèdent au même fichier, il peut être nécessaire de mettre en place des sécurités d’accès. L’API Win32 a prévu ces routines; deux routines LockFile et UnlockFile permettent de verrouiller une région du fichier, toute autre tentative se traduira par un échec. BOOL LockFile( HANDLE hFile, // handle of file to lock DWORD dwFileOffsetLow, // low-order word of lock region offset DWORD dwFileOffsetHigh, // high-order word of lock region offset DWORD cbLockLow, // low-order word of length to lock DWORD cbLockHigh); // high-order word of length to lock BOOL UnlockFile( HANDLE hFile, // handle of file to unlock DWORD dwFileOffsetLow, // low-order word of lock region offset DWORD dwFileOffsetHigh, // high-order word of lock region offset DWORD cbUnlockLow, // low-order word of length to unlock DWORD cbUnlockHigh); // high-order word of length to unlock Les routines LockFileEx et UnlockFileEx (sous Windows NT) vont un peu plus loin, elles permettent de se mettre en attente si l’accès n’est pas possible mais aussi d’effectuer un verrouillage plus personnalisé (accès exclusif ou accès concurrent autorisé en lecture) et gèrent l’accès asynchrone. Il existe beaucoup d’autres routines (très classiques) pour accéder aux fichiers dans l’API Win32, ces routines permettent de gérer les répertoires, d’accéder aux attributs des fichiers, de faire des copies ... Exemples: 1) Lecture d’un fichier existant hFic=CreateFile(‘‘Fichier’’,GENERIC_READ,0,NULL,OPEN_EXISTING,0,NULL); if (hFic!=INVALID_HANDLE_VALUE) { ReadFile(hFic,Buffer,100,&CarLus,NULL); CloseHandle(hFic); } 2) Création d’un nouveau fichier hFic=CreateFile(‘‘Fichier’’,GENERIC_WRITE,0,NULL,CREATE_NEW,FILE_ATTRIBUTE_NORMAL, NULL); if (hFic!=INVALID_HANDLE_VALUE) { WriteFile(hFic,Buffer,100,&CarEcrits,NULL); CloseHandle(hFic); } Win32 multitâches - T. Joubert © THEORIS 2005 107 E/S asynchrones La structure OVERLAPPED Elle permet de gérer l’E/S asynchrone L’offset 64 bits SetFilePointer ne doit pas être utilisée L’offset est précisé à l’appel de la routine GetOverlappedResult Cette routine permet d’attendre la fin de l’opération Signalisation de fin d’une opération La signalisation par l’objet kernel fichier La signalisation par un objet kernel événement Win32 XP - © Theoris 2005 108 Les opérations d’E/S asynchrones permettent au thread de poursuivre l’exécution en parallèle de l’opération d’E/S (Overlapped=asynchrone). L’accès asynchrone à un fichier est précisé à son ouverture et les opérations d’E/S doivent obligatoirement se faire suivant ce mode d’accès: si le fichier est ouvert en mode synchrone, il ne peut y avoir que des E/S synchrones; si le fichier est ouvert dans le mode asynchrone il ne peut y avoir que des opérations asynchrones. L’utilisation de l’accès asynchrone nécessite l’emploi d’une donnée de type OVERLAPPED qui doit être initialisée avant tout appel. typedef struct _OVERLAPPED { DWORD Internal; DWORD InternalHigh; DWORD Offset; DWORD OffsetHigh; HANDLE hEvent; } OVERLAPPED; Lors de l’initiation d’un requête d’E/S asynchrone, il est obligatoire de préciser la position de début de l’opération dans le fichier. Dans les opérations synchrones cette position est gérée automatiquement ou modifiée par SetFilePointer mais il n’est pas possible d’utiliser cette routine dans le mode asynchrone: chaque opération doit disposer de son propre pointeur dans le fichier. Ce pointeur est représenté sur 64 bits, il est décomposé en 2 mots de 32 bits. Ces 2 mots sont des composantes de la structure OVERLAPPED passée en paramètre à l’appel d’une E/S asynchrone. Le code retour d’une routine d’E/S asynchrone indique si l’opération est acceptée mais n’indique bien sûr pas si elle est terminée et dans quels termes. Pour cela il existe plusieurs méthodes. Win32 multitâches - T. Joubert © THEORIS 2005 108 Premièrement, la routine GetOverlappedResult permet de savoir si l’opération est terminée et dans quelles conditions (compte-rendu, nb d’octets transférés). Si l’opération n’est pas terminée, la routine retournera FALSE et l’appel à la routine GetLastError indiquera ERROR_IO_INCOMPLETE. Il est aussi possible de se mettre en attente de la fin de l’opération (fWait=TRUE). BOOL GetOverlappedResult( HANDLE hFile, // handle of file, pipe, or communications device LPOVERLAPPED lpoOverlapped, // address of overlapped structure LPDWORD lpcbTransfer, // address of actual bytes count BOOL fWait); // wait flag La seconde solution pour attendre (ou être averti de) la fin d’une opération d’E/S asynchrone repose sur l’objet kernel fichier (représenté par son handle). Comme tout objet kernel, il est dans l’état «signalé» ou «non signalé» et le thread peut se mettre en attente sur cet objet. A l’appel des routines d’E/S, l’objet passe automatiquement dans l’état non signalé; il passera dans l’état signalé dès lors que la routine sera terminée. Il est donc possible d’utiliser cette information pour resynchroniser l’exécution avec la fin de l’opération d’E/S. Cette méthode n’est cependant pas satisfaisante dès lors que l’on effectue plusieurs opérations en même temps puisque cette signalisation s’effectue au niveau du handle du fichier et non pas au niveau de l’opération elle même: ReadFile(...,&MonOper1); // Lancement de la première lecture asynchrone ReadFile(...,&MonOper2); // Lancement de la seconde lecture asynchrone WaitForSingleObject(hFic,INFINITE); // Oper1, Oper2 ??? Il existe une 3ème solution: la structure OVERLAPPED contient la référence à un handle d’un objet kernel événement; si ce handle est renseigné (pas NULL), la signalisation de la fin de la routine se fera en utilisant cet événement; l’application dispose ainsi d’une signalisation associée à l’opération. Le passage de l’événement de l’état signalé à l’état non signalé n’est pas effectué par l’appel à la routine d’E/S. Il doit donc être explicite si l’événement n’est pas à ré-initialisation automatique. Win32 multitâches - T. Joubert © THEORIS 2005 109 Il faut être vigilant, dans l’exemple suivant, le thread se trouvera irrémédiablement bloqué dans l’attente d’une E/S pourtant terminée: hMonEvt=CreateEvent(..) // Evt à re-init automatique MonOper.hEvent=hMonEvent; // Oper asynchrone utilisant une signalisation par evt ... ReadFile(...,&MonOper); // Lancement de l’opération ... WaitForSingleObject(hMonEvent,INFINITE); // Attendre la fin de l’opération GetOverlappedResult(...,TRUE); // récupérer les résultats MAIS ATTENTE INFINIE!!! Pourquoi? En fait, le WaitForSingleObject attend la fin de l’opération puis libère le thread mais l’événement est automatiquement remis dans l’état non signalé, l’appel à l’opération GetOverlappedResult avec l’attribut fWait à VRAI provoquera une attente sur l’événement associé à l’opération qui n’est plus signalé. Dans ce cas, il faut veiller à toujours passer fWait à FAUX. Sous Windows NT, il existe aussi un mécanisme d’E/S alertables fondées sur l’utilisation d’un callback de fin d’opération (Completion routine). Les routines ReadFileEx, WriteFileEx permettent d’initier l’E/S alertable. Les routines SleepEx, WaitFor...Ex étendent les fonctions déjà présentées pour autoriser l’appel au callback (le callback n’est appelé qu’à ce moment là). Les fonctions d ’accès aux fichiers s ’appliquent aux entrées/sorties sur les ports séries de la machine, on utilise le code du port comme nom de fichier (par exemple: hCom1=CreateFile(« COM1 »,…);). Les flux sont alors virtualisés par les fonctions ReadFile et WriteFile, qui peuvent être utilisées en mode synchrone ou asynchrone (Overlapped). Win32 multitâches - T. Joubert © THEORIS 2005 110 Pipes Communication point à point Unnamed pipe CreatePipe Named pipe CreateNamedPipe Opérations asynchrones possibles Beaucoup de routines client-serveur ConnectNamedPipe: le serveur attend la venue d’un client CallNamedPipe: ouverture+ opération E/S+ fermeture du pipe Win32 XP - © Theoris 2005 111 Les pipes permettent une communication simple entre processus. Il en existe deux types: le unnamed pipe et le named pipe. La communication par les pipes est assez sécurisée mais c’est un service point à point (cf les boites aux lettres). Traditionnellement, le unnamed pipe sert surtout à la communication entre un processus père et son fils ou entre ses fils. Il ne fonctionne pas au travers d’un réseau. Traditionnellement encore, on associera le pipe avec les E/S standards du processus (routines GetStdHandle/SetStdHandle). BOOL CreatePipe( PHANDLE phRead, PHANDLE phWrite, LPSECURITY_ATTRIBUTES lpsa, DWORD cbPipe); // address of variable for read handle // address of variable for write handle // address of security attributes // number of bytes reserved for pipe Le pipe nommé permet facilement une communication entre des processus indépendants. Il permet même une communication au travers du réseau (le nom du pipe est \\machine\pipe\nom_de_pipe). Le pipe nommé à une très forte connotation client/serveur. HANDLE CreateNamedPipe( LPCTSTR lpName, // address of pipe name DWORD dwOpenMode, // pipe open mode DWORD dwPipeMode, // pipe-specific modes DWORD nMaxInstances, // maximum number of instances DWORD nOutBufferSize, // output buffer size, in bytes DWORD nInBufferSize, // input buffer size, in bytes DWORD nDefaultTimeOut, // time-out time, in milliseconds LPSECURITY_ATTRIBUTES lpSecurityAttributes); // address of security structure Les pipes Win32 ne peuvent pas être utilisés pour communiquer avec une machine Unix. Mais la communication Windows NT/95 est possible Win32 multitâches - T. Joubert © THEORIS 2005 111 Exemples: 1) Communication père-fils avec un unnamed pipe coté du père: sa.nLength = sizeof(sa); sa.lpSecurityDescriptor = NULL; sa.bInheritHandle = TRUE; CreatePipe(&HRead,&HWrite,&sa,0); CreateProcess(NULL,«Fils HRead HWrite»,NULL,NULL,TRUE,0,NULL,NULL,&si,&pi); WriteFile(HWrite,Data,strlen(Data)+1,&Nb,NULL); CloseHandle(HRead); CloseHandle(HWrite); coté du fils: ReadFile(HRead,Data,9,&Nb,NULL); CloseHandle(HRead); CloseHandle(HWrite); 2) Communication entre 2 applications avec un named pipe (client/serveur) coté du serveur: hPipe=CreateNamedPipe(‘‘\\\\.\\pipe\\mon_pipe’’,PIPE_ACCESS_DUPLEX,PIPE_TYPE_MESSAGE| PIPE_READMODE_MESSAGE|PIPE_WAIT,PIPE_UNLIMITED_INSTANCES,BUFSIZE,BUFSIZE,0, NULL); if (hPipe!=INVALID_HANDLE_VALUE) { if (ConnectNamedPipe(hPipe,NULL)) { ReadFile(hPipe,Data,BUFSIZE,&NbData,NULL); DisconnectNamedPipe(hPipe); } CloseHandle(hPipe); } coté du client: hPipe=CreateFile(‘‘\\\\machine\\pipe\\mon_pipe’’,GENERIC_WRITE,FILE_SHARE_READ|FILE_SHA RE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL); if (hPipe!=INVALID_HANDLE_VALUE) { WriteFile(hPipe,Data,strlen(Data)+1,&NbData,NULL); CloseHandle(hPipe); } Win32 multitâches - T. Joubert © THEORIS 2005 112 Mailslot Pipe mono-directionnel Ressource locale Les clients (CreateFile) ECRIVENT Les serveurs (CreateMailSlot ou héritage) LISENT Les serveurs créent des MailSlots locales Une MailSlot spécifique est unique pour le système Diffusion sur le réseau Les clients peuvent ne donner que le nom de MailSlot Le système assure la délivrance des messages dans toutes les MailSlot présentes sur le domaine et ayant ce nom Win32 XP - © Theoris 2005 113 Les boîtes aux lettres (mailslot) permettent une communication mono-directionnelle entre processus: les clients écrivent et les serveurs lisent. • Un serveur est un processus qui a obtenu le handle en créant une mailslot (CreateMailSlot) ou en héritant depuis un processus créateur • Un client est un processus qui a obtenu le handle en ouvrant une mailslot avec un CreateFile La mailslot peut être utilisée entre 2 machines distantes, mais il existe de fortes différences par rapport aux named pipes : • Un serveur ne peut créer que des boîtes locales (\\.\mailslot\ma_bal) • Sur une machine, une mailslot donnée ne peut être ouverte que par un serveur à la fois • Un client peut ouvrir explicitement une mailslot sur une machine locale ou distante • Un client peut ne pas désigner la machine serveur (\\*\mailslot\ma_bal) dans ce cas les messages seront diffusés sur toutes les boîtes ouvertes dont le nom correspond dans le domaine HANDLE CreateMailslot( LPCTSTR lpszName, // address of string for mailslot name DWORD cbMaxMsg, // maximum message size DWORD dwReadTimeout, // milliseconds before read time-out LPSECURITY_ATTRIBUTES lpsa); // address of security structure Win32 multitâches - T. Joubert © THEORIS 2005 113 Exemple: Communication entre 2 applications avec une mailslot (client/serveur) coté du serveur: hBal=CreateMailslot(‘‘\\\\.\\mailslot\\\\ma_bal’’ ,0,MAILSLOT_WAIT_FOREVER,NULL); if (hBal!=INVALID_HANDLE_VALUE) { if (GetMailslotInfo(hBal,NULL,&NbData,&NbMsg,NULL)) if (NbData!=MAILSLOT_NO_MESSAGE) ReadFile(hBal,Data,BUFSIZE,&NbData,NULL); CloseHandle(hBal); } coté du client: hBal=CreateFile(‘‘\\\\*\\\\mailslot\\\\ma_bal’’,GENERIC_WRITE,FILE_SHARE_READ,NULL,OPEN_EX ISTING,FILE_ATTRIBUTE_NORMAL,NULL); if (hBal!=INVALID_HANDLE_VALUE) { WriteFile(hBal,Data,strlen(Data)+1,&NbData,NULL); CloseHandle(hBal); } Win32 multitâches - T. Joubert © THEORIS 2005 114 Fichiers «mappés» CreateFileMapping / openFileMapping MapViewOfFile / UnmapViewOfFile Créer un fichier mappé à partir d’un fichier Dans la zone d’échange: notion de mémoire partagée Créer/Détruire une vue Cohérence des vues multiples La cohérence n’est assurée qu’entre 2 vues issues du même fichier mappé Win32 XP - © Theoris 2005 115 Le mécanisme des fichiers mappés en mémoire permet d’avoir un accès direct au contenu d’un fichier en passant par une zone de la mémoire. Cette technique est à la base du partage de données entre processus dans Win32. HANDLE CreateFileMapping( HANDLE hFile, LPSECURITY_ATTRIBUTES lpsa, DWORD fdwProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, LPCTSTR lpszMapName); // handle of file to map // optional security attributes // protection for mapping object // high-order 32 bits of object size // low-order 32 bits of object size // name of file-mapping object HANDLE OpenFileMapping(DWORDdwAccess, // access mode BOOL bInheritHandle, // inherit flag LPCTSTR lpName); // address of name of file-mapping object La primitive CreateFileMapping permet de créer un fichier mappé soit à partir d ’un fichier réel (spécifié par un handle de fichier valide), soit à partir d ’un fichier géré par le système qui sera placé dans le fichier de partage Windows (hFile=0xFFFFFFFF); dans ce cas on peut parler de mémoire partagée. OpenFileMapping donne accès à un fichier mappé existant. Si la taille n’est pas précisée (0), le système prend la taille du fichier réel. S’il s’agit de mémoire partagée, ce paramètre ne doit pas être nul. A la création du fichier mappé, les attributs de protections sont des attributs de pages: PAGE_READONLY, PAGE_READWRITE ou PAGE_WRITECOPY. Si le fichier mappé est basé sur un fichier physique, celui doit être ouvert avec les drapeaux GENERIC_X correspondants. A l’ouverture d’un fichier mappé existant, les attributs d’accès sont FILE_MAP_READ, FILE_MAP_WRITE (et lecture) , FILE_MAP_ALL_ACCESS ou FILE_MAP_COPY. Ces attributs doivent être en accord avec ceux utilisés à la création du fichier mappé (PAGE_X). Les primitives retournent NULL si la fonction échoue, et sinon un handle valide. Win32 multitâches - T. Joubert © THEORIS 2005 115 La primitive MapViewOfFile permet de réaliser le mapping, c ’est à dire établir la correspondance entre une partie du fichier et une zone dans l’espace d’adressage. La valeur cbMap=0 permet de mapper tout le fichier. LPVOID MapViewOfFile( HANDLE hMapObject, // file-mapping object to map into address space DWORD fdwAccess, // access mode DWORD dwOffsetHigh, // high-order 32 bits of file offset DWORD dwOffsetLow, // low-order 32 bits of file offset DWORD cbMap); // number of bytes to map Le mode d’accès est FILE_MAP_READ, FILE_MAP_WRITE (lecture autorisée!) FILE_MAP_ALL_ACCESS ou FILE_MAP_COPY. Il doit y avoir accord avec les attributs de page utilisés lors du mapping. La vue doit toujours être alignée sur l’unité d’allocation Win32. Cette valeur (64Ko) peut être obtenue en appelant la primitive GetSystemInfo. Il existe aussi une routine MapViewOfFileEx qui permet de spécifier l’adresse de base de la vue. Si la fonction échoue, la routine retourne NULL. BOOL UnmapViewOfFile(LPVOID lpBaseAddress); // address where mapped view begins BOOL FlushViewOfFile( LPCVOID lpvBase, // start address of byte range to flush DWORD cbFlush); // number of bytes in range La primitive UnmapViewOfFile permet de supprimer la vue créée précédemment et met à jour l’image sur le disque si nécessaire. La routine FlushViewOfFile permet de forcer cette mise à jour sans supprimer la vue. La principale question que l’on peut se poser concerne la cohérence entre des accès multiples. Win32 garantit seulement que des vues multiples d’un même «fichier mappé» sont cohérentes. Ceci implique que la cohérence n’est pas garantie au travers du réseau, ni au travers de plusieurs fichiers mappés, ni entre les accès traditionnels et les accès via des vues du même fichier physique. Il n’y a aucun mécanisme de synchronisation lié à l’utilisation des fichiers mappés. L’application devra utiliser ses propres objets de synchronisation si nécessaire. Win32 multitâches - T. Joubert © THEORIS 2005 116 Résumé Plusieurs mécanismes Fichiers Echanges: pipes, mailslot Ports d’E/S fichiers mappés: accès à la mémoire partagée Echanges synchrones ou asynchrones Unification des primitives d’accès CreateFile, ReadFile, WriteFile Un adressage 64 bits Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 117 117 Chapitre 8 Horlogerie - Gestion du temps Win32 multitâches - T. Joubert © THEORIS 2005 118 Plan du chapitre Les services d’horlogerie Le message Windows WM_TIMER Les timers multimédia Les timer du noyau (WaitableTimer) Les chiens de garde Time-out sur les appels systèmes Time-out sur les IO Prendre la mesure du temps Le temps système Le temps d’exécution La notion de temps sur les fichiers (hors périmètre) Win32 XP - © Theoris 2005 119 Les services d’horlogerie permettent à l’utilisateur de disposer d’une base de temps. Il existe trois méthodes pour accéder aux fonctions timers: • Les timers «Windows» associés au message WM_TIMER • Les timers «multimédia» • Les objets noyau «WaitableTimer» Ces trois mécanismes font appels à des techniques différentes. Seuls les timers multimédia et les WaitableTimers offrent un réel service d’horlogerie fiable. Les WaitableTimer sont apparus dans la Win32 avec NT 4.0, leur usage devrait s ’imposer face aux timer multimedias. Les time-outs sont des mécanismes mis en place par l’application pour limiter une attente de ressource ou la durée d’une opération. Les time-outs d’attente (WaitFor...) sont gérés par le système de façon transparente: l’application est avertie de l’expiration du chien de garde par un compte-rendu spécifique WAIT_TIMEOUT. Les opérations d’entrées/sorties peuvent bénéficier d’un service de chien de garde qui surveille la durée de l’opération de lecture ou d’écriture pour des opérations synchrones ou asynchrones. Deux paramètres peuvent être ajustés pour définir ce chien de garde: la durée totale de l’opération et le délai entre 2 caractères. Les différentes combinaisons permettent d’obtenir une grande variété dans la gestion de ces chiens de garde. Les derniers services liés au temps permettent de répondre aux questions «quelle heure estil?», «Depuis combien de temps ?». Il existe plusieurs services pour accéder à ces informations et il convient de bien en comprendre l’utilisation dans un contexte multi-thread. Le système de datation des fichiers ne sera pas examiné car il ne correspond pas vraiment à notre centre d’intérêt. Win32 multitâches - T. Joubert © THEORIS 2005 119 L’horlogerie WM_TIMER SetTimer KillTimer Création de timer périodique Emission d’un message Windows WM_TIMER Suppression d’un timer existant Le callback TimerProc Appel d’une procédure en remplacement du message Une procédure différente de celle utilisée pour les timers multimédia Win32 XP - © Theoris 2005 120 Le mécanisme de «timers» associé au message WM_TIMER est fidèle à la philosophie Windows. A chaque occurrence du timer, un message WM_TIMER est envoyé à la fenêtre qui a en charge ce timer; le dispatcher habituel récupère et traite le message. UINT SetTimer(HWND hwnd, // handle of window for timer messages UINT idTimer, // timer identifier UINT uTimeout, // time-out value TIMERPROC tmprc); // address of timer procedure BOOL KillTimer(HWND hwnd, UINT idEvent); VOID CALLBACK TimerProc(HWND hwnd, // handle of window that installed timer // timer identifier // handle of window for timer messages UINT uMsg, // WM_TIMER message UINT idEvent, // timer identifier DWORD dwTime); // current system time La procédure SetTimer est appelée pour associer un timer périodique à une fenêtre. A chaque expiration du timer, le message WM_TIMER sera envoyé vers la fenêtre identifiée par le handle et wParam contient idTimer. Une procédure ‘callback’ peut être précisée, dans ce cas c’est elle qui sera appelée. Il est important de signaler que le callback associé aux timers multimédia n’a pas le même format. La procédure KillTimer permet de supprimer le service de timer demandé par la procédure SetTimer. Ces timers ne sont pas vraiment fiables mais ils sont simples à mettre en œuvre. L’arrivée et donc le traitement du message n’est pas régulier, il y a même une dérive et de plus il peut y avoir une perte de signalisation si la fenêtre est «occupée». Win32 multitâches - T. Joubert © THEORIS 2005 120 L’horlogerie multimédia timeGetDevCaps timeBeginPeriod & timeEndPeriod Détermine les capacités des timers multimédia Déclare et définit les besoins de timers au système Signale la fin de l’utilisation de timers multimédia timeSetEvent & timeKillEvent Création d’ un timer périodique ou à expiration unique La procédure de callback «TimerProc» Suppression d’un timer multimédia Win32 XP - © Theoris 2005 121 Les services de timer multimédia permettent l’accès à un service d’horlogerie bien plus précis mais plus complexe à mettre en œuvre. MMRESULT timeGetDevCaps(LPTIMECAPS lptc, // @ of structure for returned capabilities UINT cbtc); // size of structure in bytes MMRESULT timeBeginPeriod(UINT cMillisec); // new minimum timer period in milliseconds MMRESULT timeEndPeriod(UINT cMillisec); // previous timer period in milliseconds La procédure timeGetDevCaps permet de récupérer des informations sur les possibilités du système, les valeurs minimales et maximales de temporisation sont retournées (1ms100000ms sur un Pentium 100). La procédure timeBeginPeriod permet de signaler au système que l’on souhaite bénéficier pour notre application d’un service d’horlogerie multimédia pas plus rapide que celui indiqué. En fait, le système utilise la valeur pour ajuster au mieux son gestionnaire interne afin d’adapter son fonctionnement aux besoins de l’application. Il est possible d’effectuer plusieurs appels successifs à cette routine. La procédure timeEndPeriod est obligatoirement appariée à la procédure précédente et doit être appelée lorsque l’on ne souhaite plus utiliser de service de timer multimédia associé à la demande précédente. En cas d’appels consécutifs à timeBeginPeriod, un nombre identique d’appels à timeEndPeriod doit être effectué. Si la procédure timeEndPeriod n’est pas appelée à la fin de l’utilisation des timers, le système d’horlogerie multimédia ne fonctionnera plus jusqu’au prochain redémarrage du système Windows. Win32 multitâches - T. Joubert © THEORIS 2005 121 UINT timeSetEvent(UINT wDelay, // specifies event period UINT wResolution, // specifies delay accuracy LPTIMECALLBACK lptc, // address of callback function DWORD dwUser, // application-defined data UINT Type); // callback type flags void CALLBACK TimeProc(UINT IDEvent, UINT uReserved, DWORD dwUser, DWORD dwR1, DWORD dwR2); // identifies timer event // not used // application-defined instance data // not used // not used MMRESULT timeKillEvent(UINT IDEvent); // identifies event to be destroyed La procédure timeSetEvent permet de créer un timer multimédia périodique ou à expiration unique (Type TIME_PERIODIC ou TIME_ONESHOT). Si l’appel est accepté, l’indentificateur du timer est renvoyé (différent de 0) et la procédure callback précisée sera appelée à chaque expiration du timer. La procédure timeBeginPeriod définissant la période minimale doit être appelée avant timeSetEvent et le délai demandé lors de l’appel à timeSetEvent doit être supérieur ou égal à cette période minimale et inférieure à la valeur maximale retournée par timeGetDevCaps. L’utilisation du paramètre wResolution n’est pas explicite et n’est pas très probante. Le système utilise cette valeur pour ajuster ses mécanismes d’horlogerie. Une faible valeur (donc une grande précision) peut conduire le système à utiliser une part significative de la CPU pour des fins de gestion interne (dixit Microsoft). La valeur 0 (= défaut) peut être utilisée. Il n ’a pas été constaté d’anomalies lors de «l’oubli» de l’appel à timeBeginPeriod (ce n’est pas pareil pour timeEndPeriod!). Le paramètre dwUser et l’identificateur du timer sont passés à la procédure callback à l’expiration du timer. Les autres paramètres ne sont pas utilisés mais font partie du prototype des procédures callback. L’utilisation des timers multimédia provoque la création automatique d’un nouveau thread rattaché au processus. La procédure de callback s’exécute dans le contexte de ce thread. La procédure timeKillEvent est appelée pour détruire un timer multimédia. Les timers à expiration unique doivent aussi être détruits. Win32 multitâches - T. Joubert © THEORIS 2005 122 Les objets WaitableTimer Un objet noyau comme les autres Synchronisation via le HANDLE Manipulation via un HANDLE Attributs de sécurité Fonctions standardisées (Create, Open …) Réarmement manuel ou automatique SetWaitableTimer & CancelWaitableTimer Fourniture d ’un délai Timer périodique ou à expiration unique Fourniture d ’une procédure callback Win32 XP - © Theoris 2005 123 Les WaitableTimer sont apparus avec NT 4.0, ils donnent accès à un service d ’horlogerie reposant sur de véritables objets noyau. Leur usage ainsi que leurs performances sont sensiblement différents de ceux des timers multimédia, leur périodicité est alignée sur le quantum de temps, elle est donc moins fine. HANDLE CreateWaitableTimer( LPSECURITY_ATTRIBUTES lpTimerAttributes, // pointer to security attributes BOOL bManualReset, // flag for manual reset state LPCTSTR lpTimerName ); // pointer to timer object name HANDLE OpenWaitableTimer( DWORD dwDesiredAccess, // access flag BOOL bInheritHandle, // inherit flag LPCTSTR lpTimerName ); // pointer to timer object name Une des conséquences les plus importantes du fait d ’avoir affaire à un objet noyau réside dans la possibilité d ’utiliser les fonctions WaitForSingleObjet et WaitForMultipleObject pour récupérer la synchronisation. Le fait de manipuler cet objet via un HANDLE implique que l ’abandon de l ’objet passe par un appel à la fonction CloseHandle. La présence d ’un mode manuel ou automatique ne rend pas l ’objet WaitableTimer tout à fait comparable à l ’objet Event dans la mesure où il n’existe pas l’équivalent des fonctions Reset et Pulse. L ’usage des WaitableTimer nécessite une définition spécifique: #define _WIN32_WINNT 0x0400 Win32 multitâches - T. Joubert © THEORIS 2005 123 BOOL SetWaitableTimer( HANDLE hTimer, // handle to a timer object const LARGE_INTEGER *pDueTime, // when timer will become signaled LONG lPeriod, // periodic timer interval PTIMERAPCROUTINE pfnCompletionRoutine, // pointer to the completion routine LPVOID lpArgToCompletionRoutine, // data passed to the completion routine BOOL fResume ); // flag for resume state BOOL CancelWaitableTimer( HANDLE hTimer); // handle to a timer object Les timers sont inactifs à leur création, la procédure SetWaitableTimer permet de rendre actif un timer qui sera périodique ou à expiration unique en fonction de l ’argument lPeriod (nul pour un timer apériodique). Le timer démarre son activité après un laps de temps défini par pDueTime (structure LARGE_INTEGER). typedef union _LARGE_INTEGER { struct { DWORD LowPart; LONG HighPart; }; LONGLONG QuadPart; } LARGE_INTEGER; Un timer est toujours activé dans l ’état non signalé, il passe à l’état signalé lors de l’expiration du laps de temps pDueTime, il restera dans cet état jusqu’à ce que son handle fasse l’objet d’une fonction WaitFor... (s’il est à réarmement automatique) ou d’un nouveau SetWaitableTimer. La procédure CancelWaitableTimer provoque l ’arrêt du timer, elle ne modifie pas son état de signalisation, ce qui fait que des threads en attente sur le Handle resteront bloqués jusqu ’à un timeout ou une nouvelle activation du timer par SetWaitableTimer. Un timer à réarmement manuel restera dans l ’état signalé tant qu ’il ne fait pas l ’objet d ’un autre SetWaitableTimer (ou d ’une destruction pure et simple par la fermeture du dernier handle). Win32 multitâches - T. Joubert © THEORIS 2005 124 WaitableTimer et APC La procédure CALLBACK Paramètre utilisateur Paramètres système (heure courante) Placée en APC (Asynchronous Procedure Call) Conditions d ’exécution APC Le thread propriétaire doit être alertable Code retour spécifique du WaitFor... Dualité de l’attente Win32 XP - © Theoris 2005 125 Si l’appel à SetWaitableTimer est accepté et qu’une procédure CALLBACK est précisée dans les paramètres, celle-ci sera mise en APC du thread appelant. La procédure CALLBACK sera exécutée à chaque expiration du timer à condition que le thread appelant soit dans l ’état alertable, il doit donc s ’être mis en attente bloquante avec une des fonctions spécialisées (WaitFor…Ex, …), en mettant l’argument bAlertable à TRUE. Cette procédure reçoit en argument, d ’une part des données applicatives dont le pointeur est défini lors de l ’appel à SetWaitableTimer, et d ’autre part l ’heure à laquelle le délai du timer a expiré (champs d ’une structure FILETIME). VOID (APIENTRY *PTIMERAPCROUTINE)( LPVOID lpArgToCompletionRoutine, DWORD dwTimerLowValue, DWORD dwTimerHighValue ); La valeur de retour d ’une fonction d ’attente alertable sur exécution d ’une APC est égale à WAIT_IO_COMPLETION L’exécution de la procédure de callback n ’a aucune influence implicite sur l ’état signalé ou non-signalé de l ’objet WaitableTimer. Win32 multitâches - T. Joubert © THEORIS 2005 125 Prendre la mesure du temps Le temps système GetTickCount Mesurer le temps qui passe, unité la ms et granularité 10ms GetCurrentTime (obsolète) timeGetTime, timeGetSystemTime, GetSystemTime GetLocalTime Obtenir l’heure légale QueryPerformanceFrequency QueryPerformanceCounter Mesurer le temps qui passe avec une précision d’une µ-seconde Win32 XP - © Theoris 2005 126 Toutes ces procédures permettent de mesurer le temps qui passe de façon absolue. Le temps s’écoule indépendemment de la charge du PC et de l’exécution de l’application. Les procédure GetTickCount, GetCurrentTime, timeGetTime et timeGetSystemTime renvoient toutes le temps absolu décompté en milli secondes depuis le lancement de la session Windows: • GetCurrentTime porte un nom peu adapté, on utilisera GetTickCount mais les deux routines sont identiques!!! • timeGetTime fonctionne de la même manière mais appartient au module des timers multimédia. Cette fonction existe-t-elle pour une compatibilité future Win32 ? • timeGetSystemTime retourne la même information en passant par une structure fort complexe. Il n’y a pas d’explications de Microsoft sur l’utilité de cette routine très spécifique. Microsoft nous signale même que cette routine est plus lente. DWORD GetTickCount(VOID) Le compteur de temps interne de Windows est sur 32 bits, il repassera à 0 tous les 49.7 jours... (232-1 milli-secondes). Il y a néanmoins un problème de granularité, sous Windows NT, l’incrémentation du compteur est de 10ms en 10ms. Win32 multitâches - T. Joubert © THEORIS 2005 126 Les procédures GetSystemTime et GetLocalTime remplissent une structure renseignant la date, le jour de la semaine et l’heure. La première routine retourne le temps universel et la seconde l’heure locale officielle. VOID GetSystemTime(LPSYSTEMTIME lpst); // address of system time structure VOID GetLocalTime(LPSYSTEMTIME lpst); // address of system time structure Les routines QueryPerformanceCounter et QueryPerformanceFrequency permettent de lire la valeur et la fréquence (en Hz) de mise à jour d’un compteur très puissant si la machine en possède un. Sur mon PC, le pas est de 0,8µs!!! BOOL QueryPerformanceFrequency(LARGE_INTEGER *lpliPerfFreq); // address of frequency BOOL QueryPerformanceCounter(LARGE_INTEGER *lpliPerfCount); // address of counter Win32 multitâches - T. Joubert © THEORIS 2005 127 Prendre la mesure du temps Le temps d’exécution GetThreadTimes – L’heure de création – L’heure de fin – Le temps passé dans le noyau – Le temps d’exécution du code utilisateur GetProcessTimes Win32 XP - © Theoris 2005 128 A la différence du temps système, le temps d’exécution ne s’écoule plus dès lors que l’exécution est suspendue et il n’est pas universel mais local à une entité de mesure: le processus ou le thread. Or ce temps d’exécution n’est pas accessible directement dans un environnement préemptif, dans l’exemple suivant, la mesure sera fausse: Debut=GetTickCount(); // Effectuer un traitement long pendant lequel on peut être interrompu Duree=GetTickCount()-Debut; // durée de mon traitement long La procédure GetThreadTimes permet d’obtenir les dates de création et de destruction du thread (tant que le handle existe encore) et d’obtenir les durées d’exécution dans le noyau et dans le code utilisateur. La procédure GetProcessTimes permet d’obtenir les mêmes informations pour un processus. Les temps d’exécution sont rigoureusement la somme des temps d’exécution des différents threads qui composent le processus. BOOL GetThreadTimes(HANDLE hThread, LPFILETIME lpCreationTime, LPFILETIME lpExitTime, LPFILETIME lpKernelTime, LPFILETIME lpUserTime); BOOL GetProcessTimes(HANDLE hProcess, LPFILETIME lpCreationTime, LPFILETIME lpExitTime, LPFILETIME lpKernelTime, LPFILETIME lpUserTime); // thread of interest // creation date // destruction date // time in kernel mode // time in user mode // process of interest // creation date // destruction date // time in kernel mode // time in user mode Pour nous simplifier la vie, l’unité de durée est de 100 nano-seconde et les dates sont exprimées sous la forme de la durée écoulée depuis le 1er janvier 1601 0 heure à Greenwich (il y a néanmoins des routines de conversion). Win32 multitâches - T. Joubert © THEORIS 2005 128 Résumé Les timers Message WM_TIMER: existant mais peu précis Timers multimédia précis Les chiens de garde précis sous Windows NT, moins sous 9x La granularité dépend de l’horloge de base (10ms) L’accès au temps Beaucoup de routines pour obtenir le temps actuel. Souvent un temps relatif (début de Windows). La précision va jusqu’à 0,8µs. L’accès au temps d’exécution est disponible sous Windows NT. Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 129 129 Chapitre 9 Librairies dynamiques Win32 multitâches - T. Joubert © THEORIS 2005 130 Lien Statique ou Dynamique EXE Lien Statique Code objet Code objet LINK LIB toto(…) LIB EXE toto(…) LINK DLL Code objet LINK Lien Dynamique Précoce Lien Dynamique Tardif Code objet EXE Code objet LINK LoadLibrary(…) GetProcAddress(…) Win32 XP - © Theoris 2005 131 Comme leur nom ne l ’indique pas, les librairies dynamiques (Dynamic Link Librairies) ne sont pas des librairies, ce sont des fichiers exécutables. Toutefois ces fichiers ne peuvent pas être chargés seuls, ils ont besoin d ’un « vrai » exécutable d ’accueil pour tourner. Contrairement au schéma statique, l’accès au code contenu dans une DLL passe par un chargement de celle-ci au RunTime et par une résolution des adresses d ’appel. La principale différence pour le développeur d ’application se situe au niveau de l ’édition de liens, pour utiliser une DLL il devra produire deux exécutables au lieu d ’un. L ’appel à une fonction de DLL peut se faire selon deux schémas distincts : • Le lien précoce se distingue très peu du lien statique dans la mesure où on dispose à l ’édition de lien d ’une carte des fonctions de la DLL dans un fichier LIB (format différent d ’une librairie d ’objets). Le nom de la DLL est alors marqué dans l'exécutable et elle est chargée systématiquement au lancement. • Le lien tardif (late binding) qui nécessite une participation du développeur car le chargement de la DLL ainsi que la résolution des adresses de fonctions sont faites explicitement avec les fonctions LoadLibrary(…) ou GetModuleHandle(…) puis GetProcAddress(…). On peut libérer avec FreeLibrary(…) Les DLL ont des usages multiples et variés qui vont des pilotes de périphériques aux composants ActiveX. Le principal avantage étant de pouvoir échanger un code exécutable sans modifier les modules dépendants (tant que l ’on ne modifie pas l ’interface !!!) Win32 multitâches - T. Joubert © THEORIS 2005 131 DLL & Processus Extension du code de l’application «Mappée» dans l’espace d’adressage du processus Une symbiose complète avec le processus: – – – Utilise la Stack du thread appelant et son espace d’adressage Utilise les handles du processus et Publie les handles créés Publie les variables globales de la DLL La signalisation «d’événements Dll» La routine DllMain de la DLL est appelée – – – – A l’attachement d’un processus A l’attachement d’un nouveau thread Au détachement d’un thread Au détachement du processus Win32 XP - © Theoris 2005 132 Lorsqu’un exécutable utilise une DLL, la DLL est «mappée» dans l’espace d’adressage du processus. Plusieurs exécutables peuvent faire référence à la même DLL en même temps mais chacun possède sa copie propre. La DLL représentée par des routines que l’application peut utiliser se comporte comme si elle faisait partie intégrante de l’application. Elle utilise l’espace d’adressage du processus et utilise la Stack du thread appelant. Elle peut manipuler les handles créés par l’application et symétriquement l’application peut directement manipuler les handles créés par la DLL. La Dll peut contenir un point d’entrée spécifique DllMain facultatif. Le système appelle automatiquement cette routine lorsque les événements suivants se produisent: • Le processus est attaché à cette librairie: DLL_PROCESS_ATTACH • Le processus est détaché de cette librairie: DLL_PROCESS_DETACH • Un nouveau thread est créé dans un processus DLL_THREAD_ATTACH (sauf thread principal non signalé) déjà attaché: • Un thread est détruit dans le processus attaché à cette DLL_THREAD_DETACH (sauf dernier thread du processus non signalé) librairie: Ce mécanisme permet d’avertir la Dll de son utilisation par une application en l’informant de ces «événements». BOOL WINAPI DllMain( HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved); // handle of DLL module // reason for calling function // reserved Les appels concernant les Threads peuvent être invalidés avec la fonction : BOOL DisableThreadLibraryCalls ( HMODULE hLibModule Win32 multitâches - T. Joubert © THEORIS 2005 132 DLL & Données Des variables par processus Des variables partagées Il y a une zone de donnée par processus attaché C’est le mode par défaut La zone de donnée est unique pour tous les processus Le mécanisme est dépendant de la chaîne de production D’autres types d’allocations à gérer Fichiers mappés données partagées TLS stockage au niveau des threads Win32 XP - © Theoris 2005 133 L’écriture d’une Dll ne pose pas de problème particulier. Il s’agit d’un module de code classique disposant de routines et de données publiques (utilisables par l’application) et privées. La principale particularité concerne l’utilisation d’un fichier de définition du module qui liste les fonctions exportées (utilisables par les applications). Certains compilateurs permettent de se dispenser de ce fichier en ajoutant des mots clés devant les routines concernées. Ainsi dans Microsoft Visual C++, les mots clés __declspec(dllexport) et __declspec(dllimport) remplissent cette fonction respectivement au niveau de la déclaration de la routine dans la Dll et au niveau du prototypage d’une routine de Dll dans l’application. Pour une Dll écrite le plus simplement possible, toutes les données globales de la Dll seront dupliquées autant de fois qu’il y a de processus utilisant la Dll. Il est néanmoins possible d’avoir des données qui ne seront pas dupliquées mais qui seront partagées par tous les processus utilisant la Dll. Les données sont déclarées dans une section partagée. Ce type de déclaration n’est pas très documenté et semble très spécifique des outils utilisés. L’exemple suivant fonctionne sous Microsoft Visual C++ 5.0: #pragma data_seg("toto") // Ouvre une section spécifique de nom «toto» int UneDonneePourTous=0; // Cette donnée n’aura qu’une seule instance #pragma data_seg() // Ferme la section en cours #pragma comment(linker,"/section:toto,rws") // Définit « toto » comme section partagée int ChacunSaDonnee=0; // cette donnée aura autant d ’instances que de processus Win32 multitâches - T. Joubert © THEORIS 2005 133 Stricto sensu, il n’existe pas d’autre façon de gérer les données dans une Dll (dupliquées ou partagées) mais il est bon de garder à l’esprit la présence de mécanismes qui ne sont pas réservées aux Dll mais qui peuvent servir et que l’on utilisera souvent en liaison avec la signalisation par DllEntryPoint: Le fichier mappé peut permettre de partager des données • Le TLS fournit une allocation spécifique au thread Exemple: Dans cet exemple, on souhaite implémenter le petit module permettant de disposer d’un heap par thread sous la forme d’une Dll (cf le chapitre sur la mémoire et les TLS). DWORD indexTasHandler=TLS_OUT_OF_INDEXES; // L’utilisation de la signalisation permet de supprimer la primitive d’initialisation du module et permet aussi de gérer facilement la destruction d’un thread. BOOL WINAPI DllMain( HANDLE hModule,DWORD dwReason,LPVOID lpReserved ) { HANDLE hHeap; switch( dwReason) { case DLL_PROCESS_ATTACH: indexTasHandler=TlsAlloc(); // fixer l’index ici, c’est plus performant et plus simple d’emploi break; case DLL_THREAD_DETACH: // Accéder au heap du thread hHeap=(HANDLE)TlsGetValue(indexTasHandler); if (hHeap!=0) HeapDestroy(hHeap); // détruire le heap local, c’est simple et plus propre break; } return TRUE; } // Allocation dans le heap du thread __declspec(dllexport) LPVOID ThreadHeapAlloc(DWORD dwBytes) { HANDLE hHeap; // Accéder au heap du thread hHeap=(HANDLE)TlsGetValue(indexTasHandler); if (hHeap==0) { // Le heap n’existe pas, en créer un de 4Ko hHeap=HeapCreate(HEAP_NO_SERIALIZE,4096,0); TlsSetValue(indexTasHandler,(LPVOID)hHeap); } return HeapAlloc(hHeap,0,dwBytes); } Win32 multitâches - T. Joubert © THEORIS 2005 134 Résumé Une extension du code du processus La Dll est informée de son utilisation Appartient à l’espace d’adressage du processus Partage des handles L’attachement/le détachement de processus La création/la destruction de thread Les données sont privées ou partagées Une zone de données associées au processus Une zone de données privée à la Dll Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 135 135 Chapitre 10 Windows Sockets Win32 multitâches - T. Joubert © THEORIS 2005 136 Plan du chapitre sockets BSD / sockets Windows, protocoles standards UDP et TCP primitives de l’API Winsock 1.1 apport de la version 2.0 de l’API Winsock Win32 XP - © Theoris 2005 137 Cette partie du cours Win32 présente l'API Winsock 32 bit qui permet la communication réseau par les objets sockets. On s'intéressera d'abord aux bases: les sockets des systèmes BSD et les protocoles réseaux TCP/IP et UDP. Vient ensuite la présentation de l'API Winsock qui a introduit les sockets dans tous les systèmes Windows: quelques généralités suivies d'une description des primitives et des structures nécessaires dans tout programme utilisant les sockets. Une description des apports de la version 2.0 de la winsock sera faite en fin de chapitre. Pour mémoire, cette version n’est qu’une version beta et ne sera définitive qu’avec la version 5 de Windows NT. Win32 multitâches - T. Joubert © THEORIS 2005 137 Socket BSD: rappel Canal de communication inter-processus Standard de fait sur les réseaux de stations UNIX comme sur Internet modèle en couches: Processus Applicatif 7 Sockets Win32 XP - © Theoris 2005 TCP/UDP Transports 4 Réseau IP Réseau 3 Liaison 2 Physique 1 138 Le concept de sockets a été implémenté au départ comme une méthode de communication inter-processus sur les systèmes multitâches. Leur intérêt était d’apporter un canal de communication bidirectionnel entre applications. Les sockets sont apparus la première fois dans le noyau Unix Bsd 4.2. Mais les possibilités d’accès au réseau n’ont été ajoutées qu'à partir de la version 4.3 qui intégrait également les protocoles TCP/IP (Transmission Control Protocol / Internet Protocol) et UDP (User Datagram Protocol) dans le système d’exploitation. Le fort développement, ces dernières années, de réseaux locaux de stations UNIX, éventuellement réunis par Internet, a favorisé les développements et les utilisations massifs d’applications orientées réseaux, telles que la messagerie (email), le transfert de fichiers (ftp), ou le World Wide Web (mosaic, netscape). Toutes ces applications UNIX utilisent les sockets et ont fait des protocoles sous-jacents TCP/IP des standards de fait des protocoles réseaux. L’implémentation des sockets se trouve à un niveau relativement bas du modèle en couche, de type ISO, d’accès au réseau. L’API Socket gère les appels à la couche transport TCP ou UDP (niveau 4) qui utilise la couche réseau IP (niveau 3), elle-même en communication avec le driver MAC (médium access control, niveau 2) qui régule l’accès au médium physique (niveau 1). Ce découpage permet l’utilisation des sockets sur différent types de réseaux, ethernet, tokenring, appletalk ou encore à travers des modems. Les sockets ne sont pas forcément liées à l’utilisation des protocoles TCP ou UDP (XNS de Xerox et IPX/SPX de Novell sont supportés également) bien qu’ils soient certainement les plus utilisés, la principale contrainte étant alors qu’elles ne permettent pas de réaliser une programmation indépendante de la couche transport à utiliser. Win32 multitâches - T. Joubert © THEORIS 2005 138 Mode Connecté Protocole TCP Caractéristiques: bidirectionnel, simultanées (Full Duplex) contrôle des erreurs, du flux contrôle de l’ordre des séquences, des répétitions notion Le de ports de communication plus couramment utilisé: ftp, telnet, ... Win32 XP - © Theoris 2005 139 Deux modes de communication, connecté ou non, sont couramment disponibles pour utiliser les sockets. En mode connecté (sockets de type SOCK_STREAM), deux processus qui veulent converser doivent préalablement ouvrir un canal virtuel en s’étant mis d’accord sur les conditions à adopter, puis le refermer une fois l’échange d’informations terminé. Dans le domaine des sockets d'Internet, ce mode est réalisé grâce au protocole TCP qui assure un mécanisme fiable de communication au dessus du protocole IP qui ne sait que router les informations entre machines. Le protocole TCP permet un échange de donnés en simultané dans les deux sens de communication et ce sans préoccupation (de structure, de longueur) sur le flots de données, d’où le nom de socket de flux d’octets (bytes stream). Le protocole TCP s’assure que les données s’échangent: - sans pertes ou altérations, en demandant une ré-émission si nécessaire - dans le bon ordre: car malgré le découpage des données en paquets vers le réseaux, le protocole est chargé de les concaténer correctement à la réception. Ce protocole introduit également la notion de port de communication qui permet de répartir l’ensemble des données réseaux à destination d’une même machine vers les différentes applications s’y exécutant simultanément. un port est une adresse interne à la machine codé sur 16 bits. Normalement les numéros de port inférieurs à 256 sont réservés par le système, et sous UNIX les numéros sont même réservés jusqu’à 1024. Ainsi des processus serveurs travaillant avec TCP initient leur échanges sur leur port attribué d’un commun accord sur toute machine UNIX, comme ftp (port 21), telnet (port 23), sendmail (port 25). Win32 multitâches - T. Joubert © THEORIS 2005 139 Datagrammes Protocole UDP: mode sans connexion Caractéristiques: moins fiable que TCP séquencement non garanti simplicité utilisation locale Orienté messagerie, envois répétitifs Win32 XP - © Theoris 2005 140 Il est aussi possible d’utiliser les sockets en mode «déconnecté». Ce mode est associé au mode datagrammes (socket de type SOCK_DGRAM). Dans ce cas on utilise le protocole UDP, plus simple mais de même niveau que TCP. Les datagrammes gérés par UDP sont du même ordre de grandeur qu’un simple paquet de données envoyé sur le réseau. UDP ne donne aucune assurance de bonne livraison des données. Comme aucun canal virtuel n’est ouvert entre les deux interlocuteurs, UDP ne demande à la couche IP que de router un datagramme vers un destinataire. Si celui-ci est à l’écoute tant mieux, sinon les données sont perdues. UDP peut envoyer des paquets de données plus gros qu’une certaine limite admise sur le réseau, appelé l’unité maximum de transmission (MTU ~ 1460 octets), car il est capable de les découper comme TCP. ceci est pourtant fortement déconseillé, car il n’y a pas de notion d’ordre dans les paquets reçus pour UDP, alors selon la configuration réseaux des datagrammes partis tard peuvent arriver plus tôt que d’autres. Ces caractéristiques de UDP font qu’il est recommandé de l’utiliser uniquement sur un réseau local, qui assure pour sa part une bonne fiabilité. Le protocole UDP est plus simple d’utilisation que TCP (pas de connexion à gérer) dans les cas de paquets de données de longueur réduites et sur des réseaux fiables. Ce protocole est particulièrement orienté messagerie, comme dans le cas d’échange d’informations réduites ou périodiques tel que le timeserver, le tftp, etc..., et est utilisé également pour la diffusion (broadcast). Dans les cas plus complexes, du genre long transfert, ou dans les cas où l’ordre des paquets est importants, la mise en œuvre devient beaucoup plus lourde pour l’utilisateur. Win32 multitâches - T. Joubert © THEORIS 2005 140 Sockets Windows 32 bits Ressemblance énorme avec BSD: transposition facilitée Stack TCP/IP livrée avec Windows modèle en couches: Application 32 bits user kernel API WSOCK32.DLL Drivers TCP/UDP, IP Driver de carte réseau Win32 XP - © Theoris 2005 141 L’implémentation des sockets Windows est très proche de celle de BSD 4.3. Les sockets de Windows comme d’UNIX existent à l’intérieur de ce que l’on appelle un domaine de communication. Pour l’utilisateur cette notion de domaine se résume à un type d’adressage spécifique pour les sockets. les sockets Windows supportent plusieurs domaines: le domaine Internet qui utilise l’adressage des protocoles TCP/UDP/IP est celui qui nous intéresse mais elle supportent aussi d’autres domaines tels que Netware, Xerox, etc.. Les installations de Windows NT proposent en standard la livraison d’une Stack de protocoles nécessaire au domaine Internet. Cette Stack fournit les drivers kernels qui implémente les couches TCP, UDP et IP au dessus du driver kernel de carte réseau. Le partage de ressources «Windows» telles que les imprimantes et les disques réseaux est encore possible sur cette Stack par le protocole netbios. En revanche TCP/IP rend inutile le protocole netbeui (couche transport, compagnon de netbios) et permet d’homogénéiser le trafic réseau. Livrée également en standard, la librairie WSOCK32.DLL, qui est accompagnée par des applications typiquement dérivées d’application UNIX (telnet, ftp, netscape) , permet l’accès à cette Stack TCP/IP à partir de programmes de développeurs travaillant avec les librairies Win32. L’API actuellement implémentée dans la librairie WSOCK32.DLL est la Winsock version 1.1 . Win32 multitâches - T. Joubert © THEORIS 2005 141 Particularités Besoin d’accéder à une DLL: WSAStartup(), WSACleanup() WSAGetLastError() Sens de rangement des octets: processeur x86 (little endian): faible --> fort standard réseau (big endian): fort --> faible Structures: (SOCKADDR) SOCKADDR_IN Win32 XP - © Theoris 2005 142 L’API Winsock étant implémentée la librairie WSOCK32.DLL, il faut demander à cette DLL de s'initialiser en interne et de vérifier la compatibilité de sa version avec le programme utilisateur. Pour cette raison il est indispensable d’appeler WSAStartup() avant toute chose. En lui passant un numéro de version, cet appel: - négocie le numéro de version accepté au maximum par l’utilisateur - vérifie que la configuration réseau fonctionne - retourne des informations telles que le maximum de sockets utilisables int PASCAL FAR WSAStartup ( WORD wVersionRequested, LPWSADATA lpWSAData ); WSACleanup() doit au contraire être utilisée pour informer le système de la fin d’utilisation de la DLL et lui permettre ainsi de libérer les ressources associées (fermeture des sockets encore ouvertes). WSAGetLastError() doit remplacer la variable errno de BSD, elle retourne le code de la dernière erreur provoquée par un appel à l’API Winsock. Il n’est pas interdit d’en abuser surtout dans un environnement réseau car les erreurs sont plus fréquentes. Une autre particularité de l’API Winsock vient de l’installation possible de Windows NT sur des machines à base de processeur de la famille x86. Ces processeurs possèdent un sens de stockage des octets (little endian) inverse du sens de stockage des processeurs alpha, mips, etc. et plus important, inverse du sens d’envoi sur le réseau (big endian). Cette inversion n’a pas de conséquences sur les structures de données utilisateur échangées entre machines de même type, mais toute machine doit présenter au réseau les structures d’adressage (port et adresse IP sont des entiers) de ces paquets en mode big endian. Win32 multitâches - T. Joubert © THEORIS 2005 142 pour prévenir d’éventuelles erreurs d'adressage, l’API propose une série de fonctions de conversions, htonl(), htons() (Host To Network Long, Host To Network Short), et d’autres fonctions d’information comme inet_addr(), qui renvoient leur résultat dans l’ordre du réseau, ceci quelque soit le processeur. Les fonctions de conversions inverses existent aussi. Les «float» IEEE (codés sur 4 octets) doivent aussi être retournés. Les «double» aussi mais il faut utiliser 2 conversions et passer par une variable temporaire (retournement de 8 octets en 2 coups de 4 octets). Enfin pour spécifier et retenir les adresses des extrémités de la socket ( les adresses des interlocuteurs), on utilise une structure SOCKADDR. Cette structure généraliste est utilisée quelque soit le type d’adressage. En fait son premier champ est le type (famille) de l’adresse contenue. struct sockaddr { u_short sa_family; char sa_data[14]; }; Dans le domaine Internet la structure SOCKADDR provient toujours d’un cast de la structure SOCKADDR_IN construite pour l’adressage sous TCP/IP: struct sockaddr_in { short sin_family; unsigned short sin_port; struct in_addr sin_addr; char sin_zero[8]; }; SOCKADDR_IN doit toujours contenir comme valeur de famille la macro AF_INET. Le reste est l’adresse à remplir convenablement (en big endian) grâce aux fonctions précédentes, avec un numéro de port et une adresse IP (le champ sin_zero ne sert pas). Win32 multitâches - T. Joubert © THEORIS 2005 143 Le modèle client/serveur Le plus couramment utilisé: Serveur init. liaison écoute attente échange Client init. liaison Win32 XP - © Theoris 2005 connexion 144 Les utilisateurs, amenés à développer avec les sockets, doivent se familiariser avec l’utilisation la plus courante qui en est faite depuis qu’elles sont apparues dans les communications réseaux: le modèle client-serveur. Dans ce modèle le serveur contient les informations ou un type de traitement de données recherchés par des clients. Cette dissymétrie de rôle entre applications serveur et client implique évidemment des différences dans leur implémentation. Que ce soit un client ou un serveur, tous les deux passent par une phase d’initialisation des sockets avec marquage de leur propre adresse qui contient le numéro IP de la machine et le numéro de port qui répartira les données entre les différents threads d’un même système. Ensuite le serveur se déclare en écoute sur sa socket et passe en mode d’attente de connexions de clients. Un client connaissant l’adresse du serveur (IP + port) peut demander alors l’établissement d’un canal de communication concrétisé par les extrémités client + serveur. Une fois le canal établi, l’échange d’informations peut commencer. L’API Winsock 1.1 fournit évidemment toutes les primitives nécessaires à la réalisation de ce modèle, avec en plus des fonctions «base de données» qui permettent entre autres la résolution des noms de machines d’Internet (du type «host.domain.fr») en adresses IP (du type 192.0.0.1), et un jeu de fonctions asynchrones adaptées à l’environnement fenêtré de Windows. Win32 multitâches - T. Joubert © THEORIS 2005 144 Création / Destruction Création: socket() Options, comportement: setsockopt(), Initialisation ioctlsocket() de l’adressage local: bind() Fermeture: closesocket() Win32 XP - © Theoris 2005 145 Première étape: création d’une socket. Il suffit de préciser le format d’adresse (AF_INET pour le domaine Internet), le type (SOCK_STREAM ou SOCK_DGRAM) et le protocole (0 demande le protocole par défaut): SOCKET PASCAL FAR socket ( int af, int type, int protocole ); Il est possible de contrôler certaines options et comportements des sockets grâce aux fonctions setsockopt() et ioctlsocket() qui remplace ioctl() d’UNIX dans le cas des Winsock. Elles permettent entre autres de rendre non-bloquants les échanges de données, de choisir entre les sockets synchrones ou asynchrones... Ensuite et avant de tenter une connexion, il faut spécifier l’adresse locale de la socket avec la commande bind(). Cette étape est importante pour un serveur qui doit indiquer son point d’entrée. int PASCAL FAR bind ( SOCKET s, const struct sockaddr FAR * name, int namelen ); Pour cet appel il faut remplir une structure SOCKADDR_IN (famille AF_INET), avec le numéro de port sur lequel on veut que le thread communique. La partie adresse IP de la structure ne pose problème que si la machine locale possède plusieurs cartes réseaux et qu’il est nécessaire d’en utiliser une en particulier. Une adresse IP de valeur INADDR_ANY indique que toute adresse de carte locale est possible. Et un numéro de port nul demande au système de choisir un numéro de port libre. Un client peut se passer d’indiquer son adresse locale s’il n’y a pas de méprise possible (par exemple il ne possède qu'une seule carte réseau), le système choisira lui-même l'adresse et un port libre lors de la connexion. Win32 multitâches - T. Joubert © THEORIS 2005 145 Pour en terminer avec la gestion de l’objet socket, après la fin des échanges d’informations ou en cas d’erreurs irrécupérables il est conseillé et plus propre de libérer les ressources de la socket par un appel à closesocket(): int PASCAL FAR closesocket ( SOCKET s ); Sous Unix, on utilise la fonction close(). Win32 multitâches - T. Joubert © THEORIS 2005 146 Connexion Côté Serveur: préparer les connexions listen(): définir le nombre de connexions simultanées accept(): attendre la connexion d’un client Côté Client: demander les connexions connect(): effectuer une connection Terminaison: terminer proprement l’échange shutdown() Win32 XP - © Theoris 2005 147 Dans le cadre de sockets en mode connecté: Une fois la phase d’initialisation terminé, un serveur indique au système que sa socket veut écouter les demandes des clients. Il réalise cela par un appel à listen() dans lequel il doit spécifier la longueur de la file d’attente des demandes de connexions: int PASCAL FAR listen ( SOCKET s, int length ); Un appel de la fonction accept() bloque ensuite le thread jusqu'à ce qu’un client envoie une demande de connexion. accept() renvoie alors l’identificateur d’une nouvelle socket totalement configurée pour les échanges d’informations. La socket d’écoute sur serveur est ainsi toujours libre et peut subir l’appel d’un nouvel accept(): SOCKET PASCAL FAR accept ( SOCKET s, struct sockaddr FAR * addr, int FAR * addrlen ); Au niveau du client, la demande de connexion se fait par connect() à laquelle on passe l’adresse du serveur stockée dans une structure SOCKADDR_IN. int PASCAL FAR connect ( SOCKET s, const struct sockaddr FAR * name, int namelen ); Win32 multitâches - T. Joubert © THEORIS 2005 147 Enfin pour terminer une connexion autrement que par un brutal closesocket(), shutdown() permet de signaler à l’interlocuteur que l’application locale n’enverra plus de données, et/ou qu’elle ne veut plus en recevoir. Ceci retournera un code d’erreur particulier à traiter dans l’application distante: int PASCAL FAR shutdown ( SOCKET s, int how ); Le paramètre «how» permet de spécifier si la socket est fermée en lecture, en écriture ou dans les des sens. L’utilisation de shutdown ne nous dispense pas d’appeler closesocket pour libérer la socket. Le client ne peut pas refaire connect à partir d’un socket déjà utilisé. Win32 multitâches - T. Joubert © THEORIS 2005 148 Echanges En mode connecté: TCP send() recv() Datagrammes: UDP sendto() recvfrom() Multiplexage: select() Win32 XP - © Theoris 2005 149 En mode connecté, les échanges de données se font avec send() et recv(). int PASCAL FAR send ( SOCKET s, const char FAR * buf, int len, int flags ); int PASCAL FAR recv ( SOCKET s, char FAR * buf, int len, int flags ); Il n'existe pas vraiment de limitation sur les longueurs des données. Aussi un appel à send() bloque jusqu'à ce que toutes les données soient passées dans le buffer système. Un appel à recv() bloque tant qu'il n'y a rien de reçu. Un long envoi peut être reçu en plusieurs petits morceaux et inversement. Les échanges en mode déconnecté se font grâce aux fonctions sendto() et recvfrom() qui utilisent une structure d’adresse SOCKADDR_IN supplémentaire par rapport à send() et recv(), pour spécifier/récupérer l’adresse de l’interlocuteur: int PASCAL FAR sendto ( SOCKET s, const char FAR * buf, int len, int flags, const struct sockaddr FAR * to, int tolen ); int PASCAL FAR recvfrom ( SOCKET s, char FAR * buf, int len, int flags, struct sockaddr FAR * from, int FAR * fromlen ); une socket SOCK_DGRAM, peut spécifier son interlocuteur avec un appel à connect(), il doit alors utiliser send() et recv() à la place de sendto et recvfrom. Win32 multitâches - T. Joubert © THEORIS 2005 149 Un datagramme ne doit pas dépasser la valeur maximum retourné par WSAStartup(), et il n'est pas conseillé de dépasser la longueur d'un MTU (~ 1460 octets). Un serveur se trouve souvent dans une situation où il doit gérer plusieurs sockets à la fois. Sous UNIX la solution souvent adoptée est de créer un nouveau processus fils qui s’occupera d’une socket donnée. Sous Windows la même politique reste possible en créant un thread pour chaque socket. Une autre solution, issue aussi d’UNIX, est de gérer des groupes de sockets grâce à la fonction select() à partir des structures fd_set: int PASCAL FAR select ( int nfds, fd_set FAR * readfds, fd_set FAR * writefds, fd_set FAR * exceptfds, const struct timeval FAR * timeout ); fd_set est un ensemble de sockets. Des macros existent pour en faciliter l’utilisation. FD_CLR(s, *set) enlève un socket dans l’ensemble. FD_ISSET(s, *set) test si un socket est dans l’ensemble FD_SET(s, *set) ajoute un socket dans l’ensemble FD_ZERO(*set) initialise un ensemble vide Select() met à jour les ensembles passés en argument en y laissant les sockets qui remplissent la condition et retourne leur nombre au total: readfds writefds exceptfds // ensemble des sockets pour lesquelles une // ensemble des sockets pour lesquelles // autre ensemble de sockets (erreurs par exemple) lecture/un accept une écriture possible possible Cette technique est performante car elle permet de surveiller plusieurs sockets en même temps et de n’appeler les routines send ou recv que lorsque la fonction est possible et non bloquante. Select exploite et modifie les ensembles passés en paramètres. il est peut être nécessaire de conserver une copie des ensembles. Win32 multitâches - T. Joubert © THEORIS 2005 150 Compléments Fonctions «base de données»: Extensions asynchrones pour Windows: exemple: gethostbyname(); exemples: WSAAsyncSelect(); WSAGetHostByName(); Evolutions Win32 XP - © Theoris 2005 151 Une connexion par socket nécessite de connaître l’adresse IP (ex: 192.0.0.1) de son interlocuteur, or ces adresses numériques sont moins faciles à retenir que celles du type "host.domain.fr". C’est pour cela que l’API Winsock fournit une série de fonctions «base de données» chargées de la résolution des noms de machines en adresses IP, et également des noms de services en numéro de port, et des noms de protocoles. Ces fonctions permettent d’abstraire le niveau de service qui effectue la résolution, tels que le DNS (domain name server) ou simplement le fichier /system32/drivers/etc/hosts sous Windows NT. Une autre série de fonctions, particulières à la Winsock, permettent de tirer avantage des messages pour fenêtres de toutes les versions de Windows. Ces fonctions remplacent leur version standard dérivée de BSD par un appel non-bloquant qui signalera la fin du traitement demandé par un message de type WM_USER envoyé à la fenêtre de l’application. Ces fonctions asynchrones très appropriées à Windows 3.1 sont maintenant moins intéressantes dans des environnement multi-threads tels que NT. Pour C++, il existe des objets CSocket et CAsyncSocket dans les MFC 3.1. Win32 multitâches - T. Joubert © THEORIS 2005 151 Winsock 2.0 Nouvelle architecture Plus de limitation à TCP/IP Compatibilité WinSock 1.1 Nouvelles fonctionnalités Gestion simultanée de plusieurs protocoles Gestion du mode overlapped I/O Généralisation du préfixe WSA Win32 XP - © Theoris 2005 152 Le « WinSock Group » s ’est constitué en 1991, ce consortium, comprenant plusieurs compagnies indépendantes (Microsoft, Intel, Novell, Motorola, DEC…), travaille sur la spécification des Windows Sockets. La spécification WinSock 1.1 a été publiée en janvier 1993, et à partir de cette date elle est devenue le standard de fait pour accéder à TCP/IP sur les PC, remplaçant les sockets offerts par des produits tels que PC-NFS, PC-TCP ou Salamander. La WinSock 1.1 a été conçue pour fonctionner sous Windows 3.1, et pour se conformer au plus près aus sockets BSD 4.3. Le succès de Win32 depuis sa sortie en 1993 ainsi que la présence d ’autres protocoles dans le monde PC ou client/serveur imposaient l ’enrichissement de la WinSock 1.1 ou la définition d ’une nouvelle API. La Winsock 2.0 est ces deux choses à la fois, elle est bien compatible avec les sockets BSD 4.3 mais l ’enrichissement est passé par l ’ajout de nouvelles fonctions (préfixe WSA) avec une forte connotation Win32. La spécification a démarré mi-1994 pour une disponibilité mi1996, correspondant à la sortie de NT 4.0. Le développement d ’applications « full WinSock 2.2 » ne va donc pas dans le sens d ’une compatibilté NT - UNIX, à moins que les fournisseurs d ’UNIX n ’adoptent à leur tour la WinSock 2.2... Win32 multitâches - T. Joubert © THEORIS 2005 152 Nouvelle architecture W inS ock 2 A p plication W inS ock 1 .1 A p p licatio n W inS o ck 1.1 A P I W IN S O C K .D L L (1 6 b it) W S O C K 3 2.D L L (32 b it) W inS ock 2 A P I W S 2 _ 3 2.D L L (32 b it) W inS o ck 2 S P I’s T C P /IP T ransp o rt S ervice P rovid er Win32 XP - © Theoris 2005 T C P /IP -b ased N am esp ace S ervice P rovid er e.g. D N S 153 La winsock 2.0 s’appuie sur une nouvelle architecture afin de supporter les communications simultanées utilisant des protocoles différents. Contrairement à la WinSock 1.1 où le fournisseur de la stack TCP/IP fournissait son implémentation des librairies dynamiques WINSOCK.DLL (16 bits) et WSOCK32.DLL (32 bits), la WinSock 2.0 se compose d ’une seule librairie 32 bits fournie avec le système: WS2_32.DLL (les anciennes librairies sont toutefois conservées pour des raisons de compatibilité). L ’architecture comprend un niveau supplémentaire destiné aux fournisseurs de services, c ’est une interface standard d’accès aux différentes stacks de communication (Service Provider Interface : SPI) qui a été définie pour autoriser l ’accès de la WS2_32.DLL à d ’autres protocoles que TCP/IP (en particulier IPX/SPX, DECnet et OSI). Win32 multitâches - T. Joubert © THEORIS 2005 153 Nouvelles fonctionnalités Multi-protocoles WSAEnumProtocols Gestion du mode overlapped I/O WSAWaitforMultipleEvents, … WSACreateEvent, … Préfixe WSA Win32 XP - © Theoris 2005 154 Gestion des protocoles : La fonction WSAEnumProtocols permet de connaître la liste des protocoles disponibles sur une machine. Les informations sur chaque protocole installé sont fournies dans une structure WSAPROTOCOL_INFO. On trouve en particulier dans cette structure les valeurs des champs famille et type pour l ’adressage. Gestion de l’overlapped : Les fonctions relatives à l’utilisation des sockets ont un équivalent WSA (ex : accept a comme équivalent WSAAccept) qui permet de prendre en compte la gestion de l’overlapped. Par ailleurs, la winsock fournit des fonctions relatives à la gestion d’événement (WSACreateEvent, WSACloseEvent, WSASetEvent, WSAGetOverlappedResult WSAWaitforMultiplesEvents, ...) que l’on peut remplacer par les fonctions standard de gestion d’événement de la win32. Préfixe WSA : la winsock 2.0 fournit un équivalent WSA à toutes les fonctions de type socket même si elles n’ont aucun rapport avec la gestion de l’overlapped (ex : WSAHtonl, WSAGetAddressByName, ...). http://www.intel.com/ial/winsock2/specs.htm Win32 multitâches - T. Joubert © THEORIS 2005 154 Résumé Les Windows sockets // sockets BSD TCP UDP Mode connecté Garantie de service (sécurité, ordre) Mode sans connection Sans garantie de service La WinSock 2.0 définit un nouveau standard Win32 XP - © Theoris 2005 Win32 multitâches - T. Joubert © THEORIS 2005 155 155