Les membres ayant 30 points peuvent parler sur les canaux annonces, projets et hs du chat.

Forum Casio - Projets de programmation


Index du Forum » Projets de programmation » Ajout des threads pour Gint
Yatis Hors ligne Membre Points: 575 Défis: 0 Message

Ajout des threads pour Gint

Posté le 03/01/2021 21:14

Yo,

J'ai eu deux semaines durant ces vacances de Noël pour m'amuser un peu et j'ai voulu porter mon prototype de traceur sur la Graph90+E. Comme le temps commence sérieusement à me manquer ces derniers temps j'ai décidé d'utiliser Gint pour aller plus vite. Sauf....que c'est partie en cacahouète très vite et je me suis retrouvé à implémenter un système de "contexte chargeable à la volé" pour pouvoir tracer plusieurs choses en parallèle.

Outre le fait de faire un truc complètement overkill, je me suis dit que Gint aura besoin d'un système de threading un jour ou l'autre pour pouvoir faire des trucs incroyablement pétés si on le couple avec, je ne sais moi...le port 3-pins pour lire de la musique en même temps que dessiner des frames ou avec un driver USB/UBC pour envoyer des infos en temps réel sur nos laptop pour recevoir des logs de debug (qu'on pourra coupler avec GDB, Gidra ou tout autre joyeuseté), ...

J'ai donc décidé de mettre ça au propre.
À la base, je voulais en faire une librairie à part car déboguer un truc concurrentiel au bas-niveau...heuuu...chiant. Malheureusement, ça s'intègre trop profondément dans Gint pour pouvoir faire ça proprement. J'ai donc créer une branche "thread" sur le repo de Gint et j'ai commencé à pondre un truc.

J'aurais aimé pouvoir vous donnez quelque chose de finis, utilisable et documenté mais entre Noël et le nouvel an, j'arrive à la fin des "vacances" sans avoir finis mes carabistouilles. Donc j'ai poussé ce que j'ai sur un fork du dépôt de Gint (https://gitea.planet-casio.com/Yatis/gint-with-thread) et j'écrit ce topic pour laisser une trace de cette aventure ma foi fort sympatoche. Et aussi dans l'espoir de pouvoir reprendre ce truc a un moment.

'fin bref, qu'est-ce que c'est quoi les threads ? Et à quoi ça sert ?
Pour faire super simple, les threads sont des "mini-processus" qui exécute du code chacun de leur côté en se partageant le même adressage mémoire. En gros, dans note cas, ça donne un semblant de parallélisme (on a l'impression qu'on exécute du code en parallèle). Alors que non ! Que nenni ! C'est une grosse entourloupe, je donne juste un petit temps de CPU a chaque thread pour qu'il s'amuse avec après PAF je le retire et j'en mets un autre puis PAF je le retire et j'en mets un autre, .... (vous pouvez aller voir la page qui donne des explications le Round-robin, c'est ce que j'ai mis en place).

Qu'est-ce que tu as fait du coup ?
* j'ai mis en place un ordonnanceur pour permettre l'exécution de plusieurs thread.
* j'ai mis en place d'une interface grandement inspirée de celle d'UNIX pour les processus.
* j'ai mis en place d'une notion de signaux pour contrôler finement les threads.
* j'ai mis en place d'interface "utilitaire" pour, plus tard, pouvoir configurer le fonctionnement des threads plus finement.

Pour l'instant c'est...utilisable MAIS Gint n'est pas prêt pour assuré une stabilité avec le code qu'il propose car il n'a pas été pensé pour que plusieurs threads exécute le code, il faudra le sécuriser avant. Au-delà de ça, il me reste à corriger un crash qui se produit avec une feature de mon cru qui permet, quand un thread meurt, d'effectuer un "saut non-local" et de revenir au moment de sa création, ce qui est méga pratique pour revenir au niveau du "bootstrap" de Gint (la partie du code ou Gint appelle la fonction "main()") ça permet, en quelque sorte, de threader le kernel. (Je sais que vous vous demandez "mais...heuu...dans quel but ? Pourquoi tu fais ça ? "......bah c'est méga S-W-A-G tavu ! ; puis ça permettra de faire tourner des drivers en fond même lorsqu'on revient dans "le côté obscur" de Gint....ce qui est parfaitement inutile !

Et les pertes de performance dans tous ça ?
Bien sur, il y a des pertes de performance. Ceci-dit, je n'ai pas encore fait de tests pour avoir un comparatif concret sur les pertes de performance entre un addins avec le support des threads et un addin "classique". En théorie, l'ordonnanceur ne fonctionne pas continuellement ce qui devrai apporter des pertes négligeable pour la gestion des interruptions. La ou ça risque de ralentir, c'est lorsqu'on devra sécuriser la plupart du des API de Gint avec des mutex pour qu'elles deviennent stable avec plusieurs threads. En gros, ça rajoute encore quelque ligne de code à exécuter en plus et on risque d'être limité par la rapidité de l'ordonnanceur. Cependant, une interface est déjà en place pour "piloté" l'ordonnanceur donc on pourra "l'éteindre" lors de moment critique (genre le dessin).


Pour ce qui est des prévisions pour ceux qui aimerai participer au développement de ce "trucs":
* voir comment séparer la partie "thread" de Gint pour en faire une lib(?)
* revoir comment j'ai installé la partie "threads" (j'ai tous mis dans la ILRAM pour certaines raisons qui ne sont plus valable depuis le dernier commit)
* corrigé les problèmes actuels (bugs, crash, performance, ...)
* sécuriser 6000% du code de Gint pour le rendre thread-compatible.
* écrire une documentation complète des API mis en place (wiki)
* faire un MAXIMUM de tests et de retour pour fignoler les interfaces.
* support des SH3 (en théorie il manque juste à modifier le gestionnaire d'interruption des SH3 pour appeler l'ordonnanceur mais comme j'ai rien pour tester...complexe)


Conclusions de mes vacances:


Fichier joint


Django Hors ligne Membre Points: 36 Défis: 0 Message

Citer : Posté le 03/01/2021 21:52 | #


Ça c'est des vacances méga S-W-A-G
Cot Cot

Cliquer pour dérouler
ENCORE
Oui
Cliquer pour dérouler
ENCORE
Oui
Cliquer pour dérouler
ENCORE
Non
Cliquer pour dérouler
ENCORE
Oui
Cliquer pour dérouler
ENCORE
Non
Cliquer pour dérouler
ENCORE
Oui
Cliquer pour dérouler
ENCORE
Non
Cliquer pour dérouler
ENCORE
Encore vous ? Vous n'avez rien de mieux à faire ? Retournez coder sur calculatrice
Sinon j'ai un super site web https://hellomonsite.fr
Lephenixnoir En ligne Administrateur Points: 22869 Défis: 149 Message

Citer : Posté le 03/01/2021 22:47 | #


Alors là, tu me prends de court !

Je suis chaud pour essayer d'intégrer ça, notamment pour faire rentrer l'alternance gint/OS (qui a des effets de bord sur le matériel) dans ce paradigme. Il y a beaucoup de code dans ta PR donc ça va prendre un moment à disséquer, mais on doit pouvoir s'en sortir.

Je n'ai même pas encore tout lu, mais globalement la façon dont je vois les choses ce serait comme ça :

1. Tenter d'amincir le code en sortant ce qui est relativement indépendant (setjmp et compagnie)
2. Intégrer par morceaux les définitions de ton système de contextes, pour implémenter l'alternance gint/OS avec
3. Si possible, intégrer le scheduler en premier et ensuite l'API de threading

Idéalement il faudrait que tu puisses tester en même temps que moi au fur et à mesure, il y aura sans doute des ajustements à faire de temps en temps.

Quelque chose qui me gêne c'est que tu modifies le flot principal de toutes les interruptions pour lancer le scheduler. Pourquoi ? Les interruptions sont traitées par le noyau, ça ne pose normalement pas de problème de laisser le gestionnaire d'interruptions se lancer et appeler le scheduler seulement lorsque son timer expire.
Yatis Hors ligne Membre Points: 575 Défis: 0 Message

Citer : Posté le 04/01/2021 16:46 | #


Je suis chaud pour essayer d'intégrer ça, notamment pour faire rentrer l'alternance gint/OS (qui a des effets de bord sur le matériel) dans ce paradigme. Il y a beaucoup de code dans ta PR donc ça va prendre un moment à disséquer, mais on doit pouvoir s'en sortir.

Comment ça "faire rentrer l'alternance gint/OS" ? En plus d'avoir le contexte du CPU tu veux y ajouter tous les contextes hardwares ? Parce que si c'est ça, c'est exactement ce que je voulais faire au début.

Quelque chose qui me gêne c'est que tu modifies le flot principal de toutes les interruptions pour lancer le scheduler. Pourquoi ? Les interruptions sont traitées par le noyau, ça ne pose normalement pas de problème de laisser le gestionnaire d'interruptions se lancer et appeler le scheduler seulement lorsque son timer expire.

Je modifie le flot des interruptions parce qu'il ne faut absolument pas modifier le contexte utilisateur quand on switch de contexte sinon on risque d'avoir des problèmes (donc impossible d'utiliser les registres r8~r15 lorsque l'interruption de l'ordonnanceur a lieux). Je suis assez parano avec ce truc-là car il y a beaucoup de subtilité à prévoir et je préfère avoir la mainmise sur ce passage (c'est pour ça que, entre autres, j'ai bypass ton gestionnaire d'interruption et que je l'ai entièrement écrit en ASM. Mais je pense qu'il y a moyen de simplifier (voire de le faire en C, ce que faisait Kristaba avec FixOS il me semble)).

J'aurais pu passer par TRAPA pour basculer dans l'ordonnanceur mais l'avantage de bypass comme un sagouin ton gestionnaire d'interruption :
1) C'est plus simple à mettre en place.
2) On peut faire un ordonnanceur préemptif assez facilement (tout est là, il ne manque pas grand-chose pour qu'il soit préemptif).
3) C'est plus rapide et plus fiable parce que si on passe par le gestionnaire d'interruption de Gint: il faut que Gint traite cette interruption, qu'il appelle notre fonction attachée au timer qui, lui, va nous faire basculer dans l'ordonnanceur avec un vieux TRAPA. Et c'est seulement a ce moment là qu'on gèrera les changements de contexte. En plus de ça, tu autorises les interruptions pendant l'exécution des handlers ce qui risque de sacrement faussé la stabilité et la régularité de l'ordonnanceur.

Par contre, le gros problème de bypass ton gestionnaire, outre que ça détruit le flot des interruptions, c'est que ça demande de récupérer les infos sur le timer utilisé pour l'ordonnanceur, d'où ma fonction dégueu de "debugging" dans ton module de TMU où je viens dump toutes les infos du timer.

Je pense que le plus "propre" en termes d'architecture pour Gint, serait d'avoir une gestion correcte et indépendante de TRAPA, comme ça on ne touche pas au gestionnaire d'interruption et ça isole correctement l'ordonnanceur dans un gestionnaire d'exceptions qui lui pourra être modulaire. Par contre il faudra faire une croix sur la préemption, les changements de contexte risquent de prendre plus de temps et on perd grandement en régularité.


Idéalement il faudrait que tu puisses tester en même temps que moi au fur et à mesure, il y aura sans doute des ajustements à faire de temps en temps.

Bien sûr ! J'ai dev / testé cette PR sur une Graph35+, Graph35+E, Graph35+EII et Graph90+E donc seulement des SH4.
Lephenixnoir En ligne Administrateur Points: 22869 Défis: 149 Message

Citer : Posté le 04/01/2021 17:30 | #


Comment ça "faire rentrer l'alternance gint/OS" ? En plus d'avoir le contexte du CPU tu veux y ajouter tous les contextes hardwares ? Parce que si c'est ça, c'est exactement ce que je voulais faire au début.

Du coup j'y ai réfléchi. Le passage de gint à l'OS, dans "l'usage normal de gint", est juste un context switch sur les modules périphériques : en particulier l'exécution est continue (on saute nulle part) et il n'y a rien à ordonnancer (tout est dirigé par l'utilisateur). J'ai tenté de formuler tout ça dans un système de threads mais je suis arrivé à plusieurs problèmes, principalement dus au fait qu'on s'attend à pouvoir retourner dans l'OS peu importe dans quel thread de gint on est, ce qui viole l'idée que le thread OS a un PC unique (mon idée était de partager le contexte CPU du thread OS avec le thread gint principal, mais du coup ça empêche aux autres threads gint de revenir à l'OS).

Évidemment l'idée d'exécution parallèle avec deux contextes CPU différents a beaucoup plus de sens dans le cas de ton debugger. Il va donc falloir bien réfléchir. Je vois plusieurs choses à traiter attentivement :

• D'abord le fait que l'usage classique de gint_switch() ne contient pas d'idée d'exécution parallèle.
• Ensuite le fait que l'influence de gint est normalement inexistante quand on est dans l'OS (je suppose que tu as hacké pour garder le timer allumé).
• Et enfin le fait que passer automatiquement (par l'ordonnanceur) du thread CASIOWIN à un thread gint (ou l'inverse) est toujours ou presque toujours malvenu.

J'ai tenté de séparer comme ci-dessous, ce qui ne marche pas et que je cite juste pour noter ce que j'ai essayé :

Threads CASIOWIN
└─ Thread CASIOWIN unique
Threads gint
├─ Thread gint principal
└─ Autres threads gint...

Dans l'immédiat le plus économe conceptuellement me semble d'être de séparer le concept de contexte matériel (modules périphériques) utilisé pour l'alternance gint/OS et le concept de contexte et logiciel utilisé pour le threading, avec un tout petit extra dans le threading pour lier, si on le souhaite, un certain contexte matériel à un certain thread.

Dans ce modèle, l'usage normal de gint ressemble à ça :

[Thread 0] Le thread principal de gint (contexte matériel : variable)

Et ton usage pour le traceur ressemble à ça :

[Thread 0] Le thread principal de gint (contexte matériel : gint uniquement)
[Thread 1] Le thread à tracer (contexte matériel : celui de l'OS uniquement)

Ici l'élément important c'est que le thread OS a le contexte matériel de l'OS donc dès qu'il est sélectionné par l'ordonnanceur ou lancé par ton traceur on passe au contexte matériel de l'OS. (Tu pourrais laisser le contexte matériel variable sur le thread gint, mais comme il ne peut y avoir qu'un seul contexte matériel OS l'utiliser dans le thread principal influencerait le programme en cours de debug. Je crois que tu fais ça par endroits mais je suis plus sûr.)

De l'idée que j'en ai là tout de suite, le changement de contexte matériel et logiciel pourrait s'implémenter sans trop modifier le coeur de gint, et presque laisser la majorité de l'API de threading et l'ordonnanceur à une lib externe (modulo ce qui suit dans mon message). Je serais tenté de partir dans cette direction si c'est réalisable, parce que ça coûtera moins de travail pour l'intégrer, que ce sera plus facile de porter pour SH3, et parce que moins modifier le coeur de gint veut aussi dire moins de risque d'introduire des bugs architecturaux pétés. ^^"

Je modifie le flot des interruptions parce qu'il ne faut absolument pas modifier le contexte utilisateur quand on switch de contexte sinon on risque d'avoir des problèmes (donc impossible d'utiliser les registres r8~r15 lorsque l'interruption de l'ordonnanceur a lieux).

C'est le cas de toute interruption, de toute façon tout fonction qui utilise r8...r15 les restaure, et dans le gestionnaire d'interruptions je n'active les interruptions à aucun point où ils ne sont pas à leur valeur d'origine. En tous cas ce que tu dis là est vrai, mais ne nécessite pas de prendre toutes les interruptions en otage.

2) On peut faire un ordonnanceur préemptif assez facilement (tout est là, il ne manque pas grand-chose pour qu'il soit préemptif).

Sur SH4, tu peux interrompre un callback de timer avec l'interruption de l'ordonnanceur préemptif, il suffit que cette interruption soit d'un niveau plus haut. Si on la colle à 15 rien ne pourra lui échapper.

3) C'est plus rapide et plus fiable parce que si on passe par le gestionnaire d'interruption de Gint: il faut que Gint traite cette interruption, qu'il appelle notre fonction attachée au timer qui, lui, va nous faire basculer dans l'ordonnanceur avec un vieux TRAPA. Et c'est seulement a ce moment là qu'on gèrera les changements de contexte. En plus de ça, tu autorises les interruptions pendant l'exécution des handlers ce qui risque de sacrement faussé la stabilité et la régularité de l'ordonnanceur.

Je sais pas pourquoi tu parles de TRAPA ici (le gestionnaire d'exceptions à VBR+0x100 n'est pas du tout impliqué). Par contre tu as raison sur le fait que le gestionnaire du timer active les interruptions, ce qui est inacceptable pour l'ordonnanceur. C'est le seul problème que je vois actuellement. Ça ne me pose pas du tout de problème d'ajouter une extension à l'API timer pour créer un timer de priorité 15 dont le callback est appelé sans réactiver les interruptions ; en tous cas je préfère largement ça à interrompre le flot de toutes les interruptions. Est-ce que ça te paraît faisable ?

Je pense que le plus "propre" en termes d'architecture pour Gint, serait d'avoir une gestion correcte et indépendante de TRAPA, comme ça on ne touche pas au gestionnaire d'interruption et ça isole correctement l'ordonnanceur dans un gestionnaire d'exceptions qui lui pourra être modulaire. Par contre il faudra faire une croix sur la préemption, les changements de contexte risquent de prendre plus de temps et on perd grandement en régularité.

Ah d'accord parce que tu voyais le changement de contexte comme un appel système, c'est bien ça ? D'où le TRAPA. Il n'y a pas du tout de syscalls dans gint pour l'instant, faudrait que je réfléchisse pour voir si ça vaut la peine d'en ajouter. À vue de nez ce n'est pas nécessaire, puisque tout tourne déjà en ring 0 il suffit que l'ordonnanceur masque les interruptions si on l'appelle manuellement et ça fait le même travail.

L'autre truc qui me paraît louche et qui rend compliqué le fait de "simplement" implémenter le changement de contexte matériel/logiciel dans gint pour garder l'API de threading [et peut-être l'ordonnanceur] dans une lib c'est cette fonctionnalité de remonter à la création du thread quand tu le détruis. J'ai lu une partie des commentaires mais je n'en comprends pas l'usage. Tu peux m'expliquer ça ?
Yatis Hors ligne Membre Points: 575 Défis: 0 Message

Citer : Posté le 05/01/2021 13:55 | #


l'autre truc qui me paraît louche et qui rend compliqué le fait de "simplement" implémenter le changement de contexte matériel/logiciel dans gint pour garder l'API de threading [et peut-être l'ordonnanceur] dans une lib c'est cette fonctionnalité de remonter à la création du thread quand tu le détruis. J'ai lu une partie des commentaires mais je n'en comprends pas l'usage. Tu peux m'expliquer ça ?

J'avais implémenté ça pour pallier le problème suivant: "quand le thread main() meure, comment on revient au niveau du boostrap là où on appelle la fonction main() ?". En plus, de ce que j'ai lu, on peut configurer Gint pour qu'il rappelle le main() si celui-ci a fini. Donc je devais trouver un moyen de:
1) revenir au niveau du bootstrap sans trop de problème.
2) laisser la possibilité de faire tourner des threads mêmes quand le main() a fini.
3) mettre en place une sorte de hiérarchie dans le style d'UNIX pour les processus pour tuer les threads créé a la mort de main().

Je voulais mettre en place une sorte de hiérarchie entre les threads parce que comme on n'a pas de notion de "processus", personne ne va détruire les threads créés. Or, si le main() est finis et que tous les threads qu'il a invoqué n'ont pas été suicidés ça risque de poser de gros problèmes si on retourne dans le main(). Dans ma vision de ce que j'ai fait, je considère donc les threads comme des processus. C'est pour ça qu'a la mort d'un thread, je tue tous les threads enfants. Et je joue sur la notion de thread détachable / joignable pour les linker (joignable) ou non (détachable) au thread parent. Tous les threads "fils" sont détruits a la mort du thread "parent". Et tous les threads "détaché" sont détruits uniquement quand Gint appelle le destructeur de l'ordonnanceur. (ce n'est pas ce que j'ai implémenté dans la PR, c'était juste l'idée de base).

Pour en revenir a cette histoire de longjmp(), je voulais que la partie "bootstrap" soit considéré comme un thread à part pour pallier a la potentiel mort du thread main() SAUF que je ne voulais pas créer un thread juste pour cette partie car elle se résume à attendre la mort de la fonction main(). J'ai donc mis en place un système pour que, au moment de la mort d'un thread, on puisse retourner a l'endroit de la création du thread via un longjmp() pour, non pas recréer le thread, mais retouner une valeur spécial. Dans le cas de la fonction main(), le longjmp() nous fait retourner dans le bootstrap MAIS cette fois-ci, on est toujours dans le thread créé pour la fonction main(). Le bootstrap devient donc (implicitement) un thread, ce qui permet à l'ordonnanceur de continuer de fonctionner même à la "sois-disante" mort de la fonction main(), et de revenir au bootstrap alors qu'on a juste effectué un saut non-local dans le thread.

Mais c'était plus un "plan flemme" qu'autre chose, je pense que ce qu'il faudra mettre en place pour pallier a ce problème et nous débarrasser de cette feature (qui est somme toutes trop complexe et spécifique pour être utile), serait de diviser le boostrap en deux et coller la seconde partie du bootstrap dans le registre PR du thread main(), comme ça, dès qu'il meure PAF on retombe sur la fin du bootstrap pour finir et/ou redémarer l'addin.

@note:
Je m'imaginais une hiérarchie dans ce style (tous les threads au premier niveau (0, 8, 1, 2) sont "détachés").

scheduler:
  |--- [thread 0] (KERNEL) le bootstrap (_init)
  |     `--- [thread 3] (KERNEL) le _main()
  |           |--- [thread 6] (USER) une routine pour calculer le déplacement des mobs
  |           |--- [thread 4] (USER) une routine pour le moteur de jeu
  |           |--- [thread 5] (USER) une routine pour la gestion du clavier (spécifique au jeu)
  |           `--- [thread 7] (USER) une routine pour lire du son / bruitages.
  |--- [thread 8] (USER) une routine garder en cache les assets externes.
  |--- [thread 1] (KERNEL) le driver clavier
  `--- [thread 2] (KERNEL) le driver USB

C'est pour ça que j'ai implémenté les signaux aussi, pour pouvoir gérer finement les threads ainsi que cette notion de "père <-> fils" (SIGCHLD).


Ensuite le fait que l'influence de gint est normalement inexistante quand on est dans l'OS (je suppose que tu as hacké pour garder le timer allumé)

Cette PR était la juste pour te dire que les threads sont possiblement implémentable et isolable dans une lib si on prévoit les choses correctement en amont. Pour ma part, je comptais implémenter un hyperviseur de type-2 dans Gint pour cloisonner l'OS de Casio, le threader et le tracer avec des contextes complets (je m'explique à la fin)


C'est le cas de toute interruption, de toute façon tout fonction qui utilise r8...r15 les restaure, et dans le gestionnaire d'interruptions je n'active les interruptions à aucun point où ils ne sont pas à leur valeur d'origine. En tous cas ce que tu dis là est vrai, mais ne nécessite pas de prendre toutes les interruptions en otage.
[...]
Ça ne me pose pas du tout de problème d'ajouter une extension à l'API timer pour créer un timer de priorité 15 dont le callback est appelé sans réactiver les interruptions ; en tous cas je préfère largement ça à interrompre le flot de toutes les interruptions. Est-ce que ça te paraît faisable ?

Il faut que je sauvegarde le contexte tel qu'il est quand on bascule dans tes handlers, c'est inévitable. Tu auras beau sauvegarder tous les registres que tu veux dans la stack si le contexte switch se fait à un autre niveau s'est mort, a moins d'aller chercher manuellement les trucs dans la stack ce qui pose problème car j'ai mis en place un système de stack kernel au niveau de l'ordonnanceur (je m'explique dans la partie "@note").

Donc si tu ne bypass pas les interruptions comme je l'ai fait, il faudra sauvegarder le contexte (du moins R8~r15) autre part que dans la stack à chaque interruption pour que, si l'ordonnaceur décide de charger un autre contexte, qu'il puisse copier cette sauvegarde dans le context du thread actuelle (en plus de sauvegarder R0_BANK~R7_BANK) avant de charger le prochain. C'est complément faisable tu me diras, mais le truc c'est que si un seul thread tourne, l'ordonnateur est éteins donc ça ne sert à rien de perdre du temps à sauvegarder des registres.

Ce que j'ai fait est surement de la paranoïa, et en écrivant ces lignes je me rends compte qu'il y a moyen de faire moins violent pour Gint. Si tu mets en place de quoi configurer les timers avec une priorité 15 et que tu appelles les handlers SANS réactiver les interruptions ni changer de banque de registre. Alors ouais c'est jouable mais il faudra sauvegarder les registres r8~r15 quelque part.

(note après relecture : Ce qui m'a poussé à prendre en otages les interruptions, c'est qu'il fallait que je modifie tes handlers ce qui est complexe car tu fonctionnes par bloc de 32 octets et que la taille de tes handlers "principaux" pour les interruptions / exceptions sont hardcodé un certain endroits (je crois). Je n'avais pas envie d'y toucher et j'ai préféré le maroufler)


@Note - Pourquoi j'ai mis une stack kernel:
Si un changement d'interruptions a lieu, on tombe, certes, dans le gestionnaire d'interruption MAIS on est toujours en train de squatter la stack du thread actuel techniquement parlant. Et comme l'ordonnanceur délivre les signaux (parce que oui, j'ai cracké psychologiquement et j'ai trouvé ça fort pratique d'avoir la gestion des signaux sur les threads) et que la distribution des signaux se fait au moment de charger un nouveau thread. Imagine qu'on délivre un SIGKILL sur un thread "détaché", il faut bien que je le retire de la queue. Mais imaginons que le thread que je détruis était celui sur lequel je me trouve, PAF ça crash parce que toutes les ressources ont été libéré dont la stack.

Donc pour pallier ça, j'ai mis en place une stack "kernel" qui est utilisée uniquement au moment du changement de contexte. J'aurais pu générer deux stacks par thread (kernel + user) mais c'était trop overkill juste pour ça (ça aurait eu un intérêt si on avait une notion d'userland / kernel mais comme ce n'est pas le cas...). Seulement, on ne peut pas non plus avoir de stack "kernel" partagée entre tous les threads, ça poserait trop de problèmes. Il faut donc s'assurer que personne ne revienne sur une zone où la stack kernel est utilisée sinon on risque de gros problèmes de corruption. C'est pour ça que je voulais absolument isoler la partie "ordonnanceur" des threads et être le plus indépendant de Gint possible pour avoir la mainmise du 600% de ce que je fais.


Ah d'accord parce que tu voyais le changement de contexte comme un appel système, c'est bien ça ?

Non, je parle du basculement thread -> ordonnanceur sans impacter le contexte actuel. Et le seul moyen de faire ça c'est via une interruption et/ou via une exception. Je te proposais de passer par TRAPA justement pour basculer dans l'ordonnanceur dans le but de faire les contextes switch directement depuis la racine de l'exception, comme ça on laisse ton gestionnaire d'interruption tranquille, on a juste à setup un timer et l'handler s'occupe de faire un #trapa. Mais ce n'est pas viable.



J'ai tenté de séparer comme ci-dessous, ce qui ne marche pas et que je cite juste pour noter ce que j'ai essayé:
Threads CASIOWIN
└─ Thread CASIOWIN unique
Threads gint
├─ Thread gint principal
└─ Autres threads gint...

Tu sais que c'est totalement faisable ? Assez facilement même, il suffit d'installer, en plus de Gint, un hyperviseur. Et en plus, on a le choix sur le type (calme-toi).

Hyperviseur de type-1 en se basant sur la VBR:
Si on veut faire ça simplement il nous faut un hyperviseur de type-1 qui s'occupe de faire les lourds chargements de contexte (CPU+tous les périphériques hardware, VMWare appels ça des "world switch") et de rediriger les exceptions/interruption aux OS en fonction de celui qui a la main actuellement. Et rien ne nous empêche de bypass une combinaison de touches (genre MENU + Flèche) pour switch les OS comme si ne rien n'etais (comme un alt+tab quoi). C'est facilement standalone pour en faire une lib indépendante et ça résout le problème avec le retour menu bloquant.

Hyperviseur de type-2 en se basant sur l'UBC (l'utilisation "traceur" de Gint):
On restaure absolument toute la machine et on utilise GetKey() pour effectuer nos "world switch" et basculer d'un OS à l'autre. C'est exactement ce que tu fais actuellement mais la différence réside dans le fait qu'on dissocie l'hyperviseur de Gint dans une lib à part.

Si on veut faire un truc plus sympathique mais qui est plus complexe à mettre en place et probablement plus couteux en matière de performance, c'est de restaurer absolument tous SAUF la VBR et l'UBC et on utilise la DBR pour rattraper l'OS de Casio à chaque instruction via un breakpoint sur un de ses channel. Ce qui nous permet de faire ce genre de chose:
1) (VBR) Comme les interruptions / exceptions, passent par l'hyperviseur, ça nous permettra d'avoir un historique des événements internes à l'OS de Casio et de Gint.
3) (VBR) On peut bypass une combinaison de touches pour switch entre les OS.
2) (DBR) On peut facilement analyser/tracer le fonctionnement de l'OS de Casio vu qu'on aura une "snapshot" de la machine à chaque instruction.
4) (DBR) On peut créer des threads où on pourra choisir l'environnement (Gint / Casio)

Le dernier point est extrêmement intéressant parce que l'UBC a deux channels. Le premier est utilisé pour rattraper l'OS de Casio par l'hyperviseur....mais le second pourrait nous servir à intercepter, à la volée, certaines instructions / certaines adresses / zones de mémoire, et d'ignorer et/ou simuler les parties code qui peuvent nous poser des problèmes. Tout ça nous permettraait de:
- cloisonner Casio/Gint en userland en remplaçant, à la volée, les instructions qui demande trop de privilege (mais on s'en tape parce que tout le monde est en ring0)
- autoriser ou non Casio/Gint à utiliser certain périphérique
- autoriser ou non Casio/Gint à utiliser certaine zone de mémoire
- forcer Casio/Gint à ne pas blocker les exceptions / interruptions pour pouvoir exécuter des "vrai" threads avec des environnement complets (genre Casio ou Gint) ce qui permettrait, par exemple, de lire un fichier via Bfile en asynchrone tout en continuant d'exécuter l'addins de Gint.

En vrai, à la toute base (quand j'ai écrit cette partie) c'était une blague cette histoire d'hyperviseur mais au final...c'est ce qu'il nous faut tu ne trouves pas ? Une lib "thread" pour du "contexte switch" + une lib "hyperviseur" pour la gestion des "world switch" (+ un hyperviseur complément overkill pour cloisonner l'OS et le threader tranquillement).

@note:
Je sais que ce ne sera pas vraiment des hyperviseurs parce qu'on est en ring0 et qu'ont trap que dalle au niveau des privilèges mais tu comprends l'idée, j'espère. En vrai, on peut toujours envisager de déplacer la partie "hyperviseur" dans une lib à part, comme ça on pourrait dev des alternatives et choisir celui qu'on veux au moment de la compilation d'un projet.


Je ne sais pas ce qui me fait le plus marrer, le fait que ce soit 100000x trop overkill pour une calculatrice ou le fait que ce sois faisable xD
Lephenixnoir En ligne Administrateur Points: 22869 Défis: 149 Message

Citer : Posté le 06/01/2021 09:28 | # | Fichier joint


Ce message qui a commencé comme une simple discussion de ce qui est intéressant ou pas dans les idées qu'on a évoquées jusqu'à présent a tourné en modèle complet. Comme je savais que le message allait être long j'ai fait plusieurs tours de toutes les idées et je pense que le modèle que je présente ci-dessous est déjà un peu mature. La lecture longue ne devrait donc, je l'espère, pas être gâchée.

En vrai, à la toute base (quand j'ai écrit cette partie) c'était une blague cette histoire d'hyperviseur mais au final...c'est ce qu'il nous faut tu ne trouves pas ? Une lib "thread" pour du "contexte switch" + une lib "hyperviseur" pour la gestion des "world switch" (+ un hyperviseur complément overkill pour cloisonner l'OS et le threader tranquillement).

Eh bien, hmm... ça se pourrait mais je voudrais prendre les choses dans l'ordre, et notamment séparer les trucs intéressants ou pas dans ce qu'on a dit jusqu'ici.

Pour situer tout ce qui suit dans ce message, mon but est d'équilibrer trois objectifs :
(F) les fonctionnalités et la flexibilité du système. Je veux implémenter ce dont tu as besoin pour ton traceur.
(P) la performance des applications et en particulier des add-ins « classiques ».
(S) la simplicité de fusionner ton travail dans gint, et de maintenir le résultat.

(Le but donc c'est de maximiser FPS, et je te promets que c'était pas fait exprès. )

Voilà quelques points qui me paraissent importants par rapport à ces trois choses :

Fonctionnalités
• Comme on n'a qu'un seul cœur, les seuls intérêt des threads pour les applications gint classiques sont de (I1) alterner deux flots d'exécution indépendants (c'est-à-dire écrire des coroutines), et (I2) effectuer des calculs sensiblement parallèles sans avoir à ordonnancer à la main. (I1) est intéressant en tant que coroutines, et encore plus intéressant si les deux flots sont l'OS et un debugger (ça donne ton traceur). (I2) c'est moyen, je vois pas vraiment de raison de préférer un ordonnanceur automatique type round robin quand planifier à la main ira toujours plus vite ; mais je veux bien coder ce qu'il faut pour le rendre possible.
• Je préfère ne pas modifier trop le noyau : implémenter seulement les context switch et les world switch et laisser la lib threads extérieure ; pas pour des raisons idéologiques, mais pour des raisons pragmatiques de réussir à faire la fusion sans y passer des mois.
• Je préfère éviter les tricks comme hijack les interruptions ou de sauter à la fin des threads.
• Je veux que ce soit compatible en API avec gint 2.1.

Performance
• Toujours parce qu'on n'a qu'un cœur, plein de choses qu'on pourrait naïvement faire avec des threads seront plus lentes que faites proprement à la main (en inlinant en quelque sorte l'ordonnanceur dans la logique du programme, ce qu'on fait déjà tout le temps). Je ne cherche donc pas à supporter les constructions multi-threadées qui sont plus lentes que leurs équivalents mono-threadés quand il y en a.
• J'aimerais minimiser l'impact sur les programmes qui ne sont pas multi-threadés (donc ralentir les interruptions, même si on peut le faire, c'est pas idéal).

Simplicité
Pas grand-chose à dire ici, l'idée générale me plaît mais avoir une implémentation stable, testée et qui répond à ton besoin me plaît davantage.

Ce qu'on veut faire avec

Donc voilà dans tes trois messages ce que tu as (pour l'instant) raconté qu'on pourrait faire avec, comme ça je passe ça au crible et j'en tire ma vision de ce qu'on devrait pouvoir supporter.

J'ai eu deux semaines durant ces vacances de Noël pour m'amuser un peu et j'ai voulu porter mon prototype de traceur sur la Graph90+E.

Le principe du traceur c'est en gros (I1) où l'on fait un context switch et un world switch en même temps sur la base d'une interruption UBC (on ne peut pas attraper les autres sinon soit on casse l'OS soit on fait une redirection à la Kristaba que tu appelles Hyperviseur type-1, ce qui m'intéresse pas immensément par rapport à l'autre version avec l'UBC).

Gint aura besoin d'un système de threading un jour ou l'autre pour pouvoir faire des trucs incroyablement pétés si on le couple avec, je ne sais moi...le port 3-pins pour lire de la musique en même temps que dessiner des frames ou avec un driver USB/UBC pour envoyer des infos en temps réel sur nos laptop pour recevoir des logs de debug (qu'on pourra coupler avec GDB, Gidra ou tout autre joyeuseté), ...

Toutes ces choses (envoi sur le port 3-pin, échanges par USB, UBC) sont contrôlées par des interruptions, et ne nécessitent pas d'avoir du threading plus élaboré que le context switch normal des interruptions (je les planifiais déjà avant que cette idée ne soit mise sur la table). En plus, la musique et les échanges par USB sont assez sensibles au timing donc il n'y a pas vraiment de raison de les ordonnancer à un autre moment que quand le matériel signale qu'une action est nécessaire.

2) On peut faire un ordonnanceur préemptif assez facilement (tout est là, il ne manque pas grand-chose pour qu'il soit préemptif).

Pour un unikernel la notion d'ordonnanceur préemptif est assez limitée, mais je trouve ça normal de pouvoir la supporter. Notons que tu peux pas préempter n'importe quoi non plus, et ce n'est pas un problème à mon goût (on n'est pas du tout en train de faire un RTOS).

Je voulais mettre en place une sorte de hiérarchie entre les threads parce que comme on n'a pas de notion de "processus", personne ne va détruire les threads créés.

Je profite de la mention du terme pour dire que gint ne peut pas implémenter de processus parce que la mémoire n'est pas abstraite (pas de MMU) et je n'ai pas l'intention de partir dans cette direction ; donc on reste bien sur des flots d'exécution qui partagent toutes les ressources, donc des threads.

Je m'imaginais une hiérarchie dans ce style (tous les threads au premier niveau (0, 8, 1, 2) sont "détachés").

scheduler:
  |--- [thread 0] (KERNEL) le bootstrap (_init)
  |     `--- [thread 3] (KERNEL) le _main()
  |           |--- [thread 6] (USER) une routine pour calculer le déplacement des mobs
  |           |--- [thread 4] (USER) une routine pour le moteur de jeu
  |           |--- [thread 5] (USER) une routine pour la gestion du clavier (spécifique au jeu)
  |           `--- [thread 7] (USER) une routine pour lire du son / bruitages.
  |--- [thread 8] (USER) une routine garder en cache les assets externes.
  |--- [thread 1] (KERNEL) le driver clavier
  `--- [thread 2] (KERNEL) le driver USB

C'est intéressant, mais je ne pense pas que ce soit utile. Ces procédures ne sont pas parallèles : les drivers clavier et USB sont sensibles au timing et s'exécutent toujours et uniquement lorsqu'une interruption le demande ; pareil pour le thread 7 ; les threads 4 à 6 et 8 sont invoqués à des instants différents du cycle de jeu et on n'a surtout pas envie de laisser un ordonnanceur préemptif les contrôler. (Tu imagines si tu fais 14 ticks de déplacement de mobs sans simuler la physique ?)

Imaginons que tu codes un jeu avec ce modèle-là. À mon sens, si tu passes d'un thread à l'autre manuellement tu vas juste reproduire la logique qu'on utilise actuellement dans un jeu avec plus de code et des changements de contexte, violant (P) ; et si tu utilises un ordonnanceur préemptif pour changer automatiquement de thread tu fragilises énormément le design, risquant des problèmes de timing sur les drivers et la génération du son, des problèmes de désynchronisation entre les différentes parties du jeu, tout en continuant de subir les pertes de performance liées au changement de contexte.

La plupart des applications seront comme ça je pense, et sauront comment ordonnancer leurs composants bien mieux qu'une méthode automatique. C'est ce qui me fait dire que l'intérêt de (I2) est vraiment limité (et dans mon plan l'ordonnanceur est externe, gint ne fournit que les context et world switch). Donc même si c'est stylé, je vois pas de cas d'usage raisonnable, donc ça me paraît pas intéressant à supporter. (Tu peux le faire dans une lib mais gint devrait pas hiérarchiser les threads par exemple.)

Hyperviseur de type-1 en se basant sur la VBR:
Si on veut faire ça simplement il nous faut un hyperviseur de type-1 qui s'occupe de faire les lourds chargements de contexte (CPU+tous les périphériques hardware, VMWare appels ça des "world switch") et de rediriger les exceptions/interruption aux OS en fonction de celui qui a la main actuellement.

Le notion d'hyperviseur nécessite d'avoir un niveau d'abstraction/privilège plus bas que les noyaux que tu lances. Sur la calculatrice où tout est en ring 0, c'est un peu difficile à définir exactement. Mais en gros ce que tu décris là reprend le designe du debugger de Kristaba mais interromp gint en plus de l'OS (bon courage pour coopérer avec la VBR de l'OS).

Si tu veux faire un vrai hyperviseur bien séparé de l'add-in, où l'hyperviseur charge gint dans la mémoire et fait les world switch, alors c'est indépendant de gint. Ce dont tu as besoin, ce n'est pas d'ajouter des threads à gint, mais de le modifier pour en sortir les world switch et ajouter un peu de flexibilité sur le démarrage (le rendre compatible avec ton hyperviseur en sommet). Je vois pas trop d'interêt, puisqu'on peut faire plus direct et élégant.

En réalité tu peux simplement faire charger ton hyperviseur en même temps que l'add-in et modifier le world switch de gint pour garder le contrôle de la VBR. Ce qui revient exactement au gint multi-threadé que je propose plus bas mais tu as un hook sur toutes les interruptions de gint et de l'OS.

Hyperviseur de type-2 en se basant sur l'UBC (l'utilisation "traceur" de Gint):
On restaure absolument toute la machine et on utilise GetKey() pour effectuer nos "world switch" et basculer d'un OS à l'autre. C'est exactement ce que tu fais actuellement mais la différence réside dans le fait qu'on dissocie l'hyperviseur de Gint dans une lib à part.

