3

J'étudie comment exécuter un processus sur une CPU dédiée afin d'éviter les changements de contexte. Sur mon Ubuntu, j'ai isolé deux CPU en utilisant les paramètres du noyau "isolcpus = 3,7" et "irqaffinity = 0-2,4-6". Je suis sûr qu'il est correctement pris en compte:Impossible d'éviter les changements de contexte sur un processus lancé seul sur un CPU

$ cat /proc/cmdline 
BOOT_IMAGE=/boot/vmlinuz-4.8.0-27-generic root=UUID=58c66f12-0588-442b-9bb8-1d2dd833efe2 ro quiet splash isolcpus=3,7 irqaffinity=0-2,4-6 vt.handoff=7 

Après un redémarrage, je peux vérifier que tout fonctionne comme prévu. Sur une première console je lance

$ stress -c 24 
stress: info: [31717] dispatching hogs: 24 cpu, 0 io, 0 vm, 0 hdd 

Et un second, en utilisant « top » Je peux vérifier l'utilisation de mes processeurs:

top - 18:39:07 up 2 days, 20:48, 18 users, load average: 23,15, 10,46, 4,53 
Tasks: 457 total, 26 running, 431 sleeping, 0 stopped, 0 zombie 
%Cpu0 :100,0 us, 0,0 sy, 0,0 ni, 0,0 id, 0,0 wa, 0,0 hi, 0,0 si, 0,0 st 
%Cpu1 : 98,7 us, 1,3 sy, 0,0 ni, 0,0 id, 0,0 wa, 0,0 hi, 0,0 si, 0,0 st 
%Cpu2 : 99,3 us, 0,7 sy, 0,0 ni, 0,0 id, 0,0 wa, 0,0 hi, 0,0 si, 0,0 st 
%Cpu3 : 0,0 us, 0,0 sy, 0,0 ni,100,0 id, 0,0 wa, 0,0 hi, 0,0 si, 0,0 st 
%Cpu4 : 95,7 us, 4,3 sy, 0,0 ni, 0,0 id, 0,0 wa, 0,0 hi, 0,0 si, 0,0 st 
%Cpu5 : 98,0 us, 2,0 sy, 0,0 ni, 0,0 id, 0,0 wa, 0,0 hi, 0,0 si, 0,0 st 
%Cpu6 : 98,7 us, 1,3 sy, 0,0 ni, 0,0 id, 0,0 wa, 0,0 hi, 0,0 si, 0,0 st 
%Cpu7 : 0,0 us, 0,0 sy, 0,0 ni,100,0 id, 0,0 wa, 0,0 hi, 0,0 si, 0,0 st 
KiB Mem : 7855176 total, 385736 free, 5891280 used, 1578160 buff/cache 
KiB Swap: 15624188 total, 10414520 free, 5209668 used. 626872 avail Mem 

processeurs 3 et 7 sont gratuits tandis que les 6 autres les sont complètement occupés. Bien.


Pour le reste de mon test, je vais utiliser une petite application qui fait le traitement presque pur

  1. Il utilise deux tampons int de la même taille
  2. Il lit un par -one toutes les valeurs de la première mémoire tampon
    • chaque valeur est un indice aléatoire dans la seconde mémoire tampon
  3. Il lit la valeur à l'index dans le second tampon
  4. Il résume toutes les valeurs prises à partir du second tampon
  5. Il fait toutes les étapes précédentes pour de plus en plus
  6. A la fin, j'imprimer le nombre de contexte CPU volontaire et involontaire commutateurs

Je suis en train d'étudier ma demande quand je le lance:

  1. sur une CPU
  2. non isolé sur un CPU isolé

je fais par l'intermédiaire des lignes de commande suivantes:

$ ./TestCpuset    ### launch on any non-isolated CPU 
$ taskset -c 7 ./TestCpuset ### launch on isolated CPU 7 

lors de son lancement sur un processeur, le nombre de changements de contexte changement de 20 à ... milliers

En lancement sur une CPU isolée, le nombre de switchs de contexte est quasi constant (entre 10 et 20), même si je lance en parallèle un "stress -c 24". (semble tout à fait normal)

Mais ma question est: pourquoi n'est-ce pas 0 absolument 0? Quand un changement est fait sur un processus, c'est pour le remplacer par un autre processus? Mais dans mon cas, il n'y a pas d'autre processus à remplacer!

