Ir análisis de escenarios de código fuente del programador de idiomas diez: almacenamiento local de hilos

El siguiente contenido se reproduce de  https://mp.weixin.qq.com/s/-tiXJpH0IrJw-RH4x5SRdQ

Awa me encanta escribir programas originales Zhang  source Travels  2019-04-27

Este artículo es la décima sección del conocimiento preliminar del primer capítulo de la serie "Análisis de escenarios de código fuente de Go Scheduler", y también es la última sección del conocimiento preliminar.

El almacenamiento local de subprocesos también se denomina almacenamiento local de subprocesos. Su nombre en inglés es Almacenamiento local de subprocesos, o TLS para abreviar . Parece ser algo muy importante, pero en realidad es una variable global que es privada para el subproceso.

Los lectores que tienen programación de subprocesos múltiples deben saber que las variables globales ordinarias se comparten en varios subprocesos. Un subproceso lo modifica y todos los subprocesos pueden ver la modificación. Sin embargo, las variables globales privadas de subprocesos son diferentes de las variables globales ordinarias., Thread private global Las variables son propiedad privada del hilo. Cada hilo tiene su propia copia. Las modificaciones realizadas por un hilo solo se modificarán en su propia copia y no en las copias de otros hilos.

Usemos un ejemplo para ilustrar la diferencia entre las variables globales compartidas de subprocesos múltiples y las variables globales privadas de subprocesos, y hagamos un análisis simple del almacenamiento local de subprocesos de gcc.

Primer vistazo a las variables globales ordinarias

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

int g = 0; // 1, define la variable global gy asigna el valor inicial 0 

void * start (void * arg) 
{ 
printf ("start, g [% p ]:% d \ n ", & g, g); // 4, imprime la dirección y el valor de la variable global g en el hilo hijo 

g ++; // 5, modifica la variable global 

return NULL; 
} 

int main (int argc , char * argv []) 
{ 
pthread_t tid; 

g = 100; // 2. El hilo principal asigna un valor de 100 a la variable global g 

pthread_create (& tid, NULL, start, NULL); // 3. Crea un hijo subproceso y ejecute la función start () 
pthread_join (tid, NULL); // 6, espere el final del subproceso secundario ejecutar 

printf ("main, g [% p]:% d \ n", & g, g); // 7, imprime la dirección y el valor de la variable global g 

return 0; 
}

Para explicar brevemente, este programa define una variable global g en la nota 1 y establece su valor inicial en 0. Después de que se ejecuta el programa, el hilo principal primero cambia g a 100 (nota 2), y luego crea un subproceso para ejecutar start () función (nota 3), la función start () primero imprime el valor de g (nota 4) asegúrese de que el hilo principal pueda ver la modificación de g en el hilo hijo, y luego modifique el valor de g (nota 5) después de que el hilo termine de correr Después de que el hilo principal espera el final del sub-hilo en la nota 6, imprima el valor de g en la nota 7 para confirmar que la modificación de g por el sub-hilo también puede afectar la lectura de g por el hilo principal.

Compilar y ejecutar el programa:

bobo @ ubuntu: ~ / study / c $ gcc thread.c -o thread -lpthread 
bobo @ ubuntu: ~ / study / c $ ./thread 
start, g [0x601064]: 100 
main, g [0x601064]: 101

Se puede ver en el resultado de salida que la dirección de la variable global g en los dos subprocesos es la misma, cualquier subproceso puede leer la modificación de la variable global g por el otro subproceso, que realiza múltiples subprocesos de la variable global g Compartir en.

Después de comprender las variables globales comunes, veamos las variables globales privadas de subprocesos implementadas por el almacenamiento local de subprocesos (TLS). Este programa es casi el mismo que el programa anterior, la única diferencia es que la palabra clave __thread se agrega cuando se define la variable global g, de modo que g se convierte en una variable global privada de hilo.

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

__thread int g = 0; // 1. Aquí se agrega la palabra clave __thread para definir g como una variable global privada. Cada hilo tiene una variable g 

void * start (void * arg) 
{ 
printf ("start, g [% p]:% d \ n", & g, g); // 4, imprime la dirección y el valor de la variable global privada g de este hilo 

g ++; // 5, Modify el valor de la variable global privada g de este hilo 

devuelve NULL; 
} 

