Publi

Cómo obtener información de salud de tu sistema Linux o tu servidor en C y C++ (I. Memoria)

Es algo necesario para el buen funcionamiento de nuestras aplicaciones. Para añadir robustez a nuestros sistemas, haciendo que éstos se comporten de manera adecuada en situaciones imprevistas. Por ejemplo, si necesitamos crear un archivo y añadir cierta información, podemos, a priori, comprobar que el dispositivo en el que vamos a escribir tiene suficiente espacio libre, lo que nos puede ahorrar suplicios si los ficheros son demasiado grandes. También deberíamos ser capaces de comprobar que un disco de red está conectado antes de trabajar con él y, en muchos casos, anticiparnos a largas esperas y bloqueos por parte del sistema operativo. O incluso tomar una decisión ante una tarea intensiva que no es prioritaria comprobando primero cómo de ocupado está nuestro sistema.

La serie de posts sobre salud del sistema

El post me ha salido enorme por lo que he decidido dividirlo en varias partes que se publicarán una cada dos semanas. Como siempre, voy intercalando posts sobre otras temáticas relacionadas, para no estar siempre hablando de lo mismo.
Si sois impacientes y queréis ver mucha información y resultados con código, podéis ver este proyecto en GitHub (linuxmon) donde se tratan muchos de estos temas. Aunque, en esta serie de posts me gustaría completar algo más la información obtenida y, estará todo explicado.

C y C++ en los ejemplos

Muchos ejemplos son básicamente acceso a ficheros o utilización de funciones de C de bibliotecas de Linux y POSIX. Aunque he utilizado C++ para facilitar un poco la salida de datos y pelearme un poco menos con la memoria en algunos casos utilizando string, vector, map y alguna cosa más. De todas formas, para aclaraciones sobre cómo hacer todo esto en C puro, podéis preguntar en los comentarios.

Para el código C++, he utilizado algunas de las novedades disponibles a partir de la especificación de 2011 que, como ya tiene 6 años, considero que todos podremos compilarlo.

Por tanto, para compilar estos ejemplos, debemos hacer:

g++ -o ejecutable fuente.cpp -std=c++11 -lpthread

La biblioteca pthread la necesito sólo para un par de ejemplos en los que utilizo hilos y alguna que otra guarrada de la que no me siento orgulloso, pero que a lo mejor puede resultar de ayuda para alguien.

Algunas funciones que nos serán de ayuda

Está claro que como programadores, no es difícil pelearnos nos bytes, por ejemplo para comprobar que el espacio libre es mayor a un tamaño determinado. Lo malo es que imprimir en pantalla números más o menos grandes (del orden de miles de millones) para expresar tamaños puede resultar difícil de leer, así que utilizaremos funciones como size() para obtener el tamaño en formato humano.

De forma parecida, con el objetivo de simplificar nuestro código, la función extractFile() leerá un archivo en su totalidad y lo almacenará en un buffer.
Ambas funciones las pongo aquí junto con 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
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
#include <iostream>
#include <string>
#include <unistd.h>                         /* Utilidades UNIX */
#include <fcntl.h>                          /* Control de archivos */

/** Humanize size */
std::string size(long double size, int8_t precision=3)
{
    static const char* units[10]={"bytes","Kb","Mb","Gb","Tb","Pb","Eb","Zb","Yb","Bb"};
    char temp[32];
    char format[10];

    int i= 0;

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

    snprintf(format, 10, "%%.%dLf%%s", precision);
    snprintf(temp, 32, format, size, units[i]);

    return std::string(temp);
}