De ce que je comprends c'est pareil que l'autre mais l'hyperviseur est dans le handler DBR au lieu d'être dans le point d'entrée de la VBR. Et donc tu peux y entrer automatiquement dans un grand nombre de situations grâce à l'UBC. Ça tu peux vraiment facilement le faire avec le gint multi-threadé que je propose plus bas, conserver le contrôle de DBR est beaucoup plus facile que conserver le contrôler de la VBR. Comme tu l'as remarqué, c'est vraiment la même chose que gint tout seul avec un add-in bien écrit, on place juste les frontières entre les composants à des endroits différents.

Personnellement, je vois pas de raison de déployer des outils aussi compliqués qu'un hyperviseur sous gint et sous l'OS. Au contraire, je pense qu'analyser correctement la situation et mettre en place la solution correcte la plus efficace est une preuve de pertinence. L'étude préliminaire clarifie le design donc on est mieux guidés quand on code, l'architecture est plus simple donc ça va plus vite et avec moins de bugs, et à la fin le projet marche plus rapidement et donne le même résultat.

1) (VBR) Comme les interruptions / exceptions, passent par l'hyperviseur, ça nous permettra d'avoir un historique des événements internes à l'OS de Casio et de Gint.
3) (VBR) On peut bypass une combinaison de touches pour switch entre les OS.
2) (DBR) On peut facilement analyser/tracer le fonctionnement de l'OS de Casio vu qu'on aura une "snapshot" de la machine à chaque instruction.
4) (DBR) On peut créer des threads où on pourra choisir l'environnement (Gint / Casio)

