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.
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/>.
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.
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.
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.
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.
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à).
Voici quelques résultats... J'ai testé diverses opérations dans 3 contextes (aucune autre tâche ne prend du temps CPU):
nice 25
(note: la valeur
optimale dépend de la machine).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:
unzip
-t
), dans une TaskWindow.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.
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.