Linux - Comprendre la préemption noyau

Liran B.H

Traducteur

Chrtophe

Dans ce tutoriel, je vais couvrir les différents modèles de préemption et comment chacun affecte le comportement en espace utilisateur et noyau.

Commentez

- Titre : Linux - Comprendre la préemption noyau

- Auteur : Liran B.H

- Traducteur : Chrtophe

- Parution : 5 octobre 2018

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2018 Liran B.H. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- 1. Introduction

- 2. Pas de préemption forcée

- 3. Noyau préemptible

- 4. Préemption noyau volontaire

- 5. Préemption pleinement temps réel

- 6. Notes de la rédaction

1 - Introduction

Lors de la configuration d'un noyau Linux, vous pouvez positionner certains paramètres qui affectent le comportement du système. Vous pouvez jouer sur les différentes priorités, les classes de planification, et les modèles de préemption. Il est très important de choisir les bons paramètres et de les comprendre.

Dans ce tutoriel, je vais couvrir les différents modèles de préemption et comment chacun affecte le comportement utilisateur et noyau.

Si vous configurez le noyau (en utilisant make menuconfig), vous pouvez trouver habituellement dans le sous-menu des fonctionnalités noyau l'option modèle préemptif :

Note de la rédaction :

L'option se situe dans Processor type and features->Preemption model.

Pour comprendre chaque option, prenons un exemple :

- nous avons deux threads, l’un avec une priorité temps réel haute(50) et l'autre avec une priorité temps réel basse (30) ;

- le thread haute priorité est mis en veille pour 3 secondes ;

- le thread basse priorité utilise le processeur pour les calculs de l'espace utilisateur ;

- après 3 secondes, le thread haute priorité se réveille.

Ce cas est simple et raisonnable, mais que se passe-t-il si le thread de basse priorité appelle un code noyau quand celui de haute priorité dort ? Cela dépend de la configuration sous-jacente.

2 - Pas de préemption forcée

La commutation de contexte est faite seulement lors du retour du noyau. Prenons un exemple :

- nous avons deux threads, l’un avec une priorité temps réel haute(50), et l'autre avec une priorité temps réel basse (30) ;

- le thread haute priorité est mis en veille pour 3 secondes ;

- le thread basse priorité appelle un code noyau durant 5 secondes ;

- après 5 secondes, le thread basse priorité sort de l'espace noyau ;

- le thread de haute priorité va se réveiller (avec 2 secondes de retard).

Voyons le code – un simple code noyau de pilote de type caractère :

#include <asm/uaccess.h>
#include <linux/fs.h>
#include <linux/gfp.h>
#include <linux/cdev.h>
#include <linux/sched.h>
#include <linux/kdev_t.h>
#include <linux/delay.h>
#include <linux/ioctl.h>
#include <linux/slab.h>
#include <linux/mempool.h>
#include <linux/mm.h>
#include <asm/io.h>

static dev_t my_dev;
static struct cdev *my_cdev;

// callback for read system call on the device
static ssize_t my_read(struct file *file, char __user *buf,size_t count,loff_t *ppos)
{
int len=5;
if(*ppos > 0)
{
return 0;
}
mdelay(5000); // busy-wait for 5 seconds
if (copy_to_user(buf , "hello" , len)) {
return -EFAULT;
} else {
*ppos +=len;
return len;
}
}

static struct file_operations my_fops =
{
.owner = THIS_MODULE,
.read = my_read,
};

static int hello_init (void)
{

my_dev = MKDEV(400,0);
register_chrdev_region(my_dev,1,"demo");

my_cdev=cdev_alloc();
if(!my_cdev)
{
printk (KERN_INFO "cdev alloc error.\n");
return -1;
}
my_cdev->ops = &my_fops;
my_cdev->owner = THIS_MODULE;

if(cdev_add(my_cdev,my_dev,1))
{
printk (KERN_INFO "cdev add error.\n");
return -1;
}

return 0;

}

static void
hello_cleanup (void)
{
cdev_del(my_cdev);
unregister_chrdev_region(my_dev, 1);
}

module_init (hello_init);
module_exit (hello_cleanup);
MODULE_LICENSE("GPL");

La lecture est retardée de 5 secondes (l'attente est une boucle d'occupation) et retourne quelques données.

Le code en espace utilisateur :

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

void *hi_prio(void *p)
{
printf("thread1 start time=%ld\n",time(NULL));
sleep(3);
printf("thread1 stop time=%ld\n",time(NULL));
return NULL;
}

void *low_prio(void *p)
{
char buf[20];
sleep(1);
int fd=open("/dev/demo",O_RDWR);  // #mknod /dev/demo c 400 0
puts("thread2 start");
read(fd,buf,20);
puts("thread2 stop");
return NULL;
}