Pour (1) et (3), l'hyperviseur VBR requiert de détourner les interruptions de l'OS à la façon de Kristaba. Si tu fais ça, tu peux simplement mettre ton handler dans gint et coder les statistiques dans gint. (2), tu peux déjà le faire avec ton traceur ? Quant à (4) on peut le faire dans mon modèle aussi, c'est bien obligé puisque je veux supporter ton traceur et c'en est le principe même.

Tout ça nous permettrait de:
1) cloisonner Casio/Gint en userland en remplaçant, à la voler les instructions qui demande trop de privilege (mais on s'en tape parce que tout le monde est en ring0)
2) autoriser ou non Casio/Gint à utiliser certain périphérique
3) autoriser ou non Casio/Gint à utiliser certaine zone de mémoire
4) forcer Casio/Gint à ne pas blocker les exceptions / interruptions pour pouvoir exécuter des "vrai" threads avec des contextes violant (genre Casio ou Gint) ce qui permettrait, par exemple, de lire un fichier via Bfile en asynchrone tout en continuant d'exécuter l'addins de Gint.

Là aussi je ne vois pas le besoin d'avoir un hyperviseur (quelque chose en-dessous du noyau), tu donnes simplement à l'UBC des privilèges symboliques plus élevés, ce que tu peux faire même si ton code est techniquement au-dessus de noyau (typiquement dans une bibliothèque). D'ailleurs gintctl fait déjà des fourberies de ce genre dans le navigateur de mémoire en sautant les instructions qui accèdent aux adresses provoquant des TLB error. (Dans ce cas l'accès est détectée par une exception à VBR+0x400 avec un TLB miss mais c'est exactement pareil si c'était DBR avec un user break.)

