Archives de la catégorie ‘Linux’

Modifier facilement la fréquence processeur

Embarqué, Linux, Microprocesseur, Temps-réel | Publié par cpb
avr 30 2012

Linux offre une possibilité intéressante : celle de configurer assez finement la vitesse du processeur depuis la ligne de commande. Il permet d’agir indépendamment (pour autant que l’architecture matérielle le permette) sur les différents coeurs des processeurs. On m’a demandé récemment une petite illustration pratique de ces possibilités : en voici un résumé.

Configurer la vitesse CPU avec /sys

La première opération nécessaire est de connaître la liste des fréquences supportées par un CPU. Cette liste est accessible grâce au pseudo système de fichier /sys.

# ls /sys/devices/system/cpu/
cpu0  cpu1  cpu2  cpu3  cpufreq  cpuidle  kernel_max  offline  online  possible  present  probe  release  sched_mc_power_savings
# cat /sys/devices/system/cpu/online 
0-3
# cat /sys/devices/system/cpu/possible 
0-7
# cat /sys/devices/system/cpu/present 
0-3
#

Ici, le noyau peut gérer sept CPU (paramètre possible), mais il n’y en a que quatre connectés (present). Ils sont tous actifs (online). Choisissons, arbitrairement, le CPU numéro 1 et vérifions ses fréquences de fonctionnement.

# ls /sys/devices/system/cpu/cpu1/
cache  cpufreq  crash_notes  online  thermal_throttle  topology
# ls /sys/devices/system/cpu/cpu1/cpufreq/
affected_cpus               related_cpus                   scaling_max_freq
bios_limit                  scaling_available_frequencies  scaling_min_freq
cpuinfo_cur_freq            scaling_available_governors    scaling_setspeed
cpuinfo_max_freq            scaling_cur_freq               stats
cpuinfo_min_freq            scaling_driver
cpuinfo_transition_latency  scaling_governor
# cat /sys/devices/system/cpu/cpu1/cpufreq/scaling_available_frequencies 
2400000 2133000 1867000 1600000
# cat /sys/devices/system/cpu/cpu1/cpufreq/scaling_cur_freq 
2400000
#

Le CPU accepte quatre fréquences, il est actuellement configuré avec la plus rapide : 2,4GHz. On peut la modifier ainsi.

# echo userspace > /sys/devices/system/cpu/cpu1/cpufreq/scaling_governor 
# echo 1867000 > /sys/devices/system/cpu/cpu1/cpufreq/scaling_setspeed 
# cat /sys/devices/system/cpu/cpu1/cpufreq/scaling_cur_freq 
1867000
#

Mesurer les effets de la vitesse CPU

Nous expliquerons la première ligne de l’exemple ci-dessus dans le prochain paragraphe.
Pour voir les effets de la configuration précédente, on peut utiliser un petit programme qui incrémente un compteur pendant une durée donnée (cinq secondes ici).

compte-iterations.c :
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>

int main(void)
{
	struct timeval tv;
	struct timeval tv_debut;
	long long int compteur;

	// Attendre le debut d'une nouvelle seconde
	gettimeofday(& tv_debut, NULL);
	do {
		gettimeofday(& tv, NULL);
	} while (tv.tv_sec == tv_debut.tv_sec);

	// Compter pendant cinq secondes
	compteur = 0;
	tv_debut = tv;
	do {
		compteur ++;
		gettimeofday(& tv, NULL);
	} while (tv.tv_sec < (tv_debut.tv_sec + 5));

	printf("Resultats : %lld\n", compteur);
	return EXIT_SUCCESS;
}

Essayons de l’exécuter avec différentes fréquences.

# echo 1600000 > /sys/devices/system/cpu/cpu1/cpufreq/scaling_setspeed 
# taskset -pc 1 $$
pid 9778's current affinity list: 0-3
pid 9778's new affinity list: 1
# ./compte-iterations 
Resultats : 17263876
# ./compte-iterations 
Resultats : 16733090
# ./compte-iterations 
Resultats : 16810727
# echo 1867000 > /sys/devices/system/cpu/cpu1/cpufreq/scaling_setspeed 
# ./compte-iterations 
Resultats : 19926359
# ./compte-iterations 
Resultats : 19868890
# ./compte-iterations 
Resultats : 19681040
# echo 2133000 > /sys/devices/system/cpu/cpu1/cpufreq/scaling_setspeed 
# ./compte-iterations 
Resultats : 22517877
# ./compte-iterations 
Resultats : 23028619
# ./compte-iterations 
Resultats : 22310298
# echo 2400000 > /sys/devices/system/cpu/cpu1/cpufreq/scaling_setspeed 
# ./compte-iterations 
Resultats : 25317042
# ./compte-iterations 
Resultats : 25806554
# ./compte-iterations 
Resultats : 25916774
#

