[linux] concepto de hilo

Insertar descripción de la imagen aquí

¡Me gusta, favorito y sigue si te gusta!Insertar descripción de la imagen aquí

1. Reserva de conocimientos

1.1 Hablemos nuevamente de tablas de páginas

Como se mencionó en el blog anterior, además de las tablas de páginas a nivel de usuario, las tablas de páginas también incluyen tablas de páginas a nivel de kernel, hoy nos estamos expandiendo un poco.
Hay muchos otros atributos en la tabla de páginas, como dirección física, hit o no, permisos RWX, permisos U/K (¿es usted usuario o kernel?).
Insertar descripción de la imagen aquí
Independientemente de si se trata de una tabla de páginas a nivel de usuario o de una tabla de páginas a nivel de kernel, las estructuras de datos utilizadas por todos son las mismas.
Las tablas de páginas también deben ser administradas por el sistema operativo, ¿cómo administrarlas? Primero describa y luego organice .
Entonces, cada entrada en la tabla de páginas es una estructura de datos, lo que equivale a definir un atributo de tipo de estructura que incluye la dirección física, si es visitada, etc.

Con base en este conocimiento, finalmente sabemos que cuando escribimos código como el siguiente, ocurrió un error al ejecutar.

char* str="hello world";
*str='H';

En el nivel de lenguaje C / C ++, esta cadena es una cadena constante y no se puede modificar. Cuando se elimina la referencia a * str, busque la dirección inicial de esta cadena y luego modifíquela, porque este área constante de caracteres está entre las áreas inicializada y de código. . La razón por la que no me permitieron escribir mientras tú escribías ahora está muy clara. Cuando escribe en una constante de cadena, necesita convertir la dirección virtual a la dirección física. Cuando encuentre la dirección física, debe continuar verificando otros atributos de la tabla de páginas, como los permisos RWX. Por ejemplo, solo tiene permisos R, pero direccionamiento virtual a físico. Está realizando una operación de escritura, por lo que la unidad de traducción de direcciones MMU termina directamente su comportamiento actual. ¿Cómo lo terminó? Informar un error directamente al hardware. El sistema operativo reconoce el error de hardware y lo convierte en una señal, comúnmente conocida como falla de segmento. Envía la señal número 11 al proceso actual. El proceso procesa esta señal en el momento apropiado. La acción de procesamiento predeterminada es finalizar, por lo que al final su El proceso finaliza directamente. No se puede explicar claramente en el nivel del lenguaje C/C++, solo se puede explicar en el nivel del sistema operativo.

También tenemos una comprensión clara de la tabla de páginas, y el blog anterior sobre el espacio de direcciones virtuales lo explicó con más detalle.

Entonces, ¿qué opinas sobre los espacios de direcciones y las tablas de páginas?

  1. El espacio de direcciones es la ventana de recursos que un proceso puede ver.
  2. La tabla de páginas determina cuántos recursos posee realmente el proceso.
  3. Al dividir adecuadamente los recursos del espacio de direcciones + tabla de páginas, podemos clasificar todos los recursos de un proceso.

Si eres inteligente, ya debes saber que nuestra tabla de páginas no es simple.
Se ha dicho antes que la dirección virtual a dirección física se convierte a través de la tabla de páginas, pero ¿cómo se convierte?

Hablemos de este tema en detalle a continuación.
Insertar descripción de la imagen aquí
La tabla de páginas tiene entradas una por una.
Tomando como ejemplo una máquina de 32 bits, hay un total de 2 ^ 32 direcciones virtuales. ¿La tabla de páginas debe tener 2^32 entradas? Acabo de decir que una entrada es una estructura de datos que contiene una dirección física y otros atributos. Cuando la dirección es de 4 bytes y se suman otros atributos, tiene un total de 2 bytes, un total de 6 bytes, 2 ^ 32 es aproximadamente 4G de tamaño, 4X6 = 24G, solo guardar la tabla de páginas requiere 24G de espacio (datos de muestra).

Por lo tanto, nuestra comprensión previa del mapeo de tablas de páginas está bien como explicación básica de algunos problemas, pero no es una comprensión correcta.

Entonces, ¿cómo es una tabla de páginas real?
Hablemos primero de direcciones virtuales y memoria física.
La dirección virtual aquí toma como ejemplo 32 bits.
Insertar descripción de la imagen aquí
La memoria física no es tan complicada como imaginamos. Cuando generalmente realizamos operaciones a nivel de sistema operativo en la memoria física, la memoria física ya se ha dividido en bloques de datos uno por uno. Un término más profesional es página de datos.
Insertar descripción de la imagen aquí
El sistema operativo también necesita administrar la memoria física, que se describirá primero y luego se organizará.
¿Cómo describirlo? El tamaño de una página
Insertar descripción de la imagen aquí
se gestiona entonces en forma de matriz, 4 KB . Estos pequeños bloques de memoria física se denominan marcos de página .
Insertar descripción de la imagen aquí

Insertar descripción de la imagen aquí

Cuando compilamos un programa ejecutable en el disco, el programa ejecutable también se divide en áreas de bloques de datos de 4 KB durante la compilación. Esta área se llama marco de página , por lo que cuando el programa ejecutable se carga en la memoria, no se carga en la memoria. por sección de 1 palabra, 1 bit u otros métodos, pero se mueve a la memoria física
Insertar descripción de la imagen aquí
en unidades de 4 KB. Nuestro método de convertir direcciones virtuales en direcciones físicas no se considera una conversión completa, sino que se construye en unidades de 10, 10 y 12. Y nuestra tabla de páginas no es solo una tabla de páginas.

