Table of Contents

Obtener información sobre Procesos, hilos, afinidad y las CPUs / Núcleos donde se ejecutan

En esta guía se explicarán conceptos y el tratamiento básico de procesos / hilos en base a los procesadores / núcleos del sistema. Se mostrará como cambiar de núcleo / procesador un determinado proceso o uno de sus hilos en ejecución. También se nombrarán varias herramientas para visualizar los hilos en relación a los procesadores y sus núcleos.

Afinidad de CPU

Los procesos suelen ejecutarse dentro de lo que se considera un cpuset, es decir, los conjunto de hilos tienen asignados un conjunto de núcleos donde procesarse. A pesar de eso, puede que los hilos dentro del cpuset vayan cambiando de core o procesador según necesidad, de esto se encarga el núcleo del sistema operativo. En algunos momentos puede que el manejo del núcleo en cuanto a asignación de cores / núcleos no sea el óptimo, siendo recomendable cambiar la afinidad para forzar que determinados procesos / hilos se ejecuten siempre en un determinado procesador.

Cuando un proceso / hilo cambia de procesador, este pierde los datos que tenía en la cache del otro procesador y puede repercutir en una disminución del desempeño de la aplicación. En GNU/Linux se puede hacer uso del comando taskset para consultar y/o fijar la afinidad de determinados procesos y/o hilos.

# Mostrar afinidad.
taskset -c -p 12457
lista de afinidad actual del pid 12457: 0-7
 
# Modificar afinidad
taskset -c -p 0 12457
lista de afinidad actual del pid 12457: 0-7
nueva lista de afinidad del pid 12457: 0
 
taskset -c -p 12457
lista de afinidad actual del pid 12457: 0

Lectura recomendada: http://mechanical-sympathy.blogspot.de/2011/07/processor-affinity-part-1.html

Hilos / LWP

Un proceso que contiene múltiples flujos de ejecución se conoce como proceso de multi-hilo. Cada hilo es visto por el kernel como un proceso separado, pero tienen diferente trato y comportamiento que los procesos normales, se les denomina a nivel de núcleo “procesos ligeros” (LWP, Light Weight Process).

Cuando se crea un nuevo proceso, este aparece como un hilo donde tanto el PID y TGID son iguales. Cuando dicho proceso / hilo genera otro hilo, este hereda el TGID del hilo original.

No todos los procesos hacen uso de hilos de la misma forma, eso depende de como esté desarrollada la aplicación. En muchos casos es necesario especificar que el programa en uso utilice un número concreto de hilos, normalmente en base al número de procesadores y núcleos disponibles. También hay diferentes tipos de hilos y usos dependiendo del lenguaje de programación utilizado.

Enlaces remendados sobre hilos / multitarea:

Relacionado: cpuset, isolcpus, numactl.

Formas y herramientas para identificar procesos / hilos y sus CPUs/cores

Directorio /proc.

Listar los hilos del un proceso (12457).

ls /proc/12457/task/
12457  12461  12463  12465  12467  12469  12471  12473  12475  12477  12479  12482  12484  12486  12488  12490  12492  12494  12496  12498  12511  12530  12533  12535  12538  12544  12550  12555  12577  13672  13868
12460  12462  12464  12466  12468  12470  12472  12474  12476  12478  12481  12483  12485  12487  12489  12491  12493  12495  12497  12504  12512  12532  12534  12536  12539  12545  12554  12559  12653  13681  13937

# Este comando muestra información detallada del proceso o bien del hilo: Afinidad, hilos, IDs, estado, etc.

cat /proc/<pid>/status 
Name:	firefox
State:	S (sleeping)
Tgid:	12457
Ngid:	0
Pid:	12457
PPid:	1
TracerPid:	0
Uid:	1000	1000	1000	1000
Gid:	1000	1000	1000	1000
FDSize:	256
Groups:	93 108 1000 
NStgid:	12457
NSpid:	12457
NSpgid:	518
NSsid:	518
VmPeak:	 1805604 kB
VmSize:	 1801176 kB
VmLck:	       0 kB
VmPin:	       0 kB
VmHWM:	  905764 kB
VmRSS:	  844384 kB
VmData:	 1267856 kB
VmStk:	     224 kB
VmExe:	     140 kB
VmLib:	  132180 kB
VmPTE:	    2808 kB
VmPMD:	      16 kB
VmSwap:	       0 kB
Threads:	66
SigQ:	0/27638
SigPnd:	0000000000000000
ShdPnd:	0000000000000000
SigBlk:	0000000000000000
SigIgn:	0000000000001000
SigCgt:	0000002f820144af
CapInh:	0000000000000000
CapPrm:	0000000000000000
CapEff:	0000000000000000
CapBnd:	0000003fffffffff
Seccomp:	0
Cpus_allowed:	01
Cpus_allowed_list:	0
Mems_allowed:	00000000,00000001
Mems_allowed_list:	0
voluntary_ctxt_switches:	1410018
nonvoluntary_ctxt_switches:	2878