Tout ça reste très pété et très loin, donc si tu veux bien je te propose de discuter d'abord d'un modèle portant sur les applications simples et une fois qu'il sera clair de voir s'il marche pour ces applications pétées. Parce que je ne suis pas sûr qu'on le code un jour, et si on le code qu'on le code comme on l'imagine aujourd'hui, donc je préfère y aller doucement.

Pour résumer, voilà mon cahier des charges :
• Avoir une notion de thread OS qui provoque un world switch quand on y passe (pour le traceur).
• Permettre l'implémentation d'un ordonnanceur préemptif (de préférence en externe pour customiser la politique temporelle).
• Éventuellement supporter un world switch qui préserve soit la VBR soit l'UBC pour implémenter une de tes idées d'hyperviseur (de préférence l'UBC).

Voilà ce qui ne me semble pas être des objectifs intéressant :
• Mettre dans des threads du code qui est déjà très bien contrôlé par des interruptions.
• Traiter les threads comme des processus (en particulier pas besoin de hiérarchie).

Séparation kernel/add-in et « threader le kernel » ; vers un kernel atomique par construction

Prenons un peu de recul, parce que cette partie est très importante. gint est un unikernel, donc le noyau se mêle forcément au code de l'utilisateur, en particulier en exposant ses fonctionnalités dans une bibliothèque. Contrairement à Linux où tout passage dans le noyau est contrôlé par un syscall (qui induit un changement de contexte), ici les entrées et sorties de gint sont floues et non contrôlées. C'est fait exprès et ça doit rester comme ça.

Dans un unikernel, la limite entre le noyau et l'application est floue et c'est fait exprès.

La vérité c'est que la limite est partiellement artificielle. Sous Linux, on met dans le noyau les fonctions qui ne doivent pas être accessibles aux applications pour des raisons de sécurité. Mais en fait des choses comme lire l'heure peuvent parfaitement être faites par l'application, et c'est un tel avantage de performance que le vdso(7) a été inventé pour mettre des fonctionnalités originellement noyau dans le userspace.

La situation avec gint c'est une version extrême de ça couplée au fait qu'on se moque de la sécurité puisque la relation entre l'add-in et le noyau est coopérative (et de toute façon on ne peut rien sécuriser puisque l'add-in fait ce qu'il veut, il tourne en ring 0). rtc_get_time() est une fonction qui s'exécute sans sauvegarder de contexte et a toutes les raisons de le rester. Et ce serait paradoxal de payer un coût en performance pour faire un context switch si deux threads veulent lire l'heure en même temps, alors que ça marche déjà tout seul (y'en a juste un qui va s'apercevoir après s'être fait interrompre pendant 100 ms que le CF est passé à 1 et va recommencer sa lecture).

Dans un gint multi-threadé, tous les threads auront fondamentalement accès à gint en parallèle puisque le noyau est couplé avec le code.

Reste le fait qu'il y a bel et bien des fonctions qu'on ne peut pas interrompre — principalement certains drivers, le gestionnaire de threads et les gestionnaires d'interruptions. Et c'est là que je sors la carte piège : puisque la notion de noyau vs userland est volontairement floue dans ce design d'unikernel, on peut définir le noyau de gint comme l'ensemble de ces fonctions dont on doit contrôler l'accès.

À première vue ça ne fait que jouer sur les mots, mais en réalité c'est très important : comme ces parties sont ininterruptibles, on ne peut pas changer de thread pendant qu'elles s'exécutent. Formulé autrement avec la nouvelle définition, tous les opérations noyau sont atomatiques. Ça veut dire que si un thread rentre dans le noyau, on continue de l'exécuter jusqu'à ce qu'il en sorte. Et donc à tout instant l'exécution est dans une de deux situations :

• Soit tous les threads sont hors du noyau, et dans ce cas-là le thread courant peut tout à fait entrer dedans.
• Soit un thread est dedans et dans ce cas c'est le thread courant puisqu'une fois entré dans le noyau il ne peut plus être interrompu, et tous les autres threads sont dehors.

Définir le noyau comme l'ensemble des fonctions ininterruptibles révèle que même si on le partage dans tous les threads son exécution n'est jamais parallèle et toujours bien définie.

Et donc en particulier...

Seulement, on ne peut pas non plus avoir de stack "kernel" partagée entre tous les threads, ça poserait trop de problèmes.

Non seulement on peut, mais en plus on n'en a pas besoin. En effet, toute opération noyau qui est démarrée continue sans interruption jusqu'à ce qu'elle se termine. Même si le noyau utilise la pile du thread qui l'a appelé, il n'y a de risque que des données se perdent. Une seule question se pose, c'est si tu changes de thread pendant que tu es dans le noyau. Mais je vois plein de solutions ici (je peux détailler plus tard, c'est du détail d'implémentation) donc pour moi il n'y a pas besoin de pile noyau. L'intérêt ? Tu peux appeler pthread_exit() sans avoir à changer de pile (donc pas de trapa, tout est simple et performant et écrit en C).

Tout cela devrait expliquer le titre de cette partie. Non seulement le kernel n'a pas à être un thread (ce serait contraire aux principes de l'unikernel) mais en plus il n'a pas besoin de l'être, parce que si on regarde les choses sous le bon angle on réalise qu'on peut le partager entre tous les threads sans que ça crée de conflit. Pour cette exacte même raison, il n'y a pas besoin de trapa pour passer dans le kernel, il suffit d'appeler les fonctions directement.

Bien sûr, il faut quand même bien identifier les fonctions ininterruptibles dont je parle, ce qui concerne je pense :
• Certaines parties des drivers très sensibles au timing : port série, USB. Ceux-là pourront appeler un atomic_begin() et atomic_end() quand ils en ont besoin.
• Le gestionnaire de threads ; ce serait stupide qu'il se fasse interrompre.
• Les gestionnaires d'interruptions, qui sont déjà ininterruptibles parce qu'ils s'exécutent avec SR.BL=1.

Je ferais quand même la distinction entre les drivers ininterruptibles et ceux qui sont simplement MT-Unsafe. Si un driver ne peut pas être interrompu sans casser son interaction avec le matériel (genre USB), alors il est ininterruptible et doit être protégé. Si on peut l'interrompre mais qu'il faut juste ne pas y toucher jusqu'à ce qu'il reprenne, il est MT-Unsafe. Par exemple tu peux interrompre le driver TMU pendant qu'il initialise un timer et reprendre l'initialisation plus tard. Les drivers qui ne sont que MT-Unsafe n'ont pas à être protégés par le noyau, c'est à l'utilisateur de se démerder pour ne pas les appeler dans deux threads en même temps (car protéger automatiquement violerait (P) en induisant un surcoût pour les applications mono-threadées et les applications qui savent ce qu'elles font).

Enfin, pour finir cette partie note qu'actuellement tu peux interrompre n'importe quel driver de gint avec une interruption, et à bien y réfléchir je pense que ça peut créer des bugs. Je n'avais pas trop pensé à ça jusqu'à ce que tu mettes le sujet sur la table, donc merci. xD

Le modèle que je propose

Voilà donc le modèle que je propose de mettre en place, et qui répond d'après mon analyse aux besoins cités dans le cahier des charges (et qui permet même les fonctionnalités pétées type hyperviseur, mais on en reparlera plus tard).

Le noyau est structurellement atomique et partagé entre tous les threads. Si on reprend la définition que j'ai donnée tout à l'heure du noyau comme l'ensemble des fonctions atomiques, alors il est clair qu'on peut partager le noyau avec tous les threads. Ça évite d'avoir un « thread noyau », ce qui n'est pas dans la philosophie de l'unikernel. Ainsi, chaque thread dont le world est gint a accès à gint, et gint utilise soit des atomic_begin() et des atomic_end() soit SR.BL=1 pour s'assurer de son propre bon fonctionnement.

Context switch et world switch comme des fonctions de l'API. Les deux sont ininterruptibles, et programmés en assembleur (en tout cas le début et la fin sont programmés en assembleur ; le context switch est obligé parce qu'il modifie plein de registres, le world switch est initié par cpu_setvbr() qui est aussi en assembleur).

Threads OS avec un world switch. On ajoute une notion de thread OS qui fait un world switch en même temps que le context switch.

World switch customisé. Si tu veux conserver le contrôle de la VBR durant un world switch (pour l'hyperviseur type-1), tu peux appeler ou faire appeler gint_setvbr() avec ton adresse de VBR. Si tu veux conserver le contrôle de DBR (pour un hyperviseur type-2), tu peux désactiver le changement de contexte dans le driver UBC.

Notons que l'alternance gint/OS classique n'est du coup pas vraiment un changement de thread, ça consiste plus à déplacer le thread d'un world à l'autre tout en continuant de l'exécuter (ça me satisfait en termes de « faire rentrer dans le paradigme »).

Voilà le diagramme.


Tricks a priori pas nécessaires

Je modifie le flot des interruptions parce qu'il ne faut absolument pas modifier le contexte utilisateur quand on switch de contexte sinon on risque d'avoir des problèmes (donc impossible d'utiliser les registres r8~r15 lorsque l'interruption de l'ordonnanceur a lieux). Je suis assez parano avec ce truc-là car il y a beaucoup de subtilité à prévoir et je préfère avoir la mainmise sur ce passage (c'est pour ça que, entre autres, j'ai bypass ton gestionnaire d'interruption et que je l'ai entièrement écrit en ASM. Mais je pense qu'il y a moyen de simplifier (voire de le faire en C, ce que faisait Kristaba avec FixOS il me semble)).

Quand on y réfléchit, la difficulté là-dedans c'est que l'état que tu veux sauvegarder avant de changer de thread n'est pas l'état du processeur au moment où l'ordonnanceur est appelé (... par l'interruption), mais l'état du processeur où moment où le code utilisateur se fait interrompre. Un état qui existe plus tôt dans le temps, et ne peut être lu par l'ordonnanceur que si le contexte utilisateur ne change pas du tout entre le début de l'interruption et l'appel de l'ordonnanceur. Or, si l'ordonnanceur est appelé par un timer il y a toujours des trucs qui changent (comme pr) et le design d'un unikernel fait qu'on ne veut pas sauvegarder plein de registres inutilement pour les autres interruptions.

À mon sens, la solution idéale à ce problème est de sauvegarder le contexte au moment du callback et pas celui avant l'interruption. Dans gint, le callback d'une interruption est supposé être userland standard (contrairement à un signal Linux par exemple) donc si c'est possible je veux pouvoir interrompre un thread durant le callback d'une interruption. Dans cette idée, la prise de contrôle de l'ordonnanceur préemptif se passe comme ça : le flot normal du programme est interrompu pour entrer dans le noyau quand le timer underflow, le kernel rend la main où callback, et ensuite le callback re-rentre dans le noyau (puisqu'il utilisera atomic_begin()) pour sauvegarder le contexte et changer de thread. Ça ne permet pas pour autant à l'ordonnanceur préemptif de se faire interrompre si ton interruption a un niveau assez élevé (genre 15).

