Procesos e hilos

Departamento de Automática

Contenido

  • Programas
    • El concepto de programa
    • Ejemplo de programa en UNIX
    • Formato ejecutable
  • Procesos
    • El concepto de proceso
    • El bloque de control de proceso (PCB)
    • Estados de un proceso en Linux
    • Mapa de memoria de un proceso
  • Servicios POSIX para la gestión de procesos
    • Creación de procesos
    • Ejecución de programas
    • Terminación de procesos
    • Espera a la terminación de un proceso
  • Hilos
    • Objetivos y conceptos
    • Implementación y programación con hilos
    • Hilos frente a procesos
    • Ejemplo de programación
  • Sincronización de procesos e hilos
    • El problema
    • Sincronización mediante semáforos

Programas

Concepto y características de un programa

Un programa es un conjunto de instrucciones y datos que normalmente se guarda en un fichero regular.

Características de un programa:

  • Se almacena en un dispositivo no volátil
  • Para ejecutarse, tiene que cargarse en memoria
  • Es una entidad estática

Ejemplo de programa en UNIX

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int printit = 1;
char *nomprog;
 
int main(int argc, char *argv[]) {
    int i;
    int *ptr;
    nomprog = argv[0];
    printf("No. of args = %d\n", argc);
    printf("Program name.: %s\n", nomprog);
    for (i = 0; i < argc; i++) {
        ptr = malloc(strlen(argv[i]) + 1);
        strcpy(ptr, argv[i]);
        if (printit) {
            printf("%s\n", ptr);
        }
        free(ptr);
    }
    return 0;
}

(el programa está anotado con un mapa de memoria “Memory Map” que señala qué parte del código corresponde a cada región: la declaración int printit = 1; a “Código (text)” — sic, aparece mezclada con la etiqueta en inglés “(text)” doblada por el resaltado del PDF —, char *nomprog; a “Datos inicializados (data)”, las variables locales i / ptr a “Datos no inicializados (bss)”, la memoria reservada con malloc a “Dynamic memory (heap)”, y las variables automáticas de la pila a “Pila / Stack (stack)“. La posición exacta de cada llamada no se pudo recuperar de la extracción OCR.)

Formato ejecutable

Fichero ejecutable:

NombreOffsetTamaño
Código (text)10003000
Datos inicializados40001000
Datos no inicializados-100
Tabla de símbolos70001000

(diagrama del fichero ejecutable: la cabecera “Header” (“redaeH” invertido en la extracción) contiene el número mágico “Magic number” y el contador de programa inicial “Initial program counter”, seguida de la tabla de secciones “Section table” (“snoitceS” invertido); las secciones —Code, Initialized data, Symbol table— comienzan en los offsets 0, 1000, 4000 y 7000 respectivamente)

Existen muchos formatos distintos de fichero ejecutable: ELF, EXE, COM, etc.

Procesos

El concepto de proceso

Un proceso es un programa en ejecución.

Características de un proceso:

  • Es una entidad dinámica
  • Los procesos tienen sus propios datos, código, pila, heap, etc.
  • El SO les asigna recursos: CPU, memoria, ficheros, etc.
  • El SO controla su ejecución
  • El SO lleva la contabilidad de todos los procesos en ejecución

Muchos procesos pueden ejecutar el mismo programa. Un proceso puede crear otro proceso.

Procesos en UNIX

  • Cada proceso tiene un identificador de proceso único (PID)
  • Todos los procesos forman una jerarquía cuya raíz es el proceso init
    • init es el primer proceso que arranca en UNIX
    • Su PID es 1
    • El objetivo principal de init es arrancar los distintos procesos, por ejemplo: login
    • En muchas distribuciones de Linux, init está siendo sustituido por systemd
  • El PPID (Parent Process IDentifier) es el PID del proceso padre

El bloque de control de proceso (PCB)

Concepto: es una gran estructura de datos con información sobre cada proceso.

La información del PCB es:

  • El estado actual del proceso
  • El identificador de proceso único (PID)
  • La prioridad del proceso
  • El mapa de memoria del proceso
  • Los recursos asignados al proceso
  • El área de salvaguarda de los registros

En Linux, el PCB se llama task_struct.

