El bloque de control de proceso - Procesos e Hilos
← Procesos e Hilos Bloque 1 · El concepto de proceso · Tema 4 de 43

Tema 4

El bloque de control de proceso

En el tema 1 se dijo que para el núcleo un proceso «es» su bloque de control de proceso, el PCB. Es la ficha donde está escrito todo lo que el sistema sabe de un proceso: quién es, en que estado está, que memoria usa, que archivos tiene abiertos, que señales esperan. En este tema verás que campos guarda un PCB, para que los usa el núcleo, como se traduce esa idea a la estructura task_struct de linux y como puedes consultarla desde un programa o desde /proc.

1. Qué es el PCB

El bloque de control de proceso, o PCB (process control block), es la estructura de datos en la que el núcleo guarda toda la información que necesita para administrar un proceso: su identidad, su estado, su posición en la CPU, su memoria, sus archivos abiertos, sus señales pendientes, su padre y sus hijos, sus tiempos de ejecución y sus límites de recursos. Mientras el proceso vive, el núcleo mantiene un PCB para él; cuando el proceso termina, el PCB se libera.

La idea aparece en los primeros sistemas operativos por una necesidad práctica: el núcleo tiene que poder detener un proceso en cualquier instante y reanudarlo más tarde exactamente donde estaba. Para eso necesita recordar, en algún lugar, todo lo que el proceso estaba haciendo y todo lo que poseía. Ese lugar es el PCB. Sin él, el núcleo no sabría como retomar un proceso después de haberle quitado la CPU.

Por eso se dice que para el núcleo un proceso «es» su PCB: todo lo que el sistema hace con un proceso —ejecutarlo, suspenderlo, enviarle una señal, cobrarle tiempo de CPU, matarlo— pasa por leer o modificar la estructura que lo representa. El programa que corre en el espacio de usuario no ve el PCB directamente; solo ve los efectos de lo que el núcleo hace con él.

El PCB es la ficha del proceso

Piensa en el PCB como la ficha de un paciente en un hospital: el nombre, el estado, el tratamiento, la habitación, el médico asignado. El paciente es la persona, pero para el hospital lo que importa es la ficha, porque es lo que permite atenderlo, moverlo y darlo de alta. El PCB es la ficha del proceso en el sistema operativo.

2. Para que lo usa el núcleo

El núcleo consulta y modifica el PCB en muchos momentos de la vida de un proceso. Los principales son:

MomentoQue hace el núcleo con el PCB
Creación (fork)Reserva un PCB nuevo, le copia los datos del padre y le asigna un PID nuevo.
planificaciónLee el estado y la prioridad para decidir que proceso ejecutar a continuación.
Cambio de contextoGuarda los registros de la CPU en el PCB del proceso que sale y los restaura desde el PCB del que entra.
Llamada al sistemaUsa el PCB para saber que proceso pide el servicio, con que permisos y que recursos tiene.
SeñalesAnota las señales pendientes y los manejadores registrados en el PCB.
MemoriaConsulta las tablas de páginas y los límites de memoria asociados al proceso.
Terminación (exit)Marca el estado como terminado, guarda el código de salida y avisa al padre.
Recogida (wait)El padre lee el código de salida del PCB del hijo y el núcleo libera la estructura.

Todos esos accesos ocurren en modo núcleo, mientras el proceso está ejecutándose o mientras el núcleo decide que proceso ejecutar. El programa del usuario nunca toca el PCB: solo ve las consecuencias, como el valor que devuelve getpid() o el hecho de que un malloc haya funcionado.

3. Los campos de un PCB

Aunque la implementación varía entre sistemas, todo PCB guarda, como mínimo, la siguiente información. Se puede agrupar en seis categorías:

Identidad

Estado y planificación

Memoria

Recursos y archivos

Señales

Jerarquía y sincronización

Es una lista larga, y es deliberadamente así: el PCB es la estructura más importante del núcleo en lo que respecta a la administración de procesos. Todo lo que el sistema necesita saber de un proceso está ahí o se alcanza desde ahí.

