[Kos-dev] Travaux de ce WE

d2 kos-dev@enix.org
12 Aug 2002 23:27:10 +0200


Bonjour,

Ce WE, Thomas et moi avons de nouveau reflechi a Babel, et avons
essaye de pondre une petite demo pour tester notre approche (voir
repertoire syscall-test du module kos-dev).

Dans cette approche, tout est articule autour du principe simple
suivant :
  - une _ressource_ peut posseder plusieurs _interfaces_
  - quand l'utilisateur demande une ressource (typiquement : open()),
    il doit preciser avec quelle "interface" il dialoguera avec elle.
  - les interfaces sont gerees globalement pour tout le systeme, mais
    les implantations des methodes d'une interface sont faites par les
    _gestionnaires de ressources_.
  - une interface est elle-meme une ressource (=> reflexivite), ce qui
    permet la generation automatique de stubs, et le lookup en
    run-time des methodes.
(Le modele est donne dans kos-dev/syscall-test/syscall.dia)

Les ressources, ce sont les objets logiques que l'utilisateur manipule
(fichier, peripheriques, sockets). Elles sont associees aux objets que
le noyau manipule (dits "shadow resource") : plusieurs ressources
(analogie unix =~ file descriptor) de plusieurs processus peuvent etre
associees a la meme shadow ressource (analogie Unix =~
inode/vnode). Par exemple (analogie Unix), le fichier sur disque
"/home/toto", en tant qu'ensemble de secteurs sur le disque, est
l'entite que le noyau manipulera (=> shadow ressource), et les
ressources associees (crees avec open()) contiennent juste le
"pointeur courant" pour les appels a read/write que l'utilisateur
fait. Je ne parle plus de shadow ressource dans la suite, pour me
concentrer au dialogue cpl3/cpl0. Ca fait que j'englobe a tort le
terme "shadow resource" dans celui de "resource".

Une interface, c'est la definition (sous la forme de noms de methodes)
d'un ensemble de fonctions que l'utilisateur pourra utiliser pour agir
sur les ressources. Un gestionnaire de ressources est un objet qui est
capable de creer des ressources (suivant une interface donnee, et a
partir de shadow ressource), et de definir l'implantation des methodes
declarees dans l'interface. Un gestionnaire de ressources, ca
ressemble a un point de montage Unix, et c'est en general responsable
d'un espace de nommage pour acceder aux ressources qu'il gere. Nous ne
traitons pas tout de suite du probleme de l'espace de nommage.

