Publi

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

procesos del sistema

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
32768

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):

pidof emacs
23406 10997 7345
cat /proc/23406/stat
23406 (emacs) S 30548 23406 30548 34835 30548 4194304 53536 589 224 13 1762 284 0 0 20 0 4 0 161959260 731086848 49501 18446744073709551615 4194304 6539236 140735377727232 140735377722352 139924333532780 0 0 67112960 1535209215 0 0 0 17 1 0 0 52 0 0 8637344 21454848 51675136 140735377731945 140735377731964 140735377731964 140735377735657 0

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <unistd.h>
#include <stdio.h>

int main(int argc, char* argv[])
{
    if (argc<2)
        return perror("Falta un argumento"), 1;

    char statFileName[128];             /* /proc/PIC/stat - I think 512 bytes is far enough */
   
    sprintf(statFileName, "/proc/%s/stat", argv[1]);
    /* Podíamos comprobar que argv[1] es numérico y que es de poco
       tamaño, pero para el ejemplo nos vale. */

    FILE *fd = fopen(statFileName, "r");
    if (fd == NULL)
        return perror("No puedo encontrar el proceso especificado"),1;
    char
    state,
      name[32];
    int
      pid,
      ppid,
      pgrp,
      session,
      tty,
      tpgid,
      nlwp;

    unsigned long
    flags,
      min_flt,
      cmin_flt,
      maj_flt,
      cmaj_flt,
      vsize;

    unsigned long long
    utime,
      stime,
      cutime,
      cstime,
      start_time;

    long
    priority,
      nice,
      alarm,
      rss;
   
    fscanf(fd, "%d %s "
                 "%c "
                 "%d %d %d %d %d "
                 "%lu %lu %lu %lu %lu "
                 "%Lu %Lu %Lu %Lu "
                 "%ld %ld "
                 "%d "
                 "%ld "
                 "%Lu "
                 "%lu "
                 "%ld",
                 &pid,
                 name,
                 &state,
                 &ppid, &pgrp, &session, &tty, &tpgid,
                 &flags, &min_flt, &cmin_flt, &maj_flt, &cmaj_flt,
                 &utime, &stime, &cutime, &cstime,
                 &priority, &nice,
                 &nlwp,
                 &alarm,
                 &start_time,
                 &vsize,
                 &rss);
   
    fclose(fd);

    printf ("PID: %d\n"
                    "CMD: %s\n"
                    "Estado: %c\n"
                    "PPID: %d\n"
                    "Tiempo usuario: %Lu\n"
                    "Tiempo kernel: %Lu\n"
                    "Nice: %ld\n"
                    "Threads: %d\n"
                    "Iniciado en: %Lu\n"
                    "Tamaño: %lu\n",
                    pid, name, state, ppid, utime, stime, nice, nlwp, start_time, vsize);
}

El resultado será algo como:

./procesos 4971
PID: 4971
CMD: (firefox)
Estado: S
PPID: 4776
Tiempo usuario: 642512
Tiempo kernel: 41987
Nice: 0
Threads: 60
Iniciado en: 23836769
Tamaño: 5043314688

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).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <sys/sysinfo.h>

/* Presenta un intervalo de tiempo (en segundos) en horas:minutos:segundos */
char* timeInterval(char* buffer, size_t bufferSize, unsigned long seconds);
/* Presenta la fecha en formato humano */
char* humanSize(char* buffer, size_t bufferSize, long double size, short precission);
/* Error fatal! */
void panic(char* error);
/* Obtiene uptime del sistema */
long uptime();