Nous voyons bien que le nombre d’itérations fluctue en fonction de la fréquence configurée.

Configurer le comportement du CPU

Plutôt qu’agir directement sur la fréquence, ce qui nécessite de lire la valeur et d’inscrire celle choisie, le noyau nous propose des comportements, des heuristiques, qu’il nomme governors, qui vont assurer la modification de la fréquence CPU afin de l’ajuster aux besoins de l’utilisateur.
On trouvera des explications plus détaillées sur les governors dans cet article. Il existe (suivant la configuration du noyau à la compilation) jusqu’à cinq governors, dont les noms sont visibles ainsi.

# cat /sys/devices/system/cpu/cpu1/cpufreq/scaling_available_governors 
conservative ondemand userspace powersave performance
#

Leurs comportements :

  • powersave : économiser la batterie (d’un ordinateur portable) en utilisant la fréquence la plus faible possible.
  • performance : optimiser la vitesse de traitement en adoptant la fréquence disponible la plus élevée.
  • ondemand : faire varier la fréquence en fonction de la charge système afin d’optimiser la vitesse de traitement lorsqu’il y a une forte demande, tout en économisant la batterie lorsqu’il y a peu de tâches en cours.
  • conservative : adopter le même comportement que ondemand, en évitant les modifications trop fréquentes de la vitesse du CPU.
  • userspace : laisser l’utilisateur fixer lui-même la vitesse qui lui convient, comme nous l’avons fait dans les paragraphes précédents.

Essayons les effets des principaux governors sur notre programme.

# echo performance > /sys/devices/system/cpu/cpu1/cpufreq/scaling_governor 
# cat /sys/devices/system/cpu/cpu1/cpufreq/scaling_cur_freq 
2400000
# ./compte-iterations 
Resultats : 25932762
# ./compte-iterations 
Resultats : 25937413
# ./compte-iterations 
Resultats : 25335626
# echo powersave > /sys/devices/system/cpu/cpu1/cpufreq/scaling_governor 
# cat /sys/devices/system/cpu/cpu1/cpufreq/scaling_cur_freq
1600000
# ./compte-iterations 
Resultats : 17625101
# ./compte-iterations 
Resultats : 16788736
# ./compte-iterations 
Resultats : 17017337
# echo ondemand > /sys/devices/system/cpu/cpu1/cpufreq/scaling_governor 
# cat /sys/devices/system/cpu/cpu1/cpufreq/scaling_cur_freq
1600000
# ./compte-iterations 
Resultats : 25371803
# ./compte-iterations 
Resultats : 25294453
# ./compte-iterations 
Resultats : 25920801
#

Conclusion

Il est intéressant de pouvoir configurer facilement la vitesse du processeur indépendamment des fréquences exactes, en employant par exemple une heuristique powersave sur un portable, performance sur un serveur, ou ondemand sur un poste de travail généraliste.

Pour les systèmes embarqués, les choix ne sont pas toujours évidents, comme on peut le voir dans ces articles : Temps réel et économie d’énergie 1 et 2.

[ACTU] Simulateur de second tour des présidentielles

Intérêts divers, Linux | Publié par cpb
avr 23 2012

Vous trouverez sur cette page un petit simulateur en javascript qui permet de calculer les résultats du second tour en fonction de votre estimation personnelle des participations et reports de voix des candidats du premier tour.

Traitements parallèles dans un script shell

Linux, Shell | Publié par cpb
avr 21 2012

Il est rare de devoir utiliser des traitements en tâche de fond dans un script shell. À moins, bien entendu, qu’il s’agisse d’un script de démarrage servant justement à lancer plusieurs traitements en parallèle.

Il peut néanmoins être parfois nécessaire de gérer des lancements en arrière-plan, comme cela m’est arrivé une fois.

Situation

Un système aéroportuaire de traitement de plots radar m’envoyait des informations en continu sur un port réseau UDP/IP. Je devais les enregistrer dans des fichiers horodatés. Chaque fichier contenait une heure d’enregistrement environ. Je devais également, à l’issue de chaque enregistrement, effectuer une analyse des données qui durait environ cinq minutes.

Logiciels existants

Deux logiciels, écrits en C, étaient disponibles.

Le premier, appelons-le enregistreur prenait plusieurs arguments sur sa ligne de commande

  • L’adresse IPv4 du groupe multicast dans lequel s’inscrire. Ceci n’a pas d’intérêt pour ce qui nous concerne ici, prenons par exemple 224.10.10.10.
  • Le numéro de port UDP pour recevoir les données. La valeur ne nous concerne pas plus que la précédente. Fixons-la arbitrairement à 2012.
  • La durée en secondes de l’enregistrement à réaliser. Pour une durée d’une heure, nous passerons 3600.
  • Le nom du fichier où stocker l’enregistrement. Pour que les fichiers soient aisément manipulables par la suite, il est nécessaire que leurs noms contiennent un horodatage compréhensible.