Obtener información ahora de un determinado hilo del proceso.

cat /proc/12457/task/12470/status
Name:	JS Helper
State:	S (sleeping)
Tgid:	12457
Ngid:	0
Pid:	12470
PPid:	1
TracerPid:	0
Uid:	1000	1000	1000	1000
Gid:	1000	1000	1000	1000
FDSize:	256
Groups:	93 108 1000 
NStgid:	12457
NSpid:	12470
NSpgid:	518
NSsid:	518
VmPeak:	 1805604 kB
VmSize:	 1769940 kB
VmLck:	       0 kB
VmPin:	       0 kB
VmHWM:	  905764 kB
VmRSS:	  842520 kB
VmData:	 1236620 kB
VmStk:	     224 kB
VmExe:	     140 kB
VmLib:	  132180 kB
VmPTE:	    2808 kB
VmPMD:	      16 kB
VmSwap:	       0 kB
Threads:	62
SigQ:	0/27638
SigPnd:	0000000000000000
ShdPnd:	0000000000000000
SigBlk:	0000000000000000
SigIgn:	0000000000001000
SigCgt:	0000002f820144af
CapInh:	0000000000000000
CapPrm:	0000000000000000
CapEff:	0000000000000000
CapBnd:	0000003fffffffff
Seccomp:	0
Cpus_allowed:	ff
Cpus_allowed_list:	0-7
Mems_allowed:	00000000,00000001
Mems_allowed_list:	0
voluntary_ctxt_switches:	25895
nonvoluntary_ctxt_switches:	55

Si se observan los IDs se puede comprobar la relación entre PIDs comentada anteriormente.

Herramienta Top. (Permite ver hilos, su nombre + núcleos de procesador)

top -H -p <pid> 
# Pulsar tecla f
# pulsar tecla j / de usar procps-ns, bajar hasta "Last Used Cpu" y marcarlo con la barra espaciadora.
# A la parte derecha encontraremos una columna llamada "P".

Herramienta Htop. (Permite ver hilos y sus nombres). htop > F2 > “Display option” > “Three view” > “Show custom thread names” > F10.

El comando ps. (Permite ver hilos, su nombre y el procesador / núcleo donde se ejecutan).

ps -mo pid,tid,lwp,%cpu,cpuid,lastcpu -p 12457
  PID   TID   LWP %CPU CPUID   C
12457     -     -  9.0     -   -
    - 12457 12457  7.2     0   0
    - 12460 12460  0.0     2   2
    - 12461 12461  0.0     6   6
    - 12462 12462  0.2     1   1
    - 12463 12463  0.0     7   7

# NOTA: TTID = LWP, se puede elegir cualquiera de los dos.

ps -p 12457 -L
  PID   LWP TTY          TIME CMD
12457 12457 ?        00:10:16 firefox
12457 12460 ?        00:00:00 Gecko_IOThread
12457 12461 ?        00:00:00 Link Monitor
12457 12462 ?        00:00:20 Socket Thread
...

NOTA: Se recomienda utilizar “ps L” para definir los campos de ps que se necesiten.

Comando pstree. (No muestra el procesador donde se ejecuta cada hilo pero sí el nombre del hilo).

