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.
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) ├─{...} (...)
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:
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.
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: ...