Esquema de un PCB con seis grupos de campos: identidad, estado y planificación, memoria, recursos y archivos, señales, y jerarquía. Cada grupo lista los campos principales. PCB — bloque de control de proceso Identidad PID, PPID, UID, GID nombre (comm) grupo de procesos, sesión TTY de control Estado y planificación estado (R, S, D, T, Z) prioridad, nice cola de planificación tiempo de CPU usado Memoria tablas de páginas (mm) zonas: texto, datos, heap, stack límites de memoria (rlimit) brk, start_stack Recursos y archivos tabla de archivos abiertos (fd) cwd, root variables de entorno límites de recursos Señales señales bloqueadas y pendientes manejadores registrados máscara de señales Jerarquía puntero al padre lista de hijos hermanos, grupo de hilos
Los campos de un PCB agrupados por categoría. En linux todos viven dentro de una sola estructura, task_struct.

4. task_struct en linux

En linux el PCB se llama task_struct, y está definido en el código fuente del núcleo en include/linux/sched.h. Es una de las estructuras más grandes y más importantes del núcleo: tiene miles de líneas de definición y contiene punteros a decenas de otras estructuras. No es un bloque contiguo simple, sino un nodo central que apunta a subestructuras especializadas.

Esta es una versión muy simplificada, con los campos que más se mencionan. Los nombres reales en el código del núcleo son exactamente estos:

C — task_struct (simplificada, estilo linux)
struct task_struct {
    /* Identidad */
    pid_t               pid;
    pid_t               tgid;          /* thread group id */
    struct task_struct  *parent;       /* padre */
    struct list_head     children;      /* lista de hijos */
    struct list_head     sibling;      /* hermanos */

    /* Estado y planificación */
    volatile long       state;         /* TASK_RUNNING, TASK_INTERRUPTIBLE, ... */
    int                 prio;          /* prioridad dinámica */
    int                 static_prio;   /* prioridad estática (nice) */
    struct sched_entity se;            /* datos del planificador */

    /* Memoria */
    struct mm_struct    *mm;           /* espacio de direcciones */
    struct mm_struct    *active_mm;    /* mm activo en la CPU */

    /* Archivos y sistema de archivos */
    struct files_struct *files;        /* tabla de archivos abiertos */
    struct fs_struct    *fs;           /* cwd, root, umask */

    /* Señales */
    struct signal_struct *signal;      /* señales compartidas del grupo */
    struct sighand_struct *sighand;    /* manejadores de señales */
    sigset_t            blocked;       /* señales bloqueadas */
    sigset_t            pending;       /* señales pendientes */

    /* Tiempos y contabilidad */
    u64                 utime;         /* tiempo en modo usuario */
    u64                 stime;         /* tiempo en modo núcleo */
    struct timespec64   start_time;    /* cuándo se creó */

    /* Credenciales */
    const struct cred   *cred;         /* UID, GID, capacidades */

    /* Registros guardados (al hacer cambio de contexto) */
    struct thread_struct thread;       /* registros de la CPU */

    /* ... cientos de campos más ... */
};

Varias observaciones sobre esta estructura:

task_struct es el PCB, pero más

En los libros de texto clásicos, el PCB es una estructura compacta y autocontenida. En linux, task_struct es el PCB, pero también es el descriptor de hilos, el descriptor de tareas del planificador y el ancla de muchas otras estructuras. La idea es la misma; la implementación es la de un sistema operativo real y complejo.

5. El puntero current y el acceso al PCB

Mientras un proceso se está ejecutando en modo núcleo, el código del núcleo necesita saber a que proceso corresponde la ejecución actual. Para eso existe el puntero current, una variable (en realidad una macro) que apunta al task_struct del proceso que está corriendo en la CPU actual.

Por eso, dentro del núcleo, obtener el PID del proceso actual es tan simple como current->pid, y obtener su padre es current->parent->pid. La función getpid() que usa un programa en C termina llamando al núcleo, que devuelve current->tgid (el PID del grupo de hilos, que es el que el usuario ve).

El puntero current se implementa de forma distinta según la arquitectura. En x86-64, se guarda en un registro especial del procesador (el gs base) o se deriva de la pila del núcleo. La idea es que, dada la pila actual, se puede calcular la dirección del task_struct restando un desplazamiento fijo, porque el núcleo reserva la estructura del proceso justo debajo de su pila de núcleo.

Desde el espacio de usuario no se puede acceder a current ni a task_struct. El único camino es pedirle al núcleo que exponga los datos, y eso es exactamente lo que hace /proc: cada archivo de /proc/PID es una ventana controlada a los campos del task_struct correspondiente.

6. El PCB y el cambio de contexto

