02
CPU

API de procesos y ejecución directa limitada

Virtualización de la CPU

Semana 2 — API de procesos y ejecución directa limitada

Objetivos

  • Usar fork(), exec() y wait() 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:

Temas:

  • fork(): dos procesos, un valor de retorno distinto en cada uno.
  • exec(): reemplazar la imagen del proceso. wait()/waitpid().
  • Por qué fork + exec está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.S y del camino de un ecall en xv6-riscv.
  • Costo de una syscall y de un cambio de contexto.

Laboratorio — Programación con procesos

  1. Tarea de código del cap. 5 (preguntas 1–8): fork, exec*, wait, pipe, dup2.
  2. Usar strace -f para observar las syscalls de un fork/exec.
  3. Tarea de medición del cap. 6: medir el costo de una syscall (read de 0 bytes) y de un cambio de contexto con dos pipes.
  4. Tiempo para avanzar P0.

Preguntas de repaso

  1. ¿Cuántos procesos crea fork(); fork(); fork();? Dibuje el árbol.
  2. ¿Por qué un programa de usuario no puede ejecutar directamente una instrucción de E/S?
  3. 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 (5)Compilación (1)

Ejemplos

Los programas que usan los apuntes y la clase.

exec.cDescargar exec.c
// 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.cDescargar fork.c
// 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.cDescargar redirect.c
// 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.cDescargar signal.c
// 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.cDescargar wait.c
// 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.

MakefileDescargar Makefile
CFLAGS = -Wall -Wextra -g
PROGS  = fork wait exec redirect signal

all: $(PROGS)

clean:
	rm -f $(PROGS) salida.txt

.PHONY: all clean