Diagrama de estados de un proceso en Linux

(diagrama de estados del proceso en Linux: Ready —(Dispatch)→ Run —(Preempt)→ Ready; Run —(Sleep)→ Sleep —(Wake up)→ Ready; Run —(End)→ Zombie (EXIT_ZOMBIE); los estados del kernel de Linux asociados son TASK_RUNNING, TASK_INTERRUPTIBLE, TASK_UNINTERRUPTIBLE / TASK_KILLABLE (ambos para Sleep), TASK_STOPPED (Parado) y TASK_TRACED)

Mapa de memoria de un proceso

El espacio de direcciones virtual de un proceso

(diagrama de traducción de direcciones: los espacios de direcciones virtuales de “application A” y “application B” (con páginas A-G) se traducen, cada uno mediante su propia tabla de traducción (“Translation table”) y la MMU, al mismo espacio de memoria física (“Physical Memory”); la disposición exacta de las páginas no se pudo recuperar de la extracción OCR)

El fichero ejecutable frente al mapa de memoria de un proceso

Fichero ejecutableMapa de memoria
Cabecera (“redaeH” invertido): número mágico, contador de programa inicial—
Tabla de secciones (“snoitceS” invertido)—
CódigoCódigo (text)
Datos inicializadosDatos inicializados (data)
La tabla de símbolosDatos no inicializados (bss)
—Memoria dinámica (heap)
—Proyección del fichero X
—Memoria compartida
—Código de la biblioteca dinámica Z
—Datos de la biblioteca dinámica Z
—Pila del hilo 1 / Pila

(la correspondencia fila a fila entre el fichero ejecutable —offsets 0, 1000, 4000, 7000— y el mapa de memoria resultante no se pudo recuperar con precisión de la extracción OCR; se listan ambas columnas por separado)

Propiedades de las regiones: soporte (fichero/anónima), compartida, protección y tamaño. («Posibles regiones presentes en tiempo de ejecución» — texto invertido en la extracción como “ni / tneserp / snoiger / elbissoP / emit-nur”)

Mapa de memoria de un proceso en UNIX

(diagrama de contextos: el “User context” incluye Code (text), Initialized data, Uninitialized data (BSS) y HEAP (dynamic memory); el “Kernel Context” incluye datos del kernel sobre el proceso y la pila en modo kernel (“Kernel mode stack”). La pregunta que ilustra el diagrama es “What’s the relationship between context and CPU execution mode?”: en modo supervisor (“Supervisor mode”) se distinguen las transiciones 2 (User context) y 4 (Interrupts); en modo usuario (“User mode”) las transiciones 1 (Normal execution) y 3 (System calls). La disposición exacta del diagrama no se pudo recuperar de la extracción OCR.)

Mapa de memoria de un proceso en Linux en arquitecturas de 32 bits

(diagrama de espacio de direcciones de 32 bits: de 0x00000000 a 0xC0000000 el espacio de Usuario (3 GB), que empieza en 0x08048000 con Code (text), Initialized data (data), Uninitialized data (bss), crece hacia arriba con la memoria dinámica (heap, con desplazamiento aleatorio a partir de start_brk) hasta 0xBFFFFFFF, y hacia abajo con la pila (Stack, con un “Stack max size” y desplazamiento aleatorio a partir de start_stack) y la memoria compartida; de 0xC0000000 a 0xFFFFFFFF el espacio de Kernel (1 GB). La disposición exacta no se pudo recuperar de la extracción OCR.)

Mapa de memoria de un proceso en Windows en arquitecturas de 32 bits

(diagrama de espacio de direcciones de 32 bits: de 0x00000000 a 0x80000000 el espacio de Usuario (2 GB) — Code .EXE, Thread stacks, Heap, Code .DLL, hasta 0x7FFFFFFF—; de 0x80000000 a 0xFFFFFFFF el espacio de Kernel (2 GB) — Executive, kernel, HAL, drivers, kernel threads stack, Win32K.sys, Page map tables, Filesystem cache—. La disposición exacta no se pudo recuperar de la extracción OCR.)

Servicios POSIX para la gestión de procesos