J'ai une hypothèse qui est que l'option « isolcpus » isolerait CPU former tout processus (à moins que le processus d'une affinité CPU serait donné, comme ce qui se fait avec « taskset ») mais pas de tâches du noyau. Cependant, je trouve aucune documentation à ce sujet

Je vous serais reconnaissant toute aide pour atteindre 0 contexte commutateurs

Pour votre information, cette question est fermée à l'autre j'ai ouvert précédemment: Cannot allocate exclusively a CPU for my process

Voici le code du programme que je me sers:

#include <limits.h> 
#include <iostream> 
#include <unistd.h> 
#include <sys/time.h> 
#include <sys/resource.h> 

const unsigned int BUFFER_SIZE = 4096; 

using namespace std; 


class TimedSumComputer 
{ 

public: 
    TimedSumComputer() : 
    sum(0), 
    bufferSize(0), 
    valueBuffer(0), 
    indexBuffer(0) 
    {} 


public: 
    virtual ~TimedSumComputer() 
    { 
    resetBuffers(); 
    } 


public: 
    void init(unsigned int bufferSize) 
    { 
    this->bufferSize = bufferSize; 
    resetBuffers(); 
    initValueBuffer(); 
    initIndexBuffer(); 
    } 


private: 
    void resetBuffers() 
    { 
    delete [] valueBuffer; 
    delete [] indexBuffer; 
    valueBuffer = 0; 
    indexBuffer = 0; 
    } 


    void initValueBuffer() 
    { 
    valueBuffer = new unsigned int[bufferSize]; 
    for (unsigned int i = 0 ; i < bufferSize ; i++) 
    { 
     valueBuffer[i] = randomUint(); 
    } 
    } 


    static unsigned int randomUint() 
    { 
    int value = rand() % UINT_MAX; 
    return value; 
    } 


protected: 
    void initIndexBuffer() 
    { 
    indexBuffer = new unsigned int[bufferSize]; 
    for (unsigned int i = 0 ; i < bufferSize ; i++) 
    { 
     indexBuffer[i] = rand() % bufferSize; 
    } 
    } 


public: 
    unsigned int getSum() const 
    { 
    return sum; 
    } 


    unsigned int computeTimeInMicroSeconds() 
    { 
    struct timeval startTime, endTime; 

    gettimeofday(&startTime, NULL); 
    unsigned int sum = computeSum(); 
    gettimeofday(&endTime, NULL); 

    return ((endTime.tv_sec - startTime.tv_sec) * 1000 * 1000) + (endTime.tv_usec - startTime.tv_usec); 
    } 


    unsigned int computeSum() 
    { 
    sum = 0; 

    for (unsigned int i = 0 ; i < bufferSize ; i++) 
    { 
     unsigned int index = indexBuffer[i]; 
     sum += valueBuffer[index]; 
    } 

    return sum; 
    } 


protected: 
    unsigned int sum; 
    unsigned int bufferSize; 
    unsigned int * valueBuffer; 
    unsigned int * indexBuffer; 

}; 



unsigned int runTestForBufferSize(TimedSumComputer & timedComputer, unsigned int bufferSize) 
{ 
    timedComputer.init(bufferSize); 

    unsigned int timeInMicroSec = timedComputer.computeTimeInMicroSeconds(); 
    cout << "bufferSize = " << bufferSize << " - time (in micro-sec) = " << timeInMicroSec << endl; 
    return timedComputer.getSum(); 
} 



void runTest(TimedSumComputer & timedComputer) 
{ 
    unsigned int result = 0; 

    for (unsigned int i = 1 ; i < 10 ; i++) 
    { 
    result += runTestForBufferSize(timedComputer, BUFFER_SIZE * i); 
    } 

    unsigned int factor = 1; 
    for (unsigned int i = 2 ; i <= 6 ; i++) 
    { 
    factor *= 10; 
    result += runTestForBufferSize(timedComputer, BUFFER_SIZE * factor); 
    } 

    cout << "result = " << result << endl; 
} 



void printPid() 
{ 
    cout << "###############################" << endl; 
    cout << "Pid = " << getpid() << endl; 
    cout << "###############################" << endl; 
} 



void printNbContextSwitch() 
{ 
    struct rusage usage; 
    getrusage(RUSAGE_THREAD, &usage); 
    cout << "Number of voluntary context switch: " << usage.ru_nvcsw << endl; 
    cout << "Number of involuntary context switch: " << usage.ru_nivcsw << endl; 
} 



int main() 
{ 
    printPid(); 

    TimedSumComputer timedComputer; 
    runTest(timedComputer); 

    printNbContextSwitch(); 

    return 0; 
} 
+0

D'où proviennent vos données? Utilisez-vous plus de mémoire que votre machine a physiquement? Je m'attendrais à ce que l'accès à une section paginée de la mémoire va forcer une augmentation dans le compteur de changement de contexte quand le processus est suspendu en attendant l'opération de pagination. –

+0

Le programme que j'utilise est juste un simple programme de test, il accède uniquement aux tampons qui sont initialisés avec des valeurs aléatoires (voir la fonction rand()) –

Répondre

1

potentiellement tout syscall c ould impliquerait un changement de contexte. Lorsque vous accédez à la mémoire paginée, cela peut également augmenter le nombre de commutations de contexte. Pour atteindre les 0 commutateurs de contexte, vous devez forcer le noyau à conserver toute la mémoire que votre programme utilise dans son espace d'adressage, et vous devez être sûr qu'aucun des appels système que vous appelez n'implique un changement de contexte. Je crois que cela peut être possible sur les noyaux avec des correctifs RT, mais probablement difficile à obtenir sur le noyau de distribution standard.

+0

Merci beaucoup pour cette réponse. Je suis presque sûr que dans mon exemple simple (dont le code est fourni ci-dessus) toute la mémoire utilisée par mon programme reste mappée => pas de page sortie. –

+0

En outre, je n'ai volontairement presque aucun syscalls sauf: 1) ceux introduits par new/delete 2) getrusage() ??? 3) cout Je peux me tromper mais les changements de contexte entraînés par les syscalls sont enregistrés dans des "switchs contextuels volontaires" mais de mon côté, le problème principal est avec les "involontaires" –

1

Aujourd'hui, j'ai obtenu plus d'indices concernant mon problème. Je me suis rendu compte que je devais étudier en profondeur ce qui se passait dans le planificateur du noyau. J'ai trouvé ces deux pages:

j'ai activé planificateur suivi pendant que ma demande était en cours d'exécution comme ça:

# sudo bash 
# cd /sys/kernel/debug/tracing 
# echo 1 > options/function-trace ; echo function_graph > current_tracer ; echo 1 > tracing_on ; echo 0 > tracing_max_latency ; taskset -c 7 [path-to-my-program]/TestCpuset ; echo 0 > tracing_on 
# cat trace 

Comme mon programme a été lancé sur le processeur 7 (taskset -c 7), je dois filtrer la sortie "trace"

# grep " 7)" trace 

Je peux alors rechercher des transitions, d'un processus à l'autre:

# grep " 7)" trace | grep "=>" 
... 
7) TestCpu-4753 => kworker-5866 
7) kworker-5866 => TestCpu-4753 
7) TestCpu-4753 => watchdo-26 
7) watchdo-26 => TestCpu-4753 
7) TestCpu-4753 => kworker-5866 
7) kworker-5866 => TestCpu-4753 
7) TestCpu-4753 => kworker-5866 
7) kworker-5866 => TestCpu-4753 
7) TestCpu-4753 => kworker-5866 
7) kworker-5866 => TestCpu-4753 
... 

