14Exceptions et processus exceptionnels de flux de contrôle (Exceptions et processus exceptionnels de flux de contrôle)

Flux de contrôle des exceptions

Lorsqu'un flux de contrôle anormal se produit :
un flux de contrôle exceptionnel (ECF) est un changement dans le flux de contrôle provoqué par certains événements ou conditions spéciaux pendant l'exécution du programme. Un flux de contrôle anormal se produit généralement dans les situations suivantes :

  • Exceptions et interruptions matérielles : les exceptions matérielles sont des conditions d'erreur potentielles détectées par le processeur, telles qu'une division par zéro, l'accès à une adresse mémoire illégale ou un débordement de virgule flottante. Une interruption matérielle est un signal envoyé par un périphérique externe (tel qu'une souris, un clavier ou une carte d'interface réseau) pour informer le processeur d'un événement à traiter. Dans ces cas, le processeur suspend la séquence d'instructions en cours d'exécution et exécute à la place un gestionnaire d'exceptions ou un gestionnaire d'interruption prédéfini.
  • Noyau du système d'exploitation : le noyau du système d'exploitation peut modifier le flux de contrôle lorsqu'il répond aux appels système. Par exemple, lorsqu'un processus émet un appel système pour lire un fichier, le système d'exploitation peut avoir besoin de faire passer le flux de contrôle du processus utilisateur au mode noyau, de gérer la demande de lecture du fichier, puis de rebasculer le flux de contrôle vers le processus utilisateur. .
  • Signal : un signal est un flux de contrôle d'exception logicielle utilisé pour transmettre des notifications entre les processus. Lorsqu'un processus reçoit un signal, le système d'exploitation interrompt l'exécution du processus en cours et exécute à la place la fonction de traitement du signal associée au signal. Les signaux peuvent être envoyés par d'autres processus ou générés par le système d'exploitation, par exemple lorsqu'un processus effectue une opération illégale (telle que l'accès à une adresse mémoire illégale).
  • Instructions de gestion des exceptions : dans les langages de programmation de haut niveau, le flux de contrôle des exceptions est généralement représenté par des instructions de gestion des exceptions (telles que les instructions try-catch-finally). Les programmeurs peuvent utiliser ces instructions pour intercepter et gérer les exceptions pouvant survenir au moment de l'exécution, telles que les erreurs de lecture de fichiers, les références de pointeurs nuls ou les interruptions de connexion réseau.
  • Synchronisation et planification des threads : dans un environnement multithread, la planification et la synchronisation des threads peuvent également conduire à un flux de contrôle anormal. Lorsqu'un thread attend qu'un autre thread libère un mutex ou une ressource, le planificateur de thread peut basculer le flux de contrôle vers l'autre thread jusqu'à ce que la ressource soit disponible. Dans ce cas, le fluide de contrôle des exceptions change désormais les threads et les synchronise.
    Résolution des problèmes de flux de contrôle des exceptions :
    le flux de contrôle des exceptions (Exception Control Flow) est un mécanisme permettant de gérer les événements anormaux pendant l'exécution du programme. Les événements d'exception sont des situations inhabituelles ou inattendues qu'un programme peut rencontrer pendant son fonctionnement, telles qu'une division par zéro, un tableau hors limites, des erreurs de lecture de fichier, etc. Afin de garantir que le programme puisse fonctionner de manière plus stable, les développeurs doivent détecter et gérer ces exceptions via un flux de contrôle des exceptions.
    Avantages du flux de contrôle des exceptions dans la résolution des problèmes :
    (1) Améliorer la stabilité du programme : en détectant et en gérant les exceptions, le programme peut continuer à s'exécuter lorsqu'une erreur se produit, au lieu de planter directement.
    (2) Améliorer la maintenabilité du programme : la gestion centralisée des exceptions rend le code plus facile à lire et à maintenir, aidant ainsi à identifier les problèmes potentiels.
    (3) Améliorer l'expérience utilisateur : la gestion des exceptions permet au programme de fournir aux utilisateurs des informations d'erreur plus conviviales et plus détaillées lorsqu'une erreur se produit.

définition d'exception

L'anomalie fait généralement référence à une situation ou à un événement différent de la normale, inattendu ou inhabituel. En programmation informatique, une exception fait généralement référence à une erreur ou à une condition inattendue qui se produit lors de l'exécution d'un programme, telle qu'une panne du système, une perte de données ou une erreur de saisie.

tableau des exceptions

Le tableau des exceptions fait référence à un tableau des situations anormales pouvant survenir lors de l'exécution d'un programme informatique et de leurs méthodes de traitement correspondantes. Les exceptions incluent généralement les aspects suivants :

  • Type d'exception : répertoriez les types d'exceptions qui peuvent survenir, tels qu'une exception de pointeur nul, une exception de tableau hors limites, une exception de fichier introuvable, etc.
  • Informations sur les exceptions : décrivez chaque type d'exception en détail, y compris la cause de l'exception et les scénarios possibles.
  • Méthode de gestion : pour chaque type d'exception, indiquez comment le programme doit gérer l'exception, par exemple intercepter l'exception et fournir des informations d'invite, relancer l'exception, journaliser, etc.
  • Exemples de code : fournissez du code qui montre comment intercepter et gérer les exceptions.

La table d'exceptions est un outil très important pour les programmeurs dans le processus de développement. Elle peut les aider à identifier et gérer les exceptions et à garantir la stabilité et la robustesse du programme.

Méthodes de classification anormales

Les exceptions peuvent être classées de différentes manières, notamment :

  • Selon le type d'exception : comme une exception d'exécution, une exception de vérification, une exception système, etc.
  • Selon la méthode de gestion des exceptions : comme intercepter les exceptions, lancer des exceptions, gérer les exceptions, etc.
  • Selon le moment où l'exception se produit : comme une exception synchrone, une exception asynchrone, etc.

Les exceptions synchrones et les exceptions asynchrones sont classées en fonction du moment où elles surviennent. Les exceptions synchrones font référence aux erreurs et aux exceptions qui se produisent lors de l'exécution du programme, provoquant l'arrêt du programme et l'émission d'exceptions, qui doivent être traitées immédiatement. Par exemple, si le dénominateur d'une opération de division est zéro, le programme s'arrêtera lorsqu'il atteindra ce point et générera un message d'exception. L'exception doit être gérée immédiatement. L'exception asynchrone fait référence à la situation dans laquelle une erreur ou une exception se produit pendant l'exécution du programme, mais le programme peut toujours continuer à s'exécuter. Les informations sur l'exception ne seront pas générées immédiatement, mais seront automatiquement traitées par le système ou le programme à un certain moment. moment. Par exemple, une requête réseau peut provoquer une exception asynchrone en raison de problèmes de connexion réseau, et le programme tentera de renvoyer ou attendra une réponse réseau avant d'envoyer.

Les interruptions sont une autre façon de gérer les exceptions. Une interruption est une exception asynchrone. Cela signifie que lorsque l'ordinateur traite une certaine tâche, un événement sans rapport avec la tâche se produit, tel qu'une panne matérielle ou une entrée de l'utilisateur. L'ordinateur suspend la tâche en cours, passe au traitement de l'événement, puis revenez à la tâche d'origine. Les interruptions sont généralement initiées par des périphériques matériels ou des programmes au sein du système et peuvent être gérées par des gestionnaires d'interruptions. La méthode de traitement des interruptions permet à l'ordinateur de gérer plusieurs tâches en même temps, améliorant ainsi l'efficacité et la fiabilité du système informatique.

Exceptions, interruptions et appels système synchrones

Les exceptions synchrones, les interruptions et les appels système sont trois concepts importants du système d'exploitation. Ils sont tous liés au processus d'exécution des programmes informatiques.

