Introduction
En programmation système Linux, deux idées reviennent partout :
- le programme utilisateur ne parle pas directement au matériel ;
- il demande des services au noyau à travers des appels système ;
- les ressources d’entrée/sortie sont manipulées via des descripteurs de fichiers ;
- un même modèle permet de manipuler fichiers, terminaux, pipes, sockets et périphériques.
On va voir :
- ce qu’est un appel système ;
- le rôle de la libc/glibc ;
- la différence entre fonction de bibliothèque et syscall ;
- la gestion des erreurs avec errno ;
- les macros de portabilité POSIX/GNU ;
- les types système Linux ;
- le modèle universel d’I/O ;
- les file descriptors ;
- open(), read(), write(), close() ;
- lseek() et le file offset ;
- les fichiers creux ;
- ioctl() et les opérations spécifiques aux périphériques.
1. Programme utilisateur, libc et noyau
1.1. Séparation user mode / kernel mode
Sous Linux, un programme classique tourne en espace utilisateur.
Cela signifie qu’il ne peut pas directement :
- accéder au disque ;
- manipuler librement la mémoire physique ;
- créer des processus au niveau noyau ;
- piloter les périphériques ;
- modifier les structures internes du système.
Cette demande passe par un appel système.
Schéma simplifié :
Code: Select all
Programme utilisateur
|
v
Fonction libc / wrapper
|
v
Instruction de transition vers le noyau
|
v
Noyau Linux
|
v
Routine système réelle
1.2. Qu’est-ce qu’un appel système ?
Un appel système, ou system call, est une entrée contrôlée dans le noyau.
Exemples d’appels système Linux :
Code: Select all
open()
read()
write()
close()
fork()
execve()
mmap()
ioctl()
lseek()
- d’ouvrir un fichier ;
- de lire des données ;
- d’écrire des données ;
- de créer un processus ;
- de manipuler la mémoire virtuelle ;
- de communiquer avec un périphérique.
1.3. Ce qui se passe pendant un syscall
Lorsqu’un programme appelle un syscall, plusieurs choses se produisent :
- les arguments sont préparés ;
- le numéro de l’appel système est placé dans un registre ;
- le processeur passe de l’espace utilisateur à l’espace noyau ;
- le noyau vérifie les arguments ;
- le noyau exécute la routine demandée ;
- le résultat est renvoyé au programme ;
- le processeur revient en espace utilisateur.
Code: Select all
open("test.txt", O_RDONLY)
|
v
Wrapper libc open()
|
v
syscall
|
v
Table des appels système
|
v
Routine noyau chargée d’ouvrir le fichier
|
v
Retour vers le programme
Un appel système n’est pas une simple fonction C classique.
Il implique :
- un changement de privilège CPU ;
- une transition user mode vers kernel mode ;
- des validations de pointeurs ;
- des vérifications de permissions ;
- une sauvegarde/restauration de contexte ;
- un retour vers l’espace utilisateur.
Mauvais exemple :
Code: Select all
for (int i = 0; i < 1000000; i++)
{
write(fd, "A", 1);
}
Code: Select all
char buffer[4096];
/* remplir buffer */
write(fd, buffer, sizeof(buffer));
2. Fonctions de bibliothèque et appels système
2.1. Toutes les fonctions C ne sont pas des syscalls
Il ne faut pas confondre :
- les appels système ;
- les fonctions de bibliothèque.
Code: Select all
printf()
malloc()
strcpy()
strlen()
fopen()
fprintf()
perror()
strerror()
Exemple :
Code: Select all
printf("Hello\n");
2.2. La libc / glibc
Sous Linux, la bibliothèque C la plus courante est la glibc.
Elle fournit :
- les wrappers autour des appels système ;
- les fonctions standards C ;
- les fonctions POSIX ;
- des extensions GNU ;
- des outils de conversion, formatage, chaînes, mémoire, etc.
Code: Select all
Programme C
|
v
glibc
|
v
syscall si nécessaire
|
v
noyau Linux
La fonction fopen() appartient à la bibliothèque standard C.
Elle retourne un FILE* :
Code: Select all
FILE *f = fopen("test.txt", "r");
Code: Select all
int fd = open("test.txt", O_RDONLY);
- fopen() donne une interface C standard avec buffering de stdio ;
- open() donne accès au modèle UNIX bas niveau avec file descriptor.
Code: Select all
fopen()
|
v
stdio / buffering
|
v
open(), read(), write(), close()
|
v
noyau
3. Gestion des erreurs : errno
3.1. Les appels système peuvent échouer
Un programme système doit toujours vérifier les valeurs de retour.
Exemple :
Code: Select all
int fd = open("test.txt", O_RDONLY);
if (fd == -1)
{
perror("open");
return 1;
}
3.2. errno
Quand un appel système ou une fonction de bibliothèque échoue, une variable nommée errno indique souvent la raison de l’erreur.
Exemples d’erreurs possibles :
Code: Select all
EACCES Permission refusée
ENOENT Fichier introuvable
EEXIST Le fichier existe déjà
EBADF Mauvais file descriptor
EINVAL Argument invalide
ENOMEM Mémoire insuffisante
EINTR Appel interrompu par un signal
Code: Select all
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
int main(void)
{
int fd = open("inexistant.txt", O_RDONLY);
if (fd == -1)
{
perror("open");
return 1;
}
close(fd);
return 0;
}
3.3. strerror()
strerror() transforme un code errno en texte.
Code: Select all
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main(void)
{
printf("Erreur : %s\n", strerror(errno));
return 0;
}
Il ne faut pas lire errno au hasard.
Mauvais raisonnement :
Code: Select all
printf("errno = %d\n", errno);
La bonne logique est :
Code: Select all
if (fonction() == -1)
{
perror("fonction");
}
4. Macros de portabilité et standards UNIX
4.1. Linux, POSIX et GNU
Linux n’est pas seulement un noyau. Pour programmer dessus, on utilise aussi des bibliothèques et des standards.
Les standards importants :
- POSIX ;
- SUS / Single UNIX Specification ;
- X/Open ;
- extensions GNU ;
- extensions spécifiques Linux.
- portable POSIX ;
- spécifique Linux ;
- spécifique GNU/glibc ;
- spécifique à un UNIX particulier.
Certaines macros contrôlent quelles fonctions et définitions sont visibles dans les headers.
Exemple :
Code: Select all
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
Code: Select all
#define _POSIX_C_SOURCE 200809L
#include <unistd.h>
Elles peuvent rendre visibles :
- certaines fonctions POSIX ;
- certaines structures ;
- certaines constantes ;
- certaines extensions GNU.
_GNU_SOURCE active souvent beaucoup d’extensions GNU et Linux.
Exemple :
Code: Select all
#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>
4.4. _POSIX_C_SOURCE
_POSIX_C_SOURCE permet de demander une certaine version de POSIX.
Exemple :
Code: Select all
#define _POSIX_C_SOURCE 200809L
#include <unistd.h>
4.5. Analogie avec Windows
Sous Windows, on trouve une logique proche avec :
Code: Select all
#define _WIN32_WINNT 0x0A00
#include <Windows.h>
Sous Linux/UNIX, les feature test macros jouent un rôle similaire pour les standards POSIX/GNU.
5. Les types système
5.1. Pourquoi utiliser des types spécialisés ?
En programmation système, il ne faut pas remplacer tous les types par int ou long.
Linux définit des types spécialisés :
Code: Select all
pid_t
uid_t
gid_t
off_t
mode_t
size_t
ssize_t
time_t
socklen_t
dev_t
ino_t
nlink_t
5.2. Exemple avec pid_t
Mauvais style :
Code: Select all
int pid;
Code: Select all
pid_t pid;
Code: Select all
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(void)
{
pid_t pid = getpid();
printf("PID = %ld\n", (long) pid);
return 0;
}
5.3. size_t et ssize_t
size_t est un type non signé utilisé pour représenter une taille.
Exemple :
Code: Select all
size_t taille = 1024;
Exemple :
Code: Select all
ssize_t n = read(fd, buffer, sizeof(buffer));
if (n == -1)
{
perror("read");
}
5.4. off_t
off_t représente un offset dans un fichier.
Exemple :
Code: Select all
off_t pos = lseek(fd, 0, SEEK_CUR);
5.5. Attention à printf()
Certains types système n’ont pas toujours la même taille selon les systèmes.
Exemple prudent :
Code: Select all
pid_t pid = getpid();
printf("PID = %ld\n", (long) pid);
6. Le modèle universel d’I/O UNIX
6.1. Le principe : presque tout est fichier
UNIX utilise un modèle très simple :
Code: Select all
open()
read()
write()
close()
- fichiers réguliers ;
- terminaux ;
- pipes ;
- sockets ;
- FIFO ;
- périphériques ;
- pseudo-fichiers.
6.2. Exemple
Le même appel peut lire des données depuis plusieurs types de ressources :
Code: Select all
read(fd, buffer, sizeof(buffer));
- un fichier texte ;
- un socket réseau ;
- un pipe ;
- un terminal ;
- un périphérique dans /dev.
6.3. Le kernel dispatch selon le type réel
Derrière le file descriptor, le noyau sait quel objet est réellement ouvert.
Il peut donc rediriger l’opération vers :
- le système de fichiers ;
- la couche socket ;
- le pilote de terminal ;
- le pilote de périphérique ;
- une implémentation spéciale.
Code: Select all
read(fd, ...)
write(fd, ...)
close(fd)
7. Les file descriptors
7.1. Définition
Un file descriptor est un petit entier utilisé par le processus pour référencer une ressource ouverte.
Déclaration :
Code: Select all
int fd;
Code: Select all
int fd = open("test.txt", O_RDONLY);
7.2. Table des descripteurs
Chaque processus possède sa propre table de descripteurs.
Exemple conceptuel :
Code: Select all
Processus
fd 0 -> stdin
fd 1 -> stdout
fd 2 -> stderr
fd 3 -> fichier ouvert
fd 4 -> socket
fd 5 -> pipe
7.3. stdin, stdout, stderr
Par convention, au démarrage d’un programme :
Code: Select all
0 = stdin
1 = stdout
2 = stderr
Code: Select all
STDIN_FILENO
STDOUT_FILENO
STDERR_FILENO
Code: Select all
#include <unistd.h>
int main(void)
{
write(STDOUT_FILENO, "Hello\n", 6);
return 0;
}
Comme 0, 1 et 2 sont généralement déjà utilisés, le premier open() retourne souvent 3.
Exemple :
Code: Select all
int fd = open("test.txt", O_RDONLY);
/* fd vaut souvent 3 si aucun autre fd n'a été ouvert */
Si le programme a déjà ouvert autre chose, le fd peut être 4, 5, 6, etc.
7.5. Un socket est aussi un fd
Un socket n’a pas un numéro spécial.
Exemple :
Code: Select all
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
On peut donc faire :
Code: Select all
close(sockfd);
Code: Select all
read(sockfd, buffer, sizeof(buffer));
write(sockfd, buffer, taille);
7.6. Le nom de variable n’a aucune importance
fd est une convention.
Ceci est valide :
Code: Select all
int fichier = open("test.txt", O_RDONLY);
Code: Select all
int logFd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0644);
8. open()
8.1. Prototype
open() permet d’ouvrir ou de créer un fichier.
Code: Select all
#include <fcntl.h>
int open(const char *pathname, int flags, ...);
- un file descriptor si succès ;
- -1 si erreur.
Code: Select all
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(void)
{
int fd = open("test.txt", O_RDONLY);
if (fd == -1)
{
perror("open");
return 1;
}
close(fd);
return 0;
}
Les trois modes de base sont :
Code: Select all
O_RDONLY lecture seule
O_WRONLY écriture seule
O_RDWR lecture et écriture
Code: Select all
int fd1 = open("a.txt", O_RDONLY);
int fd2 = open("b.txt", O_WRONLY);
int fd3 = open("c.txt", O_RDWR);
O_CREAT crée le fichier s’il n’existe pas.
Avec O_CREAT, il faut généralement fournir un troisième argument : le mode.
Exemple :
Code: Select all
int fd = open("log.txt", O_WRONLY | O_CREAT, 0644);
Code: Select all
propriétaire : lecture + écriture
groupe : lecture
autres : lecture
O_TRUNC vide le fichier à l’ouverture s’il existe déjà.
Exemple :
Code: Select all
int fd = open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
8.5. O_APPEND
O_APPEND force les écritures à se placer à la fin du fichier.
Exemple :
Code: Select all
int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0644);
Avec O_APPEND, le noyau garantit que l’écriture se fait à la fin au moment de l’appel.
8.6. O_EXCL
O_EXCL est souvent utilisé avec O_CREAT pour échouer si le fichier existe déjà.
Exemple :
Code: Select all
int fd = open("unique.txt", O_WRONLY | O_CREAT | O_EXCL, 0644);
if (fd == -1)
{
perror("open");
}
8.7. O_NONBLOCK
O_NONBLOCK demande un mode non bloquant.
Exemple :
Code: Select all
int fd = open("fifo", O_RDONLY | O_NONBLOCK);
Attention : un mauvais programme peut alors tourner en boucle et consommer 100% CPU.
Mauvais exemple :
Code: Select all
while (1)
{
read(fd, buffer, sizeof(buffer));
}
8.8. O_CLOEXEC
O_CLOEXEC indique que le file descriptor doit être automatiquement fermé lors d’un exec().
Exemple :
Code: Select all
int fd = open("secret.txt", O_RDONLY | O_CLOEXEC);
C’est important pour :
- la sécurité ;
- les programmes multi-processus ;
- les serveurs ;
- les shells ;
- les démons.
O_SYNC demande des écritures synchrones.
Exemple :
Code: Select all
int fd = open("data.bin", O_WRONLY | O_CREAT | O_SYNC, 0644);
8.10. O_DIRECT
O_DIRECT demande de limiter l’usage du cache d’I/O du noyau.
Ce mode est plus complexe et impose souvent des contraintes d’alignement.
Il est utilisé dans certains logiciels bas niveau :
- bases de données ;
- outils de stockage ;
- benchmarks disque ;
- systèmes spécialisés.
9. read()
9.1. Prototype
read() lit des octets depuis un file descriptor.
Code: Select all
#include <unistd.h>
ssize_t read(int fd, void *buffer, size_t count);
Code: Select all
> 0 nombre d'octets lus
0 fin de fichier / EOF
-1 erreur
Code: Select all
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main(void)
{
char buffer[128];
int fd = open("test.txt", O_RDONLY);
if (fd == -1)
{
perror("open");
return 1;
}
ssize_t n = read(fd, buffer, sizeof(buffer));
if (n == -1)
{
perror("read");
close(fd);
return 1;
}
write(STDOUT_FILENO, buffer, n);
close(fd);
return 0;
}
read() peut retourner moins d’octets que demandé.
Exemple :
Code: Select all
ssize_t n = read(fd, buffer, 4096);
Code: Select all
4096
2000
512
1
0
-1
9.4. read() ne rajoute pas de '\0'
read() lit des octets bruts.
Elle ne transforme pas les données en chaîne C.
Mauvais exemple :
Code: Select all
char buffer[100];
read(fd, buffer, sizeof(buffer));
printf("%s\n", buffer);
Version correcte :
Code: Select all
char buffer[101];
ssize_t n = read(fd, buffer, 100);
if (n == -1)
{
perror("read");
}
else
{
buffer[n] = '\0';
printf("%s\n", buffer);
}
Pour lire tout un fichier :
Code: Select all
char buffer[4096];
ssize_t n;
while ((n = read(fd, buffer, sizeof(buffer))) > 0)
{
write(STDOUT_FILENO, buffer, n);
}
if (n == -1)
{
perror("read");
}
10. write()
10.1. Prototype
write() écrit des octets vers un file descriptor.
Code: Select all
#include <unistd.h>
ssize_t write(int fd, const void *buffer, size_t count);
Code: Select all
>= 0 nombre d'octets écrits
-1 erreur
Code: Select all
#include <unistd.h>
int main(void)
{
write(STDOUT_FILENO, "Hello Linux\n", 12);
return 0;
}
write() n’écrit pas toujours tout.
Exemple :
Code: Select all
ssize_t n = write(fd, buffer, 4096);
Cela arrive notamment avec :
- sockets ;
- pipes ;
- fichiers en mode non bloquant ;
- signaux ;
- conditions particulières d’I/O.
Exemple de fonction qui boucle jusqu’à avoir écrit tout le buffer :
Code: Select all
#include <unistd.h>
#include <errno.h>
int write_all(int fd, const void *buffer, size_t count)
{
const char *p = buffer;
size_t total = 0;
while (total < count)
{
ssize_t n = write(fd, p + total, count - total);
if (n == -1)
{
if (errno == EINTR)
continue;
return -1;
}
total += n;
}
return 0;
}
11. close()
11.1. Prototype
close() ferme un file descriptor.
Code: Select all
#include <unistd.h>
int close(int fd);
Code: Select all
0 succès
-1 erreur
Code: Select all
if (close(fd) == -1)
{
perror("close");
}
Fermer un fd permet :
- de libérer l’entrée dans la table du processus ;
- de libérer ou décrémenter les ressources noyau ;
- de terminer proprement l’usage d’un fichier ;
- d’éviter les fuites de descripteurs.
Mauvais code :
Code: Select all
close(fd);
close(fd);
Après le premier close(), le numéro fd redevient disponible.
Le noyau peut réutiliser ce même numéro pour une autre ressource.
Donc le deuxième close() pourrait fermer autre chose.
Exemple conceptuel :
Code: Select all
int fd = open("a.txt", O_RDONLY); /* fd = 3 */
close(fd); /* 3 est libre */
int sock = socket(...); /* sock peut devenir 3 */
close(fd); /* ferme le socket par erreur */
Code: Select all
close(fd);
fd = -1;
12. Exemple complet : copier un fichier
Voici un exemple simple qui copie un fichier avec open(), read(), write() et close().
Code: Select all
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#define BUFFER_SIZE 4096
int main(int argc, char *argv[])
{
int inputFd;
int outputFd;
char buffer[BUFFER_SIZE];
ssize_t nread;
if (argc != 3)
{
fprintf(stderr, "Usage: %s source destination\n", argv[0]);
return 1;
}
inputFd = open(argv[1], O_RDONLY);
if (inputFd == -1)
{
perror("open source");
return 1;
}
outputFd = open(argv[2],
O_WRONLY | O_CREAT | O_TRUNC,
0644);
if (outputFd == -1)
{
perror("open destination");
close(inputFd);
return 1;
}
while ((nread = read(inputFd, buffer, sizeof(buffer))) > 0)
{
ssize_t total = 0;
while (total < nread)
{
ssize_t nwritten = write(outputFd,
buffer + total,
nread - total);
if (nwritten == -1)
{
if (errno == EINTR)
continue;
perror("write");
close(inputFd);
close(outputFd);
return 1;
}
total += nwritten;
}
}
if (nread == -1)
{
perror("read");
}
if (close(inputFd) == -1)
{
perror("close source");
}
if (close(outputFd) == -1)
{
perror("close destination");
}
return nread == -1 ? 1 : 0;
}
Code: Select all
open source
open destination
boucle read/write
close source
close destination
13. File offset et lseek()
13.1. Le file offset
Chaque fichier ouvert possède une position courante appelée file offset.
C’est l’endroit où la prochaine lecture ou écriture aura lieu.
Exemple conceptuel :
Code: Select all
Fichier : A B C D E F G H
Offset : 0 1 2 3 4 5 6 7
^
position courante
13.2. Exemple
Code: Select all
read(fd, buffer, 10);
Un deuxième read() continuera à partir de la nouvelle position.
13.3. Prototype de lseek()
lseek() permet de déplacer l’offset.
Code: Select all
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
Code: Select all
nouvel offset si succès
-1 si erreur
Les trois modes principaux :
Code: Select all
SEEK_SET depuis le début du fichier
SEEK_CUR depuis la position courante
SEEK_END depuis la fin du fichier
Code: Select all
lseek(fd, 0, SEEK_SET); /* début du fichier */
lseek(fd, 0, SEEK_END); /* fin du fichier */
lseek(fd, 10, SEEK_CUR); /* avance de 10 octets */
lseek(fd, -5, SEEK_CUR); /* recule de 5 octets */
Code: Select all
off_t pos = lseek(fd, 0, SEEK_CUR);
if (pos == -1)
{
perror("lseek");
}
Code: Select all
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(void)
{
int fd = open("test.txt", O_RDONLY);
if (fd == -1)
{
perror("open");
return 1;
}
if (lseek(fd, 5, SEEK_SET) == -1)
{
perror("lseek");
close(fd);
return 1;
}
char c;
if (read(fd, &c, 1) == 1)
{
write(STDOUT_FILENO, &c, 1);
}
close(fd);
return 0;
}
13.7. lseek() ne marche pas partout
lseek() fonctionne sur les fichiers réguliers.
Mais il ne fonctionne généralement pas sur :
- sockets ;
- pipes ;
- FIFO ;
- certains périphériques ;
- certains flux spéciaux.
Code: Select all
lseek(sockfd, 0, SEEK_SET);
14. Fichiers creux : file holes
14.1. Principe
On peut déplacer l’offset au-delà de la fin du fichier puis écrire.
Exemple :
Code: Select all
lseek(fd, 1000000, SEEK_SET);
write(fd, "X", 1);
Ces zones vides s’appellent des trous.
14.2. Lecture d’un trou
Si on lit une zone correspondant à un trou, le système retourne des octets à zéro.
Conceptuellement :
Code: Select all
Début du fichier
|
v
[ données réelles ][ trou ][ données réelles ]
14.3. Utilité
Les fichiers creux sont utiles pour :
- images disque ;
- fichiers de machines virtuelles ;
- certains dumps ;
- bases de données ;
- systèmes de stockage ;
- tests bas niveau.
Tous les systèmes de fichiers ne gèrent pas forcément les trous de la même manière.
De plus, certains outils de copie peuvent transformer un fichier creux en fichier plein s’ils ne préservent pas les trous.
15. ioctl()
15.1. Pourquoi ioctl() existe ?
Le modèle universel d’I/O repose sur :
Code: Select all
open()
read()
write()
close()
lseek()
Exemples :
- changer les paramètres d’un terminal ;
- demander une information spéciale à un périphérique ;
- envoyer une commande spécifique à un driver ;
- configurer une interface ;
- faire une opération qui n’est ni une lecture ni une écriture simple.
15.2. Prototype
Code: Select all
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
request indique l’opération demandée.
Le troisième argument dépend de la commande.
15.3. Exemple conceptuel
Code: Select all
ioctl(fd, REQUEST_CODE, &data);
Code: Select all
"Sur cette ressource ouverte, exécute cette commande spéciale avec ces données."
ioctl() est souvent utilisé pour parler à un périphérique ou un driver.
C’est très proche du concept Windows :
Code: Select all
DeviceIoControl()
IOCTL
IRP_MJ_DEVICE_CONTROL
Code: Select all
ioctl(fd, request, arg);
Code: Select all
DeviceIoControl(hDevice,
IOCTL_CODE,
inputBuffer,
inputSize,
outputBuffer,
outputSize,
&bytesReturned,
NULL);
Code: Select all
envoyer une commande spécifique à un objet/périphérique
16. Comparaison rapide avec Windows
16.1. File descriptor vs HANDLE
Sous Linux :
Code: Select all
int fd;
Code: Select all
HANDLE h;
Différence importante :
- Linux expose un petit entier ;
- Windows expose une valeur opaque typée HANDLE ;
- dans les deux cas, le noyau possède les vraies structures internes.
Linux :
Code: Select all
int fd = open("test.txt", O_RDONLY);
Code: Select all
HANDLE h = CreateFileW(L"test.txt",
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
- ouvrir une ressource ;
- définir les droits ;
- obtenir un identifiant ;
- utiliser cet identifiant pour les opérations suivantes.
Linux :
Code: Select all
read(fd, buffer, size);
write(fd, buffer, size);
Code: Select all
ReadFile(h, buffer, size, &readBytes, NULL);
WriteFile(h, buffer, size, &writtenBytes, NULL);
16.4. lseek() vs SetFilePointer
Linux :
Code: Select all
lseek(fd, offset, SEEK_SET);
Code: Select all
SetFilePointer(hFile, offset, NULL, FILE_BEGIN);
17. Pièges classiques
17.1. Oublier de vérifier open()
Mauvais :
Code: Select all
int fd = open("test.txt", O_RDONLY);
read(fd, buffer, sizeof(buffer));
Code: Select all
int fd = open("test.txt", O_RDONLY);
if (fd == -1)
{
perror("open");
return 1;
}
Mauvais :
Code: Select all
read(fd, buffer, sizeof(buffer));
printf("%s", buffer);
Code: Select all
ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = '\0';
printf("%s", buffer);
}
Mauvais :
Code: Select all
write(fd, buffer, size);
17.4. Double close
Mauvais :
Code: Select all
close(fd);
close(fd);
Code: Select all
close(fd);
fd = -1;
Mauvais :
Code: Select all
lseek(sockfd, 0, SEEK_SET);
18. Mini-programme d’entraînement
Objectif :
Créer un petit programme Linux qui :
- ouvre un fichier donné en argument ;
- lit son contenu ;
- l’affiche sur stdout ;
- gère les erreurs ;
- ferme correctement le fd.
Code: Select all
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#define BUF_SIZE 1024
int main(int argc, char *argv[])
{
int fd;
char buffer[BUF_SIZE];
ssize_t n;
if (argc != 2)
{
fprintf(stderr, "Usage: %s fichier\n", argv[0]);
return 1;
}
fd = open(argv[1], O_RDONLY);
if (fd == -1)
{
perror("open");
return 1;
}
while ((n = read(fd, buffer, sizeof(buffer))) > 0)
{
ssize_t total = 0;
while (total < n)
{
ssize_t w = write(STDOUT_FILENO,
buffer + total,
n - total);
if (w == -1)
{
if (errno == EINTR)
continue;
perror("write");
close(fd);
return 1;
}
total += w;
}
}
if (n == -1)
{
perror("read");
close(fd);
return 1;
}
if (close(fd) == -1)
{
perror("close");
return 1;
}
return 0;
}
Code: Select all
gcc -Wall -Wextra -o catmini catmini.c
Code: Select all
./catmini test.txt
19. Ce qu’il faut retenir absolument
Résumé chapitre concepts système
- Un appel système est une entrée contrôlée dans le noyau.
- Le programme user mode ne manipule pas directement les ressources matérielles.
- La glibc fournit des wrappers et des fonctions de bibliothèque.
- Fonction de bibliothèque et syscall ne sont pas toujours la même chose.
- Les syscalls ont un coût.
- Il faut toujours vérifier les valeurs de retour.
- errno indique la cause d’une erreur après un échec.
- Les macros comme _GNU_SOURCE et _POSIX_C_SOURCE contrôlent les définitions visibles.
- Les types système comme pid_t, off_t, ssize_t existent pour écrire du code propre et portable.
- UNIX utilise un modèle universel d’I/O.
- Un file descriptor est un int, mais il représente une ressource noyau ouverte.
- 0, 1 et 2 correspondent généralement à stdin, stdout et stderr.
- Un socket, un pipe, un terminal ou un fichier peuvent tous être représentés par un fd.
- open() ouvre ou crée une ressource.
- read() lit des octets et retourne le nombre d’octets lus.
- write() écrit des octets et peut écrire partiellement.
- close() libère le fd.
- lseek() déplace le file offset.
- read() ne rajoute jamais de '\0'.
- EOF est indiqué par read() qui retourne 0.
- Les fichiers creux permettent d’avoir une taille logique plus grande que l’espace réellement utilisé.
- ioctl() sert aux opérations spécifiques qui ne rentrent pas dans read/write/lseek.
20. Conclusion
Ces deux chapitres sont une base énorme pour comprendre Linux en profondeur.
La philosophie est simple :
Code: Select all
Programme
|
v
file descriptor
|
v
open/read/write/close
|
v
noyau Linux
|
v
ressource réelle
- des structures noyau ;
- des permissions ;
- des offsets ;
- des flags ;
- des opérations spécifiques ;
- des systèmes de fichiers ;
- des drivers ;
- des sockets ;
- des pipes.
Comprendre les file descriptors, c’est comprendre une grande partie de la philosophie UNIX :
Code: Select all
tout est manipulé comme un flux ou un fichier ouvert
- dup() et dup2() ;
- redirections shell ;
- pipes ;
- fork() et héritage de fd ;
- exec() et O_CLOEXEC ;
- sockets ;
- epoll ;
- mmap ;
- VFS ;
- drivers Linux ;
- IPC ;
- I/O asynchrone.
