1450 lines
57 KiB
Markdown
1450 lines
57 KiB
Markdown
# 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 <fcntl.h>
|
|
#include <unistd.h>
|
|
|
|
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 <unistd.h>
|
|
|
|
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 <sys/file.h>
|
|
|
|
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 <fcntl.h>
|
|
|
|
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 <sys/select.h>
|
|
|
|
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 <poll.h>
|
|
|
|
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 <dirent.h>
|
|
#include <limits.h>
|
|
#include <unistd.h>
|
|
|
|
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 <unistd.h>
|
|
|
|
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 <unistd.h>
|
|
|
|
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 `<stdio.h>`** 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.
|
|
|
|
|