1. Una región, dos espacios de direcciones
Memoria compartida entre procesos es una región cuyo contenido está asociado a mapeos de más de un proceso. Sus direcciones virtuales pueden ser distintas, pero los accesos al mismo desplazamiento del objeto compartido se refieren a los mismos datos.
En los temas de paginación vimos que una dirección virtual se traduce mediante la tabla de páginas del proceso. El aislamiento habitual se consigue asignando marcos distintos o restringiendo permisos. Para compartir, el núcleo puede instalar traducciones que hagan accesibles las mismas páginas desde varios espacios de direcciones.
| Proceso | Base virtual del mapeo | Dirección del byte 120 | Contenido al que accede |
|---|---|---|---|
| A | 0x70000000 | 0x70000078 | Byte 120 del objeto compartido |
| B | 0x50000000 | 0x50000078 | El mismo byte 120 |
El resto de la memoria sigue aislado: compartir una región no comparte automáticamente las variables de la pila, los bloques de malloc ni las demás páginas del proceso. Tampoco convierte un puntero cualquiera en una referencia válida para todos.
En RAM, las páginas compartidas evitan mantener una copia privada del contenido por participante. Las traducciones, los permisos y cierta información administrativa siguen siendo propios de cada proceso. Además, el respaldo puede moverse o intercambiarse: es más preciso identificar el objeto y su desplazamiento que imaginar un número de marco físico permanente.
2. Formas de compartir memoria
| Mecanismo | Cómo encuentran los procesos la región | Uso en este curso |
|---|---|---|
MAP_SHARED | MAP_ANONYMOUS | Un hijo hereda el mapeo al hacer fork | Procesos con un antecesor común |
shm_open y MAP_SHARED | Abren el mismo objeto POSIX por su nombre | Ejecuciones independientes en la misma máquina |
Archivo ordinario con MAP_SHARED | Abren y mapean el mismo archivo | Contenido asociado a un archivo, como en el tema 41 |
| Memoria compartida System V | Identificadores obtenidos con shmget y shmat | Otra API, presente en programas existentes |
En Linux también existen objetos creados con memfd_create, cuyos descriptores pueden transmitirse a otros procesos. No necesitamos esa variante para entender el mecanismo. En todos los casos hay que responder dos preguntas: cómo se accede al mismo objeto y cómo se coordina el uso de sus datos.
MAP_PRIVATE no sirve para publicar cambios: sus escrituras quedan en copias privadas. Dos mapeos anónimos creados de forma independiente tampoco se conectan por tener igual tamaño o aparecer casualmente en la misma dirección virtual.
3. Mapeo anónimo compartido y fork
La forma más directa para un padre y un hijo es crear el mapeo compartido antes del fork. El hijo lo hereda y ambos conservan acceso al contenido común. El siguiente ejemplo escribe únicamente en el hijo; el padre espera su terminación antes de leer.
#define _DEFAULT_SOURCE
#include <errno.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
struct resultado {
int valor;
};
int main(void)
{
size_t longitud = sizeof(struct resultado);
struct resultado *r = mmap(NULL, longitud, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (r == MAP_FAILED) { perror("mmap"); return 1; }
r->valor = 0;
pid_t hijo = fork();
if (hijo == -1) {
perror("fork"); munmap(r, longitud); return 1;
}
if (hijo == 0) {
r->valor = 2026;
_exit(0);
}
int estado;
pid_t fin;
do {
fin = waitpid(hijo, &estado, 0);
} while (fin == -1 && errno == EINTR);
int codigo = 1;
if (fin == -1) {
perror("waitpid");
} else if (WIFEXITED(estado) && WEXITSTATUS(estado) == 0) {
printf("El padre lee: %d\n", r->valor);
codigo = 0;
} else {
fprintf(stderr, "El hijo no terminó correctamente\n");
}
if (munmap(r, longitud) == -1) { perror("munmap"); codigo = 1; }
return codigo;
}
$ gcc -std=c17 -Wall -Wextra -O2 compartida_fork.c -o compartida_fork
$ ./compartida_fork
El padre lee: 2026
waitpid establece aquí la secuencia: primero termina la escritura del hijo y luego el padre consulta el resultado. No hay accesos concurrentes conflictivos al campo. Con MAP_PRIVATE, manteniendo el resto del programa, el padre leería su valor inicial, 0.
El hijo termina con _exit; al terminar, sus mapeos se eliminan automáticamente. El munmap del padre elimina su propia vista. Si el hijo siguiera vivo, desmapear en el padre no retiraría el mapeo del hijo. Una llamada a exec, por su parte, reemplaza el espacio de direcciones y no conserva estos mapeos.
4. Objetos de memoria compartida POSIX
Para ejecuciones independientes necesitamos un objeto que ambos puedan localizar. shm_open crea o abre un objeto POSIX de memoria compartida y devuelve un descriptor. El nombre portable comienza con una barra y no contiene otras, por ejemplo /gm43_mensaje.
int fd = shm_open("/gm43_mensaje", O_CREAT | O_EXCL | O_RDWR, 0600);
/* comprobar fd, establecer tamaño y comprobar cada resultado */
ftruncate(fd, longitud);
void *base = mmap(NULL, longitud, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
close(fd);
| Operación | Responsabilidad | No hace por sí sola |
|---|---|---|
shm_open | Obtener un descriptor del objeto | Mapearlo o darle un tamaño inicial distinto de cero |
ftruncate | Fijar su longitud | Publicar que la estructura ya está inicializada |
mmap | Crear la vista en el espacio virtual | Sincronizar lectores y escritores |
close | Cerrar el descriptor | Quitar el mapeo o borrar el nombre |
munmap | Retirar una vista local | Quitar las vistas de otros procesos |
shm_unlink | Eliminar el nombre | Invalidar descriptores y mapeos que ya existen |
O_CREAT | O_EXCL distingue al creador: si el nombre ya existe, la operación falla con EEXIST. No se debe truncar a ciegas un objeto existente que otros pueden estar utilizando. Los permisos 0600, sujetos a la máscara umask, restringen el acceso inicial al propietario.
5. Crear, leer y eliminar desde ejecuciones distintas
Este programa tiene tres modos. crear prepara un mensaje y termina; leer lo abre con permiso de solo lectura; borrar retira el nombre. Ejecuta los comandos en ese orden y espera a que cada uno termine. El mensaje queda inmutable entre la creación y el borrado: este ejemplo no es un buzón con actualizaciones simultáneas.
#define _POSIX_C_SOURCE 200809L
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#define NOMBRE "/gm43_mensaje"
struct mensaje {
uint32_t version;
uint32_t longitud;
char texto[128];
};
int main(int argc, char *argv[])
{
if (argc != 2 || (strcmp(argv[1], "crear") != 0 &&
strcmp(argv[1], "leer") != 0 && strcmp(argv[1], "borrar") != 0)) {
fprintf(stderr, "Uso: %s crear|leer|borrar\n", argv[0]);
return 1;
}
if (strcmp(argv[1], "borrar") == 0) {
if (shm_unlink(NOMBRE) == -1) { perror("shm_unlink"); return 1; }
puts("Nombre eliminado.");
return 0;
}
int crear = strcmp(argv[1], "crear") == 0;
int fd = shm_open(NOMBRE,
crear ? O_CREAT | O_EXCL | O_RDWR : O_RDONLY, 0600);
if (fd == -1) { perror("shm_open"); return 1; }
size_t tam = sizeof(struct mensaje);
int codigo = 1;
if (crear) {
if (ftruncate(fd, (off_t) tam) == -1) {
perror("ftruncate"); goto cerrar;
}
} else {
struct stat st;
if (fstat(fd, &st) == -1) { perror("fstat"); goto cerrar; }
if (st.st_size != (off_t) tam) {
fprintf(stderr, "Tamaño de objeto incompatible\n"); goto cerrar;
}
}
struct mensaje *m = mmap(NULL, tam,
crear ? PROT_READ | PROT_WRITE : PROT_READ, MAP_SHARED, fd, 0);
if (m == MAP_FAILED) { perror("mmap"); goto cerrar; }
if (crear) {
const char texto[] = "Hola desde otro proceso";
memcpy(m->texto, texto, sizeof texto);
m->longitud = (uint32_t) (sizeof texto - 1);
m->version = 1;
puts("Mensaje creado. Ahora se puede ejecutar el lector.");
codigo = 0;
} else if (m->version != 1 || m->longitud >= sizeof m->texto) {
fprintf(stderr, "Formato de mensaje inválido\n");
} else {
printf("Mensaje: %.*s\n", (int) m->longitud, m->texto);
codigo = 0;
}
if (munmap(m, tam) == -1) { perror("munmap"); codigo = 1; }
cerrar:
if (close(fd) == -1) { perror("close"); codigo = 1; }
/* Solo el creador limpia su objeto si la creación no se completó. */
if (crear && codigo != 0 && shm_unlink(NOMBRE) == -1)
perror("shm_unlink tras error");
return codigo;
}
$ gcc -std=c17 -Wall -Wextra -O2 mensaje_posix.c -o mensaje_posix -lrt
$ ./mensaje_posix crear
Mensaje creado. Ahora se puede ejecutar el lector.
$ ./mensaje_posix leer
Mensaje: Hola desde otro proceso
$ ./mensaje_posix borrar
Nombre eliminado.
Se incluye -lrt para entornos Linux que requieren enlazar esa biblioteca para la API POSIX de memoria compartida; en versiones modernas de glibc puede no ser necesario. Al terminar el creador, el objeto con nombre continúa existiendo. El lector puede ejecutarse en otra terminal bajo el mismo usuario después de completar la creación.
No hace falta msync para que el siguiente proceso lea este objeto: cerrar o desmapear no descarta su contenido compartido. Tampoco hace falta mantener vivo al creador. Lo que exige este ejemplo es que no haya un escritor concurrente y que nadie trunque, elimine y sustituya el objeto durante la secuencia.
6. Direcciones, offsets y formato
El núcleo puede elegir una base diferente para cada mapeo, debido a la disposición del espacio virtual y a ASLR. Guardar dentro de la región un puntero devuelto por malloc es especialmente problemático: apunta al heap privado del creador. Incluso un puntero a otro byte del mismo objeto puede ser inválido para un proceso que lo mapeó en otra base.
Una solución es almacenar desplazamientos desde el comienzo del objeto. Cada proceso reconstruye su propio puntero sumando el offset a su base y comprueba que la referencia quede dentro del tamaño mapeado.
Objeto compartido: offset_texto = 128; longitud_texto = 20
Proceso A: base 0x70000000 + 128 = 0x70000080
Proceso B: base 0x50000000 + 128 = 0x50000080
Ambos consultan los mismos 20 bytes del objeto.
/* base es unsigned char *; tam, offset y longitud son size_t. */
if (offset <= tam && longitud <= tam - offset) {
unsigned char *datos = base + offset;
/* usar como máximo longitud bytes */
}
La resta evita desbordar una suma como offset + longitud. Para interpretar estructuras también deben respetarse la alineación y el formato. Los participantes necesitan acordar tamaños, versión, representación de enteros y organización de los campos. Compartir una estructura C no constituye automáticamente un formato portable entre compiladores, arquitecturas o versiones diferentes.
El ejemplo del mensaje utiliza el mismo programa en el mismo sistema, campos de tamaño fijo y un arreglo contenido en la estructura. El campo version sirve para rechazar un formato incompatible, pero escribirlo al final no lo convierte en un mecanismo de publicación concurrente.
7. Compartir no sincroniza
Supón un contador compartido inicialmente en cero y dos procesos que ejecutan contador++. Conceptualmente cada incremento lee, calcula y escribe. Una intercalación posible es:
| Paso | Proceso A | Proceso B | Contador compartido |
|---|---|---|---|
| 1 | Lee 0 | — | 0 |
| 2 | — | Lee 0 | 0 |
| 3 | Calcula 1 | — | 0 |
| 4 | — | Calcula 1 | 0 |
| 5 | Escribe 1 | — | 1 |
| 6 | — | Escribe 1 | 1 |
Se hicieron dos incrementos, pero uno se perdió. Que las cachés mantengan coherencia sobre el contenido no hace indivisible la secuencia completa. Este desglose es un modelo explicativo: un programa C con accesos concurrentes conflictivos sin la sincronización exigida no debe usarse esperando una intercalación concreta ni un resultado predecible.
volatileno aporta exclusión mutua ni convierte un incremento en una operación atómica.msyncse refiere a sincronizar contenido mapeado con su respaldo; no protege una sección crítica.sleepno establece un protocolo fiable entre participantes: los tiempos de planificación cambian.- Los semáforos y mutex compartidos entre procesos permiten coordinar accesos; ciertas operaciones atómicas también pueden servir cuando la implementación garantiza su uso entre procesos.
Para publicar un mensaje mientras ambos procesos siguen vivos, se puede usar un semáforo «listo», inicialmente cero. El productor escribe y hace sem_post; el consumidor hace sem_wait antes de leer. Para reutilizar el búfer también hace falta saber cuándo el consumidor terminó: por ejemplo, otro semáforo «libre».
8. Contador protegido con un semáforo
Colocaremos tanto el contador como el semáforo en el mapeo compartido. sem_init(&semaforo, 1, 1) indica que el semáforo se comparte entre procesos y comienza con un permiso. Cada participante toma ese permiso antes de incrementar y lo devuelve después.
#define _DEFAULT_SOURCE
#include <errno.h>
#include <semaphore.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
#define VUELTAS 50000
struct compartida {
sem_t acceso;
unsigned long contador;
};
static int incrementar(struct compartida *c)
{
for (int i = 0; i < VUELTAS; i++) {
int r;
do {
r = sem_wait(&c->acceso);
} while (r == -1 && errno == EINTR);
if (r == -1) { perror("sem_wait"); return 1; }
c->contador++;
if (sem_post(&c->acceso) == -1) { perror("sem_post"); return 1; }
}
return 0;
}
int main(void)
{
size_t tam = sizeof(struct compartida);
struct compartida *c = mmap(NULL, tam, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (c == MAP_FAILED) { perror("mmap"); return 1; }
c->contador = 0;
if (sem_init(&c->acceso, 1, 1) == -1) {
perror("sem_init"); munmap(c, tam); return 1;
}
pid_t hijo = fork();
if (hijo == -1) {
perror("fork"); sem_destroy(&c->acceso); munmap(c, tam); return 1;
}
if (hijo == 0)
_exit(incrementar(c));
int codigo = incrementar(c);
int estado;
pid_t fin;
do {
fin = waitpid(hijo, &estado, 0);
} while (fin == -1 && errno == EINTR);
if (fin == -1) {
perror("waitpid");
/* No destruir el semáforo sin confirmar el fin del otro usuario. */
munmap(c, tam);
return 1;
}
if (!WIFEXITED(estado) || WEXITSTATUS(estado) != 0)
codigo = 1;
if (codigo == 0) {
printf("Contador: %lu; esperado: %lu\n", c->contador, 2UL * VUELTAS);
if (c->contador != 2UL * VUELTAS) codigo = 1;
}
if (sem_destroy(&c->acceso) == -1) { perror("sem_destroy"); codigo = 1; }
if (munmap(c, tam) == -1) { perror("munmap"); codigo = 1; }
return codigo;
}
$ gcc -std=c17 -Wall -Wextra -O2 contador_compartido.c -o contador_compartido -pthread
$ ./contador_compartido
Contador: 100000; esperado: 100000
El segundo argumento de sem_init no es el valor inicial: es el indicador pshared. Un valor distinto de cero pide uso entre procesos; el tercer argumento establece el contador inicial del semáforo. Además, el objeto sem_t debe estar realmente en memoria compartida. Ponerlo en una variable global privada no basta.
Se inicializa una sola vez, antes de crear el hijo, y se destruye después de confirmar que el hijo terminó. El contador no necesita volatile: los accesos se realizan bajo la sincronización del semáforo. Un mutex POSIX podría cumplir una función semejante si se configura con PTHREAD_PROCESS_SHARED; su configuración predeterminada es para hilos de un mismo proceso.
Este ejemplo supone que ambos participantes continúan ejecutándose normalmente. Si uno muere después de tomar el permiso y antes de devolverlo, el otro puede quedar esperando indefinidamente: un semáforo no recupera automáticamente un permiso perdido. Un sistema real necesita un protocolo de fallas, plazos o mecanismos de recuperación, como mutex robustos cuando correspondan.
9. Inicialización y ciclo de vida
Un objeto POSIX tiene nombre, descriptores abiertos y mapeos. Son recursos distintos. Para diseñar su limpieza conviene separar estas etapas:
- Crear y dimensionar. Un único creador obtiene el objeto y establece su tamaño antes de que los participantes accedan a sus páginas.
- Inicializar. Se preparan cabeceras, datos y primitivas de sincronización una sola vez.
- Publicar y utilizar. Un protocolo hace saber a los demás que pueden entrar. Si hay cambios simultáneos, se coordinan.
- Retirar el nombre.
shm_unlinkimpide nuevas aperturas por ese nombre, sin quitar referencias existentes. - Terminar y liberar. Cada participante cierra descriptores y retira su mapeo. Las primitivas compartidas se destruyen cuando ya no quedan usuarios ni esperas sobre ellas.
Si se elimina el nombre mientras A conserva un mapeo, A puede seguir usando su objeto. Si B crea después otro objeto con el mismo nombre, B obtiene un objeto diferente: A no se conecta mágicamente al nuevo. Para liberar el objeto anterior deben desaparecer sus referencias pendientes.
Quitar el nombre pronto es útil cuando todos los participantes ya recibieron el descriptor o heredaron el mapeo. Para clientes que necesitan abrir por nombre en el futuro, el nombre debe permanecer disponible. Ninguno de esos diseños se obtiene llamando indiscriminadamente a shm_unlink desde todos los procesos.
En una inicialización concurrente, una bandera ordinaria «listo» consultada en un bucle no sustituye la sincronización. Se puede coordinar con una primitiva externa ya disponible, un proceso coordinador o un protocolo de publicación correctamente implementado. No debe llamarse a sem_init otra vez sobre un semáforo que ya está inicializado.
10. Simulador de actualizaciones perdidas
Cada proceso hará exactamente un incremento. Sus pasos son leer, sumar localmente y escribir. Pulsa los botones para elegir la intercalación, o usa una demostración. Activa el semáforo para que el permiso se tome antes de leer y se devuelva después de escribir.
Simulador · dos procesos y un contador compartido
Contador compartido: 0 · Dueño del permiso: ninguno
| Proceso | Próximo paso | Valor local privado |
|---|
| # | Proceso | Operación | Contador |
|---|
Sin sincronización, la demostración ejecuta A, B, A, B, A, B: ambos leen cero y el resultado final es uno. Con semáforo, B intenta entrar mientras A conserva el permiso; después A termina y B lee el valor actualizado, por lo que el resultado es dos. También puedes obtener dos sin semáforo ejecutando A, A, A, B, B, B: un resultado correcto en una ejecución no demuestra ausencia de carreras.
Es un modelo de intercalación, no una emulación de instrucciones C, cachés ni planificación. Un intento bloqueado no avanza el proceso; para retomarlo se vuelve a pulsar su botón cuando el permiso está disponible. Cambiar de modo reinicia todo el estado.
11. Observarlo en Linux
Linux normalmente expone los objetos POSIX de memoria compartida en un sistema de archivos tmpfs montado en /dev/shm. Después de ejecutar el modo crear del ejemplo puedes inspeccionar el objeto:
$ ls -l /dev/shm/gm43_mensaje
-rw------- 1 usuario usuario 136 oct 25 10:00 /dev/shm/gm43_mensaje
$ stat /dev/shm/gm43_mensaje
... tamaño, propietario y permisos del objeto ...
$ strace -e trace=openat,ftruncate,mmap,munmap,close ./mensaje_posix leer
... extracto ilustrativo ...
openat(AT_FDCWD, "/dev/shm/gm43_mensaje", O_RDONLY|O_NOFOLLOW|O_CLOEXEC) = 3
mmap(NULL, 136, PROT_READ, MAP_SHARED, 3, 0) = 0x7f...
munmap(0x7f..., 136) = 0
close(3) = 0
Los números, fechas y detalles de las llamadas dependen del entorno. El tamaño de 136 bytes corresponde al formato del ejemplo en un entorno habitual. Aunque el objeto sea menor que una página, los mapeos siguen teniendo la granularidad de páginas del gestor de memoria.
Para ver el mapeo en /proc/PID/maps, agrega temporalmente un getchar() al lector antes de munmap e imprime su PID con getpid. Un mapeo compartido de solo lectura aparece con permisos r--s; la letra final s lo distingue de uno privado. El RSS de varios procesos puede contabilizar las mismas páginas compartidas; el PSS de smaps reparte proporcionalmente su costo.
Al finalizar la práctica, usa ./mensaje_posix borrar. Si una ejecución dejó un objeto incompleto, verifica primero que no haya participantes utilizándolo antes de retirar su nombre y volver a crearlo. El programa no borra un objeto ajeno cuando O_EXCL detecta que ya existía.
12. Límites, rendimiento y errores
La memoria compartida evita ciertas copias entre búferes privados y puede reducir llamadas al sistema durante el intercambio de datos. Es útil para grandes volúmenes o estructuras reutilizadas. Eso no significa que todas sus operaciones sean gratuitas: siguen existiendo fallos de página, movimiento de líneas de caché, sincronización y competencia por el ancho de banda.
Dos procesos que actualizan campos diferentes dentro de una misma línea de caché pueden producir falso compartimiento: no compiten por el mismo dato lógico, pero fuerzan tráfico sobre la misma línea. Repartir el trabajo, agrupar actualizaciones o separar campos calientes puede ser más útil que añadir más procesos. La ubicación NUMA también influye, como veremos en el tema 44.
| Error o suposición | Problema | Corrección |
|---|---|---|
| Mapear antes de dar tamaño al objeto | El objeto nuevo tiene longitud cero; acceder sin respaldo suficiente puede producir SIGBUS | Dimensionar antes y coordinar la apertura |
| Usar MAP_PRIVATE | Las escrituras no se publican al objeto compartido | Usar MAP_SHARED cuando los cambios deban compartirse |
| Guardar punteros privados | Otra base virtual o un heap distinto los invalida | Offsets comprobados y un formato acordado |
| Inicializar el semáforo en cada proceso | Reinicializar uno activo tiene comportamiento indefinido | Un creador y un protocolo de inicialización |
| Truncar mientras otros acceden | Sus direcciones pueden perder respaldo | Tamaño estable o redimensionamiento coordinado |
| Confundir close con shm_unlink | El nombre puede permanecer y acumular objetos | Definir quién retira el nombre y cuándo |
| Confiar en que el productor siempre termina | Un consumidor puede quedar esperando para siempre | Plazos, detección de fallas y recuperación |
El espacio de /dev/shm y los recursos del sistema tienen límites. Crear y dimensionar un objeto no garantiza que cualquier escritura futura disponga de recursos: por ejemplo, agotar el espacio de su tmpfs puede causar SIGBUS durante un acceso. Es necesario dimensionar el diseño y comprobar los errores; no usar tamaños enormes para una prueba básica.
Los permisos del objeto controlan quién puede abrirlo, pero un participante con escritura puede alterar los datos compartidos. Validar tamaños, offsets y versiones sigue siendo necesario. La memoria compartida tampoco es por sí sola almacenamiento durable: un objeto POSIX no promete sobrevivir a un reinicio. Y este mecanismo comunica procesos de la misma máquina; no sustituye un protocolo de red.
Las tuberías y las colas de mensajes ofrecen otras abstracciones, a menudo más simples cuando importa delimitar mensajes o desacoplar participantes. Se estudiarán en el curso de Comunicación entre Procesos. Aquí el objetivo es entender cómo el gestor de memoria permite compartir páginas y qué responsabilidades deja al programa.
Para consultar las interfaces y sus condiciones exactas, revisa los manuales de Linux de mmap, shm_open y shm_unlink, memoria compartida POSIX, sem_init y sem_wait.
13. Resumen
- Varios procesos pueden mapear el mismo objeto y acceder a sus datos desde bases virtuales diferentes.
- Un mapeo anónimo compartido creado antes de fork permite compartir contenido con los descendientes.
- shm_open localiza un objeto POSIX; ftruncate fija su tamaño y mmap con MAP_SHARED crea la vista.
- Compartir bytes no hace atómicas las operaciones ni establece un protocolo de publicación.
- Un semáforo entre procesos debe estar en memoria compartida y se inicializa una sola vez con pshared distinto de cero.
- Los formatos compartidos deben evitar punteros privados y validar offsets, longitudes y versiones.
- close, munmap y shm_unlink cumplen funciones diferentes; quitar un nombre no invalida vistas existentes.
- El diseño debe contemplar quién inicializa, cómo se espera, qué ocurre si un proceso muere y quién limpia.
- Se ahorran ciertas copias, pero la sincronización, las cachés, los recursos y la ubicación de los datos siguen importando.
14. Preguntas de repaso
Responde antes de abrir cada explicación.
1. ¿Dos procesos necesitan la misma dirección virtual para compartir?
No. Necesitan mapear el mismo objeto con la semántica adecuada. El mismo desplazamiento puede traducirse al contenido común desde bases diferentes.
2. ¿Por qué el primer ejemplo crea el mapeo antes de fork?
Para que el hijo herede ese mismo mapeo compartido. Dos llamadas anónimas independientes no crean una región común.
3. ¿Qué leería el padre si se usara MAP_PRIVATE?
Leería cero, su valor inicial. La escritura del hijo produciría un cambio privado mediante copy-on-write.
4. ¿Qué tamaño tiene un objeto recién creado con shm_open?
Cero. El creador debe establecer el tamaño, por ejemplo con ftruncate, antes de acceder a su contenido mapeado.
5. ¿MAP_SHARED garantiza que contador++ sea atómico?
No. Compartir el contenido no vuelve indivisible una secuencia de lectura, modificación y escritura. Hay que usar sincronización adecuada.
6. ¿Qué significa sem_init(&s, 1, 1)?
Inicializa s para compartirlo entre procesos, con un permiso inicial. El propio semáforo debe encontrarse en memoria compartida.
7. ¿Por qué usar un offset en lugar de un puntero?
Porque cada proceso puede tener otra base virtual. Sumando un offset validado a su base reconstruye una referencia local al mismo contenido.
8. ¿Qué pasa si se llama a shm_unlink mientras otro proceso tiene el objeto mapeado?
Se retira el nombre. Las vistas y los descriptores existentes siguen siendo utilizables; el objeto se libera cuando desaparecen las referencias que lo mantienen vivo.
9. ¿Es suficiente escribir version = 1 al final para publicar datos concurrentemente?
No. Ese campo permite comprobar un formato, pero una escritura ordinaria no sustituye un protocolo de sincronización entre productor y consumidor.
10. ¿Qué ocurre si un proceso muere con el permiso del semáforo tomado?
El permiso no se devuelve automáticamente. Otros participantes pueden quedar bloqueados; el diseño necesita contemplar esa falla.
Para practicar
- Compila
compartida_fork.c, comprueba el resultado y repite con MAP_PRIVATE. Explica la diferencia. - Ejecuta los modos crear, leer y borrar del programa POSIX. Intenta crear dos veces seguidas y observa EEXIST sin borrar el mensaje original.
- Después de borrar el objeto, ejecuta el lector y comprueba que falla al abrir el nombre.
- Agrega al lector una pausa antes de desmapear. Retira el nombre desde otra terminal y comprueba que esa vista sigue siendo válida.
- Amplía el formato con dos mensajes y offsets relativos. Verifica los límites antes de formar los punteros de cada lector.
- Ejecuta el contador protegido varias veces y cambia VUELTAS a un valor moderado diferente. Comprueba que el resultado siempre sea el doble en ejecuciones correctas.
- En el simulador, construye una intercalación sin sincronización que dé uno y otra que dé dos. Explica por qué ambos resultados son posibles en el modelo.
- Diseña un buzón de un solo elemento con semáforos «libre» y «listo». Escribe el orden de operaciones y quién puede tocar el búfer en cada etapa.
- Propón una política para limpiar el objeto si el creador falla durante la inicialización. Distingue recuperar un objeto abandonado de borrar uno activo.