Le second logiciel, que nous appellerons statistiques était un outil étudiant les données reçues (qu’il lisait depuis un fichier), et sortant des éléments d’information sur sa sortie standard. Ce programme prenait uniquement en argument le nom du fichier d’enregistrement à traiter.

Ces deux logiciels fonctionnaient très bien, et je devais les enchaîner dans un script shell afin qu’ils tournent 24H/24H.

Problème

Tout d’abord il fallait générer le nom du fichier en incluant un horodatage lisible. Pour cela le plus simple est souvent de s’appuyer sur la commande date dont les options permettent facilement de construire une chaîne de caractères en insérant des éléments de date et d’heure.

NOM_FICHIER=$(date +"enregistrement-LFPG-%Y-%m-%d-%H-%M.dat")

La variable NOM_FICHIER contiendra une chaîne de caractères comme enregistrement-LFPG-2012-04-21-09-00.dat.

Le second problème était plus gênant. Si l’on pouvait tolérer la perte d’une seconde de messages entre deux enregistrements, il n’était pas envisageable d’avoir un « trou » de cinq minutes (durée du programme statistiques). Je ne pouvais donc pas utiliser le shéma évident suivant.

#! /bin/sh
ADRESSE_IP=224.10.10.10
PORT_UDP=2012
while true
do
    # Generer le nom du fichier d'enregistrement
    FICHIER=$(date +"enregistrement-LFPG-%Y-%m-%d-%H-%M.dat")

    # Declencher l'enregistrement
    enregistreur ${ADRESSE_IP} ${PORT_UDP} 3600 "${FICHIER}"

    # Traiter l'heure ecoulee
    statistiques "${FICHIER}" > fichier-resultat.txt
done

Il fallait donc paralléliser le traitement statistique et l’enregistrement des données.

Première idée

On peut tout d’abord imaginer un schéma où le traitement statistique fonctionne en arrière-plan, la boucle principale étant cadencée par l’enregistreur. En voici un exemple.

#! /bin/sh
[...]
while true
do
    # Generer le nom du fichier d'enregistrement
    FICHIER=$(date +"enregistrement-LFPG-%Y-%m-%d-%H-%M.dat")

    # Declencher l'enregistrement
    enregistreur ${ADRESSE_IP} ${PORT_UDP} 3600 "${FICHIER}"

    # Traiter l'heure ecoulee
    statistiques "${FICHIER}" > fichier-resultat.txt &
done

On remarquera, à la fin de la ligne de la commande statistiques, le & qui provoque le passage à l’arrière-plan. Ceci permet au shell de reprendre immédiatement son exécution en début de boucle et donc de démarrer un nouvel enregistrement.

Cette méthode fonctionnait, mais elle présentait néanmoins un petit inconvénient. A l’issue du traitement statistique, le fichier de résultats devait être renommé en utilisant un numéro d’ordre séquentiel et copié dans un répertoire indépendant ; ceci seulement dans le cas où le fichier n’était pas vide. Il devenait compliqué de réaliser ces opérations dans une exécution en arrière-plan.

L’idée fut donc d’inverser le principe précédent, en effectuant l’enregistrement en arrière-plan et le traitement statistique dans la boucle principale.

Seconde idée

Voici un premier aperçu de la nouvelle solution.

#! /bin/sh
[...]
NUMERO_RESULTAT=1

while true
do
    # Generer le nom du fichier d'enregistrement
    FICHIER=$(date +"enregistrement-LFPG-%Y-%m-%d-%H-%M.dat")

    # Declencher l'enregistrement
    enregistreur ${ADRESSE_IP} ${PORT_UDP} 3600 "${FICHIER}" &

    # Traiter l'heure ecoulee
    statistiques "${FICHIER}" > fichier-resultat.txt

    # si le fichier n'est pas vide
    if [ -s fichier-resultat.txt ]

        # Le renommer et le déplacer
        mv fichier-resultat.txt "resultat-${NUMERO_RESULTAT}.txt"
        mv "resultat-${NUMERO_RESULTAT}.txt"  ~/resultats-statistiques/

        # Incrémenter le numéro d'ordre
        NUMERO_RESULTAT=$(( NUMERO_RESULTAT + 1 ))
    fi
done

Si cette idée semble séduisante au premier abord, elle ne fonctionne pas du tout en réalité. Lorsqu’on lance l’enregistreur en arrière-plan, le traitement statistique s’exécute en quelques minutes et relance un nouvel enregistreur alors que le précédent n’est pas terminé, et ainsi de suite.