Ca doit paraitre un peu abstrait tout ca. L'idee, c'est qu'on peut
acceder a une meme shadow ressource de plusieurs manieres (plusieurs
interface pour y acceder). Pour chaque maniere qu'on choisit pour y
acceder (ie choix d'interface), on doit creer une
ressource. L'implantation des fonctions qui realisent ces acces sont
dependantes d'un gestionnaire de ressources, qui est invisible depuis
le cpl3.


En pratique, une interface, c'est juste une liste de noms de methodes :
  { "read", "write", "seek" }
Cette interface est maintenue par un objet central et unique pour tout
le systeme : l'interface registry. C'est un bete objet qui maintient
une correspondance entre ces listes, et un nom fourni lors de
l'enregistrement de l'interface. Par exemple, l'interface precedente
pourra etre enregistree avec le nom "file_if" aupres du registry.

Et une ressource, c'est 2 choses :
  - un lien vers le gestionnaire de ressource qui l'a cree
  - un lien vers l'implantation des methodes d'acces a la ressources
    qui se trouve dans le gestionnaire de ressource. Ces methodes
    d'acces dependent selon quelle interface la ressource a ete
    ouverte.

Le gestionnaire de ressource lui, maintient une liste de "vmt"
("virtual method table", bien que ce ne soit pas "virtual" du tout),
une "vmt" etant :
  - un lien vers une interface
  - un tableau qui renferme, associe a chacun des noms de methode de
    l'interface, une implantation de la methode. Par exemple, dans
    l'exemple precedent, le gestionnaire peut declarer une vmt
    associee a l'interafce "file_if" en fournissant les methodes
    my_read(), my_write(), ... qui implantent "read", "write"
    respectivement.
Le gestionnaire permet de creer et d'acceder a tout un paquet de
ressources, qu'on reference par leur "chemin" : de ce point de vue, le
gestionnaire est en quelque sorte un systeme de fichier monte. Nous ne
detaillons pas pour le moment cet aspect "espace de nommage global (au
sens : montage de systeme de fichiers" : on part sur un modele simple
pour la demo, sans espace de nommage/sans point de montage.

Un gestionnaire de ressource peut proposer plusieurs vmt pour
l'implantation de la meme interface. A l'inverse, il n'est pas oblige
d'implanter toutes les interfaces declarees dans le registry. C'est
lui qui doit s'occuper de la creation des ressources, en indiqueant
quelle VMT leur est associee, suivant l'interface qui est demandee au
open() => c'est sa methode build() qui fait ca. Cette methode est en
charge de detecter si la ressource demandee supporte l'interface
demandee. Par exemple, si on essaye d'ouvrir "/home/" avec l'interface
"file_if", ca marche pas : le gestionnaire sait que "/home/" est un
repertoire, et que par consequent on ne peut l'ouvrir qu'avec
l'interface "dir_if" (par exemple).

Voila pour les premiers details cote noyau. Pour l'utilisation de
cette chose, voila comment ca marche : on accede aux ressources avec 1
seul syscall, qui fait :
    syscall(id_resource, id_method, args...);
Ou id_resource est un identifiant (int) fourni par le noyau lors d'un
precedent open(), et id_method est aussi un identifiant (int).

En interne (ie cote cpl0), voila ce qu'on en fait :
  - recuperation du vrai objet ressource : le noyau utilise
    l'id_resource comme index dans le tableau des ressources utilisees
    par le processus (maintenu cote noyau => p_resource[]).
  - recuperation de la VMT associee a la ressource via le gestionnaire
    de ressources (=> identifie en fonction de l'espace de nommage,
    pour l'instant reduit a sa plus simple expression).
  - appel de la methode numero id_method dans la VMT
  - retour a l'utilisateur.

Moyennant quelques verifications d'usage
(id_interface/id_method/nb_args corrects).

Ca, c'etait le fonctionnement general. Mais comment demande-t'on une
ressource me direz-vous ? ie qu'est-ce qui remplace open() ?

La response est la suivante : le processus a un certain nombre de
ressources declarees par defaut, et toujours avec le meme
id_ressource. Comme stdin/stdout/stderr sous Unix. Ici, l'id_resource
0 correspond a la ressource "process" courante, ie au processus
courant. Cette ressource est toujours ouverte par le noyau au moment
du lancement du processus. Elle a ete ouverte avec l'interface
"process", et cette interface est tres simple :

DEFINE_INTERFACE(process,
                 IFE("open", // Nom de la methode
                     "int self, int id_interface, const char *path", //Type des arguments
                     2 /* Nombre d'arguments attendus */));

Autrement dit, la methode 0 de cette interface est le fameux open().

La deuxieme question qui vient est bien evidemment de savoir comment
identifier l'interface selon laquelle on va ouvrir la ressource (le
id_interface de open)...

La response est la suivante : tout comme la ressource "processus
courant" est toujours a la meme place dans la table des ressources du
processus, l'interface "process" est toujours a la meme place. De
quelle type de "place" je parle ? Eh bien de la place dans... la table
des ressources ;) 

Car une interface est aussi une ressource. Ce qui veut dire 1/ qu'on
peut ouvrir de nouvelles interfaces de la meme facon qu'on peut ouvrir
de nouvelles ressources : une partie de l'espace de nommage est
reservee a l'acces aux interfaces (les noms du type
"/interfaces/file_if" par exemple). 2/ Qu'on dispose d'une interface
speciale pour agir dessus (genh stub, lookup_method, ..., cf plus
loin). En corollaire, 3/ le registry est aussi un gestionnaire de
ressources (des ressources de type "interface").

Bref, en plus de la ressource "processus courant", un index special
est reserve pour faire figurer l'interface "process". A l'avenir, il y
aura d'autres interfaces ouvertes par defaut, en particulier celles
necessaires a la libc.


Voila les interfaces dont on dispose pour le moment :

_DEFINE_INTERFACE(resource, false,
                  IFE("next_capability", "int id_resource, char *dest, size_t len", 3),
                  IFE("check_capabilty", "int id_resource, char *if_name", 2));

DEFINE_INTERFACE(process,
                 IFE("open",
                     "int self, int id_interface, const char *path",
                     2));

_DEFINE_INTERFACE(meta, false,
                  IFE("lookup_method",
                      "int id_interface, const char *method",
                      2),
                  IFE("gen_stub_decl",
                      "int id_interface, char *dest, size_t len",
                      3),
                  IFE("gen_stub_impl",
                      "int id_interface, char *dest, size_t len",
                      3));

Quelques explications. L'interface resource, c'est pour pouvoir savoir
(par exemple) toutes les interfaces qui sont supportees par la
ressource /home/toto. Pour l'instant cette interface n'est pas
implantee completement. Toutes les shadow ressources (ie "/home/toto")
qui peuvent etre ouvertes dans le systeme *doivent* pouvoir etre
ouvertes *au moins* avec cette interface.

L'interface process, c'est pour open.

L'interface meta, c'est pour acceder aux ressources "interface". On
dispose ainsi de mecanismes qui renvoient le numero de methode
associee a une methode donnee (sous forme char*), et... de mecanismes
de generation de stubs (.c et .h).

