De quoi est fait une infrastructure [2] ? De logiciel et d’outils logiciels de base…
par Jean-Luc Dormoy
Le miracle de l’informatique est, si l’on voulait être ironique, que l’on peut réaliser un même système de façon aussi complexe qu’on le souhaite. Evidemment, il n’y aurait pas de gain à cette fuite de la simplicité si elle ne s’accompagnait d’un gain en généralité. Chaque progrès de l’infrastructure correspond à une généralisation de la plate-forme d’exécution et à une extension de ses utilisations potentielles.
Tout d’abord la « machine nue » s’est vite avérée impraticable. Le langage dans lequel le logiciel doit être exprimé (le « langage machine ») est très difficilement appréhendable par l’être humain. On a donc inventé des langages dits « de haut niveau » pour faciliter la tâche du programmeur. Il s’est agi d’assembleurs, qui restent proches de la machine, mais utilisent autre chose que des « 0 » et des « 1 », puis de langages plus évolués, pour lesquels on a conçu des compilateurs et des interpréteurs capables de les exécuter par traduction en langage machine avant ou pendant leur exécution.
L’autre constatation a été qu’il était nécessaire pour tout programme d’introduire de façon répétitive certaines fonctionnalités, qui relevaient de tâches génériques à l’exécution de tout programme – comme la façon dont la machine doit réagir en cas d’erreur, dont elle doit communiquer avec l’extérieur via des périphériques, ou de la façon de structurer la mémoire en fichiers – ou qui facilitaient la gestion de la machine indépendamment des logiciels qui s’exécutent – comme de donner l’accès à plusieurs utilisateurs. On a alors regroupé toutes ces fonctions en un ensemble cohérent et autonome, le système d’exploitation.
On a donc dès lors disposé d’une machine équivalente dans le principe, c’est-à-dire une machine universelle programmable; mais la programmation était rendue plus facile par le niveau du langage de programmation, et l’existence de toute une série de services et de fonctions de base. On a généralisé la machine physique via des couches logicielles.
Détaillons cela.
Le développement de l’informatique a conduit à créer des langages plus facilement accessibles aux êtres humains, et possédant en outre des propriétés particulières adaptées aux applications considérées, par exemple de prendre en compte le temps réel pour les applications de systèmes embarqués interagissant directement avec le monde physique ou de commande d’un système, ou de vastes ensembles de données pour les applications en entreprise. Pour passer du langage d’écriture du logiciel au langage machine, qui est beaucoup plus frustre, on utilise un compilateur ou un interpréteur. Un compilateur est un traducteur traduisant le logiciel écrit en langage source en un logiciel écrit en langage machine. Bien sûr le langage machine dépend … de la machine, c’est-à-dire du processeur utilisé. Le compilateur est conçu de façon à réutiliser le maximum de sa capacité de traduction indépendamment de la machine particulière, et de n’avoir à adapter qu’un back end pour toute nouvelle machine pour laquelle on veut pouvoir compiler. Dans les compilateurs modernes, le front end lui-même est adaptable, pour rendre le compilateur capable de traduire plusieurs langages sources. Les compilateurs sont devenus des outils d’une grande complexité, qui permettent de compiler les logiciels par morceaux pour assembler les morceaux plus tard, voire de tester diverses options de compilation, ou d’apprendre de leur exécution afin d’optimiser la traduction.
Exécuter un programme en langage source en utilisant un compilateur exige deux étapes : d’abord traduire en langage machine; puis utiliser le logiciel dit exécutable ainsi obtenu sur les données d’exécution fournies par ailleurs. L’avantage est que l’on ne doit pas traduire à nouveau pour utiliser d’autres données. Lorsque l’on achète un logiciel dont le code source – c’est-à-dire l’expression du logiciel dans le langage de programmation utilisé par les développeurs – n’est pas fourni, c’est en réalité le logiciel en langage exécutable que l’on acquiert, qui a été obtenu par le vendeur en compilant le logiciel en langage source, qu’il garde secret. Il est en effet difficile – mais pas impossible – de reconstituer un code source à partir du code exécutable – cela s’appelle du reverse engineering. C’est en outre le plus souvent illégal. L’open source, comme Linux et sa galaxie, consiste à fournir aussi à l’utilisateur sous certaines conditions le « code source », ou logiciel en langage source.
Un interpréteur ressemble à un compilateur, sinon que la traduction se fait « à la volée » au moment de l’exécution du programme. Cela a l’avantage de supprimer la phase initiale de traduction. Néanmoins, il faudra procéder à cette traduction d’une manière ou d’une autre au moment de l’exécution, ce qui ralentit celle-ci et demande plus de ressources. Il faut aussi disposer du code source à l’exécution, ce qui est gênant si vous êtes un vendeur de logiciel qui ne souhaitez pas diffuser votre code source. Néanmoins, l’interprétation a des avantages que n’a pas la compilation. La plus importante est de pouvoir insérer à la volée au cours de l’interprétation des morceaux de calcul, selon les besoins ou le contexte, voire de modifier le cours du calcul, notamment en modifiant le code source. Cela peut paraître curieux de vouloir modifier, de surcroît à l’exécution, le logiciel fourni par le concepteur. Mais le concepteur peut avoir lui-même prévu cette possibilité. Le but de telles manipulations est par exemple de savoir réagir en cas de problème – résultat intermédiaire jugé incohérent – de s’adapter au contexte, par exemple lorsqu’il s’agit de la résolution de problèmes complexes, d’insérer dynamiquement de nouveaux éléments de connaissance non prévus à l’avance, par exemple obtenus par apprentissage.
Bien sûr, les informaticiens ont inventé toute une série d’intermédiaires entre compilation et interprétation, nous allons en voir quelques exemples importants.
L’autre grand progrès rendant une machine isolée plus pratique à utiliser a été d’insérer entre le logiciel applicatif et le processeur un système d’exploitation, on l’a déjà introduit. Un système d’exploitation est un logiciel faisant partie dans notre classification de l’infrastructure et qui assure des fonctions de base pour l’ensemble des applications susceptibles d’être développées. Par exemple, le système d’exploitation simplifie les relations avec les matériels périphériques (mémoire, entrées-sorties, …), applique des traitements standards en cas de défaillance du matériel ou d’opération interdite par le logiciel. Souvent, le système d’exploitation permet de faire tourner plusieurs tâches logicielles « en même temps », tâches qui correspondent à un seul et même ensemble logique, ou simplement à divers logiciels partageant la même ressource d’exécution. « En même temps » signifie que le système d’exploitation donne aux tâches accès aux ressources de calcul « chacune à son tour », selon des règles diverses mais précisées, donnant ainsi le sentiment que les tâches sont exécutées simultanément – en réalité elles avancent un petit peu chacune à leur tour. Pour un ordinateur partagé entre plusieurs utilisateurs, le système d’exploitation peut également leur donner accès aux ressources de calcul simultanément. Dans ce cas des droits et devoirs sont connus pour chaque utilisateur, et le système d’exploitation est là pour les faire respecter. Enfin, le système d’exploitation peut être adapté à certains contextes, comme par exemple les systèmes temps réel interagissant avec le monde physique ou un système de commande – systèmes embarqués. Il offre alors certaines garanties, comme l’assurance que certaines fonctions seront correctement terminées avant un certain laps de temps physique, ou qu’aucune tâche ne sera indéfiniment remise à plus tard.
En pratique, il est très difficile d’utiliser une machine sans système d’exploitation, même réduit à sa plus simple expression. On trouve maintenant des systèmes d’exploitation dans des systèmes d’apparence aussi simple que des cartes à puce. Dans ce cas, le système d’exploitation est « comprimé » au maximum, c’est-à-dire qu’il est programmé de façon à utiliser un minimum de ressources en mémoire et en temps d’exécution, pour laisser le maximum de ressources aux applications elles-mêmes. Mais dans les domaines à maturité, comme les PC, le système d’exploitation peut devenir très gros, incluant de plus en plus de fonctionnalités, comme l’interface graphique avec Windows, voire l’accès au web avec Internet Explorer avant que la loi n’oblige à séparer cette fonction des systèmes d’exploitation. De tels OS sont des monstres de plusieurs dizaines de millions de lignes de code.
Langages évolués et systèmes d’exploitation sont des « dévoreurs de performance ». En effet, si l’on consacre tout l’effort humain nécessaire à l’écriture d’un programme en langage machine, on aura presque certainement un programme plus efficace que s’il est écrit en langage de haut niveau, puis compilé ou interprété. De la même façon, l’utilisation d’un système d’exploitation consomme une partie des ressources de la machine sur lequel il s’exécute, laissant ainsi autant de moins pour les tâches applicatives « utiles ». Mais l’effort humain requis pour s’en passer serait considérable, et ce pour chaque application. En outre les erreurs commises (« bugs ») et la mise au point seraient probablement rédhibitoires. On ne peut donc tout simplement pas s’en passer.
Mais cette consommation de ressources par « l’intendance » de l’infrastructure n’est pas très grave : elle est payée par la loi de Moore ! En effet, la puissance de calcul ainsi consommée est fournie par un peu plus de 3 mois de développement de la loi de Moore – au cas où cette puissance représente 1/8 des ressources disponibles. Trois mois plus tard on retrouve donc une machine avec la même puissance de calcul, mais avec un confort de développement infiniment supérieur !
En d’autres termes, une partie de la puissance de calcul supplémentaire fournie par la loi de Moore est allouée au concepteur d’application, pas à son utilisateur. Mais ce dernier y gagne bien évidemment, car il dispose alors d’applications beaucoup plus puissantes, fiables, et beaucoup plus rapidement !
D’autres types de logiciel intermédiaire et d’infrastructure entre le logiciel applicatif et le hardware sont apparus au cours des deux dernières décennies. Le premier est la notion de machine virtuelle. Une machine virtuelle peut tourner sur un processeur nu, ou au-dessus d’un système d’exploitation. Les plus connues sont la machine virtuelle Java et celle liée à l’environnement .NET de Microsoft. Une machine virtuelle, comme l’indique son nom, est un logiciel qui recrée l’apparence d’une machine sur laquelle exécuter un programme. Un logiciel écrit en langage source sera donc compilé avec pour langage cible le langage compris par la machine virtuelle, comme s’il s’agissait d’une machine réelle. Cependant, le code ainsi obtenu n’est pas exécutable par le processeur, mais interprété par la machine virtuelle. La machine virtuelle est donc un interpréteur d’un langage qui n’est pas accessible directement aux concepteurs humains, mais n’est pas non plus le langage du processeur.
Les avantages des machines virtuelles sont multiples. Tout d’abord, le langage interprété de la machine virtuelle est indépendant de la machine réelle sur laquelle la machine virtuelle s’exécute. Un même logiciel peut ainsi s’exécuter sur n’importe quelle machine réelle disposant d’une machine virtuelle qui a été préparée pour s’exécuter sur cette machine réelle. Cela n’est d’ailleurs pas très difficile, la machine virtuelle est elle-même un logiciel écrit dans un langage source, qu’il a suffi de compiler vers la machine réelle considérée, pour laquelle il faut disposer d’un compilateur. C’est cet avantage qui a rendu populaire le langage Java et sa machine virtuelle aux débuts de l’Internet commercial : un même logiciel – connu alors sous le nom d’applet – pouvait s’exécuter sur n’importe quelle machine de l’utilisateur. Le browser web – aujourd’hui Mozilla, Internet Explorer, Chrome ou Safari – dispose en natif d’une machine virtuelle Java. On s’émancipe ainsi des idiosyncrasies des machines réelles, et on développe « pour le web ». D’autres avantages sont ceux déjà mentionnés pour l’interprétation : en effet, le code généré par le compilateur est interprété par la machine virtuelle, et celle-ci peut donc insérer à la volée en tant que de besoin toute action pertinente. On se sert de ce principe par exemple pour de la sécurité, ou pour exécuter des programmes complexes sur des plates-formes de calcul aux ressources très restreintes, par exemple des cartes à puce. On réunit donc en quelque sorte les avantages de la compilation et de l’interprétation. Le seul défaut de l’approche est qu’il reste un overhead, c’est-à-dire un surcoût à l’exécution dû à l’interprétation. Mais comme on l’a vu, ce surcoût est absorbé en quelques mois de loi de Moore.
Enfin, une machine virtuelle peut être prévue pour exécuter plusieurs tâches en même temps, tâches dont l’exécution s’entrecroise en se divisant puis en se réunissant – on parle alors de threads, pour « fils » d’exécution. Il s’agit d’une forme « faible » de parallélisme, de surcroît partageant une même mémoire, faible parce que les tâches sont indépendantes et ne participent pas à un même objectif de calcul – par exemple les threads correspondent à plusieurs utilisateurs connectés simultanément à un site web et exécutant ainsi le logiciel mettant en œuvre le comportement de ce site sur ses serveurs.
Une autre approche intermédiaire entre logiciel applicatif et hardware d’exécution est constituée par ce que l’on appelle la virtualisation – qu’il ne faut pas confondre avec les machines virtuelles. Ici, on souhaite prendre toute la pile logicielle s’exécutant sur une machine réelle nue – c’est-à-dire que l’on prend le code applicatif, mais aussi l’OS, les éventuelles machines virtuelles, etc. – on prend tout tel quel, et on souhaite exécuter ce tout sur une autre machine réelle nue, sans aucunement modifier le logiciel. D’ailleurs cela serait impossible, car on n’a a priori que la version exécutable de ces logiciels, pas les codes sources. Pour cela on doit écrire un simulateur de la machine réelle n°1 dans le langage de la machine réelle n° 2.
Cela est possible, car fondamentalement une machine – générique, un ordinateur universel – est un interpréteur d’instructions chaînées dans un programme. Chaque machine a donc son jeu d’instruction et ses règles d’interprétation[1]. Et la réalisation d’une machine réelle consiste à mettre en œuvre un interpréteur de son jeu d’instruction sous la forme d’un circuit gravé dans le silicium. C’est ce que vous achetez lorsque vous achetez un Atom ou un ARM : vous achetez un interpréteur des jeux d’instructions correspondants mis en œuvre « en dur » dans du silicium. Dans la virtualisation, on va donc également mettre en œuvre un tel interpréteur du jeu d’instructions de la machine n°1, pas sous forme d’un circuit, mais d’un logiciel s’exécutant sur la machine n°2. On peut réaliser ce logiciel dans n’importe quel langage source pour lequel on dispose d’un compilateur ciblant la machine n°2. Une fois que vous aurez fait cela, vous pourrez « poser » la pile logicielle qui tournait sur la machine n°1 nue au-dessus du virtualisateur tournant sur la machine n°2. Le comportement sera logiquement équivalent, les seules différences seront de performance, car les machines n°1 et 2 n’ont pas a priori les mêmes performances, et car il y a là aussi un overhead de l’exécution du virtualisateur au-dessus de la machine n°2. Néanmoins, l’avantage est considérable, car on n’a pas à retoucher ni à réécrire aucun élément de l’empilement logiciel qui tournait sur la machine n°1.
Transformer ou adapter du logiciel existant peut revenir très cher lorsque le logiciel a grossi. Cette propriété des virtualisateurs les a rendus très populaires dans les infrastructures de systèmes d’information des entreprises, en permettant l’upgrade de machine sans changement dans le logiciel – en effet la loi de Moore vous fournit sans arrêt des machines de base plus puissantes, ce qui vous incite à les changer pour rendre plus performantes vos applications. Mais la virtualisation a d’autres utilisations, notamment en permettant de simuler plusieurs machines totalement indépendantes sur une même machine physique. Plusieurs logiciels pourront ainsi tourner au sein du virtualiseur sur une même machine physique, mais d’une façon réellement plus indépendante que des tâches tournant sur un système d’exploitation multitâche ou via un système de threads. Cette indépendance est sensible en cas de crash d’un des logiciels : on est certain que les autres logiciels en cours d’exécution ne seront aucunement affectés, alors que tout utilisateur d’un ordinateur personnel sait par expérience qu’un crash d’une tâche peut provoquer le crash complet de la machine. Outre cet avantage technique, cela est très important pour la sécurité et la sûreté, ainsi que pour la responsabilité des créateurs des divers logiciels lorsque ceux-ci sont critiques ou liés à des enjeux commerciaux. Imaginez par exemple que votre box à la maison supporte des services de divers fournisseurs, et que l’un de ces services en se « plantant » « plante » ceux des autres fournisseurs – par exemple qu’une panne du service vidéo stoppe le chauffage en hiver – vous ne serez pas content, et les autres fournisseurs encore moins, non plus d’ailleurs que l’opérateur de la box. La virtualisation est aussi utilisée pour des raisons semblables dans l’automobile, où des logiciels de divers équipementiers peuvent tourner sur une même infrastructure de calcul.
[1] Les jeux d’instruction sont en général protégés légalement et propriétés d’un acteur du hardware, qui peut ou pas concéder des licences d’exploitation.
Moore’s Law and the Future of [Technology] Economy de Jean-Luc Dormoy est mis à disposition selon les termes de la licence Creative Commons Attribution – Pas d’Utilisation Commerciale – Partage à l’Identique 3.0 non transposé.
Basé(e) sur une oeuvre à mooreslawblog.com.