[Top] | [Contents] | [Index] | [ ? ] |
Ce document devrait permettre une compréhension globale du Hurd. Pour toute remarque et suggestion, mailez-moi: racin at free dot fr
1. Presentation générale sur les OS | ||
2. Mach en détail | ||
3. Les translators |
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
Attention, certains de ces principes ne s'appliquent qu'aux "véritables" systèmes d'exploitations, qui différencient plusieurs utilisateurs et qui peuvent exécuter plusieurs programmes en même temps.
Dans ces systèmes, un utilisateur n'est pas libre de faire tout ce qu'il veut, et cela constitue une protection. Par exemple, un processus ne peut pas écrire dans l'espace d'adressage de son voisin, et si il le fait il plante et émet un SEGFAULT (sous les systèmes compatibles POSIX). Cela protège l'ensemble du système.
En particulier, pour un processus donné, les autres processus, la mémoire qui n'appartient pas à ce processus, les entrées sorties sont protégés. Aucun programme ne peut y accéder directement, il doit demander au noyau de le faire à sa place. Pour cela, Un processeur possède plusieurs modes d'exécutions (ring), dont uniquement 2 sont utilisés sous les systèmes POSIX: le mode utilisateur, et le mode noyau (aussi appelé mode superviseur). En mode noyau, un certain nombre d'instructions supplémentaires sont disponibles, comme les instructions d'entrée sortie.
Si il existait une instruction permettant à n'importe quel processus utilisateur de s'exécuter en mode noyau, cela poserait évidemment un gros problème de sécurité, puisque qu'un utilisateur malicieux pourrait outrepasser les mesures de sécurité mises en places par le système.
Un programme utilisateur a pourtant besoin d'effectuer des opérations qui ne sont permises que par le noyau, comme par exemple l'affichage d'un texte à l'écran (qui constitue une entrée sortie).
(On comprend bien que seul le noyau puisse faire cela, sinon pendant que notre programme s'exécute, un autre programme pourrait afficher plein de choses que nous ne voudrions pas).
Comment alors contrôler ce passage en mode noyau? La solution mise en
place s'appelle l'appel système: le noyau fournit un certain
nombres de fonctions que tout utilisateur peut appeler. On n'appelle
pas ces fonctions de manière classique , mais par une instruction
"spéciale", qui indique quel est le numéro de cet appel. Les
paramètres sont le plus souvent transmis par les registres (ce qui est
plus rapide). L'un des rôles de la librairie C est de fournir
une interface standard pour effectuer ces appels systèmes. Ainsi sous
GNU/Linux, un programme effectuant un appel à fork()
est lié a
la glibc, qui remplira les registres et paramètres nécessaires
à l'appel système Linux sys_fork()
.
Ces appels systèmes sont divers et variés. Sur un noyau UNIX standard, il y en a un certain nombre. Par exemple, Linux en fournit plus de 200. Minix en fournit 40.
Un noyau monolithique comme Linux fournit un grand nombre d'appels systèmes pour rendre les différents services aux programmes. Par exemple, à chaque modification du système de fichier (chdir, chown, etc) correspond un appel système.
Il existe une autre possibilité: c'est d'implémenter ces fonctions dans des processus systèmes tournant en mode utilisateur et appelés par le noyau. Le noyau joue ainsi le rôle de messager entre ces processus et les processus de l'utilisateur. Les appels systèmes du noyau se réduisent donc qu'à un ensemble de primitives de transmission de message. Un tel noyau s'appelle alors un micronoyau. L'exécution de ces fonctionnalités n'est plus faite au sein du processus utilisateur, mais du processus système appelé, qui peut même être situe sur une autre machine. C'est pour cela qu'un appel de ce type s'appelle un RPC (Remote Procedure Call).
Pour résumer, un noyau monolithique fournit beaucoup d'appels systèmes, des serveurs au dessus d'un autre noyau fournissent des RPC.
Les avantages de construction d'un système par micronoyau sont les suivants:
Le gros désavantage est que la transmission d'un message demande bien plus de temps qu'un appel système classique.
Le problème est que le design du Hurd fait qu'il ne se sert pas de toutes les fonctionnalités de Mach, comme le serveur de nom. Le Hurd a "juste" besoin de passages de paramètres extrêmement rapide. L'objectif des micronoyaux actuel est de réduire ce temps de transmissions d'un message. Pistachio est particulièrement bon dans ce domaine, c'est pourquoi le portage du Hurd sur l4 est si prometteur.
Pour le moment le Hurd tourne sur Mach, et c'est largement suffisant pour être heureux de développer dessus!
Le Hurd constitue donc un "troupeau" de processus systèmes s'exécutant en mode noyau, et qui fournit un certains nombres de fonctions le rendant compatible POSIX. Mais le Hurd est bien plus que cela. Chaque utilisateur est libre de remplacer la quasi-totalité de ces processus pour le remplacer par le sien. Chaque utilisateur peut ainsi littéralement remplacer une partie du noyau pour qu'elle lui convienne, et ce sans gêner les autres utilisateurs qui continueront à utiliser le processus système qu'il y a par défaut! Ceci permet d'étendre considérablement la liberté de l'utilisateur, qui peut ainsi par exemple "monter" (c'est-à-dire, attacher la racine d'un système de fichier à un répertoire, mais nous verrons que ce concept est bien étendu avec le Hurd) tout système de fichier qu'il souhaite en préservant la sécurité de chacun. C'est un défi particulièrement difficile à relever, et c'est pour cela que le Hurd semble si complexe.
Toutefois, la modularité du Hurd permet de comprendre certaines parties indépendemment des autres. C'est ce que je vais faire: vous expliquer chaque partie que vous allez comprendre indépendemment, et a la fin de votre lecture, vous aurez une vue d'ensemble du Hurd. Au début, certaines choses que nous verrons vous paraîtront même sans rapport (par exemple, entre l'implémentation d'un translator avec trivfs et l'envoi de 2 messages sous Mach.) N'hésitez pas a relire les parties plusieurs, fois, ainsi vous verrez mieux les liens entre elles.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
Mach est un micronoyau de première génération, développé à l'Université Cargenie-Mellon. Il fournit une base intéressante à d'autres OS. Le Hurd, Darwin (la base de Mac OS X) sont basés dessus.
Mach étant pour l'instant le seul micronoyau sur lequel tourne le Hurd, vous allez devoir apprendre quelques concepts de Mach pour comprendre le Hurd. Pour autant, il vaudra mieux par la suite utiliser des librairies de plus haut niveau pour développer des translators, pour les rendre plus portables. Mais il est important de comprendre les concepts de bas niveau.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
La fonction principale de Mach sous le Hurd étant la transmission de messages, intéressons-nous aux IPC sous Mach.
IPC peut être considéré comme un abus de langage, puisque les ressources d'exécution sous Mach sont appelées des tâches. Le système d'IPC (inter-process communication) permet donc aux différentes tâches de communiquer entre elles.
Les IPC sous Mach reposent sur trois concepts: celui de message, celui de port, et celui de droit sur un port (port right, ou port capability).
Les ports sont les moyens de communication entre les différentes
tâches. Les port rights sont les moyens d'accéder à ces ports.
Il y en a 3 sortes: les droits de réception (receive right), les
droits d'envoi (send rights) et les droits d'envoi unique (send once
right). Pour manipuler ces port rights, on dispose de
port name (type mach_port_t
). Lorsqu'une tâche crée un
port, elle obtient ainsi un port name sur un receive right vers ce
port. Ce receive right est unique: une seule tâche a la fois peut
recevoir des données en provenance d'un port. Les tâches peuvent se
fabriquer, copier ou transférer des port rights par les messages. Le
receive right peut être transféré, mais pas copié ni fabriqué.
Il existe aussi la notion de port set, qui permet à une tâche d'écouter plusieurs ports en même temps.
Les ports rights sont gérés par le noyau et sont entièrement sécurisés: aucune tâche ne peut se rajouter des droits. Les messages sont assurés de parvenir à leur destinataires, et dans l'ordre dans lequel ils ont été envoyés.
Pour plus d'info consulter le manuel info de GNU Mach, et le wiki http://hurd.gnufans.org.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
Pour illustrer ces propos je vais vous montrer comment envoyer des messages avec Mach. Si nous voulions écrire 2 programmes séparés, un client et un serveur, qui communiquent entre eux, il serait difficile pour chacun de ces programmes de trouver le port de l'autre, et donc de communiquer. Pour simplifier, nous allons donc écrire un programme qui va tout d'abord allouer un port puis se scinder en deux threads, ainsi chacun de ces threads pourra communiquer avec l'autre. Nous utiliserons les pthread plutôt que les cthreads pour faire cela, puisque les pthreads sont destinés à devenir la bibliothèque de référence en threading sous le Hurd (comme sous tout UNIX).
/* machipc.c Communication entre deux threads par envoi de messages * * mach. * * Copyright 2003-2004 Matthieu Lemerre * * Distributed under the terms of the GNU General Public License. * * This is distributed as is". No warranty is provided at all. */ #define _GNU_SOURCE 1 #include <stdio.h> #include <string.h> /*memcpy */ #include <unistd.h> /*sleep */ #include <pthread.h> #include <error.h> #include <mach/message.h> #include <mach/port.h> #include <hurd/hurd_types.h> #define HEADER_LEN sizeof(mach_msg_header_t) #define TYPE_LEN sizeof(mach_msg_type_t) #define BUFF_LEN 200 /*Ce que l'on veut vraiment envoyer*/ #define MSG_LEN HEADER_LEN+TYPE_LEN+BUFF_LEN mach_port_t port; void * fn_client (void *arg) { char nom[] = "thread client"; mach_msg_return_t msg_err; mach_msg_header_t header = { MACH_MSGH_BITS (MACH_MSG_TYPE_MAKE_SEND_ONCE, MACH_MSG_TYPE_MAKE_SEND_ONCE), /*mach_msg_bits */ BUFF_LEN, /*msg_size */ port, /*Le port ou envoyer */ 0, /*Le port local ou l'autre va renvoyer(non indispensable) */ 0, /*Le numero du message. Ne sert pas quand on envoie un message */ 0 /*Un champ qui ne sert a rien */ }; mach_msg_type_t type = { MACH_MSG_TYPE_STRING, /*type */ 8, /*Nombre de bits de chaque datum. */ sizeof (nom) + 1, /*Nombre de datums */ TRUE, /*On envoie un message in-line */ 0, /*C'est un message pas "long" */ 0, /*2 champs qui servent qu'aux out-line */ 0 }; //faire un mmap plutot mach_msg_header_t *buffer = malloc (MSG_LEN); char *ptr=buffer; memcpy (buffer, &header, HEADER_LEN); ptr+=HEADER_LEN; memcpy (ptr, &type,TYPE_LEN); ptr+=TYPE_LEN; strcpy (ptr, nom); msg_err = mach_msg (buffer, MACH_SEND_MSG, buffer->msgh_size, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL); /*Envoi du message */ free(buffer); pthread_exit(0); } int main (int argc, char **argv) { pthread_t client; mach_port_t current_task; void *resultat_client; kern_return_t err; const char nomclient[] = "nom_du_client"; mach_msg_header_t *buffer = malloc (BUFF_LEN); char *dup; mach_msg_return_t msg_err; mach_msg_header_t server_header = { 0, /*mach_msg_bits */ BUFF_LEN, /*msg_size */ 0, /*Le port ou envoyer */ 0, /*Le port local ou l'autre va renvoyer */ 0, /*Le numero du message. Ne sert pas quand on envoie un message */ 0 /*Un champ qui ne sert a rien */ }; /*Recuperation de la tâche courante*/ current_task = mach_task_self (); /*Creation d'un nouveau port dans port, avec un droit de reception pour la tâche courante*/ err = mach_port_allocate (current_task, MACH_PORT_RIGHT_RECEIVE, &port); /*On se rajoute le droit d'envoyer des messages vers ce port*/ err = mach_port_insert_right (current_task, port, port,MACH_MSG_TYPE_MAKE_SEND); /*Creation d'un client*/ client = pthread_create (&client, NULL, fn_client ,"hello"); /*Preparation de la reception*/ memcpy (buffer, &server_header, HEADER_LEN); /*Cet appel est bloquant*/ msg_err = mach_msg (buffer, MACH_RCV_MSG, 0, buffer->msgh_size, port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL); /*Reception du message */ printf ("Le client m'a envoye son nom. C'est %s\n", (char *) buffer + HEADER_LEN + TYPE_LEN); pthread_join(client,&resultat_client); return (0); } |
Le fonctionnement global du programme est le suivant: nous initialisons
le port dans port
, puis le programme se scinde en 2
threads. L'un écrit dans ce port et l'autre lit.
L'appel mach_task_self()
permet de récupérer la tâche du
programme. Nous en avons besoins pour indiquer au noyau dans quelle
tâche nous allons créer ce port (mach_port_allocate()
). Le port
étant créé, port
est un port name, un moyen d'accéder à
un port en réception. Pour pouvoir envoyer des messages dans ce port,
il faut insérer un droit d'envoi dedans
(mach_port_insert_right()
).
Enfin on se scinde (pthread_create()
), puis on prépare la
réception. La transmission d'un message sous Mach se passe par
l'intermédiaire de la fonction mach_msg()
.
Un message a la structure suivante:
+----------------------------------------- |Header|Type1|Contenu1|Type2|Contenu2|...| +----------------------------------------- |
Enfin, le thread serveur attend le client (pthread_join()
) et
le programme quitte.
Dans un vrai programme, les droits d'envois sont transmis par les messages.
Pour plus d'info voir ftp://ftp.cs.cmu.edu/afs/cs/project/mach/public/doc/osf
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
Comme vous venez de le voir, faire passer des messages n'est pas une chose triviale. Dans le Hurd, nous utilisons les messages pour envoyer nos RPC. Pour nous faciliter la vie, il existe un outil appelle Mig, le Mach Interface Generator. Son seul et unique but est de simplifier l'écriture d'interfaces de serveur Mach et de rendre les choses bien plus lisibles.
Voila la manière de définir une interface:
/* hello.defs Definition de l'interface d'un serveur de bonjours * * Copyright 2003-2004 Matthieu Lemerre * * Distributed under the terms of the GNU General Public License. * * This is distributed äs is". No warranty is provided at all. */ #include <mach/std_types.defs> type string_t = c_string[1024]; /*un type Hurd standard*/ import "type.h"; subsystem hello 53400; /*534 is arbitrary*/ serverprefix S_; /*These two are necessary because we use the 2 functions in the same program*/ serverdemux demux; /*Pour que Mig genere la fonction de demultiplexage du message*/ /*Le premier argument doit etre un "request port"*/ /*Il est utilise lors du transfert des messages, mais aussi communique au serveur*/ routine hello_hello ( server: mach_port_t; name: string_t; out greeting: string_t ); |
Une fois ce fichier créé, on le passe à la moulinette avec Mig: `mig hello.defs'
Cela produit les fichiers `helloServer.c', `helloUser.c' et `hello.h'.
Mig est le Mach interface Generator, c'est a dire le programme qui permet de générer du code pour que la communication par RPC soit un appel de fonction pour le programmeur.
Dans cet exemple, hello est une routine, c'est à dire un RPC synchrone, un message aller-retour entre un client et un serveur.
Lorsque le client a connaissance du port du serveur sur lequel envoyer
sa RPC, il suffit d'écrire: hello_hello(port,"racin",greeting)
avec port
le port du serveur et greeting
un buffer
alloue dans lequel on va recevoir la réponse, pour envoyer un message
au serveur sans passer nous-même par la puissante mais complexe
commande mach_msg
directement. hello_hello
est une
fonction générée par Mig et placée dans `helloUser.c',
donc lors de la compilation il suffit de lier notre client au fichier
objet et de faire un #include "hello.h"
pour pouvoir utiliser
cette fonction. `hello.h' contient la définition de hello_hello
.
Essayons de comprendre un peu chaque ligne: (pour plus de détails sur Mig voir le server writer's guide, chapitre 3
type string_t = c_string[1024];
Définit le type string_t
subsystem hello 53400;
Définit deux choses: le nom associé au
server, qui sert de préfixe au noms des fichiers crées; et un
message-base-id, qui permet de renseigner le champ msgh_id lors
de l'envoi de messages. Ce champ permet de savoir quelle est
l'interface demandée au serveur. (En effet, la plupart des serveurs
peuvent répondre a plusieurs RPC, il faut bien savoir les distinguer).
C'est pour cela qu'il existe une directive skip
pour les
interfaces dans lesquelles on ne définit pas toutes les routines (dans
le Hurd, beaucoup d'interfaces sont séparées en interfaces write et
read)
serverprefix S_;
Lorsqu'on veut faire communiquer des programmes par RPC avec
Mig, les serveurs doivent présenter une fonction pour
renvoyer les données. serverprefix est le préfixe rajoute au nom de la
routine pour déterminer le nom de la fonction. Dans notre exemple le
prototype de cette fonction est :
kern_return_t S_hello_hello(mach_port_t port, string_t name, string_t greeting)
Mig s'occupe d'appeler cette fonction lors de la réception d'un
message. Il existe aussi une commande userprefix, et par défaut le
userprefix
et le serverprefix
sont des chaînes vides.
Dans le Hurd on utilise `S_' comme serverprefix
en
général, et les routines ont des noms composés selon le schéma
`nomsubsystem_nomroutine'
serverdemux hello_server;
Cette directive demande a Mig d'appeler la fonction qui
demultiplexe les messages vers le server
server_demux
. nomsubsystem_server
est le nom par défaut.
Cette fonction décompose les RPC vers le serveur grâce aux messages
ids, et appelle la bonne fonction.
/*Le premier argument doit être un "request port"*/ /*Il est utilisé lors du transfert des messages, mais aussi communique au serveur*/
routine hello ( server: mach_port_t; name: string_t; out greeting: string_t ); |
Ceci définit une routine, c'est-à-dire une RPC synchrone. Le
client envoie un message qui est traité par le serveur. Ce dernier
renvoie à son tour un message par un port alloué automatiquement par
la fonction créée par Mig. Pour les RPC asynchrones, on utilise
simpleroutine. Le premier argument d'une RPC est toujours un
port name, correspondant à un port sur lequel écoute le
serveur.(1) La syntaxe de passage des arguments
est la suivante:
specification nomvar: type [IPC flags];
specification
est souvent soit in
(implicite quand
specification
n'est pas définit) soit out
, soit inout
.
in
désigne un paramètre que le client fournit au serveur, et
out
l'inverse, et inout
pour les deux.
Pour s'échanger des informations, le client alloue une zone mémoire qu'il "donne" au serveur. Ce dernier peut modifier cette zone mémoire et ainsi retourner une réponse au client. Si la taille de cette zone mémoire n'est pas assez importante, le serveur peut allouer une nouvelle zone pour y mettre les informations qu'il souhaite. Ces pages sont ensuite réattachées à l'espace d'adressage du client par l'envoi du message de retour. (C'est ce qu'on appelle des messages out-of-line).
Il existe divers flags. Le flag deallocate
sert à retirer de la
tâche appelante le port right qu'elle avait ou à déallouer la zone
mémoire transférée dans le cas des messages out-of-line.
Voila ce que devient notre application hello
lorsqu'on utilise
Mig:
//Nécessaire pour compiler #define _GNU_SOURCE 1 #include <pthread.h> #include <stdio.h> //printf #include <stdlib.h> //exit #include <unistd.h> //sleep #include <string.h> #include <mach.h> #include <mach/message.h> #include <mach/notify.h> #include <mach_error.h> #include "hello.h" static mach_port_t port; #define WELCOME_MESSAGE "Bonjour a toi, " #define MAX_MSG_SIZE 5120 boolean_t hello_server(mach_msg_header_t *InHeadP, mach_msg_header_t *OutHeadP); void* client(void *param) {//l'un des threads appelle cette routine char *greeting; greeting = malloc(100); hello_hello(port,"Matthieu",greeting); printf("%s\n",greeting); pthread_exit(0); } int main() { pthread_t client_thread; void *resultat_client; //On alloue le port pour le serveur mach_port_allocate(mach_task_self(),MACH_PORT_RIGHT_RECEIVE,&port); //On s'auto-ajoute le droit de s'envoyer des messages mach_port_insert_right(mach_task_self(),port,port,MACH_MSG_TYPE_MAKE_SEND);//normalement c'est pas la methode pour recevoir des send_rights. //Creation du thread client pthread_create (&client_thread, NULL, client ,"hello"); //le thread principal est utilise comme serveur mach_msg_server(demux,MAX_MSG_SIZE,port); //On attend que le client s'arrete nous rende un resultat pthread_join(client_thread,&resultat_client); exit(0); } kern_return_t S_hello_hello(mach_port_t port, string_t name, string_t greeting) { //greeting est fournit. Normalement le client devrait fournir la longueur de son buffer. printf("Le nom du client est: %s\n",name); strcpy(greeting,WELCOME_MESSAGE); strcat(greeting,name); return KERN_SUCCESS; } |
Il suffit de compiler et tout devrait marcher: `gcc server.c helloServer.c helloUser.c -D_GNU_SOURCE -lthreads -Wall'
La grande différence entre les deux exemples est donc que celui-ci utilise deux ports pour l'envoi et la réception de messages, tandis que le précédent utilisait le même port (pour simplifier et ne pas trop augmenter la taille du code).
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
Si nous nous adoptons une vue plus globale, nous pouvons assimiler les serveurs à des objets, les interfaces étant la déclaration des méthodes, les RPC un passage de message d'un objet à un autre, et les ports étant des références vers un autre objet. Cette vision est importante, et nous aidera à comprendre le fonctionnement de L4/Hurd.
Désormais vous devriez être capable de lire toutes les différentes interfaces du Hurd. Celles-ci sont définies dans l'arbre des sources du Hurd dans le sous répertoire `hurd'.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
Dans tous nos exemples, les tâches étaient constituées de 2 threads qui communiquait par un port créé spécifiquement pour ça. Dans le cas général, on ne peut pas procéder comme ça. Quelles sont les méthodes possibles pour obtenir un port d'un serveur? Ce qui est généralement fait sous Mach est d'utiliser un serveur pour cela appelé serveur de nom, et lorsqu'on cherche un port vers un serveur on le demande au serveur de nom. Ce n'est pas la solution retenue par le Hurd. Sous le Hurd, on utilise le système de fichier comme moyen de trouver les ports. Un serveur doit donc s'attacher à un fichier, et lorsqu'on veut communiquer avec il suffit de demander le fichier en question au serveur de système de fichier parent pour obtenir un port vers lui. C'est ce qu'on appelle un translator.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
Comme l'a dit Thomas Bushnell: "If you want to be part of the filesystem, you must implement the fsys_* calls. You must also implement the file_* and io_* calls for the node you are inserting into the filesystem."
Allez voir le Hurd hacking guide pour un exemple d'utilisation de trivfs.
Voila un exemple d'utilisation de netfs: un Fibonnacci translator
[Top] | [Contents] | [Index] | [ ? ] |
Pour savoir comment le client peut obtenir ce port, voir le chapitre suivant
[Top] | [Contents] | [Index] | [ ? ] |
1. Presentation générale sur les OS
2. Mach en détail
2.1 Qu'est ce que Mach?3. Les translators
2.2 Les IPC sous Mach
2.3 Exemple: envoi de messages avec Mach
2.4 Envoi de RPC en utilisant Mig
2.5 Vision objet du Hurd.
2.6 Connaître les ports d'un serveur
[Top] | [Contents] | [Index] | [ ? ] |
1. Presentation générale sur les OS
2. Mach en détail
3. Les translators
[Top] | [Contents] | [Index] | [ ? ] |
Button | Name | Go to | From 1.2.3 go to |
---|---|---|---|
[ < ] | Back | previous section in reading order | 1.2.2 |
[ > ] | Forward | next section in reading order | 1.2.4 |
[ << ] | FastBack | beginning of this chapter or previous chapter | 1 |
[ Up ] | Up | up section | 1.2 |
[ >> ] | FastForward | next chapter | 2 |
[Top] | Top | cover (top) of document | |
[Contents] | Contents | table of contents | |
[Index] | Index | concept index | |
[ ? ] | About | this page |
where the Example assumes that the current position is at Subsubsection One-Two-Three of a document of the following structure: