Cours Linux — Concepts fondamentaux du développement système

Ce forum est dédié à apprendre le développement de programmes user mode sur Linux

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 46
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

Cours Linux — Concepts fondamentaux du développement système

Post by Hydraxx »

Cours Linux — Concepts fondamentaux du développement système

Kernel, shell, processus, fichiers, permissions, I/O, mémoire, IPC, signaux et /proc



Introduction

Linux est un système de type UNIX. Pour un développeur système, Linux est très intéressant parce qu’il expose beaucoup de mécanismes internes de façon directe : fichiers, processus, mémoire virtuelle, descripteurs de fichiers, signaux, pipes, sockets, pseudo-systèmes de fichiers, etc.

Contrairement à un environnement très encapsulé, Linux repose sur une philosophie assez simple :
  • tout est vu comme une ressource manipulable ;
  • beaucoup de ressources sont représentées par des fichiers ou des descripteurs ;
  • les programmes communiquent avec le noyau via des appels système ;
  • le shell permet d’orchestrer les programmes ;
  • les processus sont isolés mais peuvent communiquer via des mécanismes prévus.
Ce cours pose les bases nécessaires pour commencer le développement système sous Linux.



1. Le système d’exploitation et le noyau

Le terme système d’exploitation peut avoir deux sens.
  • Au sens large : l’ensemble du système, avec le noyau, les outils, le shell, les bibliothèques, l’interface graphique, les utilitaires, etc.
  • Au sens strict : le noyau, c’est-à-dire la partie centrale qui contrôle les ressources matérielles.
Sous Linux, le noyau s’appelle simplement le kernel Linux.

Le kernel est responsable de tâches fondamentales :
  • gestion des processus ;
  • ordonnancement CPU ;
  • gestion mémoire ;
  • gestion des fichiers ;
  • gestion des périphériques ;
  • réseau ;
  • permissions et sécurité ;
  • communication entre user mode et kernel mode.
Le programme utilisateur ne parle pas directement au matériel. Il demande au noyau d’effectuer certaines actions via des appels système.

Exemple :

Code: Select all

read(fd, buffer, size);
Ici, le programme demande au noyau de lire des données depuis une ressource ouverte représentée par fd.



2. User mode et kernel mode

Linux sépare l’exécution en deux grands mondes.

User mode

Le user mode contient les programmes classiques :
  • shell ;
  • navigateur ;
  • éditeur de texte ;
  • serveur web ;
  • programme C utilisateur ;
  • outils système.
Un programme en user mode ne peut pas accéder directement :
  • à la mémoire du noyau ;
  • au matériel ;
  • aux instructions privilégiées du CPU ;
  • à la mémoire d’un autre processus sans autorisation.
Kernel mode

Le kernel mode est le mode privilégié. Le noyau peut accéder à l’ensemble de la machine.

Il peut :
  • modifier les tables de pages ;
  • planifier les processus ;
  • communiquer avec les drivers ;
  • gérer les interruptions ;
  • accéder aux périphériques ;
  • forcer l’arrêt d’un processus.
Quand un programme appelle une fonction comme read(), write(), open() ou fork(), il finit par déclencher un passage contrôlé vers le noyau.

Schéma simplifié :

Code: Select all

Programme utilisateur
        |
        v
Bibliothèque C / wrapper
        |
        v
Appel système
        |
        v
Kernel Linux
        |
        v
Matériel / ressources système


3. Les appels système

Un appel système est une demande faite au noyau.

Exemples classiques :

Code: Select all

open()
read()
write()
close()
fork()
execve()
waitpid()
mmap()
pipe()
socket()
kill()
La plupart du temps, le développeur n’appelle pas directement l’instruction CPU de syscall. Il utilise une fonction fournie par la bibliothèque C, généralement glibc.

Exemple :

Code: Select all

#include <unistd.h>

int main()
{
    write(1, "Bonjour\n", 8);
    return 0;
}
Ici :
  • write() demande au noyau d’écrire des données ;
  • 1 représente la sortie standard ;
  • le texte est envoyé vers le terminal.


4. Le shell

Le shell est un interpréteur de commandes. Il lit les commandes entrées par l’utilisateur et lance les programmes correspondants.