pstree -p 12457
firefox(12457)─┬─plugin-containe(13530)─┬─{Chrome_ChildThr}(13532)
               │                        ├─{plugin-containe}(13536)
               │                        ├─{plugin-containe}(13537)
               │                        ├─{plugin-containe}(13538)
               │                        ├─{plugin-containe}(13539)
               │                        ├─{plugin-containe}(13540)
               │                        ├─{plugin-containe}(13541)
               │                        ├─{plugin-containe}(13542)
               │                        ├─{plugin-containe}(13543)
               │                        └─{plugin-containe}(13544)
               ├─{BgHangManager}(12477)
               ├─{Cache I/O}(12532)
               ├─{Cache2 I/O}(12479)
               ├─{Cert Verify}(12496)
               ├─{Compositor}(12484)
               ├─{DNS Res~ver #46}(13681)
               ├─{DNS Res~ver #54}(13937)
               ├─{DOM Worker}(12504)
               ├─{...} (...)

Evitar las pérdida de memoria en la programación thread de POSIX

Artículo original: http://www.ibm.com/developerworks/ssa/library/l-memory-leaks/

La razón principal de utilizar los threads es aumentar la performance del programa. Los threads pueden ser creados y administrados con menos gastos generales del sistema operativo y menos recursos en el sistema. Todos los threads en un proceso comparten el mismo espacio de direcciones, lo que hace que las comunicaciones entre los threads sean más eficientes y más sencillas de implementar que la comunicación entre los procesos. Por ejemplo, si un thread está esperando que se complete una llamada de entrada/salida del sistema, los otros pueden estar trabajando en las tareas intensivas de la CPU. Con los threads, las tareas importantes pueden ser programadas para que tengan prioridad sobre las tareas de menor prioridad e incluso que las puedan interrumpir. Las tareas poco frecuentes y esporádicas pueden ser intercaladas en forma regular entre las tareas programadas, creando de este modo una programación más flexible. Y, por último, los pthreads son ideales para la programación en paralelo en las máquinas con múltiples CPUs.

Y la razón principal para utilizar los threads de POSIX, o los pthreads, es más simple aún: Como parte de la interfaz estandarizada de programación de los threads del lenguaje C son sumamente portátiles.

La programación de los threads de POSIX brinda muchos beneficios, pero si usted no tiene en claro algunas de las normas básicas corre el riesgo de escribir un código difícil de depurar y causar fugas de la memoria. Comencemos por revisar los threads de POSIX que pueden serthreads que se pueden unir o threads desunidos.

Threads que se pueden unir.

Si usted quiere producir un nuevo thread y necesita saber cómo es terminado, entonces necesita un thread que se puede unir. Para estos threads, el sistema asigna un almacenamiento privado para guardar el estado de terminación de los threads. El estado es actualizado después de que el thread termina. Para recuperar el estado de la terminación del thread puede hacer lo siguiente.

pthread_join(pthread_t thread, void** value_ptr)

El sistema asigna almacenamiento subyacente para cada thread, incluyendo la pila, la ID del thread, el estado de terminación del thread y así sucesivamente. Este almacenamiento subyacente permanecerá en el espacio del proceso (y no será reciclado) hasta que el thread haya terminado y haya sido unido por otros threads.

Threads desunidos

La mayor parte del tiempo, usted crea un thread, le asigna alguna tarea y luego continúa procesando otros asuntos. En estos casos, usted no se preocupa de cómo los threads terminan, y un thread desunido sería una buena elección.

Para los threads desunidos, el sistema recicla sus recursos subyacentes en forma automática después de que el thread termina.

Reconocer las fugas.

Si usted crea un thread que se puede unir pero se olvida de unirlo, sus recursos o su memoria privada se mantienen siempre en el espacio del proceso y nunca son reclamadas. Siempre una los threads que se pueden unir, de no hacerlo, usted corre el riesgo de tener pérdidas serias de memoria.

Por ejemplo, un thread en Red Hat Enterprise Linux (RHEL4) necesita una pila de 10MB, lo que significa que se pierde al menos 10MB de memoria si usted no lo ha unido. Supongamos que usted diseña un programa en la modalidad de administrador-empleado (manager-worker) para procesar las solicitudes entrantes. Se necesitan entonces crear cada vez más threads de empleado, desarrollar tareas individuales y luego terminar. Si son threads que se pueden unir y usted no ha llamado a pthread_join() para unirlos, cada thread producido perderá una cantidad considerable de memoria (al menos 10MB por pila) después de su terminación. El tamaño de la memoria perdida aumenta en forma continua a medida que más y más threads de empleado sean creados y terminados sin ser unidos. Además, el proceso no podrá crear nuevos threads, ya que no quedará memoria disponible para crearlos.

El listado 1 muestra la pérdida grave de memoria ocasionada si usted olvida unir los hilos que se pueden unir. Puede utilizar también este código para comprobar la cantidad máxima de grupos de threads que pueden coexistir en un espacio de proceso.

Listado 1. Crear una fuga de memoria

#include<stdio.h>
#include<pthread.h>
void run() {
   pthread_exit(0);
}

int main () {
   pthread_t thread;
   int rc;
   long count = 0;
   while(1) {
      if(rc = pthread_create(&thread, 0, run, 0) ) {
         printf("ERROR, rc is %d, so far %ld threads created\n", rc, count);
         perror("Fail:");
         return -1;
      }
      count++;
   }
   return 0;
}

En el listado, pthread_create() es llamado para crear un nuevo thread con un atributo de thread por omisión. De esta forma, el nuevo creado se puede unir. Se crean nuevos threads que se pueden unir sin cesar hasta que la falla ocurre. A continuación el código de error y la causa de la falla se imprimen.

Al compilar el código del Listado 1 en Red Hat Enterprise Linux Server release 5.4 con este comando:

[root@server ~]# cc -lpthread thread.c -o thread, se obtienen los resultados que se muestran en el Listado 2:
Listado 2. Resultados de la fuga de memoria
 
[root@server ~]# ./thread
ERROR, rc es 12, hasta el momento se crearon 304 threads
Falla: No puede asignar la memoria

Después de que el código haya creado 304 threads, ya no puede crear más. El código de error es 12, lo que significa que no tiene más memoria.

Como se muestra en el listado 1 y 2, los threads que se pueden unir son producidos, pero nunca unidos, por lo tanto cada thread que se puede unir terminado aún ocupa el espacio de proceso perdiendo la memoria de proceso.

Un thread de POSIX en RHEL tiene una pila privada con un tamaño de 10MB. En otras palabras, el sistema asigna al menos 10MB de almacenamiento privado por cada pthread. En nuestro ejemplo, 304 threads fueron producidos antes de que el proceso se detuviera; estos threads ocupan 304*10MB de memoria, alrededor de 3GB. El tamaño de la memoria virtual para un proceso es de 4GB con una cuarta parte del espacio de proceso reservada para el kernel de Linux . Sume eso y obtendrá un espacio de memoria de 3GB para el espacio del usuario. De ese modo la memoria de 3GB es consumida por threads muertos. Ésta es una fuga de memoria grave. Y es fácil ver cómo sucedió tan rápidamente.

Usted puede arreglar la fuga agregando el código para llamar pthread_join(), que une cada hilo que se puede unir.

Detectar las fugas

Al igual que con otras fugas de memoria, el problema no puede ser evidente cuando el proceso se ha iniciado. Así que ésta es una manera de detectar dichos problemas sin necesidad de acceder al código fuente:

  1. Cuente la cantidad de pilas de threads en el proceso. Esto incluye la cantidad de threads activos en ejecución y terminados.
  2. Cuente la cantidad de threads en ejecución activos en el proceso.
  3. Compare ambos. Si la cantidad de pilas de threads existentes es mayor que la de threads activos en ejecución, y la dispersión de estos dos números sigue aumentando a medida que el programa sigue funcionando, entonces la memoria tiene fugas.

Y lo más probable es que dicha fuga de memoria sea causada por no poder unir los threads que se pueden unir. Utilice el pmap para contar las pilas de threads

En un proceso de ejecución, la cantidad de pilas de threads es igual a la cantidad de de grupos de threads en el proceso. Los grupos de threads constan de threads activos en ejecución y de threads inactivos que se pueden unir.

pmap es una herramienta de Linux utilizada para informar sobre la memoria del procesos. Combine los siguientes comandos para obtener la cantidad de pilas de threads:

[root@server ~]# pmap PID | grep 10240 | wc -l

(10240KB es el tamaño de la pila por omisión en Red Hat Enterprise Linux Server release 5.4.) Utilice /proc/PID/task para contar los threads activos

Cada vez que un thread es creado y está en funcionamiento, una entrada se completa en /proc/PID/task. Cuando el thread termina, ya sea que se pueda unir o que sea individual, la entrada es eliminada de /proc/PID/task. De este modo la cantidad de threads activos se puede obtener mediante la ejecución de:

[root@server ~]# ls /proc/PID/task | wc -l.
Comparar los resultados

Controlar el resultado de “pmap PID | grep 10240 | wc -l” y compararlo con el resultado de “ls /proc/PID/task | wc -l”. Si la cantidad de pilas de threads es superior a la cantidad de threads activos, y su dispersión continúa creciendo a medida que el programa funciona, usted puede llegar a la conclusión de que el problema de fuga existe.

Comandos ps genéricos de interés

La salida del comando ps se puede adaptar a las necesidades de cada uno, pero la mayoría de las veces se usa una forma genérica para listar todos los procesos y se filtra con herramientas como puede ser grep. Una forma muy habitual es ps aux, pero ahora mostraremos un par de alternativas similares a la salida de las opciones “aux” de ps que muestra la infomación más clara, sobre todo con lo referente a fechas y tiempos.

Listar todos los procesos.

Fecha de inicio, tiempo transcurrido (días-h:m:s), tiempo de cpu usado, ID de usuario, PID, PPID, Memoria, CPU y comando.

ps -axo lstart,etime,bsdtime,euser,pid,ppid,%mem,%cpu,cmd
# ps -axo lstart,etime,bsdtime,euser,pid,ppid,%mem,%cpu,cmd --sort=-%mem # Ordenando por memoria (- indica orden descendente.)
# ps -axo lstart,etime,bsdtime,euser,pid,ppid,%mem,%cpu,cmd --sort=time  # Ordenando por tiempo de CPU (- indica orden descendente.)

                 STARTED     ELAPSED   TIME EUSER      PID  PPID %MEM %CPU CMD
Sun Mar 11 16:15:29 2018  8-04:30:39   0:00 root       662     1  0.0  0.0 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
Sun Mar 11 16:15:29 2018  8-04:30:39   0:18 www-data   663   662  0.0  0.0 nginx: worker process
Sun Mar 11 16:15:29 2018  8-04:30:39   0:00 www-data   664   662  0.0  0.0 nginx: worker process
Sun Mar 11 16:15:29 2018  8-04:30:39   0:18 www-data   665   662  0.0  0.0 nginx: worker process
Sun Mar 11 16:15:29 2018  8-04:30:39   0:19 www-data   666   662  0.0  0.0 nginx: worker process
Sun Mar 11 16:15:30 2018  8-04:30:38   2:36 minidlna   667     1  0.9  0.0 /usr/sbin/minidlnad -f /etc/minidlna.conf -P /run/minidlna/minidlna.pid
...

Listar todos los procesos junto con todos sus hilos.

Fecha de inicio, tiempo transcurrido (días-h:m:s), tiempo de cpu usado, ID de usuario, PID, PPID, Memoria, CPU y comando.

ps -eLo lstart,etime,bsdtime,euser,pid,ppid,%mem,%cpu,cmd
                 STARTED     ELAPSED   TIME EUSER      PID  PPID %MEM %CPU CMD
Mon Mar 19 18:57:30 2018    02:57:44   0:24 luis      2652   984  1.0  0.2 /usr/lib/firefox/firefox -contentproc -childID 4 -isForBrowser -intPrefs 6:50|7:-1|19:0|34:1000|42:20|43:5|44:10|51:0|57:128|58:10000|63:0|65:400|66:1|67:0|68:0|69:100|74:0|75:120|76:120|159:2|160:
Mon Mar 19 18:57:30 2018    02:57:44   0:02 luis      2652   984  1.0  0.0 /usr/lib/firefox/firefox -contentproc -childID 4 -isForBrowser -intPrefs 6:50|7:-1|19:0|34:1000|42:20|43:5|44:10|51:0|57:128|58:10000|63:0|65:400|66:1|67:0|68:0|69:100|74:0|75:120|76:120|159:2|160:
Mon Mar 19 18:57:30 2018    02:57:44   0:00 luis      2652   984  1.0  0.0 /usr/lib/firefox/firefox -contentproc -childID 4 -isForBrowser -intPrefs 6:50|7:-1|19:0|34:1000|42:20|43:5|44:10|51:0|57:128|58:10000|63:0|65:400|66:1|67:0|68:0|69:100|74:0|75:120|76:120|159:2|160:
Mon Mar 19 18:57:30 2018    02:57:44   0:00 luis      2652   984  1.0  0.0 /usr/lib/firefox/firefox -contentproc -childID 4 -isForBrowser -intPrefs 6:50|7:-1|19:0|34:1000|42:20|43:5|44:10|51:0|57:128|58:10000|63:0|65:400|66:1|67:0|68:0|69:100|74:0|75:120|76:120|159:2|160:
...