¿Se implementan los hilos como procesos en Linux?

65

Estoy leyendo este libro , Programación avanzada de Linux por Mark Mitchell, Jeffrey Oldham y Alex Samuel. Es de 2001, un poco viejo. Pero me parece bastante bueno de todos modos.

Sin embargo, llegué a un punto en el que diverge de lo que mi Linux produce en la salida del shell. En la página 92 ​​(116 en el visor), el capítulo 4.5 Implementación de subprocesos GNU / Linux comienza con el párrafo que contiene esta declaración:

La implementación de hilos POSIX en GNU / Linux difiere de la implementación de hilos en muchos otros sistemas similares a UNIX de una manera importante: en GNU / Linux, los hilos se implementan como procesos.

Esto parece un punto clave y luego se ilustra con un código C. La salida en el libro es:

main thread pid is 14608
child thread pid is 14610

Y en mi Ubuntu 16.04 es:

main thread pid is 3615
child thread pid is 3615

ps La salida es compatible con esto.

Supongo que algo debe haber cambiado entre 2001 y ahora.

El siguiente subcapítulo en la página siguiente, 4.5.1 Manejo de señales, se basa en la declaración anterior:

El comportamiento de la interacción entre señales y subprocesos varía de un sistema tipo UNIX a otro. En GNU / Linux, el comportamiento está dictado por el hecho de que los hilos se implementan como procesos.

Y parece que esto será aún más importante más adelante en el libro. ¿Alguien podría explicar lo que está pasando aquí?

He visto este. ¿Los hilos del kernel de Linux son realmente procesos del kernel? , pero no ayuda mucho. Estoy confundido.

Este es el código C:

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void* thread_function (void* arg)
{
    fprintf (stderr, "child thread pid is %d\n", (int) getpid ());
    /* Spin forever. */
    while (1);
    return NULL;
}

int main ()
{
    pthread_t thread;
    fprintf (stderr, "main thread pid is %d\n", (int) getpid ());
    pthread_create (&thread, NULL, &thread_function, NULL);
    /* Spin forever. */
    while (1);
    return 0;
}
Tomász
fuente
1
No entiendo cuál es la fuente de tu confusión. Los hilos se implementan como procesos que comparten el espacio de direcciones con sus padres.
Johan Myréen
2
@ JohanMyréen Entonces, ¿por qué son iguales los pids de hilo?
Tomasz
Ah, ya veo. Sí, algo realmente ha cambiado. Ver la respuesta de @ ilkkachu.
Johan Myréen
55
Los subprocesos todavía se implementan como procesos; sin embargo, ahora getpiddevuelve lo que se llamaría una ID de grupo de subprocesos y para obtener una ID única para un proceso que debe usar gettid. Sin embargo, aparte del núcleo, la mayoría de las personas y las herramientas llamarán a un grupo de subprocesos un proceso, y llamarán a un proceso un subproceso, por coherencia con otros sistemas.
user253751
Realmente no. Un proceso tiene sus propios descriptores de archivos y memoria, nunca se llama un hilo, hacerlo sería en consonancia con otros sistemas.
reinierpost

Respuestas:

50

Creo que esta parte de la clone(2)página del manual puede aclarar la diferencia re. el PID:

CLONE_THREAD (desde Linux 2.4.0-test8)
Si se establece CLONE_THREAD, el elemento secundario se coloca en el mismo grupo de subprocesos que el proceso de llamada.
Los grupos de subprocesos fueron una característica agregada en Linux 2.4 para admitir la noción de subprocesos POSIX de un conjunto de subprocesos que comparten un único PID. Internamente, este PID compartido es el denominado identificador de grupo de subprocesos (TGID) para el grupo de subprocesos. Desde Linux 2.4, las llamadas a getpid (2) devuelven el TGID de la persona que llama.

La frase "los subprocesos se implementan como procesos" se refiere al tema de los subprocesos que tuvieron PID separados en el pasado. Básicamente, Linux originalmente no tenía subprocesos dentro de un proceso, solo procesos separados (con PID separados) que podrían haber tenido algunos recursos compartidos, como memoria virtual o descriptores de archivos. CLONE_THREADy la separación de la ID del proceso (*) y la ID del hilo hacen que el comportamiento de Linux se parezca más a otros sistemas y más a los requisitos POSIX en este sentido. Aunque técnicamente el sistema operativo todavía no tiene implementaciones separadas para subprocesos y procesos.