Exemples de shells :
  • sh ;
  • bash ;
  • zsh ;
  • ksh ;
  • fish.
Le shell est un programme utilisateur, mais il joue un rôle central sous Linux.

Exemple :

Code: Select all

ls -la
Le shell analyse cette commande, trouve le programme ls, crée un processus, puis exécute le programme.

Le shell gère aussi les redirections :

Code: Select all

ls > fichiers.txt
Ici, la sortie de ls est redirigée vers un fichier.

Il gère également les pipes :

Code: Select all

cat fichier.txt | grep Linux
Ici :
  • cat lit le fichier ;
  • sa sortie est envoyée à grep ;
  • grep filtre les lignes contenant "Linux".
La philosophie UNIX consiste à écrire de petits programmes spécialisés, puis à les combiner grâce au shell.



5. Les utilisateurs et les groupes

Linux est un système multi-utilisateur. Chaque utilisateur possède un identifiant numérique appelé UID.

Exemple :

Code: Select all

id
Sortie possible :

Code: Select all

uid=1000(jean) gid=1000(jean) groups=1000(jean),27(sudo)
Ici :
  • uid est l’identifiant utilisateur ;
  • gid est l’identifiant du groupe principal ;
  • groups liste les groupes auxquels appartient l’utilisateur.
Le superutilisateur root

L’utilisateur spécial root possède l’UID 0.

Il a des privilèges très élevés sur le système.

Code: Select all

uid=0(root)
Root peut généralement :
  • modifier des fichiers système ;
  • changer les permissions ;
  • installer des services ;
  • tuer des processus ;
  • charger certains composants système ;
  • administrer les utilisateurs.
Il faut éviter de travailler inutilement en root, car une erreur peut casser le système.



6. La hiérarchie unique des fichiers

Linux utilise une seule arborescence de fichiers.

Contrairement à Windows, il n’y a pas de :

Code: Select all

C:\
D:\
E:\
Sous Linux, tout commence à la racine :

Code: Select all

/
Exemples de répertoires importants :
  • /bin : commandes essentielles ;
  • /sbin : commandes système ;
  • /etc : fichiers de configuration ;
  • /home : dossiers utilisateurs ;
  • /root : dossier personnel de root ;
  • /usr : programmes, bibliothèques, headers ;
  • /var : fichiers variables, logs, caches ;
  • /tmp : fichiers temporaires ;
  • /dev : fichiers représentant les périphériques ;
  • /proc : pseudo-système de fichiers exposant des infos kernel/processus ;
  • /sys : informations sur les périphériques et le noyau.
Exemple :

Code: Select all

/home/jean/projets/test.c
Ce chemin est absolu parce qu’il commence par /.



7. Chemins absolus et chemins relatifs

Un chemin absolu commence depuis la racine.

Code: Select all

/home/jean/test.txt
Un chemin relatif dépend du répertoire courant du processus.

Code: Select all

test.txt
./test.txt
../test.txt
Deux noms sont fondamentaux :
  • . représente le répertoire courant ;
  • .. représente le répertoire parent.
Exemples :

Code: Select all

cd .
cd ..
pwd
Chaque processus possède un répertoire courant. Ce répertoire est utilisé pour résoudre les chemins relatifs.



8. Les types de fichiers

Sous Linux, le mot fichier ne désigne pas seulement un fichier texte ou binaire.

Il peut désigner plusieurs types d’objets :
  • fichier régulier ;
  • répertoire ;
  • lien symbolique ;
  • socket ;
  • pipe ;
  • périphérique caractère ;
  • périphérique bloc.
Exemple avec ls -l :

Code: Select all

-rw-r--r--  1 jean jean  1200 test.txt
drwxr-xr-x  2 jean jean  4096 projets
lrwxrwxrwx  1 jean jean    10 lien -> test.txt
Le premier caractère indique le type :
  • - : fichier régulier ;
  • d : directory ;
  • l : symbolic link ;
  • c : character device ;
  • b : block device ;
  • p : pipe ;
  • s : socket.


9. Les liens symboliques

Un lien symbolique est un fichier spécial qui pointe vers un autre chemin.

Création :

