09
Concurrencia

Máquinas virtuales · Hilos

Virtualización (apéndice) · Concurrencia

Semana 9 — Máquinas virtuales · Hilos

Objetivos

  • Explicar cómo un monitor de máquinas virtuales (VMM o hipervisor) virtualiza un OS completo.
  • Definir hilo (thread), entender por qué la ejecución intercalada produce condiciones de carrera y usar la API de pthreads.

Sesión A — ✂️ Máquinas virtuales

Lecturas:

Temas:

  • Ejecución directa limitada aplicada a un OS invitado (guest): trap-and-emulate.
  • Virtualización de la memoria: tablas de páginas sombra (shadow page tables) y soporte de hardware (EPT/NPT).
  • Paravirtualización. Contenedores vs. máquinas virtuales (relación con el Docker del curso).

Si hace falta recuperar tiempo, esta sesión pasa a lectura autónoma con un cuestionario corto.

Sesión B — Hilos · API de hilos

Lecturas:

Temas:

  • Hilo vs. proceso: comparten espacio de direcciones, cada uno con su pila y registros.
  • Condición de carrera (race condition), sección crítica (critical section), exclusión mutua (mutual exclusion), atomicidad.
  • pthread_create, pthread_join, pthread_mutex_*, pthread_cond_*.

Laboratorio — Simulador x86.py y pthreads

  1. x86.py: preguntas 1–8 del cap. 26 con loop.s, looping-race-nolock.s, wait-for-me.s.
  2. main-*.c del cap. 27 con valgrind --tool=helgrind: detectar condiciones de carrera y deadlocks.

Preguntas de repaso

  1. ¿Qué se comparte y qué no entre dos hilos del mismo proceso?
  2. Explique, instrucción por instrucción, cómo dos hilos que ejecutan counter++ pueden perder una actualización.
  3. ¿Por qué un VMM necesita que el hardware ayude a virtualizar la tabla de páginas?

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.

args.cDescargar args.c
// args.c — pasar argumentos a un hilo y recibir su resultado.
//
// Uso: ./args <N> <hilos>
// Suma 1 + 2 + ... + N repartiendo el rango entre los hilos. Cada hilo
// recibe un struct con su rango y devuelve un struct (en el heap) con su suma.
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int  id;
    long from, to;      // rango [from, to] que suma este hilo
} task_t;

typedef struct {
    long sum;
} result_t;

static void *sum_range(void *arg) {
    task_t *t = arg;                    // void * -> puntero al tipo real
    result_t *r = malloc(sizeof(*r));   // en el heap: sobrevive al hilo
    if (r == NULL)
        return NULL;
    r->sum = 0;
    for (long i = t->from; i <= t->to; i++)
        r->sum += i;
    printf("hilo %d: [%ld, %ld] -> %ld\n", t->id, t->from, t->to, r->sum);
    return r;                           // NUNCA la dirección de una variable local
}

int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "uso: args <N> <hilos>\n");
        return 1;
    }
    long N = atol(argv[1]);
    int n = atoi(argv[2]);
    if (N < 1 || n < 1 || n > 16) {
        fprintf(stderr, "N >= 1, hilos entre 1 y 16\n");
        return 1;
    }

    pthread_t th[16];
    task_t tasks[16];                   // un struct por hilo: no reutilizar uno solo
    long chunk = N / n;
    for (int i = 0; i < n; i++) {
        tasks[i].id = i;
        tasks[i].from = i * chunk + 1;
        tasks[i].to = (i == n - 1) ? N : (i + 1) * chunk;
        if (pthread_create(&th[i], NULL, sum_range, &tasks[i]) != 0) {
            fprintf(stderr, "pthread_create falló\n");
            return 1;
        }
    }

    long total = 0;
    for (int i = 0; i < n; i++) {
        void *ret;
        pthread_join(th[i], &ret);      // ret recibe lo que retornó el hilo
        result_t *r = ret;
        if (r != NULL) {
            total += r->sum;
            free(r);                    // lo reservó el hilo; lo libera main
        }
    }
    printf("total = %ld   (N(N+1)/2 = %ld)\n", total, N * (N + 1) / 2);
    return 0;
}
cond.cDescargar cond.c
// cond.c — esperar a que otro hilo avise, con una variable de condición.
//
// Uso: ./cond
// main crea un hilo que "prepara" un dato y avisa con pthread_cond_signal.
// main espera con pthread_cond_wait dentro de un while, con el mutex tomado.
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t  ready_cv = PTHREAD_COND_INITIALIZER;
static int ready = 0;               // el estado que se espera (protegido por lock)
static int data = 0;

