11. Memoria compartida

Tiempo de lectura: 5 minutos

La memoria compartida es una estrategia para comunicar procesos donde uno de ellos gana acceso a regiones de la memoria del otro; algo que por lo general el sistema operativo siempre intenta evitar. Por eso, para que pueda haber memoria compartida es necesario que los dos procesos estén de acuerdo en eliminar dicha restricción.

Dos procesos que comparten una región de la memoria pueden intercambiar información simplemente leyendo y escribiendo datos en la misma. Sin embargo debemos tener en cuenta que:

  • La estructura de los datos y su localización dentro de la región compartida la determinan los procesos en comunicación y no el sistema operativo, a diferencia de lo que ocurre en los sistemas de paso de mensajes.

  • Los procesos son responsables de sincronizarse para no escribir y leer en el mismo sitio de la memoria al mismo tiempo, pues esto puede generar inconsistencias (véase el Capítulo 13) .

Las principales ventajas de la memoria compartida frente a otros mecanismos de comunicación son:

  • Eficiencia. Puesto que la comunicación tiene lugar a la velocidad de la memoria principal, se trata de un mecanismo tremendamente rápido.

  • Conveniencia. Puesto que el mecanismo de comunicación solo requiere leer y escribir de la memoria, se trata de un sistema muy sencillo y fácil de utilizar.

Como ocurre con las tuberías (véase el Apartado 10.5.3) la memoria compartida puede ser anónima o con nombre.

11.1. Memoria compartida anónima

La memoria compartida anónima solo existe para el proceso que la crea y para sus procesos hijos, que heredan el acceso. Es por tanto, una forma eficiente de comunicar procesos padres e hijos.

En los sistemas POSIX, las funciones y operadores de reserva de memoria como malloc() y new, utilizan internamente la llamada al sistema mmap(). Esta función se puede llamar de la siguiente manera para reservar length bytes de memoria.

void* p = mmap( (1)
    NULL,
    length,     (2)
    PROT_READ | PROT_WRITE,      (3)
    MAP_ANONYMOUS | MAP_PRIVATE, (4)
    -1,
    0
);
1 Si todo va bien, mmap() devuelve un puntero al primer byte de la memoria reservada.
2 Cantidad de memoria a reservar en bytes.
3 Permisos de acceso para la memoria reservada. En este caso, se solicita permitir la lectura y la escritura de la memoria.
4 MAP_ANONYMOUS indica que la memoria no está respaldada por ningún archivo, por lo que su contenido inicial será cero. Mientras que MAP_PRIVATE establece que la región de memoria es privada.

Lo interesante es que si se cambia MAP_PRIVATE por MAP_SHARED la región de memoria reservada es memoria compartida:

void* p = mmap(
    NULL,
    length,
    PROT_READ | PROT_WRITE,
    MAP_ANONYMOUS | MAP_SHARED, (1)
    -1,
    0
);
1 Memoria anónima y compartida.

Es decir, que al crear un hijo con fork() este tendrá una copia de toda la memoria del proceso padre, excepto esta región en particular, que será la misma que la del padre. Por lo tanto, escribiendo y leyendo en esa región, ambos procesos pueden comunicarse.

En anom-shared-memory.cpp se puede ver un ejemplo muy simple, similar a fork-pipe.cpp pero utilizando memoria compartida para comunicar ambos procesos. Como se puede apreciar, la versión que usa memoria compartida es bastante más sencilla que la que utiliza tuberías.

En Microsoft Windows se puede hacer algo similar con CreateFileMapping():

HANDLE hMapFile = CreateFileMapping( (3)
    INVALID_HANDLE_VALUE,
    NULL,
    PAGE_READWRITE, (1)
    0,
    length,         (2)
    NULL
);
1 Permisos de acceso para la memoria reservada.
2 Cantidad de memoria a reservar en bytes.
3 A diferencia de mmap(), CreateFileMapping() crea un objeto de memoria compartida, pero no hace visible esa memoria para nuestro proceso. Para eso hay que llamar a MapViewOfFile() pasándole el manejador hMapFile devuelto por CreateFileMapping().

11.2. Memoria compartida con nombre

La memoria compartida con nombre es pública para el resto del sistema, por lo que teóricamente cualquier proceso con permisos puede acceder a ella para comunicarse con otros procesos.

Como ocurre en las tuberías con nombre, los objetos de memoria compartida con nombre hay que crearlos antes de comenzar a utilizarlos. Para eso los sistemas POSIX ofrecen la función shm_open().

int shmfd = shm_open(   (4)
    "/foo-shm",         (1)
    O_RDWR | O_CREAT,   (2)
    0666                (3)
);
1 Nombre que identifica al objeto de memoria compartida. Como ocurre con los archivos, varios procesos pueden acceder al mismo objeto indicando el mismo nombre.
2 Valores que indican diferentes opciones a la hora de abrir el objeto. Por ejemplo, usando O_RDWR indicamos que se abra para lectura y escritura. Mientras que con O_CREAT se indica que el objeto debe crearse si no existía previamente.
3 Indica los permisos del objeto de memoria compartida al crearlo nuevo, de forma similar a los permisos que se aplican a los archivos en el sistema de archivos.

El valor devuelto por shm_open() es el descriptor del objeto de memoria compartida, que utilizaremos posteriormente con mmap() al reservar una región de la memoria de nuestro proceso donde ese objeto de memoria compartida será visible:

void* p = mmap(
    NULL,
    length,                 (2)
    PROT_READ | PROT_WRITE,
    MAP_SHARED,             (1)
    shmfd,                  (1)
    0                       (2)
);
1 Al pasar el descriptor del objeto de memoria compartida, ya no se puede indicar MAP_ANONYMOUS.
2 Se puede hacer visible para el proceso todo el objeto de memoria compartida o solo una parte. Para esto último se indica el tamaño de la región y el desplazamiento dentro del objeto, que es el último argumento de mmap().

Un objeto de memoria compartida recién creado tiene tamaño 0. Para redimensionarlo se utiliza ftruncate(), que lo que necesita es el descriptor del objeto y el nuevo tamaño.

En shared-memory-server.c y shared-memory-client.cpp se puede ver el ejemplo de un programa que muestra periódicamente la hora del sistema. En este caso controlado por otro mediante memoria compartida. Ambos programas usan la clase definida en shared_memory.hpp para gestionar el objeto de memoria compartida. Sus métodos muestran de forma práctica cómo utilizar las llamadas al sistema comentadas.

En Microsoft Windows también se utiliza CreateFileMapping() para crear el objeto de memoria compartida con nombre. Simplemente hay que indicar el nombre en el último argumento de la función.

HANDLE hMapFile = CreateFileMapping(
    INVALID_HANDLE_VALUE,
    NULL,
    PAGE_READWRITE,
    0,
    length,
    "Global\\FooMemoriaCompartida" (1)
);
1 Nombre del nuevo objeto de memoria compartida.