Objectif du cours
Ce cours explique comment énumérer les processus sous Linux en utilisant le pseudo-système de fichiers /proc.
Le but est de comprendre comment un programme peut récupérer des informations sur les processus en cours d’exécution sans utiliser une API haut niveau comme ps, top ou htop.
On va voir :
- ce qu’est /proc ;
- pourquoi chaque processus possède un dossier /proc/PID ;
- comment parcourir /proc avec opendir() et readdir() ;
- comment identifier les entrées qui correspondent à des PID ;
- comment lire /proc/PID/status ;
- comment extraire le nom, le PID, le PPID et l’état d’un processus ;
- comment faire cela avec les appels Linux bas niveau open(), read() et close() ;
- quelles sont les erreurs classiques à éviter.
1. Philosophie Linux : beaucoup d’informations système sont exposées comme des fichiers
Sous Linux, beaucoup d’informations internes du système sont visibles via des pseudo-fichiers.
Cela vient de la philosophie UNIX :
“Everything is a file”
Cela ne veut pas dire que tout est littéralement un fichier stocké sur disque. Cela veut surtout dire que beaucoup de ressources sont manipulables avec des mécanismes similaires :
- fichiers ;
- répertoires ;
- périphériques ;
- pipes ;
- sockets ;
- pseudo-fichiers kernel ;
- informations de processus ;
- paramètres système.
Code: Select all
open()
read()
write()
close()
Cela est très différent de Windows où beaucoup d’informations système passent par des APIs spécialisées comme :
Code: Select all
CreateToolhelp32Snapshot()
Process32First()
Process32Next()
NtQuerySystemInformation()
EnumProcesses()
2. Qu’est-ce que /proc ?
/proc est un pseudo-système de fichiers.
Cela signifie que ce n’est pas un dossier classique stocké sur le disque dur ou le SSD.
Quand on lit un fichier dans /proc, le kernel génère dynamiquement son contenu à partir de ses structures internes.
Exemple :
Code: Select all
/proc/cpuinfo
/proc/meminfo
/proc/uptime
/proc/version
/proc/modules
/proc/net
/proc/sys
- le processeur ;
- la mémoire ;
- le temps depuis le démarrage ;
- la version du kernel ;
- les modules chargés ;
- la pile réseau ;
- certains paramètres kernel.
un dossier par processus.
3. Le dossier d’un processus : /proc/PID
Chaque processus en cours d’exécution possède un dossier dans /proc.
Exemple :
Code: Select all
/proc/1
/proc/245
/proc/1337
/proc/4242
Le PID est l’identifiant numérique du processus.
Exemple :
Code: Select all
/proc/1
Dans un dossier /proc/PID, on trouve beaucoup d’informations sur le processus.
Exemples importants :
Code: Select all
/proc/PID/status
/proc/PID/cmdline
/proc/PID/maps
/proc/PID/fd
/proc/PID/exe
/proc/PID/cwd
/proc/PID/environ
/proc/PID/task
4. Fichiers importants dans /proc/PID
4.1 /proc/PID/status
C’est le fichier le plus simple pour commencer.
Il contient des informations lisibles sur le processus.
Exemple de contenu simplifié :
Code: Select all
Name: bash
State: S (sleeping)
Pid: 245
PPid: 1
Uid: 1000 1000 1000 1000
Gid: 1000 1000 1000 1000
Threads: 1
VmSize: 12345 kB
VmRSS: 4567 kB
- Name : nom du processus ;
- State : état du processus ;
- Pid : PID du processus ;
- PPid : PID du parent ;
- Uid : identifiants utilisateur ;
- Gid : identifiants groupe ;
- Threads : nombre de threads ;
- VmSize : taille virtuelle ;
- VmRSS : mémoire résidente réellement présente en RAM.
4.2 /proc/PID/cmdline
Ce fichier contient la ligne de commande utilisée pour lancer le processus.
Exemple :
Code: Select all
/proc/1234/cmdline
Cela signifie qu’une lecture naïve avec printf("%s") peut ne pas afficher toute la ligne de commande.
4.3 /proc/PID/maps
Ce fichier montre les mappings mémoire du processus.
Exemple :
Code: Select all
00400000-00452000 r-xp 00000000 08:01 123456 /usr/bin/bash
00651000-00652000 r--p 00051000 08:01 123456 /usr/bin/bash
7f2a00000000-7f2a00200000 rw-p 00000000 00:00 0
7ffd12345000-7ffd12366000 rw-p 00000000 00:00 0 [stack]
- reverse engineering ;
- debugging ;
- analyse mémoire ;
- forensic ;
- analyse de malware ;
- compréhension du chargement des bibliothèques partagées.
- VirtualQueryEx() ;
- VAD ;
- liste des modules ;
- zones mémoire du processus.
4.4 /proc/PID/fd
Ce dossier contient les file descriptors ouverts par le processus.
Exemple :
Code: Select all
/proc/1234/fd/0
/proc/1234/fd/1
/proc/1234/fd/2
/proc/1234/fd/3
Exemple :
Code: Select all
0 -> /dev/pts/0
1 -> /dev/pts/0
2 -> /dev/pts/0
3 -> /home/user/test.txt
Cela permet de savoir :
- quels fichiers un processus a ouverts ;
- quels sockets il utilise ;
- quels pipes il possède ;
- vers quel terminal pointent stdin/stdout/stderr.
4.5 /proc/PID/exe
C’est un lien symbolique vers l’exécutable réel du processus.
Exemple :
Code: Select all
/proc/1234/exe -> /usr/bin/bash
Code: Select all
readlink()
4.6 /proc/PID/cwd
C’est un lien symbolique vers le répertoire courant du processus.
Exemple :
Code: Select all
/proc/1234/cwd -> /home/user/projects
4.7 /proc/PID/task
Ce dossier contient les threads du processus.
Sous Linux, les threads sont représentés comme des tâches.
Exemple :
Code: Select all
/proc/1234/task/1234
/proc/1234/task/1235
/proc/1234/task/1236
5. Principe de l’énumération des processus
Pour lister les processus sous Linux, une méthode classique consiste à :
- ouvrir le dossier /proc ;
- parcourir toutes ses entrées ;
- garder uniquement les entrées dont le nom est numérique ;
- considérer ces noms numériques comme des PID ;
- construire le chemin /proc/PID/status ;
- ouvrir ce fichier ;
- lire son contenu ;
- extraire les champs importants.
Code: Select all
/proc
1
2
3
sys
net
self
thread-self
245
1337
Code: Select all
1
2
3
245
1337
Les entrées :
Code: Select all
sys
net
self
thread-self
Donc il faut filtrer.
6. APIs nécessaires
6.1 Parcourir un dossier
Includes :
Code: Select all
#include <dirent.h>
Code: Select all
opendir()
readdir()
closedir()
Code: Select all
DIR
struct dirent
Code: Select all
DIR *dir = opendir("/proc");
struct dirent *entry;
while ((entry = readdir(dir)) != NULL)
{
printf("%s\n", entry->d_name);
}
closedir(dir);
6.2 Ouvrir et lire un fichier en mode Linux bas niveau
Includes :
Code: Select all
#include <fcntl.h>
#include <unistd.h>
Code: Select all
open()
read()
close()
Code: Select all
int fd;
ssize_t;
Code: Select all
int fd = open("/proc/self/status", O_RDONLY);
char buffer[4096];
ssize_t bytes = read(fd, buffer, sizeof(buffer) - 1);
close(fd);
Il faut donc faire :
Code: Select all
buffer[bytes] = '\0';
6.3 Construire un chemin
Include :
Code: Select all
#include <stdio.h>
Code: Select all
snprintf()
Code: Select all
char path[512];
snprintf(path, sizeof(path), "/proc/%s/status", entry->d_name);
Cela réduit les risques de dépassement mémoire.
6.4 Vérifier qu’un nom est numérique
Include :
Code: Select all
#include <ctype.h>
Code: Select all
isdigit()
Code: Select all
int is_number(char *s)
{
int i = 0;
while (s[i])
{
if (!isdigit((unsigned char)s[i]))
return 0;
i++;
}
return 1;
}
Exemple :
Code: Select all
"123"
Mais :
Code: Select all
"1abc"
Donc une vérification robuste doit parcourir toute la chaîne.
7. Mapping Windows / Linux
Pour comprendre rapidement, on peut faire le parallèle avec Win32.
7.1 Parcours de dossier
Code: Select all
Windows Linux
-------------------------------------------------------
FindFirstFile() opendir()
FindNextFile() readdir()
FindClose() closedir()
WIN32_FIND_DATA struct dirent
cFileName d_name
Code: Select all
WIN32_FIND_DATA data;
HANDLE h = FindFirstFile("*", &data);
do
{
printf("%s\n", data.cFileName);
} while (FindNextFile(h, &data));
FindClose(h);
Code: Select all
DIR *dir = opendir(".");
struct dirent *entry;
while ((entry = readdir(dir)) != NULL)
{
printf("%s\n", entry->d_name);
}
closedir(dir);
- Sous Windows, FindFirstFile() ouvre et retourne déjà la première entrée.
- Sous Linux, opendir() ouvre seulement le contexte de parcours.
- Sous Linux, readdir() retourne les entrées une par une.
7.2 Processus
Code: Select all
Windows Linux
-------------------------------------------------------
CreateToolhelp32Snapshot parcourir /proc
Process32First première entrée PID trouvée
Process32Next readdir()
PROCESSENTRY32 /proc/PID/status
th32ProcessID Pid:
th32ParentProcessID PPid:
szExeFile Name:
On peut simplement parcourir /proc.
7.3 Fichiers et handles
Code: Select all
Windows Linux
-------------------------------------------------------
HANDLE int fd
CreateFile() open()
ReadFile() read()
WriteFile() write()
CloseHandle() close()
Code: Select all
FILE *
FILE * appartient à la libc haut niveau et s’utilise avec :
Code: Select all
fopen()
fgets()
fread()
fprintf()
fclose()
Code: Select all
open()
read()
write()
close()
8. Différence entre FILE* et fd
Il faut bien distinguer :
8.1 FILE*
Code: Select all
FILE *f = fopen("test.txt", "r");
Elle ajoute :
- buffering ;
- fonctions haut niveau ;
- lecture ligne par ligne ;
- formatage ;
- gestion interne.
Code: Select all
fopen()
fgets()
fread()
fprintf()
fclose()
8.2 fd
Code: Select all
int fd = open("test.txt", O_RDONLY);
APIs associées :
Code: Select all
open()
read()
write()
close()
dup()
dup2()
fcntl()
Exemples classiques :
Code: Select all
0 -> stdin
1 -> stdout
2 -> stderr
3 -> premier fichier ouvert par le programme
4 -> autre fichier, socket, pipe, etc.
9. Attention : read() lit des octets bruts
Une erreur classique :
Code: Select all
char buffer[128];
read(fd, buffer, sizeof(buffer));
printf("%s\n", buffer);
Pourquoi ?
Parce que read() n’ajoute pas automatiquement \0 à la fin.
Or printf("%s") attend une chaîne C terminée par \0.
Si aucun \0 n’est présent dans le buffer, printf() continue à lire en mémoire après le buffer.
Conséquences possibles :
- lecture hors limites ;
- fuite d’information ;
- crash ;
- comportement indéfini.
Code: Select all
char buffer[4096];
ssize_t bytes = read(fd, buffer, sizeof(buffer) - 1);
if (bytes > 0)
{
buffer[bytes] = '\0';
}
10. Attention : les processus peuvent disparaître pendant l’énumération
Quand on parcourt /proc, le système continue de vivre.
Un processus peut :
- exister quand readdir() voit son dossier ;
- se terminer juste avant qu’on ouvre /proc/PID/status.
Code: Select all
int fd = open(path, O_RDONLY);
Il ne faut pas forcément considérer cela comme une erreur grave.
Il faut simplement faire :
Code: Select all
if (fd == -1)
continue;
le processus a peut-être disparu, on passe au suivant.
C’est un comportement normal en programmation système.
11. Attention : l’ordre de readdir() n’est pas garanti
Quand on parcourt un dossier avec readdir(), il ne faut jamais supposer que les entrées sont triées.
Dans /proc, on pourrait voir :
Code: Select all
1
2
3
245
1337
Le premier PID trouvé n’est pas obligatoirement 1.
Donc ce code :
Code: Select all
while ((entry = readdir(dir)) != NULL)
{
if (isdigit(entry->d_name[0]))
{
printf("%s\n", entry->d_name);
break;
}
}
le premier PID trouvé dans l’ordre retourné par le filesystem,
pas forcément :
Code: Select all
1
12. Attention : snprintf() peut tronquer
Exemple :
Code: Select all
char path[32];
snprintf(path, sizeof(path), "/proc/%s/status", entry->d_name);
Mais il peut tronquer la chaîne si le buffer est trop petit.
Donc le chemin pourrait devenir incomplet.
Version plus propre :
Code: Select all
char path[512];
int ret = snprintf(path, sizeof(path), "/proc/%s/status", entry->d_name);
if (ret < 0 || ret >= (int)sizeof(path))
{
continue;
}
13. Code complet : mini ps avec /proc
Ce programme parcourt /proc, détecte les PID, lit /proc/PID/status et affiche :
- PID ;
- PPID ;
- état ;
- nom du processus.
Code: Select all
open()
read()
close()
Code: Select all
fopen()
fgets()
fclose()
Code: Select all
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <dirent.h>
#include <unistd.h>
#include <fcntl.h>
int is_number(char *s)
{
int i = 0;
if (s == NULL || s[0] == '\0')
return 0;
while (s[i])
{
if (!isdigit((unsigned char)s[i]))
return 0;
i++;
}
return 1;
}
void get_value(char *buffer, char *key, char *out, int out_size)
{
char *pos = strstr(buffer, key);
if (pos == NULL)
return;
pos = pos + strlen(key);
while (*pos == ' ' || *pos == '\t')
pos++;
int i = 0;
while (*pos && *pos != '\n' && i < out_size - 1)
{
out[i] = *pos;
i++;
pos++;
}
out[i] = '\0';
}
int main()
{
DIR *dir = opendir("/proc");
if (dir == NULL)
{
printf("erreur opendir\n");
return 1;
}
struct dirent *entry;
printf("%-10s %-10s %-25s %-25s\n",
"PID",
"PPID",
"STATE",
"NAME");
while ((entry = readdir(dir)) != NULL)
{
if (!is_number(entry->d_name))
continue;
char path[512];
int ret = snprintf(path,
sizeof(path),
"/proc/%s/status",
entry->d_name);
if (ret < 0 || ret >= (int)sizeof(path))
continue;
int fd = open(path, O_RDONLY);
if (fd == -1)
continue;
char buffer[4096];
ssize_t bytes = read(fd, buffer, sizeof(buffer) - 1);
close(fd);
if (bytes <= 0)
continue;
buffer[bytes] = '\0';
char name[128] = "";
char pid[32] = "";
char ppid[32] = "";
char state[128] = "";
get_value(buffer, "Name:", name, sizeof(name));
get_value(buffer, "Pid:", pid, sizeof(pid));
get_value(buffer, "PPid:", ppid, sizeof(ppid));
get_value(buffer, "State:", state, sizeof(state));
printf("%-10s %-10s %-25s %-25s\n",
pid,
ppid,
state,
name);
}
closedir(dir);
return 0;
}
14. Compilation
Pour compiler :
Code: Select all
gcc main.c -o procinfo
Code: Select all
./procinfo
Code: Select all
PID PPID STATE NAME
1 0 S (sleeping) systemd
845 1 S (sleeping) bash
932 845 R (running) procinfo
15. Explication détaillée du code
15.1 Ouverture de /proc
Code: Select all
DIR *dir = opendir("/proc");
Si opendir() échoue :
Code: Select all
if (dir == NULL)
{
printf("erreur opendir\n");
return 1;
}
15.2 Parcours des entrées
Code: Select all
while ((entry = readdir(dir)) != NULL)
Cette entrée est représentée par :
Code: Select all
struct dirent *entry;
Code: Select all
entry->d_name
15.3 Filtrage des PID
Code: Select all
if (!is_number(entry->d_name))
continue;
Exemples ignorés :
Code: Select all
.
..
sys
net
self
thread-self
cpuinfo
meminfo
Code: Select all
1
2
245
1337
4242
15.4 Construction du chemin /proc/PID/status
Code: Select all
snprintf(path,
sizeof(path),
"/proc/%s/status",
entry->d_name);
Code: Select all
245
Code: Select all
/proc/245/status
15.5 Ouverture du fichier status
Code: Select all
int fd = open(path, O_RDONLY);
Si le processus a disparu entre temps :
Code: Select all
if (fd == -1)
continue;
15.6 Lecture du contenu
Code: Select all
ssize_t bytes = read(fd, buffer, sizeof(buffer) - 1);
Puis :
Code: Select all
buffer[bytes] = '\0';
15.7 Extraction des champs
Code: Select all
get_value(buffer, "Name:", name, sizeof(name));
get_value(buffer, "Pid:", pid, sizeof(pid));
get_value(buffer, "PPid:", ppid, sizeof(ppid));
get_value(buffer, "State:", state, sizeof(state));
Exemple :
Code: Select all
Name:
15.8 Affichage
Code: Select all
printf("%-10s %-10s %-25s %-25s\n",
pid,
ppid,
state,
name);
16. Limites du code
Ce code est volontairement pédagogique.
Il est suffisant pour comprendre l’énumération de processus, mais il a quelques limites.
- Il suppose que /proc/PID/status tient dans 4096 octets.
- Il parse du texte avec strstr(), ce qui est simple mais pas parfait.
- Il n’affiche pas encore la ligne de commande complète.
- Il ne trie pas les PID.
- Il ne gère pas tous les détails de permissions.
- Il ne lit pas les threads via /proc/PID/task.
- Il ne lit pas les mappings mémoire via /proc/PID/maps.
- Il ne lit pas les fd ouverts via /proc/PID/fd.
17. Améliorations possibles
Une fois cette base comprise, on peut ajouter beaucoup de fonctionnalités.
17.1 Afficher la ligne de commande
Lire :
Code: Select all
/proc/PID/cmdline
Il faut donc remplacer les \0 internes par des espaces pour afficher proprement.
17.2 Afficher l’exécutable réel
Lire le lien symbolique :
Code: Select all
/proc/PID/exe
Code: Select all
readlink()
17.3 Afficher le dossier courant
Lire :
Code: Select all
/proc/PID/cwd
Code: Select all
readlink()
17.4 Afficher les fd ouverts
Parcourir :
Code: Select all
/proc/PID/fd
Code: Select all
readlink()
17.5 Afficher les mappings mémoire
Lire :
Code: Select all
/proc/PID/maps
- les bibliothèques chargées ;
- la heap ;
- la stack ;
- les zones mmap ;
- les permissions mémoire ;
- les fichiers mappés.
17.6 Construire un arbre de processus
Avec les champs :
Code: Select all
Pid:
PPid:
Exemple :
Code: Select all
systemd
├── NetworkManager
├── sshd
└── bash
└── procinfo
Code: Select all
pstree
18. Ce qu’il faut retenir absolument
- /proc est un pseudo-système de fichiers généré par le kernel.
- Chaque processus possède un dossier /proc/PID.
- Les dossiers numériques dans /proc correspondent aux processus.
- /proc/PID/status contient des informations simples sur le processus.
- opendir() ouvre un dossier.
- readdir() retourne les entrées une par une.
- closedir() ferme le parcours.
- struct dirent décrit une entrée de dossier.
- entry->d_name contient le nom de l’entrée, pas le chemin complet.
- open() retourne un int fd.
- read() lit des octets bruts et n’ajoute pas \0.
- close() ferme le fd.
- snprintf() sert à construire un chemin en limitant la taille.
- Un processus peut disparaître pendant l’énumération, donc open() peut échouer.
- L’ordre de readdir() n’est pas garanti.
- Lire /proc, c’est lire une vue dynamique de l’état du système.
19. Résumé mental simple
Pour énumérer les processus sous Linux :
Code: Select all
1. J’ouvre /proc
2. Je parcours les entrées
3. Je garde les noms numériques
4. Chaque nom numérique est un PID
5. Je construis /proc/PID/status
6. J’ouvre le fichier
7. Je lis le texte généré par le kernel
8. Je parse Name, Pid, PPid, State
9. J’affiche les informations
20. Conclusion
L’énumération des processus avec /proc est une étape fondamentale du développement système Linux.
Ce cours montre une différence importante entre Linux et Windows.
Sous Windows, on passe souvent par des APIs spécialisées ou natives.
Sous Linux, beaucoup d’informations système sont exposées directement via des pseudo-fichiers.
Cela rend possible la création d’outils puissants avec des appels très simples :
Code: Select all
opendir()
readdir()
open()
read()
close()
- mini ps ;
- mini pstree ;
- mini lsof ;
- analyseur de mappings mémoire ;
- énumérateur de threads ;
- outil de monitoring ;
- outil forensic Linux usermode.
Sous Linux, beaucoup d’outils système ne font pas de magie : ils lisent simplement /proc proprement et interprètent les données exposées par le kernel.
