Funcionamiento del subsistema de red de Linux

Uno de los aspectos superlativos del sistema operativo Linux es todo el soporte que le proporciona a la seguridad informática y a las redes de datos, incluyendo Internet. Todos, o casi todos los equipos activos de redes (Sophos, Fortinet), funcionan con algún firmware basado en este sistema operativo. Analizar a fondo el funcionamiento del subsistema de red de Linux es algo pretencioso para este articulo, existen libros que se dedican a hacerlo, sin embargo veamos lo más puntual y relevante de su estructura.

Subsistema de red de Linux


Prácticamente el subsistema de red en Linux es la materialización del modelo OSI en este sistema operativo. En este blog ya habíamos hablado que dicho modelo es una estructura abstracta de siete capas, que soporta el funcionamiento de las redes de datos. Veamos los pasos básicos que realiza el sistema cuando le llegan los paquetes de red:

  1. La NIC (tarjeta de red) activa el subsistema NAPI
  2. Llegan los paquetes de red.
  3. La NIC ubica los paquetes  de red en su memoria interna, llamada FIFO.
  4. Estos paquetes son trasladados desde la FIFO a un espacio de la memoria RAM, a través de un procedimiento de transferencia llamado DMA (Direct Memory Access).
  5. Por medio de NAPI el controlador de la NIC avisa al kernel para que se encargue de los paquetes.
  6. El kernel toma los paquetes de la RAM y les da transito a las demás capas de la pila TCP/IP.
  7. Las regiones de memoria RAM se liberan.

Proceso de transito y transformación del paquete de red


Básicamente cuando los paquetes llegan al dispositivo de red en forma de señales eléctricas, o lumínicas, a través del medio, se descodifican según haya sido el sistema de codificación en el origen (NRZI, MLT-3 o PAM 5). Miremos el esquema en bloques del chip Realtek RTL8139C, que se encarga de ubicar cada paquete de red en su memoria FIFO (en línea roja).

Por visualización tuve que dividir la imagen en dos partes. Fuente: http://realtek.info/pdf/rtl8139cp.pdf

NAPI


Tradicionalmente una NIC generaba una solicitud de interrupción (IRQ) que indicaba a la CPU que los datos habían llegado a la RAM. Ese procedimiento es relativamente simple, pero, si llegaban grandes cantidades de paquetes, generaba una gran cantidad de IRQ que reducían el tiempo disponible de la CPU para tareas de nivel superior, tales como los procesos de usuario.

Para optimizar esa situación, desde la versión 2.5 del kernel se incluye el método NAPI (New API). Con NAPI cada paquete recibido no produce una interrupción. En su lugar, los paquetes se almacenan en la memoria RAM y el kernel periódicamente la sondea para recuperarlos. El uso de NAPI mejora el rendimiento en altas cargas de tráfico.

Veamos con más detalle el procedimiento con NAPI

1- El controlador de la NIC habilita la NAPI. El controlador debe deshabilitar los IRQ de la NIC para permitir que el subsistema NAPI procese los paquetes sin interrupción de la CPU.

2- Los paquetes entrantes son colocados en la FIFO Rx, y, cuando están completos, la NIC los traslada directamente a un espacio de memoria por medio del DMA. Esto libera a la CPU de ejecutar esa tarea. Este espacio de memoria es llamado DMA buffer, o buffer ring. Mientras más paquetes le lleguen a la NIC, más se irán almacenando en el buffer ring.

3Entonces NAPI genera una lista de los paquetes en un Poll list (poll_list), e inmediatamente le deja una «nota» al kernel por medio del registro softirq_pending (también llamado interrupt handler schedules – Programador de Manejador de Interrupciones – y se asigna uno a cada núcleo de la CPU), avisando que ya puede recoger los paquetes del DMA buffer.