static void *producer(void *arg) {
    (void)arg;
    sleep(1);                       // simula trabajo
    pthread_mutex_lock(&lock);
    data = 42;
    ready = 1;                      // 1) cambiar el estado...
    pthread_cond_signal(&ready_cv); // 2) ...y avisar, con el mutex tomado
    pthread_mutex_unlock(&lock);
    return NULL;
}

int main(void) {
    pthread_t p;
    pthread_create(&p, NULL, producer, NULL);

    pthread_mutex_lock(&lock);
    while (!ready) {                // while, no if: se vuelve a verificar
        printf("main: esperando...\n");
        pthread_cond_wait(&ready_cv, &lock);  // suelta el lock y duerme
    }                               // al despertar, tiene el lock otra vez
    printf("main: listo, data = %d\n", data);
    pthread_mutex_unlock(&lock);

    pthread_join(p, NULL);
    return 0;
}
counter.cDescargar counter.c
// counter.c — una sola línea, para ver en qué instrucciones se convierte.
//
// make asm   genera counter-rv.s con el compilador de RISC-V del contenedor.
// counter++ no es una instrucción: es cargar (ld), sumar (addi) y guardar (sd).
volatile long counter;

void increment(void) {
    counter++;
}
race.cDescargar race.c
// race.c — condición de carrera en un contador compartido, con y sin mutex.
//
// Uso: ./race <modo> <hilos> <iteraciones por hilo>
//   modo = nolock   cada hilo hace counter++ sin protección (incorrecto)
//          mutex    cada counter++ va entre lock y unlock (correcto, lento)
//          local    cada hilo suma en una variable propia y al final
//                   agrega su total al contador con un solo lock (correcto, rápido)
//
// Ejemplo: ./race nolock 4 1000000
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

static volatile long counter = 0;   // dato compartido por todos los hilos
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
static long loops;

static void *worker_nolock(void *arg) {
    (void)arg;
    for (long i = 0; i < loops; i++)
        counter++;                    // leer, sumar, escribir: no es atómico
    return NULL;
}

static void *worker_mutex(void *arg) {
    (void)arg;
    for (long i = 0; i < loops; i++) {
        pthread_mutex_lock(&lock);    // inicio de la sección crítica
        counter++;
        pthread_mutex_unlock(&lock);  // fin de la sección crítica
    }
    return NULL;
}

static void *worker_local(void *arg) {
    (void)arg;
    long mine = 0;                    // vive en la pila de este hilo: no se comparte
    for (long i = 0; i < loops; i++)
        mine++;
    pthread_mutex_lock(&lock);        // una sola sección crítica por hilo
    counter += mine;
    pthread_mutex_unlock(&lock);
    return NULL;
}

static double now(void) {
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec + ts.tv_nsec / 1e9;
}

