Tabla de contenido
Inicio rápido del proyecto BCC
Instrumentación dinámica: kprobes y uprobes
Instrumentación estática: tracepoint y USDT
Introducción a bpftrace: seguimiento de openat
¿Por qué bpf_probe_read deshabilita las interrupciones de fallas de página?
Cómo bpf_probe_read prohíbe la interrupción por falla de página
Comandos de llamada al sistema BPF
Usando strace para analizar execsnoop
seguimiento de la pila de llamadas
Seguimiento de la pila de llamadas basado en el puntero del marco
Hacer depuración basada en información de depuración
punto de rastreo punto de rastreo
Contadores de supervisión del rendimiento
introducción
¿Qué es BPF?
historia
Es la abreviatura de Berkeley Packet Filter, que nació en 1992 para mejorar el rendimiento de las herramientas de filtrado de paquetes de red.
Entró en la línea principal del kernel de Linux en 2014.
En cuanto a la forma en que se usa, se parece más a JavaScript.
composición
Conjunto de instrucciones, objetos de almacenamiento, funciones auxiliares y otras partes.
mecanismo de ejecución
En general, existen dos mecanismos de ejecución:
- intérprete
- Un compilador JIT justo a tiempo que convierte dinámicamente las instrucciones BPF en instrucciones localizadas
Antes de la ejecución, debe pasar la verificación de seguridad del validador, lo que puede garantizar que el programa BPF en sí no se bloquee.
Relación entre BPF y ebpf
La ebpf actual sigue denominándose BPF para ser coherente con la anterior.
BCC, bpftrace, Visor de E/S
Es engorroso escribir programas BPF directamente a través de instrucciones BPF, por lo que existe un lenguaje de alto nivel para admitirlo.
BCC (BPF Compiler Collection, BPF) se utilizó por primera vez para desarrollar programas de rastreo BPF, proporcionó un entorno de lenguaje C y también proporcionó entornos lua y python para implementar interfaces del lado del usuario. Fue el predecesor de las bibliotecas libbcc y libbpf. La biblioteca proporciona funciones de biblioteca para observar eventos con programas BPF.
La biblioteca de funciones de BCC proporciona más de 70 herramientas.
bpftrace es un front-end emergente que brinda soporte de lenguaje de alto nivel específicamente para crear herramientas BPF. bpftrace también se basa en las bibliotecas libbcc y libpbf.
bpftrace es bueno para escribir potentes programas de una línea y scripts cortos, mientras que BCC desarrolla principalmente procesos de back-end a gran escala relativamente complejos.
BCC y bpftrace no pertenecen al kernel de linux, pero pertenecen a una fundación IO ViSor linux en GITHUB.
Inicio rápido del proyecto BCC
ejecutivosnoop
Desde el proyecto bcc, funciona rastreando la llamada al sistema execve.
sudo execsnoop-bpfcc
efecto
Puede usar esta herramienta para verificar la carga del negocio, es decir, puede ver si el proceso se crea en un período determinado de acuerdo con sus propias ideas.
biolatencia
concepto
Dibuje un histograma de latencia de dispositivo de bloque (latencia de E/S de disco)
sudo biolatency-bpfcc
En el entorno de la máquina virtual, este comando no se ejecutará con la versión del kernel: 5.19.0-35-generic.
Instrumentación dinámica: kprobes y uprobes
concepto
Inserte puntos de observación en ubicaciones de instrucciones arbitrarias en un programa que se ejecuta en producción
defecto
A medida que se cambia la versión, la función instrumentada puede cambiar de nombre o eliminarse directamente, lo que causará problemas de estabilidad y la herramienta BPF puede no funcionar directamente.
Instrumentación estática: tracepoint y USDT
concepto
En esencia, es para resolver el problema de la estabilidad de la interfaz en la instrumentación dinámica anterior y codificar directamente el nombre del evento estable en el código, que es mantenido directamente por el desarrollador.
USDT (seguimiento definido estáticamente a nivel de usuario) describe esta tecnología.
defecto
La codificación rígida introduce costos de mantenimiento adicionales
solución recomendada
Primero use tecnología de seguimiento estático (tracpoint o USDT) y luego use tecnología de instrumentación dinámica si no es suficiente.
Introducción a bpftrace: seguimiento de openat
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat {printf("%s %s \n",comm,str(args->filename));}'
Aquí hay algunos pequeños consejos:
- openat se llama muchas más veces que open en linux
- bpftrace debe usar privilegios de root
- bpftrace es generalmente simple y admite algunas herramientas de comando relativamente pequeñas, pero la herramienta BCC es más poderosa
experiencia técnica
BPF gráfico
Funciones auxiliares de BPF
- Funciones de operación de mapas: BPF_MAP_LOOKUP_ELEM, BPF_MAP_UPDATE_ELEM, BPF_MAP_DELETE_ELEM, etc.
- Funciones de operación de memoria: BPF_MEMCPY, BPF_MEMCPY_STR, BPF_MEMSET, etc.
- Funciones de operación de red: BPF_SOCK_OPS_TCP_SOCK, etc.
- Funciones de operación de tiempo: BPF_KTIME_GET_NS, etc.
- Funciones de operación de llamadas al sistema: BPF_TRACE_PRINTK, BPF_GET_CURRENT_PID_TID, etc.
- Funciones de cálculo matemático: BPF_ADD, BPF_SUB, BPF_MUL, BPF_DIV, etc.
- Otras funciones: BPF_DEBUG, BPF_EXIT, etc.
bpf_probe_read()
El acceso a la memoria en BPF está limitado a registros BPF y espacio de pila (y acceso a tablas de mapeo BPF a través de funciones auxiliares).Si accede a otra memoria (por ejemplo, memoria que no sea BPF), necesita usar bpf_probe_read().
Esta función verifica la seguridad del proceso y prohíbe las interrupciones por fallas de página para garantizar que no se produzcan interrupciones por fallas de página en el contexto de la sonda (de lo contrario, puede causar problemas en el núcleo).
Hay otras funciones auxiliares: bpf_probe_read_kernel, bpf_probe_read_user()
¿Por qué bpf_probe_read deshabilita las interrupciones de fallas de página?
En el kernel de Linux, los programas BPF pueden acceder a la memoria del espacio del usuario en el espacio del kernel. Cuando la página en el espacio de direcciones del espacio de usuario al que accede el programa BPF no está en la memoria física, se activará una interrupción de falla de página y luego el kernel leerá la página correspondiente del disco en la memoria para completar la asignación de memoria física. Este proceso requiere mucho tiempo.
Sin embargo, en algunos casos, es posible que no necesitemos leer la página correspondiente a la interrupción de falla de página inmediatamente, por ejemplo, cuando procesamos paquetes de datos de red de alta velocidad, no podemos soportar demasiado retraso. Por lo tanto, podemos evitar la asignación de memoria física que consume mucho tiempo y mejorar el rendimiento del procesamiento al deshabilitar las interrupciones de fallas de página.
Cómo bpf_probe_read prohíbe la interrupción por falla de página
Es necesario configurar el bit de indicador de interrupción de falla de la página de salida del hilo actual
/* 禁用缺页中断 */ void disabled_page_fault(void) { preempt_disable(); actual->banderas |= PF_NOFREEZE; actual->mm->def_flags |= VM_FAULT_NOPAGE; }
muestra
Use la función bpf_probe_read para leer paquetes UDP en skb
El proceso de lectura del paquete de datos UDP necesita leer primero el encabezado IP, luego determinar el protocolo de capa superior como UDP de acuerdo con el campo de tipo de protocolo IP, luego leer el encabezado UDP y luego leer la carga útil de datos. El siguiente es un código de muestra para leer paquetes UDP usando la función bpf_probe_read()
#include <linux/bpf.h> #include <linux/if_ether.h> #include <linux/ip.h> #include <linux/udp.h> int mybpf_prog(struct __sk_buff *skb) { void *data = ( vacío *)(largo)skb->datos; vacío *fin_datos = (vacío *)(largo)skb->fin_datos; estructura ethhdr *eth = datos; si (eth + 1 > data_end) devuelve 0; // 读取IP头 struct iphdr iph; if (bpf_probe_read(&iph, sizeof(iph), (void *)(eth + 1)) != 0) devuelve 0; if (iph.protocol == IPPROTO_UDP) { // 读取UDP头 struct udphdr uh; if (bpf_probe_read(&uh, sizeof(uh), (void *)((unsigned char *)iph + (iph.ihl * 4))) ! devolver 0; // 计算数据包总长度 unsigned int len = ntohs(iph.tot_len) - (iph.ihl * 4) - sizeof(uh); if (len <= 0) // error de longitud del paquete devuelve 0; // carga útil de datos de lectura carga útil de caracteres sin firmar[ len]; if (bpf_probe_read(&payload, len, (void *)((unsigned char *)uh + sizeof(uh))) != 0) return 0; // Procesar la carga de datos leídos ... return 1; } devuelve 0; }
Comandos de llamada al sistema BPF
Usando strace para analizar execsnoop
sudo strace -ebpf execsnoop-bpfcc
bpf(BPF_BTF_LOAD, {btf="\237\353\1\0\30\0\0\0\0\0\0\0\274\4\0\0\274\4\0\0H\17 \0\0\0\0\0\0\0\0\0\2"..., btf_log_buf=NULO, btf_size=5148, btf_log_size=0, btf_log_level=0}, 28) = 3 bpf(BPF_PROG_LOAD , {map_type=BPF_MAP_TYPE_PERF_EVENT_ARRAY, key_size=4, value_size=4, max_entries=128, map_flags=0, inner_map_fd=0, map_name="events", map_ifindex=0, btf_fd=0, btf_key_type_id=0, btf_value_type_id=0, btf_vmlinux_value_type_id= 0 , mapa_extra=0}, 72) = 4 bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_KPROBE, insn_cnt=510, insns=0x7ff36007d000, license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_VERSION(5, 19, 17), prog_flags=0, prog_name="syscall__execve", prog_ifindex=0, tipo_de_conexión_esperado=BPF_CGROUP_INET_INGRESS, prog_btf_fd=3, func_info_rec_size=8, func_info=0x5645a446e430, func_info_cnt=1, line_info_rec_size=16, line_info=0x5645a4a88f2 0, línea_info_cnt=252, adjunto_btf_id=0, adjunto_prog_fd=0, fd_array=NULO}, 144) = 5 bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_KPROBE, insn_cnt=82, insns=0x7ff36021b7d0, licencia="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_VERSION(5, 19, 17), prog_flags=0, prog_name="do_ret_sys_exec", prog_ifindex=0, tipo_de_conexión_esperado=BPF_CGROUP_INET_INGRESS, prog_btf_fd=3, func_info_rec_size=8, func_info=0x5645a446e430, func_info_cnt=1, line_info_rec_size=16, line_info=0x5645a348d 760, línea_info_cnt=28, adjunto_btf_id=0, adjunto_prog_fd=0, fd_array=NULL}, 144) = 7 PCOMM PID PPID RET ARGS bpf(BPF_MAP_UPDATE_ELEM, {map_fd=4, key=0x7ff35babc690, value=0x7ff35babc590, flags=BPF_ANY}, 144) = 0 bpf(BPF_MAP_UPDATE_ELEM, {map_fd= 4, clave =0x7ff35babc590, value=0x7ff35babc690, flags=BPF_ANY}, 144) = 0 ^Cstrace: Proceso 4540 desconectado
PD: es mejor evitar usar strace directamente porque usar strace esencialmente usa ptrace, lo que reducirá seriamente la velocidad de ejecución del proceso de destino, y el rendimiento puede caer directamente al 1% del original, pero su ventaja es que puede soportar La traducción de llamadas al sistema bpf, por ejemplo, puede imprimir BPF_PROG_LOAD
Tipo de programa BPF
Los diferentes tipos de programas bpf definen los tipos de eventos que un programa BPF puede montar, así como los parámetros de los eventos.Los tipos de programas BPF que se utilizan principalmente con fines de rastreo son los siguientes:
Tipo de mapa BPF
Entre ellos, BPF_MAP_TYPE_PERF_EVENT_ARRAY puede transferir la información capturada en el kernel al usuario, y execsnoop usa este tipo.
Control de concurrencia BPF
BPF siempre ha carecido de control de concurrencia hasta que Linux 5.1 agregó asistentes de bloqueo de giro, aunque aún no están disponibles para rastreadores.
Con el seguimiento, los subprocesos paralelos pueden buscar y actualizar los campos del mapa BPF en paralelo, lo que provoca daños cuando un subproceso sobrescribe una actualización de otro. Esto también se conoce como el problema de actualización perdida (lost update), es decir, la superposición de lecturas y escrituras simultáneas conduce a la pérdida de actualizaciones. Trace front-end BCC y bpftrace usan tipos de mapa de matriz y hash por CPU siempre que sea posible para evitar esta corrupción. Esto creará instancias para cada CPU lógica (digamos usando BPF_MAP_TYPE_PERCPU_HASH con tipo PERCPU)
Así se usa el tipo BPF_MAP_TYPE_PERCPU_HASH
strace -febpf bpftrace -e 'k:vfs_read { @ = cuenta(); }'
Esta es la forma sin utilizar el control de concurrencia
strace -febpf bpftrace -e 'k:vfs_read { @++; }'
La comparación de los recuentos muestra que el hash normal subestima los eventos en un 0,01 %.
Habrá un error de 0.01% en comparación.
Interfaz BPF sysfs
En Linux 4.4, BPF introdujo comandos para exponer programas y mapas BPF a través de un sistema de archivos virtual, generalmente montado en /sys/fs/bpf. Esto se llama "fijar", y se puede utilizar de muchas maneras. Permite crear programas BPF persistentes, similares a demonios, y continuar ejecutándose después de que finaliza el proceso que los cargó. También proporciona otra forma para que los programas de la zona del usuario interactúen con los programas BPF en ejecución: pueden leer y escribir mapas BPF.
Formato de tipo BPF
Si nos falta el código fuente del programa de destino, lo que dificulta la escritura de algunas herramientas BPF, aquí hay una tecnología BTF que puede resolver este problema.
Pero la tecnología BTF aún está en desarrollo.
BPF NÚCLEO
El proyecto Compile Once - Run Everywhere de BPF tiene como objetivo permitir que los programas BPF se compilen en el código de bytes BPF una vez, se guarden y luego se distribuyan y ejecuten en otros sistemas. Esto evitará la instalación de compiladores BPF (LLVM y Clang) en todas partes, lo cual es un desafío para Linux integrado con limitaciones de espacio. También evita los costos de CPU y memoria en tiempo de ejecución de ejecutar un compilador al ejecutar herramientas de observabilidad BPF.
CO-RE también está actualmente en desarrollo.
Limitaciones de BPF
- No utilice las funciones del núcleo a voluntad.
- El tamaño de la pila BPF no puede exceder 512 (MAX_BPF_STACK).Hay una solución para esto: use espacio de almacenamiento mapeado.
seguimiento de la pila de llamadas
BPF proporciona una estructura de tabla de mapeo dedicada para almacenar información de la pila de llamadas, que puede guardar información de seguimiento de la pila de llamadas basada en punteros de tramas o en ORC.
Seguimiento de la pila de llamadas basado en el puntero del marco
Esta tecnología se basa principalmente en una premisa: el encabezado de la lista de marcos de la pila de llamadas de función siempre se almacena en un registro determinado (RBP en x86_64), y la dirección de retorno de esta llamada de función siempre se encuentra en la posición señalada por RBP. valor más un desplazamiento fijo (+8).
Esto indica que cualquier depurador puede realizar fácilmente un seguimiento de la pila después de interrumpir la ejecución del programa recorriendo la lista vinculada encabezada por el valor RBP después de leer el RBP y obtener la dirección de retorno en una posición de desplazamiento fija.
PD: en el compilador gcc, no hay un puntero de marco de función de forma predeterminada, y RBP se usa como un registro ordinario, pero la mejora del rendimiento no es muy alta, por lo que se recomienda habilitar este comportamiento predeterminado.
-fno-omitir-frame-puntero
Hacer depuración basada en información de depuración
Eso es agregar -g -wall después de gcc.
Contiene la información de depuración de ELF de DWARF
Los segmentos de archivo relacionados con la depuración en el archivo ELF son .eh_frame y .debug_frame
La desventaja es que esto hace que el ejecutable sea muy grande.
libjvm.so = 17M
libjvmd.so = 222M
último registro de sucursal
Es decir, LBR es una tecnología especial de inter, que se registra en el búfer de hardware.Esta tecnología no tiene sobrecarga adicional.
Pero habrá límites para la profundidad de los registros de soporte.
BPF no es compatible con LBR
ORCO
Un nuevo formato de información de depuración: la capacidad de rebobinado Oops (ORC) está especialmente diseñada para los requisitos de seguimiento de la pila. En comparación con el formato DWARF, el uso de este formato tiene requisitos más bajos para los procesadores. ORC también usa segmentos de archivos ELF, y el kernel de Linux actual brinda algo de soporte.
En la actualidad, no hay desarrollo de soporte para la pila de llamadas ORC en modo de usuario.
La función perf_callchain_kernel puede admitir el seguimiento de la pila de llamadas basada en ORC en el kernel.
símbolo
La información de la pila de llamadas se registra actualmente en forma de datos de direcciones en el kernel, y estas direcciones se pueden traducir a símbolos (como nombres de funciones) mediante programas en modo de usuario.
Pero esta parte del trabajo aún no está completa.
gráfico de llama
Un gráfico de llamas es una herramienta de visualización para mostrar el uso de la CPU de un programa. Aquí hay algunos métodos básicos de uso e interpretación de los gráficos de llama:
- Eje de coordenadas: el eje Y del gráfico de llamas identifica la pila de llamadas y representa la profundidad de las llamadas a funciones de arriba a abajo. El eje x representa el tiempo de CPU (que puede estar en milisegundos, segundos u otras unidades), y de izquierda a derecha representa el tiempo de ejecución del programa.
- Color: el color del gráfico de llamas indica el valor relativo del tiempo de CPU o el espacio de contador consumido por las llamadas a funciones.
- Ancho: el ancho de cada rectángulo en el gráfico de llamas representa el tiempo de CPU o la medida del contador para el código en cuestión.
- Herramientas: por ejemplo, Flamegraph, perf y otras herramientas de generación de gráficos de llama admiten la habilitación de varias configuraciones, como el orden de los rectángulos, los esquemas de color, etc.
Con base en la información anterior, aprendamos cómo interpretar el gráfico de llama:
- Comienzo y final: el comienzo y el final de un gráfico de llamas suelen ser los puntos de entrada y salida de un programa y, por lo general, tienen placas bastante anchas y colores más claros en los gráficos de llamas.
- Ancho: un rectángulo con un ancho mayor en el gráfico de la llama indica que el consumo de recursos durante la ejecución del programa es relativamente alto y que el tiempo de ejecución del código es correspondientemente más largo.
- Color: El color en el gráfico de la llama varía de verde claro a verde oscuro. El área verde claro es un área relativamente lenta y de baja sobrecarga en el programa, y el área verde oscuro persigue un mayor consumo de tiempo.
- Llamadas repetidas: en el gráfico de llamas, las llamadas repetidas de la misma función mostrarán el mismo cuadro, y el ancho del cuadro indica la cantidad de veces que se llamó y el tamaño relativo del tiempo de ejecución.
- Información de la pila: el nombre y la información de la pila de llamadas de cada llamada de función en el gráfico de llamas también se pueden usar para identificar y ajustar problemas de rendimiento en el programa.
origen del evento
Ksondas
El concepto de kprobes y kretprobes
kprobes proporciona soporte de instrumentación dinámica para el kernel sin reiniciar el kernel.
kretprobes se puede usar para instrumentar el valor de retorno de la función del kernel para obtener el valor de retorno, o usar kprobes y kretprobes para instrumentar una función al mismo tiempo para obtener la hora de una llamada de función del kernel.
principio
- Registro de puntos de interrupción: Kprobes utiliza la tecnología de asignación de memoria dinámica del kernel para registrar puntos de interrupción en ubicaciones clave en el código del servidor web del kernel.
- Activación del punto de interrupción: cuando el kernel se ejecuta en la posición del punto de interrupción registrado, suspende inmediatamente la ejecución y comienza a ejecutar el controlador registrado por Kprobes en este momento.
- Manejador: un manejador para Kprobes puede ser una función definida por el usuario que puede vincularse a una llamada de función del kernel en un punto de interrupción y registrar datos de rendimiento o cambiar el estado del kernel con fines de depuración o creación de perfiles.
- Procesamiento completado: una vez que se completa el programa de procesamiento, Kprobes continuará ejecutando el código restante en la ubicación interrumpida y completará el rastreo o el procesamiento del código del kernel.
Interfaz de Kprobes
Debe usar el lenguaje c para escribir la función de procesamiento del puerto y la función de procesamiento de retorno, y luego llamar a register_kprobe () para registrarse.
Utilice principalmente BCC o bpftrace ahora.
BCC proporciona:
adjuntar_kprobe() adjuntar_kretprobe()
Una demostración de bpftrace es la siguiente:
bpftrace -e 'kprobe:vfs_* {@[probe] = cuenta()}'
sube
concepto
Proporciona instrumentación dinámica de programas de modo de usuario.
Similar a kprobes, el principio es similar a kprobes.
principio
Al insertar una instrucción de salto en una dirección específica, se intercepta el flujo de ejecución de la CPU que ingresa o sale de la dirección, y el programa de procesamiento especificado se ejecuta antes y después. Este método puede monitorear y modificar el estado de ejecución del programa sin destruir el código binario
interfaz
Basado en Ftrace, para /sys/kernel/debug/tracing/uprobe_events: abra o cierre uprobes escribiendo una cadena específica en este archivo
perf_event_open()
Se proporcionan dos interfaces en BCC:
adjuntar_uprobe adjuntar_uretprobe
punto de rastreo punto de rastreo
concepto
Instrumentación estática
principio
Al escribir el código del kernel, los desarrolladores pueden usar definiciones de macros de puntos de seguimiento para predefinir puntos de seguimiento. Estos puntos de seguimiento y sus parámetros se fijan en el binario del kernel en el momento de la compilación.
Cuando el programa se está ejecutando, si la función de seguimiento está habilitada, cuando el programa se ejecuta en el punto de seguimiento correspondiente, se activará la función de devolución de llamada del punto de seguimiento correspondiente. Esta función de devolución de llamada se puede definir en el código para implementar las operaciones requeridas. Dado que los puntos de seguimiento se han definido de antemano, es posible evitar la inserción de instrucciones de ensamblaje adicionales, lo que reduce el impacto en el rendimiento del programa.
interfaz
BCC proporciona
rastrearpunto_sonda()
USDT
concepto
Seguimiento estático predefinido en modo de usuario, que proporciona un mecanismo de punto de seguimiento del espacio del usuario
BPF y USDT
USDT().enable_probe()
Contadores de supervisión del rendimiento
Los contadores de supervisión del rendimiento son contadores que se utilizan para medir varios estados del hardware del sistema durante la ejecución del programa. Por lo general, los proporciona el hardware subyacente del procesador, y se puede usar una gran cantidad de contadores que se ejecutan en el sistema para monitorear el rendimiento y los cuellos de botella del sistema. Los contadores de monitoreo de rendimiento pueden medir varios indicadores, como la ejecución de instrucciones, el rendimiento de la memoria caché de la CPU, el resumen de la memoria, etc., y luego evaluar el rendimiento de todo el sistema y el efecto de las soluciones de optimización.
En el proceso de desarrollo de software, el cuello de botella de rendimiento del programa se puede analizar con mayor precisión utilizando el contador de seguimiento de rendimiento. Los desarrolladores pueden combinar diferentes indicadores de contador para identificar los cuellos de botella del sistema y mejorar el rendimiento del programa mediante la optimización del código y los algoritmos.
Los contadores de monitoreo de rendimiento comunes incluyen ciclos operativos de CPU (relojes), número de ejecuciones de instrucciones (instrucciones), tasa de aciertos de caché (aciertos de caché), latencia de acceso a la memoria (latencia de memoria), etc. Estos contadores generalmente se pueden obtener a través de herramientas dedicadas o interfaces de línea de comandos del sistema, como herramientas de rendimiento proporcionadas por sistemas Linux, Intel VTune, AMD CodeXL y otras herramientas de monitoreo de rendimiento.