1078 lines
38 KiB
Markdown
1078 lines
38 KiB
Markdown
# 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 <unistd.h>
|
|
#include <stdio.h>
|
|
|
|
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 <unistd.h>
|
|
|
|
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 <unistd.h>
|
|
#include <sys/wait.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
|
|
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 <sys/wait.h>
|
|
|
|
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 <sys/wait.h>
|
|
|
|
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 <defunct> ← 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 `<defunct>` 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 <signal.h>
|
|
|
|
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 `<unistd.h>`:
|
|
|
|
```c
|
|
#include <unistd.h>
|
|
#include <stdio.h>
|
|
|
|
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 `<stdlib.h>`:
|
|
> - `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 `<stdlib.h>`.
|
|
- [ ] 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 <unistd.h>
|
|
#include <sys/types.h>
|
|
|
|
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 <sys/stat.h>
|
|
|
|
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 <time.h>
|
|
|
|
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.
|
|
|