int main(int argc, char *argv[]) {
    if (argc != 4) {
        fprintf(stderr, "uso: race nolock|mutex|local <hilos> <iteraciones>\n");
        return 1;
    }
    void *(*fn)(void *);
    if (strcmp(argv[1], "nolock") == 0)
        fn = worker_nolock;
    else if (strcmp(argv[1], "mutex") == 0)
        fn = worker_mutex;
    else if (strcmp(argv[1], "local") == 0)
        fn = worker_local;
    else {
        fprintf(stderr, "modo desconocido: %s\n", argv[1]);
        return 1;
    }
    int n = atoi(argv[2]);
    loops = atol(argv[3]);
    if (n < 1 || n > 64 || loops < 0) {
        fprintf(stderr, "hilos entre 1 y 64, iteraciones >= 0\n");
        return 1;
    }

    pthread_t th[64];
    double t0 = now();
    for (int i = 0; i < n; i++)
        if (pthread_create(&th[i], NULL, fn, NULL) != 0) {
            fprintf(stderr, "pthread_create falló\n");
            return 1;
        }
    for (int i = 0; i < n; i++)
        pthread_join(th[i], NULL);    // esperar a que todos terminen
    double t1 = now();

    long expected = n * loops;
    printf("%-6s  hilos=%d  esperado=%ld  obtenido=%ld  %s  %.3f s\n",
           argv[1], n, expected, counter,
           counter == expected ? "OK " : "MAL", t1 - t0);
    return 0;
}
stacks.cDescargar stacks.c
// stacks.c — cada hilo tiene su propia pila; el resto se comparte.
//
// Uso: ./stacks [hilos]
// Cada hilo anota la dirección de una variable local suya. Todos esperan en
// una barrera, así están vivos a la vez (si no, la biblioteca podría reutilizar
// la misma pila). Al final, main imprime las direcciones en orden.
// Todos tienen el mismo pid (un proceso), pero cada uno su propio tid.
#define _GNU_SOURCE        // para gettid()
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define MAXT 16

static int global = 0;              // segmento de datos: uno solo por proceso
static void *local_addr[MAXT];      // dónde quedó la variable local de cada hilo
static pid_t thread_pid[MAXT];
static pid_t thread_tid[MAXT];      // id del hilo para el kernel (Linux)
static pthread_barrier_t barrier;

static void *worker(void *arg) {
    long id = (long)arg;
    int local = (int)id;            // en la pila de ESTE hilo
    local_addr[id] = &local;
    thread_pid[id] = getpid();
    thread_tid[id] = gettid();
    pthread_barrier_wait(&barrier); // nadie termina hasta que todos anotaron
    return NULL;
}

int main(int argc, char *argv[]) {
    int n = (argc > 1) ? atoi(argv[1]) : 3;
    if (n < 1 || n > MAXT)
        n = 3;
    int *heap = malloc(sizeof(int));
    int local = 0;

    printf("global       %p\n", (void *)&global);
    printf("heap         %p\n", (void *)heap);
    printf("main  local  %p   pid %d  tid %d\n", (void *)&local, getpid(), gettid());

    pthread_barrier_init(&barrier, NULL, n);
    pthread_t th[MAXT];
    for (long i = 0; i < n; i++)
        pthread_create(&th[i], NULL, worker, (void *)i);
    for (int i = 0; i < n; i++)
        pthread_join(th[i], NULL);

    for (int i = 0; i < n; i++) {
        printf("hilo %d local  %p   pid %d  tid %d", i, local_addr[i],
               thread_pid[i], thread_tid[i]);
        if (i > 0) {
            long d = labs((char *)local_addr[i - 1] - (char *)local_addr[i]);
            printf("   (a %ld KiB del hilo %d)", d / 1024, i - 1);
        }
        printf("\n");
    }
    pthread_barrier_destroy(&barrier);
    free(heap);
    return 0;
}

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 9: hilos, condiciones de carrera y la API de pthreads.
#   make            compila todo
#   make demo       corre la condición de carrera con y sin mutex
#   make asm        genera counter-rv.s: counter++ compilado para RISC-V
CFLAGS = -Wall -Wextra -g
LDLIBS = -pthread
PROGS  = race args stacks cond

all: $(PROGS)

demo: race
	./race nolock 4 1000000
	./race mutex  4 1000000
	./race local  4 1000000

# Compilador cruzado de RISC-V del contenedor (el mismo que compila xv6).
asm: counter.c
	riscv64-linux-gnu-gcc $(CFLAGS) -O1 -S -o counter-rv.s counter.c

clean:
	rm -f $(PROGS) counter-rv.s

.PHONY: all demo asm clean