Bingo! Il semble que le contexte commutateurs je suivi sont des transitions vers:

  • kworker
  • chien de garde

Je dois maintenant trouver:

  • ce sont exactement ces processus/threads? (il semble qu'ils soient gérés par le noyau)
  • Puis-je les éviter pour fonctionner sur mes processeurs dédiés?

Pour bien sûr, encore une fois je vous serais reconnaissant de toute l'aide :-P

+0

J'ai trouvé cette page vraiment intéressante: [éviter le démon running dans les noyaux cpu dédiés] (http://stackoverflow.com/questions/40081780/avoid-daemon-running-in-dedicated-cpu-cores/40082778) –

+1

Il semble que le chien de garde peut être désactivé en utilisant l'option noyau Linux 'nowatchdog ' –

0

Par souci de ceux qui trouvent cela via google (comme moi), /sys/devices/virtual/workqueue/cpumask contrôles où le noyau peut en file d'attente des travaux mis en attente avec WORK_CPU_UNBOUND (Don t attention quel cpu). Au moment d'écrire cette réponse, il n'est pas défini sur le même masque que celui manipulé par défaut par isolcpus. Une fois que je l'ai changé pour ne pas inclure mon processeur isolé, j'ai vu une quantité significativement plus petite (mais non nulle) de changements de contexte à mes threads critiques. Je suppose que les travaux qui ont fonctionné sur mon processeur isolé doivent l'avoir demandé spécifiquement, par exemple en utilisant schedule_on_each_cpu.