Article écrit le 15 avril 1999 pour l'ARMada News n°17. Noter que les exemples de code ne fonctionnent qu'en mode 26 bits.

Nice

Non, non, je ne vais pas vous parler de plage ou de festival, mais d'un petit utilitaire que j'ai écrit et qui permet en gros d'avoir un système de priorités sur les tâches WIMP.

Cet utilitaire est téléchargeable sur <http://www.vinc17.net/acorn/>.

À quoi ça sert?

L'année dernière, j'ai installé sur ma machine le client RC5DES, pour participer au craquage d'un algorithme de cryptographie: la plupart du temps, ma machine n'est pas utilisée, ou en tout cas, le processeur est très peu chargé, et ce programme est fait pour utiliser le processeur pendant ce temps. Malheureusement, quand la machine est réellement utilisée, ce programme continue à prendre du temps CPU, et cela ralentit toutes les autres tâches.

Sous Unix, il existe un système de priorités entre les divers processus: on peut donner une faible priorité au client RC5DES; ainsi le processeur ne sera utilisé par ce processus que lorsqu'il n'aura rien d'autre à faire. C'est la commande nice qui permet de donner à un processus une plus faible priorité.

Nous n'avons pas de système similaire en standard sous RISC OS, d'où l'idée d'en écrire un. Je l'ai appelé nice par analogie avec la commande correspondante sous Unix.

Principe

Le système (plus précisément, le WIMP) donne régulièrement la main aux diverses tâches, mais contrairement à Unix, ce sont plutôt les tâches qui décident comment le temps doit être réparti (et souvent, pas très équitablement): une tâche n'est pas interrompue par le système, c'est elle-même qui doit rendre la main au bout d'un certain temps à l'aide de l'appel système (SWI) Wimp_Poll, mais ce temps dépend de l'application et de l'environnement; on appelle ça du multitâche coopératif. Il est donc difficile de savoir si le processeur est réellement occupé par des tâches importantes, mais l'idée est la suivante: plus le processeur est chargé, moins souvent la main sera donnée à chacune des tâches qui en ont besoin. Il suffit donc de mesurer ce temps entre deux retours de Wimp_Poll, mais ceci reste très vague, car certaines tâches peuvent très bien rendre la main rapidement bien qu'elles aient vraiment besoin du processeur.

Pour un processus qui tourne dans une TaskWindow, c'est le même principe: le processus n'appelle pas directement Wimp_Poll, mais c'est la TaskWindow qui interrompt le processus (un peu comme sous Unix — on parle de multitâche préemptif), et c'est aussi la TaskWindow qui appelle Wimp_Poll, car elle tourne dans un environnement coopératif. On ne fera donc pas de distinction entre les tâches WIMP et celles tournant en TaskWindow (vues aussi comme des tâches WIMP, de manière externe).