Pour que cette méthode soit utilisable, il faudrait s’assurer avant de démarrer un enregistrement que le précédent soit fini. Pour cela, nous allons noter après le lancement en arrière-plan de l’enregistreur son numéro de processus (PID), et avant le lancement nous attendrons que le processus enregistreur précédent soit terminé.

Le shell nous fournit dans la variable $! le PID du dernier processus lancé en arrière-plan. Ajoutons donc la ligne suivante dans notre code.

[...]
    enregistreur ${ADRESSE_IP} ${PORT_UDP} 3600 "${FICHIER}" &
    PID_ENREGISTREUR=$!
    # Traiter l'heure ecoulee
    [...]

Pour attendre la fin d’un processus lancé par le shell, celui-ci nous propose la commande wait que l’on peut faire suivre du numéro de PID. Ajoutons cette attente avant le lancement.

[...]
    wait ${PID_ENREGISTREUR}
    enregistreur ${ADRESSE_IP} ${PORT_UDP} 3600 "${FICHIER}" &
    [...]

Nous devons bien sûr prendre en considération le démarrage de notre script, où aucun enregistreur ne tourne encore. Pour cela, il suffit d’initialiser la variable PID_ENREGISTREUR à un numéro impossible de processus (les PID sont toujours supérieurs à zéro) et de la tester avant d’appeler wait.

Troisième essai

Notre code devient :

#! /bin/sh
ADRESSE_IP=224.10.10.10
PORT_UDP=2012
REPERTOIRE_STATISTIQUES="~/resultats-statistiques/"
PID_ENREGISTREUR=0
while true
do
    # Generer le nom du fichier d'enregistrement
    FICHIER=$(date +"enregistrement-LFPG-%Y-%m-%d-%H-%M.dat")

    # Attendre la fin de l'enregistreur precedent
    if [ $PID_ENREGISTREUR -gt 0 ]
    then
        wait $PID_ENREGISTREUR
    fi
    # Declencher l'enregistrement
    enregistreur ${ADRESSE_IP} ${PORT_UDP} 3600 "${FICHIER}" &
    PID_ENREGISTREUR=$!

    # Traiter l'heure ecoulee
    statistiques "${FICHIER}" > fichier-resultat.txt

    # si le fichier n'est pas vide
    if [ -s fichier-resultat.txt ]
        # Le renommer et le deplacer
        mv "fichier-resultat.txt" "resultat-${NUMERO_RESULTAT}.txt"
        mv "resultat-${NUMERO_RESULTAT}.txt" "${REPERTOIRE_STATISTIQUES}"

        # Incrementer le numero d'ordre
        NUMERO_RESULTAT=$(( NUMERO_RESULTAT + 1 ))
    fi
done

Pourtant, en observant le script, on peut s’apercevoir d’un gros défaut : le traitement statistique démarre sur le même fichier que l’enregistreur en cours ! Il faut bien sûr qu’il travaille sur l’enregistrement précédent. Modifions encore notre script.

Solution

#! /bin/sh
ADRESSE_IP=224.10.10.10
PORT_UDP=2012
REPERTOIRE_STATISTIQUES="~/resultats-statistiques/"
PID_ENREGISTREUR=0
FICHIER_COURANT=""
FICHIER_PRECEDENT=""

while true
do
    # Attendre la fin de l'enregistreur precedent
    if [ ${PID_ENREGISTREUR} -gt 0 ]
    then
        wait ${PID_ENREGISTREUR}
        FICHIER_PRECEDENT="${FICHIER_COURANT}"
    fi

    # Generer le nom du nouvel enregistrement
    FICHIER_COURANT=$(date +"enregistrement-LFPG-%Y-%m-%d-%H-%M.dat")

    # Lancer l'enregistreur en arriere-plan
    enregistreur ${ADRESSE_IP} ${PORT_UDP} 3600 "${FICHIER_COURANT}" &
    PID_ENREGISTREUR=$!

    # Traiter l'enregistrement de l'heure ecoulee
    statistiques "${FICHIER_PRECEDENT}" > fichier-resultat.txt

    # si le resultat n'est pas vide
    if [ -s fichier-resultat.txt ]
        # Le renommer et le deplacer
        mv "fichier-resultat.txt" "resultat-${NUMERO_RESULTAT}.txt"
        mv "resultat-${NUMERO_RESULTAT}.txt" "${REPERTOIRE_STATISTIQUES}"

        # Incrementer le numero d'ordre
        NUMERO_RESULTAT=$(( NUMERO_RESULTAT + 1 ))
    fi
done

Cette fois le script est correct et fonctionne.

Conclusion

