{"id":1011,"date":"2011-07-29T12:00:03","date_gmt":"2011-07-29T11:00:03","guid":{"rendered":"http:\/\/www.blaess.fr\/christophe\/?p=1011"},"modified":"2011-07-29T12:00:03","modified_gmt":"2011-07-29T11:00:03","slug":"eviter-les-inversions-de-priorite-causees-par-des-mutex","status":"publish","type":"post","link":"https:\/\/www.blaess.fr\/christophe\/2011\/07\/29\/eviter-les-inversions-de-priorite-causees-par-des-mutex\/","title":{"rendered":"\u00c9viter les inversions de priorit\u00e9 caus\u00e9es par des mutex"},"content":{"rendered":"<p style=\"text-align: justify;\">Dans les applications temps-r\u00e9el il est tr\u00e8s important d&rsquo;\u00e9viter les situations dites \u00ab\u00a0d&rsquo;inversion de priorit\u00e9\u00a0\u00bb. Il s&rsquo;agit de cas dans lesquels une t\u00e2che de haute priorit\u00e9 est bloqu\u00e9e en attente de la terminaison d&rsquo;une t\u00e2che de plus faible priorit\u00e9 alors qu&rsquo;elles n&rsquo;ont rien en commun.<\/p>\n<p>\n<!--more-->\n<\/p>\n<h1 style=\"text-align: justify;\">Probl\u00e8me d&rsquo;inversion de priorit\u00e9<\/h1>\n<p style=\"text-align: justify;\">Examinons un cas classique, reposant sur trois t\u00e2ches T1, T2 et T3 de priorit\u00e9s strictement croissantes ainsi qu&rsquo;un mutex M.<\/p>\n<p style=\"text-align: justify; padding-left: 90px;\">Rappelons qu&rsquo;un <em>mutex<\/em> est un \u00e9l\u00e9ment logiciel permettant d&rsquo;assurer l&rsquo;<em>exclusion mutuelle<\/em> de t\u00e2ches concurrentes. Une fois qu&rsquo;une t\u00e2che a verrouill\u00e9 le mutex, les autres ne pourront pas l&rsquo;obtenir avant qu&rsquo;elle le d\u00e9verrouille. On utilise ceci pour synchroniser l&rsquo;acc\u00e8s \u00e0 des ressources partag\u00e9es.<\/p>\n<p>&nbsp;<\/p>\n<ol>\n<li style=\"text-align: justify;\">Initialement, T2 et T3 sont endormies, en attente d&rsquo;\u00e9v\u00e9nements externes (par exemple des interruptions mat\u00e9rielles, ou l&rsquo;expiration de <em>timers<\/em>). T1 s&rsquo;ex\u00e9cute et va acc\u00e9der \u00e0 une ressource partag\u00e9e avec T3 (par exemple une zone de m\u00e9moire commune). Aussi T1 verrouille-t-elle le mutex M.<\/li>\n<li style=\"text-align: justify;\">L&rsquo;\u00e9v\u00e9nement externe qu&rsquo;attendait T3 se produit. La t\u00e2che T3 se r\u00e9veille, et pr\u00e9empte T1 car elle est plus prioritaire. T3 est donc en ex\u00e9cution, T2 endormie et T1 pr\u00e9empt\u00e9e.<\/li>\n<li style=\"text-align: justify;\">La t\u00e2che T3 va devoir acc\u00e9der \u00e0 son tour \u00e0 la ressource partag\u00e9e avec T1. Pour cela elle doit d&rsquo;abord verrouiller le mutex M, toutefois ce dernier est d\u00e9j\u00e0 tenu par T1. T3 va donc s&rsquo;endormir en attendant la lib\u00e9ration de M, et T1 va reprendre son ex\u00e9cution. Jusqu&rsquo;ici tout se d\u00e9roule parfaitement normalement.<\/li>\n<li style=\"text-align: justify;\">A pr\u00e9sent, l&rsquo;\u00e9v\u00e9nement attendu par T2 survient. Cette t\u00e2che va donc se r\u00e9veiller. Comme elle est plus prioritaire que T1, cette derni\u00e8re est pr\u00e9empt\u00e9e et la situation est la suivante:<\/li>\n<\/ol>\n<ul>\n<ul>\n<ul>\n<li style=\"text-align: justify;\">T1 pr\u00e9empt\u00e9e tient le mutex M<\/li>\n<li style=\"text-align: justify;\">T2 s&rsquo;ex\u00e9cute<\/li>\n<li style=\"text-align: justify;\">T3 endormie attend le mutex M<\/li>\n<\/ul>\n<\/ul>\n<\/ul>\n<p style=\"text-align: justify;\">La t\u00e2che T3 est bloqu\u00e9e par l&rsquo;ex\u00e9cution d&rsquo;une t\u00e2che T2 moins prioritaire, nous sommes dans une situation d&rsquo;<em>inversion de priorit\u00e9<\/em>.<\/p>\n<p style=\"text-align: justify;\">Il est normal que T3 soit bloqu\u00e9e par l&rsquo;ex\u00e9cution de T1, car ces deux t\u00e2ches partagent un \u00e9l\u00e9ment commun (une zone m\u00e9moire par exemple) et se synchronisent en utilisant un mutex. Mais il n&rsquo;est pas normal que T3 soit bloqu\u00e9e alors qu&rsquo;une t\u00e2che T2 avec laquelle elle ne partage rien s&rsquo;ex\u00e9cute tranquillement.<\/p>\n<p style=\"text-align: justify;\">Le cas survient g\u00e9n\u00e9ralement lorsque T1 et T2 sont approximativement de m\u00eame priorit\u00e9 et T3 beaucoup plus \u00e9lev\u00e9e (par exemple T1 de priorit\u00e9 10, T2 de priorit\u00e9 12 et T3 de priorit\u00e9 90).<\/p>\n<p style=\"text-align: justify;\">Un exemple typique est le probl\u00e8me survenu au <a title=\"Mars Pathfinder sur Wikipedia\" href=\"http:\/\/fr.wikipedia.org\/wiki\/Mars_Pathfinder\" target=\"_blank\">robot Pathfinder<\/a> lors de sa mission sur Mars en 1997. Un descriptif d\u00e9taill\u00e9 est pr\u00e9sent\u00e9 dans un article de <a title=\"What really happened on Mars?\" href=\"http:\/\/research.microsoft.com\/en-us\/um\/people\/mbj\/Mars_Pathfinder\/Authoritative_Account.html\" target=\"_blank\">Glenn E. Reeves<\/a>. En r\u00e9sum\u00e9 T1 \u00e9tait une t\u00e2che de pilotage d&rsquo;un outil nomm\u00e9 ASI\/MET qui r\u00e9alisait des analyses m\u00e9t\u00e9orologiques (tr\u00e8s basse priorit\u00e9), T3 \u00e9tait la t\u00e2che principale de transfert des donn\u00e9es entre les sous-syst\u00e8mes et T2 \u00e9tait repr\u00e9sent\u00e9 par un ensemble de t\u00e2ches de faibles priorit\u00e9s (mais plus \u00e9lev\u00e9es que T1). Enfin un s\u00e9maphore int\u00e9gr\u00e9 dans l&rsquo;impl\u00e9mentation de l&rsquo;appel syst\u00e8me select() &#8211; invoqu\u00e9 par T1 et par T3 &#8211; jouait le r\u00f4le de notre mutex.<\/p>\n<p style=\"text-align: justify;\">Construisons un premier exemple afin de mettre en relief le comportement lors d&rsquo;une inversion de priorit\u00e9. Dans le programme suivant, nous allons cr\u00e9er trois threads avec des r\u00f4les sp\u00e9cifiques&nbsp;:<\/p>\n<ul>\n<li style=\"text-align: justify;\">T1, de faible priorit\u00e9, d\u00e9marre en premier et verrouille le mutex. Ensuite T1 va r\u00e9aliser un nombre arbitraire de boucles actives (\u00e0 ajuster en fonction de votre processeur afin qu&rsquo;elles durent une dizaine de secondes) simulant un travail consommant du temps CPU, et lib\u00e9rera le mutex avant de se terminer..<\/li>\n<li style=\"text-align: justify;\">T2 de priorit\u00e9 moyenne va dormir deux secondes avant de d\u00e9marrer sa s\u00e9rie de boucles actives d&rsquo;une dizaine de secondes.<\/li>\n<li style=\"text-align: justify;\">T3 de priorit\u00e9 \u00e9lev\u00e9e dort une seconde, verrouille le mutex (et devra donc patienter jusqu&rsquo;\u00e0 la fin de T1) et attaque une s\u00e9rie de boucle active.<\/li>\n<\/ul>\n<p style=\"text-align: justify;\">Pour que l&rsquo;inversion se produise, il est n\u00e9cessaire que les priorit\u00e9s soient fix\u00e9es de mani\u00e8re pr\u00e9cise, aussi utiliserons-nous un ordonnancement temps-r\u00e9el FIFO. Notez que l&rsquo;ex\u00e9cution du programme ne pourra r\u00e9ussir qu&rsquo;avec les droits <em>root<\/em>. Je d\u00e9conseille de r\u00e9aliser une boucle active avec une priorit\u00e9 sup\u00e9rieure ou \u00e9gale \u00e0 50, car c&rsquo;est la priorit\u00e9 par d\u00e9faut des gestionnaires d&rsquo;interruption sur les noyaux Linux r\u00e9cents. Si un handler d&rsquo;interruption est pr\u00e9empt\u00e9 trop longtemps (20 \u00e0 30 secondes), le driver peut cesser de fonctionner, c&rsquo;est souvent le cas avec les cartes Wifi par exemple.<\/p>\n<p style=\"text-align: justify;\"><a title=\"test-inversion.c\" href=\"http:\/\/www.blaess.fr\/christophe\/files\/article-2011-07-29\/test-inversion.c\" target=\"_blank\">Le programme ci-dessous<\/a> n&rsquo;a d&rsquo;int\u00e9r\u00eat que s&rsquo;il y a une lutte pour le CPU entre T1 et T2, ce qui signifie qu&rsquo;ils doivent s&rsquo;ex\u00e9cuter sur le m\u00eame processeur (m\u00eame coeur).<\/p>\n<pre style=\"padding-left: 30px;\"><strong>test-inversion.c<\/strong>\n#define _GNU_SOURCE   \/\/ Pour sched_setaffinty\n#include\n#include\n#include\n#include\n#include\n#include \n\nstatic void * T1 (void * unused);\nstatic void * T2 (void * unused);\nstatic void * T3 (void * unused);\n\nstatic pthread_mutex_t  mutex = PTHREAD_MUTEX_INITIALIZER;\n\nint main(void)\n{\n    cpu_set_t cpu_set;\n    pthread_t t1, t2, t3;\n\n    \/\/ Fixer l'execution sur un seul coeur\n    CPU_ZERO(&amp; cpu_set);\n    CPU_SET(0, &amp; cpu_set); \/\/ Coeur 0\n    if (sched_setaffinity(0, sizeof(cpu_set), &amp; cpu_set) != 0) {\n        perror(\"sched_setaffinity\");\n        exit(EXIT_FAILURE);\n    }\n\n    \/\/ Creer les trois threads\n    if ((pthread_create(&amp; t1, NULL, T1, NULL) != 0)\n     || (pthread_create(&amp; t2, NULL, T2, NULL) != 0)\n     || (pthread_create(&amp; t3, NULL, T3, NULL) != 0)) {\n        fprintf(stderr, \"Erreur de creation des threads\");\n        exit(EXIT_FAILURE);\n    }\n    \/\/ Terminer le thread main\n    pthread_exit(NULL);\n}\n\n#define BOUCLE_1  50000\n#define BOUCLE_2  50000\n\nstatic void * T1 (void * unused)\n{\n    int i;\n    int j;\n    struct sched_param param;\n\n    fprintf(stderr, \"T1 : Creationn\");\n\n    \/\/ passer en temps reel faible priorite\n    param.sched_priority = 5;\n    if (pthread_setschedparam(pthread_self(), SCHED_FIFO, &amp; param) != 0) {\n        perror(\"pthread_setschedparam\");\n        exit(EXIT_FAILURE);\n    }\n\n    \/\/ verrouiller le mutex\n    pthread_mutex_lock(&amp; mutex);\n\n    \/\/ travail actif\n    fprintf(stderr, \"T1 : Debut bouclen\");\n    for (i = 0; i &lt; BOUCLE_1; i ++)\n        for (j = 0; j &lt; BOUCLE_2; j ++)\n            ;\n    fprintf(stderr, \"T1 : Fin Bouclen\");\n\n    \/\/ liberer le mutex\n    pthread_mutex_unlock(&amp; mutex);\n    return NULL;\n}\n\nstatic void * T2 (void * unused)\n{\n    int i;\n    int j;\n    struct sched_param param;\n\n    fprintf(stderr, \"T2 : Creationn\");\n\n    \/\/ passer en temps reel moyenne priorite\n    param.sched_priority = 15;\n    if (pthread_setschedparam(pthread_self(), SCHED_FIFO, &amp; param) != 0) {\n        perror(\"pthread_setschedparam\");\n        exit(EXIT_FAILURE);\n    }\n    \/\/ attendre deux secondes\n    sleep(2);\n    \/\/ travail actif\n    fprintf(stderr, \"T2 : Debut bouclen\");\n    for (i = 0; i &lt; BOUCLE_1; i ++)\n        for (j = 0; j &lt; BOUCLE_2; j ++)\n            ;\n    fprintf(stderr, \"T2 : Fin bouclen\");\n    return NULL;\n}\n\nstatic void * T3 (void * unused)\n{\n    int i;\n    int j;\n    struct sched_param param;\n\n    fprintf(stderr, \"T3 : Creationn\");\n\n    \/\/ passer en temps reel haute priorite\n    param.sched_priority = 45;\n    if (pthread_setschedparam(pthread_self(), SCHED_FIFO, &amp; param) != 0) {\n        perror(\"pthread_setschedparam\");\n        exit(EXIT_FAILURE);\n    }\n    \/\/ attendre une seconde\n    sleep(1);\n    \/\/ prendre le mutex\n    pthread_mutex_lock(&amp; mutex);\n    \/\/ travail actif\n    fprintf(stderr, \"T3 : Debut bouclen\");\n    for (i = 0; i &lt; BOUCLE_1; i ++)\n        for (j = 0; j &lt; BOUCLE_2; j ++)\n            ;\n    fprintf(stderr, \"T3 : Fin bouclen\");\n    \/\/ liberer le mutex\n    pthread_mutex_unlock(&amp;mutex);\n\n    return NULL;\n}<\/pre>\n<p style=\"text-align: justify;\">Pensez \u00e0 calibrer les valeurs de <code>BOUCLE_1<\/code> et <code>BOUCLE_2<\/code> pour que l&rsquo;ex\u00e9cution du programme dure une dizaine de secondes. Voici un r\u00e9sultat d&rsquo;ex\u00e9cution&nbsp;:<\/p>\n<pre># <strong>cc test-inversion.c -o test-inversion -pthread -lrt -Wall<\/strong>\n# <strong>.\/test-inversion<\/strong>\nT3 : Creation\nT2 : Creation\nT1 : Creation\nT1 : Debut boucle\nT2 : Debut boucle\nT2 : Fin boucle\nT1 : Fin Boucle\nT3 : Debut boucle\nT3 : Fin boucle\n#<\/pre>\n<p style=\"text-align: justify;\">Nous voyons bien que le thread T2 s&rsquo;ex\u00e9cute int\u00e9gralement avant que T3 puisse obtenir le mutex et commencer son travail. C&rsquo;est un cas d&rsquo;inversion de priorit\u00e9<\/p>\n<h1>Un rem\u00e8de<\/h1>\n<p style=\"text-align: justify;\">Il existe un rem\u00e8de aux probl\u00e8mes d&rsquo;inversions de priorit\u00e9s&nbsp;: le syst\u00e8me <strong>PIP<\/strong>. Toutefois pr\u00e9cisons tout de suite qu&rsquo;il ne fonctionne pas dans tous les cas. Il permit de sauver le robot Pathfinder mais il existe d&rsquo;autres situations dans lesquelles le rem\u00e8de est pire que le mal et l&rsquo;on risque de voir les performances du syst\u00e8me s&rsquo;effondrer.<\/p>\n<p style=\"text-align: justify;\">PIP (<em>priority inheritance protocol<\/em>) est un m\u00e9canisme reposant sur deux r\u00e8gles et sur une valeur de priorit\u00e9 appliqu\u00e9e \u00e0 chaque mutex (et \u00e9ventuellement \u00e0 toutes les ressources de synchronisation). Reprenons la situation d\u00e9crite plus haut, T1 est actif, T2 et T3 dorment. T1 verrouille le mutex. La premi\u00e8re r\u00e8gle de PIP s&rsquo;applique alors&nbsp;:<\/p>\n<ul>\n<li><strong>Premi\u00e8re r\u00e8gle<\/strong> : un mutex h\u00e9rite de la priorit\u00e9 la plus \u00e9lev\u00e9e parmi celles de tous les threads qui essayent de le verrouiller<\/li>\n<\/ul>\n<p style=\"text-align: justify;\">Le thread T1 s&rsquo;ex\u00e9cutant avec la priorit\u00e9 P1, le mutex h\u00e9rite alors de cette valeur.<\/p>\n<p style=\"text-align: justify;\">Ensuite T3 se r\u00e9veille et demande \u00e0 son tour le mutex. La priorit\u00e9 de celui-ci monte alors \u00e0 la valeur P3. La seconde r\u00e8gle s&rsquo;applique<\/p>\n<ul>\n<li><strong>Seconde r\u00e8gle<\/strong> : un thread s&rsquo;ex\u00e9cute avec la priorit\u00e9 la plus \u00e9lev\u00e9e parmi celles de tous les mutex qu&rsquo;il a verrouil\u00e9.<\/li>\n<\/ul>\n<p style=\"text-align: justify;\">Le thread T1 est alors propuls\u00e9 \u00e0 la priorit\u00e9 P3. Le thread T2 se r\u00e9veille, mais sa priorit\u00e9 \u00e9tant \u00e0 pr\u00e9sent inf\u00e9rieure \u00e0 celle de T1 il ne peut le pr\u00e9empter, et T1 va s&rsquo;ex\u00e9cuter int\u00e9gralement, rel\u00e2cher le mutex, redrescendre alors \u00e0 la priorit\u00e9 P1. T3 pourra alors prendre le mutex et s&rsquo;ex\u00e9cuter avant enfin de laisser passer T2.<\/p>\n<p style=\"text-align: justify;\"><a title=\"test-pip.c\" href=\"http:\/\/www.blaess.fr\/christophe\/files\/article-2011-07-29\/test-pip.c\" target=\"_blank\">Dans le programme suivant<\/a>, j&rsquo;ai ajout\u00e9 quelques lignes au d\u00e9but de la fonction <code>main()<\/code>, qui activent le protocol PIP sur le mutex.<\/p>\n<pre><strong>test-pip.c<\/strong>\n[...]\nint main(void)\n{\n    cpu_set_t cpu_set;\n    pthread_t t1, t2, t3;\n\n    pthread_mutexattr_t attr;\n    pthread_mutexattr_init(&amp; attr);\n    pthread_mutexattr_setprotocol(&amp; attr, PTHREAD_PRIO_INHERIT);\n    pthread_mutex_init(&amp; mutex, &amp; attr);\n\n    \/\/ Fixer l'execution sur un seul coeur\n    [...]<\/pre>\n<pre>Voyons l'ex\u00e9cution avec PIP :\n# <strong>cc test-pip.c -o test-pip -pthread -lrt -Wall<\/strong>\n# <strong>.\/test-pip <\/strong>\nT3 : Creation\nT2 : Creation\nT1 : Creation\nT1 : Debut boucle\nT1 : Fin Boucle\nT3 : Debut boucle\nT3 : Fin boucle\nT2 : Debut boucle\nT2 : Fin boucle\n#<\/pre>\n<p style=\"text-align: justify;\">Cette fois, T3 a la possibilit\u00e9 de s&rsquo;ex\u00e9cuter enti\u00e8rement avant que T2 d\u00e9marre. L&rsquo;inversion de priorit\u00e9 est \u00e9vit\u00e9e.<\/p>\n<h1>Conclusion<\/h1>\n<p style=\"text-align: justify;\">La vraie solution au probl\u00e8me d&rsquo;inversion de priorit\u00e9 r\u00e9side dans la conception du syst\u00e8me temps-r\u00e9el. Il ne faut pas laisser de t\u00e2ches de priorit\u00e9s interm\u00e9diaires entre deux t\u00e2ches partageant des ressources communes. Naturellement ceci est tr\u00e8s contraignant et ne peut pas toujours \u00eatre respect\u00e9. PIP repr\u00e9sente un correctif plus qu&rsquo;une v\u00e9ritable solution, mais il a le m\u00e9rite d&rsquo;\u00eatre facilement activable sur les mutex Posix.<\/p>","protected":false},"excerpt":{"rendered":"<p>Dans les applications temps-r&eacute;el il est tr&egrave;s important d&rsquo;&eacute;viter les situations dites &laquo;&nbsp;d&rsquo;inversion de priorit&eacute;&nbsp;&raquo;. Il s&rsquo;agit de cas dans lesquels une t&acirc;che de haute priorit&eacute; est bloqu&eacute;e en attente de la terminaison d&rsquo;une t&acirc;che de plus faible priorit&eacute; alors qu&rsquo;elles n&rsquo;ont rien en commun.<\/p>","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[8,14],"tags":[],"class_list":["post-1011","post","type-post","status-publish","format-standard","hentry","category-linux-2","category-temps-reel"],"_links":{"self":[{"href":"https:\/\/www.blaess.fr\/christophe\/wp-json\/wp\/v2\/posts\/1011","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=1011"}],"version-history":[{"count":0,"href":"https:\/\/www.blaess.fr\/christophe\/wp-json\/wp\/v2\/posts\/1011\/revisions"}],"wp:attachment":[{"href":"https:\/\/www.blaess.fr\/christophe\/wp-json\/wp\/v2\/media?parent=1011"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.blaess.fr\/christophe\/wp-json\/wp\/v2\/categories?post=1011"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.blaess.fr\/christophe\/wp-json\/wp\/v2\/tags?post=1011"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}