int main(int argc, char* argv[])
{
    if (argc<2)
        panic("Falta un argumento");

    char statFileName[128];             /* /proc/PIC/stat - I think 512 bytes is far enough */
   
    sprintf(statFileName, "/proc/%s/stat", argv[1]);
    /* Podíamos comprobar que argv[1] es numérico y que es de poco
       tamaño, pero para el ejemplo nos vale. */

    FILE *fd = fopen(statFileName, "r");
    if (fd == NULL)
        panic("No puedo encontrar el proceso especificado");
   
    char
    state,
      name[32];
    int
      pid,
      ppid,
      pgrp,
      session,
      tty,
      tpgid,
      nlwp;
    double
        pcpu;

    unsigned long
    flags,
      min_flt,
      cmin_flt,
      maj_flt,
      cmaj_flt,
      vsize;

    unsigned long long
    utime,
      stime,
      cutime,
      cstime,
      start_time;

    long
    priority,
      nice,
      alarm,
      rss;

    char buffer[512];
    fgets(buffer, 512, fd);
    char* cstart=strchr(buffer, '('); /* Cogemos el primer ( de la cadena */
    char* cend  =strrchr(buffer, ')'); /* Cogemos el último ) de la cadena */
    strncpy(name, cstart+1, (cend-cstart<33)?cend-cstart-1:32); /* Necesitamos delimitar el nombre a 32 caracteres (por el tamaño de nuestro buffer */
    if ( (cstart == NULL) || (cend == NULL) )
        panic("No se pudo determinar el nombre del proceso");
   
    sscanf(buffer, "%d", &pid);
    sscanf(cend+2, "%c "                    /* +2 para eliminar el ) y el espacio siguientes */
                 "%d %d %d %d %d "
                 "%lu %lu %lu %lu %lu "
                 "%Lu %Lu %Lu %Lu "
                 "%ld %ld "
                 "%d "
                 "%ld "
                 "%Lu "
                 "%lu "
                 "%ld",
                 &state,
                 &ppid, &pgrp, &session, &tty, &tpgid,
                 &flags, &min_flt, &cmin_flt, &maj_flt, &cmaj_flt,
                 &utime, &stime, &cutime, &cstime,
                 &priority, &nice,
                 &nlwp,
                 &alarm,
                 &start_time,
                 &vsize,
                 &rss);
   
    fclose(fd);

    long ticks_sec = sysconf (_SC_CLK_TCK); /* Ticks de reloj por segundo */
    char utimeStr[32], stimeStr[32], start_timeStr[32], vsizeStr[32], startedStr[32];

    /* Calculamos cuándo se inició el proceso */
    struct tm starttm;
    long now = time(NULL);
    time_t started = now - uptime() + start_time/ticks_sec;
    strftime (startedStr, 32, "%d/%m/%Y %H:%M:%S", localtime_r(&started, &starttm));
    /* Como stat nos da segundos*ticks_sec desde que se arrancó el ordenador tendremos
         que operar con el uptime del ordenador y con la fecha y hora actual para
         averiguarlo. */


    unsigned long long procuptime = uptime() - start_time / ticks_sec;
    unsigned long long total_time =  utime + stime;     /* Tiempo total en ejecución. */
    pcpu = (double)total_time / procuptime;
   
    printf ("PID: %d\n"
                    "CMD: %s\n"
                    "Estado: %c\n"
                    "PPID: %d\n"
                    "Tiempo usuario: %s\n"
                    "Tiempo kernel: %s\n"
                    "Nice: %ld\n"
                    "Threads: %d\n"
                    "Iniciado hace: %s\n"
                    "Iniciado el: %s\n"
                    "Tamaño: %s\n"
                    "%% CPU: %lf\n",
                    pid, name, state, ppid,
                    timeInterval(utimeStr, 32, utime/ticks_sec),
                    timeInterval(stimeStr, 32, stime/ticks_sec),
                    nice, nlwp,
                    timeInterval(start_timeStr, 32, uptime() - start_time/ticks_sec),
                    startedStr,
                    humanSize(vsizeStr, 32, vsize, -1),
                    pcpu);
}

char* timeInterval(char* buffer, size_t bufferSize, unsigned long seconds)
{
    int hours = seconds / 3600,
        rem = seconds % 3600,
        minutes = rem / 60,
        secs = rem % 60;
   
    snprintf (buffer, bufferSize, "%d:%d:%d", hours, minutes, secs);
   
    return buffer;
}

char* humanSize(char* buffer, size_t bufferSize, long double size, short precission)
{
    static const char* units[10]={"bytes","Kb","Mb","Gb","Tb","Pb","Eb","Zb","Yb","Bb"};

    char format[10];

    int i= 0;

    while (size>1024) {
        size = size /1024;
        i++;
    }

    if (precission < 0)
        precission=3;

    snprintf(format, 10, "%%.%dLf%%s", precission);
    snprintf(buffer, bufferSize, format, size, units[i]);

    return buffer;
}

long uptime()
{
    struct sysinfo si;
    if (sysinfo(&si) <0)
        panic("No puedo obtener sysinfo()");
   
    long tmp = si.uptime;
    return tmp;
}

void panic(char* error)
{
    fprintf (stderr, "Error: %s (%d - %s)\n", error, errno, strerror(errno));
    exit(-1);
}

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:

./procesos 4971
PID: 4971
CMD: firefox
Estado: S
PPID: 4776
Tiempo usuario: 11:37:0
Tiempo kernel: 0:32:32
Nice: 0
Threads: 63
Iniciado hace: 24:6:45
Iniciado el: 19/07/2017 21:28:00
Tamaño: 5.088Gb
% CPU: 50.000000

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 Sleeping) 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):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <sys/sysinfo.h>