El manejo de la señal era otra área problemática con la implementación anterior, esto se describe con más detalle en el documento al que @FooF se refiere en su respuesta .

Como se señaló en los comentarios, Linux 2.4 también se lanzó en 2001, el mismo año que el libro, por lo que no es sorprendente que las noticias no lleguen a esa impresión.

ilkkachu
fuente
2
procesos separados que podrían haber tenido algunos recursos compartidos, como memoria virtual o descriptores de archivos. Así es como funcionan los subprocesos de Linux, con los problemas que menciona que se han limpiado. Yo diría que llamar a las unidades de programación utilizadas en el núcleo "hilos" o "procesos" es realmente irrelevante. El hecho de que comenzaron en Linux siendo llamados solo "procesos" no significa que eso es todo lo que son ahora.
Andrew Henle
@ AndrewHenle, sí, editó un poco. Espero que capte tu pensamiento, aunque me parece difícil con la redacción. (adelante y edite esa parte si lo desea). He entendido que algunos otros sistemas operativos tipo Unix tienen una separación más clara de subprocesos frente a procesos, con Linux como una especie de excepción al tener realmente un solo tipo de servicio ambas funciones Pero no sé lo suficiente sobre otros sistemas y no tengo fuentes a mano, por lo que es difícil decir algo concreto.
ilkkachu
@tomas Tenga en cuenta que esta respuesta explica cómo funciona Linux ahora. Como insinúa ilkkachu, funcionó de manera diferente cuando se escribió el libro. La respuesta de FooF explica cómo funcionaba Linux en ese momento.
Gilles 'SO- deja de ser malvado'
38

Tienes razón, de hecho "algo debe haber cambiado entre 2001 y ahora". El libro que está leyendo describe el mundo según la primera implementación histórica de hilos POSIX en Linux, llamada LinuxThreads (vea también el artículo de Wikipedia para algunos).

LinuxThreads tuvo algunos problemas de compatibilidad con el estándar POSIX, por ejemplo, hilos que no comparten PID, y algunos otros problemas graves. Para solucionar estos defectos, Red Hat encabezó otra implementación llamada NPTL (Biblioteca nativa de hilos POSIX) para agregar el núcleo necesario y la biblioteca de espacio de usuario para alcanzar un mejor cumplimiento POSIX (tomando buenas partes de otro proyecto de reimplementación de IBM llamado NGPT (" Hilos Posix de próxima generación "), consulte el artículo de Wikipedia sobre NPTL ). Las banderas adicionales agregadas a la clone(2)llamada al sistema (especialmente lo CLONE_THREADque @ikkkachuseñala en su respuesta ) es probablemente la parte más evidente de las modificaciones del núcleo. La parte del espacio de usuario del trabajo finalmente se incorporó a la Biblioteca GNU C.

Todavía hoy en día, algunos SDK de Linux incorporados usan la antigua implementación de LinuxThreads porque están usando una versión de huella de memoria más pequeña de LibC llamada uClibc (también llamada µClibc) , y pasaron una cantidad considerable de años antes de que la implementación del espacio de usuario NPTL de GNU LibC fuera portada y asumida como implementación predeterminada de subprocesos POSIX, ya que, en general, estas plataformas especiales no se esfuerzan por seguir las últimas modas a la velocidad del rayo. Esto se puede observar al notar que, de hecho, los PID para diferentes subprocesos en esas plataformas también son diferentes a diferencia de lo que especifica el estándar POSIX, al igual que el libro que está leyendo describe. En realidad una vez que llamastepthread_create(), de repente, aumentó el recuento de procesos de uno a tres, ya que se necesitaba un proceso adicional para mantener el desorden.

La página del manual de Linux pthreads (7) proporciona una descripción completa e interesante de las diferencias entre los dos. Otra descripción esclarecedora, aunque desactualizada, de las diferencias es este artículo de Ulrich Depper e Ingo Molnar sobre el diseño de NPTL.

