Implementación de Memoria Compartida System V en Linux para IPC

La memoria compartida es una de las formas más eficientes de Comunicación entre Procesos (IPC) en sistemas Unix/Linux, perteneciente al conjunto de primitivas de System V. A diferencia de otros mecanismos como las tuberías o los sockets, la memoria compartida permite que múltiples procesos accedan directamente a la misma región de la memoria física sin necesidad de copiar datos entre espacios de direcciones del kernel y del usuario.

Fundamentos Técnicos

Cada proceso en Linux posee su propio espacio de direcciones virtuales gestionado mediante tablas de páginas y la Unidad de Gestión de Memoria (MMU). La memoria compartida funciona mapeando regiones virtuales de diferentes procesos a la misma dirección física. Cuando un proceso escribe en esta región, el cambio es inmediatamente visible para cualquier otro proceso que tenga esa misma página física mapeada en su tabla de páginas.

Es crucial destacar que la memoria compartida por sí sola no ofrece sincronización ni exclusión mutua. Por lo tanto, debe utilizarse en conjunto con semáforos u otros mecanismos de bloqueo para prevenir condiciones de carrera (race conditions) cuando varios procesos intentan escribir simultáneamente.

Flujo de Operaciones Básicas

El ciclo de vida de un segmento de memoria compartida implica cinco pasos principales:

  1. Generación de Clave: Uso de ftok() para crear un identificador único basado en una ruta de archivo existente y un ID de proyecto.
  2. Creación/Obtención: Uso de shmget() para solicitar el segmento de memoria.
  3. Adjuntar (Attach): Uso de shmat() para mapear el segmento al espacio de direcciones virtual del proceso.
  4. Desadjuntar (Detach): Uso de shmdt() para eliminar el mapeo del proceso actual.
  5. Eliminación: Uso de shmctl() con la orden IPC_RMID para liberar los recursos del sistema.

APIs Principales

1. ftok() - Generador de Claves

Esta función convierte una ruta de archivo y un ID de proyecto en una clave IPC única.


#include <sys/ipc.h>
#include <sys/types.h>

key_t ftok(const char *pathname, int proj_id);

  • pathname: Ruta absoluta a un archivo existente y accesible.
  • proj_id: Un entero no cero utilizado como "semilla" para la generación de la clave.
  • Retorno: Devuelve la clave generada o -1 en caso de error.

2. shmget() - Creación del Segmento

Solicita un nuevo segmento de memoria compartida o recupera uno existente asoicado a la clave proporcionada.


#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);

  • key: Clave IPC generada por ftok.
  • size: Tamaño solicitado en bytes. El sistema suele redondear esto a múltiplos del tamaño de página (ej. 4KB).
  • shmflg: Banderas de creación y permisos. Ejemplo: IPC_CREAT | IPC_EXCL | 0666.
    • IPC_CREAT: Crea si no existe.
    • IPC_EXCL: Falla si ya existe (útil para asegurar que se crea uno nuevo).
  • Retorno: Identificador de memoria compartida (shmid) o -1 en error.

3. shmat() - Mapeo a Dirección Virtual

Conecta el segmento identificado por shmid al espacio de direcciones del proceso llamante.


void *shmat(int shmid, const void *shmaddr, int shmflg);

  • shmid: El identificador devuelto por shmget.
  • shmaddr: Generalmente se pasa NULL para dejar que el kernel elija la dirección virtual adeucada.
  • shmflg: SHM_RDONLY para acceso solo lectura, o 0 para lectura/escritura.
  • Retorno: Puntero al inicio del segmento o (void *)-1 en error.

4. shmdt() y shmctl() - Limpieza

Una vez finalizado el uso, el proceso debe desvincularse y, opcionalmente, eliminar el recurso global.


// Desvincula el segmento del proceso actual
int shmdt(const void *shmaddr);

// Controla metadatos y elimina el segmento si cmd es IPC_RMID
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

Nota: shmdt solo reduce el contador de referencias del proceso actual. shmctl con IPC_RMID marca el segmento para eliminación; este se libera realmente cuando el último proceso vinculado realiza un shmdt.

Ejemplo Práctico de Implementación

El siguiente código demuestra cómo un servidor crea la memoria compartida y espera señales, mientras que un cliente escribe datos en ella. Se utiliza una tubería FIFO (named pipe) simple para coordinar la sincronización básica (evitando escrituras concurrentes no controladas), aunque en producción se preferirían semáforos System V o POSIX.