struct ProcessInfo
{
        char
    state,
      name[32];
    int
      pid,
      ppid,
      pgrp,
      session,
      tty,
      tpgid,
      nlwp;

    unsigned long
    flags,
      min_flt,
      cmin_flt,
      maj_flt,
      cmaj_flt,
      vsize;

    unsigned long long
    utime,
      stime,
      cutime,
      cstime,
      start_time;

    long
    priority,
      nice,
      alarm,
      rss;
};
/* Presenta un intervalo de tiempo (en segundos) en horas:minutos:segundos */
char* timeInterval(char* buffer, size_t bufferSize, unsigned long seconds);
/* Presenta la fecha en formato humano */
char* humanSize(char* buffer, size_t bufferSize, long double size, short precission);
/* Error fatal! */
void panic(char* error);
/* Obtiene uptime del sistema */
long uptime();

struct ProcessInfo getProcessInfo(int pid);

int main(int argc, char* argv[])
{
    if (argc<2)
        panic("Falta un argumento");

    double pcpu;
   
    struct ProcessInfo pi1 = getProcessInfo(atoi(argv[1]));

    long ticks_sec = sysconf (_SC_CLK_TCK); /* Ticks de reloj por segundo */
    char utimeStr[32], stimeStr[32], start_timeStr[32], vsizeStr[32], startedStr[32];

    /* Calculamos cuándo se inició el proceso */
    struct tm starttm;
    long now = time(NULL);
    time_t started = now - uptime() + pi1.start_time/ticks_sec;
    strftime (startedStr, 32, "%d/%m/%Y %H:%M:%S", localtime_r(&started, &starttm));
    /* Como stat nos da segundos*ticks_sec desde que se arrancó el ordenador tendremos
         que operar con el uptime del ordenador y con la fecha y hora actual para
         averiguarlo. */


    unsigned long long total_time_s =  pi1.utime + pi1.stime;   /* Tiempo total en ejecución. */
    sleep(3);
    struct ProcessInfo pi2 = getProcessInfo(atoi(argv[1]));
    unsigned long long total_time_e =  pi2.utime + pi2.stime;   /* Tiempo total en ejecución. */
   
    pcpu = (double)(total_time_e - total_time_s) / 3;
   
    printf ("PID: %d\n"
                    "CMD: %s\n"
                    "Estado: %c\n"
                    "PPID: %d\n"
                    "Tiempo usuario: %s\n"
                    "Tiempo kernel: %s\n"
                    "Nice: %ld\n"
                    "Threads: %d\n"
                    "Iniciado hace: %s\n"
                    "Iniciado el: %s\n"
                    "Tamaño: %s\n"
                    "%% CPU: %lf\n",
                    pi1.pid, pi1.name, pi1.state, pi1.ppid,
                    timeInterval(utimeStr, 32, pi1.utime/ticks_sec),
                    timeInterval(stimeStr, 32, pi1.stime/ticks_sec),
                    pi1.nice, pi1.nlwp,
                    timeInterval(start_timeStr, 32, uptime() - pi1.start_time/ticks_sec),
                    startedStr,
                    humanSize(vsizeStr, 32, pi1.vsize, -1),
                    pcpu);
}

char* timeInterval(char* buffer, size_t bufferSize, unsigned long seconds)
{
    int hours = seconds / 3600,
        rem = seconds % 3600,
        minutes = rem / 60,
        secs = rem % 60;
   
    snprintf (buffer, bufferSize, "%d:%d:%d", hours, minutes, secs);
   
    return buffer;
}

char* humanSize(char* buffer, size_t bufferSize, long double size, short precission)
{
    static const char* units[10]={"bytes","Kb","Mb","Gb","Tb","Pb","Eb","Zb","Yb","Bb"};

    char format[10];

    int i= 0;

    while (size>1024) {
        size = size /1024;
        i++;
    }

    if (precission < 0)
        precission=3;

    snprintf(format, 10, "%%.%dLf%%s", precission);
    snprintf(buffer, bufferSize, format, size, units[i]);

    return buffer;
}

long uptime()
{
    struct sysinfo si;
    if (sysinfo(&si) <0)
        panic("No puedo obtener sysinfo()");
   
    long tmp = si.uptime;
    return tmp;
}

void panic(char* error)
{
    fprintf (stderr, "Error: %s (%d - %s)\n", error, errno, strerror(errno));
    exit(-1);
}