Te recomiendo que no te tomes esa parte del libro demasiado en serio. En cambio, recomiendo los hilos POSIX de programación de Butenhof y las páginas de manual POSIX y Linux sobre el tema. Muchos tutoriales sobre el tema son inexactos.

FooF
fuente
22

Los subprocesos (espacio de usuario) no se implementan como procesos como tales en Linux, ya que no tienen su propio espacio de direcciones privadas, aún comparten el espacio de direcciones del proceso principal.

Sin embargo, estos subprocesos se implementan para usar el sistema de contabilidad de procesos del kernel, por lo que se les asigna su propio ID de subproceso (TID), pero se les da el mismo PID y 'ID de grupo de subprocesos' (TGID) que el proceso principal; esto contrasta con una bifurcación, donde se crean un nuevo TGID y PID, y el TID es el mismo que el PID.

Entonces, parece que los núcleos recientes tenían un TID separado que se puede consultar, esto es diferente para los subprocesos, un fragmento de código adecuado para mostrar esto en cada una de las principales () thread_function () anterior es:

    long tid = syscall(SYS_gettid);
    printf("%ld\n", tid);

Entonces, el código completo con esto es:

#include <pthread.h>                                                                                                                                          
#include <stdio.h>                                                                                                                                            
#include <unistd.h>                                                                                                                                           
#include <syscall.h>                                                                                                                                          

void* thread_function (void* arg)                                                                                                                             
{                                                                                                                                                             
    long tid = syscall(SYS_gettid);                                                                                                                           
    printf("child thread TID is %ld\n", tid);                                                                                                                 
    fprintf (stderr, "child thread pid is %d\n", (int) getpid ());                                                                                            
    /* Spin forever. */                                                                                                                                       
    while (1);                                                                                                                                                
    return NULL;                                                                                                                                              
}                                                                                                                                                             

int main ()                                                                                                                                                   
{                                                                                                                                               
    pthread_t thread;                                                                               
    long tid = syscall(SYS_gettid);     
    printf("main TID is %ld\n", tid);                                                                                             
    fprintf (stderr, "main thread pid is %d\n", (int) getpid ());                                                    
    pthread_create (&thread, NULL, &thread_function, NULL);                                           
    /* Spin forever. */                                                                                                                                       
    while (1);                                                                                                                                                
    return 0;                                                                                                                                                 
} 

Dando un ejemplo de salida de:

main TID is 17963
main thread pid is 17963
thread TID is 17964
child thread pid is 17963
einonm
fuente
3
@tomas einonm tiene razón. Ignore lo que dice el libro, es terriblemente confuso. No sé qué idea quería transmitir el autor, pero fracasó gravemente. Entonces, en Linux tienes hilos de Kernel y hilos de espacio de usuario. Los hilos de kernel son esencialmente procesos sin espacio de usuario en absoluto. Los hilos de espacio de usuario son hilos POSIX normales. Los procesos de espacio de usuario comparten descriptores de archivo, pueden compartir segmentos de código, pero viven en espacios de direcciones virtuales completamente separados. Los subprocesos de espacio de usuario dentro de un proceso comparten segmento de código, memoria estática y almacenamiento dinámico (memoria dinámica), pero tienen conjuntos y pilas de registros de procesador separados.
Boris Burkov
8

Básicamente, la información en su libro es históricamente precisa, debido a un historial de implementación vergonzosamente malo de hilos en Linux. Esta respuesta de mí a una pregunta relacionada sobre SO también sirve como respuesta a su pregunta:

https://stackoverflow.com/questions/9154671/distinction-between-processes-and-threads-in-linux/9154725#9154725

Todas estas confusiones surgen del hecho de que los desarrolladores del kernel originalmente tenían una opinión irracional e incorrecta de que los subprocesos podrían implementarse casi por completo en el espacio de usuario utilizando los procesos del kernel como primitivos, siempre que el kernel ofreciera una forma de compartir memoria y descriptores de archivos . Esto condujo a la implementación notoriamente mala de LinuxThreads de hilos POSIX, que era más bien un nombre inapropiado porque no daba nada remotamente parecido a la semántica de hilos POSIX. Finalmente, LinuxThreads fue reemplazado (por NPTL), pero persisten muchos de los términos confusos y malentendidos.

