Revista Tecnología

Obtener información básica sobre procesos del sistema Linux en C y C++ (parte 3)

Publicado el 24 julio 2017 por Gaspar Fernández Moreno @gaspar_fm

Obtener información básica sobre procesos sistema Linux (parteObtener información básica sobre procesos del sistema Linux en C y C++ (parte 3)

Cuando te independizas y te vas a vivir a un piso o a una casa te preguntas, ¿cómo serán mis vecinos? Por un lado tendremos al típico que deja la basura en la puerta de su casa durante todo el día y esparce olores al resto de los vecinos; o el que desea compartir la música que escucha con los demás y el que cuando entra al edificio y ve que vas a entrar va corriendo al ascensor para no esperarte... Aunque como procesos en ejecución en un entorno Linux muchas veces queremos saber cómo son nuestros vecinos y, al contrario de lo que puede parecer en una comunidad de vecinos, éstos suelen ser mucho más receptivos y dispuestos a darnos información.

Podemos averiguar cuántos procesos hay en ejecución, la memoria o el tiempo de CPU que consumen, PID, PID del padre, threads, usuario y grupos real, usuario efectivo, puntuación OOM, estado actual y mucho más. Y cómo no, accediendo simplemente a archivos del sistema, lo cual nos proporciona una manera muy fácil para acceder independientemente del lenguaje que utilicemos.

Conceptos básicos

Cada vez que se ejecuta un proceso, el sistema operativo nos deja ver información sobre él a través de un directorio (virtual, no está escrito en disco ni nada) llamado /proc/[PID] donde PID es el identificador del proceso o Process ID y éste es un número entre 1 y el valor que podemos ver en /proc/sys/kernel/pid_max. En mi caso:

cat /proc/sys/kernel/pid_max


Por lo tanto, en mi sistema el número máximo que puede tener un PID y, por tanto, el número máximo de procesos que puede haber al mismo tiempo en el sistema es de 32768. Podemos aumentar este número si queremos escribiendo sobre /proc/sys/kernel/pid_max o con:

sudo sysctl kernel.pid_max=65536


El sistema operativo y las aplicaciones que corren sobre él deberán utilizar dicho PID para referirse a un proceso concreto. Por ejemplo el sistema operativo deberá almacenar información relativa a la ejecución del proceso cada vez que necesite memoria, realice eventos de entrada/salida o simplemente pausar y reanudar su ejecución. Otras aplicaciones deberán utilizar este PID para comunicarse con el proceso (por ejemplo mediante señales) o si queremos saber quién es el que más memoria está comiendo.

Información básica del proceso

Para lo que queremos hacer, tendremos que leer el fichero /proc/PID/statm. Esto lo podemos hacer con la línea de comando, por ejemplo buscaremos el proceso emacs (como hay varios, cogeremos el primero y consultaremos información):


Y en todos los números que vemos en este momento, vamos a poner un cierto orden definiendo cada uno de ellos y el tipo de variable con el que podemos almacenarlo:

  • pid (número, int) - Identificador del proceso.
  • comm (cadena, char [32]) - Nombre del ejecutable entre paréntesis.
  • state (carácter, char) - R (en ejecición), S (bloqueado), D (bloqueo ininterrumpible), Z (zombie), T (ejecución paso a paso), W (transfiriendo páginas).
  • ppid (número, int) - Pid del padre.
  • pgrp (número, int) - Identificador del grupo de procesos.
  • session (número, int) - Identificador de la sesión.
  • tty_nr (número, int) - Terminal que usa el proceso.
  • tpgid (número, int) - Identificador del grupo de procesos del proceso terminal al que pertenece el proceso actual.
  • flags (número natural, unsigned) - Flags del proceso actual.
  • minflt (número natural largo, unsigned long) - Fallos de página menores, que no han necesitado cargar una página de memoria desde disco.
  • cminflt (número natural largo, unsigned long) - Fallos de página menores que han hecho los procesos hijo del proceso actual.
  • majflt (número natural largo, unsigned long) - Fallos de página mayores, que han necesitado cargar una página de memoria desde disco.
  • cmajflt (número natural largo, unsigned long) - Fallos de página mayores que han hecho los procesos hijo del proceso actual.
  • utime (número natural largo, unsigned long) - Tiempo que este proceso ha estado en ejecución en modo usuario. Se mide en ticks de reloj o jiffies.
  • stime (número natural largo, unsigned long) - Tiempo que este proceso ha estado en modo kernel.
  • cutime (número largo, long) - Tiempo que los hijos de este proceso han estado en ejecución en modo usuario.
  • cstime (número largo, long) - Tiempo que los hijos de este proces han estado en ejecución en modo kernel.
  • priority (número largo, long) - Prioridad del proceso. Depende del planificador de procesos activo.
  • nice (número largo, long) - Es la simpatía del proceso. Este valor va de 19 (prioridad baja o generosidad, porque el proceso intenta no consumir mucho) a -20 (prioridad alta o codicia, porque el proceso intenta acaparar CPU).
  • num_threads (número largo, long) - Número de hilos que tiene este proceso (mínimo 1).
  • itrealvalue (número largo, long) - No se usa desde el kernel 2.6.17, ahora siempre vale 0.
  • start_time (número natura largo largo, unsigned long long) - Momento en el que el proceso empezó. Se mide en ticks de reloj desde el arranque del equipo.
  • vsize (número natural largo, unsigned long) - Tamaño ocupado en memoria virtual en bytes.
  • rss (número largo, long) - Páginas que tiene el proceso en memoria RAM actualmente.
  • rsslim (número natural largo, unsigned long) - Límite en bytes del rss del proceso.
  • startcode (número largo, long) - Dirección de memoria donde empieza el código del programa.
  • endcode (número largo, long) - Dirección de memoria donde acaba el código del programa.
  • startstack (número largo, long) - Dirección de memoria donde empieza la pila (pero como la pila va al revés, será la parte de abajo de la pila.
  • kstkesp (número largo, long) - Posición actual del puntero de pila.
  • kstkeip (número largo, long) - Posición actual del puntero de instrucciones.
  • signal (número largo, long) - Obsoleto, se usa información procedente de /proc/PID/status.
  • blocked (número largo, long) - Obsoleto, se usa información procedente de /proc/PID/status.
  • sigignore (número largo, long) - Obsoleto, se usa información procedente de /proc/PID/status.
  • sigcatch (número largo, long) - Obsoleto, se usa información procedente de /proc/PID/status.
  • wchan (número natural largo, unsigned long) - Canal de espera dentro del kernel.
  • nswap (número natural largo, unsigned long) - Número de páginas en memoria swap.
  • cnswap (número natural largo, unsigned long) - Número de páginas en memoria swap de los procesos hijo.
  • exit_signal (número, int) - Señal que se envierá al padre cuando finalice este proceso.
  • processor (número, int) - ID de CPU donde se ejecutó este proceso por última vez.
  • rt_priority (número sin signo, unsigned) - Un número entre 1 y 99 si es un proceso de tiempo real, 0 si no.
  • policy (número sin signo, unsigned) - Política de planificación.
  • delayacct_blkio_ticks (número natural largo largo, unsigned long long) - Retrasos añadidos en ticks de reloj.
  • guest_time (número natural largo, unsigned long) - Tiempo invertido corriendo una CPU virtual.
  • cguest_time (número largo, long) - Tiempo invertido corriendo una CPU virtual por los procesos hijo.
  • start_data (número natural largo, unsigned long) - Dirección de memoria donde empieza el área de datos.
  • end_data (número natural largo, unsigned long) - Dirección de memoria donde acaba el área de datos.
  • start_brk (número natural largo, unsigned long) - Dirección de posible expansión del segmento de datos.
  • arg_start (número natural largo, unsigned long) - Dirección de memoria donde se encuentran los argumentos del programa (argv).
  • arg_end (número natural largo, unsigned long) - Dirección de memoria donde terminan los argumentos del programa (argv).
  • env_start (número natural largo, unsigned long) - Dirección donde empiezan las variables de entorno del programa.
  • env_end (número natural largo, unsigned long) - Dirección donde terminan las variables de entorno del programa.
  • exit_code (número, int) - Código de salida del thread.

Tenemos mucha información que seguramente no necesitemos, y tenemos que leerla. Afortunadamente, casi todo son números y en C podemos hacer algo rápido con scanf.

Obteniendo datos de un proceso en C

El código es algo largo por la cantidad de datos que leo, y eso que sólo leo hasta el rss y presento en pantalla algunos datos menos. Sólo quiero que tengamos una primera aproximación. Debemos tener en cuenta que aunque accedamos a la información como si fueran ficheros, en realidad no tienen que ver nada con el disco. Ni están en disco, ni gastarán tiempo de entrada/salida. Todo debería ser muy rápido porque al final estamos haciendo un par de llamadas al sistema operativo. Para el ejemplo he utilizado fscanf, porque es muy sencillo hacer la lectura y el parseo, aunque habrá algún que otro detalle (o mejor dicho, problema).

Por otro lado, dado lo pequeños que son los archivos también podemos almacenarlos por completo en un buffer y leer de nuestro buffer por si queremos utilizar otro método de lectura y parseo.

El resultado será algo como:


Sí, el tamaño es ese, firefox ahora mismo me está cogiendo 5Gb de memoria virtual (seguro que dentro de 5 años leeremos esto y parecerá utópico).

Esta información está muy bien, pero no me dice mucho. Por un lado, el tiempo de usuario y kernel tenemos que traducirlo y hacer algo con él, por ejemplo, calcular el % de CPU utilizado, poner la fecha de inicio y el tamaño en formato humano y vamos a quitar los paréntesis al nombre del proceso añadiendo algo más de control de errores, aunque esto último no es estrictamente necesario).

Vaya, ¡qué montón de código! Aunque en parte se parece al anterior. En principio se han añadido funciones para representar el tamaño en memoria en formato humano, para representar fechas e intervalos de manera legible y para calcular el tiempo que lleva encendido el ordenador, o uptime, necesario para algunos cálculos que veremos más adelante.

Veamos el resultado de la ejecución de este programa, con firefox, como antes:

Iniciado el: 19/07/2017 21:28:00

Una pequeña consideración sobre los tiempos de usuario y kernel, así como del tiempo total que lleva el proceso. Por un lado, el uptime del proceso, el tiempo que lleva arrancado es la hora actual menos la hora a la que arrancó el proceso. Es decir, si cargué el programa hace 10 minutos, el programa lleva 10 minutos encendido (es una tontería decirlo así, pero se puede confundir más adelante). Por otro lado tenemos los tiempos de kernel y de usuario, que es el tiempo total que el proceso ha hecho algo realmente; el tiempo de usuario podemos considerarlo computación propia del proceso, cuando está parseando un texto, creando ventanas, botones, ejecutando Javascript, etc; en otro plano tenemos el tiempo de kernel, que es el tiempo que el núcleo del sistema operativo ha gastado en el proceso; es decir, el proceso pedirá cosas al sistema operativo, como la lectura de un archivo, acceso a dispositivos, etc.

Eso sí, puede (y es lo más normal) que la suma de los tiempos de kernel y usuario no sea ni por asomo el uptime del proceso. Y está bien, eso es que el proceso ha habido momentos que no ha estado haciendo nada (ha estado en modo S leeping) y no ha necesitado CPU.

Y veamos los cálculos que se han hecho:

  • Para expresar el tiempo de usuario en segundos, tenemos que dividirlo por los ticks de reloj por segundo. Suelen ser 100 en la mayoría de los sistemas, es una configuración del kernel, pero podemos averiguarlo consultando sysconf (_SC_CLK_TCK). Luego ese intervalo lo transformaremos en horas:minutos:segundos.
  • Para saber cuánto hace que se inició el proceso. Utilizaremos start_time, pero claro, este valor indica cuántos ticks pasaron desde que se arrancó el ordenador hasta que se inició el proceso. Por un lado sabemos transformar de ticks a segundos. Así que restaremos al uptime actual el start_time en segundos.
  • Para saber el momento en el que se inició el proceso. Teniendo claro el punto anterior, éste es fácil. Sólo que tenemos que saber la fecha y hora actuales, que las sacamos en formato UNIX, así que le restamos el uptime y luego le sumamos el start_time...
  • Porcentaje de CPU. ¡Cuidado! Es el porcentaje de CPU total del proceso durante toda su vida. Consideraremos el porcentaje de CPU como la relación entre el tiempo que ha estado el proceso utilizando la CPU y el tiempo total que lleva el proceso arrancado. Es decir, si el proceso lleva arrancado 10 minutos y la suma de tiempos de usuario y kernel es de 2 minutos. El proceso ha consumido un 20% de CPU. Eso sí, puede ser que la suma de los tiempos de kernel y usuario sea mayor que el tiempo que lleva el proceso arrancado. Esto sucederá en aplicaciones multihilo, ya que los tiempos de kernel y usuario están calculados para un sólo núcleo de CPU. Si el proceso ha utilizado 2 núcleos al 100% durante todo el tiempo de vida del proceso, la suma de los tiempos anteriormente comentados será el doble.

No me convence mucho el % de CPU...

Normal, a mí tampoco. Hasta ahora lo hemos calculado de forma absoluta. Es decir, desde que se inició el proceso hasta ahora. Claro, puede que cuando arrancó el proceso utilizara ocho núcleos durante un minuto (8 minutos de tiempo de kernel+usuario) y haya estado 9 minutos casi sin hacer nada. Si ejecutamos top ni vemos el proceso, pero mi aplicación piensa que el proceso se come un 80% de CPU.

Para hacer el cálculo más preciso debemos centrarnos en un intervalo de tiempo. Así, calcularemos los tiempos de kernel+usuario en un momento del tiempo, esperaremos un par de segundos, y luego calculamos de nuevo, hacemos la diferencia y dividimos por el intervalo. Así que en lugar de sacar el porcentaje de CPU desde el principio, calcularemos el porcentaje de CPU para el intervalo que acaba de suceder. De esta forma tendremos una medida más precisa, y si os fijáis, es lo que hace top, espera un tiempo y vuelve a hacer el cálculo.
Para ello tendremos que volver a leer el archivo y volver a parsear stat, por eso en el siguiente ejemplo vais a ver un struct con toda la información y una función que la extrae (un paso más en el programa):

Línea de comandos y variables de entorno

¿Qué argumentos se han pasado al programa cuando se ejecutó? Si el programa está hecho en C, éste utilizará los famosos argc y argv para acceder a ellos. Pero, ¿ un programa externo podrá acceder a esta información? Es lo que nos muestra ps cuando lo llamamos así (centrados en el PID que estamos consultando en este post):

ps ax -o pid,cmd | grep 4971

24416 grep --color=auto 4971


Como vemos, yo ejecuté firefox con el argumento-ProfileManager, ¿cómo podemos consultarlo? Esta información está en /proc/PID/cmdline y encontraremos los argumentos separados por el carácter terminador, para que esta información sea mucho más fácil de manejar y procesar internamente. Veamos un ejemplo:

Si lo ejecutamos, veremos algo parecido a esto:

Argumento: /usr/lib/firefox/firefox

Argumento: -ProfileManager

Argumento: /usr/sbin/dnsmasq

Argumento: --keep-in-foreground

Argumento: --bind-interfaces

Argumento: --listen-address=127.0.1.1

Argumento: --cache-size=0

Argumento: --conf-file=/dev/null

Argumento: --proxy-dnssec


Como siempre, el primer argumento coincide con el ejecutable del programa y los siguientes serán los argumentos que hemos pasado para modificar su comportamiento. En este ejemplo los imprimimos directamente en pantalla, pero ya está en vuestra mano otro tipo de análisis: almacenarlo en arrays o en listas enlazadas, buscar un argumento en particular, mirar si se ha colado un password (que hay programadores a los que se les pasa esto...), o lo que queráis.

Del mismo modo, pero cambiando el archivo por /proc/PID/environ podemos extraer el valor de las variables de entorno del programa. Y puede haber información muy interesante acerca de la ejecución bajo un entorno de escritorio, sesión SSH (si se ejecutó en remoto), idioma, directorios, informes de errores y demás. Incluso muchos programas, para ejecutarlos utilizan un script lanzador que establece los valores de ciertas variables antes de lanzar la ejecución del binario y lo podemos ver aquí.

Más información de estado del proceso

¿Queremos más información del proceso? Pues miremos /proc/pid/status. Muy parecido a /proc/pid/stat, con la información en un lenguaje más inteligible, incluso con algunos datos más que pueden resultar interesantes como por ejemplo:

  • Cpus_allowed, Cpus_allowd_list: Para saber las CPUs pueden ejecutar código de este proceso
  • voluntary_ctxt_switches y nonvoluntary_ctxt_switches: Cambios de contexto voluntarios e involuntarios
  • Sig*: información sobre señales (que /proc/PID/stat no nos la daba muy bien.
  • Información sobre memoria tanto residente como virtual. Echad un vistazo al fichero para ver el montón de elementos que podemos consultar.

Otros ficheros de interés

Podremos consultar muchas más cosas del proceso. Basta con hacer ls /proc/PID aunque para ahorrar trabajo os dejo aquí algunos de los más interesantes (también tenemos que tener en cuenta que a medida que salen versiones nuevas del kernel Linux podremos ver más información):

  • /proc/PID/maps : Mapa de memoria. Ahí podemos ver rangos de memoria, tipo de acceso (lectura, escritura, ejecución y si es pública o privada) y si hay un fichero mapeado en esa dirección, o estamos en memoria de datos, o es pila, etc. Si estás estudiando Sistemas Operativos es recomendable echarle un ojo a uno de estos archivos, eso sí, empezad por una aplicación pequeña, porque un firefox, o un libreoffice puede ser muy heavy de analizar.
  • /proc/PID/oom_score : Esto tiene que ver con el Out Of Memory Killer. Mirad este post.
  • /proc/PID/mounts : Dispositivos o discos montados de cara a la aplicación.
  • /proc/PID/limits : Límite de memoria, procesos, ficheros abiertos, bloqueos, tiempo de CPU y más que tiene este proceso.
  • /proc/PID/exe : Éste es el ejecutable que se ha cargado. ¡Éste y no otro! Por si alguien nos intenta engañar ejecutando un ejecutable que no es, que está en otra ruta, es otra versión. Aquí tenemos el ejecutable que podremos leer con:

En definitiva, encontramos mucha información para analizar los procesos en ejecución. Y, si tienes un proyecto al respecto, déjamelo en los comentarios, que lo enlazaré encantado 🙂

Buscar todos los procesos

Por último, un pequeño código de ejemplo para buscar los PID de todos los procesos en ejecución, y poder analizarlos como hace ps, o top. O incluso para que nosotros hagamos alguna clase de análisis de procesos en tiempo real.
La clave está en listar archivos, tal y como lo haría ls. Sólo que dentro de proc y quedarnos con los que tienen aspecto numérico. En mi caso confío un poco en Linux y espero que si un fichero dentro de proc empieza por un número va a ser un PID. Obviamente en proc no suele haber cosas raras, aunque no podemos descartar que algún módulo del kernel pueda hacer de las suyas y tengamos que verificar que todos los caracteres son numéricos y que se trata de un directorio e incluso buscar la existencia de ciertos archivos dentro:

Una biblioteca en C++ que tiene muchas cosas ya hechas

Después de toda esta guía, voy con el autobombo. Hace tiempo hice un pequeño archivo .h para C++ moderno que entre otras cosas, analiza procesos y guarda las cosas en estructuras muy apañadas para la ocasión. Encontramos el código en GitHub. Con un ejemplo listo para compilar que aclara muchas cosas.
Foto principal: Daryan Shamkhali

También podría interesarte...


Volver a la Portada de Logo Paperblog