virli/tutorial/4/timens.md

263 lines
6.4 KiB
Markdown

Le *namespace* `time` {#time-ns}
---------------------
### Introduction
L'espace de noms `time`, introduit dans Linux 5.6, permet de virtualiser les
horloges `CLOCK_MONOTONIC` et `CLOCK_BOOTTIME` pour des processus. Cela est
particulièrement utile pour la migration de conteneurs ou pour tester des
applications sensibles au temps.
::::: {.warning}
Contrairement à ce que son nom pourrait suggérer, le *namespace* `time` ne
permet **pas** de virtualiser l'heure système (l'horloge temps réel). Il ne
virtualise que les compteurs monotones.
:::::
### Utilisation pratique
Voyons comment utiliser le *namespace* `time` pour modifier le temps
d'exécution perçu par un processus.
#### Lire les horloges monotones\
Commençons par observer les valeurs actuelles de nos horloges :
<div lang="en-US">
```bash
42sh$ cat /proc/uptime
123456.78 987654.32
```
</div>
Le premier nombre représente le temps depuis le démarrage du système (équivalent
à `CLOCK_BOOTTIME`), et le second le temps CPU cumulé en mode idle.
On peut également utiliser un petit programme C pour afficher les horloges :
<div lang="en-US">
```c
#include <stdio.h>
#include <time.h>
int main(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
printf("CLOCK_MONOTONIC: %ld.%09ld\n", ts.tv_sec, ts.tv_nsec);
clock_gettime(CLOCK_BOOTTIME, &ts);
printf("CLOCK_BOOTTIME: %ld.%09ld\n", ts.tv_sec, ts.tv_nsec);
return 0;
}
```
</div>
::::: {.code}
Pour compiler ce programme :
<div lang="en-US">
```bash
42sh$ gcc -o show_clocks show_clocks.c
```
</div>
:::::
#### Créer un *namespace* `time` avec décalage\
Pour créer un nouveau *namespace* `time`, il faut d'abord le dissocier puis
configurer les décalages (offsets) avant de lancer un processus enfant.
Le fichier `/proc/<PID>/timens_offsets` permet de configurer les décalages pour
les horloges. Le format est le suivant :
<div lang="en-US">
```
<clock-id> <offset-secs> <offset-nanosecs>
```
</div>
`<clock-id>` peut être :
- `monotonic` pour `CLOCK_MONOTONIC`
- `boottime` pour `CLOCK_BOOTTIME`
Voici un script shell pour créer un conteneur avec un temps décalé de 10 jours
dans le futur :
<div lang="en-US">
```bash
42sh# unshare --time /bin/bash
intimens# echo "monotonic 864000 0" > /proc/self/timens_offsets
intimens# echo "boottime 864000 0" > /proc/self/timens_offsets
intimens# exec /bin/bash
intimens-child# ./show_clocks
CLOCK_MONOTONIC: 987654.123456789
CLOCK_BOOTTIME: 987654.123456789
```
</div>
::::: {.warning}
**Important :** Les décalages doivent être configurés **avant** de créer le
premier processus enfant. Une fois qu'un processus enfant existe dans le
*namespace*, les offsets ne peuvent plus être modifiés. C'est pourquoi nous
utilisons `exec` pour remplacer le shell actuel.
:::::
#### Exemple en C\
Voici un exemple complet en C pour créer un *namespace* `time` avec un décalage :
<div lang="en-US">
```c
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
static int child_func(void *arg)
{
struct timespec ts;
// Attendre que le parent configure les offsets
sleep(1);
clock_gettime(CLOCK_MONOTONIC, &ts);
printf("Child - CLOCK_MONOTONIC: %ld.%09ld\n",
ts.tv_sec, ts.tv_nsec);
clock_gettime(CLOCK_BOOTTIME, &ts);
printf("Child - CLOCK_BOOTTIME: %ld.%09ld\n",
ts.tv_sec, ts.tv_nsec);
return 0;
}
#define STACK_SIZE (1024 * 1024)
static char child_stack[STACK_SIZE];
int main(void)
{
pid_t pid;
int fd;
char path[256];
struct timespec ts;
// Afficher les horloges du parent
clock_gettime(CLOCK_MONOTONIC, &ts);
printf("Parent - CLOCK_MONOTONIC: %ld.%09ld\n",
ts.tv_sec, ts.tv_nsec);
// Se dissocier du namespace time
if (unshare(CLONE_NEWTIME) == -1) {
perror("unshare");
exit(EXIT_FAILURE);
}
// Configurer les offsets (décalage de 1000 secondes)
fd = open("/proc/self/timens_offsets", O_WRONLY);
if (fd == -1) {
perror("open timens_offsets");
exit(EXIT_FAILURE);
}
if (write(fd, "monotonic 1000 0\n", 17) != 17) {
perror("write monotonic offset");
exit(EXIT_FAILURE);
}
if (write(fd, "boottime 1000 0\n", 16) != 16) {
perror("write boottime offset");
exit(EXIT_FAILURE);
}
close(fd);
// Créer un processus enfant qui verra les nouvelles horloges
pid = clone(child_func, child_stack + STACK_SIZE,
SIGCHLD, NULL);
if (pid == -1) {
perror("clone");
exit(EXIT_FAILURE);
}
waitpid(pid, NULL, 0);
return 0;
}
```
</div>
::::: {.code}
Pour compiler :
<div lang="en-US">
```bash
42sh$ gcc -o time_ns_demo time_ns_demo.c
42sh$ ./time_ns_demo
Parent - CLOCK_MONOTONIC: 123456.789012345
Child - CLOCK_MONOTONIC: 124456.789012345
Child - CLOCK_BOOTTIME: 124456.789012345
```
</div>
On voit bien le décalage de 1000 secondes entre le parent et l'enfant.
:::::
### Cas d'usage pratiques
#### Migration de conteneurs\
Lors de la migration d'un conteneur d'une machine à une autre, les processus
peuvent avoir des timers ou des alarmes basées sur `CLOCK_MONOTONIC`. Si on ne
virtualise pas ces horloges, tous ces timers seraient incorrects après la
migration.
Le *namespace* `time` permet de restaurer l'état des horloges monotones lors
de la restauration d'un conteneur.
#### Tests et simulation\
Pour tester des applications qui utilisent des timeouts ou des mécanismes de
retry avec backoff exponentiel, il peut être utile de virtualiser le temps pour
accélérer les tests sans modifier le code de l'application.
### Limitations
- Seules les horloges `CLOCK_MONOTONIC` et `CLOCK_BOOTTIME` peuvent être
virtualisées
- L'heure système (`CLOCK_REALTIME`) reste identique pour tous les processus
- Les offsets ne peuvent être configurés qu'une seule fois, avant la création
du premier processus enfant
- Les décalages ne peuvent pas être négatifs (on ne peut pas remonter dans le
temps)
### Aller plus loin {-}
Pour plus d'informations sur le *namespace* `time`, consultez :
- `time_namespaces(7)` - page de manuel
- [Time Namespace support](https://lwn.net/Articles/766089/) : <https://lwn.net/Articles/766089/>
- [Checkpoint/Restore and Time Namespace](https://lwn.net/Articles/801652/) : <https://lwn.net/Articles/801652/>