Le multitâche préemptif du code natif peut-il être implémenté dans l’espace utilisateur sous Linux?

Je me demande s’il est possible de mettre en œuvre un traitement multitâche préemptif du code natif au sein d’un seul processus dans l’espace utilisateur sous Linux. (C’est-à-dire, suspendre de manière externe un code natif en cours d’exécution, enregistrer le contexte, échanger dans un contexte différent et reprendre l’exécution, le tout orchestré par l’espace utilisateur, mais à l’aide d’appels pouvant entrer dans le kernel.) Je pensais que cela pourrait être fait à l’aide d’un signal gestionnaire pour SIGALRM et la famille *context() , mais il s’avère que toute la famille *context() est asynchrone-signal-non sécurisée, de sorte que cette approche n’est pas garantie. J’ai trouvé un élément essentiel qui implémente cette idée, alors apparemment, il arrive que cela fonctionne sous Linux, du moins parfois, même si, par POSIX, il n’est pas obligé de fonctionner. L’essentiel l’installe en tant que gestionnaire de signaux sur SIGALRM , qui effectue plusieurs appels *context() :

 void timer_interrupt(int j, siginfo_t *si, void *old_context) { /* Create new scheduler context */ getcontext(&signal_context); signal_context.uc_stack.ss_sp = signal_stack; signal_context.uc_stack.ss_size = STACKSIZE; signal_context.uc_stack.ss_flags = 0; sigemptyset(&signal_context.uc_sigmask); makecontext(&signal_context, scheduler, 1); /* save running thread, jump to scheduler */ swapcontext(cur_context,&signal_context); } 

Est-ce que Linux offre une garantie qui rend cette approche correcte? Y a-t-il un moyen de corriger cela? Y a-t-il une manière totalement différente de faire ceci correctement?

(Par “implémenter dans l’espace utilisateur”, je ne veux pas dire que nous n’entrons jamais dans le kernel. Je veux contraster avec le multitâche préemptif implémenté par le kernel.)

Vous ne pouvez pas modifier de manière fiable les contextes dans les gestionnaires de signaux. (Si vous le faites depuis un gestionnaire de signal, cela fonctionnera généralement dans la pratique, mais pas toujours, d’où un comportement indéfini ).

Vous pouvez définir un indicateur volatile sig_atomic_t (en savoir plus sur sig_atomic_t ) dans un gestionnaire de signaux (voir signal (7) , signal-safety (7) , sigreturn (2) …) et vérifier cet indicateur régulièrement (par exemple au moins une fois par mois). millisecondes) dans votre code, par exemple avant la plupart des appels, ou à l’intérieur de votre boucle d’événement si vous en avez un, etc. Il devient donc une planification coopérative en fonction de l’ utilisateur.

Cela est plus facile si vous pouvez modifier le code, par exemple lorsque vous concevez un compilateur émettant du code C ( pratique courante ) ou si vous piratez votre compilateur C pour émettre de tels tests. Ensuite, vous allez modifier votre générateur de code pour parfois émettre un tel test dans le code généré.

Vous pouvez interdire le blocage des appels système et les remplacer par des variantes ou des wrappers non bloquants . Voir aussi poll (2) , fcntl (2) avec F_SETFL et O_NONBLOCK , etc …

Vous voudrez peut-être que le générateur de code évite les stacks d’appels volumineuses, comme le -fsplit-stack option d’instrumentation -fsplit-stack de GCC (pour en savoir plus sur les splitstacks dans GCC).

Et si vous générez (ou écrivez certains) assembleur, vous pouvez utiliser de telles astuces. Autant que je sache, le compilateur Go utilise quelque chose de similaire pour ses goroutines. Étudiez votre ABI , par exemple d’ ici .

Cependant, la planification préemptive lancée par le kernel est préférable (et sous Linux, il se produira toujours entre des processus ou des tâches du kernel, voir clone (2) ).

PS Si les techniques de collecte des déchets utilisant des astuces similaires vous intéressent, examinez MPS et Cheney dans MTA (par exemple, dans Chicken Scheme ).