El cambio de contexto, que viste en el tema 1, es la operación en la que el núcleo detiene un proceso, guarda su estado y carga el de otro. El PCB es el lugar donde se guarda y se restaura ese estado.

Cuando el núcleo desaloja un proceso, hace, entre otras cosas:

  1. Guarda los registros de la CPU en el campo thread del task_struct del proceso que sale: el contador de programa, el puntero de pila, los registros generales.
  2. Actualiza el estado del proceso en su PCB: de «en ejecución» a «listo» o «bloqueado», según corresponda.
  3. Cambia las tablas de páginas para que la CPU use las del nuevo proceso: así, las direcciones virtuales del nuevo proceso se traducen a su propia memoria física.
  4. Carga los registros desde el task_struct del nuevo proceso.
  5. Actualiza current para que apunte al nuevo proceso.
  6. Salta a la instrucción indicada por el contador de programa restaurado.

Todo esto ocurre en modo núcleo, en unos pocos microsegundos. El proceso que fue desalojado no se entera de nada: cuando vuelve a ejecutarse, sus registros están exactamente como los dejó, y continúa desde la misma instrucción. Esa ilusión de continuidad es la que hace posible el tiempo compartido.

El cambio de contexto no es gratis

Guardar y restaurar registros, cambiar las tablas de páginas y actualizar las cachés de la CPU (TLB) tiene un costo. Por eso los sistemas operativos intentan minimizar los cambios de contexto innecesarios, y por eso los hilos, que comparten el espacio de direcciones y no requieren cambiar las tablas de páginas, son más baratos de cambiar que los procesos. Es el tema 9.

7. El PCB y la creación de procesos

Cuando se llama a fork(), el núcleo crea un proceso nuevo. Lo que hace, en términos del PCB, es:

  1. Reserva un task_struct nuevo y un PID nuevo.
  2. Copia los campos del padre al hijo: estado, prioridad, punteros a memoria, archivos abiertos, señales, credenciales. Es una copia superficial: los punteros apuntan a las mismas subestructuras.
  3. Marca al hijo como distinto: le cambia el PID, le pone el PPID igual al PID del padre, le pone el estado «listo» y le prepara un valor de retorno de fork de 0.
  4. Crea una copia del espacio de direcciones del padre para el hijo, usando copia en escritura (tema 13). El campo mm del hijo apunta a una nueva estructura que comparte las mismas páginas físicas hasta que uno de los dos las modifique.
  5. Agrega el hijo a la lista de hijos del padre y lo pone en la cola de listos.

Por eso fork es relativamente barato: no copia toda la memoria, solo el PCB y las estructuras de control. La memoria se comparte hasta que se modifica. Y por eso el hijo «hereda» tantas cosas del padre: archivos abiertos, directorio de trabajo, variables de entorno, manejadores de señales. Todo eso está en el PCB o en las subestructuras a las que apunta.

Cuando el proceso termina con exit(), el núcleo no libera el PCB inmediatamente: lo marca como «zombi», guarda el código de salida y avisa al padre. El PCB se libera solo cuando el padre llama a wait() y lee ese código. Ese es el tema 24.

8. Consultar el PCB desde /proc

Cómo el usuario no puede tocar task_struct directamente, el núcleo expone sus campos a través de /proc/PID. Cada archivo de ese directorio es una ventana a una parte del PCB o de las subestructuras a las que apunta. Los más directos son:

Archivo en /proc/PIDCampo del PCB que expone
statusNombre, PID, PPID, TGID, UID, GID, estado, hilos, memoria (Vmsize, VmRSS), señales.
statTodos los campos en formato compacto: PID, comm, state, ppid, pgrp, session, tty, tpgid, flags, minflt, cminflt, majflt, cmajflt, utime, stime, cutime, cstime, priority, nice, num_threads, starttime, vsize, rss, rsslim, ...
statmTamaño de memoria en páginas: tamaño total, residente, compartido, texto, datos.
mapsLas regiones del espacio de direcciones (el campo mm).
fd/La tabla de archivos abiertos (el campo files).
cwd, root, exeDirectorio de trabajo, raíz y ejecutable (el campo fs).
environLas variables de entorno.
limitsLos límites de recursos (rlimit).
schedEstadísticas del planificador para este proceso.
ioEstadísticas de entrada/salida.
wchanEn que función del núcleo está dormido el proceso (si está bloqueado).