Code: Select all

ln -s /home/jean/test.txt lien.txt
Lecture :

Code: Select all

ls -l lien.txt
Sortie possible :

Code: Select all

lien.txt -> /home/jean/test.txt
Un lien symbolique ressemble à un raccourci, mais il est intégré au système de fichiers.

Attention : si la cible est supprimée, le lien devient cassé.


10. Permissions Linux

Linux utilise un modèle de permissions simple mais très puissant.

Chaque fichier possède :
  • un propriétaire ;
  • un groupe ;
  • des permissions pour le propriétaire ;
  • des permissions pour le groupe ;
  • des permissions pour les autres utilisateurs.
Exemple :

Code: Select all

-rwxr-xr--
Découpage :

Code: Select all

-     rwx     r-x     r--
type  user    group   others
Signification :
  • r : read, lecture ;
  • w : write, écriture ;
  • x : execute, exécution ou traversée pour un dossier.
Exemple :

Code: Select all

chmod +x programme
Donne le droit d’exécution.

Pour retirer l’écriture aux autres :

Code: Select all

chmod o-w fichier.txt
Avec la notation octale :

Code: Select all

chmod 755 programme
Cela signifie :

Code: Select all

7 = rwx
5 = r-x
5 = r-x
Donc :
  • propriétaire : lecture, écriture, exécution ;
  • groupe : lecture, exécution ;
  • autres : lecture, exécution.


11. Le modèle I/O Linux

Une idée très importante sous UNIX/Linux est l’uniformité des entrées/sorties.

Les mêmes fonctions peuvent servir à lire ou écrire :
  • des fichiers ;
  • des pipes ;
  • des sockets ;
  • des terminaux ;
  • certains périphériques.
Les fonctions fondamentales sont :

Code: Select all

open()
read()
write()
close()
Exemple minimal :

Code: Select all

#include <fcntl.h>
#include <unistd.h>

int main()
{
    int fd = open("test.txt", O_RDONLY);

    char buffer[128];

    ssize_t n = read(fd, buffer, sizeof(buffer));

    close(fd);

    return 0;
}
Ici :
  • open() ouvre le fichier ;
  • read() lit les données ;
  • close() ferme la ressource.


12. Les file descriptors

Un file descriptor est un entier qui représente une ressource ouverte dans un processus.

Exemple :

Code: Select all

int fd = open("test.txt", O_RDONLY);
La valeur fd est un index dans la table des descripteurs du processus.

Les trois premiers descripteurs sont standards :
  • 0 : standard input, stdin ;
  • 1 : standard output, stdout ;
  • 2 : standard error, stderr.
Exemple :

Code: Select all

write(1, "Hello\n", 6);
write(2, "Erreur\n", 7);
Le premier écrit sur la sortie standard, le second sur la sortie d’erreur.

Comparaison mentale :

Code: Select all

Linux file descriptor  ~=  HANDLE Windows
open()                 ~=  CreateFile()
read()                 ~=  ReadFile()
write()                ~=  WriteFile()
close()                ~=  CloseHandle()
La différence est que Linux pousse ce modèle beaucoup plus loin : énormément de ressources sont représentées par des descripteurs.


13. Programmes et arguments de ligne de commande

Un programme C classique reçoit ses arguments avec :

Code: Select all

int main(int argc, char *argv[])
{
    return 0;
}
argc contient le nombre d’arguments.

argv est un tableau de chaînes.

Exemple d’exécution :

Code: Select all

./prog hello test
Contenu :

Code: Select all

argv[0] = "./prog"
argv[1] = "hello"
argv[2] = "test"
argc    = 3
Exemple :

Code: Select all

#include <stdio.h>

int main(int argc, char *argv[])
{
    for (int i = 0; i < argc; i++)
    {
        printf("argv[%d] = %s\n", i, argv[i]);
    }

    return 0;
}
Compilation :

Code: Select all

gcc main.c -o prog
Exécution :

Code: Select all

./prog linux kernel system

14. Les processus

Un processus est une instance d’un programme en cours d’exécution.