/** 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[])
{
    cout << size(1023339229) << endl;
    cout << extractFile("utils.cpp")<<endl;
    return 0;
}

En ambas funciones podría haber utilizado utilidades de C++ para la lectura de ficheros, o para transformar los números en cadena de caracteres y presentar la información. En este caso no lo hice así por cuestión de velocidad, ya que pienso utilizar estas funciones muchas veces y necesito que se ejecuten bien en sistemas pequeños. En mis pruebas, incluso con la optimización de compilador, he conseguido que estas funciones se ejecuten entre un 40% y un 60% más rápido. Y es normal, las bibliotecas de streams de C++ son muy complejas, no tendríamos limitación por tamaño de cadenas, por ejemplo, para el caso de size(). Y en el caso de extractFile(), utilizo directamente las funciones de unistd. Están más cerca del sistema operativo y se comportan más rápido que las de C y mucho más que las de C++.

Pongo aquí una versión en C puro de la función size() que también utilizaré en algunos ejemplos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
char* size(char* storage, long double size, int8_t precision)
{
    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++;
        }

    snprintf(format, 10, "%%.%dLf%%s", precision);
    snprintf(storage, 32, format, size, units[i]);

    return storage;
}

Información de memoria y uso del sistema: sysinfo()

En Linux, tenemos la función sysinfo() dentro de sys/sysinfo.h, que devuelve en una estructura homómina (struct sysinfo) la siguiente información:

  • long uptime : Segundos desde el arranque del ordenador.
  • unsigned long loads[3] : Carga media en 1, 5 y 15 minutos (que podemos ver con el comando uptime). La carga del sistema está expresada en la medida que diga la constante SI_LOAD_SHIFT, es decir, el 100% de carga podría ser por ejemplo 65536 por lo que para encontrar el % real debemos dividir por ese número.
  • unsigned long totalram : Memoria RAM total
  • unsigned long freeram : Memoria RAM libre
  • unsigned long sharedram : Memoria RAM compartida
  • unsigned long bufferram : Memoria RAM usada en buffers
  • unsigned long totalswap : Memoria SWAP total
  • unsigned long freeswap : Memoria SWAP libre
  • unsigned short procs : Número de procesos en marcha actualmente. Aunque en este caso indica los hilos en ejecución, que serán muchos más.
  • unsigned int mem_unit : Tamaño de la unidad de memoria en bytes

Como ejemplo podemos hacer lo siguiente:

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
#include <stdio.h>
#include <sys/sysinfo.h>
#include <time.h>
#include <stdint.h>

#include <string.h>                         /* strerror() */
#include <errno.h>
#include <stdlib.h>

/** Humanize size */
char* size(char* storage, long double size, int8_t precision)
{
    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++;
        }

    snprintf(format, 10, "%%.%dLf%%s", precision);
    snprintf(storage, 32, format, size, units[i]);

    return storage;
}

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

int main(int argc, char* argv[])
{
    struct sysinfo si;
    struct tm temptime;     /* Utilizado para presentar uptime en formato humano */
    char temp[32];              /* Cadena temporal para los tamaños. (función size()) */
   
    if (sysinfo(&si) <0)
        panic("No puedo hacer sysinfo");

    localtime_r(&si.uptime, &temptime);

    printf ("Uptime: %ld (%d años, %d meses %d dias %d horas %d minutos %d segundos)\n",
                    si.uptime,
                    temptime.tm_year-70,    /* Los años empiezan en 1970, restamos 70*/
                    temptime.tm_mon,
                    temptime.tm_mday-1,     /* El primer día es el 1 */
                    temptime.tm_hour,
                    temptime.tm_min,
                    temptime.tm_sec);

    printf ("Carga del sistema: %lu %lu %lu (%f %f %f)\n",
                    si.loads[0], si.loads[1], si.loads[2],
                    si.loads[0] / (float) (1<<SI_LOAD_SHIFT),
                    si.loads[1] / (float) (1<<SI_LOAD_SHIFT),
                    si.loads[2] / (float) (1<<SI_LOAD_SHIFT));
    printf ("Procesos corriendo: %d\n", si.procs);
    printf ("Unidad de memoria: %d (%s)\n", si.mem_unit, size(temp, si.mem_unit, 3 ));
    printf ("RAM total: %ld (%s)\n", si.totalram, size(temp, si.totalram*si.mem_unit, 3));
    printf ("RAM free: %ld (%s)\n", si.freeram, size(temp, si.freeram*si.mem_unit, 3));
    printf ("RAM compartida: %ld (%s)\n", si.sharedram, size(temp, si.sharedram*si.mem_unit, 3));
    printf ("RAM buffers: %ld (%s)\n", si.bufferram, size(temp, si.bufferram*si.mem_unit, 3));
    printf ("RAM usada: %ld (%s)\n", si.totalram - si.freeram - si.bufferram,
                    size(temp, (si.totalram - si.freeram - si.bufferram)*si.mem_unit, 3));
    printf ("SWAP total: %ld (%s)\n", si.totalswap, size(temp, si.totalswap*si.mem_unit, 3));
    printf ("SWAP free: %ld (%s)\n", si.freeswap, size(temp, si.freeswap*si.mem_unit, 3));
    printf ("SWAP usada: %ld (%s)\n", si.totalswap - si.freeswap, size(temp, (si.totalswap-si.freeswap)*si.mem_unit, 3));
    printf ("Total high: %ld (%s)\n", si.totalhigh, size(temp, si.totalhigh*si.mem_unit, 3));
    printf ("Free high: %ld (%s)\n", si.freehigh, size(temp, si.freehigh*si.mem_unit, 3));
   
    return 0;
}