El archivo stat es el más completo para programas: una sola línea con todos los campos numéricos, pensado para ser leído por scripts. El archivo status es el mismo contenido pero en formato legible por humanos, con etiquetas. La mayoría de las herramientas (ps, top) leen stat y status para mostrar la información.

terminal · leer /proc/PID/stat
$ cat /proc/self/stat
448 (cat) S 402 402 402 34816 448 4194304 107 0 0 0 0 0 0 0 20 0 1 0 12345 5652480 188 18446744073709551615 94000000000000 940000000000 7ffd... 0 0 0 0 0 0 0 0 0 17 0 0 0 0 0 0 0 0 0 0 0 0 0 0

El primer campo es el PID, el segundo el nombre entre paréntesis, el tercero el estado, el cuarto el PPID. El resto son campos numéricos que corresponden a los campos de task_struct. Por eso ps y top no necesitan llamadas al sistema especiales: solo leen este archivo.

9. PCB en distintos sistemas

La idea de PCB es universal, pero cada sistema la implementa a su manera. Las diferencias principales:

SistemaNombre de la estructuraCaracterísticas
Linuxtask_structUna sola estructura para procesos e hilos. Campos como punteros a subestructuras compartidas. Muy grande y flexible.
WindowsEPROCESS y KPROCESSDos estructuras separadas: KPROCESS para planificación y memoria, EPROCESS para recursos y seguridad. Los hilos tienen su propio ETHREAD y KTHREAD.
macOS / BSDproc y uthreadEstructura proc para el proceso y uthread para cada hilo. Similar a linux en espíritu.
Sistemas clásicos (Tanenbaum)PCBEstructura compacta y autocontenida, con todos los campos en un solo bloque. Es el modelo didáctico.

La diferencia más importante entre linux y windows es que linux usa una sola estructura para procesos e hilos (un hilo es solo un proceso que comparte memoria con otros), mientras que windows los trata como entidades distintas desde el principio. Por eso, en linux, fork y la creación de un hilo usan el mismo mecanismo (clone), y la diferencia está en que se comparte.

10. Un programa que lee su propio PCB

El siguiente programa muestra como un proceso puede consultar su propio PCB a través de /proc. No puede leer task_struct directamente, pero puede leer los archivos que el núcleo genera a partir de ella:

leer_pcb.c
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
    pid_t pid = getpid();

    printf("=== PCB del proceso %d (via /proc) ===\n\n", (int) pid);

    /* status: identidad y estado */
    char ruta[64];
    snprintf(ruta, sizeof ruta, "/proc/%d/status", (int) pid);
    FILE *f = fopen(ruta, "r");
    if (f) {
        char linea[256];
        while (fgets(linea, sizeof linea, f)) {
            if (strncmp(linea, "Name:", 5) == 0 ||
                strncmp(linea, "Pid:", 4) == 0 ||
                strncmp(linea, "PPid:", 5) == 0 ||
                strncmp(linea, "State:", 6) == 0 ||
                strncmp(linea, "Threads:", 8) == 0 ||
                strncmp(linea, "Vmsize:", 7) == 0 ||
                strncmp(linea, "VmRSS:", 6) == 0) {
                fputs(linea, stdout);
            }
        }
        fclose(f);
    }

    /* stat: tiempos de CPU */
    snprintf(ruta, sizeof ruta, "/proc/%d/stat", (int) pid);
    f = fopen(ruta, "r");
    if (f) {
        unsigned long utime, stime;
        int p, pp;
        char comm[64], state;
        /* campos: pid comm state ppid ... utime stime ... */
        fscanf(f, "%d %63s %c %d", &p, comm, &state, &pp);
        /* saltar hasta utime (campo 14) y stime (campo 15) */
        for (int i = 0; i < 9; i++) fscanf(f, "%*s");
        fscanf(f, "%lu %lu", &utime, &stime);
        printf("\ntiempo en modo usuario: %lu ticks\n", utime);
        printf("Tiempo en modo núcleo:  %lu ticks\n", stime);
        fclose(f);
    }

    /* limits: límites de recursos */
    printf("\n--- Límites de recursos (/proc/PID/limits) ---\n");
    snprintf(ruta, sizeof ruta, "/proc/%d/limits", (int) pid);
    f = fopen(ruta, "r");
    if (f) {
        char linea[256];
        while (fgets(linea, sizeof linea, f)) {
            if (strncmp(linea, "Max open files", 14) == 0 ||
                strncmp(linea, "Max processes", 13) == 0 ||
                strncmp(linea, "Max stack size", 14) == 0) {
                fputs(linea, stdout);
            }
        }
        fclose(f);
    }

    printf("\ncomprueba con: cat /proc/%d/status\n", (int) pid);
    return 0;
}