Un processus possède :
  • un PID ;
  • un espace mémoire virtuel ;
  • des file descriptors ;
  • un répertoire courant ;
  • des variables d’environnement ;
  • un utilisateur propriétaire ;
  • un ou plusieurs threads ;
  • un état d’exécution.
Pour afficher les processus :

Code: Select all

ps aux
Pour afficher le PID du shell courant :

Code: Select all

echo $$
En C :

Code: Select all

#include <stdio.h>
#include <unistd.h>

int main()
{
    printf("PID = %d\n", getpid());
    printf("PPID = %d\n", getppid());

    return 0;
}
getpid() retourne le PID du processus courant.

getppid() retourne le PID du parent.


15. Layout mémoire d’un processus

Un processus possède un espace d’adressage virtuel découpé en zones.

Schéma simplifié :

Code: Select all

+---------------------------+
| Stack                     |
+---------------------------+
|                           |
| Mémoire libre / mappings  |
|                           |
+---------------------------+
| Heap                      |
+---------------------------+
| Data / BSS                |
+---------------------------+
| Text                      |
+---------------------------+
Les principales zones sont :
  • Text : code machine du programme ;
  • Data : variables globales initialisées ;
  • BSS : variables globales non initialisées ;
  • Heap : mémoire dynamique, utilisée par malloc() ;
  • Stack : appels de fonctions, variables locales, adresses de retour.
Exemple :

Code: Select all

#include <stdio.h>
#include <stdlib.h>

int global = 123;

int main()
{
    int local = 456;
    int *heap = malloc(sizeof(int));

    printf("global = %p\n", (void*)&global);
    printf("local  = %p\n", (void*)&local);
    printf("heap   = %p\n", (void*)heap);

    free(heap);
    return 0;
}

16. Création de processus : fork()

Sous Linux, la création de processus repose historiquement sur fork().

fork() duplique le processus courant.

Après l’appel :
  • le parent continue ;
  • l’enfant continue ;
  • les deux reprennent juste après le fork ;
  • la valeur de retour permet de savoir dans quel processus on est.
Exemple :

Code: Select all

#include <stdio.h>
#include <unistd.h>

int main()
{
    pid_t pid = fork();

    if (pid == 0)
    {
        printf("Je suis l'enfant, PID = %d\n", getpid());
    }
    else
    {
        printf("Je suis le parent, PID enfant = %d\n", pid);
    }

    return 0;
}
Valeurs de retour :
  • 0 dans le processus enfant ;
  • > 0 dans le processus parent ;
  • -1 en cas d’erreur.
Ce modèle est très différent de Windows, où l’on utilise plutôt CreateProcess().


17. Exécution d’un autre programme : execve()

fork() duplique le processus.

execve() remplace l’image mémoire du processus courant par un autre programme.

Modèle UNIX classique :

Code: Select all

fork()
  |
  +--> enfant
          |
          +--> execve()
Exemple avec execlp(), wrapper plus simple :

Code: Select all

#include <unistd.h>
#include <stdio.h>

int main()
{
    execlp("ls", "ls", "-la", NULL);

    perror("execlp");
    return 1;
}
Si execlp() réussit, le code après ne s’exécute pas, car le processus a été remplacé par ls.



18. Attendre un processus : waitpid()

Le parent peut attendre la fin d’un enfant avec waitpid().

Exemple :

Code: Select all

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
    pid_t pid = fork();

    if (pid == 0)
    {
        execlp("ls", "ls", "-la", NULL);
        return 1;
    }
    else
    {
        int status;
        waitpid(pid, &status, 0);
        printf("L'enfant est terminé\n");
    }

    return 0;
}
Cela évite de laisser des processus zombies.



19. Variables d’environnement

Chaque processus possède un environnement : un ensemble de chaînes clé=valeur.

Exemples :

Code: Select all

PATH=/usr/bin:/bin
HOME=/home/jean
SHELL=/bin/bash
USER=jean
Afficher les variables :

Code: Select all

env
Lire une variable en C :

Code: Select all

#include <stdio.h>
#include <stdlib.h>

int main()
{
    char *home = getenv("HOME");

    if (home != NULL)
        printf("HOME = %s\n", home);

    return 0;
}
Le shell transmet généralement son environnement aux processus enfants.


20. Resource limits