La primera tabla de páginas: directorio de páginas.
Insertar descripción de la imagen aquí
Cuando hay una dirección virtual, primero busca en el directorio de páginas con los primeros 10 bits.
(2 ^ 10 = 1024, aproximadamente 1 KB, suponiendo que una entrada tenga 10 bytes, el directorio de páginas tiene un tamaño de solo 10 KB). 1024, el subíndice comienza de 0 a 1023.
El directorio de páginas puede coincidir con muchas tablas de páginas y luego tomar los 10 bits del segundo lote de direcciones virtuales e indexarlos en la tabla de páginas.
Insertar descripción de la imagen aquí
Cada tabla de páginas también tiene un tamaño de 2^10. Esta entrada de la tabla de páginas apunta a una página en la memoria física.
Insertar descripción de la imagen aquí
Luego están los 12 bits restantes. Lo tenemos aquí. ¿
Insertar descripción de la imagen aquí
Por qué el tamaño de nuestro bloque de memoria física es de 4 KB? Porque el tamaño de 4 KB es exactamente 2^12 potencia. Todo esto está cuidadosamente diseñado.

La entrada en la tabla de páginas se completa con la dirección física inicial del marco de página especificado.
Insertar descripción de la imagen aquí
Hemos localizado la dirección inicial del marco de la página. Los 12 bits restantes tienen exactamente el mismo tamaño que el marco de página y el marco de página, pero a lo que queremos acceder no es al tamaño de 4 KB, sino a un determinado byte ¿Cómo acceder a un determinado byte? Entonces, al final, desde la dirección inicial de la dirección física, más el desplazamiento de 2^12 potencia, podemos encontrar directamente una dirección en un marco de página.
Insertar descripción de la imagen aquí

Entonces, ¿ cómo convertir una dirección virtual en una dirección física ?
Primero busque los primeros 10 bits de la dirección virtual, indexe el directorio de páginas para encontrar la tabla de páginas especificada correspondiente, luego tome 10 bits y luego busque la posición especificada en la tabla de páginas correspondiente, busque directamente la dirección física inicial del marco de página , Busque un determinado marco de página y luego tome los 12 bits restantes. Estos 12 bits sirven como desplazamiento dentro de la página y puede encontrar directamente una dirección física específica en la memoria física.
Siempre que se encuentre una dirección, deberá buscar 4 u 8 consecutivamente, según su tipo, comenzando desde la dirección inicial de ahora y buscando continuamente.
Al igual que int a = 10; a tiene 4 bytes. Al tomar la dirección de a, solo obtienes una dirección (dirección inicial). De acuerdo con la dirección virtual y mediante la conversión de la dirección física, puedes encontrar la dirección física inicial de a en un determinado marco de página., y luego tomar 4 bytes consecutivos de acuerdo con el tipo de número entero eliminará el tipo de número entero.

Y cuando todo el proceso se use nuevamente, es posible usar solo el directorio de páginas y una tabla de páginas. La tabla de páginas sin una relación de mapeo establecida no se creará para usted. Se creará solo cuando sea necesario, por lo que la página Cuando la tabla se carga en la memoria física, el contenido obtenido se reduce considerablemente, lo que puede solucionar el problema de espacio de memoria insuficiente en este momento.

Insertar descripción de la imagen aquí

Después de aprender los conocimientos preparatorios, podemos comprender la división de recursos dentro de un proceso. Mientras esto no sea confuso, no será difícil entender los hilos más adelante.

2. Concepto de hilo

Hemos aprendido sobre procesos antes: Proceso = estructura de datos del kernel + código y datos correspondientes al proceso.
Insertar descripción de la imagen aquí
Entonces, ¿qué es un hilo? ¿Cuál es la relación entre hilos y procesos?
Esto es lo que dice la explicación convencional de los hilos en los libros de texto.
Hilo: Un flujo de ejecución dentro de un proceso.

Cuando vemos esta frase, debemos estar muy confundidos. La razón principal es que las descripciones del sistema operativo son demasiado macro y demasiado abstractas. Esto también es válido para otros sistemas.

Así que hoy seamos específicos y solo hablemos de la implementación de subprocesos múltiples del sistema operativo Linux.
Según esta declaración, la implementación subyacente de subprocesos múltiples en diferentes plataformas es diferente, sí, de hecho es diferente.
Pero una vez que terminemos de hablar sobre subprocesos múltiples en el sistema Linux, también debe cumplir con el concepto anterior dado por cualquier sistema operativo.

Anteriormente dijimos que al crear un proceso hijo, necesitamos copiar la PCB, el espacio de direcciones virtuales, la tabla de páginas y otras estructuras del núcleo del proceso padre al proceso hijo. En ausencia de copia en escritura, el código y los datos del proceso principal también se comparten con el proceso secundario.
También hablamos anteriormente sobre tablas de páginas y memoria virtual.
Ahora revisemos cómo ver la memoria virtual: la memoria virtual determina los recursos que un proceso puede ver.

El proceso se puede imaginar como una persona en una habitación y la memoria virtual como la ventana de la habitación. El paisaje exterior que el proceso quiere ver está completamente determinado por la memoria virtual. La memoria virtual se divide en áreas una por una, estos son los recursos que el proceso puede ver. Por supuesto que hay otros recursos pero no los dejemos de lado.
Se puede acceder al código cargado y a los recursos de datos correspondientes a la memoria física a través de direcciones virtuales y tablas de páginas.

Hemos dicho antes que un proceso en realidad puede dividir su código en una parte para su ejecución mediante otro flujo de ejecución. Por ejemplo, fork crea un proceso hijo y usa el juicio if para permitir que los procesos padre e hijo ejecuten diferentes bloques de código o flujos de ejecución. Esto permite que el proceso hijo acceda a parte del código y los datos del proceso padre actual.
Por supuesto que esto es bueno.

Sin embargo, crear un proceso hijo de esta manera requiere copiar todas las estructuras de datos del kernel del proceso padre al proceso hijo.

Luego, al crear el proceso, solo quiero crear la PCB correspondiente y no crearé las estructuras restantes del núcleo, como el espacio de direcciones virtuales y la tabla de páginas. Y los PCB creados por estos procesos apuntan a la misma memoria virtual que el proceso principal.
Insertar descripción de la imagen aquí
Siempre que apunte a la misma memoria virtual que el proceso principal, cada PCB puede ver los recursos de este proceso a través de la ventana de memoria virtual. Podemos dividir los recursos de este proceso y
Insertar descripción de la imagen aquí
asignarlos a estos PCB, de modo que los procesos creados solo necesiten ejecutar parte del código y acceder a parte de los recursos en el código, solo creamos PCB y se los damos desde el proceso padre. El flujo de ejecución en el que asigna recursos se puede llamar hilo.