4- Por medio del algoritmo ksoftirqd, el kernel verifica periódicamente la existencia de algún softirq pendiente en el registro softirq_pending (que ya NAPI registró en el paso 3). Al encontrar un softirq, el kernel se dirije al DMA buffer para recuperar los paquetes, y de ahí los traslada a la pila TCP/IP. Los procesos ksoftirqd del kernel se ejecutan en cada CPU del sistema y se registran en el momento de arranque.

5Una vez que no haya más trabajo por hacer, el subsistema NAPI se deshabilita y las IRQ del dispositivo se vuelven a habilitar.

6El proceso comienza de nuevo en el paso 2.

Los archivos responsables de estos procedimientos son:

/usr/src/linux-headers-4.4.0-112/include/linux/netdevice.h
/usr/src/linux-headers-4.4.0-112/include/net/busy_poll.h

Puedes leer más sobre NAPI en la página de la Linux Fundation.

Trayendo un dispositivo de red


Cuando se activa un dispositivo de red (por ejemplo, con  el comando ifconfig eth0 up), se llama a la función asociada al campo ndo_open de la estructura net_device_ops. El archivo del kernel encargado de esto es:

/usr/src/linux-headers-4.4.0-109/include/linux/netdevice.h

La función ndo_open normalmente hará cosas como:

  1. Asignar memoria de cola RX y TX
  2. Habilitar NAPI (netif_napi_add)
  3. Registrar un gestor de interrupciones
  4. Habilitar interrupciones de hardware

Preparación para recibir datos de la red


La mayoría de tarjetas de red de hoy utilizan DMA para colocar los paquetes de red directamente en la RAM. La estructura de datos que la mayoría de las NIC usan para este propósito se asemeja a un circulo giratorio, por eso le llaman Buffer Ring.

Para hacer esto, el controlador del dispositivo debe trabajar con el sistema operativo para reservar una región de memoria que pueda usar el hardware de la NIC. Una vez que esta región está reservada, se informa al hardware de su ubicación y los datos entrantes se escribirán en la RAM, donde el subsistema de red los recogerá y procesará.

Ajustando el tamaño del Búfer Ring


Precisamente con el comando ethtool podemos ajustar el tamaño del búfer ring, hasta donde lo permita la NIC. En el caso de las Intel 82575EB Gigabit Ethernet, el Espacio de Configuración Extendida (Extended Configuration Space) es el que determina el tamaño máximo del búfer ring. Miremos lo que dice su ficha técnica:

Rango de ajuste del buffer ring, desde 256 a 4096 bytes. Fuente: https://www.intel.la/content/dam/doc/manual/82575eb-gbe-controller-manual.pdf

En resumen, lo que quiere decir la ficha técnica es que esos chips de ethernet Intel, pueden direccionar búfers rings desde 256 hasta 4096 bytes (4 KB). Se recomienda aumentar el tamaño de este búfer en equipos que procesen altas densidades de tráfico, para que el kernel tenga más tiempo de evacuar todos los paquetes de la RAM y así evitar pérdida de datos.

Verificando el tamaño del búfer ring asignado a una interfaz de red


Por ejemplo, la siguiente interfaz de red tiene capacidad de crear hasta 4 KB de búfer ring, pero por defecto ha creado 256 bytes:

_# ethtool -g enp0s9
Ring parameters for enp0s9:
Pre-set maximums:
RX: 4096
RX Mini: 0
RX Jumbo: 0
TX: 4096
Current hardware settings:
RX: 256
RX Mini: 0
RX Jumbo: 0
TX: 256

Entonces incrementamos el tamaño del búfer a 4096 bytes:

_# ethtool -G enp0s9 rx 4096 tx 4096

Podemos visualizar la marca y el modelo de las tarjetas de red instaladas, con el siguiente comando:

_# lspci | egrep -i --color 'network|ethernet'

Algunas opciones del comando ethtool no funciona con los chips Realtek, de tarjetas de red baratas.

Recibiendo paquetes con una tarjeta de red Intel I350 <- <- <-