int main()
{
pthread_t t1,t2,t3;

pthread_attr_t attr;

struct sched_param param;

pthread_attr_init(&attr);
pthread_attr_setschedpolicy(&attr, SCHED_RR);

param.sched_priority = 50;
pthread_attr_setschedparam(&attr, &param);

pthread_create(&t1,&attr,hi_prio,NULL);

param.sched_priority = 30;
pthread_attr_setschedparam(&attr, &param);

pthread_create(&t2,&attr,low_prio,NULL);
sleep(10);
puts("end test");
return 0;
}

- Le thread de haute priorité se met en sommeil 3 secondes.

- Le thread de basse priorité est en sommeil pour une seconde puis appelle ensuite le noyau.

- Le thread haute priorité est réveillé après 6 secondes :

# insmod demo.ko
# ./app
thread1 start time=182
thread2 start
thread1 stop time=188
thread2 stop
end test

3 - Noyau préemptible

Dans cette configuration, la commutation de contexte se fait dans les temps dans le noyau également, cela signifiant que si vous lancez le test ci-dessus, nous verrons le thread haute priorité se réveiller après 3 secondes.

Cela signifie qu'avec cette option, le système va effectuer plus de commutations de contexte par seconde, mais qu'il sera plus « temps réel ». Sur les systèmes embarqués ayant des logiciels avec contrainte de temps réel, il est de meilleure pratique d'utiliser cette option, mais dans un système serveur qui travaille habituellement de façon asynchrone, la première option est la meilleure. Moins de commutation de contexte, moins de consommation de CPU.

La sortie :

# insmod ./demo.ko
#./app
thread1 start time=234
thread2 start
thread1 stop time=237
thread2 stop
end test

4 - Préemption noyau volontaire

Dans cette configuration, le système travaille comme une « préemption non forcée ». Mais si le développeur noyau écrit un code complexe, il est chargé de vérifier de temps en temps si une nouvelle planification est nécessaire. Il peut faire ceci avec la fonction might_resched().

Donc, dans cet exemple, si nous voulons ajouter ce « checkpoint », nous devons changer le code :

// callback for read system call on the device
static ssize_t my_read(struct file *file, char __user *buf,size_t count,loff_t *ppos)
{
int len=5;
if(*ppos > 0)
{
return 0;
}
mdelay(4000); // busy-wait for 4 seconds
might_resched();
mdelay(3000);  // busy wait for 3 seconds
if (copy_to_user(buf , "hello" , len)) {
return -EFAULT;
} else {
*ppos +=len;
return len;
}
}

Si nous commentons la ligne might_resched(), il y aura un retard de 7 secondes, par l'ajout d'un appel à cond_resched qui vérifiera et fera une commutation de contexte si un autre thread de haute priorité est réveillé. Il sera appelé après 5 secondes (1 seconde avant l'appel et 4 secondes dans le noyau).

Sortie :

# insmod ./demo.ko
#./app
thread1 start time=320
thread2 start
thread1 stop time=325
thread2 stop
end test

5 - Préemption pleinement temps réel

Si vous appliquez le patch RT, vous aurez un noyau assidûment temps réel. Cela signifie que n'importe quel code peut en bloquer un autre. Si vous démarrez le code d'une routine d'interruption et que quelque chose de plus urgent a besoin d'être géré, cela bloquera le code de la sous-routine d'interruption. Le patch change les choses suivantes :

- convertit les interruptions matérielles en threads avec une priorité RT de 50 ;

- convertit les interruptions logicielles en threads avec une priorité RT de 49 ;

- convertit tous les spinlocks en mutex ;

- configure et utilise les timers haute résolution ;

- applique quelques autres fonctionnalités mineures.

Après l'application du patch, vous pouvez voir deux nouvelles options dans le menu de configuration de la compilation du noyau :

Note de la rédaction : L'ordre des options peut changer selon votre langue ou version de noyau.

L'option « Premptible Kernel (Basic RT) » est pour le débogage (voir la documentation). Pour mettre en œuvre tous les changements indiqués ci-dessus, vous devez sélectionner la dernière option « Fully Preemptible Kernel ».

Maintenant, si vous créez un thread avec une priorité RT plus grande que 50, il bloquera les interruptions. Notez que dans cette configuration, le système a plus de tâches et effectuera plus de changements de contexte par seconde. C.-à-d. : le processeur passera plus de temps à alterner les tâches mais nous pouvons atteindre n'importe quelle limite requise (1 ms ou plus).

6 - Notes de la rédaction

Ce tutoriel est une traduction de « Understanding Linux Kernel Preemption » de Devarea.

Traduction : Chrtophe.

Relecture orthographique : escartefigue et f-leb.