Publi

Cómo obtener información de configuración del sistema Linux o tu servidor en C y C++ (II. Sistema y CPU)

Hace unos días comentábamos algunos ejemplos sobre obtención de información relativa a la memoria de nuestro sistema en C, tanto RAM, Swap, buffers, caché, como información relativa a una aplicación. También vimos cómo obtener el uptime de nuestro equipo o la carga media del sistema en 1, 5 y 15 minutos. Son cosas que podemos hacer fácilmente desde la línea de comandos y que pueden sernos muy útiles en nuestros programas. Por ejemplo para evaluar cuándo es el mejor momento para realizar una acción, realizar una limpieza, ajustar tamaños de nuestras reservas o detectar un posible error.

En el post de hoy veremos algunas utilidades para ver la configuración del sistema y con configuración me refiero a cosas propias del equipo en el que estamos ejecutando nuestro programa como el número de procesos que puede ejecutar un usuario, el número máximo de ficheros que pueden abrirse, número de cores o núcleos con los que cuenta el ordenador, cuáles de ellos están activos, características propias del sistema y muchas más cosas.

Configuración numérica del sistema: sysconf()

sysconf() es de esas funciones de unistd.h con las que podemos obtener gran cantidad de información. Es más, en este post no está todo lo que podéis obtener, sólo algunas cosas que me han parecido interesantes. Eso sí, si veis algo curioso que se me ha pasado, dejadme un comentario. Todas las posibilidades las podéis encontrar en el archivo /usr/include/unistd.h , tal vez las constantes no estén directamente ahí, pues depende de la plataforma y versión del sistema, pero es un punto de partida.
De forma general, sysconf() devuelve un long int y como entrada admite un int, aunque ese int, es mejor indicarlo con el valor de una constante, pues será mucho más significativo para nosotros y portable en caso de que cambiemos de arquitectura o versión de las bibliotecas.

Sin más dilación aquí va un listado de las constantes que podemos utilizar y la respuesta que esperamos:

  • _SC_ARG_MAX: Longitud máxima de los argumentos a la hora de ejecutar un comando.
  • _SC_CHILD_MAX: Número de procesos que puede lanzar un usuario determinado.
  • _SC_CLK_TCK: Ticks de reloj por segunto. Los sistemas operativos utilizan los ticks de reloj para medir el tiempo de los procesos que están corriendo, o para programar la realización de tareas (tiempo de vida de páginas de memoria o tiempos de espera de un recurso) o para tomar el control de forma síncrona. Un valor de 100 aquí, indica que cada 10ms se produce un tick de reloj.
  • _SC_LOGIN_NAME_MAX: Es el tamaño máximo del nombre de usuario para identificarnos en el sistema.
  • _SC_HOST_NAME_MAX: Tamaño máximo del hostname
  • _SC_OPEN_MAX: Número máximo de archivos abiertos por un proceso.
  • _SC_PAGESIZE: Tamaño de página de memoria (igual que utilizar getpagesize())
  • _SC_PHYS_PAGES: Número de páginas físicas en memoria. (Igual que utilizar get_phys_pages())
  • _SC_AVPHYS_PAGES: Número de páginas físicas disponibles en memoria. (Igual que usar get_avphys_pages())
  • _SC_NPROCESSORS_CONF: Número de núcleos del procesador.
  • _SC_NPROCESSORS_ONLN: Número de núcleos activos del procesador.
  • _SC_ATEXIT_MAX: Máximo de funciones que podemos encolar con atexit(). Cuidado con este valor, puede ser muy grande, y podemos decir que no estamos limitados.
  • _SC_SIGQUEUE_MAX: Máximo de señales que podremos encolar para un proceso.
  • _SC_LEVEL1_ICACHE_SIZE: Tamaño en bytes de la caché de procesador de instrucciones de nivel 1.
  • _SC_LEVEL1_DCACHE_SIZE: Tamaño en bytes de la caché de procesador de datos de nivel 1.
  • _SC_LEVELX_CACHE_SIZE: Tamaño en bytes de la caché de procesador de nivel X

Aquí vemos un ejemplo con algunas más:

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
#include <unistd.h>
#include <stdio.h>