Lo primero y más importante a tener en cuenta es que "PID" significa cosas diferentes en el espacio del kernel y el espacio del usuario. Lo que el núcleo llama PID son en realidad identificadores de subprocesos a nivel del núcleo (a menudo llamados TID), que no deben confundirse con pthread_tun identificador separado. Cada subproceso del sistema, ya sea en el mismo proceso o en uno diferente, tiene un TID único (o "PID" en la terminología del núcleo).

Lo que se considera un PID en el sentido POSIX de "proceso", por otro lado, se denomina "ID de grupo de subprocesos" o "TGID" en el núcleo. Cada proceso consta de uno o más subprocesos (procesos del núcleo), cada uno con su propio TID (PID del núcleo), pero todos comparten el mismo TGID, que es igual al TID (PID del núcleo) del subproceso inicial en el que se mainejecuta.

Cuando le topmuestra hilos, muestra TID (PID de núcleo), no PID (TGID de núcleo), y es por eso que cada hilo tiene uno diferente.

Con el advenimiento de NPTL, la mayoría de las llamadas al sistema que toman un argumento PID o actúan en el proceso de llamada se cambiaron para tratar el PID como un TGID y actuar en todo el "grupo de subprocesos" (proceso POSIX).

R ..
fuente
8

Internamente, no existen procesos o subprocesos en el kernel de Linux. Los procesos y los subprocesos son principalmente un concepto de tierra de usuario, el núcleo solo ve "tareas", que son un objeto programable que puede compartir ninguno, algunos o todos sus recursos con otras tareas. Los subprocesos son tareas que se han configurado para compartir la mayoría de sus recursos (espacio de direcciones, mmaps, canalizaciones, controladores de archivos abiertos, sockets, etc.) con la tarea principal, y los procesos son tareas que se han configurado para compartir recursos mínimos con la tarea principal .

Cuando usa la API de Linux directamente ( clone () , en lugar de fork () y pthread_create () ), tiene mucha más flexibilidad para definir cuántos recursos compartir o no compartir, y puede crear tareas que no son totalmente proceso ni completamente un hilo. Si usa estas llamadas de bajo nivel directamente, también es posible crear una tarea con un nuevo TGID (tratado así como un proceso por la mayoría de las herramientas de usuario) que realmente comparte todos sus recursos con la tarea principal, o viceversa, para crear una tarea con TGID compartido (por lo tanto, tratada como un hilo por la mayoría de las herramientas de userland) que no comparte ningún recurso con su tarea principal.

Si bien Linux 2.4 implementa TGID, esto es principalmente para beneficio de la contabilidad de recursos. Muchos usuarios y herramientas de espacio de usuario encuentran útil poder agrupar tareas relacionadas e informar sobre el uso de sus recursos.

La implementación de tareas en Linux es mucho más fluida que la cosmovisión de procesos e hilos presentada por las herramientas de espacio de usuario.

Lie Ryan
fuente
El documento @FooF vinculado a describe una serie de puntos donde el núcleo tiene que considerar los procesos y los hilos como entidades separadas (por ejemplo, manejo de señales y exec ()), por lo que después de leerlo, realmente no diría que "no existe tal cosa como procesos o hilos en el kernel de Linux ".
ilkkachu
5

Linus Torvalds declaró en una publicación de la lista de correo del núcleo en 1996 que "tanto los subprocesos como los procesos se tratan como un 'contexto de ejecución'", que es "solo un conglomerado de todo el estado de ese CoE ... incluye cosas como CPU estado, estado MMU, permisos y varios estados de comunicación (archivos abiertos, manejadores de señales, etc.) ".

// simple program to create threads that simply sleep
// compile in debian jessie with apt-get install build-essential
// and then g++ -O4 -Wall -std=c++0x -pthread threads2.cpp -o threads2
#include <string>
#include <iostream>
#include <thread>
#include <chrono>

// how many seconds will the threads sleep for?
#define SLEEPTIME 100
// how many threads should I start?
#define NUM_THREADS 25