Une exception synchrone signifie qu'une erreur ou une situation anormale se produit lors de l'exécution du programme, provoquant l'arrêt de l'exécution du programme et l'émission d'une exception qui doit être traitée immédiatement. Par exemple, diviser par zéro, accéder à des adresses mémoire illégales, etc. provoquera des exceptions de synchronisation. Les exceptions de synchronisation sont généralement automatiquement détectées et gérées par le matériel ou le logiciel. Par exemple, le système d'exploitation enverra un signal au processus pour l'informer qu'une exception s'est produite, et le processus doit la gérer en conséquence en fonction du type de signal.

L'appel système est l'interface par laquelle les programmes utilisateur demandent des services au système d'exploitation, et c'est également l'interface par laquelle le système d'exploitation fournit des services externes. Les programmes utilisateur doivent effectuer certaines opérations, telles que la lecture et l'écriture de fichiers, la création de processus, les communications réseau, etc. Mais ces opérations doivent être complétées par le système d'exploitation. Par conséquent, le programme utilisateur doit lancer une requête au système d'exploitation via un appel système, permettant au système d'exploitation d'exécuter les services correspondants au nom du programme utilisateur. Les appels système sont généralement mis en œuvre via des interruptions logicielles, c'est-à-dire que le programme utilisateur passe à l'état du noyau via des instructions d'interruption, puis exécute les instructions d'appel système correspondantes. Le système d'exploitation termine les services correspondants dans l'état du noyau et renvoie les résultats au programme utilisateur.

En général, les exceptions de synchronisation, les interruptions et les appels système sont des mécanismes d'exception et des méthodes d'appel lors de l'exécution de programmes informatiques et jouent un rôle important dans le système d'exploitation.

Défauts, pages manquantes, défauts de protection

Les erreurs, les pages manquées et les erreurs de protection sont des types d'exceptions courants trouvés dans les systèmes d'exploitation informatiques.
Les erreurs font référence à des exceptions qui se produisent lors de l'exécution d'un programme, telles que des opérandes illégaux, des instructions illégales, un accès à la mémoire illégal, etc. Ces exceptions sont généralement causées par une détection matérielle et le système d'exploitation doit gérer ces exceptions. Les défauts sont différents des erreurs. Les erreurs sont généralement causées par des problèmes de logique ou de conception du programme, tels qu'une division par 0, un débordement de pile, etc.

Page manquante fait référence à une situation anormale qui se produit lorsque la page à laquelle le programme doit accéder n'est pas dans la mémoire. Le système d'exploitation chargera la page à laquelle il faut accéder à partir du disque dans la mémoire, puis laissera le programme accéder à la page. page. Si la page n'est pas modifiée dans la mémoire, une exception de page manquante sera levée. Le système d'exploitation doit lire la page du disque vers la mémoire et mettre à jour les structures de données telles que les tables de pages afin que les programmes puissent accéder à la page.

Une erreur de protection se produit lorsqu'un programme tente d'accéder à des ressources protégées, telles que des ressources en mode noyau ou la mémoire d'autres processus, sans obtenir les autorisations suffisantes. Cette situation entraînera une exception. Le système d'exploitation doit vérifier les autorisations d'accès au programme, et si les autorisations sont insuffisantes, un échec de protection se produira.

En général, les erreurs, les pages manquées et les erreurs de protection sont des types courants d'exceptions dans les systèmes d'exploitation. Le système d'exploitation doit détecter et gérer ces exceptions pour garantir la stabilité et la sécurité du système.
Voici un exemple de référence mémoire non valide, en supposant que nous ayons un pointeur vers un tableau d'entiers, mais que le pointeur pointe vers une adresse mémoire non allouée :

#include <stdio.h>
#include <stdlib.h>
int main() {
    int *ptr;
    *ptr = 10; // 无效内存引用
    printf("%d\n", *ptr);
    return 0;
}

Dans ce programme, nous déclarons un pointeur ptr vers un tableau d'entiers mais ne lui allouons aucun espace mémoire. Ensuite, nous essayons d'attribuer l'entier 10 à l'adresse mémoire pointée par le pointeur, ce qui provoque une erreur de référence mémoire non valide. Enfin, le programme tente de lire la valeur dans l'adresse mémoire pointée par le pointeur et de la sortir, mais comme cette adresse mémoire n'est pas allouée, le programme plantera et se fermera.
Pour éviter ce type d'échec, nous devons nous assurer que le pointeur pointe toujours vers l'adresse mémoire allouée et initialiser le pointeur avant de l'utiliser. Ce type de panne peut être évité en allouant une section de mémoire sur le tas à l'aide de la fonction malloc(), par exemple :

#include <stdio.h>
#include <stdlib.h>
int main() {
    int *ptr = (int *)malloc(sizeof(int));
    *ptr = 10; // 正确的内存引用
    printf("%d\n", *ptr);
    free(ptr);
    return 0;
}

Dans ce programme, nous utilisons la fonction malloc() pour allouer un espace mémoire de type int sur le tas, et pointons le pointeur ptr vers l'adresse mémoire. Ensuite, nous attribuons l'entier 10 à l'adresse mémoire et affichons la valeur. Enfin, nous utilisons la fonction free() pour libérer de l'espace mémoire. Cela évite les échecs de références de mémoire non valides

Exemple d'échec, référence mémoire invalide

Une erreur fait référence à une situation dans laquelle un programme tente d'effectuer une opération non valide ou d'accéder à une adresse mémoire non valide, ce qui amène le système d'exploitation à envoyer un signal d'exception au programme. Une référence mémoire non valide est également un échec courant qui se produit lorsqu'un programme tente d'accéder à une adresse mémoire non allouée ou à une adresse mémoire libérée.

Résiliation

La résiliation fait référence au processus dans lequel un programme ou un processus se termine ou est forcé de s'arrêter.

A la fin de l'exécution du programme, celui-ci passe par plusieurs étapes. Tout d'abord, le système d'exploitation charge le programme en mémoire et lui alloue des ressources ou des autorisations. Le programme commence alors son exécution jusqu'à ce qu'il termine sa tâche ou rencontre une exception. Si l'exécution du programme est terminée, il se terminera et se terminera automatiquement, libérant les ressources et autorisations occupées. Si le programme rencontre une situation anormale, telle qu'une erreur ou une exception non gérée, le programme se terminera et se fermera automatiquement.

La fin du processus peut être divisée en fin normale et fin anormale. La terminaison normale signifie généralement que le processus termine ses tâches et se termine, et que le système d'exploitation récupérera les ressources et les autorisations du processus. Une terminaison anormale signifie que le processus rencontre une situation anormale qui ne peut pas être gérée, comme l'accès à une adresse mémoire illégale, une division par zéro, etc. Le système d'exploitation doit terminer de force le processus et récupérer des ressources pour assurer la stabilité et la sécurité du système. .

De manière générale, la résiliation est le processus par lequel un programme ou un processus se termine ou est arrêté de force. C'est une partie très importante du système d'exploitation. Le système d'exploitation doit assurer l'arrêt normal des programmes et des processus et récupérer rapidement les ressources et les autorisations qu'ils occupent pour assurer la stabilité et l'efficacité du système.

exit
Sous Unix/Linux, un processus peut se terminer en appelant l'appel système exit(). La fonction exit() effectuera certaines opérations de nettoyage lorsque le processus se terminera et transmettra l'état de sortie du processus à son processus parent. L'état de sortie d'un processus est généralement utilisé pour indiquer la raison de la sortie ou le résultat de l'exécution du processus.
En langage C, la fonction exit() est définie dans le fichier d'en-tête stdlib.h, et son prototype est le suivant :