El programa lee tres archivos de /proc: status para la identidad y el estado, stat para los tiempos de CPU y limits para los límites de recursos. Cada uno de esos datos vive en el task_struct del proceso, y el núcleo los formatea al leerlos. Es un ejemplo de como un programa puede inspeccionar su propio PCB sin privilegios especiales.

11. Resumen

12. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Que es el bloque de control de proceso (PCB)?

Es la estructura de datos en la que el núcleo guarda toda la información que necesita para administrar un proceso: identidad, estado, memoria, archivos, señales, tiempos y jerarquía. Mientras el proceso vive, el núcleo mantiene un PCB para él.

2. ¿Por que se dice que para el núcleo un proceso «es» su PCB?

Porque todo lo que el sistema hace con un proceso —ejecutarlo, suspenderlo, enviarle señales, cobrarle tiempo, matarlo— pasa por leer o modificar la estructura que lo representa. El programa del usuario no ve el PCB, solo sus efectos.

3. Nombra los seis grupos de campos de un PCB.

Identidad (PID, PPID, UID), estado y planificación, memoria (tablas de páginas, zonas), recursos y archivos (fd, cwd, límites), señales (bloqueadas, pendientes, manejadores) y jerarquía (padre, hijos, grupo).

4. ¿Que es task_struct y en que sistema se usa?

Es la estructura que implementa el PCB en linux, definida en include/linux/sched.h. Es una de las estructuras más grandes del núcleo y apunta a subestructuras especializadas para memoria, archivos y señales.

5. ¿Que es el puntero current?

Es una variable (en realidad una macro) del núcleo que apunta al task_struct del proceso que se está ejecutando en la CPU actual. Permite al código del núcleo saber a que proceso corresponde la ejecución en curso.

6. ¿Que guarda el núcleo en el PCB durante un cambio de contexto?

Los registros de la CPU del proceso que sale: contador de programa, puntero de pila y registros generales, en el campo thread. Al reanudar el proceso, los restaura desde ahí.

7. ¿Que hace fork con el PCB del padre?

Copia sus campos al PCB del hijo (estado, prioridad, punteros a memoria, archivos, señales, credenciales), le asigna un PID nuevo y le pone como PPID el PID del padre. La memoria se comparte con copia en escritura.

8. ¿Que archivo de /proc expone los tiempos de CPU de un proceso?

/proc/PID/stat, en los campos utime (tiempo en modo usuario) y stime (tiempo en modo núcleo). /proc/PID/status también muestra algunos datos de memoria y estado.

9. ¿En que se diferencia el modelo de PCB de linux del de windows?

Linux usa una sola estructura (task_struct) para procesos e hilos, que comparten subestructuras. Windows separa el proceso (EPROCESS/KPROCESS) de los hilos (ETHREAD/KTHREAD).

10. ¿Por que el PCB no se libera inmediatamente cuando un proceso termina?

Porque el padre necesita leer el código de salida del hijo con wait(). Hasta entonces, el PCB queda como zombi (estado terminado) guardando ese código. Recién cuando el padre lo recoge se libera la estructura.

Para practicar

  1. Compila y ejecuta leer_pcb.c, y compara su salida con cat /proc/PID/status y cat /proc/PID/stat.
  2. Mientras un programa está corriendo, mira ls -l /proc/PID/fd y cat /proc/PID/limits.
  3. Usa cat /proc/PID/wchan mientras un proceso está dormido (por ejemplo, dentro de sleep) y observa el nombre de la función del núcleo.
  4. Ejecuta strace -e trace=process ./un_programa y observa las llamadas al sistema que crean y modifican el PCB.
  5. Compara el tamaño de task_struct en tu sistema con el comando grep task_struct /proc/kallsyms o revisando el código fuente del núcleo.
  6. Escribe un programa que abra varios archivos y luego imprima el contenido de /proc/self/fd para ver la tabla de archivos abiertos.