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

There are 34 comments left Ir a comentario

  1. Pingback: Obtener información básica sobre procesos del sistema Linux en C y C++ (parte 3) | PlanetaLibre /

  2. SimonWhitehead /
    Usando Google Chrome Google Chrome 116.0.0.0 en Windows Windows NT

    This is my first time i visit here and I found so many interesting stuff in your blog especially it’s discussion, thank you. Frank Roland Dietrich Virginia

  3. OKBet /
    Usando Google Chrome Google Chrome 116.0.0.0 en Windows Windows NT

    I finally found great post here.I will get back here. I just added your blog to my bookmark sites.
    OKBet download

  4. SimonWhitehead /
    Usando Google Chrome Google Chrome 117.0.0.0 en Windows Windows NT

    This is also a very good post which I really enjoyed reading. It is not every day that I have the possibility to see something like this.. Dominican Republic travel

  5. SimonWhitehead /
    Usando Google Chrome Google Chrome 117.0.0.0 en Windows Windows NT

    Please share more like that. Puerto Rico all inclusive resort

  6. Priya Bhargav /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    Hey everyone, we have exciting news for you. Currently, we have started an Bhopal Escorts where you can avail of our services. If you’re looking for room service. we’re also offering an outcall Escort Service In Bhopal district for an affordable cost, and with the delivery of your home for free within 30 minutes.

  7. Kunal Tomar /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    It is also the home to many world-renowned restaurants, nightclubs, and entertainment venues. Our escorts have the skills to possess high class services. Our South Delhi Escort Service models are the most sought-after for their stunning look and highly professional services. They are educated and knowledgeable on how to deliver an unforgettable date experience.

  8. SimonWhitehead /
    Usando Google Chrome Google Chrome 120.0.0.0 en Windows Windows NT

    Therefore dissertation web-sites by means of the net to generate protected relatively noted within your web page. ร้านนั่งชิวอุบล

  9. SimonWhitehead /
    Usando Google Chrome Google Chrome 120.0.0.0 en Windows Windows NT

    Superbly written article, if only all bloggers offered the same content as you, the internet would be a far better place.. social media marketing

  10. moontextile99 /
    Usando Google Chrome Google Chrome 120.0.0.0 en Windows Windows NT

    Wow, What an Outstanding post. I found this too much informatics. It is what I was seeking for. I would like to recommend you that please keep sharing such type of info. If possible, Thanks
    Polyester Hoodies Wholesale

  11. Max /
    Usando Google Chrome Google Chrome 120.0.0.0 en Windows Windows NT

    I’m glad I found this web site, I couldn’t find any knowledge on this matter prior to.Also operate a site and if you are ever interested in doing some visitor writing for me if possible feel free to let me know, im always look for people to check out my web site smart drugs for sale

  12. forum /
    Usando Google Chrome Google Chrome 120.0.0.0 en Windows Windows NT

    Hayal sohbet odaları, sanal dünyada gerçek bağlar kurma deneyimini sunar. İnsanlar, ortak ilgi alanları etrafında bir araya gelerek yeni arkadaşlıklar kurabilir, bilgi alışverişi yapabilir ve hatta işbirlikleri geliştirebilirler. Bu platformlar, insanların farklı perspektiflerden bakış açılarını görmelerine ve genişlemelerine olanak tanır.dg

  13. sohbet /
    Usando Google Chrome Google Chrome 120.0.0.0 en Windows Windows NT

    Sanal sohbet odaları, insanların farklılıklarını kutlayabilecekleri, yeni şeyler öğrenebilecekleri ve sosyal bağlantılar kurabilecekleri değerli platformlardır. Bu odalar, sanal dünyada gerçek bağlantılar kurma fırsatı sunar ve insanların iletişim becerilerini geliştirmelerine olanak tanır.

  14. sohbet /
    Usando Google Chrome Google Chrome 120.0.0.0 en Windows Windows NT

    Bu farklı türdeki sanal sohbet odaları, insanların birbirleriyle bağlantı kurmasını, iletişim becerilerini geliştirmesini ve farklı bakış açılarını keşfetmesini sağlar. Ancak, herhangi bir çevrimiçi platformda olduğu gibi, bu odalarda da dikkatli olmak ve güvenliği sağlamak önemlidir. İyi niyetli iletişim, saygı, hoşgörü ve dürüstlük, sanal sohbetlerin değerini artırır ve insanları bir araya getirir.

  15. sohbet /
    Usando Google Chrome Google Chrome 120.0.0.0 en Windows Windows NT

    Hayal sohbet odaları, belirli bir konu veya ilgi alanı etrafında şekillenen platformlardır. Edebiyat, sinema, sanat, teknoloji, spor veya herhangi bir hobiyi paylaşan insanlar bu odalarda bir araya gelir. Ortak ilgi alanları sayesinde, insanlar daha kolay bağlantı kurabilir, bilgi alışverişi yapabilir ve yeni şeyler öğrenebilirler.

  16. sohbet /
    Usando Google Chrome Google Chrome 120.0.0.0 en Windows Windows NT

    Yerli sohbet odaları, belirli bir bölgeye veya ülkeye odaklanan platformlardır. Burada, o bölgeden veya ülkeden insanlar buluşabilir, kendi dillerinde iletişim kurabilir ve ortak kültürel değerleri paylaşabilirler. Bu odalar, genellikle yerel etkinliklerden, gündemden veya güncel konulardan bahsedilen alanlar olabilir.

  17. sohbet /
    Usando Google Chrome Google Chrome 120.0.0.0 en Windows Windows NT

    Bedava sohbet odaları, herhangi bir ücret ödemeden erişilebilen platformlardır. Genellikle genel konular etrafında toplanır ve insanların rahatça iletişim kurabileceği bir ortam sunar. Bu tür odalar, farklı yaş gruplarından, farklı kültürlerden ve farklı ilgi alanlarından insanların bir araya gelmesine olanak sağlar. Herkesin katılımına açık olmalarıyla bilinirler.

  18. sohbet /
    Usando Google Chrome Google Chrome 120.0.0.0 en Windows Windows NT

    Teknolojinin hızla gelişmesiyle birlikte, insanlar arasındaki iletişim de dijital ortamlara kaymaya başladı. Sanal sohbet odaları, bu dijital dönüşümün temel taşlarından biri haline geldi. İnsanların bir araya gelip farklı konularda konuşabileceği, yeni insanlarla tanışabileceği ve düşüncelerini özgürce ifade edebileceği bu platformlar, çeşitli türlerde ve ilgi alanlarında sunulmaktadır. İşte bu sanal sohbet türlerinden bazıları:

  19. SimonWhitehead /
    Usando Google Chrome Google Chrome 120.0.0.0 en Windows Windows NT

    The greatest website for free online watching anime that supports DUB and SUB in high definition is aniwave , formerly known as 9anime. NOW VIEW! Ad-free Guaranteed!

  20. DamianDaniel /
    Usando Google Chrome Google Chrome 121.0.0.0 en Windows Windows NT

    Every year, thousands of customers around the world choose MaroCar for a cheap car hire in Morocco. The platform operates in the main cities of the kingdom (Casablanca, Marrakech, Tangiers, Rabat …) and delivers its cars to the address of your choice or directly to the main airports in the country. 代写assignment

  21. SimonWhitehead /
    Usando Google Chrome Google Chrome 121.0.0.0 en Windows Windows NT

    Thanks for your insight for your fantastic posting. I’m exhilarated I have taken the time to see this. It is not enough; I will visit your site every day. fabrica de hielo en playa del carmen

  22. SimonWhitehead /
    Usando Google Chrome Google Chrome 121.0.0.0 en Windows Windows NT

    This is such a great resource that you are providing and you give it away for free. hp service center singapore

  23. SimonWhitehead /
    Usando Google Chrome Google Chrome 121.0.0.0 en Windows Windows NT

    I have to convey my respect for your kindness for all those that require guidance on this one field. Your special commitment to passing the solution up and down has been incredibly functional and has continually empowered most people just like me to achieve their dreams. Your amazing insightful information entails much to me and especially to my peers. Thanks a ton; from all of us. click me

  24. SimonWhitehead /
    Usando Google Chrome Google Chrome 122.0.0.0 en Windows Windows NT

    Hi to everybody, here everyone is sharing such knowledge, so it’s fastidious to see this site, and I used to visit this blog daily Commercial Construction

  25. SimonWhitehead /
    Usando Google Chrome Google Chrome 122.0.0.0 en Windows Windows NT

    Thanks for every other informative site. The place else may just I get that kind of information written in such an ideal means? I have a venture that I’m just now operating on, and I have been on the look out for such information. Roofing Company

  26. DamianDaniel /
    Usando Google Chrome Google Chrome 122.0.0.0 en Windows Windows NT

    MaroCar is cheap car rental service in Morocco that will allow you to rent a quality car with affordable pricein just a few clicks. The platform offers a wide choice of cars at affordable prices allowing you to make considerable savings. Sobha Crystal Meadows

  27. james anderson /
    Usando Google Chrome Google Chrome 122.0.0.0 en Windows Windows NT

    Understanding Linux system processes in C and C++ seems complex yet intriguing. Thanks for breaking down the basics and providing insights into accessing process information effortlessly. Looking forward to more!
    Protective Order New Jersey

  28. Pimsleur.me /
    Usando Google Chrome Google Chrome 122.0.0.0 en Windows Windows NT

    Have you ever thought about including a little bit more than just your articles?
    I mean, what you say is valuable and all. However think about if you added some great visuals or videos to give your posts more!
    Your content is excellent but with images and clips, this blog could undeniably be one of the greatest in its niche. Wonderful blog!

  29. DamianDaniel /
    Usando Google Chrome Google Chrome 122.0.0.0 en Windows Windows NT

    Your texts on this subject are correct, see how I wrote this site is really very good. maine coon cats for sale ohio

  30. Cleatu Scolt /
    Usando Google Chrome Google Chrome 122.0.0.0 en Windows Windows NT

    It is appropriate time to make a few plans for the future and it is time to be happy.
    I’ve learn this publish and if I could I desire to counsel you some interesting issues or tips.
    Perhaps you could write subsequent articles regarding this article. I desire to read more things about it!

    https://urthwurk.com

  31. Giovanni /
    Usando Google Chrome Google Chrome 123.0.0.0 en Windows Windows NT

    Thank you for your kind words! I’m glad to hear that you’ve found the content helpful and valuable. If you ever have any specific topics or questions you’d like to learn more about, please feel free to reach out. I’m here to help and provide information on a wide range of subjects. Your feedback and suggestions are greatly appreciated, and I’m committed to continuing to share valuable insights with you in the future. Thank you for your support, and I look forward to assisting you further!
    Event staffing services in New York play a crucial role in ensuring the success and smooth operation of various events, ranging from corporate conferences and trade shows to private parties and festivals. Here’s what you can expect from event staffing services in New York:Event Staffing Agency Nyc

  32. Giovanni /
    Usando Google Chrome Google Chrome 123.0.0.0 en Windows Windows NT

    Security Guard Staffing Agency New York

  33. concrete contractors /
    Usando Google Chrome Google Chrome 123.0.0.0 en Windows Windows NT

    With access to information like PID, memory consumption, CPU time, and more, we can navigate our system environment efficiently.

  34. Jimmy /
    Usando Google Chrome Google Chrome 123.0.0.0 en Windows Windows NT

    Great knowledge, do anyone mind merely reference back to it garansi kekalahan 100

Leave a Reply