Et du coup...

Donc si tu ne bypass pas les interruptions comme je l'ai fait, il faudra sauvegarder le contexte (du moins R8~r15) autre part que dans la stack à chaque interruption pour que, si l'ordonnaceur décide de charger un autre contexte, qu'il puisse copier cette sauvegarde dans le context du thread actuelle (en plus de sauvegarder R0_BANK~R7_BANK) avant de charger le prochain.

Dans ce cas-là non, en sauvegardant au moment opportun (durant le callback) je n'ai pas besoin de tout sauvegarder durant l'interruption.

Je voulais mettre en place une sorte de hiérarchie entre les threads parce que comme on n'a pas de notion de "processus", personne ne va détruire les threads créés. Or, si le main() est finis et que tous les threads qu'il a invoqué n'ont pas été suicidés ça risque de poser de gros problèmes si on retourne dans le main().

Le code de start.c qui appelle main() peut juste faire un gros join sur tous les threads entre la fin de main() et l'appel des destructeurs ?

Pour en revenir a cette histoire de longjmp(), je voulais que la partie "bootstrap" soit considéré comme un thread à part pour pallier a la potentiel mort du thread main() SAUF que je ne voulais pas créer un thread juste pour cette partie car elle se résume à attendre la mort de la fonction main(). (...) Le bootstrap devient donc (implicitement) un thread, ce qui permet à l'ordonnanceur de continuer de fonctionner même à la "sois-disante" mort de la fonction main(), et de revenir au bootstrap alors qu'on a juste effectué un saut non-local dans le thread.

Quand main() se termine on retourne déjà automatiquement dans l'initialisation (le bootstrap) puisque c'est là que main() est appelée. Le code de construction/destruction et la fonction main() ne s'exécutant jamais en parallèle, pourquoi les séparer en plusieurs threads ?

Lephenixnoir a écrit :
J'ai tenté de formuler tout ça dans un système de threads mais je suis arrivé à plusieurs problèmes, principalement dus au fait qu'on s'attend à pouvoir retourner dans l'OS peu importe dans quel thread de gint on est (...)

Ce problème est résolu en considérant gint_switch() non comme un changement de thread mais comme un changement de world pour le thread courant. Je suppose qu'on peut avoir plusieurs threads dans l'OS d'ailleurs, c'est juste qu'absolument aucune fonction n'est prévue pour donc ça n'a pas vraiment d'intérêt (les threads de calcul pur peuvent rester dans gint).

Conclusion

Cette PR était la juste pour te dire que les threads sont possiblement implémentable et isolable dans une lib si on prévoit les choses correctement en amont. Pour ma part, je comptais implémenter un hyperviseur de type-2 dans Gint pour cloisonner l'OS, le threader et le tracer avec des contextes complets (je m'explique à la fin)

De tout ce que je vois, ça passe dans le modèle que je présente. Cela dit, pour qu'on puisse progresser, il faut séparer le processus en quelques phases :

1. Fixer ce qu'on veut faire (cahier des charges)
2. Définir et valider un modèle
3. Implémenter et tester le modèle
4. Créer les applications

Ici j'ai attaqué 1 et 2. On n'est pas très loin dans le brainstorming sur ce sujet, donc ce n'est pas trop tard pour continuer à ajuster le cahier des charges, mais je pense qu'on aura vite fait le tour (à toi de voir). Une fois que ce sera le cas il faudra se concentrer sur la suite.
Yatis Hors ligne Membre Points: 575 Défis: 0 Message

Citer : Posté le 14/01/2021 22:26 | # | Fichier joint


Je suis d'accord tout ce que tu as dit, c'est pour ça que mon message sera bref.

Après réflexion, je me rends compte (même si je l'avais déjà remarqué) qu'il n'y a aucun intérêt concret à implémenter le support des threads directement dans Gint. Mais je pense qu'il faut que Gint prennent en compte que des personnes comme moi risquent de demander des mécanismes assez complexes donc autant prévoir correctement le coup en amont pour rendre ça possible.

Basiquement je vois deux points qui posent problème pour implémenter des threads et/ou un tracer :
1) sécuriser Gint car pour l'instant il ne l'est pas du tout (même avec seulement les interruptions) du coup il faudra utiliser des "environnement" semis-atomique en bloquant les interruptions a certain endroit clef surtout au niveau des manipulations hardware.
2) revoir le comportement des timers pour permettre de donner un environnement atomique quand le callback est appelé, ça permettra de bouger entièrement le support des threads dans une librairie à part.
3) isoler la partie "world switch" de Gint et essayer de trouver un moyen de se débarrasser d'une limitation liée a ton implémentation des drivers qui "hardcode" seulement deux contextes par driver (Casio <-> Gint). Mais pour le coup, je ne vois pas trop comment rendre cette partie dynamique simplement et proprement, si tu as des idées je ne dis pas non. Je me demande aussi s’il ne faudrait carrément pas en faire un module à part (pour l'instant c'est enclavé dans l'initialisation du kernel).

Je verrai bien Gint comme ça:

On peut écraser uniquement la partie "Worlds manager".

Voilà la TODO liste que je vois :

- sécurisé le code avec des notions de semis-atomicité en bloquant uniquement les interruptions (en gros, IMASK a 15).
    - Coder les `atomic_begin()` et `atomic_end()`.
    - Modifier les fonctions de Gint qui doivent etre sécurisé.
- update l'API des timers pour permettre de choisir la priorité + autoriser ou non les interruptions.
    - Modifier `timer_setup()` en ajoutant un flag de setup (?)
    - Modifier la routine `_gint_inth_callback_reloc()` (?)
- revoir la module qui gère les drivers:
    - isoler la partie "driver"
    - trouver un moyen de rendre "dynamique" le chargement/déchargement des drivers
    - permettre d'autoriser ou non, certain driver à sauvegarder / restaurer leur contexte (USB, VBR, ...)
- isoler le "world switch" actuel de Gint
    - utiliser la nouvelle interface pour les drivers
    - ajouter `gint_switch_to_casio()`, pour rendre temporairement la main a Casio (indispensable si on veux utiliser Bfile)
    - ajouter `gint_switch_to_self()`, pour reprendre la main.
    - ajouter l'attribue "weark" a chaque function, histoire de pouvoir écraser cette partie avec un truc custom.
- tester / documenter

Voilà, c'est tout ce qu'on a à faire pour l'instant, on ne devra pas en avoir pour long je pense



Le code de start.c qui appelle main() peut juste faire un gros join sur tous les threads entre la fin de main() et l'appel des destructeurs ?

Oui mais le problème réside dans le "lancement" de l'ordonnanceur : le premier thread va écraser le contexte actuel donc on ne pourra jamais revenir là où on en était. Sauf avec un longjmp() ou en créant, à la volée, un thread (sans forcément qu'il soit lié à l'ordonnanceur) qui sera rechargé quand on voudra revenir au niveau du start.c.

J'ai fait le choix threader la fonction main() et de faire un longjmp() quand il finit (par souci d'économie de mémoire et de temps car c'étaient plus simples à mettre en place). Par contre, tu es obligé de join() dans le bootstrap en attendant de te faire écraser et / où threader car l'ordonnanceur met un petit temps avant de switcher le premier contexte (mais c'est fixable ça).

Dans tous les cas, les threads sont intéressants mais trop complexes et trop peu utiles comparés aux interruptions, leur implémentation dans Gint n'ont pas vraiment d'intérêt...mais autant laissé la possibilité de mettre en place une telle infrastructure car ça coute pas grand-chose finalement.

Le code de construction/destruction et la fonction main() ne s'exécutant jamais en parallèle, pourquoi les séparer en plusieurs threads ?

Le fait de "threader" le boostrap permettait de laisser tourner des trucs en arrière-plan même quand on sortait de la main(), comme on peut configurer Gint de telle sorte à ce qu'il rappelle la fonction main(), ça avait quelque petit intérêt pour certaines applications mais sans plus. Et le destructeur du module de thread s'occupait de kill tous les threads pour sortir de l'addins "proprement".



Autre point, qui n'a rien à voir avec cette thématique mais qui m'intéresse.
Est-ce que ça vaudrait le coup de reprendre le projet de libc dont j'avais parlé il y a quelque mois et le linker à Gint ? ça m'éviterait d'avoir une version de Gint modifié de mon côté avec des fonctions / types en plus style off_t, atoi(), atoll(), ... ? En plus, je pense avoir trouvé une "bonne" façons de bypass les limitations de Bfile mais ça demandera un post complet car on s'éloignerait du sujet principal. On verra ça plus tard.
Lephenixnoir En ligne Administrateur Points: 22869 Défis: 149 Message

Citer : Posté le 15/01/2021 15:45 | #


Mais je pense qu'il faut que Gint prennent en compte que des personnes comme moi risquent de demander des mécanismes assez complexes donc autant prévoir correctement le coup en amont pour rendre ça possible.

Voilà, c'est comme ça que je le vois aussi !

1) sécuriser Gint car pour l'instant il ne l'est pas du tout (même avec seulement les interruptions) du coup il faudra utiliser des "environnement" semis-atomique en bloquant les interruptions a certain endroit clef surtout au niveau des manipulations hardware.

Pas besoin d'environnement, mais oui il faut bloquer les interruptions. Pour correctement identifier ce qui doit être isolé et quelles conditions sont nécessaires, c'est bien de pas plonger tout de suite dans le code et de faire un peu de raisonnement.

Ce qu'on veut protéger c'est l'ensemble des fonctions ininterruptibles (« noyau »). Par construction, bloquer les interruptions suffit à garantir que le code ne soit pas interrompu. On n'a pas besoin (et on ne veut pas) bloquer les TLB miss et les exceptions génériques puisque tout le code est interruptible par ces deux choses-là. D'où IMASK=15.

On veut aussi garantir une certaine sémantique sur atomic_begin() end atomic_end(), en particulier que atomic_end() restaure SR exactement à son état du atomic_begin(), ce qui n'est pas juste IMASK=0 (en particulier si par malheur on imbrique deux fonctions atomiques). Donc il faut garder la trace à la fois du nombre d'imbrications et du IMASK d'origine, qui peut être à une valeur non-nulle durant un callback de timer. Ça nous donne un truc comme :

static int atomic_depth = 0;
static int atomic_original = 0;

void atomic_begin()
{
    if(atomic_depth == 0) {
        sr_t sr = cpu_getSR();
        atomic_original = sr.IMASK;
        sr.IMASK = 15;
        cpu_setSR(sr);
    }
    atomic_depth++;
}

void atomic_end()
{
    atomic_depth--;
    if(atomic_depth == 0) {
        sr_t sr = cpu_getSR();
        sr.IMASK = atomic_original;
        atomic_original = 0;
        cpu_setSR(sr);
    }
}

