Semana 2 — API de procesos y ejecución directa limitada
Objetivos
- Usar
fork(),exec()ywait()para crear y controlar procesos. - Explicar cómo el OS recupera el control de la CPU sin perder eficiencia: modo usuario/kernel (user/kernel mode), traps, interrupción del temporizador (timer interrupt) y cambio de contexto (context switch).
Sesión A — API de procesos
Lecturas:
- Cap. 5 Interlude: Process API · código
Temas:
fork(): dos procesos, un valor de retorno distinto en cada uno.exec(): reemplazar la imagen del proceso.wait()/waitpid().- Por qué
fork+execestán separados: redirección y pipes en la shell. - Señales (signals),
kill, usuarios y permisos sobre procesos.
Sesión B — Ejecución directa limitada
Lecturas:
Temas:
- Problema 1: operaciones restringidas → modo usuario/kernel, trap y tabla de traps (trap table).
- Problema 2: cambiar entre procesos → enfoque cooperativo vs. no cooperativo (timer interrupt).
- Cambio de contexto: guardar y restaurar registros. Lectura guiada de
kernel/swtch.Sy del camino de unecallen xv6-riscv. - Costo de una syscall y de un cambio de contexto.
Laboratorio — Programación con procesos
- Tarea de código del cap. 5 (preguntas 1–8):
fork,exec*,wait,pipe,dup2. - Usar
strace -fpara observar las syscalls de unfork/exec. - Tarea de medición del cap. 6: medir el costo de una syscall (
readde 0 bytes) y de un cambio de contexto con dos pipes. - Tiempo para avanzar P0.
Preguntas de repaso
- ¿Cuántos procesos crea
fork(); fork(); fork();? Dibuje el árbol. - ¿Por qué un programa de usuario no puede ejecutar directamente una instrucción de E/S?
- Describa, paso a paso, qué ocurre entre que llega una interrupción del temporizador y empieza a correr otro proceso.
Código de la semana
Compilar dentro del contenedor del curso con make o gcc -Wall.
Ejemplos
Los programas que usan los apuntes y la clase.
// exec.c — el hijo reemplaza su imagen por otro programa (wc).
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
const char *file = argc > 1 ? argv[1] : "exec.c";
pid_t rc = fork();
if (rc < 0) {
perror("fork");
exit(1);
}
if (rc == 0) {
char *args[] = {"wc", "-l", (char *)file, NULL};
execvp(args[0], args);
perror("execvp"); // solo se llega aquí si exec falla
exit(1);
}
waitpid(rc, NULL, 0);
printf("padre: wc terminó\n");
return 0;
}// fork.c — fork() retorna dos veces: el PID del hijo en el padre y 0 en el hijo.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
printf("hola, soy %d\n", getpid());
pid_t rc = fork();
if (rc < 0) {
perror("fork");
exit(1);
} else if (rc == 0) {
printf("hijo: mi PID es %d, mi padre es %d\n", getpid(), getppid());
} else {
printf("padre: creé al hijo %d\n", rc);
}
return 0;
}// redirect.c — así implementa la shell "wc -l redirect.c > salida.txt":
// entre fork() y exec(), el hijo cambia su salida estándar.
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void) {
pid_t rc = fork();
if (rc < 0) {
perror("fork");
exit(1);
}
if (rc == 0) {
int fd = open("salida.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644);
if (fd < 0) {
perror("open");
exit(1);
}
dup2(fd, STDOUT_FILENO); // la salida estándar ahora es el archivo
close(fd);
char *args[] = {"wc", "-l", "redirect.c", NULL};
execvp(args[0], args);
perror("execvp");
exit(1);
}
waitpid(rc, NULL, 0);
printf("listo: ver salida.txt\n");
return 0;
}// signal.c — instalar un manejador de señales con sigaction().
// Ctrl-C (SIGINT) ya no mata al programa: solo se cuenta.
// SIGTERM pide terminar de forma ordenada. SIGKILL no se puede capturar.
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static volatile sig_atomic_t interrupts = 0;
static volatile sig_atomic_t done = 0;
// En un manejador solo se deben usar operaciones seguras:
// aquí solo se modifican variables de tipo sig_atomic_t.
static void on_int(int sig) {
(void)sig;
interrupts++;
}
static void on_term(int sig) {
(void)sig;
done = 1;
}
int main(void) {
struct sigaction sa = {0};
sigemptyset(&sa.sa_mask);
sa.sa_handler = on_int;
sigaction(SIGINT, &sa, NULL);
sa.sa_handler = on_term;
sigaction(SIGTERM, &sa, NULL);
printf("PID %d: prueben Ctrl-C o kill -TERM %d\n", getpid(), getpid());
fflush(stdout);
int seen = 0;
while (!done) {
pause(); // dormir hasta que llegue una señal
if (interrupts != seen) {
seen = interrupts;
printf("SIGINT número %d: lo ignoro\n", seen);
fflush(stdout);
}
}
printf("SIGTERM: termino ordenadamente tras %d SIGINT\n", seen);
return 0;
}// wait.c — el padre espera al hijo, así la salida es determinista.
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void) {
pid_t rc = fork();
if (rc < 0) {
perror("fork");
exit(1);
}
if (rc == 0) {
printf("hijo %d trabajando...\n", getpid());
sleep(1);
exit(42);
}
int status;
pid_t done = waitpid(rc, &status, 0);
if (WIFEXITED(status))
printf("padre: el hijo %d terminó con código %d\n", done, WEXITSTATUS(status));
return 0;
}Compilación
Cómo se compilan y ejecutan los ejemplos.
CFLAGS = -Wall -Wextra -g
PROGS = fork wait exec redirect signal
all: $(PROGS)
clean:
rm -f $(PROGS) salida.txt
.PHONY: all clean