En cuanto a la comprensión de este conocimiento, primero debemos llegar a un consenso de que los recursos correspondientes a un proceso pueden asignar algunos de sus recursos a subprocesos específicos a través del espacio de direcciones virtuales + tabla de páginas, tal como dijimos antes mediante if, else Es Se juzga que este bloque de código es entregado por el proceso padre al proceso hijo para su ejecución.
Debido a que dividimos los recursos del proceso a través del espacio de direcciones virtuales + tabla de páginas, la intensidad de ejecución de un solo "proceso" debe ser más detallada que el proceso anterior.

Cuando realmente creemos estos PCB, ¿cómo verá cada task_struct aquí desde la perspectiva de la CPU?
Insertar descripción de la imagen aquí
A la CPU no le importa esto. Solía ​​​​programar los PCB uno por uno. Ahora hay tantos PCB. Cada PCB tiene su propio código y datos correspondientes. ¿Qué tiene que ver conmigo? Soy estúpido. Solo dámelo. Te ayudaré a ejecutarlo. Si no me tienes, no lo ejecutarás. Depende de ti decidir qué ejecutar. En cuanto a si el código que me das es de este proceso o de un parte del código y datos tomados del proceso padre por otros procesos creados con una magnitud más ligera, no me importa. Solo presto atención a task_struct que veo durante la programación. Todo lo que me des se ejecutará y no sentiré que ejecutarlo es más liviano que ejecutarlo.

Así que pensemos ahora en algunas cuestiones.
Si nuestro sistema operativo realmente quiere diseñar específicamente el concepto de "hilos", ¿el sistema operativo gestionará este hilo en el futuro? Definitivamente es necesario, pero ¿cómo gestionarlo? Primero describa y luego organice. Se debe diseñar una estructura de datos especial para que los subprocesos representen objetos de subproceso. Así es como funciona nuestro Windows: diseñamos un objeto TCB que representa específicamente hilos.
Dado que el subproceso también debe ejecutarse y programarse, también debe tener su propia ID, estado, prioridad, contexto, pila...
Desde una perspectiva puramente de programación de subprocesos, ¡los subprocesos y los procesos se superponen en muchos lugares!
Por lo tanto, nuestros ingenieros de Linux no quieren diseñar estructuras de datos correspondientes específicamente para "hilos" de Linux. ¡En su lugar, reutilice la PCB directamente! Utilice PCB para representar el "hilo" de la memoria de Linux.

Después de leer este párrafo, solo quiero explicar una cosa: los subprocesos originalmente tienen bloques de control de subprocesos correspondientes, pero Linux no quiere diseñar específicamente esta estructura.

Con base en el conocimiento anterior, hablemos de algunos conceptos más.
En la actualidad, tenemos un consenso:
nuestro proceso se puede dividir en varios bloques de cierta manera. Si no lo comprende bien, puede comprenderlo de manera simple y aproximada. Hay muchas relaciones de mapeo en nuestra tabla de páginas. Atribuimos Se crea parte de las relaciones de mapeo con esta PCB recién creada y luego se asigna una parte de la relación de mapeo a la PCB creada, etc. Esta es una buena manera de dividir los recursos. Aunque todos usan la misma memoria virtual, esto se puede hacer.

A continuación, tenemos dos formas de crear un proceso: una es copiar toda la PCB, el espacio de direcciones virtuales y otras estructuras del núcleo del proceso principal. Otro método es crear directamente la PCB correspondiente para que apunte al espacio de direcciones virtuales del proceso principal de la fila, de modo que cada flujo de ejecución correspondiente solo pueda acceder a parte de la tabla de páginas. Visita áreas específicas.

Entonces solo creamos estos lotes internos de PCB, que llamamos subprocesos de Linux.
Insertar descripción de la imagen aquí

¿Quién es un hilo? Un hilo es un flujo de ejecución dentro de un proceso ¿Qué flujo de ejecución es? La lengua vernácula de esta oración es que los subprocesos se ejecutan dentro del proceso. Para ser más precisos, ¡los subprocesos se ejecutan dentro del espacio de direcciones del proceso! Posee una parte de los recursos del proceso.

Definitivamente hay muchas preguntas en este momento.
Volvamos primero a la siguiente pregunta.
1. ¿Qué es un proceso?
Solíamos decir que proceso = estructura de datos del núcleo + código y datos correspondientes al proceso. Esta afirmación es absolutamente correcta, pero ahora estamos reconstruyendo el concepto.
El llamado proceso es un montón de PCB, un espacio de direcciones, un montón de tablas de páginas y la parte correspondiente de la memoria física. A todo este proceso lo llamamos proceso. En otras palabras, hoy le damos una nueva perspectiva al concepto de proceso.
Perspectiva del kernel: la entidad básica responsable de asignar los recursos del sistema

¿La PCB, un espacio de direcciones virtuales, un montón de tablas de páginas y el código y los datos cargados en la memoria física que solicita cuando crea un proceso consumen recursos del sistema? ¿Recursos de OI? ¿Recursos utilizados por las llamadas a la CPU? Definitivamente existen estos. Todos estos recursos gastados se denominan colectivamente procesos,
por lo que un proceso es una entidad básica responsable de asignar los recursos del sistema.

Al crear un proceso hoy, tenemos que pensar en cómo la creación de una gran cantidad de estructuras de datos en el sistema consumirá recursos, y cargar el código y los datos correspondientes también consumirá parte de los recursos del sistema, así como CPU, IO. recursos, etc. Estos se denominan colectivamente procesos.

2. En Linux, ¿qué es un subproceso?
Subproceso: la unidad básica de programación de CPU.
En otras palabras, un hilo es la unidad básica común cuando el sistema operativo ejecuta el código y los datos correspondientes.

Mi mente está un poco confundida ahora. Antes pensaba que las llamadas a la CPU se programaban en unidades de procesos. Entendía los conceptos de conmutación, bloqueo, suspensión, etc. mencionados anteriormente en términos de procesos. ¿Es eso incorrecto?
de nada.