Linux peut limiter les ressources d’un processus.

Exemples :
  • taille maximale de fichier ;
  • nombre maximal de fichiers ouverts ;
  • taille de stack ;
  • nombre de processus ;
  • mémoire verrouillée ;
  • temps CPU.
Commande shell :

Code: Select all

ulimit -a
En C, on peut utiliser :

Code: Select all

getrlimit()
setrlimit()
Exemple :

Code: Select all

#include <stdio.h>
#include <sys/resource.h>

int main()
{
    struct rlimit lim;

    if (getrlimit(RLIMIT_NOFILE, &lim) == 0)
    {
        printf("Limite soft = %ld\n", (long)lim.rlim_cur);
        printf("Limite hard = %ld\n", (long)lim.rlim_max);
    }

    return 0;
}

21. Memory mappings : mmap()

mmap() est une API très importante.

Elle permet de créer un mapping mémoire dans l’espace virtuel du processus.

Elle peut servir à :
  • mapper un fichier en mémoire ;
  • allouer de la mémoire anonyme ;
  • créer de la mémoire partagée ;
  • charger des bibliothèques ;
  • gérer des zones mémoire avec permissions précises.
Prototype simplifié :

Code: Select all

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
Exemple : allocation anonyme.

Code: Select all

#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

int main()
{
    void *p = mmap(
        NULL,
        4096,
        PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS,
        -1,
        0
    );

    if (p == MAP_FAILED)
        return 1;

    strcpy((char*)p, "Hello mmap");

    printf("%s\n", (char*)p);

    munmap(p, 4096);

    return 0;
}
Comparaison :

Code: Select all

Linux mmap()        ~= VirtualAlloc() + MapViewOfFile()
Windows sections    ~= mappings Linux

22. Copy-On-Write

Après un fork(), Linux ne copie pas forcément immédiatement toute la mémoire du processus.

Il utilise souvent une stratégie appelée Copy-On-Write.

Principe :
  • parent et enfant partagent temporairement les mêmes pages physiques ;
  • les pages sont marquées en lecture seule ;
  • si l’un des deux écrit dans une page, le noyau crée une copie privée ;
  • cela évite de copier inutilement toute la mémoire.
Exemple mental :

Code: Select all

Parent
  |
fork()
  |
  +--> Enfant

Au début :
Parent et enfant partagent les mêmes pages.

Si l'enfant écrit :
le noyau copie seulement la page modifiée.
C’est une raison importante pour laquelle fork() peut être très efficace.



23. Bibliothèques statiques et partagées

Linux utilise deux grands types de bibliothèques.

Bibliothèque statique

Extension courante :

Code: Select all

.a
Le code est copié dans l’exécutable au moment du linkage.

Avantages :
  • binaire plus autonome ;
  • moins de dépendances externes.
Inconvénients :
  • binaire plus gros ;
  • mise à jour plus difficile ;
  • duplication du code en mémoire.
Bibliothèque partagée

Extension courante :

Code: Select all

.so
Equivalent conceptuel des DLL Windows.

Exemple :

Code: Select all

libc.so
libpthread.so
libm.so
Avantages :
  • moins de duplication ;
  • mise à jour plus simple ;
  • partage possible du code entre processus ;
  • chargement dynamique.
Compilation avec la lib math :

Code: Select all

gcc main.c -o prog -lm


24. Communication inter-processus : IPC

Les processus sont isolés. Pour communiquer, ils doivent utiliser des mécanismes d’IPC.

Linux propose plusieurs mécanismes :
  • pipes ;
  • FIFO ;
  • sockets UNIX ;
  • sockets réseau ;
  • shared memory ;
  • message queues ;
  • semaphores ;
  • signals.
Pipe anonyme

Un pipe permet de faire communiquer deux processus, souvent parent/enfant.

Code: Select all

#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main()
{
    int fd[2];
    pipe(fd);

    pid_t pid = fork();

    if (pid == 0)
    {
        close(fd[1]);

        char buffer[64];
        read(fd[0], buffer, sizeof(buffer));

        printf("Enfant a reçu : %s\n", buffer);
    }
    else
    {
        close(fd[0]);

        char msg[] = "message depuis parent";
        write(fd[1], msg, strlen(msg) + 1);
    }

    return 0;
}
Ici :
  • fd[0] sert à lire ;
  • fd[1] sert à écrire.

