En Engendrant des Taches de Steve Krueger Un programme peut lancer des nouveaux processus de manière synchrone ou asynchrone. Le plus souvent on emploie la création synchrone si un programme a besoin d'exécuter un second programme et attend que ce dernier se termine avant de reprendre. L'utilitaire 'make' habituel est un exemple de ce type de programme. Les processus synchrones ne tirent pas totalement profit des possibilités du multitâche de l'AmigaDOS. En beaucoup de situations, on pourrait souhaiter qu'un processus exécute une tâche à l'arrière-plan tandis qu'un autre processus réagirait aux entrées de l'utilisateur - en d'autres mots, des tâches qui s'exécutent de manière asynchrone. Comme les processus synchrones sont les plus simples à créer, nous les analyserons en premiers. L'appel système Execute() est la façon la plus courante d'implémenter un nouveau processus synchrone. Execute() prend trois paramètres - une chaîne de caractères de commande, le gestionnaire du fichier d'entrée et celui du fichier de sortie - et crée un nouveau CLI qui exécute les commandes indiquées comme si celles-ci avaient été tapées lors du signal d'invite du SHELL. Une fois les commandes exécutées, le nouveau CLI prend fin et le programme qui a appelé Execute() continue à tourner. Par exemple, pour se servir de cet appel, afin d'afficher un répertoire, nous devrions faire Execute("dir", NULL, Output()); Ceci exécute la commande DIR et affiche sa sortie dans le gestionnaire du fichier de sortie, Output(). L'appel ne marchera pas si le programme est lancé à partir du Workbench car Output() n'existe pas. Si le gestionnaire du fichier de sortie est égal à NULL, la sortie se dirige vers la fenêtre courante. Une fois encore, il n'y a pas de fenêtre courante sous Workbench. Pour ouvrir une nouvelle fenêtre et lui envoyer la sortie, servez-vous du fragment de programme suivant : fh = Open("CON:",MODE_OLDFILE); Execute("dir", NULL, fh); Si le second paramètre (le gestionnaire du fichier d'entrée) n'est pas égal à NULL, alors le CLI lit à partir du gestionnaire du fichier d'entrée après exécution de la commande chaîne de caractères. Cela continue tant qu'on ne rencontre pas la fin du fichier. On peut se servir de cette particularité pour exécuter un fichier 'script' à partir de l'intérieur d'un programme. Si la commande chaîne de caractères est égale à NULL, l'entrée est lue immédiatement à partir du gestionnaire du fichier d'entrée. La routine Execute() a deux limitations. Quand un processus lancé à partir du Workbench appelle Execute(), la routine n'a pas de gestionnaire pour le fichier d'entrée, et n'hérite pas d'un chemin de recherche. On doit donc fournir un gestionnaire au fichier d'entrée lorsqu'on lance un programme à partir du Workbench. On peut réaliser cela en appelant Open() pour NIL:. Le chemin de recherche est optionnel. Pour fournir un gestionnaire, on doit créer un environnement CLI factice et copier le chemin de recherche à partir du processus du Workbench. Le deuxième inconvénient d'Execute() est que celle-ci ne renvoie pas de code de retour pour la commande qu'elle exécute. Pour rémédier à cela, Commodore a ajouté une nouvelle routine, System(), dans la version 2.0 du SE. La routine System() est similaire à Execute(), mais présente quelques différences significatives. Elle peut exécuter un programme de manière synchrone ou asynchrone, mais elle ne lira pas à partir du gestionnaire du fichier d'entrée. Le code de retour de System() est -1 si la commande n'a pu être exécutée, autrement c'est le code de retour du programme lui-même. System() accepte deux paramètres : une chaîne de caractères pour la commande et un pointeur sur la liste des tags. (Pour une introductions aux tags et aux listes tags, voyez "Digging Deep in the OS", "Amiga World Tech Journal", n.o 5, p. 8). Dans la liste des tags, on peut indiquer le gestionnaire du fichier d'entrée, celui du fichier de sortie, une exécution synchrone ou asynchrone et le type de SHELL qu'on doit utiliser. Pour plus de détails, voyez dos/dostags.h. Souvenez-vous que System() n'est disponible que dans la version 2.0 du système d'exploitation. Aussi bien System() qu'Execute() cherchent la commande indiquée dans la liste résidente. Il n'y a aucune autre manière documentée d'exécuter des commandes à partir de cette liste. PROCESSUS ASYNCHRONES. L'installation exigée pour créer un processus asynchrone nouveau est plus compliquée que la création d'un processus synchrone. Le tableau ci-dessous résume les différentes étapes des processus père (original) et fils (généré) pour des périodes de temps spécifiques. Père Fils Mise en place de la liste de segments *** Appel à CreateProc() *** Mise en place du message de démarrage Attend le message de démarrage Envoi du message de démarrage Réunit l'info nécessaire à partir du démarrage Continuation de l'exécution Exécute la fonction de l'utilisateur Attente pour répondre au démarrage Répondre au message de démarrage et terminer Libération des ressources du fils *** *** Indique que le processus fils n'existe pas à ce moment. La routine système qui fait le plus de travail est CreateProc(). Cette routine prend quatre paramètres - le nom du processus, la liste de segments, la priorité et la taille dela pile. Le nom du processus est une chaîne de caractères qui se termine par NULL et qui n'a pas besoin d'être unique. La priorité est une valeur entière indiquant la priorité à laquelle devra tourner le processus. La taille de la pile du nouveau processus s'indique en LONGWORD. La liste de segments est une valeur BPTR qui pointe sur le premier 'hunk' du code qu'on devra exécuter. On peut obtenir la liste de segments en appelant LoadSeg() ou en créant une liste factice. Servez-vous de LoadSeg(), si le nouveau processus doit exécuter un nouveau programme. Si le nouveau processus est à lancer dans le même code en tant que programme courant, on doit créer une liste de segments factice. Essayons la dernière méthode. SUIVEZ TOUS LES POINTS POUR CREER FACILEMENT DES PROCESSUS SYNCHRONES ET ASYNCHRONES. Pour créer une liste factice, avec CreateProc(), les "AutoDocs" de Commodore suggèrent le code suivant : ds.l 0 ; Alignement sur un mot long pair DC.L 16 ; "Longueur" de segments (factice) DC.L 0 ; Pointeur sur le segment suivant ...début du code... L'exemple que nous donnons ci-dessous, et qui se trouve sur la disquette (dans le tiroir Krueger), fait la même chose que le fragment en assembleur ci-dessus, mais est écrit en C. La définition de structure pour la liste de segments factice est la suivante : struct FAKE_SegList { long space; long length; BPTR nextseg; short jmp; void(*func)(); }; Les trois premiers champs correspondent aux trois champs indiqués dans les "AutoDocs". Le champ "jmp" est initialisé avec la valeur hexadécimale de l'instruction JMP. Le pointeur de la fonction, "func", est chargé avec l'adresse de la fonction "process_starter". Ces deux champs constituent le premier 'hunk' du programme du nouveau processus qui sera créé par CreateProc(). En employant cette méthode, vous vous éviterez l'utilisation de routines 'stub' en langage assembleur. Après avoir créé la liste de segments factice, on la passe à CreateProc() en même temps que le nom du processus, sa priorité et la taille de la pile. Souvenez-vous que le pointeur sur la liste de segments doit être transformé en pointeur BPTR. CreateProc() créera un nouveau processus qui commencera l'exécution à partir de l'instruction JMP de la liste de segments factice et qui sautera ensuite à "process_starter". Cette fonction attend le message de démarrage, s'occupe de l'initialisation nécessaire, appelle la fonction désignée par l'utilisateur et répond ensuite au message de démarrage en lui notifiant que le processus fils a pris fin. Le processus fils attend le message de démarrage en appelant WaitPort() en utilisant le port message de sa structure processus. Lorsque WaitPort() retourne une valeur, on est prêt à prélever le message de démarrage. On prélève ce message avec GetMsg(). Le message lui-même peut avoir une taille quelconque contenant toutes les informations nécessaires à lancer le nouveau processus. La seule restriction est que le premier élément du message doit être une structure Message. Cet exemple emploie le message de démarrage pour faire passer deux parties supplémentaires d'information au processus fils - le pointeur de données communes et un pointeur sur la fonction que l'utilisateur souhaite exécuter. La structure employée pour contenir cette information est la structure ProcMsg, telle qu'elle est définie dans process.h. On crée le message de démarrage, en réservant de la mémoire et en remplissant les champs voulus. On passe ce message au processus fils par l'intermédiaire de PutMsg(). Dès que le processus père a envoyé son message de démarrage et que le processus fils l'a reçu, les deux processus peuvent tourner simultanément. Eventuellement le processus père doit attendre que le processus fils se termine. Dans l'exemple, "wait_process" fait cela en attendant sur le port de réponse du message de démarrage. C'est ainsi que le processus fils signale qu'il a terminé. Lorsqu'il reçoit la réponse, le père sait que le fils a terminé et il est hors de danger pour libérer le port de réponse, ainsi que la mémoire associée au message de démarrage. Notez que le programme en retour est passé des processus fils aux processus père dans le champ "return_code" du message de démarrage. On extrait cette valeur avant de libérer le message de démarrage. CONSULTEZ CECI. Dans le tiroir Krueger, l'exemple est composé de trois fichiers - main.c, process.c et process.h. Le premier, main.c, n'est qu'un programme de pilotage qui appelle les routines de process.c. Il contient trois fonctions - main(), process1() et process2(). En utilisant les routines de process.c, la fonction main() crée deux processus fils qui sont lancés simultanément et attend ensuite qu'ils se terminent. Les fonctions process1() et process2() contiennent le code qui sera exécuté par les processus fils. Notez que process1() et process2() appellent Execute(), en démontrant par là qu'il est possible de combiner la création de processus synchrones et asynchrones. Le fichier process.c contient les routines nécessaires pour créer des nouveaux processus et pour en attendre la fin. On peut les employer "telles quelles" ou les modifier à son goût. Le fichier process.h contient les prototypes nécessaires, ainsi que les définition de structures, pour process.h. Pour utiliser ces routines, incluez le fichier en-tête process.h et appelez les fonctions comme indiqué ci-dessous : msg = start_process(int(*func), long priority, long stacksize); ret = wait_process(struct ProcMsg *msg); où msg est une structure ProcMsg * définie dans process.h. func est la fonction qui sera le point d'entrée du nouveau processus. priority est la priorité du nouveau processus. stacksize est la taille de la pile du nouveau processus. ret est la valeur de retour en provenance du nouveau processus. Souvenez-vous de faire bien attention aux fonctions employées pour créer des nouveaux processus. Toutes les fonctions doivent être "ré-entrantes" et ne doivent pas faire appel à exit(), étant donné qu'exit() libère toute la mémoire réservée par le programme principal, tandis que ce dernier continue à tourner. Il est possible de personnaliser les fichiers process.c et process.h. Des zones dignes d'intérêt pour des modifications sont indiquées dans les commentaires contenant la chaîne de caractères "user". Ces zones indiquent où il est possible d'ajouter du code pour passer des informations supplémentaires au processus fils. L'exemple ci-dessus utilise le code en provenance du programme courant en tant que code pour le nouveau processus. Si l'on souhaite créer un nouveau processus qui exécute du code en provenance d'un programme séparé, il faut alors se servir de LoadSeg() pour avoir une liste de segments. LoadSeg() prend un paramètre, le nom du programme à charger, et retourne la liste de segments. S'il est dans l'impossibilité de charger le programme, LoadSeg() retourne NULL. On peut ensuite se servir de la liste de segments. LoadSeg() pourvoit à appeler CreateProc(). Après cet appel, tout se passe comme dans l'exemple ci-dessus, à une exception près : lorsque le processus fils se termine, il faudra appeler UnloadSeg() pour libérer la mémoire utilisée par le processus fils. C'est la technique qu'emploient les routines forkl() et forkv() du C SAS pour créer des processus fils. Les routines fork() appellent LoadSeg() et CreateProc(), tandis que wait() s'occupe d'attendre que le processus fils se termine et d'appeler UnloadSeg(). Souvenez-vous que LoadSeg() ne charge pas une commande à partir de la liste résidente et, par conséquent, qu'il est interdit de se servir des routines fork() pour exécuter une commande résidente. COMMUNICATION ENTRE PROCESSUS. L'exemple décrit ci-dessus emploie le message de démarrage pour envoyer toutes les informations au processus fils. Cependant, on peut envoyer des messages à n'importe quel moment et à n'importe quel processus. Tout ce dont on a besoin est un port de message, qui soit connu par les deux processus. Dans l'exemple ci-dessus, où le processus père crée un processus fils, le processus père pourrait créer un autre port de message et le passer au fils par l'intermédiaire du message de démarrage. Dans le cas de deux processus indépendants, la communication est établie par un port message commun. La communication avec le processus ARREX est un exemple de cette manière de passer un message. On peut aussi employer les signaux pour communiquer entre processus. En général, on emploie cette méthode pour signaler la fin d'un processus. Par exemple, si nous présumons qu'on a mis en place un processus fils pour gérer le signal CTRL-C, le père - pour signaler l'arrêt au fils - pourrait exécuter l'instruction suivante : SetSignal(child_process, SIGBREAKF_CTRL_C); L'exemple ci-dessus n'est pas prévu pour recevoir des signaux. Si un processus fils, créé par le code de l'exemple, recevait un tel signal pendant qu'il est en train d'exécuter des E/S de niveau 2, le processus fils tenterait d'appeler la routine qui gère l'arrêt. Par défaut, le gestionnaire d'arrêt appelera exit(), qui libérera toute la mémoire réservée (y compris la mémoire du processus père) avant de terminer. Pour éviter de telles situations, le gestionnaire d'arrêt par défaut est mis à NULL avant de créer des processus fils. Dans le C SAS, cela se fait avec la ligne suivante : _ONBREAK = NULL; PARTAGE DE LA MEMOIRE. Comme l'Amiga n'a qu'un seul espace mémoire, tous les processus peuvent avoir accès à toute la mémoire. Ceci rend très facile le partage de la mémoire entre les différents processus, mais aussi très dangereux. Par exemple, imaginez le cas où un processus traverserait une liste regroupée, tandis qu'un autre serait en train de la modifier. Le processus traversant la liste aurait le possibilité de commencer à accéder à la mémoire libérée. Pour éviter de telles situations, toutes les références à la mémoire qui pourraient être modifiées doivent être entourées d'un appel à Forbid() et à Permit(). Dans l'exemple ci-dessus, on a passé le registre A4 au processus fils pour permettre aux processus père et fils d'utiliser les mêmes données communes. Par conséquent, toutes les parties du code qui ont accès à des données communes -qui ne soient pas disponibles seulement en lecture- devront être entourées d'appels à Forbid() et à Permit(). Il est très important aussi d'être prudent avec les routines de bibliothèque qu'on utilise. Evitez toutes les routines de bibliothèque qui ne sont pas "ré-entrantes". Les fonctions de réservation de mémoire et celles d'E/S du C SAS sont deux exemples de routines de bibliothèque à éviter. Les processus fils devraient employer des appels système de l'AmigaDOS pour la réservation de mémoire et les E/S des fichiers. On peut se servir des routines de réservation de mémoire -si on est extrêmement vigilant-, en entourant leurs appels avec des Forbid() et des Permit(). On peut emplyer des 'stubs', comme celui qui suit, mais on doit s'en servir avec tous les processus qui partagent des données communes, "y compris" le processus père : void *my_malloc(length) int length; { void *ret; Forbid(); ret = malloc(length); Permit(); } Cette technique ne marchera pas avec les routines d'E/S de fichiers, étant donné que celles-ci appellent les routines d'E/S de l'AmigaDOS qui bloquent les appels à Forbid(). LE PROGRAMME D'EXEMPLE. Le programme d'exemple a été prévu dans l'environnement Workbench pour le compilateur C SAS 5.10. Vous aurez à faire des modifications mineures pour que le programme tourne sous DICE ou sous C MANX. Nous avons mis des notes en commentaire aux deux endroits où il est nécessaire d'apporter des modifications pour le C MANX. Ouvrez le tiroir qui contient l'exemple. Vous devriez voir plusieurs icônes. Double-cliquez sur l'icône BUILD pour compiler le programme et en faire l'édition de liens. Double-cliquez sur l'icône TEST pour lancer le programme. Trois fenêtres s'afficheront à l'écran. La première est la fenêtre habituelle qui s'ouvre pour la sortie standard des programmes en C SAS. Deux fenêtres supplémentaires sont ouvertes. La sortie qui va dans ces deux fenêtres est celle des deux processus fils qui tournent simultanément. Le premier processus appelle Execute() pour la commande DIR, tandis que le second appelle Execute() pour ECHO. Les deux processus appellent Delay(), ainsi les deux fenêtres ne se fermeront pas avant que vous n'ayiez pris connaissance de leur contenu. Les capacités multitâche de l'Amiga sont un atout considérable. Le code que nous fournissons ici rend aisée la création de processus fils qui ont la possibilité de gérer una grande variété de tâches. Etant donné que la plupart des programmes passent un grand pourcentage de leur temps à attendre une entrée utilisateur, pourquoi ne pas tirer profit de cette capacité multitâche et ne pas commencer à employer les cycles inoccupés de l'UC ?