3. ¿Cómo vemos el proceso que aprendimos antes y cuál es el concepto de proceso correspondiente? ¿Entra en conflicto con lo que hablamos hoy?
Ahora pregunto qué es un proceso. Si responde que una PCB más otros datos del kernel + código y datos son procesos, esto está mal. Es sólo un pequeño recurso dentro del proceso.
El proceso actual que sabemos puede tener un montón de PCB, además de otras estructuras de datos del kernel + código y datos correspondientes a la memoria física. Según la perspectiva actual del núcleo, el proceso solía ser la entidad responsable de asignar los recursos del sistema. Es solo que internamente solo hay un flujo de ejecución. No entra en conflicto con lo que hablé hoy.
Hoy en día, el proceso sigue siendo la entidad responsable de asignar recursos. También necesita crear muchas estructuras de datos del kernel. Los datos y el código correspondientes aún deben cargarse. Es solo que antes solo se creaba un PCB, pero hoy Múltiples PCB, es decir, puede haber múltiples flujos de ejecución dentro de un proceso, incluida la situación en la que en el pasado solo había un flujo de ejecución.

Desde la perspectiva de la CPU,
historia vs hoy
1. Historia: proceso
2. Hoy: una rama dentro del proceso
Pero a la CPU no le importan estos y simplemente huye.
Task_struct que alimentamos a la CPU hoy <= ¡el significado de task_struct en la historia!
Es posible que tenga solo un flujo de ejecución en un proceso, como lo hemos hecho antes, o múltiples flujos de ejecución en un proceso, pero a la CPU no le importan y simplemente se ejecuta.
En Linux, la CPU parece que todo el task_struct que me dan es un proceso liviano (ya sea un proceso o un hilo)

Aquí está nuestra conclusión:
1. ¿Existen subprocesos reales en el kernel de Linux? No, Linux utiliza PCB de proceso para simular e implementar subprocesos, que es una solución completamente propia.

2. Desde la perspectiva de la CPU, cada PCB puede denominarse hilo ligero.

3. El subproceso de Linux es la unidad básica de programación de la CPU y el proceso es la unidad básica (entidad) responsable de asignar recursos.

4. El proceso se utiliza para solicitar recursos en su conjunto y el hilo se utiliza para comunicarse con el proceso y solicitar recursos. (Si los recursos asignados no son suficientes, el hilo también puede solicitar recursos al sistema operativo como proceso)

5. No existen subprocesos reales en Linux, ¿cuáles son los beneficios? ¿Cuales son las desventajas?
Ventajas: costo de mantenimiento simple y bajo (no necesita recrear la estructura de datos correspondiente para escribir el algoritmo y luego combinarlo con otras estructuras de datos, etc., solo necesita tomar un conjunto completo de algoritmos que se han escrito en la PCB, y todas las estructuras se pueden reutilizar. ¿Qué pasa con el bloqueo, el cambio de contexto, etc.? Todas las cosas del proceso anterior se hacen en el hilo) ----- ¡ confiable y eficiente!
Desventaja :
¿Por qué necesito crear hilos? El motivo es que en el futuro podemos enfrentarnos a un proceso que puede tener que realizar diferentes tareas en paralelo, como por ejemplo utilizar una APP para descargar una película y querer que se reproduzca al mismo tiempo. Estas son dos tareas. Las dos tareas solo se pueden ejecutar en serie en el proceso, por lo que si hay varios subprocesos, podemos dejar que un subproceso se descargue y el otro se reproduzca. Aunque la CPU ve dos flujos de ejecución, pero el cambio de alta frecuencia de la CPU te da la sensación de que estas dos tareas se están ejecutando al mismo tiempo. Estas dos tareas se implementan dentro de esta APLICACIÓN.

El proceso liviano que llamamos es solo una solución diseñada por Linux, el sistema operativo no lo reconoce en absoluto,
el sistema operativo solo reconoce subprocesos. La forma en que implementes este hilo no tiene nada que ver conmigo, de todos modos, solo conozco hilos.
Para nosotros los programadores, solo reconocemos hilos. Tanto en concepto como en funcionamiento, solo reconocemos hilos. No entiendo el proceso liviano del que estás hablando.
Como acabamos de decir, Linux no tiene subprocesos en el verdadero sentido, por lo que Linux no puede proporcionar directamente una interfaz de llamada al sistema. ¡Solo puede proporcionarnos una interfaz para crear procesos livianos!

Entonces, ¿cómo crear un hilo en Linux? Diga abajo.

2.1 Cómo entender el subproceso múltiple

Tomemos un ejemplo para comprender los subprocesos múltiples.
Nuestra sociedad utiliza a la familia como soporte básico para la asignación de recursos sociales. Pero dentro de una familia, todos hacen cosas diferentes. Tus padres ganan dinero, tus abuelos mantienen a los ancianos y tú y tus hermanos y hermanas van a la escuela. Aunque hacen cosas completamente diferentes, en realidad todos hacen lo mismo. Lo mismo. Haz una cosa y es mejorar la vida en casa. En otras palabras:
familia: proceso
miembro de la familia: hilo

2.2 Cómo probar

Se dijeron muchas cosas arriba. Por ejemplo, el hilo se ejecuta dentro del proceso, etc. ¿Cómo se prueban estas cosas?
Entonces escribamos algo de código y veamos al cerdo huir.

Primero cree un hilo, que es el hilo que nos proporciona nuestra biblioteca de hilos nativa.

pthread_create crea un nuevo hilo

Insertar descripción de la imagen aquí
pthread_t *hilo: ID del hilo

pthread_t *thread: atributo de hilo (normalmente establecido en nullptr)

void *(*start_routine) (void *): start_routine es un puntero de función, establece una función de devolución de llamada, cuando se crea el hilo, se le puede permitir al hilo realizar dicha tarea

void *arg: parámetros pasados ​​a la función de devolución de llamada

Insertar descripción de la imagen aquí
Devuelve 0 en caso de éxito y un código de error en caso de error.