Pour ce qui est de la liste des régions concernées, il n'y en a pas beaucoup (du tout). Voici une liste détaillée à la façon de attributes(7). Les accès à l'initialisation ne sont pas comptés parce que les threads ne seront pas activés à ce moment-là (le timer ordonnanceur de la lib de threads n'aura pas de contrôle tant que le world gint ne sera pas initialisé). Je note hwrace les fonctions qui sont MT-Unsafe à cause d'accès au hardware ; pour ceux-là je laisse l'utilisateur se démerder pour synchroniser ses affaires.

Dans l'ensemble, le but n'est pas d'exposer une API MT-Safe dans la plupart des cas. En réalité, si tu fais des threads ton programme sera structuré de façon à ne pas faire d'accès concurrents dès le début (tu n'auras pas 3 threads de dessin). Le véritable besoin c'est :

1. De préserver la sémantique actuelle, c'est-à-dire que le code actuel ne puisse pas crasher en présence d'un second threads qui fait que tourner en boucle ;
2. Dans certains cas seulement, de définir et supporter une sémantique MT-Safe, pour faciliter le job de quelqu'un qui code en multi-threadé.

Voici une liste détaillée des propriétés de l'implémentation actuelle. Les ":here:" dénotent les endroits où il y a des améliorations.

Drivers
cpg: MT-Safe (tout est en lecture seule ; sera MT-Unsafe hwrace avec l'overclocking)
dma: MT-Unsafe hwrace (un poil gênant pour les memcpy() et memset() accélérés qui utilisent toujours le channel 1)
dma_transfer_noint() devrait borderline être ininterruptible, l'ordre des flags actuel (DE, TE, AE, NMIE) est safe mais mieux vaut protéger
intc: MT-Unsafe hwrace (paramétrage des priorités)
mmu: MT-Unsafe const (quasiment MT-Safe donc)
r61524: Peut-être ininterruptible si l'écran supporte pas qu'on le fasse attendre, MT-Unsafe hwrace sinon
rtc: MT-Safe sur l'accès à l'heure, MT-Unsafe hwrace sur le timer RTC
spu: MT-Safe (pour l'instant)
t6k11: Probablement ininterruptible
tmu: MT-Unsafe hwrace
timer_reload() est ininterruptible si on change aussi TCNT
timer_start() et timer_stop() ont quelques passages ininterruptibles

Autres parties du noyau
Le world switch est ininterruptible (évidemment), et le world OS aussi (sauf UBC, éventuellement timer mais de préférence UBC seulement)
Le panic supporte les interruptions, mais devrait désactiver les interruptions qu'il ne veut pas ; subtil, à étudier
MT-Unsafe race sur les contenus de la VBR et le paramétrage des interruptions
• Les threads ne doivent pas être utilisés avant la fin des constructeurs, ce qui est bon tant que tu as pas un constructeur qui crée des threads

Bibliothèques
gray: MT-Unsafe (accès la VRAM, au mode de dessin global, aux délais)
Je planifie de modifier le dessin pour pouvoir dessiner sur des surfaces quelconques, ce qui évitera le race
gray_start() est ininterruptible (à protéger), gray_stop() aussi (mais c'est bon parce que timer_pause() l'est)
gray_int() doit être ininterruptible aussi, on peut juste demander une interruption atomique
render: MT-Unsafe (accès VRAM et des états globaux)
std: MT-Safe dans la plupart des cas, MT-Unsafe pour printf() (globales)
printf() devrait être MT-Safe

2) revoir le comportement des timers pour permettre de donner un environnement atomique quand le callback est appelé, ça permettra de bouger entièrement le support des threads dans une librairie à part.

Ça j'y ai touché de loin dans mon post précédent, mais en fait ce n'est pas nécessaire. Ça se voit en deux étapes :

1. On a besoin de pouvoir interrompre des callbacks, ne serait-ce que pour faire tourner le moteur de gris pendant un callback un peu long.
2. Si le callback utilise atomic_begin() et atomic_end(), ce n'est pas grave s'il y a une petite fenêtre interruptible.

Imaginons qu'un callback normal se fasse interrompre entre son appel et sa première instruction, par le moteur de gris ou par l'ordonnanceur. Aucun problème : le thread est à ce moment-là en userland, il a « simplement » les données laissées par le gestionnaire d'interruption dans sa pile. Le changement de thread sauvegarde SR et r15 et tout ce qui va avec donc le nouveau thread ne sera pas affecté cet état. Lorsqu'on reviendra dans le thread, on arrivera direct dans le callback avec le SR du callback (qui a un IMASK égal au niveau de l'interruption l'ayant provoqué), la fonction s'exécutera, et à la fin on remontera la pile pour retourner dans le gestionnaire d'interruption. D'une certaine façon, le gestionnaire d'interruption est interruptible durant le callback, mais ça c'est voulu et prévu.

Quant à l'ordonnanceur lui-même, il est supposé avoir une priorité maximale pour qu'il ne puisse pas se faire interrompre. Faut voir après si le moteur de gris et de son (si ça existe) ne seraient pas plus importants.

Je n'ai pas d'opposition particulière à implémenter des callbacks atomiques, mais moins il y a de choses atomiques et mieux c'est, pas mal de modules en dépendent ou en dépendraient (moteur de gris, communication 3-pin ou USB, écran, son) et il vaut mieux que le système soit le plus réactif possible (en ce sens je contredis un peu l'assertion précédente que « on ne fait pas un RTOS » : on fait un peu un RTOS dès qu'on essaie de maintenir une bonne réactivité dans les situations où les performances sont limite).

3) isoler la partie "world switch" de Gint et essayer de trouver un moyen de se débarrasser d'une limitation liée a ton implémentation des drivers qui "hardcode" seulement deux contextes par driver (Casio <-> Gint). Mais pour le coup, je ne vois pas trop comment rendre cette partie dynamique simplement et proprement, si tu as des idées je ne dis pas non. Je me demande aussi s’il ne faudrait carrément pas en faire un module à part (pour l'instant c'est enclavé dans l'initialisation du kernel).

Exactement. Pour l'instant les contextes sont alloués dans la section .gint.bss qui est en gros de la RAM statique. Je pense que la bonne façon de généraliser ça et d'allouer dynamique dans la RAM statique.

Je réfléchis à deux options, mais on en gros on peut soit faire un allocateur naïf qui ne libère jamais la mémoire, soit réfléchir à un design d'allocateur intelligent qu'on pourrait ensuite étendre pour faire des tas custom dans l'ILRAM ou autre. Cet allocateur intelligent pourrait gouverner aussi le tas de l'OS en utilisant le malloc() de l'OS à cet endroit. Ça résoudrait une partie des problèmes de l'allocation dynamique qu'on a actuellement.

Une fois que ça c'est fait la suite est pas trop dure, chaque driver a son ctx_size donc on peut créer des contextes à la volée. Le but original était d'ailleurs de stocker les contextes sur la pile lors du passage de gint à l'OS.

Je verrai bien Gint comme ça:

Le gestionnaire de mondes ne contient guère que ces quelques fonctions pour changer de monde donc ce serait difficile (ou pas très intéressant) de le séparer du gestionnaire de drivers. Mais ce n'est qu'un détail interne.

Pour ce qui est d'écraser le gestionnaire de mondes, je préfère quand c'est possible avoir des options et des callbacks plutôt que remplacer du code. La raison est que ça explicite les interactions et ça permet de mieux maintenir le code quand c'est possible. De plus, dans le cas de la lib threads et du traceur, c'est toi qui appelera le gestionnaire de mondes pour faire la transition, donc tu peux toujours ajouter le code que tu veux avant et après. Je ne me souviens pas qu'on ait évoqué de modifier le comportement de base de recharger les drivers (à part peut-être de préserver quelques drivers). Est-ce que j'ai raté quelque chose ? Est-ce que tu peux préciser ce que tu voudrais faire au gestionnaire de mondes pour voir s'il y aurait une API acceptable pour ça ?

Oui mais le problème réside dans le "lancement" de l'ordonnanceur : le premier thread va écraser le contexte actuel donc on ne pourra jamais revenir là où on en était. Sauf avec un longjmp() ou en créant, à la volée, un thread (sans forcément qu'il soit lié à l'ordonnanceur) qui sera rechargé quand on voudra revenir au niveau du start.c.

L'ordonnanceur n'a pas besoin de créer le premier thread ; gint le crée. Tes structures de données peuvent être initialisées directement sans faire de saut. Tu peux voir ça comme « gint te donne le premier thread, tu crées les autres ». Le runtime de gint te fournit de quoi arrêter tous les autres threads avec des join dans un destructeur. Actuellement le mécanisme de gint_restart est un peu vite fait, en réalité ce serait mieux de mettre les constructeurs/destructeurs dedans. Je n'ai pas tous les détails en tête mais je ne vois pas où ça planterait dans le principe.

Le fait de "threader" le boostrap permettait de laisser tourner des trucs en arrière-plan même quand on sortait de la main(), comme on peut configurer Gint de telle sorte à ce qu'il rappelle la fonction main(), ça avait quelque petit intérêt pour certaines applications mais sans plus. Et le destructeur du module de thread s'occupait de kill tous les threads pour sortir de l'addins "proprement".

Le mécanisme de redémarrage n'était pas très solide, peut-être qu'on peut regarder là en premier lieu du coup.

Est-ce que ça vaudrait le coup de reprendre le projet de libc dont j'avais parlé il y a quelque mois et le linker à Gint ? ça m'éviterait d'avoir une version de Gint modifié de mon côté avec des fonctions / types en plus style off_t, atoi(), atoll(), ... ? En plus, je pense avoir trouvé une "bonne" façons de bypass les limitations de Bfile mais ça demandera un post complet car on s'éloignerait du sujet principal. On verra ça plus tard.

Je pense qu'il ne peut y avoir que des intérêts à avoir une libc plus riche. L'allocation dynamique dont je parle plus haut est un point qui nous manque, la libc en est un autre. Il y a toujours la possibilité de la porter, mais on a eu un succès mitigé avec newlib alors...

Je déplacerais volontiers mes fonctions de gint vers la nouvelle lib. Pour l'instant je ne me suis pas précipité simplement parce qu'on n'a pas encore bien posé le cadre : tu parlais dans les derniers messages de viser un style glibc, qui ne me paraît pas forcément opportun, et les mécanismes de compilation/intégration n'étaient pas tout à fait clairs pour moi. C'est peut-être le bon moment d'en parler.
Ne0tux Hors ligne Membre d'honneur Points: 3505 Défis: 265 Message

Citer : Posté le 17/01/2021 19:03 | #


Mais où est Gavroche ?

...

Il est toujours dans le coin dès qu'il y a une bataille de pavés !

Mes principaux jeux : Ice Slider - CloneLab - Arkenstone

La Planète Casio est accueillante : n'hésite pas à t'inscrire pour laisser un message ou partager tes créations !
Lephenixnoir En ligne Administrateur Points: 22869 Défis: 149 Message

Citer : Posté le 18/01/2021 23:10 | #


Ce n'est pas une bataille, c'est juste un sujet compliqué avec beaucoup de points tordus...
Yatis Hors ligne Membre Points: 575 Défis: 0 Message

Citer : Posté le 21/01/2021 13:22 | #


Pour ce qui est de la partie sur les notions atomiques c'est déjà ce que j'utilisais dans Vhex depuis un certain temps maintenant avec exactement le même comportement que tu décris ici.


Dans l'ensemble, le but n'est pas d'exposer une API MT-Safe dans la plupart des cas.

Sans parler des threads, tu te retrouves avec les mêmes problèmes que moi avec tes interruptions. Quand je disais que c'était dangereux que Gint ne propose pas d'API MS-safe c'était surtout pour la partie qui touche à du hardware (imagine que tu te fasses couper en plein milieu d'une configuration et que le callback d'un des timers passe au même endroit, tu vas avoir pas mal de problème).

Mais je comprends pourquoi tu ne veux pas sécuriser ces parties-là mais dans ce cas il faudra (je ne sais pas si c'est le cas) explicité exactement la liste des fonctions MS-safe ou non quelque part (genre le *printf*()), histoire que les utilisateurs ne se fassent pas avoir avec des bugs fourbes. Puis ça t'évitera d'avoir des coups de frayeur si quelqu'un viens avec un crash non déterministe.


Quant à l'ordonnanceur lui-même, il est supposé avoir une priorité maximale pour qu'il ne puisse pas se faire interrompre. Faut voir après si le moteur de gris et de son (si ça existe) ne seraient pas plus importants.

Plus je pense à cette histoire de threads, plus je me dis que c'est incroyablement spécifique. Finalement, oui, j'ai besoin uniquement de pouvoir faire des opérations atomiques, j'aurais carrément pus y mettre dans une librairie tout compte fait (mais comme je comptais modifier Gint de telle sorte a ce qu'il puisse proposer uniquement une API MS-safe, c'était plus simple de faire comme j'ai fait). Donc je migrerais mon support des threads dans une lib à part (fxThread pour le nom ? Je colle les headers de la lib dans <gint/thread/*.h> ? ou dans <fxThread/*.h> ? Comment tu veux qu'on installe des libs qui sont dépendants de Gint ?)


Je réfléchis à deux options, mais on en gros on peut soit faire un allocateur naïf qui ne libère jamais la mémoire, soit réfléchir à un design d'allocateur intelligent qu'on pourrait ensuite étendre pour faire des tas custom dans l'ILRAM ou autre. Cet allocateur intelligent pourrait gouverner aussi le tas de l'OS en utilisant le malloc() de l'OS à cet endroit. Ça résoudrait une partie des problèmes de l'allocation dynamique qu'on a actuellement.

Je n'ai pas trop compris pourquoi refaire un allocateur et surtout quel sont les problèmes d'allocation actuels ? C'est juste la taille disponible qui est petite ? En gros, tu voudrais utilise le malloc de Casio jusqu'à ce qu'il casse pour basculer ensuite sur l'allocateur de Gint ? Pourquoi la ILRAM ? Juste pour stoker le cache de l'allocateur ?



Une fois que ça c'est fait la suite est pas trop dure, chaque driver a son ctx_size donc on peut créer des contextes à la volée. Le but original était d'ailleurs de stocker les contextes sur la pile lors du passage de gint à l'OS.

J'avais pensé à ça aussi mais ça risque d'être assez complexe si on a plusieurs OS / addins à gérer.

Parce que, pour faire super-simple sur ce dont j'ai besoin: J'ai un projet Epitech qui, à la base, consistais à créer un boot loader basé uniquement sur les syscall de Casio et qui m'aurais permis de charger des noyaux (Vhex / GladOS / FixOS) au début de la RAM, écrasant 60000% des données de Casio au passage. Sauf que ce n'est pas possible parce que, sur les calculatrices qui utilisent FUGUE, le FS est en RAM (donc je ne peux pas "facilement" m'installer et je n'ai pas envie de me relancer dans ce genre de projet actuellement).

Comme ça détruit l'essence même du projet que j'avais proposé a Epitech et que je n'avais pas envie de re-re-re-re-refaire un kernel, j'ai donc longuement réfléchi sur le "comment faire un truc parfaitement inutile sur calculatrice, proche de mon sujet actuel, incroyablement complexe, le tout en utilisant Gint?"... et j'ai trouvé une idée : Créer une gestionnaire de monde (que j'appelle hyperviseur car c'est en partie vrai, et peut être vrai par la suite).

Le projet fournis une console pour contrôler et installer (dans l'ILRAM) un hyperviseur qui permet de charger et de lancer des addins compilés en PIE (sans header gxa, juste au format ELF). Et, ce qui est fun, c'est que je vais relocaliser à la main les symboles de Gint pendant le chargement des jeux en mémoire. Ce qui me permet d'avoir plusieurs addin Gint qui tourne "en parallèle et en utilisant un seul exemplaire de Gint". De plus, avec une combinaison de touches, je peux switch entre les addins. En gros tu vois le "quick resume" de la Xbox Série X ? Bah c'est la même chose pour Gint.

Actuellement, j'ai une console avec quelque commandes alakon dont une qui me permet de charger un addin avec l'environnement de Casio. Et voilà, ce dont j'ai besoin (et ce que je vais faire de toute façon) :
- de pouvoir contrôler quel driver Gint restaure (pour éviter de restaurer la VBR + l'UBC parce que je vais probablement avoir besoin des deux).
- de pouvoir avoir plusieurs instances de driver (pour générer plusieurs environnent).
- de pouvoir compiler Gint en .so (je sais que le format ne fonctionne pas mais comme l'addin qui gère l'hyperviseur contient déjà un exemplaire de Gint ce n'est pas bloquant car on connaît les noms des symboles + on sait où ils sont mappés donc la relocalisation se fait sans trop de problèmes, reste à trouver une manière élégante de le faire).
- de pouvoir avoir une API MS-safe du côté de Gint (parce que, autant les threads on sait quand on utilise une fonction MS-unsafe, autant quand on switch d'addin ce n'est pas le cas. Je sais que tu ne veux pas proposer une API MS-safe donc je te propose d'attendre la fin de mon projet, s’il voit le jour que ça a vraiment un intérêt pour tout le monde de donner une API MS-safe on essaie de trouver un terrain d'entente(?)).

Les deux derniers points étant vachement expérimentaux, je compte m'occuper de ça seul. Et pour ce qui est du projet en question, je devrai pouvoir me débrouiller donc je ne t'embête pas avec ça (je sais qu'il y a beaucoup de difficultés techniques mais je pense m'en sortir).


Le gestionnaire de mondes ne contient guère que ces quelques fonctions pour changer de monde donc ce serait difficile (ou pas très intéressant) de le séparer du gestionnaire de drivers. Mais ce n'est qu'un détail interne.

C'est absolument vrai, mais pour le coup, je pense que séparer la partie "driver" (src/kernel/kernel.c -> src/kernel/driver.c) pour en faire un module à part est intéressant. C'est du détail hein? C'est juste que si on ajoute cette notion de contexte "dynamique" ça risque de complexifier un peu plus cette partie-là.



Est-ce que j'ai raté quelque chose ? Est-ce que tu peux préciser ce que tu voudrais faire au gestionnaire de mondes pour voir s'il y aurait une API acceptable pour ça ?

Je pensais avoir une API en tête mais non en fait, le plus important étant le module qui gère le chargement / déchargement des drivers, c'est juste lui qui a besoin d'être modifié pour générer des contextes sur demande. Pour le reste je devrai pouvoir faire une lib qui contourne les potentielles limitations de je croiserai et si vraiment je croise des limitations redondantes je t'en parlerai.



L'ordonnanceur n'a pas besoin de créer le premier thread ; gint le crée. Tes structures de données peuvent être initialisées directement sans faire de saut. Tu peux voir ça comme « gint te donne le premier thread, tu crées les autres ».

Même pas ! Gint n'a pas à générer quoi que ce sois en fait, je peux construire un contexte "a-la-volée" avec une globale alakon lors de la première interruption de l'ordonnanceur. Donc tu n'as même pas à t'embêter avec les threads en fait.



Je pense qu'il ne peut y avoir que des intérêts à avoir une libc plus riche. L'allocation dynamique dont je parle plus haut est un point qui nous manque, la libc en est un autre. Il y a toujours la possibilité de la porter, mais on a eu un succès mitigé avec newlib alors...
[...] tu parlais dans les derniers messages de viser un style glibc, qui ne me paraît pas forcément opportun, et les mécanismes de compilation/intégration n'étaient pas tout à fait clairs pour moi. C'est peut-être le bon moment d'en parler.

Ce que j'avais commencé à faire étais simple : Reprendre des fonctions bateaux et trouver un moyen de compiler la lib en fonction de l'ABI que l'on souhaite (entre Casio (fxcg50), Casio (fx9860) et Vhex pour l'instant). Une grosse partie de la lib et encore en local et je réfléchis à une autre façon de gérer le versionning (qui est actuellement manuelle), il faut aussi que je trouve un autre Git workflow car mon organisation actuelle ne me plais plus.

Aussi, quand je parlais de glib à l'époque, je voulais juste reprendre l'API qu'elle propose et la trafiquer un peu pour la rendre plus adaptée à la machine. Mon but n'est pas de refaire une glibc complète, ça n'aurait que peu intérêt.
Les gros points importants de la lib sont:
- (Casio/Gint) Proposer une abstraction de Bfile pour permettre de faire une bonne partie des opérations en RAM (je pense avoir trouvé une bonne méthode mais je n'ai pas eu le temps de vraiment faire des tests)
- (Gint) Proposer un allocateur de mémoire additionnel à celui de Casio.
- (All) Optimiser les fonctions bateaux.

Pour le coup, ce projet perd vachement en intérêt car je n'ai pas beaucoup de temps pour refaire mes carabistouilles de mon côté, puis je suis surement dans les dernières lignes droites de mes projets débiles sur PC. Une fois mon projet d'hyperviseur et celui de documenter FUGUE finis, je prendrai probablement ma retraite. Donc autant que j'essaie d'aider, tant bien que mal, au développement de Gint.

LienAjouter une imageAjouter une vidéoAjouter un lien vers un profilAjouter du codeCiterAjouter un spoiler(texte affichable/masquable par un clic)Ajouter une barre de progressionItaliqueGrasSoulignéAfficher du texte barréCentréJustifiéPlus petitPlus grandPlus de smileys !
Cliquez pour épingler Cliquez pour détacher Cliquez pour fermer
Alignement de l'image: Redimensionnement de l'image (en pixel):
Afficher la liste des membres
:bow: :cool: :good: :love: ^^
:omg: :fusil: :aie: :argh: :mdr:
:boulet2: :thx: :champ: :whistle: :bounce:
valider
 :)  ;)  :D  :p
 :lol:  8)  :(  :@
 0_0  :oops:  :grr:  :E
 :O  :sry:  :mmm:  :waza:
 :'(  :here:  ^^  >:)

Σ π θ ± α β γ δ Δ σ λ
Veuillez donner la réponse en chiffre
Vous devez activer le Javascript dans votre navigateur pour pouvoir valider ce formulaire.

Si vous n'avez pas volontairement désactivé cette fonctionnalité de votre navigateur, il s'agit probablement d'un bug : contactez l'équipe de Planète Casio.

Planète Casio v42 © créé par Neuronix et Muelsaco 2004 - 2023 | Il y a 56 connectés | Nous contacter | Qui sommes-nous ? | Licences et remerciements

Planète Casio est un site communautaire non affilié à Casio. Toute reproduction de Planète Casio, même partielle, est interdite.
Les programmes et autres publications présentes sur Planète Casio restent la propriété de leurs auteurs et peuvent être soumis à des licences ou copyrights.
CASIO est une marque déposée par CASIO Computer Co., Ltd