performance - sistemas - No se pueden evitar los cambios de contexto en un proceso iniciado solo en una CPU
procesos y threads (3)
Estoy investigando cómo ejecutar un proceso en una CPU dedicada para evitar interruptores de contexto. En mi Ubuntu, aislé dos CPU usando los parámetros del núcleo "isolcpus = 3,7" e "irqaffinity = 0-2,4-6". Estoy seguro de que se tiene en cuenta correctamente:
$ 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
Después de reiniciar, puedo verificar que todo funcione como se espera. En una primera consola corro
$ stress -c 24
stress: info: [31717] dispatching hogs: 24 cpu, 0 io, 0 vm, 0 hdd
Y en una segunda, usando "arriba" puedo verificar el uso de mis CPU:
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
Las CPU 3 y 7 son gratuitas, mientras que las otras 6 están completamente ocupadas. Multa.
Para el resto de mi prueba, usaré una pequeña aplicación que hace un procesamiento casi puro
- Utiliza dos almacenamientos intermedios int del mismo tamaño
- Lee uno a uno todos los valores del primer buffer
- cada valor es un índice aleatorio en el segundo buffer
- Lee el valor en el índice en el segundo buffer
- Suma todos los valores tomados del segundo buffer
- Hace todos los pasos anteriores para más grande y más grande
- Al final, imprimo la cantidad de interruptores de contexto de CPU voluntarios e involuntarios
Ahora estoy estudiando mi aplicación cuando la lanzo:
- en una CPU no aislada
- en una CPU aislada
Lo hago a través de las siguientes líneas de comando:
$ ./TestCpuset ### launch on any non-isolated CPU
$ taskset -c 7 ./TestCpuset ### launch on isolated CPU 7
Cuando se inicia en cualquier CPU, la cantidad de conmutadores de contexto cambia de 20 a ... miles
Cuando se inicia en una CPU aislada, el número de cambios de contexto es casi constante (entre 10 y 20), incluso si lanzo en paralelo un "stress -c 24". (se ve bastante normal)
Pero mi pregunta es: ¿por qué no es 0 absolutamente 0? Cuando se hace un cambio en un proceso, ¿es para reemplazarlo por otro proceso? ¡Pero en mi caso no hay otro proceso para reemplazar!
Tengo una hipótesis que es que la opción "isolcpus" aislaría CPU de cualquier proceso (a menos que el proceso proporcione una afinidad de CPU, como lo que se hace con "taskset") pero no desde las tareas del kernel. Sin embargo, no encontré documentación al respecto
Agradecería cualquier ayuda para llegar a 0 interruptores de contexto
FYI, esta pregunta está cerrada a otra que abrí anteriormente: No puedo asignar exclusivamente una CPU para mi proceso
Aquí está el código del programa que estoy usando:
#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;
}
Hoy, obtuve más pistas sobre mi problema, me di cuenta de que tenía que investigar a fondo lo que estaba sucediendo en el programador Kernel. Encontré estas dos páginas:
Permití el seguimiento del planificador mientras mi aplicación se estaba ejecutando así:
# 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
Como mi programa se lanzó en la CPU 7 (taskset -c 7), tengo que filtrar la salida "trace"
# grep " 7)" trace
Luego puedo buscar transiciones, de un proceso a otro:
# 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! Parece que los cambios de contexto que estoy rastreando son transiciones a:
- kworker
- perro guardián
Ahora tengo que encontrar:
- ¿Cuáles son exactamente estos procesos / hilos? (parece que están manejados por el kernel)
- ¿Puedo evitar que se ejecuten en mis CPU dedicadas?
Por supuesto, una vez más agradecería cualquier ayuda :-P
Por el bien de quienes encuentran esto a través de google (como yo), controles /sys/devices/virtual/workqueue/cpumask
donde el núcleo puede /sys/devices/virtual/workqueue/cpumask
en cola trabajos en cola con WORK_CPU_UNBOUND
(No importa qué CPU). Al escribir esta respuesta, no está configurada en la misma máscara que la que isolcpus
manipula por defecto.
Una vez que lo cambié para no incluir mi cpus aislada, vi una cantidad significativamente menor (pero no cero) de cambios de contexto en mis hilos críticos. Supongo que los trabajos que se ejecutaron en mi cpus aislado deben haberlo solicitado específicamente, como mediante el uso de schedule_on_each_cpu
.
Potencialmente cualquier llamada de sistema podría implicar un cambio de contexto. Cuando accede a la memoria paginada, también puede aumentar el conteo de conmutadores de contexto. Para llegar a los 0 cambios de contexto necesitarás forzar kernel para mantener toda la memoria que usa tu programa asignada a su espacio de direcciones, y necesitarás asegurarte de que ninguna de las llamadas de sistema que invocas implica un cambio de contexto. Creo que puede ser posible en kernels con parches de RT, pero probablemente sea difícil de lograr en kernel de distribución estándar.