void exit(int status);

Parmi eux, status est un entier représentant l’état de sortie du processus. Si l'état est 0, cela signifie que le processus s'est terminé normalement. D'autres statuts de sortie peuvent être utilisés pour représenter des codes d'erreur et d'autres informations.

Il convient de noter que la fonction exit() ne terminera pas le processus directement, mais transmettra l'état de sortie du processus à son processus parent, et le processus parent décidera s'il doit terminer le processus. Si le processus parent n'attend pas l'état de sortie du processus enfant, le processus enfant peut devenir un "processus zombie" et vous devez utiliser des fonctions telles que wait() ou waitpid() pour attendre l'état de fin de l'enfant. traiter et recycler ses ressources.
Voici un exemple de programme simple qui montre comment utiliser la fonction exit() pour terminer un processus :

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

int main() {
    printf("before exit()\n");
    exit(0);  // 终止进程,并返回状态码0
    printf("after exit()\n");  // 这行代码不会被执行
    return 0;
}

Dans cet exemple de programme, lorsque la fonction exit() est appelée, le processus se termine immédiatement et renvoie le code d'état 0. Par conséquent, la sortie de la fonction printf() est limitée à « avant exit() » et « après exit() » ne sera pas exécutée.

Appels système couramment utilisés sous Linux/x86-64

Linux/x86-64 est un système d'exploitation et une architecture informatique courants qui fournissent de nombreux appels système que les programmes utilisateur peuvent utiliser. Les appels système suivants sont couramment utilisés sous Linux/x86-64 :

read() : lit les données du descripteur de fichier.
write() : Écrit des données dans le descripteur de fichier.
open() : ouvre un fichier.
close() : ferme un fichier.
create() : crée un fichier.
unlink() : supprime un fichier.
mkdir() : crée un répertoire.
rmdir() : Supprime un répertoire.
chdir() : modifie le répertoire de travail actuel.
getpid() : obtient l'ID de processus du processus actuel.
fork() : crée un nouveau processus.
execve() : Exécute un nouveau programme.
wait() : Attendez la fin d'un processus enfant.
pipe() : crée un tuyau.
dup() : dupliquer un descripteur de fichier.
select() : attend les événements d'E/S sur un ensemble de descripteurs de fichiers.
socket() : crée une socket.
bind() : lier un socket à une adresse.
Listen() : écoute une socket.
accept() : Accepte une connexion client.
connect() : Connectez-vous à un hôte distant.

Ces appels système peuvent être appelés via des fonctions de bibliothèque standard du langage C ou des fonctions de bibliothèque d'appels système. Ils fournissent un ensemble de services de base du système d'exploitation grâce auxquels les programmes utilisateur peuvent effectuer des opérations sur les fichiers, la gestion des processus, les communications réseau et d'autres fonctions.

appel système、ouvrir

syscall est un mécanisme d'appel système fourni par le système d'exploitation Linux, qui permet aux programmes utilisateur d'appeler directement des fonctions dans le noyau du système d'exploitation. Sous Linux/x86-64, l'instruction syscall est utilisée pour déclencher un appel système. Cette instruction transmettra le numéro d'appel système et les paramètres au noyau du système d'exploitation. Le noyau effectuera les opérations correspondantes en fonction du numéro d'appel système et des paramètres et retournera les résultats au programme utilisateur.
open est un appel système couramment utilisé pour ouvrir un fichier. Sous Linux/x86-64, le numéro d'appel système de l'appel système ouvert est 2, qui a trois paramètres :
(1) const char *pathname : le nom de chemin du fichier à ouvrir.
(2) int flags : la méthode et les bits d'indicateur pour ouvrir le fichier, tels que O_RDONLY, O_WRONLY, O_RDWR, etc.
(3) mode mode_t : les autorisations du fichier, par exemple 0666, signifient que le fichier est lisible et inscriptible.

Le programme utilisateur peut appeler l'appel système ouvert via la fonction open() de la bibliothèque standard du langage C, par exemple :

#include <fcntl.h>
int open(const char *pathname, int flags, mode_t mode);

Cette fonction renvoie un descripteur de fichier pour les opérations ultérieures de lecture et d'écriture de fichiers. Par exemple, pour ouvrir un fichier nommé test.txt et écrire des données, vous pouvez écrire le programme comme suit :

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
    int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (fd == -1) {
        perror("open");
        return 1;
    }
    write(fd, "Hello, World!", 13);
    close(fd);
    return 0;
}

Le programme a ouvert le fichier test.txt à l'aide de la fonction open() et l'a ouvert en mode écriture. Si l'ouverture du fichier échoue, le programme imprime un message d'erreur et se ferme. Si l'ouverture réussit, le programme écrira la chaîne "Hello, World!" dans le fichier via la fonction write() et fermera le descripteur de fichier.

processus

Un processus est une instance d’un programme exécuté sur un ordinateur. Dans le système d'exploitation, le processus est l'unité de base de l'allocation et de la planification des ressources et peut également être considéré comme l'exécution séquentielle de programmes sur l'ordinateur. Un processus peut inclure plusieurs threads, chaque thread effectuant une partie des tâches du programme.

Le processus possède son propre espace mémoire, son jeu de registres, son compteur de programme, ses fichiers ouverts et d'autres ressources système. Ils sont indépendants les uns des autres et n'interfèrent pas les uns avec les autres. Le système d'exploitation gère l'exécution des processus via des algorithmes de planification de processus et alloue des tranches de temps CPU pour réaliser l'exécution simultanée de plusieurs tâches.

Chaque processus possède un identifiant de processus unique (Process Identifier, PID), qui est utilisé pour distinguer les différents processus. Lorsqu'un programme est démarré, le système d'exploitation crée un nouveau processus pour celui-ci et lui attribue un PID unique.

L'état d'un processus peut être divisé en trois types : En cours d'exécution, Prêt et Bloqué. Lorsque le processus est en cours d'exécution, il est à l'état d'exécution ; lorsqu'il attend l'apparition de certains événements, tels que l'attente d'entrées et de sorties, l'attente d'une allocation de ressources, etc., il est à l'état de blocage ; lorsque le processus Le processus est prêt à s'exécuter, mais n'a pas encore obtenu la tranche de temps CPU, il est dans un état prêt.