25. Les signaux

Un signal est une interruption logicielle envoyée à un processus.

Exemples :
  • SIGINT : Ctrl+C ;
  • SIGTERM : demande propre de terminaison ;
  • SIGKILL : terminaison forcée ;
  • SIGSEGV : accès mémoire invalide ;
  • SIGCHLD : un processus enfant s’est terminé ;
  • SIGSTOP : suspension forcée ;
  • SIGCONT : reprise.
Envoyer un signal depuis le shell :

Code: Select all

kill -TERM 1234
kill -9 1234
Intercepter Ctrl+C :

Code: Select all

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void handler(int sig)
{
    printf("Signal reçu : %d\n", sig);
}

int main()
{
    signal(SIGINT, handler);

    while (1)
    {
        pause();
    }

    return 0;
}
Attention : tous les signaux ne peuvent pas être interceptés. Par exemple, SIGKILL ne peut pas être bloqué ni intercepté.


26. Les threads

Un processus peut contenir plusieurs threads.

Les threads partagent :
  • l’espace mémoire ;
  • le heap ;
  • les variables globales ;
  • les file descriptors ;
  • le code du programme.
Chaque thread possède généralement :
  • sa stack ;
  • son contexte CPU ;
  • ses registres ;
  • son flot d’exécution.
Sous Linux, l’API portable classique est pthread.

Exemple :

Code: Select all

#include <pthread.h>
#include <stdio.h>

void* thread_func(void *arg)
{
    printf("Thread lancé\n");
    return NULL;
}

int main()
{
    pthread_t t;

    pthread_create(&t, NULL, thread_func, NULL);
    pthread_join(t, NULL);

    return 0;
}
Compilation :

Code: Select all

gcc main.c -o prog -pthread
Comparaison :

Code: Select all

Linux pthread_create()  ~=  Windows CreateThread()
Linux pthread_join()    ~=  WaitForSingleObject()
Linux pthread_mutex_t   ~=  CRITICAL_SECTION / Mutex


27. Process groups et job control

Le shell gère les processus avec des notions propres à UNIX :
  • session ;
  • process group ;
  • foreground process ;
  • background process ;
  • job control.
Lancer un programme en arrière-plan :

Code: Select all

./programme &
Afficher les jobs :

Code: Select all

jobs
Ramener un job au premier plan :

Code: Select all

fg
Suspendre un programme :

Code: Select all

Ctrl+Z
Reprendre en arrière-plan :

Code: Select all

bg
Cela repose en interne sur des groupes de processus et des signaux comme SIGTSTP, SIGCONT, SIGINT.



28. Daemons

Un daemon est un processus de fond, souvent lancé au démarrage.

Equivalent conceptuel d’un service Windows.

Exemples :
  • sshd : serveur SSH ;
  • cron : tâches planifiées ;
  • systemd : gestionnaire de services ;
  • nginx : serveur web ;
  • dbus-daemon : bus de communication.
Un daemon typique :
  • n’est pas attaché à un terminal utilisateur ;
  • tourne en arrière-plan ;
  • peut écrire dans des logs ;
  • peut être contrôlé par systemd.
Exemple :

Code: Select all

systemctl status ssh
systemctl start ssh
systemctl stop ssh


29. Pseudo-terminaux

Un pseudo-terminal est une paire de périphériques virtuels qui imite un vrai terminal.

On retrouve ce concept dans :
  • SSH ;
  • terminal graphique ;
  • tmux ;
  • screen ;
  • docker exec -it ;
  • émulateurs de terminal.
Il existe généralement deux côtés :
  • master : contrôlé par le programme hôte ;
  • slave : vu par le programme lancé comme un terminal normal.
Cela permet à un programme de croire qu’il parle à un terminal réel, alors qu’il parle à une interface virtuelle.


30. Date et temps

Linux utilise souvent l’epoch UNIX comme référence.

L’epoch UNIX correspond au :

Code: Select all

1 janvier 1970 00:00:00 UTC
Le type time_t représente souvent un nombre de secondes depuis cette date.