#pragma once
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cassert>
#include <cstring>
#include <fcntl.h>

#define RUTA_ARCHIVO "/tmp/shared_mem_file" // Archivo base para ftok
#define ID_PROYECTO 0x5A
#define TAMANO_MEMORIA 4096
#define NOMBRE_FIFO "./sync_fifo"

// Funciones auxiliares para manejo de FIFO (sincronización simple)
inline int abrirFifo(std::string path, int flags) {
    int fd = open(path.c_str(), flags);
    assert(fd >= 0 && "Error abriendo FIFO");
    return fd;
}

inline void esperarSeñal(int fd) {
    uint32_t dummy;
    ssize_t r = read(fd, &dummy, sizeof(dummy));
    assert(r == sizeof(dummy));
}

inline void enviarSeñal(int fd) {
    uint32_t val = 1;
    ssize_t w = write(fd, &val, sizeof(val));
    assert(w == sizeof(val));
}


#include "cabecera_compartida.hpp"

int main() {
    // 1. Generar clave
    key_t k = ftok(RUTA_ARCHIVO, ID_PROYECTO);
    if (k == -1) { perror("ftok"); exit(1); }

    // 2. Crear segmento (IPC_CREAT|IPC_EXCL asegura que sea nuevo)
    int shmid = shmget(k, TAMANO_MEMORIA, IPC_CREAT | IPC_EXCL | 0666);
    if (shmid == -1) { perror("shmget"); exit(1); }

    // 3. Adjuntar a memoria
    char *ptrMemoria = (char *)shmat(shmid, NULL, 0);
    if (ptrMemoria == (void *)-1) { perror("shmat"); exit(1); }

    printf("[Servidor] Memoria compartida creada. Esperando datos...\n");

    // 4. Bucle de lectura sincronizada
    int fdFifo = abrirFifo(NOMBRE_FIFO, O_RDONLY);
    
    while (true) {
        esperarSeñal(fdFifo); // Bloquea hasta que el cliente escriba
        
        if (strcmp(ptrMemoria, "salir") == 0) break;
        
        printf("[Servidor] Recibido: %s\n", ptrMemoria);
        memset(ptrMemoria, 0, TAMANO_MEMORIA); // Limpiar buffer
    }

    // 5. Limpieza
    close(fdFifo);
    unlink(NOMBRE_FIFO); // Eliminar archivo FIFO
    
    shmdt(ptrMemoria);   // Desvincular
    shmctl(shmid, IPC_RMID, NULL); // Eliminar segmento
    
    printf("[Servidor] Recursos liberados.\n");
    return 0;
}


#include "cabecera_compartida.hpp"

int main() {
    // 1. Generar la misma clave
    key_t k = ftok(RUTA_ARCHIVO, ID_PROYECTO);
    if (k == -1) { perror("ftok"); exit(1); }

    // 2. Obtener el segmento existente (sin IPC_CREAT)
    int shmid = shmget(k, 0, 0); 
    if (shmid == -1) { perror("shmget"); exit(1); }

    // 3. Adjuntar
    char *ptrMemoria = (char *)shmat(shmid, NULL, 0);
    if (ptrMemoria == (void *)-1) { perror("shmat"); exit(1); }

    printf("[Cliente] Conectado. Escriba mensajes (o 'salir' para terminar):\n");

    int fdFifo = abrirFifo(NOMBRE_FIFO, O_WRONLY);

    char buffer[TAMANO_MEMORIA];
    while (fgets(buffer, sizeof(buffer), stdin)) {
        // Copiar entrada estándar a memoria compartida
        strncpy(ptrMemoria, buffer, TAMANO_MEMORIA - 1);
        ptrMemoria[strcspn(ptrMemoria, "\n")] = 0; // Quitar newline
        
        // Señalar al servidor que hay datos listos
        enviarSeñal(fdFifo);
        
        if (strcmp(ptrMemoria, "salir") == 0) break;
    }

    close(fdFifo);
    shmdt(ptrMemoria); // Solo desvincular, no eliminar (responsabilidad del creador)
    return 0;
}


g++ -std=c++11 servidor.cpp -o servidor
g++ -std=c++11 cliente.cpp -o cliente

Etiquetas: Linux IPC System V Shared Memory C++ shmget shmat

Publicado el 9-24 09:23