Voila la liste des ressources (index dans la table des ressources du
processus) ouvertes par defaut :
#define RID_SELF       0 /* open.... */
#define IFID_PROCESS   1 /* Interface for open... */
#define IFID_INTERFACE 2 /* Interface for lookup method.... */
#define RID_STDIN      3 /* read, write... */
#define RID_STDOUT     4 /* read, write... */
#define RID_STDERR     5 /* read, write... */
(pas encore les RID_STDIN/OUT/ERR, mais c'est pour donner une idee)


Conclusion, pour appeler la methode check_capability("file_if") sur
/home/toto (ie demander si /home/toto peut etre accede avec les
methodes de l'interface [encore inexistante] file_if), on a 2
possibilites :

  - soit on appelle les stubs :
      res_if = process.open(IFID_INTERFACE, "/interfaces/resource");
      rid = process.open(res_if, "/home/toto");
      result = resource.check_capability(rid, "file_if");

  - soit on fait la totale a la main :
    res_if = syscall(RID_SELF, 0/*id open()*/,
                     IFID_INTERFACE, "/interfaces/resource");
    rid = syscall(RID_SELF, 0/*id open()*/, res_if, "/home/tot");
    result = syscall(rid, 1/*id check_capability*/, "file_if");


Si on voulait, on pourrait recuperer la methode "check_cpability()" en
utilisant la methode lookup_method() de l'interface "interface" :
    method_id = interface.lookup_method(res_if, "check_capability");
ou :
    method_id = syscall(IFID_INTERFACE, 0/*id lookup_method()*/,
                        res_if, "check_capability");
Avant de faire :
    result = syscall(rid, method_id, "file_if");


Bref, vous l'avez compris, soit on connait les identifiants de methode
(via les stubs cpl3), soit on peut les demander. La generation des
stubs s'occupe de generer des bouts de code a compiler en cpl3 et qui
fournissent directement le numero de methode a appeler, en meme temps
qu'ils donnent le protototype exact de la methode (pour que gcc
souligne les erreurs cote cpl3). Voici un exemple de stub cpl3 .h
automatiquement genere :

------------------
struct __kos_if_meta_s {
  int (* lookup_method) (int id_interface, const char *method);
  int (* gen_stub_decl) (int id_interface, char *dest, size_t len);
  int (* gen_stub_impl) (int id_interface, char *dest, size_t len);
}; /* struct __kos_if_meta_s */
extern struct __kos_if_meta_s __kos_if_meta_impl;
------------------

Et une partie du stub cpl3 .c genere :
------------------
/* Interface "meta", method 0:
   int lookup_method(int id_interface, const char *method) */
static int __kos_impl_meta__lookup_method (int arg1)
  { 
     int * top = & arg1;
     return call_kernel(arg1, 0, 1, top+1);
  }
...

struct __kos_if_meta_s __kos_if_meta_impl =
  {
    ...
  };
------------------

Donc, en cpl3, on ecrira :
  __kos_if_meta_impl.lookup_method();


Voila cote grands principes. Cote implantation de la demonstration,
c'est du C++ pour linux/x86. Le cpl3 est un thread, et le cpl0 en est
un autre. Le cpl0 est endormi jusqu'a temps que le cpl3 fasse un
syscall (synchro avec conditions). Le cpl3 s'endort alors, et attend
que le cpl0 lui redonne la main (idem). Le syscall est en realite une
fonction qui prend un nombre fixe de parametres :
    call_kernel(id_res, id_method, nb_args, int *args);
=> args est le tableau qui rassemble les nb_args arguments. Les stubs
se chargent de construire ce tableu en ne faisant aucune copie bidon
sur la pile => petit hack non portable a ce niveau. Idem cote noyau,
pour transformer le int *args en parametres pour l'appel des methodes
noyau (utilisation de alloca).

Pour l'instant le test cpl0 affiche juste le stub pour l'interface
"interface", puis essaye de provoquer un cas d'erreur pour tester le
mecanisme d'exception et d'errno ala kos. Ca devrait donner :

-----------------------
Registering interface resource
Registering interface meta
Registering interface process
Create user process...
Kernel Ready
User process started.
====== NO ERROR:
STUB decl user =
$$$struct __kos_if_meta_s {
  int (* lookup_method) (int id_interface, const char *method);
  int (* gen_stub_decl) (int id_interface, char *dest, size_t len);
  int (* gen_stub_impl) (int id_interface, char *dest, size_t len);
}; /* struct __kos_if_meta_s */
extern struct __kos_if_meta_s __kos_if_meta_impl;
$$$ (ret=308)
====== ERROR:
Throw exception KE_Resource_bad_If [cpl0/registry.cc:152]
SET ERRNO 2
ret=-1 (should be -1), errno=2
====== NO ERROR:
ret=3 (should be -1), errno=0

-----------------------
[... puis faut killer le truc parcequ'il attend a l'infini, c'est
     volontaire ]


Les trucs qui restent encore a faire : cf TODO.

Voila. En esperant que ca eclaire un peu tout le monde.

-- 
d2