{"id":1967,"date":"2012-04-21T12:00:41","date_gmt":"2012-04-21T11:00:41","guid":{"rendered":"http:\/\/www.blaess.fr\/christophe\/?p=1967"},"modified":"2012-04-21T12:00:41","modified_gmt":"2012-04-21T11:00:41","slug":"traitements-paralleles-dans-un-script-shell","status":"publish","type":"post","link":"https:\/\/www.blaess.fr\/christophe\/2012\/04\/21\/traitements-paralleles-dans-un-script-shell\/","title":{"rendered":"Traitements parall\u00e8les dans un script shell"},"content":{"rendered":"<p style=\"text-align: justify;\">Il est rare de devoir utiliser des traitements en t\u00e2che de fond dans un script shell. \u00c0 moins, bien entendu, qu&rsquo;il s&rsquo;agisse d&rsquo;un script de d\u00e9marrage servant justement \u00e0 lancer plusieurs traitements en parall\u00e8le.<\/p>\n<p style=\"text-align: justify;\">Il peut n\u00e9anmoins \u00eatre parfois n\u00e9cessaire de g\u00e9rer des lancements en arri\u00e8re-plan, comme cela m&rsquo;est arriv\u00e9 une fois.<\/p>\n<p>\n<!--more-->\n<\/p>\n<h1>Situation<\/h1>\n<p style=\"text-align: justify;\">Un syst\u00e8me a\u00e9roportuaire de traitement de plots radar m&rsquo;envoyait des informations en continu sur un port r\u00e9seau UDP\/IP. Je devais les enregistrer dans des fichiers horodat\u00e9s. Chaque fichier contenait une heure d&rsquo;enregistrement environ. Je devais \u00e9galement, \u00e0 l&rsquo;issue de chaque enregistrement, effectuer une analyse des donn\u00e9es qui durait environ cinq minutes.<\/p>\n<h1>Logiciels existants<\/h1>\n<p style=\"text-align: justify;\">Deux logiciels, \u00e9crits en C, \u00e9taient disponibles.<\/p>\n<p style=\"text-align: justify;\">Le premier, appelons-le <code>enregistreur<\/code> prenait plusieurs arguments sur sa ligne de commande<\/p>\n<ul>\n<li style=\"text-align: justify;\">L&rsquo;adresse IPv4 du groupe multicast dans lequel s&rsquo;inscrire. Ceci n&rsquo;a pas d&rsquo;int\u00e9r\u00eat pour ce qui nous concerne ici, prenons par exemple 224.10.10.10.<\/li>\n<li style=\"text-align: justify;\">Le num\u00e9ro de port UDP pour recevoir les donn\u00e9es. La valeur ne nous concerne pas plus que la pr\u00e9c\u00e9dente. Fixons-la arbitrairement \u00e0 2012.<\/li>\n<li style=\"text-align: justify;\">La dur\u00e9e en secondes de l&rsquo;enregistrement \u00e0 r\u00e9aliser. Pour une dur\u00e9e d&rsquo;une heure, nous passerons <code>3600<\/code>.<\/li>\n<li style=\"text-align: justify;\">Le nom du fichier o\u00f9 stocker l&rsquo;enregistrement. Pour que les fichiers soient ais\u00e9ment manipulables par la suite, il est n\u00e9cessaire que leurs noms contiennent un horodatage compr\u00e9hensible.<\/li>\n<\/ul>\n<p style=\"text-align: justify;\">Le second logiciel, que nous appellerons <code>statistiques<\/code> \u00e9tait un outil \u00e9tudiant les donn\u00e9es re\u00e7ues (qu&rsquo;il lisait depuis un fichier), et sortant des \u00e9l\u00e9ments d&rsquo;information sur sa sortie standard. Ce programme prenait uniquement en argument le nom du fichier d&rsquo;enregistrement \u00e0 traiter.<\/p>\n<p style=\"text-align: justify;\">Ces deux logiciels fonctionnaient tr\u00e8s bien, et je devais les encha\u00eener dans un script shell afin qu&rsquo;ils tournent 24H\/24H.<\/p>\n<h1>Probl\u00e8me<\/h1>\n<p style=\"text-align: justify;\">Tout d&rsquo;abord il fallait g\u00e9n\u00e9rer le nom du fichier en incluant un horodatage lisible. Pour cela le plus simple est souvent de s&rsquo;appuyer sur la commande <code>date<\/code> dont les options permettent facilement de construire une cha\u00eene de caract\u00e8res en ins\u00e9rant des \u00e9l\u00e9ments de date et d&rsquo;heure.<\/p>\n<pre>NOM_FICHIER=$(date +\"enregistrement-LFPG-%Y-%m-%d-%H-%M.dat\")<\/pre>\n<p style=\"text-align: justify;\">La variable <code>NOM_FICHIER<\/code> contiendra une cha\u00eene de caract\u00e8res comme <code>enregistrement-LFPG-2012-04-21-09-00.dat<\/code>.<\/p>\n<p style=\"text-align: justify;\">Le second probl\u00e8me \u00e9tait plus g\u00eanant. Si l&rsquo;on pouvait tol\u00e9rer la perte d&rsquo;une seconde de messages entre deux enregistrements, il n&rsquo;\u00e9tait pas envisageable d&rsquo;avoir un \u00ab\u00a0trou\u00a0\u00bb de cinq minutes (dur\u00e9e du programme <code>statistiques<\/code>). Je ne pouvais donc pas utiliser le sh\u00e9ma \u00e9vident suivant.<\/p>\n<pre>#! \/bin\/sh\nADRESSE_IP=224.10.10.10\nPORT_UDP=2012\nwhile true\ndo\n    # Generer le nom du fichier d'enregistrement\n    FICHIER=$(date +\"enregistrement-LFPG-%Y-%m-%d-%H-%M.dat\")\n\n    # Declencher l'enregistrement\n    enregistreur ${ADRESSE_IP} ${PORT_UDP} 3600 \"${FICHIER}\"\n\n    # Traiter l'heure ecoulee\n    statistiques \"${FICHIER}\" &gt; fichier-resultat.txt\ndone<\/pre>\n<p style=\"text-align: justify;\">Il fallait donc parall\u00e9liser le traitement statistique et l&rsquo;enregistrement des donn\u00e9es.<\/p>\n<h1>Premi\u00e8re id\u00e9e<\/h1>\n<p style=\"text-align: justify;\">On peut tout d&rsquo;abord imaginer un sch\u00e9ma o\u00f9 le traitement statistique fonctionne en arri\u00e8re-plan, la boucle principale \u00e9tant cadenc\u00e9e par l&rsquo;enregistreur. En voici un exemple.<\/p>\n<pre>#! \/bin\/sh\n[...]\nwhile true\ndo\n    # Generer le nom du fichier d'enregistrement\n    FICHIER=$(date +\"enregistrement-LFPG-%Y-%m-%d-%H-%M.dat\")\n\n    # Declencher l'enregistrement\n    enregistreur ${ADRESSE_IP} ${PORT_UDP} 3600 \"${FICHIER}\"\n\n    # Traiter l'heure ecoulee\n    statistiques \"${FICHIER}\" &gt; fichier-resultat.txt <strong>&amp;<\/strong>\ndone<\/pre>\n<p style=\"text-align: justify;\">On remarquera, \u00e0 la fin de la ligne de la commande <code>statistiques<\/code>, le <code>&amp;<\/code> qui provoque le passage \u00e0 l&rsquo;arri\u00e8re-plan. Ceci permet au shell de reprendre imm\u00e9diatement son ex\u00e9cution en d\u00e9but de boucle et donc de d\u00e9marrer un nouvel enregistrement.<\/p>\n<p style=\"text-align: justify;\">Cette m\u00e9thode fonctionnait, mais elle pr\u00e9sentait n\u00e9anmoins un petit inconv\u00e9nient. A l&rsquo;issue du traitement statistique, le fichier de r\u00e9sultats devait \u00eatre renomm\u00e9 en utilisant un num\u00e9ro d&rsquo;ordre s\u00e9quentiel et copi\u00e9 dans un r\u00e9pertoire ind\u00e9pendant&nbsp;; ceci seulement dans le cas o\u00f9 le fichier n&rsquo;\u00e9tait pas vide. Il devenait compliqu\u00e9 de r\u00e9aliser ces op\u00e9rations dans une ex\u00e9cution en arri\u00e8re-plan.<\/p>\n<p style=\"text-align: justify;\">L&rsquo;id\u00e9e fut donc d&rsquo;inverser le principe pr\u00e9c\u00e9dent, en effectuant l&rsquo;enregistrement en arri\u00e8re-plan et le traitement statistique dans la boucle principale.<\/p>\n<h1>Seconde id\u00e9e<\/h1>\n<p style=\"text-align: justify;\">Voici un premier aper\u00e7u de la nouvelle solution.<\/p>\n<pre>#! \/bin\/sh\n[...]\nNUMERO_RESULTAT=1\n\nwhile true\ndo\n    # Generer le nom du fichier d'enregistrement\n    FICHIER=$(date +\"enregistrement-LFPG-%Y-%m-%d-%H-%M.dat\")\n\n    # Declencher l'enregistrement\n    enregistreur ${ADRESSE_IP} ${PORT_UDP} 3600 \"${FICHIER}\" <strong>&amp;<\/strong>\n\n    # Traiter l'heure ecoulee\n    statistiques \"${FICHIER}\" &gt; fichier-resultat.txt\n\n    # si le fichier n'est pas vide\n    if [ -s fichier-resultat.txt ]\n\n        # Le renommer et le d\u00e9placer\n        mv fichier-resultat.txt \"resultat-${NUMERO_RESULTAT}.txt\"\n        mv \"resultat-${NUMERO_RESULTAT}.txt\"  ~\/resultats-statistiques\/\n\n        # Incr\u00e9menter le num\u00e9ro d'ordre\n        NUMERO_RESULTAT=$(( NUMERO_RESULTAT + 1 ))\n    fi\ndone<\/pre>\n<p style=\"text-align: justify;\">Si cette id\u00e9e semble s\u00e9duisante au premier abord, elle ne fonctionne pas du tout en r\u00e9alit\u00e9. Lorsqu&rsquo;on lance l&rsquo;enregistreur en arri\u00e8re-plan, le traitement statistique s&rsquo;ex\u00e9cute en quelques minutes et relance un nouvel enregistreur alors que le pr\u00e9c\u00e9dent n&rsquo;est pas termin\u00e9, et ainsi de suite.<\/p>\n<p style=\"text-align: justify;\">Pour que cette m\u00e9thode soit utilisable, il faudrait s&rsquo;assurer avant de d\u00e9marrer un enregistrement que le pr\u00e9c\u00e9dent soit fini. Pour cela, nous allons noter apr\u00e8s le lancement en arri\u00e8re-plan de l&rsquo;enregistreur son num\u00e9ro de processus (<em>PID<\/em>), et avant le lancement nous attendrons que le processus enregistreur pr\u00e9c\u00e9dent soit termin\u00e9.<\/p>\n<p style=\"text-align: justify;\">Le shell nous fournit dans la variable <strong><code>$!<\/code><\/strong> le PID du dernier processus lanc\u00e9 en arri\u00e8re-plan. Ajoutons donc la ligne suivante dans notre code.<\/p>\n<pre>[...]\n    enregistreur ${ADRESSE_IP} ${PORT_UDP} 3600 \"${FICHIER}\" &amp;\n    <strong>PID_ENREGISTREUR=$!<\/strong>\n    # Traiter l'heure ecoulee\n    [...]<\/pre>\n<p style=\"text-align: justify;\">Pour attendre la fin d&rsquo;un processus lanc\u00e9 par le shell, celui-ci nous propose la commande <strong><code>wait<\/code><\/strong> que l&rsquo;on peut faire suivre du num\u00e9ro de PID. Ajoutons cette attente avant le lancement.<\/p>\n<pre>[...]\n    <strong>wait ${PID_ENREGISTREUR}<\/strong>\n    enregistreur ${ADRESSE_IP} ${PORT_UDP} 3600 \"${FICHIER}\" &amp;\n    [...]<\/pre>\n<p style=\"text-align: justify;\">Nous devons bien s\u00fbr prendre en consid\u00e9ration le d\u00e9marrage de notre script, o\u00f9 aucun enregistreur ne tourne encore. Pour cela, il suffit d&rsquo;initialiser la variable <code>PID_ENREGISTREUR<\/code> \u00e0 un num\u00e9ro impossible de processus (les PID sont toujours sup\u00e9rieurs \u00e0 z\u00e9ro) et de la tester avant d&rsquo;appeler <code>wait<\/code>.<\/p>\n<h1>Troisi\u00e8me essai<\/h1>\n<p style=\"text-align: justify;\">Notre code devient&nbsp;:<\/p>\n<pre>#! \/bin\/sh\nADRESSE_IP=224.10.10.10\nPORT_UDP=2012\nREPERTOIRE_STATISTIQUES=\"~\/resultats-statistiques\/\"\nPID_ENREGISTREUR=0\nwhile true\ndo\n    # Generer le nom du fichier d'enregistrement\n    FICHIER=$(date +\"enregistrement-LFPG-%Y-%m-%d-%H-%M.dat\")\n\n    # Attendre la fin de l'enregistreur precedent\n    if [ $PID_ENREGISTREUR -gt 0 ]\n    then\n        wait $PID_ENREGISTREUR\n    fi\n    # Declencher l'enregistrement\n    enregistreur ${ADRESSE_IP} ${PORT_UDP} 3600 \"${FICHIER}\" &amp;\n    PID_ENREGISTREUR=$!\n\n    # Traiter l'heure ecoulee\n    statistiques \"${FICHIER}\" &gt; fichier-resultat.txt\n\n    # si le fichier n'est pas vide\n    if [ -s fichier-resultat.txt ]\n        # Le renommer et le deplacer\n        mv \"fichier-resultat.txt\" \"resultat-${NUMERO_RESULTAT}.txt\"\n        mv \"resultat-${NUMERO_RESULTAT}.txt\" \"${REPERTOIRE_STATISTIQUES}\"\n\n        # Incrementer le numero d'ordre\n        NUMERO_RESULTAT=$(( NUMERO_RESULTAT + 1 ))\n    fi\ndone<\/pre>\n<p style=\"text-align: justify;\">Pourtant, en observant le script, on peut s&rsquo;apercevoir d&rsquo;un gros d\u00e9faut&nbsp;: le traitement statistique d\u00e9marre sur le m\u00eame fichier que l&rsquo;enregistreur en cours&nbsp;! Il faut bien s\u00fbr qu&rsquo;il travaille sur l&rsquo;enregistrement pr\u00e9c\u00e9dent. Modifions encore notre script.<\/p>\n<h1>Solution<\/h1>\n<pre>#! \/bin\/sh\nADRESSE_IP=224.10.10.10\nPORT_UDP=2012\nREPERTOIRE_STATISTIQUES=\"~\/resultats-statistiques\/\"\nPID_ENREGISTREUR=0\nFICHIER_COURANT=\"\"\nFICHIER_PRECEDENT=\"\"\n\nwhile true\ndo\n    # Attendre la fin de l'enregistreur precedent\n    if [ ${PID_ENREGISTREUR} -gt 0 ]\n    then\n        wait ${PID_ENREGISTREUR}\n        FICHIER_PRECEDENT=\"${FICHIER_COURANT}\"\n    fi\n\n    # Generer le nom du nouvel enregistrement\n    FICHIER_COURANT=$(date +\"enregistrement-LFPG-%Y-%m-%d-%H-%M.dat\")\n\n    # Lancer l'enregistreur en arriere-plan\n    enregistreur ${ADRESSE_IP} ${PORT_UDP} 3600 \"${FICHIER_COURANT}\" &amp;\n    PID_ENREGISTREUR=$!\n\n    # Traiter l'enregistrement de l'heure ecoulee\n    statistiques \"${FICHIER_PRECEDENT}\" &gt; fichier-resultat.txt\n\n    # si le resultat n'est pas vide\n    if [ -s fichier-resultat.txt ]\n        # Le renommer et le deplacer\n        mv \"fichier-resultat.txt\" \"resultat-${NUMERO_RESULTAT}.txt\"\n        mv \"resultat-${NUMERO_RESULTAT}.txt\" \"${REPERTOIRE_STATISTIQUES}\"\n\n        # Incrementer le numero d'ordre\n        NUMERO_RESULTAT=$(( NUMERO_RESULTAT + 1 ))\n    fi\ndone<\/pre>\n<p style=\"text-align: justify;\">Cette fois le script est correct et fonctionne.<\/p>\n<h1>Conclusion<\/h1>\n<p style=\"text-align: justify;\">Nous voyons que l&rsquo;encadrement par un script shell de programmes existants n&rsquo;est pas toujours \u00e9vident surtout lorsqu&rsquo;il existe une d\u00e9pendance temporelle entre eux (synchronisation). Ici, certains points du script ont \u00e9t\u00e9 ignor\u00e9s (gestion des erreurs d&rsquo;ex\u00e9cution comme la saturation du disque par exemple), mais il nous a quand m\u00eame fallu quatre versions pour obtenir enfin un programme correct. On retiendra \u00e9gtalement que le soin apport\u00e9 \u00e0 la lisibilit\u00e9 et \u00e0 la clart\u00e9 du script est un gage de facilit\u00e9 de maintenance ult\u00e9rieure, surtout lorsque l&rsquo;algorithme global sort quelque peu de l&rsquo;ordinaire (parall\u00e9lisme par exemple).<\/p>","protected":false},"excerpt":{"rendered":"<p>Il est rare de devoir utiliser des traitements en t&acirc;che de fond dans un script shell. &Agrave; moins, bien entendu, qu&rsquo;il s&rsquo;agisse d&rsquo;un script de d&eacute;marrage servant justement &agrave; lancer plusieurs traitements en parall&egrave;le. Il peut n&eacute;anmoins &ecirc;tre parfois n&eacute;cessaire de g&eacute;rer des lancements en arri&egrave;re-plan, comme cela m&rsquo;est arriv&eacute; une fois.<\/p>","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[8,13],"tags":[],"class_list":["post-1967","post","type-post","status-publish","format-standard","hentry","category-linux-2","category-shell"],"_links":{"self":[{"href":"https:\/\/www.blaess.fr\/christophe\/wp-json\/wp\/v2\/posts\/1967","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.blaess.fr\/christophe\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.blaess.fr\/christophe\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.blaess.fr\/christophe\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.blaess.fr\/christophe\/wp-json\/wp\/v2\/comments?post=1967"}],"version-history":[{"count":0,"href":"https:\/\/www.blaess.fr\/christophe\/wp-json\/wp\/v2\/posts\/1967\/revisions"}],"wp:attachment":[{"href":"https:\/\/www.blaess.fr\/christophe\/wp-json\/wp\/v2\/media?parent=1967"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.blaess.fr\/christophe\/wp-json\/wp\/v2\/categories?post=1967"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.blaess.fr\/christophe\/wp-json\/wp\/v2\/tags?post=1967"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}