Insertar descripción de la imagen aquí
En el futuro, el nuevo hilo que creamos se ejecutará para ejecutar el código anterior, mientras que el hilo principal continuará ejecutándose hacia abajo.
De hecho, todos estos códigos pertenecen al proceso, pero la función start_routine está asignada a este nuevo hilo.
El nuevo hilo que creamos ya no pertenece a la relación padre-hijo. El hilo recién creado es un hilo nuevo y el que va directamente hacia abajo es el hilo principal.

#include<iostream>
#include<pthread.h>
#include<cassert>
#include<unistd.h>

using namespace std;

//新线程
void* start_routine(void* args)
{
    
    
    while(true)
    {
    
    
        cout<<"我是新线程, 我正在运行!"<<endl;
        sleep(1);
    }
} 

int main()
{
    
    
    pthread_t tid;
    int n=pthread_create(&tid,nullptr,start_routine,(void*)"thread one");
    assert(n == 0);
    (void)n;

    //主线程
    while(true)
    {
    
    
        cout<<"我是主线程, 我正在运行!"<<endl;
        sleep(1);
    }

    return 0;
}

Insertar descripción de la imagen aquí
Este error se informó al ejecutar,

Eche otro vistazo a esta función.
Insertar descripción de la imagen aquí
Esta función no es una llamada al sistema que nos proporciona directamente el sistema operativo.
¿por qué? ¡Acabamos de decir que Linux no puede proporcionar directamente una interfaz de llamada al sistema para crear subprocesos! ¡Solo puede proporcionarnos una interfaz para crear procesos livianos!

Si hay una interfaz de llamada al sistema, simplemente empaquete el archivo de encabezado y llámelo directamente. Dije antes que si usa la interfaz proporcionada por la biblioteca, no se puede pasar directamente durante la compilación, entonces, ¿cuál es la razón por la que no se pasa?
Debes decirnos dónde está tu biblioteca.

La biblioteca que usamos para crear hilos es la biblioteca pthread.
Insertar descripción de la imagen aquí

mythread:mythread.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f mythread

Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí
Esta es la biblioteca dinámica y la biblioteca estática de pthread que utilizamos.

A esta biblioteca la llamamos biblioteca de subprocesos nativa .

Ahora responda las preguntas restantes anteriores.
Insertar descripción de la imagen aquí
Cualquier sistema operativo Linux debe llevar esta biblioteca (biblioteca de subprocesos nativa).
Insertar descripción de la imagen aquí
Esto también muestra que no hay un solo flujo de ejecución dentro de este proceso. Si es un solo flujo de ejecución, no puede salir del ciclo while en absoluto y ahora ambos se están ejecutando, lo que significa que debe haber dos flujos de ejecución.

¿Cómo comprobarlo? ¿Sigue siendo como antes?

 ps ajx | head -1 && ps ajx | grep mythread

Insertar descripción de la imagen aquí
Lo siento, acabo de ver el proceso.
Entonces, ¿qué debo hacer si quiero ver dos flujos de ejecución?

ps -aL  //查看轻量级进程

Insertar descripción de la imagen aquí
Aquí vemos dos flujos de ejecución.

Entonces, ¿qué significa todo esto?
Insertar descripción de la imagen aquí
Decimos que los subprocesos son la unidad básica de llamadas cuando se programa la CPU, la premisa es que los subprocesos deben tener sus propios identificadores para garantizar su unicidad.
Insertar descripción de la imagen aquí
Y también noté que el PID y el LWP del primer flujo de ejecución son los mismos, que es el hilo principal legendario. A continuación se muestra el nuevo hilo correspondiente.
Insertar descripción de la imagen aquí
Cuando se programa la CPU, ¿qué ID se utiliza como identificador para representar un flujo de ejecución específico?
De hecho, no es PID, sino LWP.

Por lo tanto, el sistema operativo utiliza directamente LWP internamente para distinguir procesos. Todos los conceptos que hemos aprendido antes se pueden transferir, como prioridad de estado, contexto, cambio de proceso, etc. Lo que miramos es LWP.
Cuando antes lo entendíamos pero PID, hoy hablamos de LWP ¿Será el mismo error de antes?

Comenté el código del hilo, lo volví a compilar y lo ejecuté.

int main()
{
    
    
    // pthread_t tid;
    // int n=pthread_create(&tid,nullptr,start_routine,(void*)"thread one");
    // assert(n == 0);
    // (void)n;

    //主线程
    while(true)
    {
    
    
        cout<<"我是主线程, 我正在运行!"<<endl;
        sleep(1);
    }

    return 0;
}

Actualmente, solo hay un proceso en ejecución
Insertar descripción de la imagen aquí
y solo hay un LWP
Insertar descripción de la imagen aquí
. Y si observa con atención, encontrará que cuando solo tiene un proceso, su LWP y PID son iguales. Entonces, cuando solo tiene un flujo de ejecución internamente, usar PID o LWP es equivalente.

Proceso: El proceso anterior solo tenía un flujo de ejecución dentro
Hilo: Hay múltiples flujos de ejecución dentro de un proceso.

Cuando hablé de pthread_create hace un momento,
Insertar descripción de la imagen aquídije que void* arg es el parámetro de la función de devolución de llamada.

//新线程
void* start_routine(void* args)
{
    
    
    const char* name=(const char*)args;
    while(true)
    {
    
    
        cout<<"我是新线程, 我正在运行! name: "<<name<<endl;
        sleep(1);
    }
} 

int main()
{
    
    
    pthread_t tid;
    int n=pthread_create(&tid,nullptr,start_routine,(void*)"thread one");
    assert(n == 0);
    (void)n;

    //主线程
    while(true)
    {
    
    
        cout<<"我是主线程, 我正在运行!"<<endl;
        sleep(1);
    }

    return 0;
}

Insertar descripción de la imagen aquí
pthread_t *thread: El ID del hilo es un parámetro de salida. Ahora quiero ver este ID.
Insertar descripción de la imagen aquí
Este ID del hilo es un entero largo sin signo.
Insertar descripción de la imagen aquí