Nous voyons que l’encadrement par un script shell de programmes existants n’est pas toujours évident surtout lorsqu’il existe une dépendance temporelle entre eux (synchronisation). Ici, certains points du script ont été ignorés (gestion des erreurs d’exécution comme la saturation du disque par exemple), mais il nous a quand même fallu quatre versions pour obtenir enfin un programme correct. On retiendra égtalement que le soin apporté à la lisibilité et à la clarté du script est un gage de facilité de maintenance ultérieure, surtout lorsque l’algorithme global sort quelque peu de l’ordinaire (parallélisme par exemple).

[ACTU] Mais pourquoi la Freebox reste-t-elle à l’heure d’hiver ?

Actualité, Embarqué, Linux | Publié par cpb
mar 27 2012

Comme beaucoup d’entre vous le savent déjà, la Freebox v.6 a raté son passage à l’heure d’été. Ce matin, alors que mon PC, ma montre et mon téléphone affichent 09:44, la Freebox posée à côté de mon bureau affiche encore 08:44. Au-delà du fait divers amusant, source de plaisanteries et de sarcasmes sur Twitter, je trouve cette mésaventure intéressante, car elle trouve son origine dans des logiciels que connaissent bien les amateurs de Linux embarqué !Freebox a l'heure d'hiver

La bibliothèque µClibc regroupe les fonctionnalités communes dont les applications de haut-niveau ont besoin (par exemple des traitements de chaîne de caractères, des fonctions de calcul, des méthodes d’accès aux fichiers, etc.) en fournissant les points d’entrée pour le noyau Linux (les appels-système). La particularité de la µClibc par rapport aux bibliothèques « C » plus classiques comme la Gnu Glibc est d’être écrite en essayant de réduite au maximum la taille du code exécutable, et de pouvoir ainsi être employée dans les systèmes embarqués avec de fortes contraintes de place mémoire.

La plupart des systèmes embarqués construits avec des outils comme Buildroot (comme ceux que nous avons mis en oeuvre dans plusieurs articles) s’appuient sur la µClibc, et la Freebox v.6 ne fait apparemment pas exception, même si la bibliothèque n’est pas explicitement mentionnée dans la liste de logiciels libres décrites ici.

Dans la dernière version stable de la µClibc (0.9.33 du 1er février), un bug a été corrigé, comme cela est mentionné dans sa page de ChangeLog. Voici la portion qui nous intéresse :

commit 47f3da1cf49377c25772bb54d07db55225bbb142
Author: Guillaume Bourcier
Date:   Tue Oct 11 13:45:33 2011 +0200

    libc: fix daylight saving time handling

    The algorithm computing daylight saving time incorrectly adds a day for
    each month after January for leap years. The clock shift from/to DST can
    be delayed if the last Sunday of a transition month is exactly seven
    days before the first of the following month.

    This change adds a day for the February month only.

    Signed-off-by: Guillaume Bourcier
    Signed-off-by: Richard Braun
    Signed-off-by: Carmelo Amoroso

Pour les années bissextiles (comme 2012), l’algorithme de calcul de la date de passage à l’heure d’été ajoutait par erreur un jour au mois de février.

Ainsi le dernier dimanche du mois de mars (ce qui définit officiellement la date du changement d’heure) était considéré comme le dimanche 31 mars (alors que le 31 mars tombe en réalité un samedi). Visiblement le firmware de la Freebox ne contient pas encore cette correction, aussi pense-t-elle que le changement horaire doit avoir lieu dimanche prochain.

Si cette explication est la bonne, nos Freebox devraient basculer en heure d’été dimanche 1er avril à 02:00. Peut être Free va-t-il plaider l’hypothèse d’un poisson d’avril sophistiqué et anticipé ? À moins qu’une mise à jour du firmware intervienne d’ici là.

Une touche d’humour supplémentaire : dans le message ChangeLog ci-dessus j’ai supprimé la fin des adresses mails des personnes mentionnées pour leur éviter le spam. Il faut quand même savoir que l’adresse de G.Bourcier qui a détecté et corrigé le bug est « Free.fr » !

[AJOUT le 30/03/2012]: Free annonce avoir modifié le firmware du Freebox Server (version 1.1.5) pour corriger l’erreur. Il faut le redémarrer pour que la mise à jour ait lieu. Je ne l’ai pas fait sur ma Freebox pour vérifier dimanche si le changement horaire est bien décalé d’une semaine.

[AJOUT le 01/04/2012]: Comme on pouvait s’y attendre, le changement horaire a eu lieu cette nuit et l’afficheur de la Freebox indique bien l’heure correcte. Il est temps de faire la mise à jour du firmware, car sa nouvelle version intègre plusieurs corrections.

Envoi d’un signal vers un processus depuis le kernel

Embarqué, Linux, Temps-réel | Publié par cpb
mar 21 2012

Nous avons examiné récemment le temps de réveil d’une tâche endormie dans un appel-système. Je voulais compléter cette expérience en m’intéressant au passage d’un signal depuis le noyau vers l’espace utilisateur. Nous allons plus particulièrement mesurer le temps d’activation d’un processus lorsqu’un signal temps réel lui est envoyé depuis le noyau.