En el ejemplo anterior, lo que puede que no quede muy claro es la memoria alta total y libre. Este tipo de memoria se utiliza en sistemas que tienen que dividir la memoria principal para poder acceder a ella correctamente. Como ejemplo, podemos indicar dispositivos con un ancho de bus inferior a la cantidad de memoria a la que pueden acceder, hace unos años era muy común esto, por ejemplo en CPUs de 16bit que necesitaban poder acceder a varios megabytes de RAM (cuando con 16bit no podríamos expresar números mayores de 65535). O, hace algunos años menos, cuando CPUs de 32bit con arquitectura Intel debían acceder a más de 4Gb de RAM, lo que se conocía como PAE o Physical Address Extension. La memoria alta sería aquella a la que no podíamos acceder tan directamente.

Información sobre paginado de memoria

Existen tres funciones más para averiguar:

  • Tamaño de páginas de memoria. getpagesize() o sysconf(_SC_PAGESIZE) como veremos más adelante.
  • Total de páginas en RAM (páginas físicas). get_phys_pages(). Este número de páginas será la cantidad total de memoria entre el tamaño de página.
  • Total de páginas físicas disponibles. get_avphys_pages(). Que debe ser la cantidad de memoria libre entre el tamaño de página.

Vemos un pequeño ejemplo aquí:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <unistd.h>
#include <sys/sysinfo.h>

int main(int argc, char* argv[])
{
    printf ("Tamaño de página: %d\n", getpagesize());
    printf ("Páginas físicas: %ld\n", get_phys_pages());
    printf ("Páginas físicas disponibles: %ld\n", get_avphys_pages());

    return 0;
}

Más información sobre la memoria: /proc/meminfo

Con el comando anterior vemos mucha información sobre la memoria aunque, no toda la información que nos da Linux. Si hemos curioseado proc veremos que existe el archivo /proc/meminfo con mucha información más. Eso sí, tenemos que parsear el fichero para extraer la información. Afortunadamente no es un fichero físico, sino un vínculo en nuestro sistema de archivos que se actualiza en tiempo real. Y podemos acceder a él como si fuera un archivo, y eso es muy bueno, muy filosofía unix y nos facilita el acceso a la información desde cualquier lenguaje. Aunque ahora vamos a hacerlo en C++ para facilitarnos un poco la vida:

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
#include <iostream>
#include <sstream>
#include <string>
#include <unistd.h>                         /* Utilidades UNIX */
#include <fcntl.h>                          /* Control de archivos */

/** Humanize size */
std::string size(long double size, int8_t precision=3)
{
    static const char* units[10]={"bytes","Kb","Mb","Gb","Tb","Pb","Eb","Zb","Yb","Bb"};
    /* Using a char instead of ostringstream precission because it's faster */
    char temp[32];
    char format[10];

    int i= 0;

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

    snprintf(format, 10, "%%.%dLf%%s", precision);
    snprintf(temp, 32, format, size, units[i]);

    return std::string(temp);
}

/** 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 memInfo = extractFile ("/proc/meminfo");
    if (memInfo.empty())
        {
            cerr << "Error al leer información"<< endl;
            terminate();
        }

    stringstream ss (memInfo);
    string unit;
    string name;
    unsigned long num;
    ss >> name >> num >> unit;
    while (ss.good())
        {
            name.erase(name.size()-1, 1);
            cout << name<< " -> " << size(num*1024, 3)<<"\n";
            ss >> name >> num;
            ss.ignore(10, '\n');            /* Eliminamos la unidad o hasta el salto de línea */
        }

    return 0;
}

