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

Documents pareils