int main()
{
    
    
    pthread_t tid;
    int n=pthread_create(&tid,nullptr,start_routine,(void*)"thread one");
    assert(n == 0);
    (void)n;

    //主线程
    while(true)
    {
    
    
        cout<<"我是主线程, 我正在运行!,我创建出来的线程的tid: "<<tid<<endl;
        sleep(1);
    }

    return 0;
}

Entonces, ¿este es LWP impreso?
Insertar descripción de la imagen aquí
Lamentablemente no es lo mismo.

Este número es demasiado grande, imprímelo en 16 mecanismos y echa un vistazo.

int main()
{
    
    
    pthread_t tid;
    int n=pthread_create(&tid,nullptr,start_routine,(void*)"thread one");
    assert(n == 0);
    (void)n;

    //主线程
    while(true)
    {
    
    
        char tidbuffer[64];
        snprintf(tidbuffer,sizeof(tidbuffer),"0x%x",tid);
        cout<<"我是主线程, 我正在运行!,我创建出来的线程的tid: "<<tidbuffer<<endl;
        sleep(1);
    }

    return 0;
}

Insertar descripción de la imagen aquí
En base 10, los números son muy grandes, pero en hexadecimal, los números no son tan grandes.
¿Entonces qué es esto?
En realidad es una dirección . No hay forma de explicarlo aquí, lo explicaremos más adelante.

Hemos estado hablando de código hace un momento, pero ¿qué pasa con otras cosas, como datos inicializados, datos no inicializados y montones?

Una vez que se crea un hilo, casi todos los recursos son compartidos por todos los hilos.

string func()
{
    
    
    return "我是一个独立的方法";
}

//新线程
void* start_routine(void* args)
{
    
    
    const char* name=(const char*)args;
    while(true)
    {
    
    
        cout<<"我是新线程, 我正在运行! name: "<<name<<" : "<<func()<<endl;
        sleep(1);
    }
} 

int main()
{
    
    
    pthread_t tid;
    int n=pthread_create(&tid,nullptr,start_routine,(void*)"thread one");
    assert(n == 0);
    (void)n;

    //主线程
    while(true)
    {
    
    
        char tidbuffer[64];
        snprintf(tidbuffer,sizeof(tidbuffer),"0x%x",tid);
        cout<<"我是主线程, 我正在运行!,我创建出来的线程的tid: "<<tidbuffer<<" : "<<func()<<endl;
        sleep(1);
    }

    return 0;
}

Las funciones se pueden compartir
Insertar descripción de la imagen aquí

string func()
{
    
    
    return "我是一个独立的方法";
}

int g_val=0;

//新线程
void* start_routine(void* args)
{
    
    
    const char* name=(const char*)args;
    while(true)
    {
    
    
        cout << "我是新线程, 我正在运行! name: " << name << " : "<< func()\
        << " : " << g_val++ << " &g_val : " << &g_val << endl;
        sleep(1);
    }
} 

int main()
{
    
    
    pthread_t tid;
    int n=pthread_create(&tid,nullptr,start_routine,(void*)"thread one");
    assert(n == 0);
    (void)n;

    //主线程
    while(true)
    {
    
    
        char tidbuffer[64];
        snprintf(tidbuffer,sizeof(tidbuffer),"0x%x",tid);
        cout << "我是主线程, 我正在运行!, 我创建出来的线程的tid: " << tidbuffer \
        << " : " << g_val << " &g_val : " << &g_val << endl;
        sleep(1);
    }

    return 0;
}

Insertar descripción de la imagen aquí

En primer lugar, tienen la misma dirección, lo cual no es problema porque usan el mismo espacio de direcciones y todos los valores deben ser iguales.
Ahora el nuevo hilo ha cambiado el valor de g_val, ¿lo ha visto el hilo principal?
Insertar descripción de la imagen aquí
Lo ví. Lo vi tan pronto como lo cambiaste. ¿Qué significa esto? Este recurso global es compartido por dos hilos.

Los dos ejemplos que acabamos de citar ilustran una vez más que
una vez que se crea un hilo, casi todos los recursos son compartidos por todos los hilos.

Intercambiar datos entre hilos no es fácil, ahora sabes que es muy fácil, es mucho más fácil que comunicarse entre procesos.

¿Es privado para ti? Hay.

Los subprocesos también deben tener sus propias propiedades internas privadas. ¿Qué recursos deberían ser privados de subprocesos?

1. Los atributos de PCB son privados (id, estado, prioridad, etc.)
2. Debe haber una determinada estructura de contexto privada (es posible que el código no termine de ejecutarse cuando es necesario cambiar el hilo. Para cambiar, se debe guardar el contexto. Hilos dentro de un proceso es necesario guardar el contexto. Guardar, ¿tiene que tener su propia estructura de contexto?)

Acabo de escribir un código. El hilo principal está en la función principal y el nuevo hilo está en su propia función. Cada hilo tiene su propia función. ¿Formará variables temporales cuando se esté ejecutando? Es decir, variables locales ¿Dónde se almacenan las variables temporales que hemos aprendido antes? área de pila.
3. Cada hilo debe tener su propia estructura de pila independiente.

Si alguien le pregunta hoy qué recursos de hilo se comparten, de hecho, la mayoría de los recursos en el espacio de direcciones se comparten, pero si le pregunta qué es privado, 2 y 3 son en realidad los más importantes. Por supuesto, hay otras cosas. que son privados. de. Pero no es tan importante como ninguno de ellos.
El contexto privado significa que el subproceso se ejecuta dinámicamente y tiene una estructura de pila independiente, lo que también significa que el subproceso se ejecuta dinámicamente.

¿Todavía tienes preguntas ahora? Debe haber
1, 2 y 3 estructuras de pila independientes. Desde nuestro espacio de direcciones actual, podemos ver que solo hay un área de pila. ¿Cómo guardar que cada hilo tenga su propia estructura de pila privada?
Insertar descripción de la imagen aquí
Esta cuestión también se deja para discusión a continuación.

Agreguemos algunos conocimientos sobre hilos.