struct ProcessInfo getProcessInfo(int pid)
{
    char statFileName[128];             /* /proc/PIC/stat - I think 512 bytes is far enough */

    struct ProcessInfo pi;
   
    sprintf(statFileName, "/proc/%d/stat", pid);
    FILE *fd = fopen(statFileName, "r");
    if (fd == NULL)
        panic("No puedo encontrar el proceso especificado");
   

    char buffer[512];
    fgets(buffer, 512, fd);
    char* cstart=strchr(buffer, '('); /* Cogemos el primer ( de la cadena */
    char* cend  =strrchr(buffer, ')'); /* Cogemos el último ) de la cadena */
    size_t namesize = (cend-cstart<33)?cend-cstart-1:32;
    strncpy(pi.name, cstart+1, namesize); /* Necesitamos delimitar el nombre a 32 caracteres (por el tamaño de nuestro buffer */
    pi.name[namesize]='\0';
    if ( (cstart == NULL) || (cend == NULL) )
        panic("No se pudo determinar el nombre del proceso");
   
    sscanf(buffer, "%d", &pi.pid);
    sscanf(cend+2, "%c "                    /* +2 para eliminar el ) y el espacio siguientes */
                 "%d %d %d %d %d "
                 "%lu %lu %lu %lu %lu "
                 "%Lu %Lu %Lu %Lu "
                 "%ld %ld "
                 "%d "
                 "%ld "
                 "%Lu "
                 "%lu "
                 "%ld",
                 &pi.state,
                 &pi.ppid, &pi.pgrp, &pi.session, &pi.tty, &pi.tpgid,
                 &pi.flags, &pi.min_flt, &pi.cmin_flt, &pi.maj_flt, &pi.cmaj_flt,
                 &pi.utime, &pi.stime, &pi.cutime, &pi.cstime,
                 &pi.priority, &pi.nice,
                 &pi.nlwp,
                 &pi.alarm,
                 &pi.start_time,
                 &pi.vsize,
                 &pi.rss);
   
    fclose(fd);
    return pi;
}

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
4971 /usr/lib/firefox/firefox -ProfileManager
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>


/* Error fatal! */
void panic(char* error);

int main(int argc, char* argv[])
{
    if (argc<2)
        panic("Falta un argumento");

    char cmdlineFileName[128];              /* /proc/PID/cmdline - I think 128 bytes is far enough */

    sprintf(cmdlineFileName, "/proc/%s/cmdline", argv[1]);
    FILE *fd = fopen(cmdlineFileName, "r");
    if (fd == NULL)
        panic("No puedo encontrar el proceso especificado");
  char *arg = 0;
    size_t argsize = 0;
    while(getdelim(&arg, &argsize, '\0', fd) != -1)
        {
      printf ("Argumento: %s\n", arg);
        }
    if (arg)
        free(arg);

    fclose(fd);
}

void panic(char* error)
{
    fprintf (stderr, "Error: %s (%d - %s)\n", error, errno, strerror(errno));
    exit(-1);
}

Si lo ejecutamos, veremos algo parecido a esto:

./cmdline 4971
Argumento: /usr/lib/firefox/firefox
Argumento: -ProfileManager
./cmdline 2526
Argumento: /usr/sbin/dnsmasq
Argumento: --no-resolv
Argumento: --keep-in-foreground
Argumento: --no-hosts
Argumento: --bind-interfaces
Argumento: --pid-file=/var/run/NetworkManager/dnsmasq.pid
Argumento: --listen-address=127.0.1.1
Argumento: --cache-size=0
Argumento: --conf-file=/dev/null
Argumento: --proxy-dnssec
Argumento: --enable-dbus=org.freedesktop.NetworkManager.dnsmasq
Argumento: --conf-dir=/etc/NetworkManager/dnsmasq.d

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:
    readelf -a /proc/PID/exe

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <errno.h>
#include <ctype.h>
#include <dirent.h>
#include <fcntl.h>

/* Error fatal! */
void panic(char* error);

int main(int argc, char* argv[])
{
    DIR* proc_dir;
    struct dirent *ent;
    unsigned total = 0;
    proc_dir = opendir("/proc");
    while ((ent = readdir(proc_dir)))
      {
        if ((*ent->d_name>'0') && (*ent->d_name<='9')) /* Be sure it's a pid */
          {
        printf("%s\n", ent->d_name);
        ++total;
          }
      }
    closedir(proc_dir);
  printf ("Total de procesos: %u\n", total);
}


void panic(char* error)
{
    fprintf (stderr, "Error: %s (%d - %s)\n", error, errno, strerror(errno));
    exit(-1);
}

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...

Leave a Reply