Objetivo de los servicios POSIX para la gestión de procesos:

  • Creación de procesos (fork)
  • Asignación de un programa al proceso
  • Terminación de procesos
  • Identificación de procesos
  • Gestión del entorno del proceso

Creación de procesos

Visión general

(diagrama de fork(): el proceso A (PA), con su mapa de memoria virtual —text, data-PA, bss-PA, heap-PA, stack-PA, shared libs, tabla de traducción y PCB—, se duplica en un proceso hijo B (PB) con su propia copia —data-PB, bss-PB, heap-PB, stack-PB, tabla de traducción y PCB—, compartiendo el mismo text y las mismas bibliotecas compartidas (“shared libs”); ambos PCB quedan enlazados en la lista de PCBs del sistema. La disposición exacta del diagrama no se pudo recuperar de la extracción OCR.)

Creación de procesos (servicio POSIX)

Prototipo

pid_t fork()

Descripción

Crea una copia del proceso que realiza la llamada (padre) en un proceso nuevo (hijo).

Devuelve

Si la función tiene éxito:

  • Al padre: el pid del proceso hijo
  • Al proceso hijo: 0

En caso contrario: -1, y el código de error en la variable global errno.

Creación de procesos (ejemplo)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
int main() {
    pid_t id;
    id = fork();
    if (id == -1) {
        perror("Error when calling fork");
        exit(1);
    }
    if (id == 0) {
        while (1) {
            printf("Hi! I'm the child\n");
        }
    } else {
        while (1) {
            printf("Hi! I'm your father\n");
        }
    }
    return 0;
}

Ejecución de programas

Esquema

(diagrama de exec(): el proceso A (PA), con su mapa de memoria virtual —text, data, bss, heap, stack, shared libs, tabla de traducción y PCB—, carga el código del fichero ejecutable correspondiente desde el dispositivo de almacenamiento secundario (“Executable Secondary storage device”), sustituyendo su imagen de memoria; la lista de PCBs se mantiene. La disposición exacta del diagrama no se pudo recuperar de la extracción OCR.)

Ejecución de programas (servicio POSIX)

Prototipo

int execl(const char *path, const char *arg, ...)
int execlp(const char *file, const char *arg, ...)
int execv(const char *path, char const *argv[])
int execvp(const char *path, char const *argv[])
int execvpe(const char *path, char const *argv[], char const *evnp[])

Descripción: sustituye la imagen del proceso actual por otra cargada desde un fichero ejecutable.

Devuelve

En caso de error: -1, y el código de error en la variable global errno.

Ejecución de programas (ejemplo)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
int main() {
    pid_t id;
    char *args[3];
    args[0] = "ps";
    args[1] = "-l";
    args[2] = NULL;
    if (execvp(args[0], args) == -1) {
        perror("Error when calling execvp");
        exit(1);
    }
    return 0;
}

Terminación de procesos

Prototipo

void exit(int status)

Descripción

Termina la ejecución de un proceso. El proceso indica cómo ha terminado mediante el parámetro status.

  • Cuando un proceso termina, sus hijos los adopta el proceso init/systemd.
  • El valor de status se entrega al proceso padre

Espera a la terminación de un proceso

Prototipo

pid_t wait(int *status)
pid_t waitpid(pid_t pid, int *status, int options)

Descripción: hace que el proceso que llama espere a que termine un proceso hijo, y obtiene su estado de terminación.

Devuelve: el PID del proceso que ha terminado.

¿Cómo se ejecuta una orden de bash?

Espera a la terminación de un proceso (ejemplo)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
int main() {
    pid_t id;
    int status;
    id = fork();
    if (id == -1) {
        perror("Error when calling fork");
        exit(1);
    }
    if (id == 0) {
        printf("I'm the child\n");
        sleep(5);
        printf("Child: awakes and ends\n");
        exit(0);
    } else {
        printf("I'm the father\n");
        wait(&status);
        printf("Father: The child ended up with status = %d\n", estado);
        exit(0);
    }
    return 0;
}

Hilos

Objetivos y conceptos

Objetivo: compartir recursos entre procesos que cooperan.

