Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

  1. #1
    Nouveau membre du Club
    Fin de programme propre après interception de SIGINT
    Bonjour,

    Je me demandais comment traiter proprement une fin de programme lors de l'interception d'un signal SIGINT par un gestionnaire de signaux. Prenons un programme réalisant deux allocations mémoire puis qui boucle sur un while(1) ; Lors de la réception du signal SIGINT une fonction void handler(int) ; est appelée. Comment faire pour que le programme libère la mémoire allouée avant de quitter avec un appel à la fonction exit ? Faut-il utiliser des variables globales pour connaître les adresses des pointeurs à libérer ?

    Je cherche la manière la plus propre de faire mais je ne vois pas trop. J'ai également déjà lu qu'on devait mettre le moins de code possible dans le handler ce qui à priori dans ma méthode n'est pas le cas. Voilà, donc comment faire ? Je m'en remet à vos connaissances.

    Quoiqu'il en soit merci d'avance pour vos conseils.

  2. #2
    Modérateur

    Salut

    Si tu quittes ton programme, la mémoire allouée avec malloc() est libérée automatiquement..... puisque le programme et son espace mémoire n'existent plus ^^ Je pense que tu n'as pas besoin de t'embêter avec ça : tu termines ton programme, c'est tout.

    En revanche, tu peux avoir d'autres ressources à libérer : connexion ouverte à une base de donnée, fichier ouvert, etc.

  3. #3
    Expert éminent sénior
    Juste pour préciser, sur certains systèmes très anciens, ou très particulier, la mémoire allouée par un malloc() n'est pas libérée automatiquement à la mort d'un programme.

    Mais si c'était ton cas, tu le saurais.
    Mes principes de bases du codeur qui veut pouvoir dormir:
    • Une variable de moins est une source d'erreur en moins.
    • Un pointeur de moins est une montagne d'erreurs en moins.
    • Un copier-coller, ça doit se justifier... Deux, c'est un de trop.
    • jamais signifie "sauf si j'ai passé trois jours à prouver que je peux".
    • La plus sotte des questions est celle qu'on ne pose pas.
    Pour faire des graphes, essayez yEd.
    le ter nel est le titre porté par un de mes personnages de jeu de rôle

  4. #4
    Nouveau membre du Club
    Merci pour vos réponses.
    Je sais en effet que sur les systèmes récents la mémoire est libérée automatiquement mais comme tu le dis il peut s'agir de fermer des descripteurs de fichiers ou autre. Mon exemple était peut-être mal choisi mais j'ai voulu faire simple et le problème reste le même : comment fermer ou libérer la mémoire de manière propre à l'interception d'un SIGINT ?

  5. #5
    Modérateur

    Bonjour,

    Citation Envoyé par Dliw0 Voir le message
    Je sais en effet que sur les systèmes récents la mémoire est libérée automatiquement mais comme tu le dis il peut s'agir de fermer des descripteurs de fichiers ou autre. Mon exemple était peut-être mal choisi mais j'ai voulu faire simple et le problème reste le même : comment fermer ou libérer la mémoire de manière propre à l'interception d'un SIGINT ?
    C'est une question pertinente car c'est un problème de conception initiale, qui devrait occuper tous les développeurs dès les premières lignes de leur programme. En gros, il faut écrire ton programme tel que tu le ferais si c'était le système d'exploitation lui-même que tu écrivais.

    D'une manière générale, on essaie d'éviter de recourir aux variables globales pour [POST="5271611"]un certain nombre de raisons[/POST], mais c'est effectivement typiquement dans ce genre de cas qu'on va les trouver. En fait, on va surtout utiliser une fonction qui ne conservera qu'un pointeur dans une variable statique et qui nous enverra son contenu sur demande, pointeur vers une structure dont l'espace est alloué avec un malloc et qui, elle, va contenir toutes les infos que l'on a besoin de partager.

    Mais la manière la plus propre, dans ce cas précis, de gérer la chose reste de n'en faire toujours que le minimum dans les handlers de signaux, qui doivent rendre la main le plus vite possible. La bonne réponse, dans ton cas, consiste donc à faire en sorte que ton gestionnaire de signal place un flag quelque part, qui soit interprété ensuite comme une demande de sortie ordinaire par ton programme. Ainsi, tu es sûr que le workflow de ton programme ira quand même jusqu'à son terme, ce qui est le meilleur moyen de ne rien oublier.

    Tu peux également utiliser atexit() pour appeler des fonctions de nettoyage dès que ton programme se termine. Attention : c'est valable pour les sorties en conditions normales avec exit() ou lorsque que l'on sort de main(), mais pas sur _exit() ou lorsque tu reçois un signal tueur non pris en charge. En revanche, tu peux invoquer plusieurs fois cette fonction, ce qui te permet de la placer partout où tu fais des allocations. Par contre, à terme, il vaut mieux s'en passer et écrire un programme qui soit naturellement conçu pour passer par les phases de sortie.

    À noter enfin qu'il faut également prendre en compte la sémantique des signaux eux-mêmes (pas toujours très claire, d'ailleurs) : SIGINT demande au programme de s'interrompre et SIGTERM de prendre fin normalement. Ça veut dire que dans le premier cas, on ne souhaite pas nécessairement mener à terme l'opération en cours (ce qui ne veut pas dire qu'il ne faut pas sortir proprement). Dans le second, on termine ce que l'on est en train de faire, mais sans entamer une nouvelle tâche s'il en reste. :-)

    Empiriquement, on peut se laisser une à deux secondes de grâce pour finir ce que l'on est en train de faire quand on reçoit SIGTERM mais guère plus, car lorsque l'on fait un shutdown ou un redémarrage d'un système Unix lors d'un init 0 ou d'un init 6, c'est typiquement ce délai que nous laisse le système : il envoie SIGTERM à tout le monde, attend quelques secondes, puis fais un SIGKILL sur ce qui reste.

  6. #6
    Nouveau membre du Club
    Bonjour Obsidian et merci pour cette réponse très enrichissante.

    La solution d'utiliser une structure dédiée aux éléments partagés dans le programme est en effet bien pratique et je m'en vais de ce pas la mettre en oeuvre.
    J'aurai cependant une petite question concernant la sortie ordinaire d'un programme :
    Citation Envoyé par Obsidian
    La bonne réponse, dans ton cas, consiste donc à faire en sorte que ton gestionnaire de signal place un flag quelque part, qui soit interprété ensuite comme une demande de sortie ordinaire par ton programme.
    Dans l'exemple que j'ai formulé dans mon premier message cela reviendrai grosso-modo à remplacer (en considérant pour faire simple une variable globale continuer initialisée à 1) le while(1) ; par while(continuer) ; le handler ayant donc pour simple fonction de mettre la valeur de continuer à zéro, c'est bien ça ? Mais prenons le cas d'un programme travaillant avec des tubes nommés réalisant une ouverture bloquante dans la boucle. L'utilisation de cette méthode propre est donc impossible et du coup pas d'autre solution que d'appeler un atexit() ?

    Autre petite chose au risque de me faire taper sur les doigts : bien que je ne l'ai jamais utilisée et que je ne le souhaite pas, la fumeuse instruction goto ne serait-elle pas utile dans ce dernier cas (sûrement une méthode très sale puisqu'elle casse fortement l'exécution structurelle des instructions mais qui aurai le mérite de fonctionner, et donc de terminer le programme de manière ordinaire, non ?)

  7. #7
    Modérateur

    Citation Envoyé par Dliw0 Voir le message
    Dans l'exemple que j'ai formulé dans mon premier message cela reviendrai grosso-modo à remplacer (en considérant pour faire simple une variable globale continuer initialisée à 1) le while(1) ; par while(continuer) ; le handler ayant donc pour simple fonction de mettre la valeur de continuer à zéro, c'est bien ça ?
    Dans le principe, oui. Cependant, ce n'est pas une manière universelle de faire. L'idée générale consiste à placer certaines informations au bon endroit de façon à ce que le programme principal prenne la décision de sortir. C'est le cas lorsque, par exemple, l'utilisateur saisit « quit » sur la ligne de commande, mais également s'il clique sur la croix de la fenêtre de l'application ou, ici, si le handler du signal change l'état d'un flag donné.

    Cela permet aussi de se pencher sur la façon dont cette sortie est effectuée en temps normal dans son programme. Souvent, c'est à l'aide d'un gros « exit() ». Ce n'est pas un mal en soi si l'on sait ce que l'on fait, et l'utilisation de de « atexit() » permet justement de ne rien oublier et de distinguer ce cas de figure du reste de l'algorithme. Par contre, si c'est pour se sortir d'une impasse, cela ne vaut pas mieux qu'un goto.

    Mais prenons le cas d'un programme travaillant avec des tubes nommés réalisant une ouverture bloquante dans la boucle. L'utilisation de cette méthode propre est donc impossible et du coup pas d'autre solution que d'appeler un atexit() ?
    Bonne question :

    1. La réception d'un signal va toujours débloquer un appel en attente. Y compris sleep() ! C'est un cas de figure prévu par le système et il appartient donc au programmeur de le prendre en charge (même s'il est possible dans certains cas de demander à reprendre l'attente… si c'est possible) ;
    2. Que ferais-tu si tu devais surveiller non pas un mais deux tubes nommés, voire plus ? C'est spécialement intéressant lorsque tes tubes sont en fait des sockets et que ton application est un serveur à l'écoute de plusieurs clients. Il te faut un appel système pour cela : http://man.developpez.com/man2/select.2.php et ses dérivés : pselect(), poll() et ppoll().



    Autre petite chose au risque de me faire taper sur les doigts : bien que je ne l'ai jamais utilisée et que je ne le souhaite pas, la fumeuse instruction goto ne serait-elle pas utile dans ce dernier cas (sûrement une méthode très sale puisqu'elle casse fortement l'exécution structurelle des instructions mais qui aurai le mérite de fonctionner, et donc de terminer le programme de manière ordinaire, non ?)
    Si. Mais pas forcément de la manière dont tu l'envisages.

    D'abord, un goto ne te permet pas de sauter d'une fonction à une autre, ne serait-ce que parce que l'étiquette de la destination est forcément définie à l'intérieur du bloc d'une fonction et que sa portée est donc limitée à celui-ci. Ensuite, l'un des principaux problèmes du goto est que, par définition, il fait un saut arbitraire d'une position à une autre et qu'il laisse donc la pile dans l'état où elle l'était déjà.

    La norme précise notamment ceci :

    Citation Envoyé par C99 n1256
    6.8.6.1 The goto statement
    Constraints
    1
    The identifier in a goto statement shall name a label located somewhere in the enclosing
    function. A goto statement shall not jump from outside the scope of an identifier having
    a variably modified type to inside the scope of that identifier.

    Ça veut dire que tu ne peux pas sauter de l'extérieur vers l'intérieur d'un bloc qui définit et utilise un identifiant pouvant être variable. Autrement dit, tu ne peux pas faire ceci :

    Code :Sélectionner tout -Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
     
    int main (void)
    {
        goto dedans;
     
        if (1)
        {
            int tab[200];
     
            dedans:
            tab[100] = 1;
        }
     
        return 0;
    }

    … et ceci parce le tableau « tab[200] » est réservé dans la pile au moment où tu entres dans le bloc « if ». Si tu n'es pas passé par cette étape, le tableau n'existera pas encore au moment où tu voudras l'utiliser. Note que le problème est exactement le même dans l'autre sens : si tu sors du bloc artificiellement avec un goto, tu ne libères pas le tableau !

    En fait, le compilateur C va quand même retomber sur ses pattes car il va généralement directement réserver dans la pile tout l'espace dont il aura besoin tout au long de la fonction. Sauf dans le cas des VLA, mais des tests avec GCC montrent que, dans ce cas, le compilateur reconnaît le cas et sauve auparavant le pointeur de pile dans un registre (autre que [ER]BP déjà utilisé à cette fin lors de l'établissement du cadre de pile).

    Pour éviter ce genre de problème et pouvoir sauter d'une fonction à une autre, on utilise setjmp() et longjmp. Le premier appel sauvegarde l'état de pile à un moment donné et le second le rétablit et ramène l'exécution juste après le setjmp initial. C'est déjà ce qui se passe lorsque tu quittes une fonction avec return avant son terme : le compilateur replace BP dans le pointeur de pile et, de là, est capable de retrouver et dépiler l'adresse de retour. setjmp et longjmp vont faire à peu près la même chose et en sautant vers l'adresse explicitement sauvegardée plutôt que celle empilée.

    Cela dit, ces fonctions sont déconseillés pour les mêmes raisons que le goto : ça brise l'enchaînement des fonctions, c'est difficile à suivre et à débuguer et ça pose exactement les mêmes problèmes que ceux que tu cites au départ : on n'est pas sûr de libérer les ressources qui auront été allouées entre temps.

    Elles ont donc deux intérêts principaux : permettre la mise en place d'un système « d'exceptions » permettant de remonter jusqu'à un point donné où l'on peut la rattraper et, par extension, établir une sorte de checkpoint où l'on peut reprendre lorsque quelque chose de grave et d'imprévu se produit, par exemple une corruption de pile. À l'instar des bornes « SAVE » que l'on trouve parfois dans les jeux d'aventure, cela permet de revenir à un état en principe connu plutôt que de laisser le programme planter complètement et, de là, tenter de sauver ce qui peut l'être et préparer une sortie prématurée mais propre.

  8. #8
    Nouveau membre du Club
    Eh bien merci beaucoup pour tous ces éclaircissements.
    Je commence justement à travailler sur des programmes client / serveur et ces informations me seront bien utiles.