Exemple :

Code: Select all

#include <stdio.h>
#include <time.h>

int main()
{
    time_t now = time(NULL);

    printf("time_t = %ld\n", (long)now);
    printf("date   = %s", ctime(&now));

    return 0;
}
Il existe plusieurs notions :
  • temps réel ;
  • temps CPU ;
  • temps monotone ;
  • temps utilisateur ;
  • temps noyau.

31. Architecture client/serveur

Le modèle client/serveur est très utilisé sous Linux.

Un client demande un service.

Un serveur répond.

Exemples :
  • navigateur web -> serveur HTTP ;
  • client SSH -> sshd ;
  • client SQL -> serveur de base de données ;
  • application -> daemon local ;
  • programme utilisateur -> service réseau.
Avantages :
  • séparation des responsabilités ;
  • centralisation d’un service ;
  • partage de ressources ;
  • communication réseau possible ;
  • possibilité d’avoir plusieurs clients pour un serveur.


32. Realtime

Le temps réel ne signifie pas forcément “rapide”. Il signifie surtout que le système doit répondre dans un délai garanti ou borné.

Exemples :
  • robotique ;
  • audio faible latence ;
  • industrie ;
  • automobile ;
  • systèmes embarqués ;
  • contrôle matériel.
Linux classique n’est pas toujours totalement temps réel, mais il possède des extensions et des mécanismes pour s’en rapprocher.

Exemples de mécanismes :
  • scheduling temps réel ;
  • priorités spécifiques ;
  • verrouillage mémoire ;
  • timers haute précision ;
  • signaux temps réel ;
  • PREEMPT_RT sur certains noyaux.


33. Le système de fichiers /proc

/proc est un pseudo-système de fichiers.

Il ne correspond pas à de vrais fichiers stockés sur disque. Il expose des informations du noyau et des processus.

Exemples :

Code: Select all

cat /proc/cpuinfo
cat /proc/meminfo
cat /proc/uptime
Chaque processus possède un dossier :

Code: Select all

/proc/PID/
Exemple :

Code: Select all

ls /proc/1234
On peut y trouver :
  • cmdline : ligne de commande ;
  • environ : environnement ;
  • exe : lien vers l’exécutable ;
  • fd : descripteurs ouverts ;
  • maps : mappings mémoire ;
  • status : état du processus ;
  • task : threads.
Exemple très utile :

Code: Select all

cat /proc/self/maps
Cela affiche les mappings mémoire du processus courant.

Exemple de lignes possibles :

Code: Select all

00400000-00452000 r-xp 00000000 08:01 12345 /bin/cat
00652000-00653000 r--p 00052000 08:01 12345 /bin/cat
7ffd12345000-7ffd12366000 rw-p 00000000 00:00 0 [stack]
On voit :
  • les plages d’adresses ;
  • les permissions mémoire ;
  • les fichiers mappés ;
  • la stack ;
  • le heap ;
  • les bibliothèques.
Pour un développeur système, /proc est une mine d’or.



34. Comparaison Linux / Windows

Pour quelqu’un venant de Win32/NT, certaines correspondances aident beaucoup.

Code: Select all

Linux                          Windows
-----------------------------------------------------
file descriptor                HANDLE
open()                         CreateFile()
read()                         ReadFile()
write()                        WriteFile()
close()                        CloseHandle()

fork() + execve()              CreateProcess()
waitpid()                      WaitForSingleObject()
pthread_create()               CreateThread()
pthread_mutex_t                CRITICAL_SECTION / Mutex

mmap()                         VirtualAlloc() / MapViewOfFile()
mprotect()                     VirtualProtect()
shared library .so             DLL
daemon                         Windows Service
signal                         Console event / exception / APC-like concept
/proc/PID/maps                 VMMap / VAD-style view
pipe()                         Anonymous Pipe
FIFO                           Named Pipe
socket()                       Winsock socket
Attention : ce tableau donne des équivalences mentales. Les systèmes ne fonctionnent pas exactement pareil en interne.



35. Mini-programme complet : fork + exec + wait

Ce programme crée un processus enfant, exécute ls, puis le parent attend sa fin.