Concepto

  • Hilo = proceso ligero (LWP)
  • Son las unidades fundamentales que utiliza el procesador.
  • Cada hilo tiene su propia copia de los registros de la CPU y su propia pila
  • Las tareas pasan a ser contenedores de hilos; por tanto, un hilo pertenece a una única tarea.
  • El código, los datos y los recursos (ficheros, memoria compartida, etc.) pertenecen a la tarea

Características

  • Cada hilo comparte el código, los datos y los recursos del SO con los demás hilos de su tarea.
  • Una tarea sin hilos no tiene capacidad de ejecución
  • Los hilos son muy convenientes para los sistemas multiprocesador (SMP)
  • Un proceso convencional está formado por un único hilo de ejecución

(diagrama: un proceso pesado “Heavy process” corresponde a una única Task; los procesos ligeros “Lightweight processes” son los threads dentro de esa misma Task)

Implementación y programación con hilos

  • Todos los SO modernos admiten hilos (Windows, Linux, MacOS, etc.)
  • Existen bibliotecas para programar con hilos, como POSIX Threads, Sun Threads, DCE Threads, etc.
  • Al programar con hilos hay que tener especial cuidado para evitar las condiciones de carrera y otros problemas de sincronización

Hilos frente a procesos

  • Los hilos se crean y se destruyen más rápido que los procesos
  • El tiempo de cambio entre hilos es mucho menor que el tiempo de cambio entre procesos (cambio de contexto)
  • Todos los hilos de un mismo proceso comparten la memoria, por lo que hay menos sobrecarga de comunicación (y muchos más problemas de sincronización)

Ejemplo de programación

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
 
void *threadfun(void *arg) {
    int thread_num = *((int *)arg);
    printf("I'm the child -> %d\n", thread_num);
    pthread_exit(0);
}
 
int main() {
    pthread_t thread1, thread2;
    int id1 = 1, id2 = 2;
    pthread_create(&thread1, NULL, threadfun, &id1);
    pthread_create(&thread2, NULL, threadfun, &id2);
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    return 0;
}

Sincronización de procesos e hilos

El problema

Sincronización de hilos o procesos: los hilos o procesos necesitan con frecuencia coordinarse, porque cooperan para alcanzar un objetivo o porque compiten por un recurso.

Motivos por los que es necesario proporcionar sincronización:

  • Algunos recursos deben usarse en exclusión mutua
  • Un proceso debe esperar a la acción de otro proceso
  • Varios procesos/hilos pueden leer-modificar-escribir la misma posición de memoria, lo que da lugar a las famosas condiciones de carrera

Un proceso no puede detener su propia ejecución ni la de otro ⇒ se necesita la intervención del núcleo.

Sincronización mediante semáforos

Los semáforos son objetos del núcleo compartidos por todos los procesos que intervienen en la sincronización.

Operaciones sobre los semáforos:

  • Operación P (semáforo):
    • Si el semáforo tiene un valor positivo, su valor se decrementa en una unidad
    • Si el semáforo vale cero, el hilo/proceso que llama se duerme en una lista de espera
  • Operación V (semáforo):
    • Si no hay hilos/procesos dormidos, su valor se incrementa en una unidad.
    • Si hay hilos/procesos dormidos, se despierta al primero de la lista

Algunos problemas típicos de sincronización

Sección crítica (condición de carrera) frente a productor-consumidor básico:

P1P2productorconsumidor
…………
P(S1);P(S1);data = create();P(S1);
A++;a++;V(S1);consume(data);
V(S1);V(S1);……
……

¿Importa el valor inicial de los semáforos?

Bibliografía

  • Sebastián Sánchez. Operating Systems. Second Edition. Universidad de Alcalá - Servicio de Publicaciones, 2005
  • Francisco M. Márquez. UNIX. Advanced Programming. Ed. Ra-Ma, 2004
  • A. S. Tanenbaum. Modern Operating Systems. 3rd Edition. Prentice Hall, 2009
  • William Stallings. Computer organization and architecture. 10th Edition. Pearson, 2015

© Pablo Parra, Óscar García. Departamento de Automática. Universidad de Alcalá. Este documento se distribuye bajo los términos de la licencia Creative Commons Attribution ShareAlike 4.0 (internacional): https://creativecommons.org/licenses/by-sa/4.0/