Les processus peuvent communiquer et partager des données via des mécanismes de communication inter-processus (tels que des canaux, une mémoire partagée, des files d'attente de messages, etc.). Dans le même temps, le système d'exploitation fournit également certains appels système pour la gestion des processus, tels que la création de processus, la destruction de processus, l'attente de la fin du processus, etc.

Insérer la description de l'image ici
Insérer la description de l'image ici
Détails du statut :

  • État d'exécution ---- Lorsqu'un processus est affecté au processeur et démarre son exécution, il est dans l'état d'exécution. À ce stade, le processus exécute des instructions, utilise les ressources du processeur et peut rivaliser avec d'autres processus pour les tranches de temps du processeur. À l'état d'exécution, un processus peut effectuer toute opération pouvant être effectuée en mode utilisateur ou noyau, telle que la lecture et l'écriture de fichiers, l'envoi de requêtes réseau.
  • État bloqué ----- Lorsqu'un processus attend que certains événements se produisent et ne peut pas continuer son exécution, il est dans un état bloqué. Par exemple, lorsqu'un processus attend la fin d'une opération d'E/S, attend un verrou ou un sémaphore, attend qu'un autre processus envoie un message, etc., il est dans un état bloquant. Dans l'état de blocage, le processus n'utilise pas de ressources CPU et n'est pas en concurrence pour les tranches de temps CPU.
  • État de fin------Lorsqu'un processus termine ses tâches d'exécution et se termine, il est dans l'état de fin. Dans cet état, le processus libère toutes les ressources qu'il utilise, y compris le temps CPU, la mémoire, les descriptions de fichiers, les verrous, etc. .Les informations du processus terminé seront laissées dans la table des processus pour que le processus parent ou d'autres processus puissent afficher les informations sur l'état de sortie du processus.
    Ces trois états sont les états de base du processus dans le système d'exploitation, et leur conversion et La gestion est un processus du système d'exploitation qui constitue un élément clé de la planification et de la gestion des ressources.

Deux abstractions de processus

  • CPU et registres exclusifs
    Les deux abstractions d'un processus font référence aux deux concepts abstraits les plus fondamentaux d'un processus dans le système d'exploitation, qui sont l'état d'exécution du processus et l'espace d'adressage du processus.
    La première abstraction signifie que le processus occupe exclusivement le CPU et les registres pendant l'exécution et peut être considéré comme une unité d'exécution indépendante. Lorsque le processus est en cours d'exécution, il occupera la tranche de temps du processeur et une exécution simultanée multitâche peut être réalisée en changeant de processeur. Pendant l'exécution du processus, le processeur et les registres sont exclusivement occupés par le processus. Le processus peut utiliser ces ressources pour exécuter son propre code, lire et écrire ses propres données.

  • Avoir son propre espace d'adressage
    La deuxième abstraction est que chaque processus possède son propre espace d'adressage, également appelé espace d'adressage virtuel, comprenant le code, les données, la pile, etc. Chaque processus a son propre code, ses données, sa pile et d'autres contenus lors de son exécution, et l'espace d'adressage de chaque processus est indépendant les uns des autres et n'interfère pas les uns avec les autres. Le système d'exploitation utilise un mécanisme de gestion de mémoire virtuelle pour allouer et protéger l'espace d'adressage, permettant à chaque processus de s'exécuter indépendamment, garantissant ainsi la sécurité et la stabilité du système.

L'espace d'adressage d'un processus est virtuel, c'est-à-dire que le processus pense avoir un accès exclusif à toute la mémoire physique, mais en réalité il ne peut accéder qu'à une partie de la mémoire qui lui est allouée par le système d'exploitation. En effet, les systèmes d'exploitation modernes adoptent la technologie de mémoire virtuelle, qui divise la mémoire physique en plusieurs pages de taille égale. Chaque page possède une adresse virtuelle et une adresse physique. Le système d'exploitation gère la relation entre les adresses virtuelles et les adresses physiques via une table de mappage. relation correspondante. Chaque processus possède sa propre table de mappage, de sorte que le processus ne peut accéder qu'à son propre espace d'adressage lors de l'accès à la mémoire.

L'espace d'adressage d'un processus comprend généralement les parties suivantes :
1. Segment de code : stocke le code exécutable du programme.
2. Segment de données : stocke les variables globales et les variables statiques définies dans le programme
3. Heap : stocke la mémoire allouée dynamiquement, telle que la mémoire allouée à l'aide de la fonction malloc
4. Pile : stocke les variables locales et les paramètres de fonction dans les compositions d'appels de fonction

Commande Top pour afficher les processus
Dans les systèmes Linux, la commande top peut être utilisée pour surveiller l'état des processus et l'utilisation des ressources dans le système en temps réel. Elle fournit une interface de terminal interactive qui affiche l'utilisation du processeur, l'utilisation de la mémoire et la durée d'exécution de chaque processus et d’autres informations.

Pour utiliser la commande top, tapez top dans le terminal et appuyez sur Entrée. Dans l'interface de la commande top, vous pouvez voir une liste de tous les processus en cours d'exécution dans le système, chaque processus occupant une ligne. Chaque ligne affiche le PID (identifiant de processus) du processus, l'utilisateur auquel appartient le processus, l'utilisation du processeur par le processus, l'utilisation de la mémoire et d'autres informations.

Dans l'interface de commande supérieure, vous pouvez utiliser certaines touches de raccourci pour effectuer certaines opérations, telles que :
P : trier par utilisation du processeur ;
M : trier par utilisation de la mémoire ;
k : arrêter un processus ;
H : afficher les informations sur le thread.

De plus, vous pouvez également utiliser la commande top -p pour afficher les informations de processus du PID spécifié. Par exemple, pour afficher des informations sur le processus avec le PID 123, saisissez top -p 123 dans le terminal.

Partage de gestion multi-processus, le changement de contexte est le changement de l'espace d'adressage et des registres.
Dans un système d'exploitation, plusieurs processus peuvent s'exécuter en même temps. Ces processus peuvent devoir partager certaines ressources, telles que la mémoire et les fichiers. Afin d'assurer la sécurité et l'indépendance entre les processus, le système d'exploitation adopte un mécanisme de gestion multi-processus.

Dans le cadre du mécanisme de gestion multi-processus, chaque processus possède son propre espace d'adressage et ses propres registres indépendants. L'espace d'adressage fait référence à l'espace mémoire que le processus peut utiliser, y compris le code\données\pile, etc. utilisé Il est utilisé pour stocker les données nécessaires au CPU pour exécuter les instructions.

Lorsque le système d'exploitation doit changer de processus, il enregistre l'espace d'adressage et enregistre l'état du processus en cours. Il charge ensuite l'espace d'adressage et enregistre l'état du nouveau processus. Ce processus est appelé changement de contexte. Dans le changement de contexte, le système d'exploitation doit faire deux choses :
(1) Enregistrer l'état du processus en cours. Le système d'exploitation doit enregistrer l'espace d'adressage actuel et l'état du registre afin qu'il puisse ensuite être restauré à l'état d'exécution du processus en cours. (2)
Charger l'état du nouveau processus. Le système d'exploitation doit charger le nouveau processus. L'espace d'adressage et l'état du registre du nouveau processus afin que le processeur puisse commencer à exécuter le code du nouveau processus.

Dans le cadre du mécanisme de gestion multi-processus, l'espace d'adressage et l'état des registres entre les différents processus sont indépendants les uns des autres. Cela garantit que les données entre les processus n'interféreront pas les unes avec les autres, améliorant ainsi la sécurité et la stabilité du système. Cependant, en raison de changement de contexte Une grande quantité d'informations d'état doit être enregistrée et restaurée, ce qui entraînera une certaine surcharge de performances.

Le processus de changement de contexte
Le changement de contexte fait référence au partage des ressources CPU par plusieurs processus ou threads dans le système d'exploitation. Lorsqu'un processus ou un thread doit abandonner des ressources CPU, le système d'exploitation doit enregistrer les informations contextuelles du processus ou du thread actuel, passer aux informations contextuelles du processus ou du thread suivant, puis redémarrer le CPU pour exécuter le nouveau processus. ou du fil.

Le processus de changement de contexte comprend généralement les étapes suivantes :
(1) Le processus ou le thread actuel effectue une opération qui nécessite d'attendre qu'un événement externe se produise, comme une opération d'E/S, ou effectue une opération à la fin d'un temps. tranche et doit abandonner les ressources du processeur. Le système déclenchera un changement de contexte.
(2) Le système d'exploitation enregistrera les informations contextuelles du processus ou du thread actuel, y compris la valeur du registre CPU, du compteur de programme, du pointeur de pile, etc. Ces informations sont enregistrées dans le bloc de contrôle du processus ou du thread (PCB/TCB). du noyau du système d’exploitation.
(3) Le système d'exploitation sélectionnera le prochain processus ou thread à exécuter en fonction de l'algorithme de planification de processus ou de thread, et chargera ses informations contextuelles dans le processeur.
(4) Le système d'exploitation confiera le contrôle du processeur au processus ou au thread suivant, et le processeur commencera à exécuter le nouveau processus ou le nouveau thread.
(5) Si le processus ou le thread précédemment suspendu est toujours à l'état prêt, il sera de nouveau ajouté à la file d'attente exécutable pour attendre la prochaine planification.
  Le changement de contexte est une opération très gourmande en ressources, car le processus de sauvegarde et de restauration d'un contexte de processus ou de thread nécessite des opérations de lecture et d'écriture en mémoire ainsi que des opérations de registre CPU, qui consomment du temps et des ressources CPU. Par conséquent, pour les applications nécessitant des changements fréquents, le changement de contexte peut devenir un goulot d'étranglement et affecter les performances du système.

** Fonctions de contrôle de processus, getpid, getppid :** Sous Unix/Linux, vous pouvez utiliser la fonction getpid pour obtenir l'ID de processus du processus actuel et la fonction getppid pour obtenir l'ID de processus du processus parent du processus actuel. . Ces deux fonctions constituent l'un des éléments de base de la fonction de contrôle des processus et peuvent nous aider à identifier et à contrôler le processus.

flux de contrôle logique

Le flux de contrôle logique fait référence à l'ordre et au processus qu'un programme suit lors de son exécution. Dans un programme, il peut y avoir plusieurs instructions et opérations, et la relation et la séquence entre ces instructions et opérations constituent le flux de contrôle logique du programme. Le flux de contrôle logique d'un programme se compose généralement des structures suivantes :
(1) Structure séquentielle : le programme est exécuté séquentiellement dans l'ordre des instructions sans aucune branche ni boucle. Il s’agit de la structure de programme la plus simple et de la base de toutes les structures de programme.
(2) Structure de branche : le programme exécute différentes instructions selon les conditions. Les structures de branches sont généralement implémentées par des instructions if et des instructions switch.
(3) Structure de boucle : le programme exécute à plusieurs reprises une section de code en fonction des conditions de boucle. Les structures de boucle sont généralement implémentées avec des instructions while, for et do-while.
(4) Structure de saut : le programme peut sauter vers un autre emplacement à n'importe quel endroit pour continuer l'exécution. Les structures de saut sont généralement implémentées par des instructions goto, mais leur utilisation n'est pas recommandée dans la programmation réelle.
Ces structures de programme peuvent être combinées pour former des flux de programmes plus complexes. Par exemple, vous pouvez imbriquer une structure de boucle dans une structure de branche ou utiliser une structure de branche dans une structure de boucle. Dans la programmation réelle, le choix de la combinaison appropriée de structures de programme peut améliorer l'efficacité et la lisibilité du programme.

concurrent

La simultanéité signifie que plusieurs tâches ou processus s'exécutent en même temps. Dans les systèmes d'exploitation, il est souvent nécessaire de traiter plusieurs tâches ou processus en même temps, ce qui nécessite l'utilisation d'une mémoire concurrente.

La technologie de concurrence comprend des étapes telles que la programmation multi-processus, multi-thread et asynchrone. Le multi-processus fait référence à l'exécution simultanée de plusieurs processus indépendants dans le système d'exploitation. Chaque processus a son propre espace d'adressage et son propre état de registre. Le multithreading signifie exécuter plusieurs processus indépendants simultanément dans un seul processus. Chaque thread a son propre état de pile et de registre, mais partage l'espace d'adressage du processus. La programmation asynchrone fait référence à l'utilisation de rappels asynchrones, de boucles d'événements et d'autres technologies pour gérer plusieurs tâches dans un seul thread tout en garantissant la vitesse de réponse et l'utilisation des ressources.

Avantages de la technologie de concurrence : elle peut améliorer la vitesse de réponse et l'utilisation des ressources du système. Plusieurs tâches peuvent être exécutées en même temps, améliorant ainsi l'efficacité et la fiabilité du système. Cependant, la technologie de concurrence pose également certains problèmes, tels que des problèmes de sécurité des threads, des problèmes de blocage, des conditions de concurrence, etc. Afin d'éviter ces problèmes, vous devez utiliser certaines bonnes pratiques et modèles de conception de programmation simultanée, tels que l'utilisation de mécanismes de verrouillage et l'évitement des données partagées.

Mode utilisateur et mode noyau

Le mode utilisateur et le mode noyau sont deux modèles d'exécution différents dans le système d'exploitation.

Lorsqu'un programme s'exécute en mode utilisateur, il ne peut accéder qu'à des ressources restreintes, comme lui-même dans l'espace d'adressage du processus, mais ne peut pas accéder directement aux ressources système, telles que les périphériques matériels et le code du noyau. Lorsqu'un programme doit accéder aux ressources système, il doit passer en mode noyau via des appels système et le noyau effectue les opérations associées. Le mode utilisateur est un mécanisme de sécurité qui empêche les erreurs d'application ou les codes malveillants de causer des dommages au système.

Le mode noyau est également appelé mode privilégié ou mode système, car dans ce mode, le système d'exploitation a un contrôle total sur les ressources système. Le code en mode noyau peut accéder directement à tous les périphériques matériels et à l'espace mémoire et exécuter des instructions privilégiées. Tels que la définition de la table des vecteurs d'interruption et l'état de fonctionnement du processeur. Étant donné que le code en mode noyau peut accéder aux ressources système, l'exactitude et la sécurité du code du noyau doivent être assurées.

Lorsqu'un processus lance un appel système, il passe du mode utilisateur au mode noyau. Le système effectuera les opérations correspondantes en mode noyau et renverra les résultats au processus en mode utilisateur. Ce processus est appelé changement de contexte et constitue également un mécanisme courant dans les systèmes d'exploitation. Parce que le changement de contexte implique un changement d’état du processeur et un changement d’accès à la mémoire. Par conséquent, sa surcharge est relativement importante et le nombre de changements de contexte doit être réduit autant que possible pour améliorer les performances du système.

Le rôle des interruptions dans les transitions d'état :
Lorsqu'une application s'exécute dans l'espace utilisateur, elle ne peut accéder qu'à son propre espace mémoire et aux ressources limitées fournies par le CPU. Si une application doit accéder aux ressources de l'espace du noyau, telles que les pilotes de périphérique ou les services système, elle doit accéder à l'espace du noyau via un appel système. En effet, l'espace du noyau contient toutes les ressources système et les périphériques matériels, et il dispose d'autorisations plus élevées et de plus de privilèges.

Dans les systèmes d'exploitation modernes, l'espace utilisateur et l'espace noyau sont complètement isolés et ne peuvent pas accéder directement à l'espace mémoire de l'autre. Par conséquent, lorsqu'une application doit accéder à l'espace du noyau, elle doit utiliser des opérations d'interruption. Une interruption est un mécanisme qui met en pause la tâche en cours pendant que le processeur exécute des instructions en réponse à un événement matériel ou à une demande logicielle. Lorsqu'une application appelle un appel système, elle déclenche une interruption, transférant le contrôle du processeur à l'espace du noyau. Le noyau effectue les opérations requises et renvoie les résultats à l'application, lui redonnant ainsi le contrôle.

Par conséquent, en interrompant les opérations, les applications peuvent accéder en toute sécurité à l'espace du noyau sans compromettre la sécurité et la stabilité du système. Dans le même temps, le noyau peut effectuer une vérification et un contrôle de sécurité sur les requêtes des applications afin de garantir la sécurité et la stabilité du système.

Gestion des erreurs d'appel système

Dans le système d'exploitation, lorsqu'une application lance un appel système, une erreur peut se produire. Les erreurs peuvent être causées par des erreurs dans l'application elle-même ou par des problèmes dans le système d'exploitation. Le système d'exploitation renvoie généralement un code d'erreur. Pour informer l'application des problèmes survenus avec l'appel système. Les applications doivent gérer ces erreurs correctement afin de répondre de manière appropriée aux conditions d'erreur.

La gestion des erreurs d'appel système comprend généralement les étapes suivantes :
(1) Vérifiez la valeur de retour de l'appel système. Les appels système renvoient généralement une valeur entière représentant le résultat de l'opération ou un code d'erreur. L'application doit vérifier cette valeur de retour pour déterminer si l'opération a réussi.
(2) Si l'appel système renvoie un code d'erreur, l'application doit prendre les mesures appropriées en fonction du type d'erreur. Par exemple, si l'ouverture d'un fichier échoue, l'application peut choisir de réessayer d'ouvrir le fichier ou d'afficher un message d'erreur à l'utilisateur.
(3) L'application doit ajouter le code de gestion des erreurs approprié au code. Par exemple, si l'ouverture d'un fichier échoue, l'application doit ajouter du code pour fermer le descripteur de fichier ouvert afin que les ressources soient libérées en cas d'erreur.
(4) L'application peut choisir d'enregistrer les messages d'erreur dans un fichier journal pour une utilisation ultérieure lors du dépannage des erreurs.
Lors du traitement des erreurs d'appel système, les applications doivent examiner attentivement le type d'erreur et la méthode de traitement pour garantir l'exactitude et la stabilité de l'application.

Fonction de rapport d'erreurs (unix_error)

unix_error est une fonction de rapport d'erreurs, généralement utilisée pour gérer les erreurs dans les appels système UNIX. Il se trouve dans le fichier csapp.c du cas CSAPP. Sa fonction principale est de convertir les erreurs d'appel système UNIX en messages d'erreur lisibles par l'homme et d'imprimer les messages d'erreur dans le flux d'erreurs standard (stderr). Sa mise en œuvre est la suivante :

void unix_error(char *msg) /* UNIX-style error */
{
    fprintf(stderr, "%s: %s\n", msg, strerror(errno));
    exit(0);
}

Cette fonction reçoit un paramètre de chaîne msg, qui représente le préfixe du message d'erreur. Il utilise la fonction strerror pour convertir le code d'erreur errno en un message d'erreur et imprime le préfixe et le message d'erreur dans le flux d'erreur standard. Enfin, il appelle la fonction exit pour quitter le programme.

Par exemple, si une erreur se produit lors de l'appel de la fonction open pour ouvrir un fichier, vous pouvez utiliser la fonction unix_error pour signaler le message d'erreur :

int fd = open("nonexistent_file", O_RDONLY);
if (fd == -1) {
    unix_error("open error");
}

Dans le code ci-dessus, si l'ouverture du fichier échoue, la fonction unix_error affiche le message d'erreur suivant :

open error: No such file or directory

Utilisez la fonction unix_error pour gérer facilement les erreurs dans les appels système UNIX et fournir de meilleures capacités de rapport d'erreurs.

Fonction wrapper de gestion des erreurs (Fork)

La gestion des erreurs est un problème très important dans les systèmes d’exploitation. Pour faciliter la gestion des erreurs, vous pouvez utiliser les fonctions wrapper de gestion des erreurs pour encapsuler les appels système et vérifier s'ils ont réussi. Ces fonctions wrapper convertissent les erreurs d'appel système en valeurs de retour afin que les applications puissent rechercher les erreurs et prendre les mesures appropriées.
Voici un exemple de la fonction wrapper de gestion des erreurs Fork, qui encapsule l'appel système fork :

pid_t Fork(void) 
{
    pid_t pid;

    if ((pid = fork()) < 0)
        unix_error("Fork error");
    return pid;
}

Cette fonction renvoie l'ID de processus du processus enfant. Si l’appel de la fonction fork échoue, la fonction imprime un message d’erreur et termine le programme.
Lors de l'utilisation de la fonction Fork, si une erreur se produit, elle lèvera une exception et mettra fin au programme. Par exemple:

pid_t pid = Fork();
if (pid == 0) {  // Child process
    // ...
} else if (pid > 0) {  // Parent process
    // ...
} else {  // Error
    printf("Fork error\n");
    exit(1);
}

Dans le code ci-dessus, si l'appel à la fonction Fork échoue, le programme imprime un message d'erreur et se termine. Sinon, il effectuera différentes opérations respectivement dans le processus parent et le processus enfant.

Fork crée un processus enfant et renvoie un exemple de deux forks.
Sous Unix/Linux, fork() est un appel système utilisé pour créer un processus enfant. Après avoir appelé la fonction fork(), un nouveau processus sera créé, qui est le processus enfant du processus d'origine. Ce processus enfant aura le même code et les mêmes données que le processus parent, mais aura des ID de processus et des ressources système différents (tels que des descripteurs de fichiers, des cartes mémoire, des minuteries, etc.). Le prototype de la fonction fork() est le suivant :

#include <unistd.h>
pid_t fork(void);

Parmi eux, pid_t est un type de données entier, représentant l'ID du processus. La fonction fork() retournera deux fois : dans le processus parent, elle renvoie le PID du nouveau processus enfant et dans le processus enfant, elle renvoie 0. Si l’appel de la fonction fork() échoue, un nombre négatif est renvoyé.
Ce qui suit est un exemple de programme simple qui montre comment utiliser la fonction fork() pour créer un processus enfant :

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

int main() {
    pid_t pid = fork();  // 创建子进程

    if (pid < 0) {  // 创建进程失败
        printf("fork error\n");
    } else if (pid == 0) {  // 子进程
        printf("child process: pid=%d\n", getpid());
    } else {  // 父进程
        printf("parent process: child pid=%d\n", pid);
    }

    return 0;
}

Dans cet exemple de programme, la fonction fork() est d'abord appelée pour créer un processus enfant. Si la fonction fork() crée avec succès un processus enfant, "processus enfant : pid=XXX" est affiché dans le processus enfant, où XXX est l'ID de processus du processus enfant ; "processus parent : enfant pid=XXX" est affiché dans le processus parent. , où XXX est l'ID de processus du processus enfant nouvellement créé. Notez que les processus parent et enfant exécutent le même code, mais leur ordre d'exécution et leur sortie peuvent différer.

Il convient de noter que la fonction fork() copiera l'image mémoire du processus parent, y compris tous les descripteurs de fichiers ouverts, les fonctions de traitement du signal, la mémoire virtuelle, etc. Par conséquent, aucune variable ou structure de données n'est partagée entre les processus parent et enfant. Si vous devez partager des données entre les processus parent et enfant, vous pouvez utiliser des mécanismes de communication inter-processus (IPC), tels que des canaux, une mémoire partagée, des files d'attente de messages, etc.

Capturer ce qui se passe lorsque fork est appelé Utilisé
pour capturer ce qui se passe lorsque fork est appelé, où les sommets représentent l'exécution des instructions et les arêtes correspondent aux variables :
Insérer la description de l'image ici
Ce graphe de processus décrit un programme qui génère un message au début, puis appelle fork pour créer un processus enfant. Dans le processus enfant, le programme génère un message et termine le processus enfant en appelant exit(). Dans le processus parent, le programme imprime un message, puis poursuit l'exécution et imprime un message à la fin.

Dans ce graphe de processus, les sommets représentent différentes instructions du programme et les arêtes représentent le flux de contrôle. En particulier, les étiquettes sur un bord représentent des variables ou des conditions associées à ce bord. Dans cet exemple, pid est une variable qui stocke la valeur renvoyée par fork. Si pid est égal à 0, cela signifie que le processus enfant est actuellement exécuté, sinon cela signifie que le processus parent est actuellement exécuté.

Exemple de diagramme de processus

Vous trouverez ci-dessous un exemple de diagramme de processus illustrant un processus simple qui imprime un message et attend la saisie de l'utilisateur. Lorsque l'utilisateur saisit une chaîne, le processus imprime la chaîne et se termine.
Insérer la description de l'image ici
Dans ce graphe de processus, les sommets représentent différentes instructions du programme et les arêtes représentent le flux de contrôle. Dans cet exemple, le programme imprime d'abord un message, puis attend la saisie de l'utilisateur. Une fois la saisie terminée, le programme stockera la chaîne d'entrée dans une variable nommée buffer et imprimera la chaîne. Enfin, le programme termine le processus en appelant exit().

Les flèches dans le diagramme de processus représentent le flux de contrôle du processus. Bien que cet exemple soit simple, il montre que les diagrammes de processus peuvent être utilisés pour décrire le flux de contrôle d'un programme. En convertissant un programme en graphe de processus, nous pouvons mieux comprendre l'exécution du programme et mieux analyser son comportement.

pense?

Chaque processus a-t-il des processus enfants et des processus parents ?
Oui, chaque processus aura un processus parent et peut avoir un ou plusieurs processus enfants. Lorsqu'un processus appelle la fonction fork(), il en créera un nouveau. Thread enfant, et ceci le thread enfant sera une copie du processus actuel. Ce processus enfant aura le même code et les mêmes données que le processus parent, mais ils auront des espaces d'adressage séparés.

Le processus enfant héritera de certains attributs du processus parent, tels que les descripteurs de fichiers, les ID utilisateur, les ID de groupe, etc. L'état du processus enfant et du processus parent lors de leur création sera quelque peu différent.Le plus important est que le processus enfant héritera du contexte d'exécution du processus parent. Autrement dit, lorsque le processus enfant commence à s'exécuter, il commencera l'exécution à partir de l'instruction actuelle du processus parent, puis il poursuivra son propre chemin d'exécution.

Une fois le processus enfant créé avec succès, le processus parent et le processus enfant commenceront à s'exécuter en parallèle. Ils ont des contextes d’exécution indépendants, leur comportement est donc indépendant les uns des autres. Lorsque le processus enfant se termine, il envoie un signal au processus parent pour lui indiquer qu'il est terminé. Le processus parent peut utiliser des fonctions telles que wait() ou waitpid() pour attendre la fin du processus enfant et vérifier son état, ou ignorer directement le signal de sortie du processus enfant.

En résumé, chaque processus a un processus parent et éventuellement un ou plusieurs processus enfants. Ces processus peuvent se trouver dans des états quelque peu différents au moment de leur création, mais ils sont indépendants les uns des autres et peuvent s'exécuter en parallèle sur le système.

Exemple simple du fonctionnement de l'attente

Supposons que nous ayons un processus parent et deux processus enfants. Le processus enfant 1 dormira pendant 2 secondes, le processus enfant 2 dormira pendant 3 secondes, puis se terminera. Le processus parent doit attendre la fin des deux processus enfants avant de quitter. Ce processus peut être implémenté à l'aide de la fonction wait(). L'exemple de code est le suivant :

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

int main()
{
    pid_t pid1, pid2;
    int status1, status2;

    pid1 = fork();
    if (pid1 == 0) {
        // 子进程1
        sleep(2);
        printf("Child process 1 is exiting.\n");
        exit(0);
    } else {
        pid2 = fork();
        if (pid2 == 0) {
            // 子进程2
            sleep(3);
            printf("Child process 2 is exiting.\n");
            exit(0);
        } else {
            // 父进程
            wait(&status1);
            printf("Child process 1 has exited with status %d.\n", status1);
            wait(&status2);
            printf("Child process 2 has exited with status %d.\n", status2);
            printf("Parent process is exiting.\n");
            exit(0);
        }
    }

    return 0;
}

Dans cet exemple, le processus parent élimine d'abord deux processus enfants, puis utilise la fonction wait() pour attendre la fin des processus enfants et récupérer leurs ressources. Lorsque le processus enfant se termine, il enverra un signal au processus parent. Le processus parent capture le signal dans la fonction wait() et obtient l'état de sortie du processus enfant du processus enfant.

Après avoir exécuté ce programme, nous pouvons voir le résultat suivant :
Insérer la description de l'image ici

Fonction wait() La
fonction wait() est utilisée pour attendre la fin des processus enfants et recycler leurs ressources. Lorsqu'un processus enfant se termine, il ne disparaîtra pas immédiatement du système, mais deviendra un "processus zombie". du processus (y compris la mémoire, la description du fichier, l'état, etc.) sera toujours occupé, mais il ne pourra plus effectuer aucune opération.