Suena sencillo, pero ¿qué pasaría si la tasa de paquetes fuera lo suficientemente alta como para que una sola CPU no pudiera procesar correctamente todos los paquetes entrantes? (tal como se presentan en serviores web con alta densidad de tráfico) La estructura de datos se basa en una región de longitud fija de la memoria, por lo que los paquetes entrantes que desborden el buffer se descartarían. Sabemos que podemos aumentar el tamaño del buffer ring, pero también podemos complementar esta medida con algo conocido como Recibo Escalado lateral (RSS), también llamado Recepción Multi-cola (Multi-queue Receive).

Algunas NIC vienen con soporte Multi-cola, o sea que pueden escribir paquetes entrantes en varias regiones diferentes de RAM simultáneamente. Esto permite que el sistema operativo utilice varias CPU para procesar datos entrantes en paralelo.

La NIC Intel I350 admite Recepción Multi-cola. Podemos ver evidencia de esto en el controlador igb. Una de las primeras cosas que hace el controlador igb cuando se activa, es llamar la función igb_setup_all_rx_resources. Al mismo tiempo llama a la función igb_setup_rx_resources, una sola vez por cola RX, y así organiza un DMA buffer por cada FIFO:

Subsistema de red con una NAPI

Subsistema de red con multiples NAPI debido a que la NIC soporta varios FIFO. Fuente: https://www.intel.com/content/dam/www/public/us/en/documents/white-papers/multi-core-processor-based-linux-paper.pdf

Una forma de contar los búfers rings que puede crear un dispositivo de red, es con el siguiente comando:

_# ethtool -u enp1s0f0
4 RX rings available
Total 0 rules

Siguiendo con el transito de los paquetes, tenemos que el kernel traslada los paquetes del DMA buffer hacía la estructura del socket buffer (sk_buff), por medio de la siguiente función:

skb_copy_to_linear_data()
ó
skb_copy_to_linear_data_offset()

Linux utiliza búfers de socket, llamados sk_buffs, para pasar datos entre protocolos y entre los controladores de dispositivo de red. Los sk_buffs contiene campos de puntero y longitud que permiten que cada capa de protocolo manipule los datos de la aplicación a través de funciones estándar. Puedes leer más sobre la estructura sk_buffs en la página de la Linux Fundation.

Esta función se ubica en el archivo /usr/src/linux-headers-4.4.0-112/include/linux/skbuff.h.

Después el kernel traslada el contenido de la estructura sk_buff a la cola de la CPU con netif_rx(skb), del archivo /usr/src/linux-headers-4.4.0-112/include/net/gro_cells.h

Acto seguido se mueven los datos del sk_buff, de la cola de la CPU a la capa de red, por medio de la función ip_rcv() del archivo /usr/src/linux-headers-4.4.0-112/include/net/ip.h.

Entonces el kernel regresa a verificar la existencia de otro softirq, y se repite el proceso.

Proceso de la capa de Red


El archivo /usr/src/linux-headers-4.4.0-112/include/net/ip.h implementa la mayor parte de las funciones correspondientes de la Capa de Red, del modelo OSI. Tareas como verificar la versión del paquete IP, su tipo, su longitud, restar su tiempo de vida TTL, todo eso lo trabaja este archivo. Además, el framework Netfilter (net/netfilter/nf_tables_ipv4.h) usa a ip.h como librería para que iptables realice su correspondiente análisis y filtrado de paquetes (firewall).

En versiones anteriores del kernel, estas funciones se distribuían entre cinco archivos:

  • ipv4_input.c → Función relacionada con el ingreso de los paquetes
  • ipv4_input.c → Función relacionada con la trasmisión de paquetes
  • ipv4_forward.c → Función relacionada con el reenvio de paquetes (cuando Linux trabaja como router)
  • ipv4_fragment.c → Funciones relacionadas con la fragmentación IP
  • ipv4_route.c → Función relacionada con enrutamiento IP