Module du noyau

Le petit module ci-dessous déclare un nouveau périphérique qui apparaîtra sous forme de fichier spécial dans /dev/. Lorsqu’un processus ouvre ce fichier, la structure task_struct qui le représente – pointée par la variable globale current au moment de l’appel système open() – est mémorisée. Un timer, déclenché tous les dixièmes de seconde, enverra alors un signal SIGRTMAX à ce processus jusqu’à ce qu’il referme le fichier spécial.

En accompagnement d’un signal temps-réel, il est possible de transmettre une union sigval_t contenant soit un entier soit un pointeur. Nous allons transmettre un entier contenant la partie « nanosecondes » de l’heure actuelle.

Voici le code du module signal-sur-timer.c. L’archive contenant les codes cource se trouve ici.

#include <linux/interrupt.h>
#include <linux/version.h>
#include <linux/device.h>
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/cdev.h>
#include <linux/fs.h>

#include <asm/uaccess.h>

static dev_t          dev_reveil;
static struct cdev    cdev_reveil;
static struct class * class_reveil = NULL;

struct timer_list timer_reveil;
static void timer_function(unsigned long);

static int open_reveil  (struct inode *, struct file *);
static int flush_reveil (struct file *,  fl_owner_t);

static struct file_operations fops_reveil = {
    .owner   =  THIS_MODULE,
    .open    =  open_reveil,
    .flush   =  flush_reveil,
};

static int __init init_reveil (void)
{
    int erreur;

    erreur = alloc_chrdev_region(& dev_reveil, 0, 1, THIS_MODULE->name);
    if (erreur < 0)
        return erreur;

    class_reveil = class_create(THIS_MODULE, "classe-signal");
    if (IS_ERR(class_reveil)) {
        unregister_chrdev_region(dev_reveil, 1);
        return -EINVAL;
    }
    device_create(class_reveil, NULL, dev_reveil,
                  NULL, THIS_MODULE->name);

    cdev_init(& cdev_reveil, & fops_reveil);

    erreur = cdev_add(& cdev_reveil, dev_reveil, 1);
    if (erreur != 0) {
        device_destroy(class_reveil, dev_reveil);
        class_destroy(class_reveil);
        unregister_chrdev_region(dev_reveil, 1);
        return erreur;
    }
    init_timer(& timer_reveil);
    timer_reveil.function = timer_function;
    timer_reveil.expires  = HZ;
    add_timer(& timer_reveil);
    return 0;
}

static void __exit exit_reveil (void)
{
    del_timer(& timer_reveil);
    cdev_del(& cdev_reveil);
    device_destroy(class_reveil, dev_reveil);
    class_destroy(class_reveil);
    unregister_chrdev_region(dev_reveil, 1);
}

static struct task_struct * task_reveil = NULL;

static int open_reveil(struct inode * ind, struct file * filp)
{
    task_reveil = current;
    return 0;
}

static int flush_reveil(struct file * fil,  fl_owner_t id)
{
    task_reveil = NULL;
    return 0;
}

static void timer_function(unsigned long unused)
{
    struct siginfo info;
    struct timespec ts;

    ktime_get_real_ts(& ts);

    info.si_signo = SIGRTMAX;
    info.si_errno = 0;
    info.si_code = SI_QUEUE;
    info.si_pid = 0;
    info.si_int = ts.tv_nsec;
    if (task_reveil != NULL)
        send_sig_info(SIGRTMAX, & info, task_reveil);

    mod_timer(& timer_reveil, jiffies + HZ/10);
}

module_init(init_reveil);
module_exit(exit_reveil);
MODULE_LICENSE("GPL");

Nous pouvons compiler ce programme et le charger. Pour savoir sur quel CPU se déclenchera le timer, je vais utiliser la commande shell taskset au moment du chargement du module.

# taskset -c 0 insmod ./signal-sur-timer.ko 
# ls /sys/class/
ata_device  bdi  classe-signal [...]
# ls /sys/class/classe-signal/
signal_sur_timer
# ls /sys/class/classe-signal/signal_sur_timer
dev  power  subsystem  uevent
# cat /sys/class/classe-signal/signal_sur_timer/dev
250:0
# ls -l /dev/signal_sur_timer
crw------- 1 root root 250, 0 2012-03-22 07:11 /dev/signal_sur_timer
#

 Programme d’attente du signal

Le processus  qui recevra le signal dans l’espace utilisateur va également lire l’heure et calculer la différence avec celle reçue depuis le noyau. Comme nous n’avons pas transmis le champ « secondes » de l’heure, mais seulement le complément en nanosecondes, il faudra gérer un éventuel débordement en rajoutant un milliard de secondes.

