itmo_conspects

Лекция 14. Работа с сетями в Linux

В сети передачу информации можно разделить на два типа:

Идеи пакетной сети заложились в реализации ARPANET - ранней версии Интернета. Уже тогда появилась концепция сокета - интерфейс для общения приложений, который на уровне ОС абстрагировал сеть для программиста в удобных буфер чтения/записи, что облегчало взаимодействие с сетью (как раз разные типы коммутаций привели к разделению на TCP и UDP)

Определение сокета менялось до тех пор, пока не пришли к тому, что сокет - это конечная точка для коммуникации процесса с другими процессами путем отправки и приема сообщений

Впервые сокет появился в BSD. Сокеты в Unix, а также в Unix-подобных системах, включая Linux, делятся:

Источник: https://man7.org/linux/man-pages/man2/socket.2.html

Для установки соединения с сокетами типов SOCK_STREAM и SOCK_DGRAM используется базовый системный вызов - connect()

Хотя connect() является основой, в процессе установки соединения для SOCK_STREAM на стороне сервера используются и другие ключевые вызовы:

Подключенный сокет представляет из себя файловый дескриптор, к которому применимы системные вызовы read и write. Для сокетов также применимы системные вызовы recv и send с дополнительными флагами (например, не ждать получение подтверждения MSG_DONTWAIT)


Для сокетов есть виртуальная файловая система sockfs. Локальные сокеты могут быть представлены файлами в файловой системе. В псевдофайлах /proc/net/tcp и /proc/net/udp содержится информация о созданных сокетах. Так, например, выглядят сокеты TCP:

  sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
   0: 3500007F:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000   101        0 17053 1 ffff91f576046300 100 0 0 10 0
   1: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 20021 2 ffff91f57512e300 100 0 0 10 0
   2: 0100007F:1538 00000000:0000 0A 00000000:00000000 00:00000000 00000000   109        0 20138 1 ffff91f57512a400 100 0 0 10 0
   3: 0100007F:AC43 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 25724 1 ffff91f575128000 100 0 0 10 0
   4: 0100007F:8124 00000000:0000 0A 00000000:00000000 00:00000000 00000000   110        0 20456 1 ffff91f57512ec00 100 0 0 10 0
   5: 0100007F:0CEA 00000000:0000 0A 00000000:00000000 00:00000000 00000000   110        0 20458 1 ffff91f577aebf00 100 0 0 10 0

Здесь содержатся:

  1. sl - индекс сокета в таблице
  2. local_address - локальный адрес и порт в шестнадцатеричном виде Здесь в первой строке 3500007F - это 127.0.0.53
  3. rem_address - удалённый адрес и порт
  4. st - состояние сокета в шестнадцатеричном виде. Коды состояний определены в заголовочных файлах ядра, то есть в /include/net/tcp_states.h
  5. tx_queue - количество данных в очереди отправки в байтах
  6. rx_queue - количество данных в очереди приёма в байтах
  7. tr - тип таймера активности (timer run)
  8. tm->when - время в джиффи (примерно 10 мс) до срабатывания текущего таймера, если он запущен
  9. retrnsmt - счётчик повторных передач для текущего сегмента
  10. uid - идентификатор пользователя, которому принадлежит сокет. Здесь в первой строке 101 - это пользователь systemd-resolve
  11. timeout - таймаут для текущего состояния сокета
  12. inode - номер индексного дескриптора сокета в файловой системе sockfs. Этот номер можно связать с открытыми файловыми дескрипторами процессов через /proc/<pid>/fd/

Далее идут дополнительные поля (например, указатель на структуру в памяти), количество которых зависит от конфигурации и версии ядра

Для упрощенного просмотра существует утилита ss (от socket statistics). Так для просмотра слушающих TCP и UDP сокетов можно использовать команду ss -tulnp


Пакет для ядра определяется структурой sk_buff с множеством полей

Сетевая карта в компьютере получает новый поток данных, драйвер сетевой карты получает прерывание (или запрос NAPI), обрабатывает это, создает структуру sk_buff и передает в подсистему Ethernet (или другую для конкретного протокола). Далее эта подсистема проверяет пакет, а потом сетевая подсистема определяет протокол транспортного уровня (в нашем случае IPv4 или IPv6) и передает пакет через сетевой фильтр

На сетевой фильтре на этапе премаршрутизации (Prerouting) принимается решение о дальнейшей судьбе пакета, которое кэшируется в таблицу, чтобы похожие пакеты проходили проверку быстрее

После премаршрутизации локальные пакеты для этого хоста попадают в фильтр для входящих пакетов (Input), где их обрабатывают TCP и UDP обработчики соответственно, которые находят сокеты и процессы, которые владеют сокетами

Проходящие через хост процессы проходят через фильтр для транзитных пакетов (Forward). Исходящие пакеты, созданные процессами, проходят фильтр для исходящих пакетов (Output)

После фильтров для транзитных и исходящих пакетов идет фильтр постмаршрутизации (Postrouting), который аналогично премаршрутизации принимает решение об отправке пакет

На этапах фильтров, премаршрутизации и постмаршрутизации, ядро вызывает хуки зарегистрированных обработчиков - так, например, работает утилита iptables

Далее после постмаршрутизации пакет проходит через подсистемы Neigh, Egress, Taps и другие, попадает в очередь отправки, откуда драйвер передает его на сетевую карту

Путь пакета в Linux

Источник: https://thermalcircle.de/doku.php?id=blog:linux:routing_decisions_in_the_linux_kernel_1_lookup_packet_flow

Так как проходящий пакет приводит к вызову множества системных вызовов, для оптимизации применяют технологию RDMA (Remote Direct Memory Access). В ней сетевые карты напрямую общаются и могут записывать в память удаленного компьютера, минуя ядро ОС и процессор. RDMA применяется для баз данных, распределенных хранилищ и высокопроизводительных вычислениях


В ядре Linux реализации протоколов содержатся в отдельных модулях в папке /net/ архива с исходным кодом. Например, реализация Ethernet хранится в файле /net/ethernet/eth.c

Для встраиваемых (embedding) системах, например, одноплатных компьютеров или контроллеров, до компиляции ядро конфигурируется так, что бы не включать ненужные сетевые модули и компоненты. Дополнительно, так как мы знаем подключенные, нужные нам устройства (например, датчик температуры), то вместо процедуры обнаружения идентификатора устройства и производителя, загрузки драйвера из таблицы, можно составить дерево устройств, где описаны все периферийные устройства, их адреса и нужные драйверы (которые часто вшиты в ядро для скорости и надёжности)