2.3 ¿Qué es un hilo?

  • Una ruta de ejecución en un programa se llama hilo. Una definición más precisa es: un hilo es "una secuencia de control dentro de un proceso"
  • Todos los procesos tienen al menos un hilo de ejecución.
  • Los subprocesos se ejecutan dentro del proceso, esencialmente ejecutándose dentro del espacio de direcciones del proceso.
  • En el sistema Linux, a los ojos de la CPU, la PCB que se ve es más liviana que el proceso tradicional.
  • A través del espacio de direcciones virtuales del proceso, se puede ver la mayoría de los recursos del proceso. Al asignar racionalmente los recursos del proceso a cada flujo de ejecución, se forma un flujo de ejecución de subprocesos
    :Insertar descripción de la imagen aquí

2.4 Ventajas de los hilos

  • Crear un nuevo hilo es mucho menos costoso que crear un nuevo proceso:
    crear un hilo solo requiere crear una PCB, mientras que crear un proceso requiere muchas estructuras de datos del kernel.
  • En comparación con el cambio entre procesos, el cambio entre subprocesos requiere que el sistema operativo haga mucho menos trabajo
    1. Proceso: cambiar PCB y cambiar espacio de direcciones virtuales y cambiar tabla de páginas y cambiar de contexto
    2. Hilo: cambiar PCB y cambiar de contexto

Pero también he dicho que la tabla de páginas y el espacio de direcciones virtuales son solo punteros. Por ejemplo, la tabla de páginas es un valor en el registro de la CPU y el espacio de direcciones virtuales es la dirección en la PCB. Siempre que la PCB correspondiente sea corte, el espacio de direcciones virtuales también se cortará. Estos costos no parecen ser altos.

Entonces, ¿cómo podemos decir que el cambio de subprocesos requiere que el sistema operativo haga mucho menos que el cambio de procesos?

Insertar descripción de la imagen aquí

Además de varios registros, la CPU también tiene algo muy importante llamado caché a nivel de hardware, que es más lento que los registros y mucho más rápido que la memoria. El caché es equivalente al caché a nivel de hardware dentro de la CPU, que es el caché legendario. . Este hardware también tiene la función de guardar datos como la memoria.

El software tiene una propiedad llamada principio de localidad, lo que significa que el código cercano al código o a los datos a los que se accede actualmente tiene una mayor probabilidad de ser accedido.

El código y los datos a los que accede el proceso actual se colocan en el caché por adelantado o en su totalidad.
Insertar descripción de la imagen aquí
Entonces, la CPU no necesita acceder a la memoria durante la lectura, sino que accede a ella en el caché. Si el caché no llega, leerá de la memoria. Después de leer, se almacenará en el caché y luego se leerá desde el caché. .

Un proceso en ejecución ha almacenado en caché una gran cantidad de datos de puntos de acceso en su caché interna. Cuando se cambian varios subprocesos, estos datos de puntos de acceso son originalmente compartidos por estos subprocesos, por lo que no es necesario cambiar el caché al cambiar de subprocesos. Sin embargo, el proceso en caché almacena en caché muchos datos del punto de acceso. Los datos almacenados en caché dejan de ser válidos inmediatamente cuando se cambia el proceso, y el nuevo proceso debe volver a almacenarse en caché, y la próxima vez que cambie al proceso, deberá volver a almacenarlo en caché.

  1. No es necesario actualizar demasiado el caché al cambiar de hilo, pero al cambiar de proceso, todo se actualiza.
  • Los hilos ocupan muchos menos recursos que los procesos.
  • Capacidad para utilizar completamente la cantidad de procesadores paralelos disponibles
  • Mientras espera que se complete la lenta operación de E/S, el programa puede realizar otras tareas informáticas.
  • Para aplicaciones computacionalmente intensivas, para ejecutarse en un sistema multiprocesador, los cálculos se dividen en múltiples subprocesos.
  • En aplicaciones con uso intensivo de E/S, para mejorar el rendimiento, las operaciones de E/S se superponen. Los subprocesos pueden esperar diferentes operaciones de E/S al mismo tiempo.

Aplicaciones informáticas intensivas: principalmente los recursos utilizados por subprocesos o procesos son recursos de la CPU, como cifrado, descifrado, algoritmos autoescritos, etc.
Aplicaciones intensivas de E/S: principalmente los recursos utilizados por subprocesos o procesos son recursos periféricos, como como acceder a discos, monitor, red, etc.

2.4 Desventajas de los hilos

  • pérdida de rendimiento
    • Un subproceso computacionalmente intensivo que rara vez es bloqueado por eventos externos a menudo no puede compartir el mismo procesador con otros subprocesos. Si la cantidad de subprocesos de procesamiento intensivo excede los procesadores disponibles, puede haber una gran pérdida de rendimiento, donde la pérdida de rendimiento se refiere a la adición de sincronización adicional y sobrecarga de programación, mientras que los recursos disponibles permanecen sin cambios.

Es decir, cuanto más subprocesos múltiples requieran un uso intensivo de la computación, mejor. Por ejemplo, si tiene un sistema de doble o cuádruple núcleo, generalmente la cantidad de subprocesos que cree debe ser la misma que la cantidad de núcleos que tiene, y la cantidad de procesos que cree debe ser la misma que la cantidad de CPU que tenga. El número es el mismo si se crean demasiados. Por ejemplo, si tiene una sola CPU y un solo núcleo, entonces ha creado tres, cuatro o cinco subprocesos en este momento. Lo siento, además del costo del cálculo del subproceso, también está el costo del cambio de subproceso. Pero si solo tienes un hilo, no hay costo de cambio. Más hilos no siempre son mejores.

  • Robustez reducida
    • Escribir subprocesos múltiples requiere una consideración más amplia y profunda. En un programa de subprocesos múltiples, la posibilidad de efectos adversos debido a ligeras desviaciones en la asignación de tiempo o al compartir variables que no deben compartirse es muy alta. En otras palabras, los subprocesos Hay una falta de protección entre ellos.

