Autrefois, vous aviez un ordinateur et il faisait une chose à la fois. Au sens propre. Vous chargeriez vos cartes ou votre bande perforée ou quoi que ce soit et appuyez sur un bouton. L’ordinateur lirait votre programme, l’exécuterait et recracherait les résultats. Ensuite, il se rendormirait jusqu’à ce que vous lui donniez plus d’informations.
Le problème est que les ordinateurs – surtout à l’époque – étaient chers. Et pour un programme typique, l’ordinateur passe beaucoup de temps à attendre que la prochaine carte perforée apparaisse ou que la bande magnétique arrive à la bonne position. Dans ces cas, l’ordinateur tapait du pied au sens figuré en attendant le prochain événement.
Quelqu’un d’intelligent s’est rendu compte que l’ordinateur pouvait travailler sur autre chose pendant qu’il attendait, vous devriez donc alimenter plus d’un programme à la fois. Lorsque le programme A attend une opération d’E/S, le programme B peut progresser. Bien sûr, si le programme A n’effectuait aucune entrée/sortie, le programme B était affamé, nous avons donc inventé le multitâche préemptif. Dans ce schéma, le programme A s’exécute jusqu’à ce qu’il ne puisse plus s’exécuter ou jusqu’à ce qu’une limite de temps prédéfinie se produise, selon la première éventualité. Si le temps expire, le programme est forcé de dormir un peu pour que le programme B (et d’autres programmes) ait son tour. C’est ainsi que fonctionnent pratiquement tous les ordinateurs modernes en dehors des minuscules systèmes embarqués.
Mais il y a une différence. La plupart des ordinateurs disposent désormais de plusieurs processeurs et de moyens spéciaux pour changer rapidement de tâche. Le bureau sur lequel j’écris ceci a 12 processeurs et chacun peut agir comme deux processeurs. Ainsi, l’ordinateur peut exécuter jusqu’à 12 programmes à la fois et en avoir 12 de plus qui peuvent remplacer très rapidement n’importe lequel des 12 actifs. Bien sûr, le système d’exploitation peut également activer et désactiver les programmes de cette pile de 24, vous pouvez donc exécuter beaucoup plus que cela, mais le basculement entre le 12 principal et le 12 de secours est extrêmement rapide.
Ainsi, le cas est plus fort que jamais pour écrire votre solution en utilisant plus d’un programme. Il y a beaucoup d’avantages. Par exemple, j’ai pris en charge un programme qui effectuait de nombreux calculs, puis j’ai passé des heures à imprimer les résultats. J’ai séparé l’impression pour séparer les tâches sur différentes imprimantes et j’ai réduit environ 80 % du temps d’exécution, ce qui représentait presque une journée lorsque j’ai commencé. Mais même en dehors des performances, l’isolation des processus est comme l’encapsulation ultime. Les choses que vous faites dans le programme A ne devraient pas pouvoir affecter le programme B. Tout comme nous isolons le code dans les modules et les objets, nous pouvons aller plus loin et les isoler dans les processus.
Épée à double tranchant
Mais c’est aussi un problème. Vraisemblablement, si vous voulez que deux programmes coopèrent, ils doivent s’influencer d’une manière ou d’une autre. Vous pouvez simplement utiliser un fichier pour parler entre eux, mais c’est notoirement inefficace. Ainsi, les systèmes d’exploitation comme Linux fournissent des communications interprocessus IPC. Tout comme vous rendez publiques certaines parties d’un objet, vous pouvez exposer certaines choses de votre programme à d’autres programmes.
La façon la plus fondamentale de le faire est avec le fork
appel. Lorsque vous forkez un nouveau processus, le nouveau processus est une copie totale de son parent. Vous ne vous en rendez pas toujours compte parce que la prochaine chose que vous faites souvent est d’appeler quelque chose comme exec
pour charger un nouveau programme ou vous utilisez un système de type wrapper qui appelle fork
et exec
pour toi. Mais chaque fois que tu cours, dis, ls
à l’invite de commande, vous exécutez ls
programme commence sa vie comme une copie totale du shell. Cette copie charge alors le ls
exécutable et l’exécute.
Et si ce n’était pas le cas ? C’est ainsi que mon rédacteur de rapport a travaillé. Les grands calculs, qui ont pris des heures sur un ordinateur Sequent avec beaucoup de processeurs, se sont déroulés en un seul processus. Au moment d’imprimer, j’ai créé un tas de sous-processus. Chacun avait une copie totale des données que j’ai ensuite traitées en lecture seule et j’ai commencé à imprimer. C’est une façon de communiquer entre les processus.
Une autre façon est les tuyaux. Imaginez une ligne de commande comme :
cat data.txt | sort | more
Ici, vous créez trois processus. L’un vide les données d’un fichier texte. Il envoie ces données à un tuyau qui est connecté au sort
programme. Il sort vers un autre tuyau qui est connecté au more
programme.
Sens Unique
Les canaux comme celui-ci sont à sens unique, mais vous pouvez créer des canaux nommés et en parler dans les deux sens. Vous pouvez faire les deux dans le shell — mknod
crée un canal nommé – mais vous pouvez également faire les deux dans un programme. (popen
est très facile à utiliser pour les tuyaux réguliers et il y a un mknod
Appel API également.)
Il existe plusieurs autres méthodes que vous pouvez utiliser pour parler à travers les processus :
- Files d’attente de messages – Un moyen d’envoyer des messages à un autre processus de manière asynchrone
- Sémaphores – Un moyen de partager un compteur avec un autre programme
- Mémoire partagée – Partager un bloc de mémoire
- Signal – Vous pouvez envoyer des signaux à d’autres processus qui peuvent être utilisés comme forme de communication
Vous pourriez vous demander pourquoi vous avez besoin de quelque chose au-delà de la mémoire partagée. Honnêtement, vous ne le faites pas, mais il est plus facile dans de nombreux cas d’utiliser une méthode différente. Le problème est que vous avez besoin d’un moyen d’avoir une opération atomique et des choses comme les sémaphores gèrent cela pour vous. Imaginez si nous avions une variable en mémoire partagée appelée busy
. Si busy
est 1, alors nous savons que nous ne devrions pas modifier les données dans notre mémoire partagée parce que quelqu’un les utilise.
On pourrait écrire :
while (busy) ; // wait for busy==0 busy=1; do_stuff(); busy=0;
Ça a l’air génial, non ? Non. Quelque part dans le CPU, cette boucle while ressemble à ceci :
while_loop2384: TST busy ; set flags on busy JNZ while_loop2384 ; if no zero flag, jump MOV busy,#1 ; move 1 to busy
La plupart du temps, cela fonctionnera bien. Le plus souvent. Mais que se passe-t-il si je fais le TST
instruction, puis je suis mis en veille pour qu’un autre programme puisse exécuter le même code ? Ou un autre processeur exécute exactement le même code au même moment ? Cela peut arriver. Ainsi, les deux programmes verront maintenant que busy
est zéro. Ensuite, ils fixeront tous les deux busy
à 1 et continuer. C’est un échec.
Les sémaphores gèrent cela via un mécanisme d’accès atomique qui permet au programme de tester et de définir l’opération en un seul endroit. Il y a plus à craindre, comme ce qui se passe si j’attends que le processus B libère un sémaphore et que le processus B attend que j’en libère un autre. Mais cette situation – l’impasse – est un sujet pour l’avenir, avec d’autres pièges comme l’inversion de priorité.
En cours
J’ai un petit problème inventé. Sous Linux, si je tape df
Je peux découvrir toutes les choses montées et leurs caractéristiques. Mais cette liste inclut des éléments tels que le répertoire racine et le fichier d’échange. Et si vous vouliez simplement lire les périphériques de boucle et afficher le même format de sortie ? Il existe de nombreuses façons de le faire, bien sûr. Vous pouvez lire les fichiers de boucle dans /etc/mtab, puis lire les autres données à partir de /sys ou de tout autre endroit où elles résident. Ca m’a l’air d’être beaucoup de boulot.
Bien sûr, courir df
nous y amène presque. En fait, je pourrais simplement exécuter un pipeline dans le shell pour obtenir ce que je veux, en quelque sorte :
df | grep '^/dev/loop'
Cela fonctionne mais la sortie est brouillée. Sur mon système, /dev/loop3 est le premier et /dev/loop0 est le dernier et il n’y a aucune raison claire pour laquelle le numéro 4 se situe entre 8 et 14. Je veux donc le trier. Passer par le tri n’aide pas beaucoup car il va trier par ordre alphabétique. Vous pourriez penser au drapeau -n pour sort
, mais cela ne fonctionnera pas car le nombre se trouve à la fin de la chaîne. Bien sûr, je pourrais utiliser une combinaison étrange de cut
ou sed
pour peut-être régler tout ça, mais ça devient trop compliqué. Écrivons simplement du code C.
La première étape consiste à obtenir df
pour tout imprimer et capturer la sortie. Puisque nous voulons traiter la sortie, nous devons lire un tube, et popen()
est un moyen facile de le configurer :
#include <stdio.h> int main(int argc, char * argv[]) { // This part reads the output of DF into the lines array (with some modifications) FILE * result = popen("df", "r"), * sort; int i; if (!result) { perror("Can't open df"); return 1; } while (!feof(result)) { int c = getc(result); if (c != EOF) putchar(c); } pclose(result); return 0; }
À moitié résolu
C’est la moitié du problème résolu. Si vous avez les caractères, vous pouvez faire tout le tri et le filtrage que vous voulez, mais… attendez une minute ! Je suis toujours paresseux. Alors demandons au shell de nous aider. Voici mon plan. Je sais que je ne veux que des lignes qui commencent par /dev/loop alors faisons ceci :
- Lire une ligne entière à la fois
- Si ce n’est pas une ligne /dev/loop, jetez-la
- S’il s’agit d’une ligne /dev/loop, enregistrez-la dans un tableau mais coupez la partie /dev/loop
- Une fois que nous avons toutes les lignes, demandez au shell de trier, puis ajoutez le /dev/loop après le tri
Assez facile:
#include <stdio.h> #include <string.h> char buffer[4097]; char * lines[512]; unsigned int maxline = 0; int main(int argc, char * argv[]) { // This part reads the output of DF into the lines array (with some modifications) FILE * result = popen("df", "r"), * sort; int i; if (!result) { perror("Can't open df"); return 1; } while (!feof(result)) { // get a line from df char * rc = fgets(buffer, sizeof(buffer), result); // only save lines that start with /dev/loop if (rc &amp;&amp; !strncmp(buffer, "/dev/loop", 9)) { // out of space if (maxline >= sizeof(lines) / sizeof(char * )) { fprintf(stderr, "Too many loopsn"); return 2; } lines[maxline++] = strdup(buffer + 9); // copy just the number part and the rest of the line // should check lines[maxline[1] for null here } } pclose(result); // Now we are going to print through sort // The sed replaces the /dev/loop at the front of the line after sorting sort = popen("sort -n | sed 's/^/\/dev\/loop/'", "w"); if (!sort) { perror("Can't open sort"); return 3; } // for each line, send to pipe (note order didn't really matter here ;-) for (i = 0; i < maxline; i++) fputs(lines[i], sort); pclose(sort); return 0; }
Et voila. Oui, vous pouvez le faire avec le shell seul, mais ce serait beaucoup plus difficile à moins que vous n’ayez recours à un autre langage de programmation comme awk et que vous n’utilisiez pas seulement un shell. De plus, cela fait un bel exemple et il y a beaucoup de choses que vous pourriez faire comme ça qui seraient très difficiles à faire autrement.
Vous vous demandez peut-être si vous pourriez créer quelque chose comme sort
et les deux l’alimentent en entrée et lisent sa sortie. La réponse est oui, mais pas avec popen()
. le popen()
l’appel n’est qu’un emballage pratique pipe
et fork
. Si vous vouliez faire les deux extrémités, vous devriez utiliser le pipe()
appeler directement (deux fois) puis exécuter sort
ou peu importe. Mais c’est un sujet pour l’avenir.
Il existe également de nombreux autres sujets futurs pour les communications interprocessus. Mais pour l’instant, essayez les tuyaux en utilisant popen
. Les sections critiques apparaissent également dans les scripts shell. Si vous préférez écrire vos scripts en C, c’est également possible.