int main (int argc, char * argv []) 
{ 
pthread_t tid; 

g = 100; // 2, el hilo principal asigna el valor de 100 al privado variable global 

pthread_create (& tid, NULL, start, NULL); // 3, crea un hilo hijo y ejecuta la función start () 
pthread_join (tid, NULL); // 6, espera el final del hilo hijo 

printf (" main, g [% p]:% d \ n ", & g, g); // 7, imprime la dirección y el valor de la variable global privada g del hilo principal 

return 0; 
}

Ejecute el programa para ver el efecto:

bobo @ ubuntu: ~ / study / c $ gcc -g thread.c -o thread -lpthread 
bobo @ ubuntu: ~ / study / c $ ./thread 
start, g [0x7f0181b046fc]: 0 
main, g [0x7f01823076fc]: 100

Se puede ver en los resultados de salida: primero, la dirección de la variable global g en los dos subprocesos no es la misma; en segundo lugar, el valor asignado por la función principal a la variable global g no afecta el valor de g en el subproceso hijo y pares de subprocesos secundarios g Se han realizado todos los cambios y el valor de g en el subproceso principal no se ve afectado. Este resultado es exactamente lo que esperamos. Esto muestra que cada subproceso tiene su propia variable global privada g.

Esto parece sorprendente, es obvio que ambos hilos usan el mismo nombre de variable global para acceder a las variables, pero es como si estuvieran accediendo a diferentes variables.

Analicemos qué utiliza la magia negra gcc para lograr esta función. ¿Cómo comenzamos a investigar características como esta implementadas por el compilador? La forma más rápida y directa es usar herramientas de depuración para depurar el funcionamiento del programa, aquí usamos gdb para depurar.

bobo @ ubuntu: ~ / study / c $ gdb ./thread

Primero, coloque un punto de interrupción en la línea 20 del código fuente (correspondiente ag = 100 en el código fuente) y luego ejecute el programa. El programa se detiene en el punto de interrupción. Desmonte la función principal:

(gdb) b thread.c: 20 
Breakpoint 1 en 0x400793: file thread.c, línea 20. 
(gdb) r 
Programa de inicio: / home / bobo / study / c / thread 

Breakpoint 1, en thread.c: 20 
20g = 100; 
(gdb) Disass 
Volcado de código ensamblador para la función main: 
  0x0000000000400775 <+0>: push% rbp 
  0x0000000000400776 <+1>: mov% rsp,% rbp 
  0x0000000000400779 <+4>: sub $ 0x20,% rsp 
  0x000000000040077d <+8> : mov% edi, -0x14 (% rbp) 
  0x0000000000400780 <+11>: mov% rsi, -0x20 (% rbp) 
  0x0000000000400784 <+15>: mov% fs: 0x28,% rax 
  0x000000000040078d <+24>: mov% rax , -0x8 (% rbp) 
  0x0000000000400791 <+28>: xor% eax,% eax
=> 0x0000000000400793 <+30>: movl $ 0x64,% fs: 0xfffffffffffffffc 
  0x000000000040079f <+42>: lea -0x10 (% rbp),% rax 
  0x00000000004007a3 <+46>: mov $ 0x0,% ecx 
  0x0000004007a8 < mov $ 0x400736,% edx 
  0x00000000004007ad <+56>: mov $ 0x0,% esi 
  0x00000000004007b2 <+61>: mov% rax,% rdi 
  0x00000000004007b5 <+64>: callq 0x4005e0 <pthread_create @ plt> 
  0x0000004007ba mov <0000004007ba -0x10 (% rbp),% rax 
  0x00000000004007be <+73>: mov $ 0x0,% esi 
  0x00000000004007c3 <+78>: mov% rax,% rdi 
  0x00000000004007c6 <+81>: callq 0x400620 <pthread_join @ plt> 0x0000 
  <00004007c >: mov% fs: 0xfffffffffffffffc,% eax 
  0x00000000004007d3 <+94>: mov% eax,% esi 
  <0000 +96 mov $ 0x4008df,% edi 
  0x00000000004007da <+101>:mov $ 0x0,% eax
  0x00000000004007df <+106>: callq 0x400600 <printf @ plt> 
  ......

El programa se detiene en la línea g = 100, mire las instrucciones de montaje,