int main(int argc, char* argv[])
{
    printf ("Máximo de argumentos: %ld\n", sysconf(_SC_ARG_MAX));
    printf ("Máximo de procesos por usuario: %ld\n", sysconf(_SC_CHILD_MAX));
    printf ("Ticks por segundo: %ld\n", sysconf(_SC_CLK_TCK));
    printf ("Máximo de streams abiertos: %ld\n", sysconf(_SC_STREAM_MAX));
    printf ("Máximo de archivos abiertos: %ld\n", sysconf(_SC_OPEN_MAX));
    printf ("Control de tareas POSIX: %ld\n", sysconf(_SC_JOB_CONTROL));
    printf ("Tamaño máximo del login: %ld\n", sysconf(_SC_LOGIN_NAME_MAX));
    printf ("Tamaño máximo del hostname: %ld\n", sysconf(_SC_HOST_NAME_MAX));
    printf ("Caché de instrucciones de nivel 1: %ld\n", sysconf(_SC_LEVEL1_ICACHE_SIZE));
    printf ("Caché de datos de nivel 1: %ld\n", sysconf(_SC_LEVEL1_DCACHE_SIZE));
    printf ("Caché de nivel 2: %ld\n", sysconf(_SC_LEVEL2_CACHE_SIZE));
    printf ("Caché de nivel 3: %ld\n", sysconf(_SC_LEVEL3_CACHE_SIZE));
    printf ("Caché de nivel 4: %ld\n", sysconf(_SC_LEVEL4_CACHE_SIZE));
    printf ("Núcleos del procesador: %ld\n", sysconf(_SC_NPROCESSORS_CONF));
    printf ("Núcleos online del procesador: %ld\n", sysconf(_SC_NPROCESSORS_ONLN));
    printf ("Bits que tiene un char: %ld\n", sysconf(_SC_CHAR_BIT));
    printf ("Bits que tiene un long: %ld\n", sysconf(_SC_LONG_BIT));
    printf ("Máximo de bytes en nombre de huso horario: %ld\n", sysconf(_SC_TZNAME_MAX));
    printf ("Funciones que podemos encolar con atexit(): %ld\n", sysconf(_SC_ATEXIT_MAX));
    printf ("Máximo de señales para encolar: %ld\n", sysconf(_SC_SIGQUEUE_MAX));
    printf ("Máximo número de referencias en enlaces simbólicos: %ld\n", sysconf(_SC_SYMLOOP_MAX));

    return 0;
       
}

Configuración textual de sistema: confstr()

A veces necesitamos un texto para expresar todo lo que necesitamos decir. En este caso, POSIX, nos brinda la función confstr() en la que, pasandole una cadena de caracteres, copia en ella el valor que le pedimos a través de una constante (como hacíamos antes). Aunque, claro, para reservar memoria en consecuencia (ajustado al tamaño del texto a almacenar) podemos llamar a esta función con NULL en lugar de un puntero a cadena y que nos devuelva el tamaño que necesita para almacenar el valor.

De todas formas, veremos pocos valores aquí. (Muchos de ellos son para ver flags de compilación, como vemos aquí). Por ejemplo encontramos cosas como:

  • _CS_PATH: Es la ruta donde están las utildades POSIX por defecto. Sería el $PATH más básico del sistema.
  • _CS_GNU_LIBC_VERSION: Versión de LIBC.
  • _CS_GNU_LIBPTHREAD_VERSION: Versión de pthreads.

Información del sistema operativo

Como cuando ejecutamos el comando uname, en C, tendremos una función también llamada uname que devolverá un struct utsname con la siguiente información (todos los elementos son cadenas de caracteres):

  • sysname: Tipo de sistema, como este post es sobre Linux, siempre veremos Linux
  • nodename: Nombre del equipo (hostname)
  • release: Versión del kernel
  • version: Versión del SO: Ubuntu, Fedora, Arch Linux junto con versión y fecha
  • machine: arquitectura de máquina: i686, arm, x86_64…
  • __domainname: Nombre de dominio (extansión GNU)

La longitus de cada campo no está claramente definida, en unos sistemas serán los campos más grandes que en otros. Eso sí, todos tienen terminador, el sistema operativo se asegura de ello, por lo que podemos utilizarlos de forma segura.

Aquí tenemos un ejemplo de uso:

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
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/utsname.h>

int main(int argc, char* argv[])
{
    struct utsname unameInfo;
   
    if (uname(&unameInfo)==-1)
        {
            fprintf (stdout, "ERROR\n");
            exit(-1);          
        }
       
    printf ("S: %s\n", unameInfo.sysname);
    printf ("S: %s\n", unameInfo.nodename);
    printf ("S: %s\n", unameInfo.release);
    printf ("S: %s\n", unameInfo.version);
    printf ("S: %s\n", unameInfo.machine);

    return 0;
       
}

Hostname y nombre de dominio

Aunque uname() obtenemos el nodename que a fin de cuentas es nuestro hostname. El nombre de dominio, o domain name, no está claro y no estará siempre presente, es una extensión de GNU, que puede que no siempre tengamos disponible. Así que dejaremos por aquí otro fragmento de código que puede ser muy interesante para obtener esta información:

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 <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char* argv[])
{
    char domainname[10];

    /* Podemos hacer esto, si queremos hacerlo seriamente, utilizando
         la configuración del sistema para el tamaño máximo del hostname
         asegurándonos de no tener problemas.
         Aunque siempre podríamos declarar:

         char hostname[64];

         O un valor más grande, y no vamos a tener problemas.
    */

    char* hostname;
    int hostname_size = sysconf(_SC_HOST_NAME_MAX);
    hostname = (char*)malloc(hostname_size);
   
    if (gethostname(hostname, hostname_size)!=-1)
        printf ("Hostname: %s\n", hostname);
    else
        printf ("errno: %s (%d)\n", strerror(errno), errno);

    if (getdomainname(domainname, 10) != -1)
        printf ("Domain name: %s\n", domainname);

    return 0;      
}