Le processus parent peut utiliser la fonction wait() pour attendre la fin du processus enfant et recycler les ressources du processus enfant, afin que le processus enfant puisse être complètement supprimé du système. Avant que le processus parent n'appelle la fonction wait() , les ressources du processus enfant ne seront pas recyclées et continueront à occuper les ressources du système.

Lorsque le processus parent appelle la fonction wait(),il bloque et attend la fin du processus enfant. Si plusieurs processus enfants se terminent en même temps, la fonction wait() renverra l'état de l'un des processus enfants et recyclera le processus enfant. ressources du processus enfant. Si le processus parent n'a pas attendu la fin d'un processus enfant, la fonction wait() se bloquera jusqu'à la fin d'un processus enfant.

En résumé, la fonction wait() est utilisée pour attendre la fin des processus enfants et récupérer leurs ressources. Si le processus parent ne recycle pas les ressources du processus enfant, le processus enfant deviendra un processus zombie, occupant les ressources système.

Fonction waitpid :
La fonction waitpid peut attendre la fin du processus enfant spécifié et recycler ses ressources. Elle a trois paramètres :
 pid : l'ID du processus enfant à attendre. S'il est -1, attendez que n'importe quel processus enfant fin.
status : Un pointeur pour enregistrer l’état de sortie du processus enfant.
options : paramètres d'option.

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

