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:
- Cap. 25 Diálogo sobre concurrencia
- Cap. 26 Concurrency: An Introduction · código
- Cap. 27 Interlude: Thread API · código
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
x86.py: preguntas 1–8 del cap. 26 conloop.s,looping-race-nolock.s,wait-for-me.s.main-*.cdel cap. 27 convalgrind --tool=helgrind: detectar condiciones de carrera y deadlocks.
Preguntas de repaso
- ¿Qué se comparte y qué no entre dos hilos del mismo proceso?
- Explique, instrucción por instrucción, cómo dos hilos que ejecutan
counter++pueden perder una actualización. - ¿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
Los programas que usan los apuntes y la clase.
// 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.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.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.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.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.
# 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