12. Memoria compartida
La memoria compartida como mecanismo de comunicación entre procesos y su uso en sistemas POSIX y Windows.
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 (ver el capítulo «Sincronización»).
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.
Al igual que ocurre con las tuberías la memoria compartida puede ser anónima o con nombre.
Memoria compartida anónima
Sección titulada «Memoria compartida anónima»La memoria compartida anónima solo existe para el proceso que la crea y para los procesos con los que decide compartirla —normalmente sus hijos—. Una vez que dos procesos tienen mapeada la misma región, cualquier escritura que haga uno es visible para el otro sin necesidad de copiar datos entre ellos, lo que la convierte en una forma muy eficiente de comunicarlos.
En los sistemas POSIX, la memoria compartida anónima se reserva con la llamada al sistema mmap(), el indicador MAP_SHARED.
La función devuelve un puntero a la región de memoria reservada, que puede ser utilizado para leer y escribir en ella.
void* p = mmap( NULL,
length,
PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0);La opción MAP_ANONYMOUS indica que la memoria no está respaldada por ningún archivo, por lo que su contenido es inicializado a cero.
Mientras que MAP_SHARED establece que la región de memoria es compartida.
En los sistemas POSIX, la forma habitual de compartir esta región es crear un hijo con fork(). Este heredará una copia de toda la memoria de su padre, excepto las regiones de memoria compartida que estarán respaldadas por la misma memoria física en ambos procesos.
En Microsoft Windows se puede hacer algo similar con CreateFileMapping():
HANDLE hMapFile = CreateFileMapping( INVALID_HANDLE_VALUE, NULL,
PAGE_READWRITE,
0, length, NULL);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().
La función devuelve un puntero a la región de memoria reservada.
LPVOID p = MapViewOfFile(
hMapFile,
FILE_MAP_ALL_ACCESS, 0, 0,
length);En anom-shared-memory.cpp se puede ver un ejemplo muy simple del uso de la memoria compartida en sistemas POSIX. Se trata de un caso similar al ejemplificado en fork-pipe.cpp pero comunicando ambos procesos mediante memoria compartida. Como se puede apreciar, la versión que usa memoria compartida es bastante más sencilla que la que utiliza tuberías.
Memoria compartida con nombre
Sección titulada «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, sin necesidad de que exista relación de parentesco entre ellos —como ocurre con las tuberías con nombre—.
-
Crear o abrir el objeto de memoria compartida, indicando su nombre.
En los sistemas POSIX se usa la función shm_open(), indicando el nombre con el que otros procesos podrán acceder al mismo objeto.
int shmfd = shm_open("/foo-shm",O_RDWR | O_CREAT,0666);Resultado
shmfdcontiene el descriptor del objeto de memoria compartida, que se usará en el siguiente paso para mapearlo.Un objeto de memoria compartida recién creado tiene tamaño 0. Para redimensionarlo se utiliza ftruncate(), que lo que espera es el descriptor del objeto
shmfdy el nuevo tamaño. -
Mapear el objeto en el espacio de direcciones del proceso, para obtener un puntero con el que leer y escribir en él.
El descriptor devuelto por shm_open() se utiliza con mmap():
void* p = mmap(NULL,length,PROT_READ | PROT_WRITE,MAP_SHARED,shmfd,0);A diferencia de la memoria compartida anónima, aquí se pasa el descriptor
shmfddel objeto en lugar de-1, por lo que ya no se usa el indicadorMAP_ANONYMOUS. El último argumento —el desplazamiento— permite, junto conlength, hacer visible para el proceso solo una parte del objeto en lugar de su totalidad; ya que permiten indicar la posición inicial y extensión de la memoria del objeto que se quiere mapear en la memoria del proceso.Resultado
papunta a la región de memoria compartida, lista para leer y escribir en ella.
-
Crear o abrir el objeto de memoria compartida, indicando su nombre.
En Microsoft Windows también se utiliza CreateFileMapping() para crear o abrir objetos de memoria compartida. Basta con indicar su nombre en el último argumento de la función.
HANDLE hMapFile = CreateFileMapping(INVALID_HANDLE_VALUE,NULL,PAGE_READWRITE,0,length,"Global\\FooMemoriaCompartida");Resultado
hMapFilecontiene el manejador del objeto de memoria compartida, que se usará en el siguiente paso para mapearlo. -
Mapear el objeto en el espacio de direcciones del proceso, para obtener un puntero con el que leer y escribir en él.
El manejador devuelto por CreateFileMapping() se utiliza con MapViewOfFile():
LPVOID p = MapViewOfFile(hMapFile,FILE_MAP_ALL_ACCESS,0,0,length);Resultado
papunta a la región de memoria compartida, lista para leer y escribir en ella.
En shared-memory.cpp se puede ver el ejemplo de un programa POSIX que muestra periódicamente la hora del sistema. El programa es controlado por shared-memory-control.cpp mediante memoria compartida.
Ejemplo Comunicación con memoria compartida
Sección titulada « Comunicación con memoria compartida»Vamos a ilustrar el uso de la memoria compartida en sistemas POSIX con un ejemplo sencillo: un proceso padre escribe un número en la memoria compartida y crea un proceso hijo que lo dobla varias veces seguidas. Antes de leer cada resultado, el padre tiene que esperar a que el hijo termine de calcularlo, así que ambos procesos se tienen que sincronizar para no leer datos antes de que estén listos.
En este ejemplo utilizaremos un semáforo, que es un tipo de objeto de sincronización que permite a un proceso llamar a sem_wait() para esperar a que otro procesos llame a sem_post(). Este mecanismo de sincronización es muy sencillo, pero no es el único que se puede utilizar para sincronizar procesos. Estudiaremos este y otros mecanismos de sincronización en el capítulo «Sincronización».
-
Definir la estructura para interpretar el contenido de la memoria compartida.
struct memory_content{sem_t ready;int value;};Resultado En la estructura
memory_contentse define un semáfororeadyy un enterovalueque contendrá el número que el hijo va a doblar, para que sean accesibles tanto para el proceso padre como para el hijo. -
Reservar la memoria compartida anónima e inicializar el contenido.
constexpr int num_steps = 5;// Reserva de memoria compartida anónima del tamaño de memory_contentvoid* shared_mem = mmap( nullptr, sizeof(memory_content),PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0 );memory_content* memory_region = static_cast<memory_content*>(shared_mem);// Inicializar el semáforo y el valor a través de un puntero `memory_region*`sem_init( &memory_region->ready, 1, 0 );memory_region->value = 1;Resultado El semáforo
readyqueda listo para usarse entre procesos y se asigna 1 —el valor inicial— avalue. -
Crear el proceso hijo con fork(), para que doble el valor
num_stepsveces.pid_t child = fork();if (child == 0){for (int i = 0; i < num_steps; i++){sleep(1); // Simula que el hijo tarda un poco en cada pasomemory_region->value *= 2;sem_post( &memory_region->ready );}_exit(EXIT_SUCCESS);}Resultado El proceso hijo dobla el valor y notifica al padre utilizando sem_post(),
num_stepsveces seguidas. -
En el padre, esperar la notificación del hijo con sem_wait() antes de leer cada resultado.
if (child > 0){for (int i = 0; i < num_steps; i++){sem_wait( &memory_region->ready );std::println( "[PADRE] El valor ahora es {}", memory_region->value );}// ...}Resultado Cada vuelta del bucle, el proceso padre queda bloqueado en sem_wait() hasta que el hijo llama a sem_post(), así que ambos avanzan al mismo ritmo aunque el padre se ejecutara más rápido. De esta forma, el padre nunca lee antes de que el hijo haya escrito un nuevo valor.
-
Esperar la terminación del hijo con wait(), liberar el semáforo con sem_destroy() y terminar.
if (child > 0){wait( nullptr );sem_destroy( &memory_region->ready );return EXIT_SUCCESS;}Resultado wait() evita que el proceso hijo, que ya ha terminado, se quede como proceso zombi. En este caso no nos interesa recuperar su valor de estado, así que pasamos
nullptrcomo argumento. sem_destroy() libera los recursos del semáforo, que ya no hace falta. Y, finalmente, terminamos el proceso padre conEXIT_SUCCESS.
Referencias de las API
Sección titulada «Referencias de las API»Lenguaje C
Lenguaje C++
POSIX
Trunca o extiende un archivo a un tamaño específico.
int ftruncate(int fd, off_t length);Proyecta un archivo o dispositivo en la memoria del proceso.
void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset);Destruye un semáforo POSIX anónimo.
int sem_destroy(sem_t* sem);Decrementa (espera) un semáforo POSIX, bloqueando si es necesario.
int sem_wait(sem_t* sem);Abre o crea un objeto de memoria compartida POSIX.
int shm_open(const char* name, int oflag, mode_t mode);Windows API
Crea o abre un objeto de asignación de archivo en memoria.
HANDLE CreateFileMapping(HANDLE hFile, LPSECURITY_ATTRIBUTES lpFileMappingAttributes, DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, LPCTSTR lpName);Proyecta una vista de un objeto de asignación de archivo en la memoria del proceso.
LPVOID MapViewOfFile(HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, SIZE_T dwNumberOfBytesToMap);