Objectif du chapitre
Ce chapitre ne parle pas simplement de “lire et écrire dans un fichier”.
Il explique surtout comment Linux pense réellement les fichiers au niveau système :
- un fichier ouvert n’est pas seulement un chemin ;
- un file descriptor n’est pas le fichier lui-même ;
- plusieurs descriptors peuvent partager le même état kernel ;
- certaines opérations sont atomiques ;
- certaines opérations peuvent provoquer des race conditions ;
- les offsets peuvent être partagés ou indépendants ;
- les I/O peuvent être bloquantes, non bloquantes, vectorisées ou positionnelles ;
- les fichiers temporaires doivent être créés avec des fonctions sûres.
- programmation système Linux ;
- shells ;
- serveurs ;
- pipes ;
- redirections ;
- sockets ;
- bases de données ;
- sandboxing ;
- sécurité système ;
- programmation concurrente.
1. Rappel : open(), read(), write(), close()
Avant d’entrer dans les détails avancés, il faut rappeler la base.
Sous Linux, l’ouverture d’un fichier se fait généralement avec :
Code: Select all
int fd = open("fichier.txt", O_RDWR);
Code: Select all
3
4
5
...
Ce n’est PAS le fichier lui-même.
C’est seulement un index dans une table de descriptors appartenant au processus.
Exemple simple :
Code: Select all
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(void)
{
int fd = open("test.txt", O_CREAT | O_WRONLY, 0644);
if (fd == -1) {
perror("open");
return 1;
}
write(fd, "Hello\n", 6);
close(fd);
return 0;
}
- open() demande au kernel d’ouvrir ou créer le fichier ;
- write() écrit des octets ;
- close() libère le descriptor dans le processus.
2. Atomicité
Une opération atomique est une opération qui est vue comme indivisible.
Cela veut dire :
- elle s’exécute entièrement ;
- aucun autre thread/processus ne voit un état intermédiaire ;
- elle réussit complètement ou échoue complètement.
Pourquoi ?
Parce que sous Linux, plusieurs processus peuvent accéder en même temps aux mêmes ressources :
- un même fichier ;
- un même répertoire ;
- une même socket ;
- un même pipe ;
- une même zone mémoire ;
- un même device.
2.1 Exemple de bug sans atomicité
Imaginons ce code :
Code: Select all
if (access("lockfile", F_OK) == -1) {
fd = open("lockfile", O_CREAT | O_WRONLY, 0644);
}
- si le fichier n’existe pas ;
- alors je le crée.
Pourquoi ?
Parce qu’entre :
Code: Select all
access("lockfile", F_OK)
Code: Select all
open("lockfile", O_CREAT | O_WRONLY, 0644)
Chronologie possible :
Code: Select all
Processus A : vérifie si lockfile existe
Processus A : lockfile n’existe pas
Le scheduler interrompt A
Processus B : crée lockfile
Processus A reprend
Processus A : crée ou ouvre lockfile en croyant être seul
3. Race Condition
Une race condition apparaît quand le résultat dépend de l’ordre exact d’exécution entre plusieurs threads/processus.
C’est un problème classique en système.
Forme typique :
Code: Select all
check
puis
use
Code: Select all
if (file_exists(path)) {
open(path);
}
Ce type de bug est souvent appelé :
Code: Select all
TOCTOU
En français :
- moment où on vérifie ;
- moment où on utilise.
4. O_EXCL : création exclusive atomique
La bonne solution pour créer un fichier uniquement s’il n’existe pas est :
Code: Select all
int fd = open("lockfile", O_CREAT | O_EXCL | O_WRONLY, 0644);
- O_CREAT demande la création si le fichier n’existe pas ;
- O_EXCL demande l’échec si le fichier existe déjà.
la vérification et la création sont atomiques côté kernel.
Donc il n’y a pas de fenêtre entre le check et l’usage.
Si le fichier existe déjà :
Code: Select all
open() échoue avec EEXIST
Code: Select all
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
int main(void)
{
int fd = open("lockfile",
O_CREAT | O_EXCL | O_WRONLY,
0644);
if (fd == -1) {
if (errno == EEXIST) {
printf("Le fichier existe déjà\n");
} else {
perror("open");
}
return 1;
}
write(fd, "lock\n", 5);
close(fd);
return 0;
}
Quand on veut créer un fichier de manière sûre, on évite le couple access() + open(). On utilise open() avec O_CREAT | O_EXCL.
5. O_APPEND : écriture atomique en fin de fichier
O_APPEND est un flag très important.
Exemple :
Code: Select all
int fd = open("log.txt", O_WRONLY | O_APPEND);
Même si le file offset courant est ailleurs.
Exemple :
Code: Select all
lseek(fd, 0, SEEK_SET);
write(fd, "ABC", 3);
Le plus important :
le déplacement à la fin du fichier et l’écriture sont faits atomiquement par le kernel.
Sans O_APPEND, un programme pourrait faire :
Code: Select all
lseek(fd, 0, SEEK_END);
write(fd, buffer, size);
Chronologie dangereuse :
Code: Select all
Processus A : lseek fin fichier
Processus B : lseek fin fichier
Processus A : write
Processus B : write
Avec O_APPEND :
Code: Select all
Processus A : write atomique en fin de fichier
Processus B : write atomique en fin de fichier
5.1 Exemple mental avec logs
Un serveur web peut avoir plusieurs processus qui écrivent dans le même fichier de log.
Si chaque processus écrit avec O_APPEND :
Code: Select all
open("server.log", O_WRONLY | O_APPEND);
C’est indispensable pour éviter que deux processus écrivent au même offset.
6. fcntl() : fonction de contrôle des file descriptors
fcntl() est une fonction très générale.
Prototype :
Code: Select all
#include <fcntl.h>
int fcntl(int fd, int cmd, ...);
Selon la commande cmd, elle peut :
- dupliquer un descriptor ;
- lire les flags ;
- modifier certains flags ;
- poser des verrous ;
- configurer le comportement non bloquant ;
- manipuler des propriétés avancées.
6.1 Lire les flags : F_GETFL
Exemple :
Code: Select all
int flags = fcntl(fd, F_GETFL);
if (flags == -1) {
perror("fcntl F_GETFL");
}
On peut tester certains flags :
Code: Select all
if (flags & O_APPEND) {
printf("O_APPEND est activé\n");
}
6.2 Modifier les flags : F_SETFL
Pour ajouter O_NONBLOCK :
Code: Select all
int flags = fcntl(fd, F_GETFL);
if (flags == -1) {
perror("fcntl F_GETFL");
return 1;
}
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
perror("fcntl F_SETFL");
return 1;
}
- on lit d’abord les flags actuels ;
- on ajoute le flag voulu ;
- on remet le tout.
Code: Select all
fcntl(fd, F_SETFL, O_NONBLOCK);
7. File descriptor, open file description et inode
C’est probablement la partie la plus importante du chapitre.
Sous Linux, il faut distinguer trois niveaux :
Code: Select all
file descriptor
↓
open file description
↓
inode
7.1 File descriptor
Le file descriptor est un entier local au processus.
Exemple :
Code: Select all
0 = stdin
1 = stdout
2 = stderr
3 = premier fichier ouvert
4 = deuxième fichier ouvert
Chaque processus a sa propre table.
Donc le fd 3 dans un processus A n’est pas forcément le même fichier que le fd 3 dans un processus B.
7.2 Open file description
L’open file description est une structure kernel créée lorsqu’un fichier est ouvert.
Elle contient notamment :
- le file offset courant ;
- les file status flags ;
- le mode d’ouverture ;
- une référence vers le fichier réel.
Exemple :
Code: Select all
read(fd, buffer, 10);
7.3 Inode
L’inode représente le fichier au niveau filesystem.
Il contient des métadonnées :
- taille du fichier ;
- permissions ;
- owner ;
- timestamps ;
- nombre de liens ;
- emplacement des blocs disque ;
- type de fichier.
Le nom est dans le répertoire.
Un même inode peut avoir plusieurs noms si on utilise des hard links.
7.4 Schéma simplifié
Code: Select all
Processus A
fd table :
0 -> stdin
1 -> stdout
2 -> stderr
3 ----------------------+
|
v
open file description
offset = 128
flags = O_RDWR
|
v
inode
fichier.txt
7.5 Pourquoi c’est important ?
Parce que plusieurs file descriptors peuvent pointer vers la même open file description.
Donc ils peuvent partager :
- le même offset ;
- les mêmes file status flags ;
- le même état d’ouverture.
- dup() ;
- dup2() ;
- fork() ;
- certains mécanismes de transmission de fd.
8. dup(), dup2() et F_DUPFD
dup() duplique un file descriptor.
Prototype :
Code: Select all
#include <unistd.h>
int dup(int oldfd);
Code: Select all
int fd2 = dup(fd1);
Mais ce nouveau fd pointe vers la même open file description.
Code: Select all
fd1 ----+
|
v
open file description
^
|
fd2 ----+
fd1 et fd2 partagent le même offset.
8.1 Exemple avec offset partagé
Code: Select all
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(void)
{
int fd1 = open("test.txt", O_CREAT | O_TRUNC | O_RDWR, 0644);
int fd2 = dup(fd1);
write(fd1, "ABC", 3);
write(fd2, "DEF", 3);
close(fd1);
close(fd2);
return 0;
}
Code: Select all
ABCDEF
Parce que :
- fd1 écrit ABC ;
- l’offset partagé avance à 3 ;
- fd2 écrit DEF à partir de l’offset 3.
8.2 dup2()
Prototype :
Code: Select all
int dup2(int oldfd, int newfd);
Exemple :
Code: Select all
dup2(fd, STDOUT_FILENO);
Tous les printf() iront vers fd.
Exemple complet :
Code: Select all
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(void)
{
int fd = open("sortie.txt", O_CREAT | O_TRUNC | O_WRONLY, 0644);
if (fd == -1) {
perror("open");
return 1;
}
if (dup2(fd, STDOUT_FILENO) == -1) {
perror("dup2");
return 1;
}
close(fd);
printf("Cette ligne part dans sortie.txt\n");
return 0;
}
Quand on écrit :
Code: Select all
programme > sortie.txt
- open("sortie.txt") ;
- dup2(fd, STDOUT_FILENO) ;
- execve(programme).
8.3 Redirection stderr vers stdout
En shell :
Code: Select all
commande > out.txt 2>&1
- stdout est redirigé vers out.txt ;
- stderr devient une copie de stdout.
Code: Select all
dup2(fd, STDOUT_FILENO);
dup2(STDOUT_FILENO, STDERR_FILENO);
8.4 fcntl() avec F_DUPFD
fcntl() peut aussi dupliquer un fd :
Code: Select all
int newfd = fcntl(oldfd, F_DUPFD, 10);
Différence mentale :
- dup() choisit le plus petit fd disponible ;
- dup2() force exactement un numéro ;
- F_DUPFD choisit le plus petit numéro disponible à partir d’une valeur minimale.
9. fork() et partage des descriptors
Lorsqu’un processus fait fork(), le fils hérite des file descriptors du parent.
Exemple :
Code: Select all
int fd = open("test.txt", O_RDWR);
pid_t pid = fork();
- le parent a fd ;
- le fils a fd ;
- les deux descriptors pointent vers la même open file description.
l’offset est partagé entre parent et fils.
Schéma :
Code: Select all
Parent fd 3 ----+
|
v
open file description
^
|
Child fd 3 -----+
- les redirections ;
- les pipes ;
- les shells ;
- les serveurs multi-process ;
- les interactions parent/enfant.
10. Transmission de file descriptors entre processus
Un file descriptor est local à un processus.
Le nombre 4 n’a pas de sens universel.
Dans le processus A :
Code: Select all
fd 4 -> fichier secret.txt
Code: Select all
fd 4 -> socket réseau
10.1 Pipe classique
Une pipe classique transmet des octets.
Exemple :
Code: Select all
pipe(pipefd);
- pipefd[0] pour lire ;
- pipefd[1] pour écrire.
Elle transmet des bytes.
Donc on peut envoyer le texte :
Code: Select all
"4"
10.2 Vrai fd passing : UNIX domain socket + SCM_RIGHTS
Pour transmettre réellement un file descriptor à un autre processus, Linux utilise généralement :
Code: Select all
UNIX domain socket + sendmsg()/recvmsg() + SCM_RIGHTS
Ce nouveau fd pointe vers la même open file description.
Schéma :
Code: Select all
Processus A
fd 5 ----+
|
v
open file description
^
|
fd 8 ----+
Processus B
Il reçoit une nouvelle entrée valide dans sa propre table de descriptors.
Ce mécanisme est utilisé dans :
- systemd ;
- Docker ;
- Chromium ;
- Wayland ;
- PulseAudio ;
- QEMU ;
- sandboxing ;
- privilege separation.
11. pread() et pwrite()
read() et write() utilisent le file offset courant.
Exemple :
Code: Select all
read(fd, buf, 100);
Mais parfois on veut lire à une position précise sans toucher à l’offset partagé.
C’est le rôle de pread().
Prototype :
Code: Select all
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
Code: Select all
pread(fd, buffer, 100, 4096);
Mais le file offset global n’est pas modifié.
Même idée pour pwrite() :
Code: Select all
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
11.1 Pourquoi c’est important ?
pread()/pwrite() sont très utiles quand plusieurs threads utilisent le même fichier.
Avec read()/write(), ils partagent l’offset.
Cela peut provoquer des conflits.
Avec pread()/pwrite(), chaque opération indique explicitement son offset.
Donc pas besoin de faire :
Code: Select all
lseek(fd, offset, SEEK_SET);
read(fd, buf, size);
pread() résout ça.
11.2 Équivalent mental Windows
Pour un développeur Win32, pread()/pwrite() ressemblent beaucoup à l’idée de :
Code: Select all
OVERLAPPED
- on indique un offset dans la structure OVERLAPPED ;
- l’opération peut se faire sans dépendre du pointeur de fichier global.
- pread() et pwrite() font directement ça avec un paramètre offset.
12. read() et write() peuvent être partiels
C’est un point très important en programmation système.
Un appel à write() peut écrire moins d’octets que demandé.
Exemple :
Code: Select all
ssize_t n = write(fd, buffer, 4096);
Code: Select all
4096
Code: Select all
1024
Code: Select all
-1
Il faut toujours vérifier la valeur de retour.
12.1 Fonction writen()
Pour écrire exactement N octets, on écrit souvent une fonction qui boucle.
Code: Select all
#include <unistd.h>
#include <errno.h>
ssize_t writen(int fd, const void *buf, size_t count)
{
const char *p = buf;
size_t left = count;
while (left > 0) {
ssize_t n = write(fd, p, left);
if (n == -1) {
if (errno == EINTR)
continue;
return -1;
}
if (n == 0)
break;
p += n;
left -= n;
}
return count - left;
}
Parce qu’un signal peut interrompre un appel système.
12.2 Fonction readn()
Même logique pour lire exactement N octets :
Code: Select all
#include <unistd.h>
#include <errno.h>
ssize_t readn(int fd, void *buf, size_t count)
{
char *p = buf;
size_t left = count;
while (left > 0) {
ssize_t n = read(fd, p, left);
if (n == -1) {
if (errno == EINTR)
continue;
return -1;
}
if (n == 0)
break;
p += n;
left -= n;
}
return count - left;
}
- sockets ;
- pipes ;
- protocoles binaires ;
- fichiers spéciaux ;
- I/O robustes.
13. Scatter/Gather I/O : readv() et writev()
Le scatter/gather I/O permet de lire ou écrire plusieurs buffers avec une seule syscall.
Fonctions :
Code: Select all
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
Code: Select all
struct iovec {
void *iov_base;
size_t iov_len;
};
13.1 writev()
Exemple :
Code: Select all
#include <sys/uio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
int fd = open("message.txt", O_CREAT | O_TRUNC | O_WRONLY, 0644);
struct iovec iov[3];
char header[] = "HEADER\n";
char body[] = "BODY\n";
char footer[] = "FOOTER\n";
iov[0].iov_base = header;
iov[0].iov_len = strlen(header);
iov[1].iov_base = body;
iov[1].iov_len = strlen(body);
iov[2].iov_base = footer;
iov[2].iov_len = strlen(footer);
writev(fd, iov, 3);
close(fd);
return 0;
}
Code: Select all
HEADER
BODY
FOOTER
- une seule syscall ;
- pas besoin de concaténer les buffers ;
- meilleure performance ;
- moins de copies mémoire.
13.2 Où c’est utilisé ?
Très utilisé dans :
- serveurs HTTP ;
- réseau ;
- protocoles ;
- bases de données ;
- kernels ;
- drivers ;
- systèmes haute performance.
Code: Select all
header HTTP
+
body
+
footer
14. truncate() et ftruncate()
truncate() modifie la taille d’un fichier à partir de son chemin.
Prototype :
Code: Select all
int truncate(const char *path, off_t length);
Code: Select all
int ftruncate(int fd, off_t length);
Code: Select all
ftruncate(fd, 0);
Si la nouvelle taille est plus petite :
- les données après cette taille sont supprimées.
- le fichier est agrandi ;
- les nouvelles zones lisibles valent zéro.
14.1 Exemple
Code: Select all
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(void)
{
int fd = open("data.bin", O_CREAT | O_RDWR, 0644);
write(fd, "ABCDEFGH", 8);
ftruncate(fd, 4);
close(fd);
return 0;
}
Code: Select all
ABCD
15. Nonblocking I/O
Par défaut, beaucoup d’opérations I/O sont bloquantes.
Exemple avec un pipe :
Code: Select all
read(fd, buffer, size);
Avec O_NONBLOCK :
Code: Select all
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
Si aucune donnée n’est disponible :
Code: Select all
read() retourne -1
errno = EAGAIN ou EWOULDBLOCK
15.1 Exemple
Code: Select all
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
ssize_t n = read(fd, buffer, sizeof(buffer));
if (n == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
printf("Aucune donnée disponible pour le moment\n");
} else {
perror("read");
}
}
15.2 Utilité
Le nonblocking est essentiel pour :
- sockets ;
- serveurs réseau ;
- event loops ;
- select() ;
- poll() ;
- epoll() ;
- applications haute performance.
Code: Select all
O_NONBLOCK + epoll
- ne jamais bloquer sur une seule connexion ;
- surveiller beaucoup de descriptors ;
- réagir quand un fd devient prêt.
15.3 Équivalent mental Windows
Sous Windows, on pense souvent à :
- OVERLAPPED I/O ;
- IOCP ;
- WSARecv()/WSASend() asynchrones.
- Linux avec epoll est souvent un modèle de readiness : “ce fd est prêt”.
- Windows IOCP est plutôt un modèle de completion : “cette opération est terminée”.
ne pas bloquer un thread pour chaque opération I/O.
16. Large File Support
Historiquement, sur les systèmes 32 bits, off_t pouvait être limité.
Cela posait problème pour les fichiers supérieurs à environ 2 Go.
Linux a donc introduit le Large File Support.
La manière classique d’activer les offsets 64 bits :
Code: Select all
#define _FILE_OFFSET_BITS 64
Code: Select all
#define _FILE_OFFSET_BITS 64
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
Donc les fonctions comme :
- open()
- lseek()
- truncate()
- stat()
17. /dev/fd
Linux expose les file descriptors via :
Code: Select all
/dev/fd/
Code: Select all
/dev/fd/0
/dev/fd/1
/dev/fd/2
- stdin ;
- stdout ;
- stderr.
Code: Select all
/proc/self/fd
Code: Select all
ls -l /proc/self/fd
Code: Select all
0 -> /dev/pts/0
1 -> /dev/pts/0
2 -> /dev/pts/0
3 -> /home/user/file.txt
18. Fichiers temporaires
Beaucoup de programmes ont besoin de fichiers temporaires.
Exemples :
- compilateurs ;
- éditeurs ;
- outils de compression ;
- navigateurs ;
- serveurs ;
- scripts ;
- programmes de traitement de données.
Mauvaise idée :
Code: Select all
char *name = tmpnam(NULL);
int fd = open(name, O_CREAT | O_WRONLY, 0644);
Parce qu’entre la génération du nom et la création du fichier, un autre processus peut créer ce fichier.
C’est encore une faille TOCTOU.
18.1 mkstemp()
La fonction recommandée est :
Code: Select all
#include <stdlib.h>
int mkstemp(char *template);
Code: Select all
XXXXXX
Code: Select all
char template[] = "/tmp/myprogXXXXXX";
int fd = mkstemp(template);
- remplace les X par une chaîne unique ;
- crée le fichier ;
- l’ouvre ;
- retourne un fd ;
- fait tout ça de manière sûre.
Code: Select all
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int main(void)
{
char template[] = "/tmp/demoXXXXXX";
int fd = mkstemp(template);
if (fd == -1) {
perror("mkstemp");
return 1;
}
printf("Fichier temporaire : %s\n", template);
write(fd, "temporary data\n", 15);
close(fd);
return 0;
}
template doit être modifiable.
Donc ceci est incorrect :
Code: Select all
char *template = "/tmp/demoXXXXXX";
Il faut faire :
Code: Select all
char template[] = "/tmp/demoXXXXXX";
18.2 Supprimer le nom après création
Technique Unix classique :
Code: Select all
int fd = mkstemp(template);
unlink(template);
Mais tant que fd est ouvert, les données restent accessibles.
Quand fd est fermé, le kernel libère le fichier.
Exemple :
Code: Select all
char template[] = "/tmp/secretXXXXXX";
int fd = mkstemp(template);
unlink(template);
/*
Le fichier existe encore tant que fd est ouvert,
mais il n'est plus accessible par son nom.
*/
18.3 tmpfile()
tmpfile() utilise l’interface stdio :
Code: Select all
#include <stdio.h>
FILE *tmpfile(void);
Code: Select all
FILE *f = tmpfile();
fprintf(f, "hello\n");
Avantage :
- simple ;
- pas besoin de choisir un nom ;
- nettoyage automatique.
- on travaille avec FILE* ;
- pas directement avec un fd brut sauf conversion avec fileno().
19. Liens avec Windows / Win32
Pour quelqu’un qui connaît Win32, les concepts Linux peuvent se comprendre avec des équivalences mentales.
Attention : ce ne sont pas toujours des équivalences exactes, mais ça aide.
19.1 fd et HANDLE
Linux :
Code: Select all
file descriptor
Code: Select all
HANDLE
Mais Windows a un modèle plus général avec l’Object Manager NT.
19.2 dup() et DuplicateHandle()
Linux :
Code: Select all
dup(fd);
dup2(oldfd, newfd);
Code: Select all
DuplicateHandle(...);
19.3 pread()/pwrite() et OVERLAPPED
Linux :
Code: Select all
pread(fd, buffer, size, offset);
Code: Select all
ReadFile(handle, buffer, size, &bytesRead, &overlapped);
- faire une I/O à un offset précis ;
- ne pas dépendre seulement du pointeur global.
19.4 O_APPEND et FILE_APPEND_DATA
Linux :
Code: Select all
O_APPEND
Code: Select all
FILE_APPEND_DATA
- écriture forcée en fin de fichier ;
- utile pour logs ;
- évite certains conflits de concurrence.
19.5 O_NONBLOCK + epoll et IOCP
Linux :
Code: Select all
O_NONBLOCK + epoll
Code: Select all
OVERLAPPED I/O + IOCP
- gérer beaucoup d’I/O sans bloquer un thread par opération.
20. Exercices importants du chapitre
Exercice : O_APPEND
Créer un fichier avec O_APPEND, faire un lseek() au début, puis écrire.
Question :
Code: Select all
Où les données sont-elles écrites ?
À la fin du fichier.
Parce que O_APPEND force le kernel à écrire en fin de fichier.
Exercice : dup() et offset partagé
Prédire le contenu final :
Code: Select all
int fd1 = open("test.txt", O_CREAT | O_TRUNC | O_RDWR, 0644);
int fd2 = dup(fd1);
write(fd1, "ABC", 3);
write(fd2, "DEF", 3);
lseek(fd1, 0, SEEK_SET);
write(fd2, "X", 1);
- fd1 et fd2 partagent le même offset ;
- ABC écrit offset 0-2 ;
- DEF écrit offset 3-5 ;
- lseek(fd1, 0) remet l’offset partagé à 0 ;
- write(fd2, "X", 1) écrit au début.
Code: Select all
XBCDEF
Exercice : readn()/writen()
Implémenter des fonctions robustes qui bouclent jusqu’à lire ou écrire le nombre d’octets demandé.
C’est indispensable en réseau.
21. Résumé ultra important
À retenir absolument :
- Un file descriptor est un entier local à un processus.
- Le fd n’est pas le fichier.
- Le vrai état d’ouverture est dans l’open file description.
- L’open file description contient notamment l’offset et les flags.
- L’inode représente le fichier côté filesystem.
- dup() crée un nouveau fd vers la même open file description.
- dup2() permet de forcer un numéro de fd.
- fork() fait hériter les fd au processus fils.
- Après dup() ou fork(), l’offset peut être partagé.
- O_APPEND rend l’écriture en fin de fichier atomique.
- O_EXCL permet une création exclusive atomique.
- pread()/pwrite() lisent/écrivent à un offset précis sans modifier l’offset global.
- read()/write() peuvent être partiels.
- Il faut toujours vérifier les valeurs de retour.
- readv()/writev() permettent plusieurs buffers en une syscall.
- O_NONBLOCK permet de ne pas bloquer.
- mkstemp() est la bonne méthode pour créer un fichier temporaire sécurisé.
- Une pipe transmet des bytes, pas directement des fd.
- Le vrai fd passing se fait avec UNIX domain sockets et SCM_RIGHTS.
22. Les 5 idées les plus importantes
1. fd != fichier
Le fd est seulement un index dans une table du processus.
Code: Select all
fd -> open file description -> inode
2. dup() partage l’offset
Après :
Code: Select all
fd2 = dup(fd1);
3. O_APPEND est atomique
Avec O_APPEND, le kernel garantit :
Code: Select all
écriture à la fin du fichier
4. pread()/pwrite() évitent les conflits d’offset
Très utile en multithreading.
5. Les fichiers temporaires doivent être créés proprement
Utiliser :
Code: Select all
mkstemp()
Code: Select all
tmpnam()
mktemp()
Conclusion
Ce chapitre est un chapitre central en programmation système Linux.
Il explique comment le kernel gère réellement les fichiers ouverts.
Ce n’est pas seulement une histoire de fichiers texte.
C’est la base de :
- la redirection shell ;
- les pipes ;
- les processus ;
- les serveurs ;
- les sockets ;
- la concurrence ;
- les logs ;
- les fichiers temporaires sécurisés ;
- le fd passing ;
- la compréhension du modèle UNIX.
C’est exactement le genre de bases qu’il faut maîtriser pour aller ensuite vers :
- processus ;
- threads ;
- signals ;
- pipes ;
- sockets ;
- epoll ;
- IPC ;
- sandboxing ;
- sécurité système ;
- kernel internals.