Información sobre la CPU

Si has curioseado alguna vez tu sistema, habrás visto un archivo llamado /proc/cpuinfo que contiene mucha información útil sobre la CPU. Como la marca, modelo, extensiones del procesador, cores físicos, fpu, familia, frecuencia para la que fue diseñado y mucho más. Podemos parsear la información de /proc/cpuinfo en C (aunque en el ejemplo utilizo una función de C++ para extraer el archivo, podríamos hacerlo en C puro si queremos). Eso sí, me gusta extraer el fichero y luego parsear la información, aunque no sea lo más rápido (podemos leer el fichero y al mismo tiempo ir extrayendo información), porque hace nuestro código más reutilizable para otras situaciones. Aquí va el 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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>
#include <string>
#include <unistd.h>                         /* Utilidades UNIX */
#include <fcntl.h>                          /* Control de archivos */
#include <cstring>
#include <cstdio>
#include <cstdlib>

/** Extract a whole file into a string  */
std::string extractFile(const char *filename, size_t bufferSize=512)
{
    int fd = open(filename, O_RDONLY);
    std::string output;

    if (fd==-1)
        return "";      /* error opening */

    char *buffer = (char*)malloc(bufferSize);
    if (buffer==NULL)
        return "";      /* Can't allocate memory */

    int datalength;
    while ((datalength = read(fd, buffer, bufferSize)) > 0)
        output.append(buffer, datalength);

    close(fd);
    return output;
}

using namespace std;

int main(int argc, char* argv[])
{
    string cpuinfo = extractFile("/proc/cpuinfo");
    char* tokenized;
    char* ptrptr;

    /* Es más fácil hacer el parse con sscanf() que con streams de C++
       no es tan seguro hablando de memoria, pero este tipo de ficheros
         no suelen tener fallos y no suelen exceder unos tamaños determinados. */

    tokenized=strtok_r((char*)cpuinfo.c_str(), "\n", &ptrptr);
    while (tokenized)
        {
            char name[64];
            char value[1024];
            memset(value, '\0', 1024);
            sscanf (tokenized, "%[^\t:] : %[^\t\n]", name, value);
            cout << "["<<name<<"]" << " = " << value << "\n";

            tokenized = strtok_r(NULL, "\n", &ptrptr);
        };
 
    return 0;
}

Para parsear, en este caso prefiero las utilidades que nos da C como strtok y sscanf porque son mucho más rápidas que hacerlo con streams de C++. Además, con estos últimos tenemos que hacer algunas comprobaciones o reescribir los streams para que sea más amigable y resultarían muchas más líneas de código.

Identificador único

Si necesitas un identificador único para el equipo en el que se está ejecutando el programa, puedes leer el archivo /etc/machine-id (o generalmente podemos encontrarlo en /var/lib/dbus/machine-id y debe ser el mismo archivo) ; también podemos utilizar la función gethostid() que devolverá el mismo número que devuelve el comando hostid, aunque es un identificador muy pequeño para ser universalmente único y puede que se repita alguna vez si manejamos muchos IDs de sistema.

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main (void)
{
    printf ("%ld\n", gethostid());
    return 0;
}

Es más, este ID de host se puede definir mediante la función sethostid() sólo si eres root, pero se puede definir. Otro valor que es único para cada ordenador es el obtenido directamente del hardware. Podemos consultarlo desde /sys/devices/virtual/dmi/id/product_uuid aunque sólo podremos leerlo si somos root.

Otra forma de hacerlo es obtener información variada sobre el sistema: CPU, núcleos, MAC de interfaces de red, y en definitiva, muchos de los datos que podemos ver en este post, mezclarlos y hacer un hash.

Variables de entorno

En stdlib.h encontramos la función getenv() que nos ayuda a obtener la información encerrada en variables de entorno del sistema. No sólo para Linux. Esto lo podemos ver en práctica en este ejemplo: buscando archivos dentro de nuestro PATH.

Dispositivos de red

Para no repetirme, os remito a este post: Hallar la IP de un dispositivo de red. En el cual, no sólo podremos averiguar la IP de un dispositivo de red, sino mucha más información útil del mismo como también podemos ver aquí.

En el próximo post…

El próximo post estará dedicado a los procesos. Obtención de información propia del proceso en el que nos encontramos y otros procesos a los que tenemos acceso. Estará publicado el 15 de Mayo.

Foto principal: Team UI8

También podría interesarte...

Leave a Reply