Le programme s’arrêtera automatiquement au bout de 1000 signaux (1 minute 40 secondes, car les signaux surviennent tous les dixièmes de secondes). La durée entre le déclenchement du timer noyau et l’activation du processus utilisateur est affichée sur la sortie standard.

attente-signal.c:
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>

void handler_sigrtmax(int num, siginfo_t * info, void * unused)
{
    static int compteur = 0;
    struct timespec ts;
    long int duree;
    clock_gettime(CLOCK_REALTIME, & ts);

    if (info != NULL) {
        duree = ts.tv_nsec - info->si_int;
        if (duree < 0)
            duree += 1000000000;
    }
    fprintf(stdout, "%ld\n", duree);
    compteur ++;
    if (compteur == 1000)
        exit(0);
}

int main(int argc, char * argv[])
{
    int fd;
    struct sigaction action;

    sigfillset(& action.sa_mask);
    action.sa_sigaction = handler_sigrtmax;
    action.sa_flags = SA_SIGINFO;
    sigaction(SIGRTMAX, & action, NULL);

    if ((argc != 2) || ((fd = open(argv[1], O_RDONLY)) < 0)) {
        fprintf(stderr, "usage: %s fichier-spcial\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    while (1) {
        pause();
    }
    return EXIT_FAILURE;
}

On notera que dans le corps principal du programme, celui-ci reste endormi en permanence en attente du signal suivant.

Premiers résultats

Avant d’exécuter notre programme, nous allons fixer la fréquence CPU des deux coeurs afin d’obtenir des résultats plus constants.

# echo userspace > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
# echo userspace > /sys/devices/system/cpu/cpu1/cpufreq/scaling_governor
# cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq > /sys/devices/system/cpu/cpu0/cpufreq/scaling_setspeed
# cat /sys/devices/system/cpu/cpu1/cpufreq/scaling_max_freq > /sys/devices/system/cpu/cpu1/cpufreq/scaling_setspeed
#

Exécutons-le une première fois, sur un CPU différent du timer.

# taskset -c 1 ./attente-signal /dev/signal_sur_timer > resultats-1.txt
# cat resultats-1.txt
43131
30878
101946
137725
30878
31858
29898
27447
31369
31858
32348
34309
133315
179877
32838
[...]
30388
29897
29408
27447
#

Pour analyser les résultats, je vais utiliser quelques scripts que j’ai développé pour mon livre « Solutions temps réel sous Linux » (à paraître très prochainement). Ils seront disponibles dans quelques jours avec les sources des exemples du livre.

# calculer-statistiques < resultats-1.txt
 Nb mesures = 1000
 Minimum = 22055
 Maximum = 1631636
 Moyenne = 46002
 Ecart-type = 66924
# calculer-histogramme 100 0 3000000 < resultats-1.txt > histo-1.txt
# afficher-histogramme.sh histo-1.txt "Attente signal - CPU différents"
#

Figure 1 - Activation d'un processus par un signal du kernel

Ceci nous fournit une première figure avec la durée d’activation en abscisse et en ordonnée le nombre d’activations observées (l’échelle est logarithmique).

Sur la figure 1, nous observons que l’essentiel des temps d’activation sont inférieurs à 500 microsecondes (500000 nanosecondes), mais qu’il y a des pointes jusqu’à 1631 microsecondes, soit 1,6 millisecondes.

La moyenne s’établit à 46002 nanosecondes, soit 46 microsecondes.

Réitérons cette expérience en exécutons le processus en attente sur le même CPU que le timer du kernel.

# calculer-statistiques < resultats-2.txt
 Nb mesures = 1000
 Minimum = 15194
 Maximum = 3102998
 Moyenne = 39288
 Ecart-type = 113346
#

Nous remarquons que la durée maximale a augmenté, ce qui nous oblige à allonger légèrement l’axe des abscisses pour notre figure.

# calculer-histogramme 100 0 3200000 < resultats-2.txt > histo-2.txt
# afficher-histogramme.sh histo-2.txt "Attente signal - Même CPU"
#

 

Figure 2 - Activation d'un processus par un signal kernel - Même CPU

Figure 2 - Activation d'un processus par un signal kernel - Même CPU

La figure montre quelques activations sensiblement retardées, ce qui est typique d’une exécution sous un ordonnancement temps partagé. Toutefois, nous pouvons remarquer que la valeur moyenne est meilleure (39 microsecondes contre 46 microsecondes précédemment).

En outre, la figure présente un meilleur regroupement des données sur la gauche du graphique, la plupart des activations étant survenues dans le premier intervalle du graphique.

Nous pouvons en conclure que l’activation par un signal est plus efficace si le code émetteur et le processus récepteur s’exécutent sur le même CPU.

Pour dépasser les limites de l’ordonnancement temps partagé, nous pouvons recommencer notre mesure en passant le processus récepteur en temps réel grâce la commande shell chrt.

# taskset -c 0 chrt -f 40  ./attente-signal /dev/signal_sur_timer > resultats-3.txt
# calculer-statistiques < resultats-3.txt
 Nb mesures = 1000
 Minimum = 20096
 Maximum = 212225
 Moyenne = 47692
 Ecart-type = 37429
#

La valeur moyenne est proche de la première mesure, mais la valeur maximale est bien meilleure, nous allons « zoomer » sensiblement l’axe des abscisses de notre figure.

# calculer-histogramme 100 0 250000 < resultats-3.txt > histo-3.txt
# afficher-histogramme.sh histo-3.txt "Attente signal - Temps réel"
#
Figure 3 - Activation d'un processus temps réel par un signal kernel

Figure 3 - Activation d'un processus temps réel par un signal kernel

Cette fois, la plupart des mesures sont en-dessous de 150 microsecondes, et seules quelques unes se trouvent dans la zone entre 150 et 212 microsecondes. Nous nous rapprochons des performances attendues sur un système temps réel. Notons que ces expériences sont menées sur un système avec un noyau 3.0 générique provenant d’une distribution classique. Les résultats avec un noyau modifié par le patch Linux-rt seraient probablement meilleurs (je ferai l’essai dans quelques jours et posterai les résultats ici).

Notification de processus actif

Dans cette première étape, nous avons laissé notre processus endormi dans un appel système pause(), dont il n’émerge que pour exécuter le handler du signal reçu avant de se rendormir. Mais l’intérêt d’un signal est avant tout sa possibilité de notification asynchrone. Autrement dit, il n’est pas nécessaire d’être en attente bloquante. Le processus peut très bien réaliser des opérations de calcul ou de traitement des données, dès que le signal lui sera délivré, le programme déroutera son exécution pour se brancher sur le gestionnaire de signal avant de reprendre son travail comme précédemment.

Dans notre nouvelle expérience, nous allons simplement modifier la fin de la fonction main(), ainsi.

boucle-et-signal.c:
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
[...]
int main(int argc, char * argv[])
{
    [...]
    while (1) {
        ;
    }
    return EXIT_FAILURE;
}

Première exécution, comme précédemment, commençons par lancer le processus sur un autre CPU que celui du timer du kernel.

# taskset -c 1 ./boucle-et-signal /dev/signal_sur_timer > resultats-4.txt
# cat resultats-4.txt
24507
12743
10293
8822
10293
8822
9802
8823
9803
[...]
10100065
22056
42641
17154
18135
18625
18135
20096
18625
18135
18134
119101
119101
119591
117140
14704
12743
# calculer-statistiques < resultats-4.txt
 Nb mesures = 1000
 Minimum = 8332
 Maximum = 10100065
 Moyenne = 28521
 Ecart-type = 326958
#

Les résultats sont sensiblement meilleurs en moyenne que précédemment, c’est normal car le processus étant actif, le processeur ne peut pas se mettre en veille et la délivrance du signal est d’autant plus efficace.
Toutefois la durée maximale a bien empiré : plus de 10ms ! Ceci est également normal car le processus s’exécute sous un ordonnancement temps partagé.

Relançons-le en temps réel, sur le même CPU que le timer du kernel.

# taskset -c 0 chrt -f 40  ./boucle-et-signal /dev/signal_sur_timer > resultats-5.txt
# calculer-statistiques < resultats-5.txt
 Nb mesures = 1000
 Minimum = 2940
 Maximum = 328386
 Moyenne = 20025
 Ecart-type = 37036
#
Figure 4 - Signal sur processus actif en temps-réel

Figure 4 - Signal sur processus actif en temps-réel

Nous voyons sur la figure 4 que la majorité des signaux sont traités en moins de 120 microsecondes, et que seules quelques exceptions peuvent être retardées jusqu’à 328 microsecondes.

Il faudrait laisser fonctionner le processus beaucoup plus longtemps, avec une charge système importante pour s’assurer de la durée maximale d’activation d’un processus par un signal provenant du kernel. Toutefois, je pense que l’ordre de grandeur (quelques centaines de microsecondes) est celui que l’on peut attendre avec Linux vanilla.

Nous pourrions probablement obtenir des résultats similaires avec un noyau contenant le patch Linux-rt, voire meilleurs en ce qui concerne les cas extrêmes.

Conclusion

En conclusion je pense que le réveil ou l’activation d’un processus par un signal provenant du kernel, pour le notifier par exemple de l’occurrence d’une interruption matérielle, est une alternative à la mise en sommeil sur un appel système bloquant – read(), ioctl(), poll()… – qu’il faut considérer lors de la mise au point d’un système interactif devant répondre à des événements externes.