Considérons maintenant une tâche dont on veut baisser la priorité; appelons-la la tâche courante. Elle appelle régulièrement Wimp_Poll pour rendre la main au système ou communiquer avec d'autres applications, et entre deux appels Wimp_Poll consécutifs, elle effectue des calculs; cela peut aussi être une tâche tournant dans une TaskWindow, et les appels à Wimp_Poll sont effectués par le système de manière transparente. Le principe de mon utilitaire est de filtrer les Wimp_Poll à leur retour, i.e. quand la main doit normalement être redonnée à la tâche courante: si le processeur n'est pas chargé, la main est effectivement redonnée à la tâche courante (comme si nice n'était pas là); si le processeur est chargé, la main n'est pas redonnée à la tâche (en gros, on intercepte le retour de Wimp_Poll). En pratique, c'est un peu plus compliqué que cela, et il se peut, suivant la charge de la machine, que seule une certaine proportion de Wimp_Poll soit interceptée.

Plus précisément, Wimp_Poll peut retourner un événement à la tâche (une fenêtre doit être redessinée, ouverte ou fermée, événement lié à la souris ou au clavier, message en provenance d'une autre application). Ou alors il peut ne pas y avoir d'événement (on peut parler de null event); c'est typiquement le cas d'un programme qui effectue des calculs en tâche de fond, le Wimp_Poll servant exclusivement au multitâche, et non à communiquer. Pour privilégier les communications et éviter de filtrer les vrais événements, nice ne filtrera que les Wimp_Poll retournant un null event.

À chaque fois qu'un Wimp_Poll de la tâche courante retourne un null event, une routine est donc appelée. Cette routine calcule le temps total qui s'est écoulé entre le dernier null event et l'avant-avant-dernier null event (sauter un null event dans le calcul du temps permet d'être un peu plus précis). Si ce temps est strictement inférieur à un certain temps T (choisi par l'utilisateur), alors la main sera redonnée à la tâche courante; sinon, la main est redonnée au système.

Implémentation

Note: les SWI ne seront pas détaillés. Je vous renvoie aux manuels StrongHelp (que tout programmeur sous RISC OS doit avoir) pour cela.

À chaque tâche nicée, on associe un post-filtre WIMP (créé avec SWI XFilter_RegisterPostFilter) et une structure de 16 octets. Cette structure contient le paramètre T en centisecondes (8 bits), le temps entre les deux derniers null event (8 bits), le handle de la tâche nicée (16 bits), la date du dernier null event (32 bits), et un vecteur de 64 bits, appelé vecteur d'activité.

Les bits du vecteur d'activité correspondent aux 64 derniers null event. Le bit correspondant est à 1 si et seulement si le Wimp_Poll a été intercepté. Cela permettra à l'utilisateur de connaître la proportion de Wimp_Poll interceptés parmi les derniers.

Une tâche est ajoutée à l'aide du code suivant (en partie). Le registre R2 pointe sur la structure, R4 contient le paramètre T, et R3 le handle de la tâche.

          ORR     R4, R4, R3, LSL #16
          STR     R4, [R2]            ;Store the task handle and nice time.
          STR     R0, [R2, #8]        ;Activity vector = 0
          STR     R0, [R2, #12]       ;(two 32-bit words).
          ADR     R0, RM_Title        ;Filter name: Nice.
          ADR     R1, filter          ;Filter routine.
          MVN     R4, #1              ;Event mask: null reason.
          SWI     XFilter_RegisterPostFilter
          MOVVS   PC, R6              ;Return with V=1 if error.
          SWI     XOS_ReadMonotonicTime
          STR     R0, [R2, #4]        ;Store monotonic time to the table.

La routine est la suivante...

filter    TEQ     R0, #0
          MOVNES  PC, LR              ;Return unless null reason.
          STMFD   SP!, {R1,LR}
          LDR     R1, [R12, #4]       ;Old monotonic time.
          SWI     XOS_ReadMonotonicTime
          LDMVSFD SP!, {R1,PC}^       ;Return if error.
          STR     R0, [R12, #4]       ;New monotonic time.
          SUB     R0, R0, R1          ;Wimp_Poll time.
          LDRB    R1, [R12, #1]       ;Previous Wimp_Poll time.
          CMP     R0, #&FF
          MOVHI   R0, #&FF
          STRB    R0, [R12, #1]
          ADD     R0, R0, R1          ;R0: sum of the last 2 Wimp_Poll times.
          LDRB    R1, [R12]           ;R1: nice time.
          CMP     R0, R1              ;Last 2 Wimp_Poll times < nice time?
          MOVCC   R0, #0              ;Yes, R0 = 0. No, R0 = -1.
          LDR     R1, [R12, #8]       ;Update the activity vector...
          MVNCS   R0, #0
          MOVS    R1, R1, RRX
          STR     R1, [R12, #8]
          LDR     R1, [R12, #12]
          MOV     R1, R1, RRX
          STR     R1, [R12, #12]
          LDMFD   SP!, {R1,PC}^

Bien qu'à l'appel de Filter_RegisterPostFilter, on ait spécifié que l'on veut filtrer uniquement les null event, on teste le type de l'événement au début par mesure de précaution, et on retourne si ce n'est pas un null event. Ensuite, on sauve les registres qui seront modifiés; le registre LR doit être sauvé car la routine est exécutée en mode SVC et l'instruction SWI modifie donc ce registre. La date du null event précédent est lue dans la structure (pointée par le registre R12); cette date est mise à jour et on calcule la différence. La séquence CMP/MOVHI permet de faire une saturation à 255 cs (car ce temps est ensuite stocké sur un octet), ce qui est amplement suffisant: deux secondes et demi entre deux null event, c'est beaucoup. La somme des deux derniers temps est comparée à la valeur de T; on met R0 à 0 (valeur initiale correspondant à un null event) ou à -1 suivant que l'événement doit être passé à la tâche ou qu'il doit être intercepté. On met enfin à jour le vecteur d'activité à l'aide de rotations avec le bit C.

Le reste du code se trouve dans le source du module, distribué avec le module lui-même.

Manuel

Le module nice fournit 3 commandes: nice, nice+ et nicestat.

La commande *nice temps tâche permet de nicer une tâche existante, où temps est le paramètre T mentionné plus haut, et tâche est le nom de la tâche (comme ceux apparaissant après avoir cliqué sur l'icône Acorn, à droite sur la barre d'icônes). Par exemple:

*nice 25 "RC5DES client"

Si le temps est 0 (valeur spéciale), la tâche ne sera plus nicée. La commande nice+ est similaire à nice et a la même syntaxe, mais elle est permanente; la tâche n'a pas besoin d'exister au moment où cette commande est effectuée: à chaque fois que la tâche en question (reconnue par son nom) est exécutée, elle sera automatiquement nicée. Ceci est particulièrement utile pour le client RC5DES (le client, pas l'interface graphique), qui est très souvent quitté et relancé. La commande nicestat affiche l'état de nice et les statistiques concernant les tâches nicées (paramètre T et poids du vecteur d'activité).

Pour donner une basse priorité à une tâche, utiliser nice ou nice+ et choisissez le temps tel que quand la machine n'est pas chargée, la tâche prend quasiment tout le temps CPU, et quand la machine est chargée, la tâche prend peu de temps CPU ou pas du tout. C'est plus compliqué que sous Unix, car il faut choisir ce paramètre temps, mais en revanche, cela peut parfois être plus efficace. Le choix peut se faire à l'aide d'utilitaires, comme ceux de David J. Ruck, que l'on trouve sur: <ftp://ftp.armclub.org.uk/pub/pd/druck/>.

On peut nicer plusieurs tâches, mais cela ne marchera pas forcément bien (contrairement à Unix, où cela ne pose aucun problème), car il y a plus de paramètres qui interviennent.

Utilisation automatique avec le client RC5DES

Voici comment je fais pour l'utiliser automatiquement avec le client RC5DES. Je fais un RMRun dessus depuis le fichier !Boot.Choices.Boot.Tasks.!Boot et j'ai ajouté les lignes suivantes au début du fichier StartRC5 (qui se trouve dans le même répertoire, si le client RC5DES est lancé au démarrage):

Set Nice$Loaded yes
RMEnsure Nice 0 Set Nice$Loaded no
If "<Nice$Loaded>"="yes" Then %nice+ 20 "RC5DES client"
Unset Nice$Loaded

J'ai ensuite renommé StartRC5 en StartRC5_ et désélectionné l'option Autolaunch client, sinon à chaque fois que les choix du client sont modifiés, le fichier StartRC5 est recréé (et les lignes ci-dessus ne sont plus là).

Résultats

Voici quelques résultats... J'ai testé diverses opérations dans 3 contextes (aucune autre tâche ne prend du temps CPU):

  1. Client RC5DES arrêté. Il s'agit du même contexte que quand il n'y a pas de client RC5DES (machine non chargée).
  2. Client RC5DES tournant, mais avec un nice 25 (note: la valeur optimale dépend de la machine).
  3. Client RC5DES tournant, sans nice.

Le contexte habituel est le 1. Quelqu'un qui veut faire tourner le client RC5DES passe dans le contexte 3. Le but de mon programme nice (contexte 2) est de donner l'impression à l'utilisateur qu'il repasse dans le contexte 1 (machine non chargée) au moment de l'opération.

Les diverses opérations sont:

Le tableau suivant donne les temps en secondes pour effectuer ces opérations dans les 3 contextes.

Contexte Ouverture d'une page web CRC unzip -t
chargement traitement
RC5 arrêté 42 40 16 11
RC5 nicé 43 61 16 11
RC5 non nicé 44 88 25 16

Maintenant, est-ce que le client RC5DES est aussi efficace dans le contexte 2 que dans le contexte 3 quand la machine n'est pas chargée? Le tableau suivant montre que oui, en donnant le nombre de milliers de clés (RC5 ou DES) testées par seconde.

Contexte RC5 DES
RC5 non nicé 201 471
RC5 nicé 198 465

En effet, la différence relative est très faible. D'autre part, la machine est un peu chargée, car le programme de benchmark prend un peu de temps CPU.

Conclusion

Mon module nice marche plutôt bien, même s'il n'est pas complètement efficace (cf traitement lors de l'ouverture d'une page web). On peut obtenir de meilleurs temps en jouant sur le paramètre T, mais cela peut ralentir les calculs en tâche de fond (est-ce vraiment un problème?). En revanche, il n'est pas du tout efficace pour certaines choses, mais on doit pouvoir l'améliorer en prenant en compte d'autres paramètres (certains événements, comme les redraw). Mais en tout cas, il ne fait qu'améliorer les choses.



Valid XHTML 1.0! Level Double-A conformance icon, W3C-WAI Web Content Accessibility Guidelines 1.0.
Dernière modification:
webmaster@vinc17.org