Code: Select all

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
    pid_t pid = fork();

    if (pid == -1)
    {
        perror("fork");
        return 1;
    }

    if (pid == 0)
    {
        execlp("ls", "ls", "-la", NULL);

        perror("execlp");
        return 1;
    }

    int status = 0;

    if (waitpid(pid, &status, 0) == -1)
    {
        perror("waitpid");
        return 1;
    }

    printf("Processus enfant terminé\n");

    return 0;
}
Compilation :

Code: Select all

gcc main.c -o test
Exécution :

Code: Select all

./test
Ce code montre le modèle UNIX fondamental :

Code: Select all

parent
  |
fork()
  |
  +-- child
        |
        +-- exec()
parent
  |
waitpid()


36. Mini-programme complet : open/read/write

Ce programme ouvre un fichier, lit son contenu et l’écrit sur la sortie standard.

Code: Select all

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main()
{
    int fd = open("test.txt", O_RDONLY);

    if (fd == -1)
    {
        perror("open");
        return 1;
    }

    char buffer[256];

    ssize_t n;

    while ((n = read(fd, buffer, sizeof(buffer))) > 0)
    {
        write(1, buffer, n);
    }

    close(fd);

    return 0;
}
Ici, on voit bien le modèle Linux :
  • ouvrir une ressource ;
  • obtenir un file descriptor ;
  • lire avec read() ;
  • écrire avec write() ;
  • fermer avec close().


37. Mini-programme complet : lire /proc/self/maps

Ce programme lit les mappings mémoire du processus courant.

Code: Select all

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main()
{
    int fd = open("/proc/self/maps", O_RDONLY);

    if (fd == -1)
    {
        perror("open");
        return 1;
    }

    char buffer[512];
    ssize_t n;

    while ((n = read(fd, buffer, sizeof(buffer))) > 0)
    {
        write(1, buffer, n);
    }

    close(fd);

    return 0;
}
Compilation :

Code: Select all

gcc maps.c -o maps
Exécution :

Code: Select all

./maps
Ce programme montre que /proc se lit comme un fichier, même si les données viennent du noyau.



38. Ce qu’il faut retenir

Les concepts fondamentaux du développement système Linux sont :
  • kernel : cœur du système ;
  • syscall : entrée contrôlée vers le noyau ;
  • processus : programme en cours d’exécution ;
  • thread : flot d’exécution dans un processus ;
  • file descriptor : entier représentant une ressource ouverte ;
  • shell : interpréteur et orchestrateur de commandes ;
  • / : racine unique du système de fichiers ;
  • /proc : pseudo-système de fichiers exposant des informations kernel ;
  • fork() : création de processus ;
  • execve() : remplacement de l’image mémoire ;
  • waitpid() : attente d’un processus enfant ;
  • mmap() : mapping mémoire ;
  • signals : interruptions logicielles ;
  • IPC : communication entre processus.
Linux est très cohérent : beaucoup de choses se manipulent avec les mêmes primitives.

Code: Select all

open()
read()
write()
close()
Cette uniformité est une des grandes forces de la philosophie UNIX.



Conclusion

Pour bien apprendre le développement système Linux, il faut penser autour de quelques piliers :
  • le noyau contrôle les ressources ;
  • les programmes passent par des appels système ;
  • les ressources ouvertes sont représentées par des file descriptors ;
  • les processus sont isolés ;
  • les processus communiquent via IPC ;
  • la mémoire virtuelle est manipulable avec mmap() ;
  • le shell est un outil central ;
  • /proc expose une partie de l’état interne du système.
Quand ces bases sont claires, on peut ensuite aborder les sujets plus avancés :
  • appels système en détail ;
  • gestion des erreurs avec errno ;
  • fichiers avancés ;
  • répertoires ;
  • processus zombies ;
  • signaux avancés ;
  • threads et synchronisation ;
  • sockets ;
  • epoll ;
  • futex ;
  • ptrace ;
  • chargement ELF ;
  • internals du kernel Linux.
Linux est un système très formateur pour le développement bas niveau, car il expose clairement les mécanismes qui relient le code utilisateur au noyau.

Who is online

Users browsing this forum: No registered users and 1 guest