int main()
{
    pid_t pid1, pid2;
    int status1, status2;

    pid1 = fork();
    if (pid1 < 0) {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (pid1 == 0) {
        // 子进程1
        printf("I am child process 1. My pid is %d.\n", getpid());
        sleep(2);
        exit(1);
    } else {
        pid2 = fork();
        if (pid2 < 0) {
            perror("fork");
            exit(EXIT_FAILURE);
        } else if (pid2 == 0) {
            // 子进程2
            printf("I am child process 2. My pid is %d.\n", getpid());
            sleep(4);
            exit(2);
        } else {
            // 父进程
            printf("I am parent process. My pid is %d.\n", getpid());
            printf("Waiting for child process %d and %d...\n", pid1, pid2);

            // 等待子进程1结束
            waitpid(pid1, &status1, 0);
            if (WIFEXITED(status1)) {
                printf("Child process %d terminated with exit status %d.\n", pid1, WEXITSTATUS(status1));
            } else {
                printf("Child process %d terminated abnormally.\n", pid1);
            }

            // 等待子进程2结束
            waitpid(pid2, &status2, 0);
            if (WIFEXITED(status2)) {
                printf("Child process %d terminated with exit status %d.\n", pid2, WEXITSTATUS(status2));
            } else {
                printf("Child process %d terminated abnormally.\n", pid2);
            }
        }
    }

    return 0;
}

Le programme crée deux processus enfants, attend respectivement 2 secondes et 4 secondes avant de quitter. Le processus parent utilise la fonction waitpid pour attendre la fin du processus enfant et imprimer l'état de sortie du processus enfant. Le résultat est le suivant :
Insérer la description de l'image ici
vous pouvez voir que le processus parent attend d'abord la fin du processus enfant 1, puis attend la fin du processus enfant 2. Les états de sortie des processus enfants sont respectivement 1 et 2.

execve exécute différents programmes.
execve() est un appel système Linux/Unix qui peut être utilisé pour démarrer un tout nouveau programme et l'exécuter, de sorte que le code et les données du processus actuel soient remplacés par le code et les données du nouveau programme. . Ceci peut être utilisé pour réaliser les fonctions de différents programmes.
execve reçoit trois paramètres :
(1) chemin : Spécifiez le chemin et le nom du programme à exécuter.
(2)argv : Il s'agit d'un tableau de chaînes qui contient les paramètres de ligne de commande du programme. Le premier élément de argv est généralement le nom du programme et les éléments suivants sont les paramètres du programme.
(3)envp : C'est un tableau de chaînes qui contient les variables d'environnement du programme.

execve recherchera d'abord le fichier programme à exécuter via le chemin spécifié par path. Si le fichier correspondant est trouvé, le processus en cours est remplacé par le fichier et un nouveau programme est démarré. Si le fichier correspondant n'est pas trouvé, execve l'appel de fonction échoue. et renvoie -1.
Exemple :

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

int main() {
    char *argv[] = {"/bin/ls", "-l", "/tmp", NULL};  // 要执行的程序和参数
    char *envp[] = {NULL};  // 环境变量
    if (execve("/bin/ls", argv, envp) == -1) {
        printf("execve failed\n");
        exit(1);
    }
    return 0;
}

Il convient de noter qu'une fois la fonction execve appelée avec succès, le processus actuel sera remplacé par un nouveau programme, donc le code après execve ne sera pas exécuté. Si vous souhaitez continuer à exécuter le code dans le programme d'origine après l'exécution du nouveau programme, vous pouvez appeler la fonction exit dans le nouveau programme pour terminer le programme et utiliser des appels système tels que fork et waitpid dans le programme d'origine pour attendre les résultats d'exécution du nouveau programme.

Un nouveau programme démarre la structure du cadre de pile

Lorsqu'un nouveau programme est démarré, sa structure de cadre de pile contient des informations telles que le point d'entrée du nouveau programme, les paramètres de ligne de commande et les variables d'environnement. Plus précisément, la structure de cadre de pile se compose généralement des parties suivantes : argc : un entier , indiquant le
nombre des paramètres de ligne de commande reçus par le programme.
argv : un tableau de pointeurs pointant vers des chaînes d'arguments de ligne de commande reçues par le programme.

int main(int argc, char *argv[]);

envp : un tableau de pointeurs pointant vers les variables d'environnement utilisées par le programme.
Adresse de retour : Une adresse pointant vers le point d’entrée du programme.
Variables locales : le programme peut créer des variables locales pendant l'exécution, et ces variables seront stockées dans la structure du cadre de pile.
Ces informations seront stockées séquentiellement dans la structure de trame de pile du nouveau programme. Ces informations sont accessibles et modifiables pendant l'exécution du programme. De manière générale, le système d'exploitation sera responsable de la création et de l'initialisation de la structure du cadre de pile du nouveau programme afin de fournir un environnement d'exécution approprié pour le nouveau programme.

processus zombie

Un processus zombie fait référence à un processus qui a terminé son exécution (quitté), mais son processus parent n'a pas encore appelé d'appels système tels que wait() ou waitpid() pour obtenir son statut de fin, ce qui fait que le descripteur de processus du processus reste Existe dans le système, mais ne peut effectuer aucune opération et ne peut pas être planifié.

Sur les systèmes Linux, l'état du processus zombie est marqué Z ou Z+, qui peut être visualisé en exécutant la commande ps. Les processus zombies n'occupent pas trop de ressources système, mais si le processus parent n'appelle jamais wait() et d'autres appels système pour recycler les ressources du processus, elles s'accumuleront progressivement, conduisant finalement à un gaspillage de ressources système.

Afin d'éviter la génération de processus zombies, le processus parent doit rapidement appeler wait() ou waitpid() et d'autres appels système après avoir créé le processus enfant pour obtenir l'état de sortie du processus enfant. Si le processus parent ne peut pas appeler ces appels système à temps, vous pouvez envisager d'utiliser des gestionnaires de signaux ou des processus démons pour résoudre le problème.

Le système fait en sorte que le processus d'initialisation recycle les processus orphelins.

Lorsque le processus parent d'un processus se termine avant sa sortie, le processus devient un processus orphelin (Orphan Process), c'est-à-dire un processus sans processus parent. Dans le système Linux, ces processus orphelins seront automatiquement attribués par le système au processus d'initialisation avec le processus numéro 1 comme nouveau processus parent, évitant ainsi l'existence de processus orphelins.

Le processus init est le premier processus du système Linux. Il est responsable du démarrage d'autres processus et de la surveillance de leur état d'exécution. Lorsqu'un processus devient un processus orphelin, il sera réaffecté par le système au processus init en tant que processus parent. De cette façon, lorsque le processus init appelle des appels système tels que wait() ou waitpid(), il peut obtenir et recycler les ressources de ces processus orphelins, évitant ainsi le gaspillage des ressources système.

Il convient de noter que le processus init est uniquement responsable du recyclage des processus orphelins. Pour les processus zombies, son processus parent doit appeler wait() et d'autres appels système pour le recyclage. Sinon, le processus zombie existera toujours, ce qui entraînera un gaspillage de ressources du système.
exemple:

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

int main() {
    pid_t pid;
    pid = fork();
    if (pid < 0) {
        printf("Fork Failed\n");
        exit(1);
    }
    else if (pid == 0) {
        printf("Child process\n");
        while (1) {
            sleep(1);  // 子进程不退出,一直在运行
        }
    }
    else {
        printf("Parent process\n");
        exit(0);  // 父进程退出
    }
    return 0;
}

Dans ce programme, le processus parent crée le processus enfant et se termine immédiatement après la création, mais le processus enfant est toujours en cours d'exécution. Lors de l'exécution de ce programme, vous pouvez afficher l'état du processus en exécutant la commande ps, comme indiqué ci-dessous :
Insérer la description de l'image ici

Un exemple de phénomène zombie :
supposons qu'il existe un processus parent et un processus enfant. Le processus parent crée le processus enfant et attend la fin du processus enfant. Son descripteur de processus existe toujours dans le système, mais le processus parent n'a pas encore appelé wait(). Ou waitpid() et d'autres appels système pour obtenir son statut de terminaison. À ce stade, le processus enfant deviendra un processus zombie. .

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

int main() {
    pid_t pid;
    pid = fork();
    if (pid < 0) {
        printf("Fork Failed\n");
        exit(1);
    }
    else if (pid == 0) {
        printf("Child process\n");
        exit(0);
    }
    else {
        printf("Parent process\n");
        sleep(2);  // 等待子进程结束
        // 父进程未调用wait()或waitpid()等系统调用获取子进程终止状态
        printf("Parent process exit without calling wait()\n");
    }
    return 0;
}

Dans ce programme, le processus parent crée un processus enfant et attend la fin du processus enfant. Cependant, les appels système tels que wait() ou waitpid() ne sont pas appelés dans le processus parent pour obtenir l'état de fin du processus enfant, ce qui fait que le processus enfant devient un processus zombie. Lors de l'exécution de ce programme, vous pouvez afficher l'état du processus en exécutant la commande ps, comme indiqué ci-dessous :
Insérer la description de l'image ici
Vous pouvez voir que l'état du processus est marqué Z ou Z+, qui est un processus zombie. Dans ce cas, le processus parent doit appeler des appels système tels que wait() ou waitpid() pour obtenir l'état de fin du processus enfant et ainsi recycler ses ressources.

Je suppose que tu aimes

Origine blog.csdn.net/m0_56898461/article/details/129941185
conseillé
Classement