expériences sur l’exécution du CPU, caches, fichiers, pilotes et système d’exploitation. Les extraits « pseudo-code » illustrent une relation matérielle et ne sont pas des programmes à compiler.
7. À L'INTÉRIEUR DU CPU : FETCH, DECODE, EXECUTE, RETIRE
7.1 Expérience : dérouler une suite d'instructions
Code: Select all
; x86-64, exemple conceptuel
mov eax, 3
add eax, 4
imul eax, eax, 2
; résultat EAX = (3 + 4) * 2 = 14
7.2 Expérience : dépendance vraie versus opérations indépendantes
Code: Select all
; cas A : dépendance en chaîne
add eax, 1
add eax, 1
add eax, 1
; chaque résultat dépend du précédent
;
; cas B : indépendance logique
add eax, 1
add ebx, 1
add ecx, 1
; les opérations peuvent être mises en parallèle
7.3 Expérience : boucle, saut et prédiction
Code: Select all
#include <stddef.h>
int somme_positifs(const int *a, size_t n) {
int total = 0;
for (size_t i = 0; i < n; ++i)
if (a[i] > 0) total += a[i];
return total;
}
7.4 Expérience : le programme source a plusieurs traductions
Code: Select all
int choisir(int a, int b, int c) { return c ? a : b; }
// gcc -O0 -S choisir.c -o choisir0.s
// gcc -O2 -S choisir.c -o choisir2.s
7.5 Expérience : accès mémoire et dépendance sur la donnée chargée
Code: Select all
int lire_plus_un(const int *p) {
int v = *p;
return v + 1;
}
// Instructions logiques : load -> addition -> résultat.
7.6 Expérience : ordre logique et ordre microarchitectural
Code: Select all
int x = 1;
int y = 2;
int resultat = x + y;
// À l'échelle du langage C, résultat == 3.
7.7 Expérience : superscalaire ne veut pas dire deux cœurs
Code: Select all
// Un seul cœur peut traiter plusieurs micro-opérations par cycle,
// dans la limite de ses unités d'exécution et de ses dépendances.
// Deux cœurs exécutent en parallèle des threads distincts,
// sous réserve de l'ordonnanceur et des ressources partagées.
7.8 Exercice d'analyse
Code: Select all
int traiter(int a, int b) {
int c = a + 2;
int d = b + 5;
return c * d;
}
8. SUIVRE UN PROGRAMME DU DISQUE JUSQU'AU TERMINAL
8.1 Expérience : Hello World complet, étape par étape
Code: Select all
// hello.c
#include <stdio.h>
int main(void) {
puts("Salut");
}
// gcc -O0 -g hello.c -o hello
// ./hello
8.2 Expérience : l'exécutable contient déjà des caractères
Code: Select all
# Linux :
strings -a hello | grep Salut
readelf -S hello
objdump -s -j .rodata hello
8.3 Expérience : suivre les écritures du processus
Code: Select all
# Linux :
strace -e trace=write ./hello
strace -e trace=write ./hello > resultat.txt
od -An -tx1 resultat.txt
8.4 Expérience : écrire sans printf sous Linux
Code: Select all
#include <unistd.h>
#include <errno.h>
#include <stddef.h>
int main(void) {
const char texte[] = "SALUT\n";
size_t fait = 0, total = sizeof texte - 1;
while (fait < total) {
ssize_t n = write(STDOUT_FILENO, texte + fait, total - fait);
if (n < 0 && errno == EINTR) continue;
if (n <= 0) return 1;
fait += (size_t)n;
}
return 0;
}
8.5 Expérience : sortie vers fichier avec un vrai buffer de sortie
Code: Select all
#include <stdio.h>
int main(void) {
FILE *f = fopen("sortie.bin", "wb");
if (!f) return 1;
unsigned char b[] = {0x12, 0x34, 0x00, 0xFF};
size_t n = fwrite(b, 1, sizeof b, f);
int fermeture = fclose(f);
if (n != sizeof b || fermeture != 0) return 2;
return 0;
}
8.6 Expérience : un enfant reçoit une sortie redirigée
Code: Select all
# Linux :
./hello | wc -c
./hello > resultat.txt
./hello 2> erreurs.txt
8.7 Expérience : distinguer programme, processus et thread
Code: Select all
// Le fichier hello sur disque est un programme.
// ./hello puis ./hello lancés séparément -> deux exécutions
// avec des contextes de processus distincts.
// Chaque processus a au moins un thread exécutant des instructions.
9. CACHE CPU : COMPRENDRE PAR DES MESURES
9.1 Expérience : parcours contigu et parcours à grand pas
Code: Select all
// Bench C11 (Linux/Windows) :
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void) {
enum { N = 1 << 20, PAS = 64 };
int *a = malloc((size_t)N * sizeof *a);
if (!a) return 1;
for (int i=0; i<N; ++i) a[i] = i;
volatile uint64_t somme = 0;
clock_t t = clock();
for (int repetition=0; repetition<200; ++repetition)
for (int i=0; i<N; ++i) somme += (unsigned)a[i];
double t1=(double)(clock()-t)/CLOCKS_PER_SEC;
t=clock();
for (int repetition=0; repetition<200*PAS; ++repetition)
for (int i=0; i<N; i+=PAS) somme += (unsigned)a[i];
double t2=(double)(clock()-t)/CLOCKS_PER_SEC;
printf("seq=%.3f s pas=%.3f s checksum=%llu\n",t1,t2,
(unsigned long long)somme);
free(a);
}
9.2 Expérience : pourquoi la ligne de cache est importante
Code: Select all
// Si la ligne matérielle fait 64 octets (courant sur x86-64) :
// lire a[0], un int de 4 octets, peut faire entrer
// dans un niveau de cache d'autres int voisins.
// a[1] peut alors être déjà dans la ligne chargée.
9.3 Expérience : localité temporelle
Code: Select all
#include <stdint.h>
void travailler(uint32_t *a) {
for (int r = 0; r < 1000; ++r)
for (int i = 0; i < 64; ++i) a[i] += 1;
}
9.4 Expérience : ligne chaînée contre tableau
Code: Select all
// Tableau : chaque index pointe vers une position prévisible.
for (size_t i = 0; i < n; ++i) total += a[i];
// Liste : chaque nœud indique l'adresse du suivant.
for (struct Node *p = tete; p; p = p->suivant) total += p->v;
9.5 Expérience : mauvais partage de ligne (false sharing)
Code: Select all
// Schéma de données partagé par deux threads :
struct Compteurs { long a, b; } c;
// Thread 1 modifie surtout c.a ; thread 2 surtout c.b.
// Même si les variables sont distinctes, elles peuvent occuper
// la même ligne de cache et causer de la contention de cohérence.
9.6 Expérience : une mesure de cache n'est pas universelle
Code: Select all
# Linux, si perf est disponible et autorisé :
perf stat -e cache-references,cache-misses ./benchmark
# Comparer plusieurs exécutions et tailles de jeu de données.
9.7 Cas réel : matrice parcourue dans le mauvais sens
Code: Select all
#define N 512
static int matrice[N][N];
long par_lignes(void) {
long s=0;
for (int i=0; i<N; ++i)
for (int j=0; j<N; ++j) s+=matrice[i][j];
return s;
}
long par_colonnes(void) {
long s=0;
for (int j=0; j<N; ++j)
for (int i=0; i<N; ++i) s+=matrice[i][j];
return s;
}
9.8 Exercices de prédiction
E1 : a[0] et a[1] sont voisins : pourquoi le second peut-il être rapide ? E2 : une liste chaînée a des nœuds éparpillés : pourquoi la somme est-elle souvent plus lente ? E3 : une ligne est dans L2 mais pas L1 : est-ce un hit ou un miss ? Réponse : un miss pour L1 et un hit pour L2 ; précise toujours le niveau. E4 : si une fonction devient plus rapide après 100 répétitions, explique les hypothèses (cache, instruction cache, fréquence, planification) avant de conclure.
10. HIÉRARCHIE DE STOCKAGE : LATENCE, DÉBIT, PERSISTANCE
10.1 Expérience : RAM et fichier ne sont pas équivalents
Code: Select all
#include <stdio.h>
int main(void) {
unsigned char ram[4] = {0x12,0x34,0x56,0x78};
FILE *f = fopen("octets.bin", "wb");
if (!f) return 1;
size_t n = fwrite(ram, 1, sizeof ram, f);
int etat = fclose(f);
return n == sizeof ram && etat == 0 ? 0 : 2;
}
10.2 Expérience : persistance réelle sous POSIX
Code: Select all
// Linux/POSIX, création et écriture d'un fichier :
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <stddef.h>
int main(void) {
int fd = open("etat.bin", O_WRONLY|O_CREAT|O_TRUNC, 0600);
if (fd < 0) return 1;
const char msg[] = "OK";
size_t fait = 0;
while (fait < sizeof msg - 1) {
ssize_t n = write(fd, msg + fait, sizeof msg - 1 - fait);
if (n < 0 && errno == EINTR) continue;
if (n <= 0) { close(fd); return 2; }
fait += (size_t)n;
}
int a = fsync(fd); // demande de synchronisation vers stockage
int b = close(fd);
return a == 0 && b == 0 ? 0 : 3;
}
10.3 Expérience : tampon C, cache noyau, cache CPU
Code: Select all
// fputs("abc", f);
// fflush(f); -> vide le tampon de la bibliothèque C vers l'OS
// fsync(fd); -> demande au noyau de synchroniser le fichier
// cache CPU -> accélère l'accès aux lignes mémoire, autre mécanisme
10.4 Expérience : latence versus débit
Code: Select all
// Scénario A : 1000 opérations de 1 octet
// Scénario B : 1 opération de 1000 octets
// Nombre total d'octets identique, nombre d'appels différent.
10.5 Expérience : accès aléatoire et accès séquentiel
Code: Select all
// Comparer conceptuellement :
// lire les blocs 0, 1, 2, 3 ... (séquentiel)
// lire les blocs 205, 7, 602, 12 ... (éparpillé)
// Sur HDD, les déplacements physiques coûtent cher.
// Sur SSD, pas de tête mécanique, mais lecture aléatoire
// et petites requêtes conservent un coût de traitement.
10.6 Expérience : mémoire mappée versus lecture explicite
Code: Select all
// POSIX, exemple explicatif du principe :
// read(fd, buffer, taille) : le noyau copie des octets vers buffer.
// mmap(..., MAP_PRIVATE, fd, ...) : zone virtuelle associée au fichier.
// Un accès à cette zone peut provoquer un défaut de page,
// puis un chargement depuis le cache de pages ou le stockage.
11. ENTRÉES/SORTIES : POLLING, INTERRUPTIONS ET DMA
11.1 Expérience : attente active contre notification
Code: Select all
// PSEUDO-CODE de pilote :
while (!(LireRegistreEtat() & PRET)) {
/* polling : on consulte en boucle */
}
// Alternative conceptuelle :
// lancer une commande, dormir ou faire autre chose,
// puis être notifié par interruption/événement d'achèvement.
11.2 Expérience : transfert programmé versus DMA
Code: Select all
// Sans DMA (modèle simplifié) :
// CPU : lire des registres du périphérique et copier les données.
// Avec DMA (modèle simplifié) :
// CPU : préparer des descriptors / buffers et déclencher l'opération.
// Périphérique/contrôleur : transférer des blocs vers/de la RAM.
// CPU : traiter une notification d'achèvement.
11.3 Expérience : un SSD NVMe reçoit une demande de lecture
Code: Select all
// 1. Application : ReadFile / read
// 2. Noyau : valide, route, peut satisfaire via cache de pages
// 3. Si E/S requise : pilote prépare une commande NVMe
// 4. Contrôleur : accède aux buffers autorisés via DMA
// 5. Achèvement : notification, pilote met à jour la requête
// 6. Application : reçoit ses octets ou une erreur
11.4 Expérience : écrire des octets en Win32 et expliquer chaque paramètre
Code: Select all
// Windows, code complet, MSVC : cl /W4 fichier.c
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>
int main(void) {
HANDLE f = CreateFileW(L"demo.bin", GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (f == INVALID_HANDLE_VALUE) {
printf("CreateFile: erreur %lu\n", (unsigned long)GetLastError());
return 1;
}
const BYTE octets[4] = {0x12,0x34,0x56,0x78};
DWORD ecrits = 0;
BOOL ok = WriteFile(f, octets, sizeof octets, &ecrits, NULL);
DWORD erreur = ok ? 0 : GetLastError();
if (!CloseHandle(f)) return 4;
if (!ok) {
printf("WriteFile: erreur %lu\n", (unsigned long)erreur);
return 2;
}
if (ecrits != sizeof octets) return 3;
printf("%lu octets ecrits\n", (unsigned long)ecrits);
return 0;
}
Pourquoi CreateFileW ? Windows doit associer un nom de chemin à un objet ouvert et retourner un HANDLE. Le suffixe W signifie chemin en chaînes larges UTF-16 Windows. Pourquoi CloseHandle ? Libérer l'accès à cet objet ; sans cette fermeture, le processus garde le handle ouvert jusqu'à la terminaison (ou fermeture ultérieure).
11.5 CreateFileW : les SEPT paramètres, aucun laissé flou
Code: Select all
HANDLE CreateFileW(
LPCWSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile
);
2. dwDesiredAccess = GENERIC_WRITE : droit demandé. GENERIC_READ pour lire, GENERIC_WRITE pour écrire, combinaison OR pour les deux. Piège : obtenir un handle n'accorde pas tous les droits ; ReadFile sur un handle sans droit de lecture peut échouer.
3. dwShareMode = 0 : accès exclusif selon les règles de partage Windows. FILE_SHARE_READ et FILE_SHARE_WRITE autorisent des ouvertures compatibles ultérieures. Pourquoi c'est utile ? contrôle des accès concurrents. Piège : le partage doit être compatible avec les accès des handles déjà ouverts ; c'est un contrat bilatéral, pas juste « j'autorise quelqu'un à lire ».
4. lpSecurityAttributes = NULL : attributs de sécurité par défaut et handle non héritable par défaut. Une SECURITY_ATTRIBUTES peut spécifier un descripteur de sécurité et l'héritabilité du handle ; ce n'est pas un buffer de données.
5. dwCreationDisposition = CREATE_ALWAYS : crée si absent ; si un fichier existant est accessible, le remplace logiquement par une version tronquée (son ancien contenu est perdu). OPEN_EXISTING exige un fichier existant ; OPEN_ALWAYS ouvre ou crée ; CREATE_NEW échoue si déjà présent ; TRUNCATE_EXISTING exige un fichier et le vide. Pourquoi distinguer ? ouvrir un journal ne doit pas forcément détruire son contenu.
6. dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL : attributs et indicateurs. FILE_FLAG_OVERLAPPED pour des E/S asynchrones, FILE_FLAG_SEQUENTIAL_SCAN comme indication d'usage, FILE_FLAG_NO_BUFFERING avec contraintes strictes. Pourquoi ? demander des politiques d'E/S adaptées à une utilisation précise.
7. hTemplateFile = NULL : handle optionnel d'un fichier modèle lors de certaines créations ; NULL quand aucun modèle n'est utilisé. Piège : ce n'est pas le handle du fichier qu'on essaie d'ouvrir.
Retour : HANDLE valide en cas de succès ; INVALID_HANDLE_VALUE en cas d'échec. Il faut immédiatement consulter GetLastError après l'échec si l'on veut diagnostiquer la cause.
11.6 WriteFile : les CINQ paramètres
Code: Select all
BOOL WriteFile(
HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten,
LPOVERLAPPED lpOverlapped
);
11.7 Expérience : exclusivité de partage et erreurs Windows
Code: Select all
// Extrait à lancer alors que premier handle n'est pas fermé :
HANDLE a = CreateFileW(L"demo.bin", GENERIC_READ, 0, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (a == INVALID_HANDLE_VALUE) return 1;
HANDLE b = CreateFileW(L"demo.bin", GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (b == INVALID_HANDLE_VALUE) {
DWORD err = GetLastError(); // souvent ERROR_SHARING_VIOLATION
/* exploiter err dans votre diagnostic */
} else CloseHandle(b);
CloseHandle(a);
11.8 Expérience : lire un fichier et vérifier le nombre d'octets
Code: Select all
// Windows, fonction autonome à appeler depuis un programme :
#include <windows.h>
int lire_quatre(void) {
HANDLE h = CreateFileW(L"demo.bin", GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (h == INVALID_HANDLE_VALUE) return 1;
BYTE buf[4] = {0};
DWORD recu = 0;
BOOL ok = ReadFile(h, buf, sizeof buf, &recu, NULL);
DWORD err = ok ? 0 : GetLastError();
CloseHandle(h);
if (!ok) return (int)(err ? err : 2);
if (recu != sizeof buf) return 3; // fichier plus court que prévu
return buf[0] == 0x12 && buf[1] == 0x34 &&
buf[2] == 0x56 && buf[3] == 0x78 ? 0 : 4;
}
11.9 Expérience : faire varier un octet et prédire le fichier
Code: Select all
// Cas A :
BYTE tab[] = {0xAB, 0xCD};
// WriteFile(..., tab, 2, ...): AB CD.
// Cas B, sur x86-64 little-endian :
WORD n = 0xABCD;
// WriteFile(..., &n, sizeof n, ...): CD AB.
// Cas C, encodage texte :
char texte[] = "AB";
// WriteFile(..., texte, 2, ...): 41 42 en ASCII/UTF-8.
12. SYSTÈME D'EXPLOITATION : APPELS SYSTÈME, PROTECTION, PROCESSUS
12.1 Expérience : pourquoi le code utilisateur ne peut pas tout faire
Code: Select all
// Windows :
// CreateFileW ouvre un objet du système via les API fournies.
// WriteFile transmet les données à cet objet.
// Une application ordinaire ne lit pas un disque physique
// via une instruction arbitraire qui contourne le noyau.
12.2 Expérience : processus ≠ fichier exécutable ≠ thread
Code: Select all
// Windows :
// Deux lancements de programme.exe -> deux processus distincts.
// Chaque processus peut créer plusieurs threads.
// Le code (.text) provient du même fichier sur disque,
// mais les variables ordinaires de chaque processus sont isolées.
12.3 Expérience : taille mémoire demandée et adresse virtuelle obtenue
Code: Select all
// Windows, programme complet : cl /W4 reserve.c
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>
int main(void) {
SIZE_T taille = 4096;
BYTE *p = VirtualAlloc(NULL, taille,
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
if (!p) {
printf("Erreur VirtualAlloc %lu\n", (unsigned long)GetLastError());
return 1;
}
p[0] = 0xA1;
p[taille - 1] = 0xB2;
printf("adresse virtuelle=%p valeurs=%02X %02X\n",
(void*)p, (unsigned)p[0], (unsigned)p[taille - 1]);
return VirtualFree(p, 0, MEM_RELEASE) ? 0 : 2;
}
VirtualFree : base retournée, taille 0, MEM_RELEASE pour libérer toute cette réservation. Piège : la valeur numérique de p peut varier entre lancements ; afficher une adresse virtuelle ne révèle pas l'adresse physique des cellules DRAM.
12.4 Expérience : protection d'une page virtuelle
Code: Select all
// Windows, après VirtualAlloc et accès contrôlés :
DWORD ancienne = 0;
if (!VirtualProtect(p, 4096, PAGE_READONLY, &ancienne)) {
/* traiter GetLastError() */
}
// Lire p[0] est permis si la protection a réussi.
// Écrire p[0] = 10 n'est plus autorisé.
12.5 Expérience : comparer adresse virtuelle et taille de page
Code: Select all
// Windows :
SYSTEM_INFO si;
GetSystemInfo(&si);
printf("taille page=%lu allocation granularity=%lu\n",
si.dwPageSize, si.dwAllocationGranularity);
12.6 Expérience : créer un thread sous Windows
Code: Select all
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>
DWORD WINAPI travail(LPVOID param) {
int v = *(int*)param;
printf("travail : %d\n", v);
return 0;
}
int main(void) {
int n = 42;
HANDLE th = CreateThread(NULL, 0, travail, &n, 0, NULL);
if (!th) return 1;
DWORD attente = WaitForSingleObject(th, INFINITE);
CloseHandle(th);
return attente == WAIT_OBJECT_0 ? 0 : 2;
}
12.7 Expérience : une data race en deux lignes
Code: Select all
// DANGER : si deux threads font simultanément :
compteur++;
// sur un int ordinaire partagé, sans synchronisation,
// le programme présente une data race en C/C++.
// Ce n'est pas juste une erreur de valeur : comportement indéfini.
12.8 Expérience : appeler une API ne signifie pas « toujours passer dans le noyau »
Code: Select all
// Exemple :
// strlen("ABC") peut être entièrement réalisé en code utilisateur.
// WriteFile sur un vrai fichier sollicite normalement des mécanismes
// d'E/S système, mais des couches et buffers interviennent.
// malloc peut servir une allocation depuis un pool utilisateur ;
// il ne déclenche pas forcément une allocation noyau à chaque appel.
12.9 Expérience : Windows NT et Linux, mêmes besoins, interfaces différentes
Code: Select all
// Objectif : ouvrir un fichier et écrire 2 octets
// Linux : open(...), write(...), close(...)
// Windows : CreateFileW(...), WriteFile(...), CloseHandle(...)
// Objectif : réserver des pages mémoire
// Linux : mmap(...), munmap(...)
// Windows : VirtualAlloc(...), VirtualFree(...)
12.10 Étude de cas complète : qu'arrive-t-il à 0x12345678 dans un fichier ?
Situation A : uint32_t x = 0x12345678 ; on écrit ses 4 octets natifs en mémoire x86-64 via WriteFile(f, &x, 4, ...). Octets du fichier : 78 56 34 12. Pourquoi ? x86-64 range les octets d'un entier multi-octet avec l'octet de poids faible à l'adresse basse.
Situation B : BYTE buf[] = {0x12,0x34,0x56,0x78} ; on écrit buf. Octets du fichier : 12 34 56 78. Pourquoi ? l'ordre du tableau a été défini explicitement.
Situation C : on exécute printf("%X", x) en redirigeant stdout. Octets du fichier sur une machine ASCII/UTF-8 : 31 32 33 34 35 36 37 38. Pourquoi ? printf convertit le nombre en caractères hexadécimaux (« 12345678 »), puis transmet leurs codes textuels. Aucun de ces huit octets n'est la représentation binaire brute de x.
Conclusion pratique : pour un fichier ou protocole binaire interopérable, définis explicitement l'endianness, la taille des champs et l'encodage des chaînes. Ni le nom du fichier, ni le type du pointeur passé à WriteFile ne fournissent automatiquement ce format.
12.11 Série d'exercices pièges, corrigés dans l'explication
1. CreateFileW(..., OPEN_EXISTING, ...) crée-t-il un fichier absent ? Non : il échoue. 2. CREATE_ALWAYS préserve-t-il toujours le contenu ? Non : il tronque le fichier existant si l'ouverture réussit. 3. Un WriteFile réussi écrit-il du texte Unicode ? Non : il écrit les octets fournis. 4. Un ReadFile réussi a-t-il obligatoirement rempli le buffer ? Non : vérifier recu. 5. VirtualAlloc renvoie-t-il une adresse physique ? Non : une adresse virtuelle. 6. Deux threads peuvent-ils utiliser la même adresse virtuelle dans le même processus ? Oui, l'espace d'adressage est partagé. 7. Deux processus avec la même adresse virtuelle accèdent-ils au même objet ? Pas sans configuration appropriée de partage. 8. fsync suffit-il à garantir une transaction applicative multi-fichiers ? Non : il faut concevoir le protocole et l'atomicité. 9. Un cache miss L1 implique-t-il forcément la RAM ? Non : L2/L3 peuvent répondre. 10. send TCP conserve-t-il la séparation de deux appels en deux messages reçus ? Non : TCP est un flux d'octets.
ANNEXE — RÉFLEXES DU DÉVELOPPEUR SYSTÈME
Avant une API : décrire la ressource visée (fichier, mémoire, thread), les droits nécessaires, l'unité des tailles (octets, caractères, pages), la durée de vie des buffers, les codes d'erreur et les obligations de fermeture.
Avant une écriture binaire : faire un exemple avec une valeur hexadécimale non symétrique, comme 0x12345678, et prédire tous les octets.
Avant un benchmark : identifier le travail réellement exécuté, la taille du jeu de données, les caches possibles, les optimisations, la synchronisation et les biais de mesure.
Avant d'accuser le noyau : vérifier si l'opération est réalisée par une bibliothèque en mémoire utilisateur, si stdout est redirigé et si des buffers regroupent les écritures.
Références API pour vérifier les contrats exacts :
Microsoft CreateFileW : https://learn.microsoft.com/windows/win ... reatefilew
Microsoft WriteFile : https://learn.microsoft.com/windows/win ... -writefile
Microsoft ReadFile : https://learn.microsoft.com/windows/win ... i-readfile
Microsoft VirtualAlloc : https://learn.microsoft.com/windows/win ... rtualalloc
Linux POSIX : man 2 open ; man 2 write ; man 2 fsync ; man 2 mmap.