Además, si queremos, podemos crear una función que almacene todo en un mapa de C++ con lo que podremos acceder muy fácilmente a cada uno de los elementos leidos:

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
#include <iostream>
#include <sstream>
#include <string>
#include <map>
#include <unistd.h>                         /* Utilidades UNIX */
#include <fcntl.h>                          /* Control de archivos */

/** Humanize size */
std::string size(long double size, int8_t precision=3)
{
    static const char* units[10]={"bytes","Kb","Mb","Gb","Tb","Pb","Eb","Zb","Yb","Bb"};
    /* Using a char instead of ostringstream precission because it's faster */
    char temp[32];
    char format[10];

    int i= 0;

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

    snprintf(format, 10, "%%.%dLf%%s", precision);
    snprintf(temp, 32, format, size, units[i]);

    return std::string(temp);
}

/** 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;
}

std::map<std::string, unsigned long> linuxMemory()
{
    std::map<std::string, unsigned long> out;
   
    std::string memInfo = extractFile ("/proc/meminfo");
    if (memInfo.empty())
        {
            std::cerr << "Error al leer información\n";
            std::terminate();
        }

    std::stringstream ss (memInfo);
    std::string unit;
    std::string name;
    unsigned long num;
    ss >> name >> num >> unit;
    while (ss.good())
        {
            name.erase(name.size()-1, 1);
            out[name] = num;
            ss >> name >> num;
            ss.ignore(10, '\n');
        }
    return out;
}

using namespace std;

int main(int argc, char* argv[])
{
    for (auto d : linuxMemory())
        {
            std::cout << "["<<d.first << "] : "<<d.second<<"\n";
        }
    return 0;
}

Memoria utilizada por nuestro programa

Aunque trataremos el tema en profundidad en un futuro post, podemos averiguar la memoria total del programa, tamaño del código, datos y pila leyendo el fichero /proc/self/statm, también podríamos leer el fichero /proc/self/stat (que analizaremos más adelante) o incluso /proc/self/status que deberemos parsear como /proc/meminfo . Por ahora podemos quedarnos con lo siguiente:

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
#include <iostream>
#include <sstream>
#include <string>
#include <map>
#include <cstdio>
#include <cstdlib>
#include <unistd.h>                         /* Utilidades UNIX */
#include <fcntl.h>                          /* Control de archivos */

/** 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[])
{
    std::string memInfo = extractFile ("/proc/self/statm");

    if (memInfo.empty())
        {
            std::cerr << "Error al leer información\n";
            std::terminate();
        }
    unsigned long size,
        resident,
        share,
        text,
        lib,
        data,
        dt;
   
    std::stringstream ss (memInfo);
   
    ss >> size >> resident >> share >> text >> lib >> data >> dt;

    cout << "Tamaño total: "<<size<<"\n";
    cout << "Tamaño residente: "<<resident<<"\n";
    cout << "Páginas compartidas: "<<share<<"\n";
    cout << "Tamaño de código: "<<text<<"\n";
    cout << "No usado: "<<lib<<"\n";
    cout << "Datos + Pila: "<<data<<"\n";
    cout << "No usado: "<<dt<<"\n";
   
    return 0;
}

Los tamaños vienen expresados en páginas, por lo que para extraer la información en bytes debemos multiplicar el valor obtenido por el tamaño de las páginas (el valor que nos devuelve getpagesize()). En Linux 2.6 o superior vemos que los valores de lib y de dt valen siempre 0, pero el sistema sigue utilizándolos para garantizar la retrocompatibilidad con programas más antiguos. Si queremos hacer este parseo del archivo en C puro podemos utilizar sscanf() que también nos dará muy buen resultado.

Siguiente post

Hablaremos de la CPU, definiciones del sistema, como el tamaño de página y cosas así que pueden resultar interesantes y utilización de recursos por parte de nuestras aplicaciones. Todo esto, a partir del 1 de mayo.

Foto principal: William Stitt

También podría interesarte...

Leave a Reply