diff --git a/AGENTS.md b/AGENTS.md index cc86be8..0d22768 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -262,3 +262,47 @@ Reglas de diseño (resumen): > Si se modifica `assets/pdf/print.css`, hay que **regenerar todos** los capítulos para que ningún > PDF quede con un diseño distinto. + +--- + +## Protocolo de escritura de archivos extensos + +Cuando el agente (humano o IA) necesita escribir un archivo Markdown de contenido grande +(≥ 600 líneas, como los capítulos del libro), **no usar una única llamada a `write` con todo el contenido**. +Los herramientas de escritura pasan el contenido dentro de un string JSON y los caracteres especiales +(tres backticks ```, `$`, `{`, `}`, `\`, comillas) pueden romper el parseo. + +### Enfoque correcto (confirmado en producción) + +1. **Escribir con `write` solo los primeros ~500-600 líneas**, hasta la primera sección completa + (hasta un `---` o final de una parte). El archivo se crea correctamente porque el contenido + es manejable para el parser JSON. + +2. **Usar `edit` para ir agregando las secciones restantes**. Cada llamada a `edit` reemplaza + una cadena corta (`oldString`) por otra más larga (`newString`). La cadena a buscar es siempre + muy breve (ej: el último texto del archivo actual) y la cadena de reemplazo incluye la nueva sección. + + ``` + oldString: "- [ ] Termina limpiamente al escribir \"exit\"." ← solo la última línea existente + newString: "- [ ] Termina limpiamente al escribir \"exit\".\n\n---\n# PARTE 5 — ..." ← misma + nueva sección + ``` + +3. **Verificar el conteo de líneas** con un bash/PowerShell simple después de cada edit grande, + para confirmar que la sección se incorporó correctamente: + ``` + Get-Content book\22-file-descriptors.md | Measure-Object -Line + ``` + +### Por qué fallaba el enfoque anterior + +Un archivo de capítulo tiene ~1200-1500 líneas con: +- Bloques de código C con backticks triples (```` ```c ````) que el parser JSON confunde +- Strings con comillas escapadas (`\"`) que colisionan con las del string JSON +- Caracteres especiales como `$` en ejemplos de shell o `\n` en strings C + +Con chunks de ≤ 600 líneas, el contenido cabe en un string JSON sin romper el parseo. + +### Regla práctica + +> **Nunca escribir más de 600 líneas de una sola vez con `write`.** Si el contenido total es mayor, +> usar `write` para la primera parte y `edit` sucesivo para el resto. diff --git a/book/22-file-descriptors.md b/book/22-file-descriptors.md new file mode 100644 index 0000000..54643bd --- /dev/null +++ b/book/22-file-descriptors.md @@ -0,0 +1,1449 @@ +# Capítulo 22 — File Descriptors: del Kernel a Sistemas Concurrentes de Alto Rendimiento + +> Capítulo avanzado, pero **escrito para que se entienda de verdad**. Se apoya en memoria dinámica (`09`), +> structs (`08`) y archivos (`15`). También asume que ya compilaste programas con `gcc`. +> +> Este capítulo te saca de la comodidad de `FILE*` y te mete directo al kernel: `open`, `read`, `write`, +> `lseek`, `flock`, `O_NONBLOCK`, `select`, `poll`, `epoll`, `dup`/`dup2`. Aquí mandás vos. +> El kernel obedece. + +--- + +## Cómo leer este capítulo + +Llegó el momento en que `FILE*` deja de ser suficiente. `fopen`, `fgets`, `printf` son comodísimos, +pero esconden lo que realmente importa: **los file descriptors**. Son números enteros pequeños que le +decís al kernel "accedé a este recurso" y el kernel **obedece sin preguntar**. + +Este capítulo va de cero (qué es un fd) hasta alto rendimiento (E/S no bloqueante, multiplexación, +concurrencia con bloqueos). Si entendés cada parte, vas a poder escribir programas que leen, escriben +y comunican datos **sin intermediarios**, directamente con el sistema operativo. + +Está dividido en **nueve partes**. Después de cada concepto hay una o más prácticas —uno o varios +ejercicios— para que los uses ya mismo. **No las saltees**: son la mitad del capítulo. + +| Parte | De qué habla | La idea en una frase | +|---|---|---| +| **1** | ¿Qué es un fd? | Un file descriptor es solo un número entero que le dice al kernel "a qué recurso quiero acceder". | +| **2** | `open`, `close`, `read`, `write` | Las cuatro funciones primitivas: abrir, leer, escribir y cerrar sin intermediarios de `stdio`. | +| **3** | Flags y permisos (`O_CREAT`, `O_TRUNC`, `O_APPEND`, `mode_t`) | El contrato con el kernel define creación atómica, truncamiento y seguridad de acceso. | +| **4** | `lseek`: manipulación directa del cursor | El puntero de posición del kernel es tuyo: saltá, medí y posicionate a voluntad sin búferes. | +| **5** | Concurrencia y bloqueos (`fcntl` / `flock`) | Dos procesos escribiendo al mismo archivo destruyen los datos si no usan bloqueos advisory. | +| **6** | E/S no bloqueante (`O_NONBLOCK`) y multiplexación (`select`, `poll`, `epoll`) | No te quedes colgado esperando E/S: el kernel te avisa mediante descriptores listos para operar. | +| **7** | `/proc/self/fd/`: el mapa interno del kernel | Linux te expone el estado de tus descriptores en tiempo real como un sistema de archivos virtual. | +| **8** | `dup` y `dup2`: clonación y redirección | Dos números apuntando al mismo recurso: la clave absoluta de cómo los shells redirigen stdout/stderr. | +| **9** | Ejercicio jefe: demonio de logs concurrente, rotativo y multicanal | Unifica todo: llamadas crudas, bloqueos contra procesos concurrentes, multiplexación y rotación atómica. | + +> **Regla de oro del libro:** el capítulo explica y muestra **cómo funciona** cada system call, pero +> **los ejercicios no traen la solución**. El enunciado te da el problema; la respuesta la escribís vos. + +--- + +# PARTE 1 — ¿Qué es un file descriptor? + +Antes de tocar código, entendamos qué estás pidiéndole al kernel cuando decís "abrí este archivo". +Porque la respuesta no es lo que pensás. + +## 1.1 El fd: solo un número entero positivo + +Cuando llamás a `fopen()`, te devuelve un puntero (`FILE *`). Por debajo, ese puntero apunta a una +estructura en tu espacio de memoria que contiene **un número entero**: el file descriptor (fd). + +Un file descriptor es **simple**. Demasiado simple para lo poderoso que es: + +> **Es solo un `int` mayor o igual a cero.** El kernel te asigna el próximo fd libre cuando llamás +> a `open()`, y te lo devuelve. Vos lo usás como un "handle" para decirle al kernel "quería leer de +> este recurso". Listo. Nada de punteros, nada de estructuras: solo un número. + +```text +fopen("datos.txt", "r") → FILE* → { fd = 3, ... } +open("datos.txt", O_RDONLY) → directamente el número: 3 +``` + +Por defecto, **todo proceso tiene tres fds abiertos al arrancar**: + +| Número | Nombre | Uso por defecto | +|---|---|---| +| `0` | `stdin` | Entrada estándar (teclado / redirección) | +| `1` | `stdout` | Salida estándar (pantalla / redirección) | +| `2` | `stderr` | Mensajes de error (siempre pantalla, a menos que lo desvíes) | + +El próximo `open()` te va a devolver `3`. El siguiente, `4`. Y así. El kernel siempre asigna el **fd +más bajo disponible**. + +> **¿Por qué empiezan en 0?** Porque el estándar C los reservó para `stdin`, `stdout` y `stderr` +> mucho antes de que existiera Linux. Cuando Linux llegó, simplemente respetó esa convención: cero, uno +> y dos ya estaban "ocupados" semánticamente. Si empezaban desde cero, `open()` empieza en tres. + +## 1.2 ¿Qué recursos tienen fd? + +Un file descriptor **no es solo para archivos**. Es un handle genérico del kernel para **cualquier recurso +I/O**: + +- Un archivo en disco (`/etc/passwd`, `/home/usuario/documento.txt`) +- Una tubería (pipe) entre procesos +- Un socket de red (TCP, UDP, Unix domain) +- Un dispositivo (`/dev/null`, `/dev/tty`) +- Un pipe named (FIFO) +- Un descriptor de memoria compartida + +**En Linux, todo es un archivo.** Esa es la filosofía del sistema. Cuando abris un socket con `socket()`, +el kernel te devuelve un fd igual que si hubieras abierto un archivo con `open()`. La diferencia está en +la **semántica** de lo que leás/escribas por ese fd, no en cómo lo accedés. + +```text +open("archivo.txt", O_RDONLY) → fd = 3 (disco) +socket(AF_INET, SOCK_STREAM, 0) → fd = 4 (red) +pipe(fd_array) → fd[0]=5, fd[1]=6 (IPC entre procesos) +``` + +## 1.3 El contrato con el kernel + +El trato es simple y brutalmente eficiente: + +1. **Vos llamás a `open()`** con una ruta y flags. El kernel verifica permisos, busca el inode y te + devuelve un fd. Costo de la llamada: algunas decenas de ciclos de CPU + acceso a disco si hace + falta buscar en el sistema de archivos. +2. **Usás el fd** para `read()` o `write()`. El kernel mantiene una tabla interna por proceso que mapea + cada fd al recurso subyacente (estructura de archivo del kernel). +3. **Llamás a `close()`** cuando terminás. El kernel libera la entrada en tu tabla y decrementa el + contador referenciado del recurso. + +No hay punteros, no hay estructuras que manejes vos, no hay "contexto" que lleves entre llamadas. +Solo un número. **Un número entero.** Y eso es lo que hace a las syscalls de Linux tan rápidas y tan +elegantes: el kernel solo necesita ese `int` para saber exactamente qué recurso estás tocando. + +### La tabla de descriptores por proceso + +Cada proceso tiene su propia tabla de fds. El fd `3` del proceso A puede apuntar a un archivo diferente +que el fd `3` del proceso B. Esto es porque **el fd no es un identificador global**: solo tiene sentido +dentro del contexto de un proceso. + +```text +Proceso A: Proceso B: + +fd 0 → stdin fd 0 → stdin +fd 1 → stdout fd 1 → stdout +fd 2 → stderr fd 2 → stderr +fd 3 → /tmp/a.txt fd 3 → /var/log/syslog +fd 4 → pipe[5→6] fd 4 → socket{192.168.1.1:80} +``` + +Dos procesos diferentes pueden tener el mismo número de fd apuntando a recursos completamente distintos. +El kernel los mantiene aislados gracias a que cada uno tiene **su propia tabla**. + +## Práctica de la Parte 1 + +### Ejercicio 22.1 — ¿Qué número soy? + +**Conceptos** · file descriptor, `/proc/self/fd/`, `open`, `close` + +**Contexto** · Para entender qué es un fd y cómo el kernel lo asigna, nada mejor que **mirar los propios +fds de tu proceso**. Linux expone esa información como un sistema de archivos virtual en +`/proc/self/fd/`. Cada entrada es un enlace simbólico que apunta al recurso real. + +**Consigna** · Escribí un programa que: + +1. Imprima el valor de `stdin` (`0`), `stdout` (`1`) y `stderr` (`2`). +2. Abra un archivo de prueba con `open()` y muestre qué fd le asignó el kernel. +3. Muestre los enlaces simbólicos de `/proc/self/fd/` para que el usuario vea a dónde apunta cada uno. + +**Entrada** · Ninguna: el programa no lee nada. +**Salida** · Los tres fds por defecto y el fd del archivo abierto, junto con la salida de `ls -l /proc/self/fd/`. + +**Escenario de prueba** + +```text +$ gcc 22_01.c -o inspect_fds && ./inspect_fds +stdin = 0 +stdout = 1 +stderr = 2 + +Archivo abierto → fd = 3 + +Enlaces en /proc/self/fd/: +lrwx------ 1 user user 64 Nov 5 10:30 0 -> /dev/pts/0 +lrwx------ 1 user user 64 Nov 5 10:30 1 -> /dev/pts/0 +lrwx------ 1 user user 64 Nov 5 10:30 2 -> /dev/pts/0 +lrwx------ 1 user user 64 Nov 5 10:30 3 -> ./archivo_prueba.txt +``` + +> El fd del archivo (`3` en este caso) apunta al archivo que abriste con `open()`. Los fds `0`, `1` y +> `2` apuntan a tu terminal (`/dev/pts/0` o similar). + +**Criterios de aceptación** + +- [ ] Imprime correctamente los valores de `stdin`, `stdout` y `stderr`. +- [ ] Usa `open()` para abrir un archivo y muestra el fd devuelto. +- [ ] Muestra los enlaces simbólicos de `/proc/self/fd/` (puede usar `system("ls -l /proc/self/fd/")` o leer con `readdir`). +- [ ] Cierra el archivo con `close()` antes de terminar. + +--- + +# PARTE 2 — Las cuatro funciones primitivas: `open`, `close`, `read`, `write` + +Con `FILE*` usás `fopen`, `fclose`, `fread` y `fwrite`. Con file descriptors directamente, las funciones +son **cinco letras**: `open`, `close`, `read`, `write`. Pero tienen un poder que `stdio` no tiene: +**control total**. + +## 2.1 `open`: abrir sin intermediarios + +```c +#include +#include + +int open(const char *pathname, int flags); +int open(const char *pathname, int flags, mode_t mode); +``` + +- **`pathname`**: la ruta del archivo. +- **`flags`**: uno o más de `O_RDONLY`, `O_WRONLY`, `O_RDWR`. Opcionalmente combinados con `\|` con + `O_CREAT`, `O_TRUNC`, `O_APPEND`, `O_EXCL`, etc. +- **`mode`** (solo si usás `O_CREAT`): los permisos del archivo nuevo (`0644`, `0600`, etc.). +- **Devuelve**: un fd entero positivo o `-1` en caso de error. + +A diferencia de `fopen`, que recibe un string como `"r"` o `"w"`, `open` usa **banderas binarias**. +Esto es importante: los flags son bits que combinás con OR, no strings arbitrarios. Cada combinación +tiene un significado preciso definido por POSIX. No hay ambigüedad como `"a+"` vs `"r+"` en `stdio`. + +```c +int fd = open("datos.txt", O_RDONLY); // solo lectura +int fd = open("datos.txt", O_WRONLY); // solo escritura +int fd = open("datos.txt", O_RDWR); // lectura y escritura +int fd = open("nuevo.txt", O_WRONLY | O_CREAT, 0644); // crear si no existe +``` + +> **Ojo con `O_CREAT` sin `mode`.** Si llamás a `open()` con `O_CREAT` pero **sin** el tercer argumento +> (`mode`), el resultado es **comportamiento indefinido**. El compilador no te puede avisar porque +> `open()` tiene un número variable de argumentos (`...`): si pasás menos de lo esperado, se rompe. +> **Siempre** pasá el modo cuando uses `O_CREAT`. + +## 2.2 `close`: liberar el descriptor + +```c +int close(int fd); +``` + +- Recibe el fd a cerrar. +- Devuelve `0` si salió bien, `-1` en caso de error (fd inválido, por ejemplo). + +Cuando cerrás un fd, el kernel libera la entrada en tu tabla de descriptores y decrementa el contador +referenciado del recurso subyacente (estructura de archivo, socket, etc.). Si es el último fd apuntando +al recurso, se libera completamente. + +**Si no cierras un fd:** el proceso no muere, pero consumís uno de los fds disponibles (normalmente 1024 +por defecto en Linux). Cuando se agotan, `open()` falla con `EMFILE`. + +```c +if (close(fd) == -1) { + perror("close"); // "close: Bad file descriptor" +} +``` + +## 2.3 `read`: sacar datos del fd + +```c +ssize_t read(int fd, void *buf, size_t count); +``` + +- Recibe el fd de donde leer, un buffer destino y la cantidad máxima de bytes a leer. +- Devuelve **la cantidad de bytes leídos**. Si devuelve `0`, llegaste al fin del archivo (`EOF`). Si + devuelve `-1`, hubo un error (ver `errno`). +- **No garantiza** que lea `count` bytes: puede devolver menos si el recurso no tiene más datos + disponibles todavía (es el caso de pipes, sockets y dispositivos). + +```c +char buffer[4096]; +ssize_t n = read(fd, buffer, sizeof(buffer)); +if (n == -1) { + perror("read"); +} else if (n == 0) { + // fin del archivo / fin del pipe cerrado +} else { + // leí n bytes en buffer[0..n-1] +} +``` + +### El patrón de lectura completa + +Para leer todo un archivo, el bucle clásico es: + +```c +char buf[4096]; +ssize_t total = 0; +ssize_t n; +while ((n = read(fd, buf, sizeof(buf))) > 0) { + total += n; +} +if (n < 0) { + perror("read"); +} +``` + +Este bucle lee bloques de 4096 bytes hasta que `read` devuelve `0` (EOF) o `-1` (error). Cada llamada +puede devolver menos de 4096 si el recurso no tiene más datos disponibles todavía. Es **normal** en pipes, +sockets y TTYs: el kernel entrega lo que hay disponible y te avisa que podés volver a leer cuando haya +más datos. + +## 2.4 `write`: poner datos en el fd + +```c +ssize_t write(int fd, const void *buf, size_t count); +``` + +- Recibe el fd destino, un buffer fuente y la cantidad de bytes a escribir. +- Devuelve **la cantidad de bytes escritos** (normalmente igual a `count`). Puede devolver menos en + algunos recursos (pipes, sockets), en cuyo caso debés reintentar con los bytes restantes. +- Devuelve `-1` en caso de error. + +```c +const char *msg = "Hola desde syscalls\n"; +ssize_t n = write(fd, msg, strlen(msg)); +if (n < 0) { + perror("write"); +} else if ((size_t)n != strlen(msg)) { + // escribiste menos de lo que querías — reintentá con los bytes restantes +} +``` + +### El patrón de escritura completa + +```c +ssize_t total = 0; +while (total < len) { + ssize_t n = write(fd, buf + total, len - total); + if (n < 0) { + perror("write"); + break; + } + total += n; +} +``` + +Este patrón garantiza que se escriban todos los bytes incluso si `write` no los escribe de una sola vez. + +## 2.5 stdio vs syscalls crudas: comparación directa + +| Operación | Con `stdio` (`FILE*`) | Sin intermediarios (fd) | +|---|---|---| +| Abrir | `fopen("f", "r")` → `FILE*` | `open("f", O_RDONLY)` → `int` | +| Leer | `fread(buf, 1, n, fp)` | `read(fd, buf, n)` | +| Escribir | `fwrite(buf, 1, n, fp)` | `write(fd, buf, n)` | +| Cerrar | `fclose(fp)` | `close(fd)` | +| Búfer | Automático (típicamente 8 KB) | **No hay búfer** — va directo al kernel | +| Error | `ferror(fp)`, `feof(fp)` | Retorna `-1`, revisá `errno` | + +La diferencia fundamental: **`stdio` tiene un búfer en tu espacio de usuario**. Cuando llamás a +`fread()`, puede que `stdio` haya pedido 8 KB al kernel de una vez y te esté dando trozos de ahí. +Con `read()` directo, cada llamada es **otra syscall** al kernel: más control pero potencialmente más +llamadas de sistema. + +> **¿Cuándo usar uno u otro?** Si solo trabajás con archivos locales, `stdio` es rápido y cómodo porque +> el búfer reduce syscalls. Si necesitás control fino (no bloqueante, multiplexación, fd compartidos +> entre procesos), las syscalls crudas son tu única opción. + +## Práctica de la Parte 2 + +### Ejercicio 22.2 — Copiadora cruda + +**Conceptos** · `open`, `close`, `read`, `write`, bucle de lectura/escritura, `errno` + +**Contexto** · Querés copiar un archivo sin usar ni una sola función de `stdio.h`. Solo syscalls crudas +y un búfer en tu pila. Este es el patrón fundamental que vas a repetir por todo este capítulo. + +**Consigna** · Escribí un programa que reciba dos argumentos por línea de comandos: archivo origen y archivo +destino. Copiá el contenido del origen al destino usando `open`, `read` y `write`. Usa un búfer fijo de +4096 bytes en la pila. + +**Entrada** · Dos rutas por argumento: `./copiadora original.txt copia.txt` + +**Salida** · Un archivo nuevo con el mismo contenido exacto que el origen. Imprimí `"Copiado: N bytes"` al final. + +**Escenario de prueba** + +```text +$ echo "hola mundo desde syscalls" > original.txt +$ gcc 22_02.c -o copiadora && ./copiadora original.txt copia.txt +Copiado: 31 bytes +$ diff original.txt copia.txt +(no output — los archivos son idénticos) +``` + +**Criterios de aceptación** + +- [ ] Abre el archivo origen con `open()` y verifica que no devuelve `-1`. +- [ ] Crea el archivo destino con `open(O_WRONLY | O_CREAT | O_TRUNC, 0644)`. +- [ ] Lee en bloques de hasta 4096 bytes con un bucle `while ((n = read(...)) > 0)`. +- [ ] Escribe cada bloque leído con `write()` al destino. +- [ ] Imprime la cantidad total de bytes copiados al finalizar. +- [ ] Cierra ambos fds con `close()`. + +### Ejercicio 22.3 — Lector por bloques fijos + +**Conceptos** · `read`, manejo de retornos parciales, EOF como `0` + +**Contexto** · A veces querés leer **exactamente N bytes**, incluso si el recurso no los tiene todos +de una vez. Esto es común con sockets y pipes: el kernel puede entregar 1 byte de los 10 que pediste. + +**Consigna** · Escribí una función `read_exact(int fd, void *buf, size_t count)` que lea **exactamente +`count` bytes** del fd, reintentando con `read()` hasta completar el buffer o llegar a EOF/error. + +**Entrada** · Un archivo de texto (podés usar el creado en 22.2). +**Salida** · El contenido completo leído en un buffer. + +**Criterios de aceptación** + +- [ ] La función reintentá `read()` hasta completar los bytes pedidos o encontrar EOF/error. +- [ ] Retorna la cantidad total de bytes leídos (o `-1` si hubo error). +- [ ] Maneja correctamente el caso donde `count` es `0`. +- [ ] No hace más syscalls de las necesarias. + +--- + +# PARTE 3 — Flags y permisos: el contrato con el kernel + +`open()` no solo abre archivos: puede crearlos, truncarlos, agregarles al final o fallar si ya existen. +Todo eso se controla con **flags** y **permisos**. Es un contrato binario con el kernel: le decís exactamente +qué querés hacer y el kernel lo hace **o falla**. + +## 3.1 Los flags de acceso: cómo abrí el recurso + +Los tres flags principales son mutuamente excluyentes: elegí **uno solo**: + +| Flag | Significado | Usá cuando... | +|---|---|---| +| `O_RDONLY` | Solo lectura | Querés leer, no escribir | +| `O_WRONLY` | Solo escritura | Querés escribir, no leer | +| `O_RDWR` | Lectura y escritura | Necesitás ambos | + +Combinados con OR (`|`) podés agregar comportamientos adicionales: + +| Flag | Efecto | Nota | +|---|---|---| +| `O_CREAT` | Crea el archivo si no existe | Requiere tercer argumento `mode` | +| `O_TRUNC` | Trunca (borra) el contenido existente | Solo con `O_WRONLY` o `O_RDWR` | +| `O_APPEND` | Escribe al final siempre | Cada `write()` se va al final automáticamente | +| `O_EXCL` | Falla si el archivo ya existe | Solo tiene sentido con `O_CREAT`; evita condiciones de carrera en la creación | +| `O_NONBLOCK` | Abre sin bloquear (ver Parte 6) | Para E/S no bloqueante | + +La combinación más común es: + +```c +// Abrir existente para lectura +int fd = open("datos.txt", O_RDONLY); + +// Crear o truncar para escritura +int fd = open("nuevo.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); + +// Agregar al final (no truncá) +int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0644); +``` + +> **Trampa N.º 1:** `O_TRUNC` borra **todo** el contenido del archivo antes de abrirlo. Si abrís con +> `O_RDWR | O_TRUNC`, no solo vacías el archivo sino que podés corromper datos si otro proceso lo tiene abierto. +> Siempre pensá dos veces antes de combinar `O_TRUNC` con `O_RDWR`. + +## 3.2 Permisos: `mode_t` y los octales de Linux + +Cuando creás un archivo con `O_CREAT`, necesitás especificar **permisos** en el tercer argumento. Los +permisos de Linux se representan como una máscara de 9 bits (más los setuid/setgid/sticky bits): + +```text +0755 = rw-r--r-- (propietario lee+escribe, grupo y otros solo leen) +0644 = rw-r--r-- (el permiso más común para archivos normales) +0600 = rw------- (solo el propietario puede leer/escribir) +``` + +Cada dígito octal representa tres bits: + +| Dígito | Significado binario | Permisos | +|---|---|---| +| `0` | `000` | Sin permisos | +| `1` | `--x` | Ejecución sola | +| `2` | `-w-` | Escritura sola | +| `3` | `-wx` | Escritura + ejecución | +| `4` | `r--` | Lectura sola | +| `5` | `r-x` | Lectura + ejecución | +| `6` | `rw-` | Lectura + escritura | +| `7` | `rwx` | Lectura + escritura + ejecución | + +El orden es: **propietario**, **grupo**, **otros**. Por ejemplo, `0644`: + +```text +Propietario: 6 = rw- (puede leer y escribir) +Grupo: 4 = r-- (solo puede leer) +Otros: 4 = r-- (solo pueden leer) +``` + +> **Ojo con los ceros iniciales.** En C, un número que empieza con `0` se interpreta como **octal**, +> no decimal. `0644` es el valor correcto. Si escribís `644`, estás pidiendo permisos binarios `1010000100` +> (muy diferente a lo que querés). + +## 3.3 `O_APPEND`: atomicidad de escritura al final del archivo + +Cuando usás `O_APPEND`, el kernel mueve automáticamente el puntero de posición **al final del archivo** +antes de cada `write()`. Esto garantiza que **cada write se va al final sin pisar nada**, incluso si +múltiples procesos escriben al mismo tiempo. + +> **¿Por qué importa?** Sin `O_APPEND`, dos procesos podrían hacer `write()` y los bytes se entrelazarían +> o pisan datos del otro. Con `O_APPEND`, el kernel garantiza atomicidad a nivel de cada syscall: cada +> escritura va al final sin interferir con otras. + +## 3.4 `O_EXCL`: creación atómica, sin condiciones de carrera + +Si dos procesos llaman a `open("archivo", O_CREAT | O_WRONLY)` al mismo tiempo y el archivo **no existe**, +ambos podrían crearlo y uno pisaría al otro. Con `O_EXCL`, la combinación `O_CREAT | O_EXCL` **falla si el +archivo ya existe**, garantizando que solo un proceso logra crearlo: + +```c +int fd = open("mi_archivo.txt", O_WRONLY | O_CREAT | O_EXCL, 0644); +if (fd == -1 && errno == EEXIST) { + // otro proceso ya creó el archivo — manejá el error +} +``` + +Esto es la base de los **candados por archivo** en sistemas distribuidos: "yo gano si `open(O_EXCL)` funciona, +el otro pierde si devuelve `EEXIST`". + +## Práctica de la Parte 3 + +### Ejercicio 22.4 — Creador con permisos estrictos + +**Conceptos** · `O_CREAT`, `mode_t`, `O_EXCL`, `errno`, `perror`/`strerror` + +**Contexto** · Querés crear un archivo que **solo el propietario pueda leer**, pero si ya existe querés +fallar en vez de abrirlo para escritura. Este es el patrón típico de configuración segura: archivos de +credenciales o claves deben ser inaccesibles para otros usuarios. + +**Consigna** · Escribí un programa que reciba dos argumentos por línea de comandos: ruta del archivo y una +cadena a escribir. El programa debe: + +1. Crear el archivo con `O_WRONLY | O_CREAT | O_EXCL` y permisos `0600`. +2. Si el archivo ya existe, mostrar un error claro. +3. Escribir la cadena recibida en el archivo. +4. Mostrar los permisos reales del archivo creado (usando `ls -l` vía `system()` o leyendo `/proc/self/fd/`). + +**Entrada** · Ruta y cadena: `./creador_seguro ./secret.txt "contraseña123"` + +**Salida** · El archivo creado con permisos `0600`, la cadena escrita y confirmación. + +**Escenario de prueba** + +```text +$ gcc 22_04.c -o creador_seguro && ./creador_seguro ./secret.txt "contraseña123" +Archivo ./secret.txt creado con éxito (permisos: 0600) +$ ls -l ./secret.txt +-rw------- 1 user user 15 Nov 5 10:30 ./secret.txt +$ cat ./secret.txt +contraseña123 +$ ./creador_seguro ./secret.txt "otra cosa" +Error: no se pudo crear ./secret.txt: File exists (El archivo ya existe) +``` + +**Criterios de aceptación** + +- [ ] Usa `O_CREAT | O_EXCL` para creación atómica. +- [ ] Permisos `0600` (`rw-------`). +- [ ] Detecta y reporta correctamente el error `EEXIST`. +- [ ] Verifica que el retorno de cada syscall no sea `-1`. + +### Ejercicio 22.5 — Comportamiento atómico de `O_APPEND` + +**Conceptos** · `O_APPEND`, escritura concurrente, condiciones de carrera sin bloqueo + +**Contexto** · Sin `O_APPEND`, dos procesos escribiendo al mismo archivo pueden entrelazar sus bytes. +Querés **demostrar** que ocurre el caos y luego **mostrar** que `O_APPEND` lo resuelve. + +**Consigna** · Escribí un programa que reciba una ruta de archivo como argumento y: + +1. Abra el archivo en modo `O_WRONLY | O_CREAT` (**sin** `O_APPEND`). +2. Escriba un mensaje corto con su PID incluido (ejemplo: `"Hijo-42\n"`). +3. Repita esto 5 veces con una pequeña pausa (`usleep(100)`) entre escrituras. + +Luego, corré este programa **dos veces en paralelo** sobre el mismo archivo y observá cómo se entrelazan +los mensajes. Después repetí con `O_APPEND` y observá que cada escritura queda al final del todo. + +**Entrada** · Ruta de un archivo (ejemplo: `./append_test log.txt`). +**Salida** · El contenido del archivo tras correr el programa dos veces en paralelo. + +**Criterios de aceptación** + +- [ ] Primero demostra la condición de carrera **sin** `O_APPEND`. +- [ ] Luego demostrá que con `O_APPEND` los mensajes no se entrelazan (cada PID aparece completo y al final). +- [ ] Usa `getpid()` para identificar cada proceso en el mensaje. + +--- + +# PARTE 4 — `lseek`: manipulación directa del cursor en el kernel + +Con `stdio`, si querés saber dónde estás parados en un archivo usás `ftell()`. Con syscalls crudas, +el control es aún más directo: **movés el puntero del kernel** donde quieras. + +## 4.1 ¿Por qué mover el puntero? + +Imaginá que tenés un archivo grande y solo querés leer los últimos 64 bytes sin leer todo lo anterior. +Con `stdio`, tendrías que hacer `fseek(fp, -64, SEEK_END)` — que por debajo hace exactamente esto: +`lseek(fd, -64, SEEK_END)`. + +`lseek` te permite: + +1. **Saltar** a cualquier posición sin leer los bytes intermedios. +2. **Medir** el tamaño de un archivo moviéndote al final. +3. **Preposicionar** el cursor para escrituras secuenciales (ejemplo: escribir al inicio sin truncar). +4. **Multiplexar** con `select`/`poll` (ver Parte 6): saber si hay datos disponibles en una posición concreta. + +## 4.2 La función + +```c +#include + +off_t lseek(int fd, off_t offset, int whence); +``` + +- **`fd`**: el file descriptor del archivo. +- **`offset`**: desplazamiento en bytes (puede ser negativo). +- **`whence`**: punto de referencia para el desplazamiento: + - `SEEK_SET`: desde el inicio del archivo (`offset` bytes desde el byte 0) + - `SEEK_CUR`: desde la posición actual (`posición + offset`) + - `SEEK_END`: desde el final del archivo (`tamaño + offset`) + +Devuelve **la nueva posición** del cursor, o `-1` en caso de error. + +```c +// Ir al inicio para leer desde el principio +lseek(fd, 0, SEEK_SET); + +// Ir 10 bytes antes del final para leer los últimos 10 +lseek(fd, -10, SEEK_END); + +// Posicionarse 5 bytes después de donde estoy parado. +lseek(fd, 5, SEEK_CUR); +``` + +> **Importante:** `lseek` con `offset = 0` y `whence = SEEK_END` devuelve el **tamaño del archivo**. +> Es el truco universal para averiguar cuánto mide un archivo sin leerlo: vas al final, preguntás +> dónde llegaste y volvés atrás. + +## 4.3 El patrón de obtener el tamaño de un archivo + +```c +// Ir al final +lseek(fd, 0, SEEK_END); +// Preguntar dónde estamos (eso es el tamaño) +long size = lseek(fd, 0, SEEK_CUR); +// Volver al inicio para leer desde el principio +lseek(fd, 0, SEEK_SET); +``` + +Este patrón funciona con **cualquier** fd que soporte posicionamiento: archivos en disco, pero no así +pipes, sockets o TTYs. En esos recursos, `lseek` devuelve `-1` con `errno == ESPIPE`. + +> **¿Por qué `SEEK_END` + `SEEK_CUR` y no directamente `ftell`?** Porque estamos en el mundo de las +> syscalls crudas. No hay `ftell`. `lseek(fd, 0, SEEK_CUR)` es el equivalente directo: preguntar al kernel +> "¿dónde estoy parado ahora?" sin moverme. + +## 4.4 Escribir sin truncar: preposicionar con `lseek` + +Si abres un archivo con `O_WRONLY` (sin `O_TRUNC`), podés usar `lseek` para escribir en cualquier posición +sin borrar lo que había antes: + +```c +// Abrir existente para escritura (sin truncar) +int fd = open("datos.txt", O_WRONLY); + +// Ir al byte 100 del archivo +lseek(fd, 100, SEEK_SET); + +// Escribir a partir de ahí — los primeros 100 bytes quedan intactos. +const char *data = "reemplazo\n"; +write(fd, data, strlen(data)); +``` + +Esto es útil para actualizar registros de tamaño fijo en un archivo: buscás el registro con `lseek`, +lo sobreescribís con `write` y listo. No tocás nada más. + +## Práctica de la Parte 4 + +### Ejercicio 22.6 — Buscador de ocurrencias binarias + +**Conceptos** · `lseek`, `SEEK_SET`, `read`, búsqueda en archivo binario + +**Contexto** · Querés encontrar todas las apariciones de una secuencia de bytes dentro de un archivo +binario grande. Para hacerlo eficiente, leés bloques y usás `lseek` para reposicionar sin volver +al inicio cada vez. + +**Consigna** · Escribí un programa que reciba como argumentos: ruta del archivo, patrón a buscar (hex) +y muestre la posición (offset en bytes) de cada ocurrencia del patrón dentro del archivo. + +**Entrada** · Ruta del archivo + patrón hexadecimal (ejemplo: `./buscador datos.bin DEADBEEF`) + +**Salida** · Lista de offsets donde se encontró el patrón. + +**Criterios de aceptación** + +- [ ] Lee el archivo en bloques usando `read()`. +- [ ] Usa `lseek` para reposicionar correctamente entre bloques (manejando superposición del patrón). +- [ ] Compara bytes crudos contra el patrón. +- [ ] Imprime cada offset encontrado. + +### Ejercicio 22.7 — Cálculo de tamaño con `lseek` y `SEEK_END` + +**Conceptos** · `lseek`, `SEEK_END`, `SEEK_CUR`, cálculo de tamaño + +**Contexto** · No hay función estándar para obtener el tamaño de un fd sin abrirlo como `FILE*`. +El patrón universal es moverse al final con `SEEK_END` y preguntar la posición con `SEEK_CUR`. + +**Consigna** · Escribí una función `get_file_size(int fd)` que devuelva el tamaño en bytes del archivo +al que apunta `fd`, asumiendo que el fd ya fue abierto. El fd debe quedar reposicionado al final +después de la llamada (no hay problema). + +**Criterios de aceptación** + +- [ ] Usa `lseek(fd, 0, SEEK_END)` seguido de `lseek(fd, 0, SEEK_CUR)`. +- [ ] Devuelve el tamaño como `long` o `off_t`. +- [ ] Maneja correctamente archivos vacíos (tamaño 0). + +--- + +# PARTE 5 — Concurrencia y bloqueos (`fcntl` / `flock`) + +Dos procesos escribiendo al mismo archivo sin coordinación = datos corrompidos. Linux ofrece mecanismos +de bloqueo advisory que resuelven este problema. + +## 5.1 El problema: condiciones de carrera en escritura + +Imaginá dos procesos ejecutando esto simultáneamente: + +```c +// Proceso A // Proceso B +open("shared.log", O_WRONLY | O_APPEND); open("shared.log", O_WRONLY | O_APPEND); +write(fd, "A\n", 2); write(fd, "B\n", 2); +close(fd); close(fd); +``` + +Sin `O_APPEND`, el resultado en el archivo podría ser `"AB\n"` (mezclado) o `"BA\n"`. Con `O_APPEND` +el kernel garantiza atomicidad por syscall, pero si necesitás leer-modificar-escribir de forma atómica, +necesitás bloqueos. + +## 5.2 `flock`: bloques simples y prácticos + +```c +#include + +int flock(int fd, int operation); +``` + +- **`LOCK_SH`**: bloqueo compartido (varios procesos pueden tenerlo). Solo lectura. +- **`LOCK_EX`**: bloqueo exclusivo (solo un proceso puede tenerlo). Lectura/escritura. +- **`LOCK_UN`**: libera el bloqueo. +- **`LOCK_NB`**: bandera no bloqueante — si no se puede adquirir el bloqueo, devuelve `-1` con `EWOULDBLOCK` + en vez de esperar. + +```c +// Bloqueo exclusivo: solo yo puedo escribir ahora. +flock(fd, LOCK_EX); +write(fd, "mi dato\n", 8); +flock(fd, LOCK_UN); + +// Bloqueo compartido: cualquier lector puede tenerlo a la vez. +flock(fd, LOCK_SH); +read(fd, buf, sizeof(buf)); +flock(fd, LOCK_UN); +``` + +> **Advisory vs mandatory.** Los bloqueos `flock` son **advisory**: el kernel solo los respeta si todos +> los procesos que acceden al archivo *saben* y *usan* `flock`. Si un proceso abre el archivo sin usar +> bloqueos, puede escribir lo que quiera. No hay coerción a nivel de sistema de archivos. + +## 5.3 `fcntl`: locking más granular + +```c +#include + +struct flock { + short l_type; // F_RDLCK, F_WRLCK, F_UNLCK + short l_whence; // SEEK_SET, SEEK_CUR, SEEK_END + off_t l_start; // offset del inicio del lock + off_t l_len; // longitud del lock (0 = hasta EOF) + pid_t l_pid; // PID que tiene el bloqueo (solo al leer con F_GETLK) +}; + +int fcntl(int fd, int cmd, struct flock *lock); +``` + +`fcntl` permite bloquear **regiones específicas** de un archivo, no solo el archivo completo. Esto es +útil cuando tenés archivos con múltiples registros y querés que los procesos accedan a diferentes registros +sin interferirse. + +```c +struct flock lock; +lock.l_type = F_WRLCK; +lock.l_whence = SEEK_SET; +lock.l_start = 0; +lock.l_len = 1024; // bloqueo exclusivo de los primeros 1024 bytes + +fcntl(fd, F_SETLK, &lock); // intentar bloqueo (no bloquea si no puede) +// o bien: +fcntl(fd, F_SETLKW, &lock); // intentar bloqueo (bloquea hasta que pueda) +``` + +> **`F_SETLK` vs `F_SETLKW`.** El sufijo `W` significa "wait" (esperar). `F_SETLK` intenta y falla +> inmediatamente si el bloqueo no está disponible. `F_SETLKW` bloquea hasta que consiga el lock. + +## 5.4 Buenas prácticas con bloqueos + +1. **Bloqueá el menor tiempo posible.** El tiempo que mantengás un lock es tiempo que otros procesos + quedan esperando. Hacé solo las operaciones necesarias dentro del lock y soltalo rápido. +2. **Siempre liberá en todos los caminos.** Si tenés salidas anticipadas (`return`, `break`) dentro + de la sección crítica, asegurate de liberar el lock antes de cada una. +3. **El lock se libera al cerrar el fd o terminar el proceso.** Pero no dependas de eso: liberá explícitamente. + +## Práctica de la Parte 5 + +### Ejercicio 22.8 — Escritor sincronizado con `flock` + +**Conceptos** · `flock`, `LOCK_EX`, escritura atómica, condiciones de carrera + +**Contexto** · Escribí un programa que cree N procesos hijos, cada uno escribiendo líneas en el mismo +archivo. Sin bloqueos, las líneas se mezclan. Con `flock`, cada escritura queda intacta. + +**Consigna** · Escribí un programa que: + +1. Reciba por argumento la cantidad de writers y el archivo destino. +2. Cree N procesos hijos con `fork()`. +3. Cada hijo escribe 10 líneas en formato `"PID-XXXX\n"` (donde XXXX es un número único) al archivo, + usando `flock(LOCK_EX)` antes de cada escritura y `flock(LOCK_UN)` después. +4. El padre espera a todos los hijos con `wait()`. + +**Criterios de aceptación** + +- [ ] Usa `fork()` para crear writers paralelos. +- [ ] Cada escritura individual está protegida por `flock(LOCK_EX)` / `flock(LOCK_UN)`. +- [ ] Las líneas en el archivo final no están mezcladas (cada línea es completa). +- [ ] El padre espera a todos los hijos con `wait()`. + +### Ejercicio 22.9 — Detector de condiciones de carrera + +**Conceptos** · `flock`, `LOCK_EX`, `fcntl`, demostración de race condition + +**Contexto** · Este ejercicio te pide que **demuestres** el problema sin bloqueos y su solución con ellos. +Es un programa que podés correr de dos maneras: una corrupta, una correcta. + +**Consigna** · Escribí un programa que reciba dos argumentos: `modo` (`sin-lock` o `con-lock`) y ruta +de archivo. El programa abre el archivo, hace `flock(LOCK_EX)` si el modo es `con-lock`, escribe +3 veces con pausa intermedia de 10ms, y cierra. + +Corré **cinco instancias en paralelo** del mismo programa (tres sin-lock, tres con-lock) y compará +los resultados para ver la diferencia. + +**Criterios de aceptación** + +- [ ] Funciona tanto en modo `sin-lock` como `con-lock`. +- [ ] En modo `con-lock`, las escrituras no se superponen. +- [ ] En modo `sin-lock`, podés observar líneas mezcladas o truncadas. +- [ ] El programa informa qué modo está usando al arrancar. + +--- + +# PARTE 6 — E/S no bloqueante (`O_NONBLOCK`) y multiplexación (`select`, `poll`, `epoll`) + +Imaginá que estás leyendo del teclado y el usuario todavía no escribió nada. Con `read()` normal, tu +programa **se queda esperando** indefinidamente. No podés hacer otra cosa hasta que escriba. + +La solución: **E/S no bloqueante**. Le decís al kernel "si no hay datos, avisame pero no me pares". + +## 6.1 Activar `O_NONBLOCK` + +Hay dos formas de activarlo: + +```c +// Opción 1: en open() (no funciona con todos los tipos de fd) +int fd = open("datos.txt", O_RDONLY | O_NONBLOCK); + +// Opción 2: con fcntl() (funciona con todo tipo de fds) +int flags = fcntl(fd, F_GETFL); // obtiene los flags actuales +fcntl(fd, F_SETFL, flags | O_NONBLOCK); // agrega O_NONBLOCK +``` + +Cuando el fd está en modo no bloqueante, si llamás `read()` y **no hay datos**, devuelve `-1` con +`errno == EAGAIN` o `EWOULDBLOCK` (son lo mismo en Linux). No se para: te avisa "ahora no hay nada". + +```c +char buf[256]; +ssize_t n = read(fd, buf, sizeof(buf)); +if (n > 0) { + // hay datos — procesalos +} else if (n == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + // no hay datos ahora. puedo hacer otra cosa y volver a intentarlo después. +} else if (n < 0) { + // error real + perror("read"); +} +``` + +## 6.2 `select()`: la abuela de la multiplexación + +`select()` te permite preguntar al kernel: "de estos fds, **¿cuáles están listos para leer/ escribir?**" +Le pasás un conjunto de fds y el kernel te devuelve cuáles cambiaron. + +```c +#include + +int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout); +``` + +- **`nfds`**: el mayor fd + 1 (para saber cuántos fds chequear). +- **`readfds`**: fds que querés monitorear para lectura. +- **`writefds`**: fds que querés monitorear para escritura. +- **`errorfds`**: fds que querés monitorear para errores. +- **`timeout`**: cuánto esperar (o `NULL` = esperar indefinidamente, o `0` = no esperar nada). + +Macros de manejo: + +| Macro | Qué hace | +|---|---| +| `FD_ZERO(fd_set *)` | Limpia el set | +| `FD_SET(int fd, fd_set *)` | Agrega un fd al set | +| `FD_CLR(int fd, fd_set *)` | Quita un fd del set | +| `FD_ISSET(int fd, fd_set *)` | Verifica si un fd está en el set (devuelve 1 sí) | + +```c +fd_set reads; +FD_ZERO(&reads); +FD_SET(STDIN_FILENO, &reads); // monitorear stdin +FD_SET(socket_fd, &reads); // monitorear socket + +struct timeval tv; +tv.tv_sec = 5; // 5 segundos +tv.tv_usec = 0; // 0 microsegundos + +int ready = select(max_fd + 1, &reads, NULL, NULL, &tv); + +if (ready < 0) { + perror("select"); +} else if (ready == 0) { + printf("timeout — nadie escribió en 5 segundos\n"); +} else { + // Hay fds listos. Verificar cuáles: + if (FD_ISSET(STDIN_FILENO, &reads)) { + read(STDIN_FILENO, buf, sizeof(buf)); + } + if (FD_ISSET(socket_fd, &reads)) { + read(socket_fd, buf, sizeof(buf)); + } +} +``` + +> **Limitación de `select`:** el límite de fds que podés monitorear es limitado (típicamente 1024 +> en Linux). Si necesitás manejar miles de conexiones simultáneas, usá `poll` o `epoll`. + +## 6.3 `poll()`: sin límite de fds + +`poll()` funciona como `select` pero usa una **struct** por fd en lugar de bitmaps fijos: + +```c +#include + +struct pollfd { + int fd; // file descriptor a monitorear + short events; // eventos que me importan + short revents; // eventos que ocurrieron (devuelve el kernel) +}; + +int poll(struct pollfd *fds, nfds_t nfds, int timeout); +``` + +Eventos comunes: + +| Evento | Significado | +|---|---| +| `POLLIN` | Hay datos para leer | +| `POLLOUT` | Podés escribir sin bloquear | +| `POLLERR` | Error en el fd | +| `POLLHUP` | Colgado (hung up) | +| `POLLNVAL` | fd no válido | + +```c +struct pollfd fds[2]; +fds[0].fd = STDIN_FILENO; +fds[0].events = POLLIN; +fds[1].fd = socket_fd; +fds[1].events = POLLIN; + +int ready = poll(fds, 2, -1); // -1 = esperar indefinidamente + +if (ready < 0) { perror("poll"); } +else if (ready > 0) { + if (fds[0].revents & POLLIN) { + read(STDIN_FILENO, buf, sizeof(buf)); + } + if (fds[1].revents & POLLIN) { + read(socket_fd, buf, sizeof(buf)); + } +} +``` + +## 6.4 `epoll()`: la evolución de Linux (bonus) + +`select` y `poll` recopilan información sobre **todos** los fds que les pasás en cada llamada. Si tenés +10.000 conexiones activas y solo 3 tienen datos, desperdiciaste tiempo chequeando las otras 9.997. + +`epoll` (Linux-specific) resuelve esto con un **árbol interno**: registrás los fds una vez y el kernel +te avisa **solo** cuando cambian. No hay límite de fds, es O(1) por evento. + +```c +int epfd = epoll_create1(0); // crear instancia epoll +struct epoll_event ev; +ev.events = EPOLLIN; // monitorear lectura +ev.data.fd = socket_fd; +epoll_ctl(epfd, EPOLL_CTL_ADD, socket_fd, &ev); // registrar fd + +struct epoll_event events[10]; +int n = epoll_wait(epfd, events, 10, -1); // esperar eventos + +for (int i = 0; i < n; i++) { + read(events[i].data.fd, buf, sizeof(buf)); +} +``` + +> **Nota:** `epoll` es específico de Linux. No está disponible en macOS o Windows. Si necesitás +> portabilidad entre sistemas, quedate con `poll`. + +## Práctica de la Parte 6 + +### Ejercicio 22.10 — Lector no bloqueante con `O_NONBLOCK` + +**Conceptos** · `O_NONBLOCK`, `fcntl`, `EAGAIN`/`EWOULDBLOCK`, `errno` + +**Contexto** · Escribí un programa que lea de stdin en modo no bloqueante y, mientras no hay datos, +imprima un "." cada segundo como indicador de actividad. + +**Consigna** · Configurar stdin como no bloqueante con `fcntl()`. En un bucle infinito: intentar leer; +si no hay datos (`EAGAIN`), esperar 1 segundo e imprimir "."; si hay datos, procesarlos normalmente. +Salir al detectar EOF o error. + +**Criterios de aceptación** + +- [ ] Usa `fcntl(fd, F_SETFL, flags | O_NONBLOCK)` para configurar stdin como no bloqueante. +- [ ] Cuando no hay datos, el programa no se para: espera 1 segundo e imprime ".". +- [ ] Cuando hay datos en stdin, los procesa normalmente (`read()` devuelve bytes reales). +- [ ] Termina limpiamente al detectar EOF o error real. + +### Ejercicio 22.11 — Monitor multicanal con `select`/`poll` + +**Conceptos** · `select`, `poll`, `FD_SET`, `POLLIN`, multiplexación de descriptores + +**Contexto** · Escribí un programa que monitoree stdin Y un pipe (entre procesos) al mismo tiempo. +Cuando haya datos en cualquiera de los dos, leer y procesar del canal disponible. Si ningún canal +tiene datos en 10 segundos, imprimir timeout. + +**Consigna** · El programa crea un pipe con `pipe()`, luego se fork(). El padre escribe fechas/hora +en el pipe cada segundo. El hijo usa `select()` (o `poll()`) para monitorear stdin Y el read-end +del pipe. Cuando hay datos en stdin, los imprime como "Entrada usuario: ...". Cuando hay datos en el +pipe, los imprime como "Reloj: ...". Si ninguno tiene datos en 10 segundos, imprime "timeout". + +**Criterios de aceptación** + +- [ ] Crea un pipe y fork() correctamente. +- [ ] El hijo usa `select()` o `poll()` para monitorear stdin + pipe-read-end simultáneamente. +- [ ] Distingue claramente entre datos de stdin vs datos del pipe en la salida. +- [ ] Maneja correctamente el timeout (10 segundos sin actividad). +- [ ] Bonus: usar ambos (`select` y `poll`) en secciones separadas para comparar. + +--- + +# PARTE 7 — `/proc/self/fd/`: el mapa interno del kernel + +Linux te muestra sus propios recursos como si fueran archivos normales. `/proc/self/fd/` es un directorio +virtual donde cada entrada es un **enlace simbólico** que apunta al recurso real de cada fd. + +## 7.1 ¿Qué es `/proc/self/fd/`? + +```bash +$ ls -l /proc/self/fd/ +lrwx------ 1 user user 64 Nov 5 10:30 0 -> /dev/pts/0 +lrwx------ 1 user user 64 Nov 5 10:30 1 -> /dev/pts/0 +lrwx------ 1 user user 64 Nov 5 10:30 2 -> /dev/pts/0 +lrwx------ 1 user user 64 Nov 5 10:30 3 -> /home/user/archivo.txt +``` + +Cada entrada (`0`, `1`, `2`, `3`...) es el fd. El `->` muestra a qué recurso apunta. Esto te permite +**inspeccionar en tiempo real** los archivos que tu programa tiene abiertos, sin herramientas externas. + +## 7.2 Leer `/proc/self/fd/` desde C + +Podés abrir `/proc/self/fd/` como un directorio normal con `opendir()`/`readdir()` e inspeccionar +cada symlink con `readlink()`: + +```c +#include +#include +#include + +DIR *dir = opendir("/proc/self/fd"); +if (dir != NULL) { + struct dirent *entry; + while ((entry = readdir(dir)) != NULL) { + // Solo entries numéricos (0, 1, 2, 3...) + if (entry->d_name[0] >= '0' && entry->d_name[0] <= '9') { + char path[PATH_MAX]; + snprintf(path, sizeof path, "/proc/self/fd/%s", entry->d_name); + + char target[PATH_MAX]; + ssize_t len = readlink(path, target, sizeof(target) - 1); + if (len != -1) { + target[len] = '\0'; + printf("fd %s -> %s\n", entry->d_name, target); + } + } + } + closedir(dir); +} +``` + +Este patrón es la base de herramientas de diagnóstico como `lsof` y `strace`. Te permite ver **qué +archivos tiene abiertos tu proceso** en tiempo real. + +## 7.3 Uso práctico: auditoría de recursos + +¿Tu programa se queda sin fds? ¿Alguien dejó un archivo abierto por error? Con `/proc/self/fd/` podés: + +- Contar cuántos fds tiene abiertos el proceso. +- Identificar fd's que apuntan a archivos temporales olvidados. +- Detectar leaks de memoria (archivos que deberían haberse cerrado). + +## Práctica de la Parte 7 + +### Ejercicio 22.12 — Auditor y explorador de fds activos + +**Conceptos** · `/proc/self/fd/`, `opendir`, `readdir`, `readlink` + +**Contexto** · Escribí un programa que liste todos los fds abiertos por el propio proceso, mostrando +el número de fd, su tipo (archivo, terminal, pipe) y el recurso al que apunta. + +**Consigna** · El programa debe: + +1. Abrir `/proc/self/fd/` como directorio con `opendir()`. +2. Recorrer cada entrada, ignorando "." y "..". +3. Para cada fd numérico, usar `readlink()` para obtener el recurso al que apunta. +4. Mostrar un reporte en formato legible: número de fd, tipo (archivo/terminal/pipe/socket) y ruta. + +**Criterios de aceptación** + +- [ ] Usa `opendir("/proc/self/fd/")` para listar los fds. +- [ ] Usa `readlink()` para obtener el recurso real de cada fd. +- [ ] Ignora entradas no numéricas. +- [ ] Muestra un reporte claro y ordenado por número de fd. + +--- + +# PARTE 8 — `dup` y `dup2`: clonación y redirección + +Dos números apuntando al mismo recurso: eso es lo que hacen `dup()` y `dup2()`. Son la base absoluta +de cómo los shells redirigen `stdout` a un archivo o conectan programas con tuberías. + +## 8.1 `dup`: clonar un fd + +```c +#include + +int dup(int oldfd); +``` + +Crea una **copia** del fd pasado como argumento. El nuevo fd apunta al **mismo recurso** que el original: + +```c +// Clonar stdout (fd 1) en fd 5 +int fd5 = dup(1); // ahora fd 1 y fd 5 apuntan al mismo archivo/terminal + +// Escribir a cualquiera de los dos escribe en el mismo lugar. +write(fd5, "hola\n", 5); // aparece en stdout porque fd5 == clon de stdout +write(1, "mundo\n", 6); // también aparece en stdout (mismo recurso) + +close(fd5); // liberar la copia cuando no la necesitemas. +``` + +> **No es una copia del archivo.** Es una copia de la **entrada en la tabla de descriptores**. Ambos fds +> comparten el mismo puntero de posición, los mismos flags (incluido `O_APPEND`), y cualquier escritura +> por uno afecta lo que el otro ve. + +## 8.2 `dup2`: clonar y elegir el destino + +```c +#include + +int dup2(int oldfd, int newfd); +``` + +Igual que `dup()`, pero vos elegís **cuál** será el nuevo fd: + +- Si `newfd` ya estaba abierto, se cierra primero. +- Si `newfd == oldfd`, devuelve `oldfd` sin hacer nada (no hay error). +- Devuelve el nuevo fd o `-1` en error. + +```c +int save_stdout = dup(1); // guardar una copia de stdout actual + +// Redirigir stdout a un archivo +int log_fd = open("salida.log", O_WRONLY | O_CREAT | O_TRUNC, 0644); +dup2(log_fd, 1); // ahora fd 1 (stdout) apunta al archivo + +printf("Esto va al archivo, no a pantalla\n"); // stdout fue reemplazado por dup2! + +dup2(save_stdout, 1); // restaurar stdout original +close(save_stdout); +close(log_fd); +``` + +> **Este es el patrón de redirección de shells.** Cuando escribís `comando > archivo` en la terminal, +> lo que hace el shell internamente es exactamente esto: abre el archivo, usa `dup2()` para reemplazar +> fd 1 con el nuevo fd, y ejecuta el comando. El programa no sabe que stdout fue redirigido. + +## 8.3 Redirigir stderr también + +El mismo patrón funciona para `stderr` (fd 2): + +```c +int err_fd = open("errores.log", O_WRONLY | O_CREAT | O_TRUNC, 0644); +dup2(err_fd, 2); // stderr ahora apunta al archivo +perror("este error va a errores.log"); +close(err_fd); +``` + +Y para redirigir stdout y stderr al mismo destino: + +```c +int log_fd = open("todo.log", O_WRONLY | O_CREAT | O_TRUNC, 0644); +dup2(log_fd, 1); // stdout → archivo +dup2(log_fd, 2); // stderr → mismo archivo +close(log_fd); // el fd original ya no lo necesitamos (dos duplo comparten recurso) +``` + +## Práctica de la Parte 8 + +### Ejercicio 22.13 — Redirección manual de stdout a archivo + +**Conceptos** · `dup`, `dup2`, redirección, `close` + +**Contexto** · Querés que **todo** lo que tu programa escribe por `stdout` vaya a un archivo en vez +de la pantalla. Usás `dup()` para guardar el stdout original y `dup2()` para reemplazarlo temporalmente. + +**Consigna** · Escribí un programa que: + +1. Guarde una copia del fd actual de stdout con `dup(1)`. +2. Abra un archivo `captura.txt` en modo escritura. +3. Use `dup2(log_fd, 1)` para redirigir stdout al archivo. +4. Escriba varias líneas por `printf` (que ahora van al archivo). +5. Restaure el stdout original con `dup2(saved_stdout, 1)`. +6. Imprima un mensaje de confirmación en pantalla. + +**Criterios de aceptación** + +- [ ] Usa `dup(1)` para guardar una copia del stdout original. +- [ ] Abre el archivo destino correctamente con `open()`. +- [ ] Usa `dup2()` para reemplazar fd 1 con el fd del archivo. +- [ ] Verifica que los `printf` van al archivo (no a pantalla). +- [ ] Restaura stdout con `dup2(saved, 1)` antes de terminar. + +### Ejercicio 22.14 — Canal invertido (fd a stdin) + +**Conceptos** · `dup`, `dup2`, redirección inversa, pipe write-end → fd0 + +**Contexto** · Escribí un programa que cree una tubería con `pipe()`, escriba datos en el write-end +y luego use `dup2()` para hacer que **stdin (fd 0)** apunte a ese mismo pipe. De esta forma, cuando +el programa lee de stdin (con `read(0, ...)` o `getchar()`), está leyendo del pipe. + +**Consigna** · El programa debe: + +1. Crear un pipe con `pipe()`. +2. Escribir datos en el write-end del pipe. +3. Cerrar ambos ends originales del pipe. +4. Usar `dup2(pipe[0], 0)` para clonar el read-end sobre fd 0 (stdin). +5. Leer de stdin normalmente y mostrar lo que se leyó. + +> Esto es esencialmente cómo los shells implementan tuberías (`|`): conectan stdout de un proceso +> al stdin del siguiente usando `dup2()`. + +**Criterios de aceptación** + +- [ ] Crea el pipe con `pipe()`. +- [ ] Escribe datos en el write-end antes de clonar. +- [ ] Usa `dup2(pipe[0], 0)` correctamente. +- [ ] Lee stdin y obtiene los datos del pipe. +- [ ] Cierra todos los fds originales del pipe después del dup. + +--- + +# PARTE 9 — Ejercicio jefe: demonio de logs concurrente, rotativo y multicanal + +Llegó el momento de juntar todo lo que aprendiste en un solo programa que unifica llamadas crudas, +bloqueos contra procesos concurrentes, multiplexación y rotación atómica. + +## 9.1 El desafío completo + +Escribí un **demonio de logs** que: + +1. Escriba registros a un archivo usando **exclusivamente syscalls crudas** (`open`, `write`, `close`), + con **cero uso de `stdio.h`**. Cada registro tiene el formato: `[TIMESTAMP] PID Mensaje\n`. +2. Implemente sincronización entre procesos usando `flock()` para evitar condiciones de carrera cuando + múltiples instancias escriben simultáneamente al mismo archivo de log. +3. Soporte **rotación automática** cuando el archivo supera N KB (por ejemplo, 5 KB): renombra `app.log` + → `app.log.1` y recrea el archivo de forma atómica (`rename()` + nuevo `open`). +4. Gestione entradas de comandos o señales de forma asíncrona mediante **E/S no bloqueante** (`O_NONBLOCK`) + y supervisión de descriptores con `select()` o `poll()`. Los comandos válidos son: + - `FLUSH` → imprime estadísticas (total de bytes escritos, cantidad de registros) + - `STATUS` → muestra el estado actual del daemon (archivo activo, tamaño, rotaciones realizadas) + - `QUIT` → cierra limpiamente todos los recursos y termina +5. Redireccione **stdout y stderr** al archivo de log mediante `dup2()` para capturar la salida de + cualquier subproceso heredado. + +## 9.2 Estructura sugerida + +```c +// Estructura principal del daemon +typedef struct { + int log_fd; // fd del archivo de log actual + char log_path[256]; // ruta del archivo (ej: /var/log/app.log) + long total_bytes; // total de bytes escritos en toda la vida del daemon + long total_records; // cantidad de registros escritos + int rotations; // cantidad de rotaciones realizadas +} LogDaemon; + +// Funciones principales: +void log_init(LogDaemon *daemon, const char *path); +void log_write(LogDaemon *daemon, const char *msg, size_t len); +void log_rotate(LogDaemon *daemon); +void log_flush(LogDaemon *daemon); +void log_quit(LogDaemon *daemon); +void handle_command(LogDaemon *daemon, const char *cmd); + +// main(): bucle principal que monitorea stdin con select/poll +int main(void) { + // 1. Inicializar daemon + // 2. Redirigir stdout/stderr al log con dup2() + // 3. Configurar stdin como no bloqueante con O_NONBLOCK + // 4. Bucle principal: + // - select/poll espera datos en stdin + // - si hay datos, leer comando y procesar + // - verificar tamaño del log y rotar si es necesario + // 5. Al salir (QUIT o señal): liberar recursos, restaurar fds, cerrar todo +} +``` + +## 9.3 Puntos clave de implementación + +### Rotación atómica + +Usá `rename()` para mover el archivo actual a `.log.N` y abrir uno nuevo: + +```c +void log_rotate(LogDaemon *daemon) { + // Cerrar el archivo actual si está abierto + if (daemon->log_fd >= 0) close(daemon->log_fd); + + // Renombrar con incremento en el número + char new_path[256]; + snprintf(new_path, sizeof new_path, "%s.%d", daemon->log_path, daemon->rotations); + rename(daemon->log_path, new_path); // atómico en POSIX + + // Crear nuevo archivo + daemon->log_fd = open(daemon->log_path, O_WRONLY | O_CREAT | O_APPEND, 0644); + daemon->rotations++; +} +``` + +### Verificación de tamaño + +Usá `lseek(fd, 0, SEEK_END)` para obtener el tamaño actual del archivo sin leerlo. + +### Comandos asíncronos con poll + +Configurá stdin no bloqueante y usá `poll()` para detectar cuándo hay datos disponibles: + +```c +struct pollfd pfd; +pfd.fd = STDIN_FILENO; +pfd.events = POLLIN; + +while (1) { + int ret = poll(&pfd, 1, -1); // espera indefinida + + if (ret > 0 && (pfd.revents & POLLIN)) { + char cmd[256]; + ssize_t n = read(STDIN_FILENO, cmd, sizeof(cmd) - 1); + if (n > 0) { + cmd[n] = '\0'; + // Remover el \n final + if (n > 0 && cmd[n-1] == '\n') cmd[n-1] = '\0'; + handle_command(daemon, cmd); + } + } +} +``` + +> **Recuerda:** `handle_command()` debe llamar a `log_flush()` para `FLUSH`, imprimir el estado +> del daemon para `STATUS`, y llamar a `log_quit()` para salir limpiamente. + +### Restaurar stdout/stderr al terminar + +Cuando el daemon reciba `QUIT`, restaurá los fds originales antes de salir: + +```c +void log_quit(LogDaemon *daemon) { + // Restaurar stdout/stderr desde las copias guardadas + dup2(daemon->saved_stdout, 1); + dup2(daemon->saved_stderr, 2); + + if (daemon->log_fd >= 0) close(daemon->log_fd); + + // Cerrar las copias de seguridad + close(daemon->saved_stdout); + close(daemon->saved_stderr); +} +``` + +## 9.4 Ejercicio: implementación completa + +### Ejercicio 22.15 — JEFE FINAL: Rotador avanzado de logs tolerante a concurrencia y E/S no bloqueante + +**Conceptos** · `open`, `write`, `close`, `lseek`, `flock`, `dup2`, `O_NONBLOCK`, `poll`/`select`, +rotación con `rename()`, redirección de streams, manejo asíncrono de comandos + +**Contexto** · Un daemon de logs de producción real necesita escribir a disco sin bloquear el hilo +principal, sincronizar escrituras entre múltiples procesos, rotar archivos automáticamente y aceptar +comandos interactivos. Este ejercicio implementa todo eso con syscalls crudas. + +**Consigna** · Implementá el demonio de logs descrito en la sección 9.1 siguiendo la estructura sugerida +en 9.2 y los puntos clave de 9.3. El programa debe ser robusto, limpio y funcional. + +**Requisitos funcionales:** +- Escritura de logs con syscalls crudas (cero `stdio.h`) +- Sincronización con `flock()` para escrituras concurrentes +- Rotación automática del archivo cuando supera 5 KB +- Comandos asíncronos por stdin: `FLUSH`, `STATUS`, `QUIT` +- Redirección de stdout y stderr al archivo de log +- Restauración limpia de fds al terminar + +**Entrada** · El programa se ejecuta en la consola. Escribe comandos por stdin mientras el daemon corre. + +**Salida** · Archivos de log rotados (`app.log`, `app.log.1`, `app.log.2`...), estadísticas impresas +en stdout/stderr cuando corresponde, y terminación limpia con `QUIT`. + +**Criterios de aceptación** + +- [ ] No usa **ninguna función de ``** para escribir logs. Usa `open()`/`write()`/`close()`. +- [ ] Usa `flock(LOCK_EX)` para proteger cada escritura al archivo de log. +- [ ] Rota el archivo automáticamente cuando supera 5 KB usando `rename()` y recreación. +- [ ] Acepta comandos `FLUSH`, `STATUS` y `QUIT` de forma no bloqueante (stdin + `poll`/`select`). +- [ ] Redirige stdout y stderr al archivo de log con `dup2()`. +- [ ] Restaurá los fds originales (`stdout`/`stderr`) al recibir `QUIT` antes de terminar. +- [ ] Cierra todos los recursos limpiamente al salir (log_fd, saved_stdout, saved_stderr). +- [ ] Maneja correctamente el caso donde se reciben múltiples comandos en una misma línea. + +**Bonus:** + +- Soportar múltiples archivos rotados (`app.log.1`, `app.log.2`, ... `app.log.N`). +- Recibir señal SIGUSR1 para forzar rotación sin necesidad de comando. +- Guardar el PID del daemon en un archivo `.pid` para permitir señales externas. + + diff --git a/book/23-procesos.md b/book/23-procesos.md new file mode 100644 index 0000000..9ea5db8 --- /dev/null +++ b/book/23-procesos.md @@ -0,0 +1,1077 @@ +# Capítulo 23 — Procesos en Paralelo: Fork, Exec y la Vida de un Proceso + +> Capítulo avanzado, pero **escrito para que se entienda de verdad**. Se apoya en file descriptors +> (`22-file-descriptors`), memoria dinámica (`09`) y structs (`08`). También asume que ya escribiste +> programas con syscalls crudas. +> +> Este capítulo te saca del mundo monocoral y te mete dentro del kernel: cada programa que corre tiene +> su propio espacio de memoria, sus propias variables y su propio identificador. `fork`, `exec`, `wait`, +> zombies, pipes, señales... acá aprendés a controlar la vida y muerte de otros procesos desde C. + +--- + +## Cómo leer este capítulo + +Llegó el momento en que tu programa deja de ser **el único que corre**. Linux ejecuta miles de procesos +al mismo tiempo: servidores, shells, editors, browsers. Cada uno con su espacio de memoria aislado, +sus archivos abiertos y su propio identificador numérico. + +Este capítulo va desde lo básico (`getpid`, `fork()`) hasta aplicaciones avanzadas (mini-shell paralelo, +copiador recursivo concurrente). Si entendés cada parte, vas a poder escribir programas que **crean otros +programas**, **coordinan entre ellos** y **monitorean su progreso**. + +Está dividido en **ocho partes**. Después de cada concepto hay una o más prácticas —uno o varios +ejercicios— para que los uses ya mismo. **No las saltees**: son la mitad del capítulo. + +| Parte | De qué habla | La idea en una frase | +|---|---|---| +| **1** | ¿Qué es un proceso? | Cada programa tiene su propio espacio de memoria y su ID: el kernel los aísla unos de otros. | +| **2** | `fork()`: clonarte a ti mismo | Crear un hijo es detenerte en el tiempo y crear un gemelo idéntico que sigue por su lado. | +| **3** | `exec*`: reemplazarte completamente | Lanzar otro programa es cambiar la cara del proceso sin perder el PID ni los fds abiertos. | +| **4** | `wait`, `waitpid`: esperar a que terminen | El hijo zombie muere solo cuando el padre lo recoge: `wait` es la tarea pendiente que nadie salta. | +| **5** | Estado de salida: exit code y señales | Tu proceso puede "morir" bien, con código de error, o ser asesinado por una señal del sistema. | +| **6** | Variables de entorno y `argv` | Cada proceso vive con su conjunto de variables; podés verlas, filtrarlas y pasarlas al lanzar otro. | +| **7** | UID, GID, umask: tu identidad en Linux | No solo tenés un PID: tenés usuario real/efectivo, grupo, y el umask define qué permisos heredás. | +| **8** | Ejercicio jefe: copiador recursivo paralelo | Spawnea un hijo por archivo + `waitpid(WNOHANG)` + estadísticas en tiempo real de copia. | + +> **Regla de oro del libro:** el capítulo explica y muestra **cómo funciona** cada syscall, pero +> **los ejercicios no traen la solución**. El enunciado te da el problema; la respuesta la escribís vos. + +--- + +# PARTE 1 — ¿Qué es un proceso? + +Antes de hablar de cómo crear procesos, entendamos qué es uno y por qué importa. + +## 1.1 Un espacio de memoria aislado + +Cada programa que ejecutás en Linux tiene su propio **espacio de direcciones virtuales**. Eso significa: + +- Tus variables **no existen** en el proceso vecino. +- Tu heap, tu stack, tu código binario —todo— está en una zona de memoria que **solo vos** podés tocar. +- Si hacés `*pointer = 42` y ese puntero pertenece a otro proceso, el kernel te mata con `SIGSEGV`. + +Esta aislamiento es lo que hace posible que Linux sea un sistema multi-tarea seguro: ningún programa +puede corromper la memoria de otro. + +```text +Proceso A (PID 100) Proceso B (PID 200) +┌──────────────┐ ┌──────────────┐ +│ código │ │ código │ +│ datos │ │ datos │ +│ heap │ │ heap │ +│ stack │ │ stack │ +└──────────────┘ └──────────────┘ + PID = 100 PID = 200 + UID = horacio UID = horacio +``` + +## 1.2 El PID: tu número de identidad + +Cada proceso tiene un **PID (Process ID)** que lo identifica de forma única en el sistema: + +```c +#include +#include + +int main(void) { + printf("Mi PID: %d\n", getpid()); // Mi propio PID + printf("Mi padre (PPID): %d\n", getppid()); // PID de mi padre + + return 0; +} +``` + +- **`getpid()`**: devuelve el PID del proceso que lo llama. +- **`getppid()`**: devuelve el PID del proceso padre que creó este proceso. +- **`getuid()`**: devuelve el UID real del usuario que ejecuta el proceso. +- **`geteuid()`**: devuelve el UID efectivo (puede ser distinto por setuid). +- **`getgid()`** / **`getegid()`**: mismas funciones para grupos. + +> **El PID 1.** Cuando Linux arranca, el primer proceso que inicia se llama `init` (o `systemd`). Tiene PID 1 +> y es el ancestro de todos los demás. Nadie puede tener padre con PID 0 excepto procesos del kernel mismo. +> Cuando un proceso muere sin ser reclamado por su padre, se convierte en **zombie** y queda como PID huérfano +> hasta que `init` lo recoja. + +## 1.3 El árbol de procesos + +Los procesos forman un **árbol jerárquico**. Cada proceso (excepto PID 1) tiene exactamente un padre: + +```text +PID 1 (systemd/init) +├── PID 42 (bash shell) +│ ├── PID 101 (gcc -o prog prog.c) +│ └── PID 102 (./prog) +├── PID 55 (sshd: server) +│ └── PID 200 (user@pts/0) +│ └── PID 300 (vim archivo.c) +``` + +Cuando `PID 101` muere, el shell (`PID 42`) lo reclama con `wait()`. Cuando el shell sale, `init` reclama +sus hijos huérfanos y los limpia. + +## Práctica de la Parte 1 + +### Ejercicio 23.1 — ¿Quién soy? + +**Conceptos** · `getpid`, `getppid`, `getuid`, `geteuid`, `getgid` + +**Contexto** · Querés entender tu identidad como proceso. Linux te da varias identidades: PID, PPID, +UID real, UID efectivo, GID y más. Cada una tiene un significado distinto. + +**Consigna** · Escribí un programa que imprima toda la información de identidad del proceso actual: +PID, PPID, UID real, UID efectivo, GID real, GID efectivo, y nombre de usuario/grupo si es posible. + +**Entrada** · Ninguna. +**Salida** · Lista completa de identidades del proceso. + +**Escenario de prueba** + +```text +$ gcc 23_01.c -o whoami && ./whoami +Proceso: ¿Quién soy? +==================== +PID: 4512 +PPID: 4510 (bash) +UID real: 1000 +UID efectivo: 1000 +GID real: 1000 +GID efectivo: 1000 +``` + +**Criterios de aceptación** + +- [ ] Usa `getpid()` y `getppid()` correctamente. +- [ ] Usa `getuid()`, `geteuid()`, `getgid()`, `getegid()`. +- [ ] Muestra la información en un formato legible y ordenado. +- [ ] Incluye al menos 6 campos de identidad. + +--- + +# PARTE 2 — `fork()`: clonarte a ti mismo + +`fork()` es la syscall más poderosa de Linux. Crea una **copia exacta** de tu proceso: mismas variables, +mismos archivos abiertos, mismo código. Solo que ahora hay dos procesos corriendo simultáneamente. + +## 2.1 La magia en una línea + +```c +#include + +pid_t fork(void); +``` + +- Devuelve **el PID del hijo** al proceso padre. +- Devuelve **`0`** al proceso hijo. +- Devuelve **`-1`** si no se pudo crear el proceso (sin memoria, sin recursos). + +La misma línea de código se ejecuta **dos veces**: una en el padre, otra en el hijo. Cada uno con +su propio espacio de memoria: + +```c +int valor = 42; +pid_t pid = fork(); + +if (pid == -1) { + perror("fork falló"); + return 1; +} else if (pid > 0) { + // Esto SOLO lo ejecuta el PADRE. pid es el PID del hijo. + printf("Soy el padre (PID %d). Mi hijo es el PID %d.\n", getpid(), pid); + valor = 100; // El hijo NO VE este cambio: tiene su propia copia de `valor` +} else { + // Esto SOLO lo ejecuta el HIJO. pid es 0. + printf("Soy el hijo (PID %d). Mi padre es PID %d.\n", getpid(), getppid()); + valor = 999; // El padre NO VE este cambio +} + +// Aquí ambos llegan, pero por caminos diferentes: +printf("Valor final: %d\n", valor); // Padre imprime 100, hijo imprime 999 +``` + +> **`fork` es copy-on-write.** El kernel no copia físicamente la memoria del proceso al hacer `fork()`. +> Marca las páginas como "solo lectura" y solo las copia cuando uno de los dos procesos intenta escribir. +> Esto hace que `fork()` sea sorprendentemente rápido incluso para procesos con gigabytes de memoria. + +## 2.2 El patrón fork-then-exec: crear otro programa + +El uso más común de `fork()` es: clonarte a ti mismo y luego **reemplazar** la copia con otro programa +usando `exec*`. Así el hijo deja de ser tu proceso y se convierte en lo que le digas: + +```c +pid_t pid = fork(); + +if (pid == 0) { + // Hijo: reemplazate por "ls -la" + execl("/bin/ls", "ls", "-la", NULL); + + // Si exec llega acá, falló. Manejá el error. + perror("execl falló"); + _exit(1); // Salir SIN fflush ni exit(): solo syscalls +} + +// Padre: seguís corriendo. Esperás al hijo con wait(). +``` + +> **`_exit()` vs `exit()`.** En el hijo, después de un `exec` fallido, usá `_exit(1)` (syscall directa) +> no `exit(1)`. `exit()` hace cleanup de stdio (fflush, handlers) que puede corromper si el padre y el hijo +> comparten buffers. `_exit()` es más limpio: cierra fds, libera memoria, termina. + +## 2.3 Variables independientes: el gemelo que se diferencia + +Después de `fork()`, padre e hijo comparten **copia inicial** de todo. Pero a partir de ahí son +completamente independientes: + +```c +int counter = 0; +fork(); +counter++; // Padre incrementa su counter a 1, hijo al 1 — pero son variables distintas. +printf("%d\n", counter); // Imprime "1" dos veces, no "2" +``` + +Este comportamiento explica por qué algunos programas imprimen cosas dos veces después de hacer `fork()`: +porque **dos procesos ejecutan el mismo código**. Si tenés un `printf` después de `fork()` y no lo separás +con `if (pid == 0) ... else if (pid > 0) ...`, ambos procesos lo ejecutarán. + +## Práctica de la Parte 2 + +### Ejercicio 23.2 — Gemelos digitales + +**Conceptos** · `fork`, `getpid`, `getppid`, variables independientes, `if/else` post-fork + +**Contexto** · Querés crear un proceso hijo que calcule algo por su cuenta sin interferir con el padre. +Cada uno opera sobre sus propias variables. + +**Consigna** · Escribí un programa que: + +1. Cree un proceso hijo con `fork()`. +2. El padre sumará números del 1 al 1000 y mostrará el resultado. +3. El hijo multiplicará los mismos números y mostrará el resultado. +4. Cada uno imprimirá su PID para diferenciarse. + +**Entrada** · Ninguna. +**Salida** · Resultado de la suma (padre) y del producto (hijo), junto con sus PIDs. + +**Criterios de aceptación** + +- [ ] Usa `fork()` para crear el hijo. +- [ ] El padre realiza la suma, el hijo el producto. +- [ ] Cada proceso imprime su PID con `getpid()`. +- [ ] Las variables son independientes (no hay interferencia). + +### Ejercicio 23.3 — Árbol de procesos + +**Conceptos** · `fork`, estructura de árbol, profundidad variable + +**Contexto** · Creá un programa que cree un **árbol de procesos**: cada proceso puede tener hasta N hijos. +Es una forma visual de entender la jerarquía padre-hijo. + +**Consigna** · Escribí un programa que reciba dos argumentos: `profundidad` y `ramificacion`. +En cada nivel del árbol, el proceso actual crea `ramificacion` hijos. Cada proceso imprime su PID, +su PPID y el nivel en el árbol antes de terminar (o esperar a sus hijos). + +**Criterios de aceptación** + +- [ ] Crea correctamente un árbol de profundidad N con ramificación M. +- [ ] Cada proceso imprime su PID y PPID. +- [ ] Los padres esperan a todos sus hijos antes de imprimir "Nivel X completado". +- [ ] Maneja correctamente casos base (profundidad 0). + +--- + +# PARTE 3 — `exec*`: reemplazarte completamente + +Después de `fork()`, el hijo sigue siendo tu proceso. Pero si llamás a `exec*`, **dejas de ser vos**. +Todo tu código, tus variables, tu stack —todo— se reemplaza por otro programa. El PID permanece igual, +pero el contenido es completamente distinto. + +## 3.1 Las variantes de `exec` + +Hay siete funciones `exec`. Se diferencian en cómo pasás los argumentos y el path: + +| Función | Path absoluto | Args como... | Terminado con... | +|---|---|---|---| +| `execl` | Sí (`/bin/ls`) | Separados | `NULL` | +| `execle` | Sí | Separados | `NULL` + envp[] | +| `execlp` | No (busca en PATH) | Separados | `NULL` | +| `execv` | Sí (`/bin/ls`) | `char *argv[]` | — | +| `execvp` | No (busca en PATH) | `char *argv[]` | — | +| `execvpe` | No (busca en PATH) | `char *argv[]` + envp[] | — | + +Las más útiles son: + +**`execlp`**: path relativo (busca en PATH), args separados, NULL al final: + +```c +// Ejecuta "ls -la /tmp" +execlp("ls", "ls", "-la", "/tmp", NULL); +``` + +Nota el primer argumento es **el nombre del programa** (como aparece en `ps`), después los argumentos. + +**`execvp`**: path relativo, args como arreglo: + +```c +char *args[] = {"ls", "-la", "/tmp", NULL}; +execvp("ls", args); +``` + +> **`p` significa "PATH".** `execlp` y `execvp` buscan el executable en las rutas de `PATH`. +> `execl` y `execv` necesitan la ruta completa: `/bin/ls`, `/usr/bin/python3`, etc. + +## 3.2 El patrón fork-exec completo + +```c +#include +#include +#include +#include + +pid_t pid = fork(); + +if (pid < 0) { + perror("fork falló"); + exit(1); +} else if (pid == 0) { + // HIJO: reemplazate por "wc -l archivo.txt" + char *argv[] = {"wc", "-l", "archivo.txt", NULL}; + execvp(argv[0], argv); + + // Si llegamos acá, exec FALLÓ (el programa no se ejecutó) + perror("exec falló"); + _exit(1); // Importante: usar _exit, no exit() +} + +// PADRE: seguís corriendo. Esperar al hijo. +int status; +waitpid(pid, &status, 0); +printf("Hijo terminó con status %d\n", WEXITSTATUS(status)); +``` + +## 3.3 Herencia de file descriptors + +Cuando hacés `fork()`, el hijo hereda **todos los fds abiertos** del padre: stdin (0), stdout (1), stderr (2), +y cualquier fd que hayas abierto con `open()` o `pipe()`. Esto es útil para redirección: + +```c +// El hijo hereda el archivo de log abierto con dup2(stdout) +int log_fd = open("app.log", O_WRONLY | O_CREAT | O_APPEND, 0644); +dup2(log_fd, 1); // stdout ahora apunta al archivo + +pid_t pid = fork(); +if (pid == 0) { + // Hijo: todo lo que imprima por printf va al archivo de log + execlp("mi_programa", "mi_programa", NULL); +} +``` + +## Práctica de la Parte 3 + +### Ejercicio 23.4 — Lanzador de comandos + +**Conceptos** · `fork`, `execvp`, manejo de errores, `_exit` + +**Contexto** · Escribí un programa que reciba una línea de comando por stdin y la ejecute usando +`fork` + `execvp`. Debe soportar cualquier comando que el usuario escriba. + +**Consigna** · El programa lee líneas de stdin con `fgets()`, las separa en argumentos (por espacios), +y ejecuta cada una como un comando con `execvp()`. Si el comando no se encuentra, imprime error. + +**Entrada** · Comandos por línea: `ls -la`, `grep foo *.txt`, `date` + +**Salida** · Salida del comando ejecutado. + +**Criterios de aceptación** + +- [ ] Usa `fgets()` para leer líneas de stdin. +- [ ] Separa la línea en argumentos (splitea por espacio). +- [ ] Usa `fork()` y `execvp()` correctamente. +- [ ] Maneja errores de exec con `perror()` y `_exit(1)` en el hijo. + +### Ejercicio 23.5 — "Mi propio shell" + +**Conceptos** · `fork`, `execvp`, `waitpid`, bucle interactivo, parsing de comandos + +**Contexto** · Escribí un shell mínimo que ejecute comandos del usuario, con soporte para: +comandos simples, argumentos y redirección básica (`>`). + +**Consigna** · Un bucle infinito que: + +1. Imprima el prompt `$ ` +2. Lea un comando con `fgets()` +3. Parsee el comando (separar en tokens) +4. Detecte si hay redirección (`> archivo`) y abra/prepare el archivo destino +5. Ejecute con `fork()` + `execvp()` + +**Criterios de aceptación** + +- [ ] Bucle interactivo que termina con `exit` o EOF. +- [ ] Soporta comandos simples con argumentos. +- [ ] Soporta redirección básica con `>`. +- [ ] Espera cada comando antes de seguir (`waitpid`). + +--- + +# PARTE 4 — `wait`, `waitpid`: esperar a que terminen + +Cuando creás un hijo con `fork()`, el proceso sigue corriendo después del fork. Si querés saber **cuándo +terminó** y **cómo terminó**, necesitás `wait()` o `waitpid()`. + +## 4.1 `wait()`: la forma simple de esperar + +```c +#include + +pid_t wait(int *status); +``` + +- Bloquea el padre hasta que **cualquier hijo** termine. +- Devuelve el PID del hijo que terminó. +- Escribe el estado de salida en `*status` (macros para interpretar). +- Si no hay hijos, devuelve `-1`. + +```c +pid_t pid = fork(); +if (pid == 0) { + _exit(42); // Hijo termina con código 42 +} + +int status; +pid_t child = wait(&status); +// child == pid +// WEXITSTATUS(status) == 42 +printf("Hijo %d terminó con código %d\n", child, WEXITSTATUS(status)); +``` + +## 4.2 `waitpid()`: el control fino + +```c +#include + +pid_t waitpid(pid_t pid, int *status, int options); +``` + +- **`pid > 0`**: espera solo al proceso con ese PID. +- **`pid == -1`**: espera a cualquier hijo (igual que `wait()`). +- **`pid == 0`**: espera a cualquier hijo del mismo grupo de procesos. +- **`pid < -1`**: espera a cualquier hijo del grupo `-pid`. + +**Options:** +| Option | Significado | +|---|---| +| `0` | Bloquea hasta que el hijo termine (comportamiento por defecto) | +| `WNOHANG` | No bloquea: devuelve inmediatamente si el hijo no terminó | +| `WUNTRACED` | También devuelve si el hijo fue detenido (para debugging) | + +```c +// No bloqueante: revisar sin esperar +pid_t result = waitpid(-1, &status, WNOHANG); +if (result == 0) { + // El hijo aún está corriendo. Puedo hacer otra cosa. +} else if (result > 0) { + // Hijo terminó con este PID. Revisar status. +} else { + // Error o no hay hijos. +} +``` + +> **`WNOHANG`** es clave para el ejercicio jefe de este capítulo: permite monitorear múltiples hijos +> sin bloquearse mientras unos terminan y otros siguen trabajando. + +## 4.3 Interpretar `status`: macros esenciales + +El `int status` que devuelve `wait()`/`waitpid()` es un valor comprimido. Usá las macros para leerlo: + +| Macro | Qué devuelve | +|---|---| +| `WIFEXITED(status)` | Verdadero si el hijo terminó normalmente (`_exit`, `exit`, o retorno de main) | +| `WEXITSTATUS(status)` | Código de salida (0-255). **Solo válido si** `WIFEXITED` es verdadero | +| `WIFSIGNALED(status)` | Verdadero si el hijo fue asesinado por una señal (segfault, SIGKILL...) | +| `WTERMSIG(status)` | Número de la señal que mató al hijo. **Solo válido si** `WIFSIGNALED` es verdadero | + +```c +if (WIFEXITED(status)) { + printf("Terminó normalmente con código %d\n", WEXITSTATUS(status)); +} else if (WIFSIGNALED(status)) { + printf("Asesinado por señal %d%s\n", WTERMSIG(status), + WTERMSIG(status) == SIGSEGV ? " (segfault)" : ""); +} +``` + +## 4.4 El zombie: el hijo que no muere + +Cuando un hijo termina, su espacio de memoria se libera, pero **queda una entrada en la tabla de procesos** +con su estado de salida hasta que el padre lo reclame con `wait()`. A esto se llama **zombie**: + +```text +PID STATUS COMMAND +450 S+ bash +451 Z+ mi_proceso ← zombie! esperando wait() +``` + +El zombie no consume CPU ni memoria significativa, pero sí una entrada en la tabla de procesos. +Si el padre nunca llama a `wait()`, los zombies se acumulan y eventualmente el sistema no puede crear nuevos procesos (`EAGAIN`). + +```c +// Si el padre muere antes que el hijo, el hijo queda huérfano y pasa a ser hijo de init (PID 1). +// Init llama a wait() periódicamente para limpiar todos los zombies. +// Por eso nunca ves un zombie con PID huérfano: init lo limpia automáticamente. +``` + +## Práctica de la Parte 4 + +### Ejercicio 23.6 — Contador de procesos terminados + +**Conceptos** · `wait`, `waitpid`, `WEXITSTATUS`, `WIFEXITED` + +**Contexto** · Creá N procesos hijos y contá cuántos terminaron con éxito (código 0) y cuántos fallaron +(código distinto de 0). Usá `waitpid()` para controlar qué hijo esperás. + +**Consigna** · El programa recibe como argumento la cantidad de hijos a crear. Cada hijo elige aleatoriamente +si termina con éxito (exit code 0) o falla (exit code 1-255). El padre espera a todos, cuenta los éxitos +y fracasos, e imprime un resumen. + +**Criterios de aceptación** + +- [ ] Crea N procesos hijos con `fork()`. +- [ ] Cada hijo elige aleatoriamente exit code (éxito o fracaso). +- [ ] El padre usa `waitpid()` para esperar a cada uno. +- [ ] Cuenta y reporta éxitos vs fracasos correctamente. + +### Ejercicio 23.7 — Zombie factory: crear y limpiar zombies + +**Conceptos** · `waitpid`, zombies, señales de limpieza + +**Contexto** · Este ejercicio es **solo diagnóstico**: creá un programa que produzca zombies intencionalmente +para luego entender por qué aparecen y cómo limpiarlos. + +**Consigna** · Escribí dos programas: + +1. **zombie_factory.c**: Crea 3 hijos con `fork()`, pero el padre **nunca llama a `wait()`**. Los hijos + terminan inmediatamente. Observá el comportamiento con `ps`. +2. **zombie_cleaner.c**: Igual que factory, pero el padre llama a `waitpid(-1, NULL, WNOHANG)` en un bucle + para limpiar zombies sin bloquearse. + +**Criterios de aceptación** + +- [ ] zombie_factory: los 3 hijos aparecen como `` hasta que el proceso termina. +- [ ] zombie_cleaner: ninguno aparece como zombie; `waitpid` con WNOHANG los limpia correctamente. +- [ ] Ambos programas terminan limpiamente al final. + +--- + +# PARTE 5 — Estado de salida: exit code y señales + +Cuando un proceso "muere", lo hace de una de dos formas: **terminó normalmente** (con código de error) o +fue **asesinado por una señal** (segfault, SIGKILL, etc.). + +## 5.1 Código de salida: el último número que imprimes + +```c +int main(void) { + return 42; // exit code 42 +} +// O equivalentemente: +int main(void) { + exit(42); // lo mismo +} +// O con syscall directa: +int main(void) { + _exit(42); // sin cleanup de stdio +} +``` + +En la línea de comandos, Linux guarda el código de salida en `$?`: + +```bash +$ ./prog && echo "OK" || echo "FAIL" # chequea exit code implícitamente +$ echo $? # muestra: 0 (éxito) o distinto (fallo) +$ false; echo $? # falso siempre devuelve 1 +1 +$ ls /no/existe; echo $? # archivo no encontrado = 2 +2 +``` + +> **Regla informal:** código `0` = éxito. Código `1-255` = fallo (cada número puede tener un significado). +> Códigos mayores a 255 se truncan a 8 bits: `exit(300)` equivale a `exit(44)`. + +## 5.2 Señales: el asesino silencioso + +Un proceso puede ser matado por señales enviadas por el sistema, otros procesos o él mismo: + +| Señal | Número | Causa típica | +|---|---|---| +| `SIGINT` | 2 | Ctrl+C del usuario | +| `SIGKILL` | 9 | `kill -9 PID` (no se puede capturar) | +| `SIGSEGV` | 11 | Segmentation fault (acceso inválido a memoria) | +| `SIGABRT` | 6 | `abort()` llamado explícitamente | +| `SIGPIPE` | 13 | Escribir a un pipe sin lectores | + +```c +#include + +void handler(int sig) { + printf("Recibí señal %d\n", sig); +} + +signal(SIGINT, handler); // Capturar Ctrl+C y actuar en vez de morir +``` + +## Práctica de la Parte 5 + +### Ejercicio 23.8 — Analizador de salidas + +**Conceptos** · `waitpid`, macros `WIFEXITED`, `WEXITSTATUS`, `WIFSIGNALED`, `WTERMSIG` + +**Contexto** · Escribí un programa que ejecute múltiples hijos (cada uno con exit code diferente o señal) +y analice el estado de cada uno después de que terminen. + +**Consigna** · Crea 5 hijos: + +1. Uno que termina con `exit(0)` (éxito). +2. Uno que termina con `exit(42)` (fallo con código específico). +3. Uno que se mata a sí mismo con `raise(SIGSEGV)` (segfault). +4. Otro que usa `_exit(127)`. +5. Un último que termina con `exit(255)`. + +El padre espera a todos y para cada uno imprime: PID, si terminó normalmente o por señal, y el código/señal. + +**Criterios de aceptación** + +- [ ] Crea 5 hijos con comportamientos distintos. +- [ ] Usa `waitpid()` para esperar a cada uno. +- [ }`Usa correctamente las macros para interpretar `status`. +- [ ] El output es claro y muestra tanto códigos como señales. + +### Ejercicio 23.9 — "¿Por qué moriste?" + +**Conceptos** · Señales, `signal()`, `raise()`, `kill()`, `errno` post-signal + +**Contexto** · Escribí un programa que demuestre cómo diferentes señales terminan procesos y cómo +el padre puede detectar cada una. + +**Consigna** · Crea dos hijos: uno que se mata con `SIGTERM` y otro con `SIGKILL`. El padre usa +`waitpid()` para detectar ambos y reporta la señal que los mató. Bonus: un tercer hijo que captura +`SIGINT` con `signal()` e imprime "¡No me mates!" en vez de morir. + +**Criterios de aceptación** + +- [ ] Detecta correctamente `SIGTERM`, `SIGKILL` y el hijo capturador. +- [ ] Usa `WIFSIGNALED()` y `WTERMSIG()` para reportar. +- [ ] El hijo con `signal()` muestra "¡No me mates!" en stdout. + +--- + +# PARTE 6 — Variables de entorno y `argv` + +Cada proceso tiene un conjunto de **variables de entorno** (PATH, HOME, USER...) que usa el sistema +y las aplicaciones para configurarse. También recibe argumentos a través de `argv`. + +## 6.1 `argv`: los argumentos del programa + +```c +int main(int argc, char *argv[]) { + // argc = cantidad de argumentos + // argv[0] = nombre del programa + // argv[1], argv[2], ... = argumentos pasados desde la línea de comandos + + for (int i = 0; i < argc; i++) { + printf("Arg %d: %s\n", i, argv[i]); + } + + return 0; +} +``` + +```bash +$ ./mi_programa Hola Mundo +Arg 0: ./mi_programa +Arg 1: Hola +Arg 2: Mundo +``` + +## 6.2 Variables de entorno con `environ` + +Las variables de entorno están disponibles a través del símbolo externo `environ`, declarado en ``: + +```c +#include +#include + +extern char **environ; + +int main(void) { + // Recorrer todas las variables de entorno + for (int i = 0; environ[i] != NULL; i++) { + printf("%s\n", environ[i]); + } + + // O buscar una específica con getenv(): + char *home = getenv("HOME"); + if (home) { + printf("HOME=%s\n", home); + } + + return 0; +} +``` + +> **`getenv()`, `setenv()`, `putenv()`** están en ``: +> - `getenv(name)` → devuelve el valor de la variable o NULL si no existe. +> - `setenv(name, value, overwrite)` → establece una variable (`overwrite=1` sobrescribe). +> - `putenv("NAME=value")` → igual pero la cadena debe permanecer en memoria. + +## Práctica de la Parte 6 + +### Ejercicio 23.10 — Listador de envvars + +**Conceptos** · `environ`, `getenv`, variables de entorno, `setenv` + +**Contexto** · Escribí un programa que liste todas las variables de entorno del proceso actual y permita +buscar una específica por nombre. + +**Consigna** · El programa recibe opcionalmente el nombre de una variable como argumento: +- Sin argumentos: lista **todas** las variables de entorno (una por línea). +- Con un argumento (ejemplo: `./env PATH`): busca y muestra solo esa variable. +- Si la variable no existe, imprime "Variable no encontrada". + +**Criterios de aceptación** + +- [ ] Usa `environ` para recorrer todas las variables. +- [ ] Soporta búsqueda específica con `getenv()`. +- [ ] Maneja el caso donde la variable no existe. +- [ ] Muestra output en formato `NOMBRE=valor`. + +### Ejercicio 23.11 — "Mi propio export" + +**Conceptos** · `setenv`, `putenv`, `getenv`, herencia de variables en fork/exec + +**Contexto** · Escribí un programa que funcione como un mini-`export`: permite establecer, mostrar y eliminar +variables de entorno. Al final, crea un proceso hijo con `execvp()` para verificar que las variables +seheredan al nuevo programa. + +**Consigna** · Un bucle interactivo que acepte: +- `SET nombre valor` → establece la variable. +- `SHOW [nombre]` → muestra todas o solo esa. +- `UNSET nombre` → elimina la variable (`unsetenv`). +- `RUN comando...` → ejecuta un comando (con `fork`/`exec`) que imprime las variables de entorno + para verificar la herencia. + +**Criterios de aceptación** + +- [ ] Implementa SET, SHOW, UNSET correctamente. +- [ ] Usa `setenv()`, `getenv()` y `unsetenv()` de ``. +- [ ] El comando RUN ejecuta un proceso hijo que muestra sus propias variables de entorno. +- [ ] Las variables establecidas en el padre aparecen en el hijo. + +--- + +# PARTE 7 — UID, GID, umask: tu identidad en Linux + +Además del PID, cada proceso tiene una identidad de seguridad: usuario real/efectivo, grupo, y permisos +por defecto para nuevos archivos (umask). + +## 7.1 UID real vs efectivo + +```c +#include +#include + +uid_t getuid(void); // Usuario real (quién ejecutó el programa) +uid_t geteuid(void); // Usuario efectivo (qué permisos tiene el programa) +gid_t getgid(void); // Grupo real +gid_t getegid(void); // Grupo efectivo +``` + +La diferencia entre UID real y efectivo aparece con **setuid**: cuando un ejecutable tiene el bit setuid, +se ejecuta con los permisos del **propietario del archivo**, no del que lo lanzó: + +```bash +$ ls -l /usr/bin/passwd +-rwsr-xr-x 1 root root 68K ... /usr/bin/passwd # ← 's' en vez de 'x' = setuid bit! +$ whoami +horacio +$ /usr/bin/passwd +Cambiar contraseña para horacio. +(Puedo modificar archivos de /etc/shadow porque el binary es owned por root) +``` + +## 7.2 Umask: los permisos que heredás al crear archivos + +El **umask** determina qué permisos se **quitan** a nuevos archivos cuando los creás con `open()`/`mkdir()`: + +| Umask | Permisos resultantes de un archivo creado con 0666 | +|---|---| +| `0022` (default) | `0644` (rw-r--r--) | +| `0002` (grupo writable) | `0664` (rw-rw-r--) | +| `0077` (solo propietario) | `0600` (rw-------) | + +```c +#include + +mode_t umask(mode_t mask); // Devuelve el umask anterior, establece uno nuevo. + +// Guardar el umask actual y ponerle cero temporalmente: +mode_t old_mask = umask(0); // Quitar todas las restricciones de umask +int fd = open("privado.txt", O_WRONLY | O_CREAT, 0666); // Crea con permisos exactos (no modificados por umask) +umask(old_mask); // Restaurar el umask original +close(fd); +``` + +> **Regla mnemotécnica:** `umask` es una **máscara de exclusión**. Lo que tenga 1 en la máscara se quita. +> Un umask de `0022` significa "quitar write al grupo y a otros". Por eso los archivos nuevos son +> `644`: 6 (rw-) sin nada, 4 (r--) sin write de grupo, 4 (r--) sin write de otros. + +## Práctica de la Parte 7 + +### Ejercicio 23.12 — "¿Quién soy realmente?" + +**Conceptos** · `getuid`, `geteuid`, `getgid`, `getegid`, `umask` + +**Contexto** · Escribí un programa que muestre toda la información de seguridad del proceso actual: +UIDs, GIDs, umask actual, y permisos efectives. + +**Consigna** · El programa debe imprimir: + +1. UID real y efectivo. +2. GID real y efectivo. +3. Umask actual (en octal). +4. Permisos que tendría un archivo creado ahora con `open("f", 0666)`. +5. Si el proceso tiene setuid activo (UID real != efectivo). + +**Criterios de aceptación** + +- [ ] Usa correctamente `getuid()`, `geteuid()`, `getgid()`, `getegid()`. +- [ ] Obtienes y muestras el umask con `umask(0)` guardando/restaurando el valor. +- [ ] Calcula los permisos efectivos de un nuevo archivo a partir del umask. +- [ ] Indica si setuid está activo. + +--- + +# PARTE 8 — Ejercicio jefe: copiador recursivo paralelo + +Llegó el momento de juntar todo lo que aprendiste sobre `fork()`, syscalls crudas, monitoreo asíncrono +y estadísticas en un solo programa realista: un **copiador de directorios que crea un proceso hijo por +archivo copiado**. + +## 8.1 El desafío + +Escribí un programa `copiar-dir` que recibe dos argumentos por línea de comandos: +1. **directorio origen** (ruta del directorio a copiar) +2. **directorio destino** (donde se copiará el árbol) + +El programa debe hacer lo siguiente: + +### Estructura general + +``` +main(argc, argv[1]=origen, argv[2]=destino) +│ +├── scan_and_copy(dir_origen, dir_destino, &stats) +│ │ +│ ├── Para cada entry del directorio (opendir/readdir): +│ │ │ +│ │ ├── Si es archivo regular: +│ │ │ ├── fork() +│ │ │ │ HIJO: copy_file(origen/archivo, destino/archivo) + exit(0 o 1) +│ │ │ └── PADRE: stats.total++ (contador local de este nivel) +│ │ │ +│ │ └── Si es subdirectorio (no "." ni ".."): +│ │ ├── mkdir(dir_destino/nombre) con permisos 0755 +│ │ └── scan_and_copy(origen/nombre, destino/nombre, &stats) ← padre llama recursivamente +│ │ +│ └── Durante el recorrido: waitpid(-1, &status, WNOHANG) para recoger estadísticas de hijos terminados +│ +└── Al final imprimir resumen: + Archivos copiados: 42 + Archivos fallidos: 0 + Total de archivos: 42 + Directorios creados: 7 +``` + +### Reglas clave + +1. **Cada archivo regular se copia en un proceso hijo separado.** El hijo usa `open/read/write` (del capítulo 22) para copiar el contenido. No usás `exec`: el hijo mismo hace la copia. +2. **Los subdirectorios se crean en el padre** (no spawnea hijos para directorios). Se recurren de forma secuencial desde el padre. +3. **El padre monitorea hijos con `waitpid(-1, &status, WNOHANG)`** en cada vuelta del bucle principal para recolectar estadísticas sin bloquearse. +4. **Cada hijo termina con `exit(0)` si copió bien o `exit(1)` si falló.** El padre verifica el estado con `WIFEXITED` y `WEXITSTATUS`. + +## 8.2 Funciones auxiliares sugeridas + +```c +// Estructura de estadísticas compartida por toda la recursión. +typedef struct { + long files_copied; // archivos copiados con éxito + long files_failed; // archivos que fallaron al copiarse + int dirs_created; // directorios creados +} CopyStats; + +// Copiar un solo archivo: abre origen, lee en chunk, escribe destino. +int copy_file(const char *src_path, const char *dst_path); + +// Crear recursivamente un árbol de directorios copiando archivos con fork() por cada uno. +void scan_and_copy(const char *src_dir, const char *dst_dir, CopyStats *stats); + +// Mostrar estadísticas finales formateadas. +void print_stats(const CopyStats *stats, double elapsed_sec); +``` + +## 8.3 Puntos clave de implementación + +### Medir tiempo total + +Usá `clock_gettime(CLOCK_MONOTONIC, &ts)` para obtener timestamps y calcular la duración: + +```c +#include + +struct timespec start_ts, end_ts; +clock_gettime(CLOCK_MONOTONIC, &start_ts); + +// ... ejecutar copia ... + +clock_gettime(CLOCK_MONOTONIC, &end_ts); +double elapsed = (end_ts.tv_sec - start_ts.tv_sec) + + (end_ts.tv_nsec - start_ts.tv_nsec) / 1e9; +``` + +### Recolectar estadísticas sin bloquear + +En cada vuelta del bucle principal (después de spawnea un hijo o antes de procesar el siguiente entry), +llamá a `waitpid` con `WNOHANG`: + +```c +// Recoger todos los hijos terminados que estén disponibles SIN bloquearse. +while (1) { + int status; + pid_t pid = waitpid(-1, &status, WNOHANG); + + if (pid == 0) { + // No hay más hijos terminados — seguir con el bucle. + break; + } else if (pid > 0) { + // Un hijo terminó. + if (WIFEXITED(status)) { + int code = WEXITSTATUS(status); + if (code == 0) { + stats->files_copied++; + } else { + stats->files_failed++; + } + } + } else { + // Error real — salir del bucle. + break; + } +} +``` + +### Copiar un archivo con syscalls crudas + +El hijo debe copiar un archivo usando `open/read/write`: + +```c +int copy_file(const char *src, const char *dst) { + int infd = open(src, O_RDONLY); + if (infd < 0) return 1; // Falló abrir origen + + int outfd = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (outfd < 0) { close(infd); return 1; } // Falló abrir destino + + char buf[4096]; + ssize_t n; + while ((n = read(infd, buf, sizeof(buf))) > 0) { + ssize_t written = 0; + while (written < n) { + ssize_t w = write(outfd, buf + written, n - written); + if (w < 0) break; + written += w; + } + if (written < n) break; // Error de escritura + } + + close(infd); + close(outfd); + return (n >= 0 || errno == 0) ? 0 : 1; +} +``` + +> **Nota:** esta función devuelve `0` si la copia fue exitosa y `1` si falló en algún paso. +> El hijo la llama y luego hace `exit(code)`. + +## 8.4 Ejercicio: implementación completa + +### Ejercicio 23.13 — Copiador recursivo paralelo + +**Conceptos** · `fork`, `waitpid`, `WNOHANG`, `dirent.h`, `sys/stat.h`, `open/read/write`, +`mkdir()`, `errno.h`, estadísticas concurrentes, `clock_gettime` + +**Contexto** · Copiar un directorio grande archivo por archivo es lento si se hace secuencialmente. +Este ejercicio crea un proceso hijo **por cada archivo**, que copia su contenido, mientras el padre +monitorea el progreso sin copiar nada. Es una simplificación porque no manejamos enlaces simbólicos, +permisos especiales ni casos de error extremos. + +**Consigna** · Implementá `copiar-dir` siguiendo la estructura y reglas descritas en las secciones 8.1-8.3. + +El programa debe: + +1. Recorrer recursivamente el árbol de directorios origen. +2. Por cada **archivo regular**, crear un **proceso hijo** con `fork()` que copie ese archivo. + El hijo usa `open/read/write` (no `exec`). Termina con `exit(0)` si copió bien, `exit(1)` si falló. +3. Por cada **subdirectorio** (excluyendo `.` y `..`): crear el directorio en destino con `mkdir()` + y llamar recursivamente a `scan_and_copy()` desde el proceso padre (**no** spawnea hijos para directorios). +4. Mientras los hijos corren, el padre debe llamar a `waitpid(-1, &status, WNOHANG)` en cada vuelta + del bucle para **recolectar estadísticas sin bloquearse**. +5. Al terminar, imprimir un resumen con formato: + +```text +Archivos copiados: 42 +Archivos fallidos: 0 +Total de archivos: 42 +Directorios creados: 7 +Tiempo total: 0.34s +``` + +**Entrada** · Dos rutas por argumento: `./copiar-dir /origen /destino` + +**Salida** · El árbol copiado + resumen estadístico en stdout. + +**Ejemplo de ejecución:** Dado un directorio origen así: + +```text +origen/ +├── README.md +├── src/ +│ ├── main.c +│ └── util.c +└── data/ + └── input.txt +``` + +La salida por consola sería: + +```text +Archivos copiados: 3 +Archivos fallidos: 0 +Total de archivos: 3 +Directorios creados: 2 +Tiempo total: 0.12s +``` + +**Criterios de aceptación** + +- [ ] Copia recursivamente todos los archivos y directorios del árbol origen al destino. +- [ ] Cada archivo regular se copia en un **proceso hijo separado** con `fork()`. +- [ ] El padre usa `waitpid` con `WNOHANG` para monitorear hijos sin bloquearse. +- [ ] Los subdirectorios se crean en el proceso padre (no spawnea hijos para directorios). +- [ ] Imprime estadísticas correctas: copiados, fallidos, total, directorios creados. +- [ ] Maneja errores: si un hijo falla al copiar, se cuenta como "fallido" y no se detiene el resto. +- [ ] El árbol destino es idéntico (misma estructura) al origen. +- [ ] Mide tiempo con `clock_gettime(CLOCK_MONOTONIC, ...)`. + +**Criterios de aceptación bonus:** + +- [ ] Soporta enlaces simbólicos (solo los lista, no copia contenido). +- [ ] Imprime progreso en tiempo real (porcentaje o contador de archivos restantes). +- [ ] Permite establecer un límite máximo de hijos concurrentes con una variable de entorno. + diff --git a/book/24-ipc.md b/book/24-ipc.md new file mode 100644 index 0000000..6aca0d1 --- /dev/null +++ b/book/24-ipc.md @@ -0,0 +1,1162 @@ +# Capítulo 24 — Comunicación entre Procesos (IPC): Pipes, FIFOs, Memoria Compartida y Semáforos + +> Capítulo avanzado, pero **escrito para que se entienda de verdad**. Se apoya en file descriptors +> (`22-file-descriptors`), procesos paralelos (`23-procesos`), structs (`08`) y memoria dinámica (`09`). +> +> Este capítulo te lleva del aislamiento total al trabajo en equipo: dos o más procesos que necesitan +> hablar entre sí. Pipes anónimos, pipes con nombre (FIFOs), memoria compartida (`shm_open`/`mmap`), +> semáforos POSIX y protocolos de mensajes. Todo con syscalls crudas. + +--- + +## Cómo leer este capítulo + +Dos procesos aislados no se ven ni se hablan. Cada uno tiene su espacio de memoria propio, sus +variables propias y su PID independiente. Pero hay momentos en que necesitás que dos procesos +**compartan datos**: un productor genera información y un consumidor la procesa; un servidor responde +peticiones de múltiples clientes; un sistema distribuye trabajo entre varios workers. + +Linux ofrece **varios mecanismos IPC**, cada uno con sus fortalezas: + +- **Pipes anónimos**: caños simples,unidireccionales, solo entre procesos relacionados (padre-hijo). +- **FIFOs (pipes con nombre)**: como pipes pero accesibles por cualquier proceso. +- **Memoria compartida (`shm_open` + `mmap`)**: el mecanismo más rápido — ambos procesos ven la misma memoria RAM. +- **Semáforos POSIX**: para sincronizar el acceso a recursos compartidos y evitar carreras. + +Este capítulo va desde lo básico (un pipe, dos procesos) hasta lo complejo (protocolo length-prefixed, +productor-consumidor con semáforos). Si entendés cada parte, vas a poder escribir programas distribuidos +simples sin frameworks externos. + +Está dividido en **nueve partes**. Después de cada concepto hay una o más prácticas —uno o varios +ejercicios— para que los uses ya mismo. **No las saltees**: son la mitad del capítulo. + +| Parte | De qué habla | La idea en una frase | +|---|---|---| +| **1** | ¿Por qué IPC? | Dos procesos aislados no se ven ni se hablan: si necesitan cooperar, Linux les da canales. | +| **2** | Pipes anónimos (`pipe()`) | Un caño de una sola dirección: el padre escribe, el hijo lee — los bytes fluyen sin tocar disco. | +| **3** | Pipes + fork: patrón clásico | El caño se crea antes del `fork`, así ambos procesos comparten los mismos fds y pueden hablar. | +| **4** | Named pipes (FIFOs) | Un caño con nombre en el sistema de archivos: procesos no relacionados se conectan al mismo canal. | +| **5** | `read`/`write` en pipes: comunicación pura sin stdio | Los pipes son fds: solo `read` y `write`. Cero `printf`, cero `FILE*`: es comunicación a nivel kernel. | +| **6** | Mensajes length-prefixed | Un pipe no sabe dónde termina un mensaje y empieza el otro: necesitás un mini-protocolo. | +| **7** | Memoria compartida (`shm_open` + `mmap`) | En vez de copiar bytes por un tubo, ambos procesos mapean el mismo segmento de memoria del kernel. | +| **8** | Semáforos POSIX (`sem_init`/`semop`) | Dos procesos escribiendo al mismo lugar es caos: un semáforo deja pasar de a uno. | +| **9** | Ejercicio jefe: chat multi-proceso completo | Pipe bidir + length-prefixed + shm para buffer de salida + semáforos + `dup2`. | + +> **Regla de oro del libro:** el capítulo explica y muestra **cómo funciona** cada syscall, pero +> **los ejercicios no traen la solución**. El enunciado te da el problema; la respuesta la escribís vos. + +--- + +# PARTE 1 — ¿Por qué IPC? + +Antes de ver los mecanismos, entendamos el problema que resolvemos. + +## 1.1 Aislamiento y cooperación + +Linux aísla procesos por defecto. Si el proceso A modifica su variable `x`, el proceso B no lo sabe +ni le afecta. Esto es **bueno**: evita que un bug en un programa afecte a otro. Pero también significa: + +> **Dos procesos no pueden compartir datos directamente.** Necesitan un canal explícito para comunicarse, +> ya sea un pipe (bytes crudos), un archivo en disco, memoria compartida o una syscall de sincronización. + +## 1.2 Los mecanismos IPC de Linux + +| Mecanismo | Velocidad | Relación necesaria | Dirección | Persistencia | +|---|---|---|---|---| +| Pipe anónimo (`pipe()`) | Muy rápido | Sí (padre-hijo) | Unidireccional | Solo mientras existan los fds | +| FIFO (`mkfifo()`) | Rápido | No | Unidireccional | Persiste en disco hasta eliminarlo | +| Memoria compartida | Más rápido que pipe | No | Bidireccional | Hasta `shm_unlink()` | +| Semáforo POSIX | N/A (sincronización) | No | N/A (coordina acceso) | Hasta `sem_unlink()` | + +> **Elegir el mecanismo correcto:** Si solo necesitás enviar datos de un proceso a otro, usá un pipe. +> Si necesitás comunicación bidireccional o mensajes grandes, memoria compartida es más eficiente. +> Si necesitás sincronizar acceso a un recurso compartido, semáforos. + +## Práctica de la Parte 1 + +### Ejercicio 24.1 — Demostración de aislamiento + +**Conceptos** · `fork`, variables independientes, `printf` después de fork (para ver el efecto) + +**Contexto** · Querés demostrar que dos procesos **no comparten memoria**. Escribí un programa que +muestre cómo una variable modificada en el hijo no afecta al padre. + +**Consigna** · Crea un proceso hijo, modifica una variable global en el hijo después de `fork()`, y +verificá que el padre mantiene su valor original. Imprimí ambos valores con sus PIDs para diferenciarlos. + +**Criterios de aceptación** + +- [ ] Usa `fork()` para crear un hijo. +- [ ] Modifica una variable global en el hijo solo (después del `if/else`). +- [ ] El padre mantiene el valor original de la variable. +- [ ] Imprime los valores con PID para evidencia visual. + +--- + +# PARTE 2 — Pipes anónimos: `pipe()` + +Un pipe es un **caño de una sola dirección** creado por el kernel. Tiene dos extremos: un fd para escribir +(write-end) y un fd para leer (read-end). Los bytes que escribís en un extremo salen por el otro, +sin tocar disco. + +## 2.1 Crear un pipe + +```c +#include + +int pipe(int pipefd[2]); +``` + +- `pipefd[0]` → **read end** (extremo de lectura). +- `pipefd[1]` → **write end** (extremo de escritura). + +Devuelve `0` si éxito, `-1` en error. + +```c +int mypipe[2]; +if (pipe(mypipe) == -1) { + perror("pipe falló"); + return 1; +} + +// Ahora: +// write(mypipe[1], "hola", 4) → read(mypipe[0], buf, 4) ← lee "hola" +``` + +> **Regla de oro:** los datos en un pipe son **transitorios**. No persisten al disco. Si el proceso lector +> no está corriendo o no leyó todo antes de que el writer cierre, los datos se pierden (o el write se +> bloquea si no hay espacio en el buffer del kernel). + +## 2.2 Escribir y leer con pipes simples + +```c +#include +#include + +int main(void) { + int mypipe[2]; + pipe(mypipe); + + // Escribir directamente al write-end + write(mypipe[1], "Hola pipe\n", 10); + + // Leer desde el read-end + char buf[256]; + ssize_t n = read(mypipe[0], buf, sizeof(buf)); + buf[n] = '\0'; // terminador manual para printf (read no pone \0) + printf("Leí: %s", buf); // "Leí: Hola pipe" + + close(mypipe[0]); + close(mypipe[1]); + + return 0; +} +``` + +## 2.3 El orden importa: cerrar el extremo que no se usa + +Si intentás leer de un pipe y **ningún proceso cerró el write-end**, `read()` esperará infinitamente +porque cree que pueden llegar más datos. Por eso siempre hay que cerrar el extremo que no se usa: + +```c +// Correcto: cerrar write-end del padre antes de leer +close(mypipe[1]); // Padre ya no escribe → hijo puede ver EOF en read() + +char buf[256]; +ssize_t n = read(mypipe[0], buf, sizeof(buf)); +if (n == 0) { + printf("EOF — no hay más datos\n"); +} +``` + +> **Regla mnemotécnica:** para que `read()` devuelva `0` (EOF), **todos** los write-ends del pipe +> deben estar cerrados. Si incluso un proceso mantiene el write-end abierto, `read()` nunca devuelve 0. + +## Práctica de la Parte 2 + +### Ejercicio 24.2 — Conversión unidireccional simple + +**Conceptos** · `pipe()`, `write()`, `read()`, EOF como retorno 0 de `read` + +**Contexto** · Escribí un programa que cree un pipe, escriba datos en el write-end y los lea del +read-end. Demuestra que los bytes fluyen en la dirección correcta sin tocar disco. + +**Consigna** · Crea un pipe, escribe al menos 5 mensajes diferentes por `write()` al write-end, +y después léelos todos con `read()` desde el read-end. Verificá que el orden de lectura coincide +con el de escritura (pipes mantienen orden FIFO). + +**Criterios de aceptación** + +- [ ] Crea el pipe con `pipe()`. +- [ ] Usa `write(pipe[1], ...)` para escribir múltiples mensajes. +- [ ] Usa `read(pipe[0], ...)` para leer todos los mensajes. +- [ ] Verifica que el orden se mantiene (FIFO). +- [ ] Cierra ambos extremos del pipe al terminar. + +### Ejercicio 24.3 — Pipe bidireccional con doble pipe + +**Conceptos** · Dos pipes, uno en cada dirección, `read`, `write` + +**Contexto** · Un pipe es unidireccional. Para comunicación bidireccional necesitás **dos pipes**: +uno para A→B y otro para B→A. + +**Consigna** · Escribí un programa que cree dos pipes: uno de lectura (pipeA) y otro de escritura +(pipeB). El programa escribe datos por pipeA y los lee desde pipeB, y viceversa. Muestra cómo +dos procesos pueden comunicarse en ambas direcciones usando dos pipes simples. + +**Criterios de aceptación** + +- [ ] Crea dos pipes con `pipe()`. +- [ ] Escribe por un pipe y lee del otro. +- [ ] Verifica que la comunicación funciona en ambos sentidos. +- [ ] Cierra los extremos no usados correctamente (para evitar bloqueos infinitos). + +--- + +# PARTE 3 — Pipes + fork: el patrón clásico + +Un pipe creado con `pipe()` solo existe mientras ambos extremos estén abiertos por **el mismo proceso**. +La verdadera magia ocurre cuando hacés `fork()`: **ambos procesos heredan los mismos fds**, incluidos +los del pipe, y pueden comunicarse a través de ellos. + +## 3.1 El patrón padre-hijo con pipe + +```c +#include +#include +#include +#include + +int main(void) { + int mypipe[2]; + pipe(mypipe); + + pid_t pid = fork(); + + if (pid == 0) { + // HIJO: lee del read-end + close(mypipe[1]); // Cerramos write-end — ya no escribimos + + char buf[256]; + ssize_t n = read(mypipe[0], buf, sizeof(buf)); + if (n > 0) { + buf[n] = '\0'; + printf("Hijo leyó: %s", buf); + } + + close(mypipe[0]); + return 0; + } + + // PADRE: escribe al write-end + close(mypipe[0]); // Cerramos read-end — ya no leemos + + const char *msg = "Hola desde el padre\n"; + write(mypipe[1], msg, strlen(msg)); + + close(mypipe[1]); + wait(NULL); // Esperar al hijo para limpiar zombie + + return 0; +} +``` + +> **Regla del pipe con fork:** +> - El padre cierra el extremo que no usa (read o write). +> - El hijo hace lo mismo. +> - Si el padre solo escribe y el hijo solo lee, cada uno cierra el otro extremo. +> - Si ambos necesitan leer Y escribir, necesitás **dos pipes** (uno en cada dirección). + +## 3.2 Cadena de procesos: pipe → fork → pipe → fork → ... + +Podés encadenar múltiples procesos usando un pipe entre cada par: + +```c +// Proceso A | proceso B | proceso C +// pipe1 | pipe2 + +pipe(pipe1); +pipe(pipe2); + +pid_t pA = fork(); +if (pA == 0) { + // Proceso A: escribe en pipe1[1] y cierra + write(pipe1[1], "mensaje", 8); + close(pipe1[1]); + close(pipe2[0]); // No usa pipe2 +} + +pid_t pB = fork(); +if (pB == 0) { + // Proceso B: lee de pipe1[0] y escribe en pipe2[1] + close(pipe1[1]); // Cierra write-end de pipe1 + char buf[256]; + read(pipe1[0], buf, sizeof(buf)); + + write(pipe2[1], "transformado", 12); + + close(pipe1[0]); + close(pipe2[1]); +} + +// Proceso C (padre o hijo de C): lee de pipe2[0] +close(pipe2[0]); +``` + +Este patrón se usa en la terminal para crear tuberías entre comandos (`cmd1 | cmd2 | cmd3`): cada +pipe conecta la salida de un comando con la entrada del siguiente. + +## 3.3 Cálculo distribuido: padre computa, hijo procesa + +Escribí un programa donde el padre calcula números y los envía por un pipe al hijo, que los imprime +con formato especial. Usa `fork()` + pipe para separar cálculo de presentación. + +```c +// Padre calcula números del 1 al 10 y los envía uno a uno. +for (int i = 1; i <= 10; i++) { + char msg[64]; + int len = snprintf(msg, sizeof(msg), "NUM:%d\n", i); + write(pipe_fd, msg, len); +} +// Cerrar write-end para que hijo vea EOF + +// Hijo lee y muestra cada número formateado: +while ((n = read(pipe_fd, buf, sizeof(buf))) > 0) { + // Procesar y mostrar +} +``` + +## Práctica de la Parte 3 + +### Ejercicio 24.4 — Calculadora distribuida + +**Conceptos** · `pipe()`, `fork()`, `write()`, `read()`, EOF detection, `snprintf` + +**Contexto** · Escribí un programa donde el **padre calcula** la suma de los números del 1 al N (N +recibido por argumento) y envía el resultado por un pipe al **hijo**, que lo imprime formateado como +`"Resultado: SUMA = X"`. El hijo espera a recibir el resultado, no lo calcula él mismo. + +**Consigna** · Usa `fork()` + `pipe()`: + +1. El padre recibe N por argumento (`argv[1]`). +2. Calcula la suma 1+2+3+...+N. +3. Envía el resultado como texto al hijo a través del pipe con `write()`. +4. El hijo lee del pipe, parsea el número y lo imprime en formato `"Resultado: SUMA = X"`. + +**Criterios de aceptación** + +- [ ] Crea el pipe antes de `fork()`. +- [ ] El padre calcula la suma correctamente. +- [ ] El hijo NO realiza ningún cálculo — solo lee del pipe y formatea. +- [ ] Usa `write()`/`read()` para comunicar (sin variables compartidas). +- [ ] Cierra ambos extremos correctamente para evitar bloqueos infinitos. + +### Ejercicio 24.5 — Cadena de procesadores + +**Conceptos** · Doble pipe, triple fork, cada proceso hace una transformación distinta + +**Contexto** · Escribí un programa que cree **tres procesos en cadena**: +Padre → Hijo1 → Hijo2. Cada uno recibe texto del anterior y le aplica una transformación antes +de enviarlo al siguiente. + +- **Proceso A (padre):** escribe `"hello world"` por pipe1. +- **Proceso B (hijo1):** lee de pipe1, convierte a MAYÚSCULAS (`"HELLO WORLD"`), lo envía por pipe2. +- **Proceso C (hijo2):** lee de pipe2 e imprime el resultado final. + +**Consigna** · Implementá la cadena de tres procesos con dos pipes: + +1. Pipe 1 conecta A → B. +2. Pipe 2 conecta B → C. +3. Cada proceso cierra los extremos del pipe que no usa (para evitar bloqueos en `read()`). +4. El proceso final imprime `"Resultado final: HELLO WORLD"`. + +**Criterios de aceptación** + +- [ ] Usa dos pipes separados para la cadena. +- [ ] Proceso B convierte a mayúsculas usando solo syscalls (sin `stdio`). +- [ ] Cada proceso cierra correctamente los fds del pipe que no usa. +- [ ] El resultado final es correcto y el programa termina limpio. + +--- + +# PARTE 4 — Named pipes (FIFOs) + +Los pipes anónimos solo funcionan entre procesos relacionados (padre-hijo, abuelo-nieto). Los **named +pipes** o **FIFOs** son como pipes pero con un nombre en el sistema de archivos: cualquier proceso +puede abrirlos y comunicarse a través de ellos. + +## 4.1 Crear un FIFO + +```c +#include + +int mkfifo(const char *pathname, mode_t mode); +``` + +Crea un archivo especial en el sistema de archivos (tipo FIFO). Solo se puede crear **una vez** con +el mismo nombre: + +```c +// Crear el FIFO si no existe +mkfifo("/tmp/mi_fifo", 0666); +``` + +> **El archivo "vive"** hasta que lo eliminás con `unlink()`. A diferencia del pipe anónimo, que +> desaparece cuando se cierran los últimos fds, el FIFO persiste en disco. + +## 4.2 Conectar dos procesos independientes al mismo FIFO + +```c +// Proceso escritor (write_fifo.c): +int fd = open("/tmp/mi_fifo", O_WRONLY); // Se bloquea hasta que alguien abra para leer +write(fd, "Hola desde el otro lado\n", 26); +close(fd); + +// Proceso lector (read_fifo.c): +int fd = open("/tmp/mi_fifo", O_RDONLY); // Se bloquea hasta que alguien abra para escribir +char buf[256]; +ssize_t n = read(fd, buf, sizeof(buf)); +printf("Leí: %.*s", (int)n, buf); +close(fd); +``` + +> **Bloqueo en `open()`:** cuando abres un FIFO con `O_RDONLY`, se bloquea hasta que alguien abre el +> mismo FIFO con `O_WRONLY` (y viceversa). Esto es para garantizar que siempre haya ambos extremos +> disponibles antes de empezar a comunicarse. + +## 4.3 Abrir en modo no bloqueante + +Si no querés que `open()` se bloquee, usá `O_NONBLOCK`: + +```c +// Intentar abrir sin bloquear — devuelve -1 si nadie más abrió el otro extremo. +int fd = open("/tmp/mi_fifo", O_WRONLY | O_NONBLOCK); +if (fd < 0) { + perror("No hay lector aún"); +} +``` + +## Práctica de la Parte 4 + +### Ejercicio 24.6 — Emisor y receptor independiente + +**Conceptos** · `mkfifo`, `open` (O_RDONLY / O_WRONLY), `write`, `read`, `unlink` + +**Contexto** · Escribí dos programas independientes: uno que escribe mensajes al FIFO y otro que los +lee. No son padre e hijo: corren por separado, en terminales diferentes. + +**Consigna** · Programa 1 (`fifo_emisor.c`): crea el FIFO si no existe, escribe varios mensajes, y +al recibir "QUIT" del usuario cierra todo. + +Programa 2 (`fifo_receptor.c`): abre el FIFO para leer e imprime cada mensaje que recibe. Muestra +el PID de quién escribió (si lo incluye en el mensaje) y al recibir "QUIT" termina limpiamente. + +**Criterios de aceptación** + +- [ ] `mkfifo()` se llama solo una vez (o verifica con `access()`). +- [ ] El emisor abre el FIFO con `O_WRONLY` y el receptor con `O_RDONLY`. +- [ ] Los mensajes llegan en orden del emisor al receptor. +- [ ] "QUIT" detiene ambos programas limpiamente. +- [ ] Limpia el FIFO con `unlink()` cuando ya no se necesita. + +### Ejercicio 24.7 — Chat básico entre dos terminales + +**Conceptos** · Dos FIFOs (uno en cada dirección), bucle interactivo, EOF detection + +**Contexto** · Escribí un chat de consola simple donde dos procesos se comunican en ambas direcciones +usando **dos FIFOs**: uno para mensajes de A a B y otro para mensajes de B a A. + +**Consigna** · Cada proceso: + +1. Crea los dos FIFOs (`fifo_a_b` y `fifo_b_a`). +2. Abre `fifo_b_a` para lectura (lo que el otro escribe) y `fifo_a_b` para escritura. +3. En un bucle interactivo: lee del otro proceso Y escribe al otro por teclado. +4. Usa `select()` o `poll()` para monitorear stdin Y el pipe simultáneamente (no bloquearse esperando teclado). + +**Criterios de aceptación** + +- [ ] Crea dos FIFOs con `mkfifo()`. +- [ ] Cada proceso puede enviar Y recibir mensajes. +- [ ] No se bloquea esperando input: usa `poll()` o `select()` para stdin + pipe simultáneamente. +- [ ] Muestra los mensajes del otro con un indicador como `[OTRO]: mensaje`. + +--- + +# PARTE 5 — `read`/`write` en pipes: comunicación pura sin stdio + +Los pipes son file descriptors. Para leer y escribir, usás **exclusivamente** `read()` y `write()`: +cero `printf`, cero `FILE*`, cero `fgets`. Es comunicación a nivel kernel: cruda, directa y eficiente. + +## 5.1 El patrón de lectura con pipes + +```c +// Leer un pipe en un bucle hasta EOF +char buf[4096]; +while (1) { + ssize_t n = read(pipe_fd, buf, sizeof(buf)); + + if (n == 0) { + // EOF: todos los write-ends cerrados. No hay más datos. + printf("[EOF recibido]\n"); + break; + } + + if (n < 0) { + perror("read falló"); + break; + } + + // n > 0: leyó n bytes en buf[0..n-1] + buf[n] = '\0'; // Terminador manual para imprimirlo como string + printf("[LEIDO %ld bytes]: %s", (long)n, buf); +} +``` + +## 5.2 El patrón de escritura con pipes + +```c +// Escribir un pipe — puede fallar si nadie está leyendo (SIGPIPE) +const char *msg = "Hola desde el otro lado\n"; +ssize_t n = write(pipe_fd, msg, strlen(msg)); +if (n < 0) { + perror("write falló"); +} else if ((size_t)n < strlen(msg)) { + printf("Advertencia: escribí solo %ld de %lu bytes", (long)n, (unsigned long)strlen(msg)); +} +``` + +> **SIGPIPE:** si intentás escribir a un pipe cuyo read-end fue cerrado por todos los lectores, el +> kernel envía `SIGPIPE` al proceso. Por defecto, esto termina el proceso. Para evitarlo, capturá la +> señal: `signal(SIGPIPE, SIG_IGN);` + +## 5.3 Servidor de fecha: un proceso responde la hora + +Escribí un programa servidor que abre un FIFO en modo lectura y, cada vez que alguien le escribe +"cualquier cosa", responde con la hora actual escrita al otro FIFO. + +```c +// Servidor: +while (1) { + // Esperar dato entrante del cliente + char buf[256]; + ssize_t n = read(client_pipe, buf, sizeof(buf)); + if (n == 0) break; // Cliente se desconectó + + // Generar hora actual con localtime() y strftime() + time_t now = time(NULL); + struct tm *tm_info = localtime(&now); + char time_buf[64]; + strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", tm_info); + + // Escribir respuesta al cliente + snprintf(response, sizeof(response), "Hora: %s\n", time_buf); + write(client_pipe, response, strlen(response)); +} +``` + +## Práctica de la Parte 5 + +### Ejercicio 24.8 — Línea de comando remota + +**Conceptos** · Pipe bidireccional (dos pipes), `read`, `write`, ejecución de comandos del cliente + +**Contexto** · Escribí un programa servidor que ejecuta comandos recibidos por un pipe y devuelve +la salida al cliente. El cliente envía comandos como `"ls -la"` o `"date"` y recibe la respuesta +exacta como si los hubiera ejecutado localmente. + +**Consigna** · Implementá cliente + servidor con dos FIFOs: + +1. **Servidor:** abre fifo_client_server para lectura, ejecuta el comando recibido (con `fork/exec`), + captura la salida del proceso hijo y la envía por fifo_server_client. +2. **Cliente:** escribe un comando en fifo_client_server, lee la respuesta de fifo_server_client, + y la imprime en pantalla. + +> **Nota:** usar `dup2()` para redirigir stdout del proceso hijo al write-end de un pipe (patrón clásico +> de captura de salida de comandos). + +**Criterios de aceptación** + +- [ ] Usa dos FIFOs bidireccionales correctamente. +- [ ] El servidor ejecuta el comando recibido con `fork/exec`. +- [ ] La salida del comando llega intacta al cliente. +- [ ] El cliente imprime la respuesta completa sin corromper datos. + +### Ejercicio 24.9 — Servidor de fecha (implementado) + +**Conceptos** · `read`, `write`, `time.h`, `strftime`, `mkfifo` + +**Contexto** · Un servidor simple que, cada vez que un cliente le escribe "cuál es la hora?", responde +con la hora actual escrita al pipe de respuesta del cliente. Usa solo syscalls crudas (cero `stdio`). + +**Consigna** · Implementá el servidor y el cliente: + +- **Servidor:** abre FIFO para lectura, espera mensajes, genera hora con `time()`/`localtime()`/`strftime()`, + la envía al pipe de respuesta del cliente. +- **Cliente:** escribe "hora" por FIFO de petición, lee respuesta desde su propio FIFO. + +> Usa `pipe()` en vez de FIFO para simplificar (solo procesos locales). El servidor recibe N peticiones +> antes de terminar. + +**Criterios de aceptación** + +- [ ] Servidor y cliente se comunican exclusivamente con `read`/`write`. +- [ ] La hora se genera correctamente con la API de ``. +- [ ] Formato de salida: `"Hora: YYYY-MM-DD HH:MM:SS\n"`. +- [ ] El servidor maneja N peticiones y el cliente recibe cada respuesta completa. + +--- + +# PARTE 6 — Mensajes length-prefixed: protocolos en pipes + +Un pipe es un **stream continuo de bytes**. No tiene estructura de mensajes: si escribís `"hola\nmundo\n"` +y luego leés, podés recibir `"hol"` en una lectura y `"a\nmundo\n"` en la siguiente. Para enviar mensajes +delimitados necesitás un protocolo. El más simple: **length-prefixed**. + +## 6.1 El problema: ¿dónde termina un mensaje y empieza el otro? + +```c +// Escritor envía dos mensajes: +write(pipe_fd, "Hola", 4); // Mensaje 1: 4 bytes +write(pipe_fd, "Mundo", 5); // Mensaje 2: 5 bytes + +// Lector lee de una vez: +read(pipe_fd, buf, 9); // ¿Recibe "HolaMundo" (9 bytes)? Sí. +// Pero... ¿cómo sabe dónde termina el mensaje 1 y empieza el 2? +``` + +Sin protocolo, el lector no puede distinguir mensajes individuales. Solución: **cada mensaje comienza +con un entero de 4 bytes que indica su longitud**. + +## 6.2 Protocolo length-prefixed + +```c +// Escritor envía un mensaje con prefijo de longitud: +void send_message(int fd, const char *msg) { + uint32_t len = htonl((uint32_t)strlen(msg)); // Longitud en network byte order + write(fd, &len, sizeof(len)); // Primero: los 4 bytes de longitud + write(fd, msg, strlen(msg)); // Después: el contenido +} + +// Lector recibe un mensaje con prefijo de longitud: +ssize_t recv_message(int fd, char *buf, size_t buf_size) { + uint32_t len_net; + + // Leer los 4 bytes de longitud + ssize_t n = read(fd, &len_net, sizeof(len_net)); + if (n != sizeof(len_net)) return -1; // Error o EOF + + uint32_t len = ntohl(len_net); // Convertir a host byte order + + if (len >= buf_size) { + return -1; // Mensaje demasiado largo para el buffer + } + + // Leer exactamente `len` bytes de contenido + ssize_t total = 0; + while (total < (ssize_t)len) { + n = read(fd, buf + total, len - total); + if (n <= 0) return -1; // Error o EOF inesperado + total += n; + } + + buf[total] = '\0'; // Terminar como string + return total; // Longitud del mensaje leído +} +``` + +> **`htonl()` / `ntohl()`:** convierten entre network byte order (big-endian) y host byte order. +> Es importante para compatibilidad si los procesos corren en arquitecturas con diferente endianness. +> En una máquina x86_64 local (little-endian), no estrictamente necesario, pero es buena práctica. + +## 6.3 Chat con mensajes estructurados + +Usá el protocolo length-prefixed para enviar mensajes de tipo `"USUARIO: mensaje"` entre procesos: + +```c +// Lector en un bucle infinito: +while (1) { + char msg[4096]; + ssize_t len = recv_message(pipe_fd, msg, sizeof(msg)); + if (len == 0) { + printf("[EOF]\n"); + break; + } + if (len < 0) { + perror("recv_message falló"); + break; + } + printf("[RECIBIDO]: %s", msg); // msg ya tiene el \0 +} +``` + +## Práctica de la Parte 6 + +### Ejercicio 24.10 — Protocolo length-prefixed básico + +**Conceptos** · `htonl`, `ntohl`, `write`, `read`, protocolo de longitud prefijada + +**Contexto** · Escribí un programa que demuestre el protocolo length-prefixed: envía varios mensajes +de diferentes longitudes por un pipe y los lee en el otro extremo, verificando que cada mensaje se +recibe intacto y separado. + +**Consigna** · El programa: + +1. Crea un pipe y fork(). +2. El padre escribe 3 mensajes con longitudes diferentes (ej: `"h"`, `"hola mundo"`, `"abcde"`) usando + el protocolo length-prefixed (4 bytes de longitud + contenido). +3. El hijo lee los 3 mensajes uno por uno, imprimiendo cada uno con su longitud. + +**Criterios de aceptación** + +- [ ] Usa `htonl()` para codificar la longitud del mensaje. +- [ ] Escribe primero 4 bytes de longitud, luego el contenido. +- [ ] El hijo lee primero los 4 bytes de longitud, luego exactamente esa cantidad de bytes. +- [ ] Verifica que cada mensaje se recibe completo y sin mezclar con otros. + +### Ejercicio 24.11 — Chat con mensajes estructurados + +**Conceptos** · `fork`, pipe bidireccional, protocolo length-prefixed, lectura interactiva por stdin + +**Contexto** · Escribí un programa de chat entre padre e hijo: el padre escribe desde teclado y el +hijo imprime los mensajes recibidos en pantalla (y viceversa). Usa el protocolo length-prefixed para +delimitar mensajes. + +**Consigna** · Implementá un bucle interactivo: + +1. El padre lee una línea de stdin (`fgets`), la envía al hijo con `send_message()`. +2. El hijo recibe con `recv_message()` e imprime `[HIJO] mensaje`. +3. Invertir el flujo: hijo responde, padre imprime `[PADRE] respuesta`. +4. Terminar con "exit" en cualquiera de los dos lados. + +**Criterios de aceptación** + +- [ ] Usa protocolo length-prefixed correctamente. +- [ ] Ambos extremos pueden enviar y recibir. +- [ ] Los mensajes llegan intactos (sin mezclas, sin pérdida). +- [ ] Termina limpiamente al escribir "exit". + +--- + +# PARTE 7 — Memoria compartida (`shm_open` + `mmap`) + +Con pipes, cada mensaje se copia del espacio del escritor al del kernel y luego del kernel al del lector. +Con **memoria compartida**, ambos procesos ven la misma región de memoria física: **cero copias**. + +## 7.1 Crear una sección de memoria compartida + +```c +#include +#include +#include + +// Crear o abrir una región compartida en el sistema de archivos /dev/shm/ +int fd = shm_open("/mi_shm", O_CREAT | O_RDWR, 0666); +if (fd < 0) { perror("shm_open"); return 1; } + +// Asignarle tamaño (en bytes) — esto la "reserva" en RAM del kernel. +ftruncate(fd, sizeof(int)); // Reserva 4 bytes para un solo entero compartido + +// Mapear esa región en el espacio de memoria del proceso actual. +int *shared_counter = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, + MAP_SHARED, fd, 0); +if (shared_counter == MAP_FAILED) { perror("mmap"); return 1; } + +// Cerrar el fd — ya no lo necesitamos para acceder a la memoria. +close(fd); +``` + +## 7.2 Leer y escribir en memoria compartida + +Una vez mapeada, la región se usa como cualquier puntero: + +```c +// Proceso A escribe un valor: +*shared_counter = 42; + +// Proceso B (que también hizo shm_open + mmap) lo lee: +printf("Valor en memoria compartida: %d\n", *shared_counter); // Imprime 42 +``` + +> **La misma región física** aparece en el espacio de direcciones virtual de ambos procesos, pero +> en direcciones diferentes. `shared_counter` del proceso A apunta a una dirección distinta que la de B, +> pero ambas apuntan a la misma RAM. + +## 7.3 Limpiar y desmapear + +```c +// Desmapear el segmento del espacio de este proceso. +munmap(shared_counter, sizeof(int)); + +// Eliminar la sección compartida del sistema (debe hacerlo uno de los procesos). +shm_unlink("/mi_shm"); +``` + +> **`shm_unlink()` no destruye la memoria inmediatamente.** Solo elimina el nombre del sistema. La región +> se libera cuando **el último proceso que la tiene mapeada** llama a `munmap()`. Si un proceso la desmpea +> pero otro aún la usa, los datos permanecen accesibles. + +## Práctica de la Parte 7 + +### Ejercicio 24.12 — Contador compartido + +**Conceptos** · `shm_open`, `ftruncate`, `mmap`, `munmap`, `shm_unlink` + +**Contexto** · Escribí un programa donde dos procesos comparten un entero con memoria compartida y +cada uno lo incrementa 100 veces. Sin sincronización, el resultado final será menor a 200 debido +a condiciones de carrera. + +**Consigna** · El programa: + +1. Crea una región compartida con `shm_open`/`ftruncate` para almacenar un `int`. +2. Fork() para crear un hijo. +3. Ambos procesos incrementan el contador 100 veces cada uno (usando un bucle simple). +4. Al final, imprimen el valor final del contador. + +> Sin sincronización (semáforos), verás que el resultado es menor a 200 porque ambos procesos +> leen-modifican-escriben el mismo entero simultáneamente. Esto demuestra **por qué se necesitan +> semáforos** (Parte 8). + +**Criterios de aceptación** + +- [ ] Crea la región compartida con `shm_open` + `ftruncate`. +- [ ] Ambos procesos hacen `mmap` para acceder al entero. +- [ ] Cada proceso incrementa el contador exactamente 100 veces. +- [ ] Imprime el valor final (que será < 200 sin sincronización). + +### Ejercicio 24.13 — Buffer productor-consumidor básico + +**Conceptos** · `shm_open`, `mmap`, estructura con datos, límites de buffer + +**Contexto** · Escribí un programa productor-consumidor donde el productor escribe cadenas en un +buffer compartido (estructura con `char data[1024]` y un campo `int used`) y el consumidor lee desde ahí. + +**Consigna** · El programa: + +1. Define una estructura `SharedBuffer` con un campo `char data[1024]` y otro `int used`. +2. Crea la región compartida con `shm_open`/`ftruncate` del tamaño de la estructura. +3. Fork() para crear hijo (consumidor). +4. El padre (productor) escribe cadenas en `data` mientras el hijo (consumidor) las lee. + +**Criterios de aceptación** + +- [ ] Define correctamente la estructura `SharedBuffer`. +- [ ] Usa `shm_open`/`ftruncate` con tamaño `sizeof(SharedBuffer)`. +- [ ] Ambos procesos acceden a la misma instancia mapeada. +- [ ] El productor escribe y el consumidor lee del buffer compartido. + +--- + +# PARTE 8 — Semáforos POSIX: sincronizar acceso a recursos compartidos + +Dos procesos escribiendo al mismo lugar sin coordinación = datos corrompidos. Los semáforos son el +mecanismo de Linux para **garantizar que solo un proceso acceda a un recurso a la vez**. + +## 8.1 El problema: condiciones de carrera en escritura + +```c +// Proceso A y B hacen esto al mismo tiempo: +int x = *shared_counter; // Paso 1: leer el valor actual +x++; // Paso 2: incrementarlo +*shared_counter = x; // Paso 3: escribirlo de vuelta + +// Si ambos ejecutan los tres pasos en paralelo, uno de los incrementos se pierde. +``` + +Este es un **read-modify-write race condition**: dos procesos leen el mismo valor, lo modifican por separado, +y escriben — pero solo uno de los cambios sobrevive. + +## 8.2 Semáforos binarios: mutex (mutual exclusion) + +```c +#include + +// Crear un semáforo con valor inicial = 1 (lock disponible) +sem_t *mutex = sem_open("/mi_mutex", O_CREAT, 0666, 1); + +// Adquirir el lock: si está disponible, lo toma. Si no, espera. +sem_wait(mutex); + +// === SECCIÓN CRÍTICA: solo un proceso a la vez llega aquí === +*shared_counter++; +// ============================================================ + +// Liberar el lock: otro proceso puede entrar ahora. +sem_post(mutex); +``` + +> **`sem_wait()`** es como `wait()` en sistemas operativos clásicos: decrementa el semáforo y bloquea +> si llega a `-1`. **`sem_post()`** es como `signal()`: incrementa y despierta un proceso dormido. + +## 8.3 Semáforos con nombre vs anónimos + +| Tipo | Creación | Alcance | Ejemplo | +|---|---|---|---| +| Con nombre (`sem_open`) | `sem_open()` | Entre procesos no relacionados | `/mi_sem` | +| Anónimo (`sem_init`) | `sem_init()` | Solo entre hijos del mismo `fork()` (o misma memoria compartida) | En estructura `sem_t` | + +Para semáforos anónimos (usados en el mismo proceso padre e hijos): + +```c +sem_t sem; +sem_init(&sem, 0, 1); // 0 = no compartido entre procesos no relacionados, 1 = valor inicial + +// ... fork() ... + +sem_wait(&sem); // Adquirir +/* sección crítica */ +sem_post(&sem); // Liberar + +sem_destroy(&sem); // Al finalizar (no hay sem_unlink para anónimos) +``` + +## 8.4 Productor-consumidor con dos semáforos + +El patrón clásico de productor-consumidor usa **dos semáforos**: + +- `slots`: cuenta cuántos slots vacíos quedan en el buffer. +- `items`: cuenta cuántos items llenos hay en el buffer. + +```c +sem_t slots; // Inicializado a N (tamaño del buffer) +sem_t items; // Inicializado a 0 (no hay items al inicio) +sem_t mutex; // Mutex para proteger acceso al buffer + +// Productor: +sem_wait(&slots); // Esperar slot vacío disponible +sem_wait(&mutex); // Lock el buffer +/* escribir en buffer[write_pos++] */ +sem_post(&mutex); // Unlock buffer +sem_post(&items); // Un item más disponible + +// Consumidor: +sem_wait(&items); // Esperar item disponible +sem_wait(&mutex); // Lock el buffer +/* leer de buffer[read_pos++] */ +sem_post(&mutex); // Unlock buffer +sem_post(&slots); // Un slot libre disponible +``` + +## Práctica de la Parte 8 + +### Ejercicio 24.14 — Productor-consumidor con mutex + +**Conceptos** · `sem_init`, `sem_wait`, `sem_post`, sección crítica, read-modify-write race + +**Contexto** · Repetí el ejercicio del contador compartido (24.12) pero ahora **agregando un semáforo +mutex** para proteger el acceso al contador. Verificá que el resultado final es exactamente 200 +(era menor sin sincronización). + +**Consigna** · Modificá el programa de 24.12 para que: + +1. Cada proceso adquiera el mutex antes de leer-modificar-escribir el contador compartido. +2. Libere el mutex después de escribir el valor actualizado. +3. Imprima el resultado final (debe ser exactamente 200). + +**Criterios de aceptación** + +- [ ] Usa `sem_init()` para crear un semáforo con valor inicial 1. +- [ ] Cada incremento del contador está protegido por `sem_wait`/`sem_post`. +- [ ] El resultado final es exactamente 200 (sin carrera). +- [ ] Limpia el semáforo con `sem_destroy()`. + +### Ejercicio 24.15 — Cola de trabajo múltiple + +**Conceptos** · Dos semáforos (`slots`/`items`), productor-consumidor, buffer circular + +**Contexto** · Escribí un programa con **un productor y N consumidores** (N=3) que comparten un +buffer circular de tamaño 5. El productor genera números del 1 al 100; los consumidores los leen +y los imprimen. + +**Consigna** · Implementá el patrón productor-consumidor con: + +- Buffer circular de 5 enteros en memoria compartida. +- Tres semáforos: `slots` (inicializado a 5), `items` (inicializado a 0), `mutex` (inicializado a 1). +- Un proceso productor y 3 procesos consumidores. +- Cada consumidor imprime `"Consumidor X tomó Y"` para cada valor que procesa. + +**Criterios de aceptación** + +- [ ] Usa tres semáforos correctamente (`slots`, `items`, `mutex`). +- [ ] El buffer circular tiene tamaño fijo de 5 elementos. +- [ ] Los 3 consumidores compiten por items sin duplicar trabajo. +- [ ] Cada valor es procesado exactamente una vez. +- [ ] El programa termina cuando el productor envía un marcador de fin (ej: -1). + +--- + +# PARTE 9 — Ejercicio jefe: chat multi-proceso completo + +Llegó el momento de juntar **todo lo aprendido** en un solo programa: pipe bidireccional para comunicación, +protocolo length-prefixed para mensajes delimitados, memoria compartida para buffer de salida, y semáforos +para evitar que dos procesos impriman en pantalla simultáneamente. + +## 9.1 El desafío completo + +Escribí un **sistema de chat multi-proceso** que: + +1. Cree un padre (emisor) y un hijo (receptor), comunicados por un **pipe bidireccional** (dos pipes + anónimos, uno en cada dirección). +2. Use el protocolo **length-prefixed** para enviar mensajes delimitados sin ambigüedad. +3. Comparta un **buffer de salida** con memoria compartida (`shm_open` + `mmap`) donde cada proceso + escribe su parte antes de imprimirla en pantalla. +4. Use un **semáforo POSIX** para garantizar que solo un proceso imprima a la vez (no se mezclen mensajes). +5. Redirija stdout y stderr al pipe del hijo mediante `dup2()` si se desea capturar toda la salida del hijo. + +## 9.2 Estructura sugerida + +```c +#include + +// Estructura del buffer compartido para salida en pantalla. +typedef struct { + char data[1024]; // Buffer de texto que puede ser escrito por cualquiera de los procesos. + int len; // Longitud actual del texto en `data`. +} OutBuffer; + +int main(void) { + /* 1. Crear pipes bidireccionales (pipe_a -> pipe_b y pipe_b -> pipe_a) */ + + /* 2. Crear semáforo mutex para proteger acceso a pantalla/buffer compartido */ + sem_t *screen_mutex = sem_open("/chat_screen_mutex", O_CREAT, 0666, 1); + + /* 3. Crear región de memoria compartida para buffer de salida */ + int shm_fd = shm_open("/chat_outbuffer", O_CREAT | O_RDWR, 0666); + ftruncate(shm_fd, sizeof(OutBuffer)); + OutBuffer *outbuf = mmap(NULL, sizeof(OutBuffer), PROT_READ|PROT_WRITE, + MAP_SHARED, shm_fd, 0); + + /* 4. Fork() para crear el otro proceso (padre/receptor) */ + pid_t pid = fork(); + + if (pid == 0) { + // HIJO: bucle interactivo — lee del pipe_A y envía por pipe_B + handle_child(pipe_read, pipe_write, outbuf, screen_mutex); + _exit(0); + } + + // PADRE: bucle interactivo — escribe al pipe_B y lee del pipe_A + handle_parent(pipe_write, pipe_read, outbuf, screen_mutex); + + /* 5. Limpieza */ + sem_close(screen_mutex); + sem_unlink("/chat_screen_mutex"); + munmap(outbuf, sizeof(OutBuffer)); + shm_unlink("/chat_outbuffer"); + close(shm_fd); + + return 0; +} + +// Cada extremo del chat tiene un bucle similar: +void handle_parent(int read_pipe, int write_pipe, OutBuffer *outbuf, sem_t *mutex) { + char line[256]; + + while (fgets(line, sizeof(line), stdin)) { + // Remover el \n final. + + // Si es "QUIT", enviar mensaje de salida y romper el bucle. + + // Enviar mensaje al hijo con protocolo length-prefixed. + send_message(write_pipe, line); + + // Recibir respuesta del hijo (si la hay). + char response[4096]; + ssize_t len = recv_message(read_pipe, response, sizeof(response)); + if (len > 0) { + // Escribir en buffer compartido y tomar pantalla. + sem_wait(mutex); + printf("%s", response); + fflush(stdout); + sem_post(mutex); + } + } +} + +void handle_child(int read_pipe, int write_pipe, OutBuffer *outbuf, sem_t *mutex) { + // Leer del pipe, responder al padre. +} +``` + +## 9.3 Puntos clave de implementación + +### Protocolo length-prefixed en pipes bidireccionales + +Cada proceso tiene dos fd: uno para leer (pipe_A[0] o pipe_B[0]) y otro para escribir (pipe_A[1] o pipe_B[1]). +El protocolo length-prefixed se usa tanto en un sentido como en el otro. + +### Buffer de salida compartido + +Ambos procesos pueden escribir en `outbuf` al mismo tiempo. Para evitar que las escrituras se mezclen: +- Cada proceso escribe su mensaje completo en la zona local del buffer (o directamente a `printf` con el mutex). +- El mutex protege tanto el buffer como la escritura a stdout. + +### Semáforo para pantalla + +Usá `sem_wait(mutex)` antes de imprimir y `sem_post(mutex)` después: + +```c +sem_wait(screen_mutex); +printf("[PADRE]: %s\n", mensaje); // Imprime en pantalla. +fflush(stdout); // Asegura que se escriba inmediatamente (sin buffering). +sem_post(screen_mutex); +``` + +### Redirigir stdout del hijo con `dup2()` + +Si querés capturar toda la salida del hijo hacia el pipe: + +```c +// En el hijo, antes de empezar a leer/escribir: +int log_fd = open("child_stdout.log", O_WRONLY | O_CREAT | O_TRUNC, 0644); +dup2(log_fd, STDOUT_FILENO); // stdout ahora apunta al archivo. +close(log_fd); + +// Ahora todo lo que el hijo imprima por printf/stderr va al archivo. +``` + +## 9.4 Ejercicio: implementación completa + +### Ejercicio 24.16 — JEFE FINAL: Chat multi-proceso completo + +**Conceptos** · `pipe()`, `fork()`, protocolo length-prefixed, `shm_open`, `mmap`, semáforos POSIX +(`sem_open`/`sem_wait`/`sem_post`), `dup2` para redirección, manejo asíncrono de stdin + +**Contexto** · Un sistema de chat entre dos procesos que usa pipes bidireccionales para comunicación, +protocolo length-prefixed para mensajes delimitados, memoria compartida para buffer de salida compartido, +y semáforos POSIX para sincronizar la impresión en pantalla. Es un programa real de IPC que combina +todos los conceptos del capítulo. + +**Consigna** · Implementá el sistema de chat descrito en la sección 9.1 siguiendo la estructura sugerida +en 9.2 y los puntos clave de 9.3. El programa debe ser funcional, robusto y limpio. + +**Requisitos funcionales:** +- Comunicación bidireccional con dos pipes anónimos (uno en cada dirección). +- Protocolo length-prefixed para delimitar mensajes enviados por los pipes. +- Región de memoria compartida (`shm_open` + `mmap`) para buffer de salida compartido. +- Semáforo POSIX para garantizar exclusión mutua al imprimir en pantalla. +- Soporte para mensaje especial `"QUIT"` que cierra limpiamente ambos procesos. +- Limpieza completa al terminar: desmpeado, `munmap`, `sem_close`, `shm_unlink`. + +**Entrada** · El programa se ejecuta en la consola. Cada proceso escribe mensajes por stdin. + +**Salida** · Mensajes del otro lado aparecen en pantalla con indicador `[OTRO]: mensaje`. + +**Criterios de aceptación** + +- [ ] Usa dos pipes anónimos para comunicación bidireccional entre padre e hijo. +- [ ] Implementa protocolo length-prefixed correctamente (`send_message`/`recv_message`). +- [ ] Crea memoria compartida con `shm_open` + `mmap`. +- [ ] Usa semáforo POSIX (`sem_open`, `sem_wait`, `sem_post`) para proteger impresión en pantalla. +- [ ] Soporta mensaje `"QUIT"` que termina limpiamente ambos procesos. +- [ ] Limpia todos los recursos al salir: `munmap`, `sem_close`, `shm_unlink`. +- [ ] No hay mensajes mezclados en pantalla (el mutex protege la sección crítica correctamente). +- [ ] Maneja EOF en pipes limpiamente (sin bucles infinitos ni crashes). + +**Bonus:** + +- Soportar más de dos procesos (padre + N hijos) con un único pipe bidireccional central. +- Redirigir stdout/stderr del hijo al archivo `child.log` con `dup2()`. +- Guardar historial de conversación en memoria compartida y restaurarlo al reiniciar. + + + diff --git a/dist/22-file-descriptors.pdf b/dist/22-file-descriptors.pdf new file mode 100644 index 0000000..ea5af5a Binary files /dev/null and b/dist/22-file-descriptors.pdf differ diff --git a/dist/23-procesos.pdf b/dist/23-procesos.pdf new file mode 100644 index 0000000..f23aabd Binary files /dev/null and b/dist/23-procesos.pdf differ diff --git a/dist/24-ipc.pdf b/dist/24-ipc.pdf new file mode 100644 index 0000000..b8ef75a Binary files /dev/null and b/dist/24-ipc.pdf differ