=> 0x0000000000400793 <+30>: movl $ 0x64,% fs: 0xfffffffffffffffc

Esta instrucción de ensamblaje significa copiar la constante 100 (0x64) a la memoria en la dirección% fs: 0xfffffffffffffffc, se puede ver que la dirección de la variable global g es% fs: 0xfffffffffffffffffc, fs es el registro de segmento y 0xfffffffffffffffc es el número con signo- 4. Entonces la dirección de la variable global g es:

dirección base del segmento fs-4

Anteriormente, cuando hablamos de registros de segmento, dijimos que la dirección base del segmento es la dirección inicial del segmento. Para verificar que la dirección de g es de hecho la dirección base del segmento fs-4, necesitamos saber cuál es el La dirección base del segmento fs es, aunque podemos usar el comando gdb para ver el valor del registro fs, pero el selector de segmento se almacena en el registro fs en lugar de la dirección inicial del segmento. Para obtener la dirección base , necesitamos agregar un pequeño código para obtenerlo. El código modificado es el siguiente:

#include <stdio.h> 
#include <unistd.h> 
#include <pthread.h> 
#include <asm / prctl.h> 
#include <sys / prctl.h> 

__thread int g = 0; 

void print_fs_base () 
{dirección 
       larga sin firmar; 
       int ret = arch_prctl (ARCH_GET_FS, & addr); // 获取 fs 段 基 地址
       if (ret <0) { 
               perror ("error"); 
               regreso; 
      } 

       printf ("fs dirección base:% p \ n", dirección (void *)); // 打印 fs 段 基 址

       return; 
} 

vacío * inicio (vacío * arg) 
{ 
   print_fs_base (); // 子 线程 打印 fs 段 基 地址
printf ("inicio, g [% p]:% d \ n", & g, g); 

g ++; 

return NULL;

int main (int argc, char * argv []) 
{ 
pthread_t tid; 

g = 100; 

pthread_create (& tid, NULL, start, NULL); 
pthread_join (tid, NULL); 

   print_fs_base (); // principal 线程 打印 fs 段 基 址
printf ("principal, g [% p]:% d \ n", & g, g); 

return 0; 
}

En el código, tanto el subproceso principal como el subproceso llaman a la función print_fs_base () para imprimir la dirección base del segmento fs. Ejecute el programa para ver:

fs base addr: 0x7f36757c8700 
start, g [0x7f36757c86fc]: 0 
fs base addr: 0x7f3675fcb700 
main, g [0x7f3675fcb6fc]: 100

puede ser visto:

  • La dirección base del segmento fs del subproceso es 0x7f36757c8700, y la dirección de g es 0x7f36757c86fc, que resulta ser la dirección base -4

  • La dirección base del segmento fs del hilo principal es 0x7f3675fcb700, y la dirección de g es 0x7f3675fcb6fc, que también es la dirección base -4

Se puede concluir que el compilador gcc (de hecho, la biblioteca de subprocesos y el soporte del kernel) usa el registro del segmento fs de la CPU para implementar el almacenamiento local del subproceso . La dirección base del segmento fs en diferentes subprocesos es diferente, por lo que parece ser la misma Una variable global tiene diferentes direcciones de memoria en diferentes subprocesos, realizando variables globales privadas de subprocesos.

Aquí analizamos brevemente la implementación del almacenamiento local de subprocesos por gcc bajo la plataforma Linux AMD64. En los siguientes capítulos, también veremos cómo el tiempo de ejecución de go usa el almacenamiento local de subprocesos para asociar goroutines en ejecución con subprocesos de trabajo.

En este punto, se ha introducido el contenido principal de la primera parte de los conocimientos preliminares. Comenzamos con instrucciones de ensamblaje y discutimos registros, memoria, pila, proceso de llamada de función, programación del kernel del sistema operativo de subprocesos y almacenamiento local de subprocesos, etc. Creemos que los lectores tienen una buena comprensión de estos conocimientos básicos, luego vamos a resolver el misterio del programador goroutine juntos!


Finalmente, si crees que este artículo te es útil, por favor ayúdame a hacer clic en "Mirar" en la esquina inferior derecha del artículo o reenviarlo al círculo de amigos, ¡muchas gracias!

imagen

Supongo que te gusta

Origin blog.csdn.net/pyf09/article/details/115238634
Recomendado
Clasificación