Si el destino de ese paquete es local (a esta máquina), el archivo ip.h lo entrega al kernel con la función ip_local_deliver(), entonces éste desensambla sus cabeceras IP para entregar el segmento a la capa TCP. Si el paquete no va dirigido a esta máquina, la librería route.h lo redirije a su destino con la función ip_route_input(), precisamente esta es la función principal de un router.

El archivo tcp.h recibe el segmento por medio de la función tcp_v4_rcv().

Proceso de la capa de Transporte


En el nuevo kernel la librería /usr/src/linux-headers-4.4.0-112/include/net/tcp.h implementa las funciones del protocolo TCP. Antes, los archivos que implementaban esta capa en Linux eran:

  • tcp.c → Capa entre el espacio de usuario y el kernel
  • tcp_output.c → Motor de salida. Procesa los datos y los transfiere a la capa de red.
  • tcp_input.c → Motor de entrada. Maneja los segmentos entrantes
  • tcp_timer.c – Temporizador para el TCP.
  • tcp_ipv4.c → Procesa los segmentos entrantes de la versión 4 de TCP/IP
  • tcp_cong.c → Control de congestión

El siguiente esquema general muestra el mecanismo que manipula los paquetes de de red , entre la capa 3 y la capa 4:

subsistema de red de linux

Proceso TCP/IP en Linux

Transmisión de un paquete -> -> ->


Uno de los problemas de tener varios protocolos de red, cada uno de los cuales utiliza los servicios de otro, es que cada protocolo debe agregar encabezados y colas de protocolo a los datos a medida que se transmiten, y eliminarlos a medida que procesa los datos recibidos. Esto dificulta el paso de los buffers de datos entre los protocolos, ya que cada capa necesita encontrar dónde se encuentran los encabezados y las colas de sus protocolos particulares. Una solución es copiar buffers en cada capa, pero eso es ineficiente. Entonces para  eso se creó los sk_buff.

Los paquetes son transmitidos por aplicaciones que intercambian datos o, de lo contrario, son generados por los protocolos de red, ya que son compatibles con las conexiones establecidas o conexiones establecidas. Cualquiera que sea la forma en que se generan los datos, se crea un sk_buff para contener los datos y las capas de protocolo agregan varios encabezados a medida que pasan a través de ellos.

Los protocolos de la capa de Aplicación generan los datos y los colocan en el sk_buff proveyendola con información adicional, tal como la dirección IP destino para que se pueda armar el paquete IP más adelante.

La capa 4 arma el segmento basado en la información que contenga la estructura sk_buff, y lo pasa a la capa de red. En esta capa, el segmento es recibido por la función ip_queue_xmit del archivo ip.h. Esta función se usa para recibir datos generados localmente (no reenviados), basado en el sk_buff.

La función ip_build_and_send_pkt() (archivo ip.h) contruye el encabezado IP, lo anexa al segmento, y de ahí lo transmite a la capa de enlace. Esa función también es usada por TCP para enviar SYN ACKs.

La función ip_send_reply() la usa TCP para enviar los ACK y replys.

Optimización de las colas (queue)


Con este «brochazo» conceptual y técnico del subsistema de red de Linux, podemos inferir que tenemos herramientas para optimizar el flujo de datos en este sistema, y las hay. Esa es la razón por la que nos cautiva GNU/Linux y no Microsoft Windows. El Windows te entrega solo la ventana, pero GNU/Linux te lo entrega todo! la ventana, la puerta, los planos, casi todo. Por eso la mejor forma de aprender de sistemas operativos y de redes es con GNU/Linux, y bueno, también Cisco.

A continuación un mapa general que relaciona los comandos de optimización del transito del flujo de datos, en cada una de las capas del Modelo OSI (ubicados en búfers):

En el siguiente articulo veremos los comandos para optimizar un servidor GNU/Linux de alto tráfico.

Funcionamiento del subsistema de red de Linux Overall rating: ☆☆☆☆☆ 0 based on 0 reviews
5 1

 

Su nombre
Email
Titulo
Valoración
Opinión

 

Comparte esto en
Publicado en GNU Linux.