using namespace std;

// The function we want to execute on the new thread.
void threadSleeper(int threadid){
    // output what number thread we've created
    cout << "task: " << threadid << "\n";
    // take a nap and sleep for a while
    std::this_thread::sleep_for(std::chrono::seconds(SLEEPTIME));
}

void main(){
    // create an array of thread handles
    thread threadArr[NUM_THREADS];
    for(int i=0;i<NUM_THREADS;i++){
        // spawn the threads
        threadArr[i]=thread(threadSleeper, i);
    }
    for(int i=0;i<NUM_THREADS;i++){
        // wait for the threads to finish
        threadArr[i].join();
    }
    // program done
    cout << "Done\n";
    return;
}

Como puede ver, este programa generará 25 hilos a la vez, cada uno de los cuales dormirá durante 100 segundos y luego se unirá al programa principal nuevamente. Después de que los 25 hilos se hayan unido al programa, el programa estará listo y se cerrará.

Utilizando toppodrás ver 25 instancias del programa "threads2". Pero es un niño aburrido. La salida de ps auwxes aún menos interesante ... PERO se ps -eLfvuelve un poco emocionante.

UID        PID  PPID   LWP  C NLWP STIME TTY          TIME CMD
debian     689   687   689  0    1 14:52 ?        00:00:00 sshd: debian@pts/0  
debian     690   689   690  0    1 14:52 pts/0    00:00:00 -bash
debian    6217   690  6217  0    1 15:04 pts/0    00:00:00 screen
debian    6218  6217  6218  0    1 15:04 ?        00:00:00 SCREEN
debian    6219  6218  6219  0    1 15:04 pts/1    00:00:00 /bin/bash
debian    6226  6218  6226  0    1 15:04 pts/2    00:00:00 /bin/bash
debian    6232  6219  6232  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6233  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6234  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6235  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6236  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6237  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6238  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6239  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6240  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6241  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6242  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6243  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6244  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6245  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6246  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6247  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6248  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6249  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6250  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6251  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6252  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6253  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6254  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6255  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6256  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6257  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6260  6226  6260  0    1 15:04 pts/2    00:00:00 ps -eLf

Puede ver aquí los 26 CoE que thread2ha creado el programa. Todos comparten la misma ID de proceso (PID) e ID de proceso principal (PPID), pero cada uno tiene una ID de LWP (proceso de peso ligero) diferente, y la cantidad de LWP (NLWP) indica que hay 26 CoE: el programa principal y el 25 hilos generados por él.

ivanivan
fuente
Correcto, un hilo es solo un proceso ligero (LWP)
fpmurphy
2

Cuando se trata de procesos y subprocesos de Linux, son más o menos lo mismo. Es decir, que se crean con la misma llamada al sistema: clone.

Si lo piensa, la diferencia entre subprocesos y procesos está en que los objetos del núcleo serán compartidos por el elemento secundario y el elemento primario. Para los procesos, no es mucho: descriptores de archivos abiertos, segmentos de memoria en los que no se ha escrito, probablemente algunos otros que no se me ocurren. Para los hilos, se comparten muchos más objetos, pero no todos.

Lo que acerca los hilos y los objetos en Linux es la unsharellamada al sistema. Los objetos del núcleo que comienzan como compartidos se pueden compartir después de la creación de subprocesos. Por lo tanto, puede tener dos subprocesos del mismo proceso que tengan un espacio de descriptor de archivo diferente (al revocar el uso compartido de descriptores de archivo después de crear los subprocesos). Puede probarlo usted mismo creando un hilo, llamando unsharea ambos hilos y luego cerrando todos los archivos y abriendo nuevos archivos, tuberías u objetos en ambos hilos. Luego mire /proc/your_proc_fd/task/*/fdy verá que cada uno task(que creó como un hilo) tendrá diferentes fd.

De hecho, tanto la creación de nuevos subprocesos como los nuevos procesos son rutinas de biblioteca que llaman clonedebajo y especifican cuál de los objetos del núcleo taskcompartirá el proceso-thread-thingamajig (es decir, ) recién creado con el proceso / hilo de llamada.

Dmitry Rubanovich
fuente