Cuando hablábamos de procesos en el pasado, ¿la muerte de un proceso afectaría a otro proceso? Generalmente no. Pero en el caso de los subprocesos, siempre que ocurra un problema en un subproceso, puede afectar a otros subprocesos. Todos los códigos escritos por subprocesos múltiples suelen ser menos robustos.

  • falta de control de acceso
    • El proceso es la granularidad básica del control de acceso. Llamar a ciertas funciones del sistema operativo en un hilo afectará todo el proceso.

El código que acabamos de escribir define una variable global, un hilo ++ esta variable global y otro hilo imprime. Podemos ver que un hilo modifica la variable global y la otra variable global se puede ver inmediatamente. Si bien esto reduce nuestros costos de comunicación, puede afectar a un hilo simplemente porque otro hilo accede a él. Esto se llama falta de control de acceso.

  • La programación se vuelve más difícil
    • Escribir y depurar un programa de subprocesos múltiples es mucho más difícil que un programa de un solo subproceso

A continuación, escriba un fragmento de código para verificar la solidez del subproceso múltiple.

Si ocurre una excepción en un hilo, ¿afectará a otros hilos? ¿Por qué?

#include<iostream>
#include<pthread.h>
#include<string>
#include<unistd.h>

using namespace std;

void* start_rountine(void* args)
{
    
    
    //安全的进行强制类型转化
    string name=static_cast<const char*>(args);
    while(true)
    {
    
    
        cout<<"new thread create success, name: "<<name<<endl;
        sleep(1);
        int* p=nullptr;
        *p=0;
    }

}

int main()
{
    
    
    pthread_t id;
    int n=pthread_create(&id,nullptr,start_rountine,(void*)"thread new");

    while(true)
    {
    
    
        cout<<"new thread create success, name: main thread"<<endl;
        sleep(1);
    }

    return 0;
}

Insertar descripción de la imagen aquí
Sabemos por los resultados de ejecución que sí, esto significa robustez o robustez deficiente.
¿Entonces por qué?
Desde la perspectiva de las señales, ¿por qué se denominan señales de proceso? ¡Porque la señal se envía al proceso en su conjunto!
Los pids de todos los subprocesos son iguales, por lo que el sistema operativo escribe la señal número 11 en todos los pids con el mismo pid y se sale de la acción predeterminada.
Desde otra perspectiva, este proceso crea un nuevo hilo. Si se produce una excepción en el nuevo hilo, ¿hay algún problema con usted? Un hilo es un flujo de ejecución dentro del proceso y es parte del proceso. Si se produce una excepción ocurre en el hilo, es él mismo. Se produjo una excepción en el proceso.

2.5 Excepción de hilo

  • Si la división por cero ocurre en un solo subproceso, el problema del puntero salvaje hará que el subproceso falle y el proceso también fallará.
  • El subproceso es la rama de ejecución del proceso. Una excepción en el subproceso es similar a una excepción en el proceso, que activa el mecanismo de señal y finaliza el proceso. Cuando el proceso finaliza, todos los subprocesos del proceso saldrán inmediatamente.

Si hay múltiples procesos, si ocurre un problema en un proceso, ¿afectará a otros procesos?
No, el proceso hijo falla y el proceso padre está más feliz que nadie. Finalmente puede reciclarse y ejecutarse.
Los procesos son independientes porque utilizan estructuras de datos de núcleo independientes, códigos y datos independientes.

2.6 Proceso vs Hilo

  • El proceso es la unidad básica de asignación de recursos.
  • El hilo es la unidad básica de programación.
  • Los hilos comparten datos de proceso, pero también poseen parte de sus propios datos.
    • ID de subproceso,
      un conjunto de registros, palabra de máscara de señal de error
      de pila , prioridad de programación


Varios subprocesos del proceso comparten el mismo espacio de direcciones, por lo que el segmento de texto y el segmento de datos se comparten. Si se define una función, se puede llamar en cada subproceso. Si se define una variable global, se puede acceder a ella en cada subproceso, excepto Además, cada subproceso también comparte los siguientes recursos y entorno de proceso:

  • tabla de descriptores de archivos
  • Cada método de procesamiento de señal (SIG_ IGN, SIG_ DFL o función de procesamiento de señal personalizada)
  • directorio de trabajo actual
  • ID de usuario e ID de grupo

La relación entre procesos y subprocesos es la siguiente:

Insertar descripción de la imagen aquí
Históricamente, el C/C++ que escribimos eran procesos de un solo subproceso.
El código que acabamos de escribir es un proceso único con múltiples subprocesos.
El proceso padre-hijo son múltiples procesos de un solo subproceso.
En el futuro, podemos crear múltiples procesos primero y crear múltiples subprocesos en cada proceso, que son múltiples procesos de subprocesos múltiples. .

Todavía queda un problema arriba:
Linux no puede proporcionar directamente una interfaz de llamada al sistema para crear subprocesos, solo puede proporcionarnos una interfaz para crear procesos livianos ¿Qué interfaz proporciona el sistema operativo?
Insertar descripción de la imagen aquí

La clonación se ejecuta para crear un proceso y también permite la creación de un proceso liviano.

Insertar descripción de la imagen aquí
int (*fn)(void *): el código que ejecutará el nuevo flujo de ejecución
void *child_stack: representa la subpila

También aprendí una bifurcación para crear un proceso hijo antes. De hecho, también hay una función

vfork crea un proceso hijo

Insertar descripción de la imagen aquí
Es solo que el proceso hijo creado por esta función comparte un espacio de direcciones con el proceso padre. Si crea una variable global, tanto el proceso padre como el hijo pueden verla, y si la modifica, la otra parte puede verla.

Es solo que no usamos estas dos funciones,
pero el sistema operativo nos proporciona una interfaz para crear procesos livianos ( clon )
, de hecho, el fork y el vfork que llamamos usan este clon.

Por el momento hemos terminado de explicar el concepto de subprocesos, el próximo artículo se centrará en el control de subprocesos. Y complete los huecos que no se llenaron en este artículo (cada hilo tiene una estructura de pila independiente, ¿dónde está esta pila?).

Supongo que te gusta

Origin blog.csdn.net/fight_p/article/details/135150312
Recomendado
Clasificación