Линуксоиды делают то, что делают, потому что ненавидят Microsoft. Мы делаем то, что делаем, потому что любим Unix.
Тео де Раад, «Is Linux For Losers?», 2007
В ресурсах, перечисленных в главе 1, не всегда удается найти ответ на поставленный вопрос, особенно для конкретного ядра. Поэтому может потребоваться изучить дополнительные материалы и проанализировать исходный код ядра.
Преимущество решений Open Source заключается в том, что их исходный код доступен каждому.
Рассмотрев структуры ядра, можно представить в общих чертах, как устроены сокеты на низком уровне, что помогает глубже понять и качественно освоить материал — в данном случае только для Linux. Но если вы создаете продукт, работающий по Сети, вы, скорее всего, будете использовать Linux, ведь эта ОС — одна из наиболее распространенных на серверах. К тому же общие принципы справедливы и для некоторых других платформ.
Одного материала данной главы будет недостаточно, чтобы решать задачу разработки кода режима ядра, но он поможет выполнять поиск в исходном коде ядра более осмысленно, поэтому эту главу можно рассматривать как познавательную.
Для рассмотрения структур ядра можно воспользоваться сайтом , который содержит исходный код разных версий ядер Linux и позволяет рассматривать все структуры в браузере, удобно перемещаясь по ссылкам.
Там же имеется хороший поиск, с помощью которого можно найти объявления, места использования, определения структур и функций.
Код ядра также можно посмотреть на Github.
Кратко рассмотрим, что представляет собой реальный сокет в ядре ОС. Возьмем для примера Linux 6.9.
Основные структуры определены в include/linux/net.h и include/linux/sock.h.
Структура является представлением сокета в ядре, BSD-сокетом «общего вида»:
// Основная структура BSD-сокета.
struct socket
{
// Состояние сокета: SS_CONNECTED, etc.
socket_state state;
// Тип сокета: SOCK_STREAM, etc.
short type;
// Флаги сокета: SOCK_NOSPACE, etc.
unsigned long flags;
// Указатель на файл, используется для сборщика мусора.
struct file *file;
// Внутреннее представление сокета, независимое от протокола.
// Структура показана ниже.
struct sock *sk;
// Операции, специфичные для протокола.
const struct proto_ops *ops;
// Очередь ожидания.
struct socket_wq wq;
};
Эта структура используется системными вызовами. Большинство атрибутов имеет вполне понятное назначение. Состояние говорит о том, на каком шаге находится протокол.
Тип сокета задается явно, при вызове функции socket(). Флаги устанавливает ядро. Например, флаг SOCK_NOSPACE будет установлен, если в буфере сокета закончилось место. Когда этот флаг установлен, запись в сокет и его асинхронный опрос будут ждать освобождения буфера и сброса флага. То есть в блокирующем режиме запись будет либо ожидать, либо вернет ошибку, говорящую о том, что невозможно отправить данные без блокировки записи.
Поток ядра, отправляющий данные, после их отправки выполнит освобождение буфера, сбросит флаг и, если требуется, разбудит поток, в котором системный вызов писал данные в буфер сокета.
В реальности код работает несколько сложнее, но принцип сохраняется, и назначение поля флагов должно быть понятно.
Прочие атрибуты:
• sk, структура sock — внутреннее представление сокета, которое не зависит от протокола. Используется только внутри ядра, а детали могут изменяться в его разных версиях.
• ops содержит указатели на функции, которые выполняются над сокетом.
• wq был добавлен в структуру относительно недавно с целью оптимизации. Это очередь, в которой могут храниться, например, идентификаторы процессов, ожидающих готовности сокета.
Рассмотрим внутреннюю структуру. В ней также есть вложенные поля-структуры. Для упрощения доступа к ним определены некоторые макросы.
Прежде всего мы увидим очереди для отправки и приема данных, счетчики, опции и несколько служебных атрибутов:
struct sock
{
// Структура, которая содержит адреса, порт и параметры семейства,
// некоторые общие параметры, такие как состояние.
struct sock_common __sk_common;
// Общепринятая практика в C: sock.sk_node – то же самое,
// что и sock.__sk_common.skc_node, но более кратко.
#define sk_node __sk_common.skc_node
#define sk_nulls_node __sk_common.skc_nulls_node
#define sk_refcnt __sk_common.skc_refcnt
#define sk_tx_queue_mapping __sk_common.skc_tx_queue_mapping
#ifdef CONFIG_SOCK_RX_QUEUE_MAPPING
#define sk_rx_queue_mapping __sk_common.skc_rx_queue_mapping
#endif
#define sk_dontcopy_begin __sk_common.skc_dontcopy_begin
#define sk_dontcopy_end __sk_common.skc_dontcopy_end
...
#define sk_prot __sk_common.skc_prot
#define sk_net __sk_common.skc_net
...
// Счетчик отброшенных пакетов raw/udp.
atomic_t sk_drops;
// Смещение, по которому возможно прочитать данные в буфере сокета.
// Текущее значение peek_offset используется для работы SO_PEEK_OFF.
__s32 sk_peek_off;
// Очередь пакетов ошибок ICMP.
struct sk_buff_head sk_error_queue;
// Очередь входящих пакетов.
struct sk_buff_head sk_receive_queue;
Следующий атрибут нужен для ускорения доступа. Это очередь невыполненной работы:
// Очередь невыполненной работы используется с удерживаемой
// спин-блокировкой для каждого сокета и требует доступа с малой
// задержкой.
// rmem_alloc находится в этой структуре,
// чтобы заполнить "дыру" на 64-битных архитектурах.
struct
{
atomic_t rmem_alloc;
int len;
struct sk_buff *head;
struct sk_buff *tail;
} sk_backlog;
#define sk_rmem_alloc sk_backlog.rmem_alloc
В этой же структуре содержится поле индекса интерфейса, к которому привязан сокет, флаги, а также используемые для фильтрации трафика структуры:
...
/* Поля для раннего демультиплексирования */
// Получает входной маршрут, используемый демультиплексором.
struct dst_entry __rcu *sk_rx_dst;
// Индекс интерфейса ifindex для @sk_rx_dst
int sk_rx_dst_ifindex;
// Куки для @sk_rx_dst
u32 sk_rx_dst_cookie;
// Флаги SO_SNDBUF и SO_RCVBUF.
u8 sk_userlocks;
// Размер буфера приема в байтах.
int sk_rcvbuf;
// Инструкции фильтрации для сокета.
struct sk_filter __rcu *sk_filter;
union
{
// Очередь ожидания сокета и головной элемент списка, используемого
// асинхронными операциями.
struct socket_wq __rcu *sk_wq;
/* private: */
struct socket_wq *sk_wq_raw;
/* public: */
};
// Обратный вызов, вызываемый, когда есть данные для обработки.
void (*sk_data_ready)(struct sock *sk);
// Значение опции SO_RCVTIMEO.
int sk_rcvtimeo;
// Значение опции SO_RCVLOWAT.
int sk_rcvlowat;
Кэши, атрибуты, содержащие ошибки, а также атомарные переменные, которые используются для того, чтобы обеспечить управление доступом к этим переменным:
// Код последней ошибки.
int sk_err;
// Обратная ссылка на сокет.
struct socket *sk_socket;
// Ассоциированная с памятью сокета контрольная группа.
struct mem_cgroup *sk_memcg;
// Переменная для синхронизации.
socket_lock_t sk_lock;
// Пространство, зарезервированное для сокета.
u32 sk_reserved_mem;
// Предварительно выделенная память, доступная в квоте сокета.
int sk_forward_alloc;
// Флаги SO_TIMESTAMPING.
u16 sk_tsflags;
// Ожидающие запросы на запись в потоковый сокет.
int sk_write_pending;
// Счетчик очереди пакетов, которые пришли вне очереди.
atomic_t sk_omem_alloc;
// Размер буфера на отправку в байтах.
int sk_sndbuf;
// Постоянный размер очереди передачи.
int sk_wmem_queued;
// Подтвержденное число байтов в очереди передачи.
refcount_t sk_wmem_alloc;
Атрибут sk_socket — это обратная ссылка на структуру socket, в которой содержится данный экземпляр структуры sock.
Это нужно, так как в некоторых случаях требуется обратная ссылка на сторону приложения для обработки, например SIGIO. Характерный пример такого приложения — демон inetd.
Структура не вполне соответствует принципу разделения данных, и потому в ней можно увидеть, например, атрибуты, обычно используемые TCP:
// Флаги TCP Small Queues — механизма, предотвращающего раздувание
// буферов сокета путем ограничения числа сегментов, которые могут быть
// поставлены в очередь для передачи.
unsigned long sk_tsq_flags;
union
{
// Начало передаваемых данных.
struct sk_buff *sk_send_head;
// Очередь повторной передачи TCP.
struct rb_root tcp_rtx_queue;
};
// Очередь отправляемых пакетов.
struct sk_buff_head sk_write_queue;
// Замена более раннего атрибута dst_confirm, подтверждающего полученные
// пакеты. Нужен для подтверждения соседей.
u32 sk_dst_pending_confirm;
// Состояние планировщика: SK_PACING_NONE, SK_PACING_NEEDED, SK_PACING_FQ.
// Влияет на скорость отправки потока, связан с параметром
// sk_pacing_rate. Подробнее — в описании параметра maxrate утилиты tc.
u32 sk_pacing_status;
// Фрагмент страницы кэша.
struct page_frag sk_frag;
// Таймер очистки сокета.
struct timer_list sk_timer;
Поле sk_dst_pending_confirm нужно для работы «подсистемы соседей», которая используется для поиска соседних узлов, заполнения аппаратных заголовков и кэширования.
Некоторые опции, описанные далее, флаги, метки пакетов и настройки аллокаторов памяти и различных планировщиков, устанавливаемые через функцию setsockopt(), а иногда через ioctl(), также представляют собой «обычные атрибуты» данной структуры:
// Скорость передачи в байтах в секунду.
// Если поддерживается планировщиком транспорта/пакетов.
unsigned long sk_pacing_rate;
// Счетчик для устранения неоднозначности одновременных tstamp-запросов.
atomic_t sk_tskey;
// Счетчик для упорядочения уведомлений MSG_ZEROCOPY.
atomic_t sk_zckey;
// Значение опции SO_MAX_PACING_RATE.
unsigned long sk_max_pacing_rate;
// Значение SO_SNDTIMEO.
long sk_sndtimeo;
// Значение приоритета SO_PRIORITY.
u32 sk_priority;
// Общая метка пакета.
u32 sk_mark;
// Кэш назначения.
struct dst_entry __rcu *sk_dst_cache;
// Возможности маршрута, например NETIF_F_TSO. Буквально возможности,
// которые поддерживает сетевой адаптер, например GRO или расчет CRC.
netdev_features_t sk_route_caps;
// Тип GSO, например SKB_GSO_TCPV4.
int sk_gso_type;
// Максимальное число сегментов GSO.
u16 sk_gso_max_segs;
// Максимальный размер сегмента GSO для построения.
unsigned int sk_gso_max_size;
// Флаги выделения памяти для буферов, например GFP_ATOMIC.
gfp_t sk_allocation;
// Вычисленный хеш потока, используемый для передачи.
u32 sk_txhash;
// Коэффициент масштабирования для маленьких TCP-очередей — TSQ.
u8 sk_pacing_shift;
// Разрешить sk_page_frag() использовать атрибут сокета task_frag.
// Сокеты, которые можно использовать при освобождении памяти,
// должны установить значение false. Это сокеты, которые не используются
// сетевыми блочными устройствами, файловыми системами и т.п.
bool sk_use_task_frag;
// Если флаг установлен, NETIF_F_GSO_MASK запрещена.
u8 sk_gso_disabled : 1,
// Истина, если сокет использует классы блокировки ядра.
sk_kern_sock : 1,
// Опция SO_NO_CHECK, установка контрольной суммы
// в отправляемых пакетах.
sk_no_check_tx : 1,
// Разрешить нулевую контрольную сумму в принимаемых пакетах.
sk_no_check_rx : 1;
Данными этой структуры пользуются алгоритмы сетевой подсистемы, реализованные внутри ядра.
Именно в sock будут установлены семейство протоколов и тип сокета, которые задаются как параметры вызова функции socket(). Тут же сохраняется и значение параметра backlog, переданного функции listen():
// Маска SEND_SHUTDOWN и RCV_SHUTDOWN.
u8 sk_shutdown;
// Тип сокета, например SOCK_STREAM.
u16 sk_type;
// Протокол сокета в этом сетевом семействе.
u16 sk_protocol;
// Значение опции SO_LINGER.
unsigned long sk_lingertime;
// sk_prot создателя сокетов, например IPV6_ADDRFORM.
struct proto *sk_prot_creator;
// Блокировка, используемая с обратными вызовами в конце структуры.
rwlock_t sk_callback_lock;
// Ошибки, которые не приводят к сбою, но являются причиной
// постоянного сбоя, а не просто истечением времени ожидания.
int sk_err_soft;
// Текущий бэклог для listen, то есть количество входящих соединений.
u32 sk_ack_backlog;
// Значение параметра backlog, переданное в функцию listen().
u32 sk_max_ack_backlog;
// UID владельца сокета.
kuid_t sk_uid;
Некоторые поля структур могут дублироваться. Например, поле типа сокета присутствует как в структуре socket, которая была рассмотрена перед этим, так и в структуре sock. Это вполне нормальная практика. Структура sock более старая и является частью многих других структур.
Дальнейшую часть структуры мы пропустим. Она содержит опции SO_TIMESTAMPING, SO_PEERCRED, SO_TXREHASH, параметры контрольной группы, некоторые флаги сокета и т.п.:
// Блокировка, защищающая атрибуты sk_peer_pid и sk_peer_cred.
spinlock_t sk_peer_lock;
// SO_TIMESTAMPING связывает индекс PHC виртуальных часов PTP
// для отметки времени.
int sk_bind_phc;
// PID для этого конца сокета.
struct pid *sk_peer_pid;
// Значение опции SO_PEERCRED.
const struct cred *sk_peer_cred;
// Временная метка последнего принятого пакета.
ktime_t sk_stamp;
// Количество операций отключения, выполненных на этом сокете.
int sk_disconnects;
// Включить пересчет хеша TX. Управляется SO_TXREHASH.
u8 sk_txrehash;
// Идентификатор часов, используемый для time-based-планировщика
// (планировщика по времени).
// Опция SO_TXTIME.
u8 sk_clockid;
// Установить deadline-режим для SO_TXTIME.
u8 sk_txtime_deadline_mode : 1,
// Установить режим уведомления об ошибках для SO_TXTIME.
sk_txtime_report_errors : 1,
// Неиспользуемые флаги txtime.
sk_txtime_unused : 6;
// Закрытые данные уровня RPC.
void *sk_user_data;
// Данные cgroup для контрольной группы.
struct sock_cgroup_data sk_cgrp_data;
Для понимания общей картины эти детали нам не пригодятся, они требуются лишь при разработке на уровне ядра.
Итак, вы получили представление о том, как устанавливаются опции на самом низком уровне и к чему приводят вызовы setsockopt() и ioctl().
Последним, на что стоит обратить внимание, являются указатели на функции обратного вызова, которые используются для информирования о событиях, происходящих на сокете:
// Вызывается при изменении состояния сокета.
void (*sk_state_change)(struct sock *sk);
// Вызывается при наличии данных, готовых для обработки.
void (*sk_data_ready)(struct sock *sk);
// Вызывается, если в буфере передачи доступно место.
void (*sk_write_space)(struct sock *sk);
// Вызывается при наличии ошибок, например MSG_ERRQUEUE.
void (*sk_error_report)(struct sock *sk);
// Вызывается при обработке бэклога.
int (*sk_backlog_rcv)(struct sock *sk, struct sk_buff *skb);
// Вызывается во время освобождения сокета, то есть когда на него нет
// ссылок.
void (*sk_destruct)(struct sock *sk);
// Контейнер группы reuseport, содержащий параметры для SO_REUSEPORT.
struct sock_reuseport __rcu *sk_reuseport_cb;
// Используется в течение RCU grace period, в котором ожидается,
// что все клиенты очереди будут отключены.
struct rcu_head sk_rcu;
// Число ссылок на сетевой неймспейс. Требуется для того, чтобы
// неймспейс не разрушился, пока в нем есть сокеты.
netns_tracker ns_tracker;
};
Фактически сокет в ядре — это «объект», за исключением того, что он весь реализован на чистом C. Указатели на функции используются внутри ядра для вызова соответствующих обработчиков.
Адреса и, если требуется, порты хранятся в отдельной структуре, «заголовке» в структуре sock. Тип заголовка — это структура sock_common:
typedef __u32 __bitwise __portpair;
typedef __u64 __bitwise __addrpair;
// Минимальное представление сетевого уровня для сокетов.
// Заголовок для структур sock и inet_timewait_sock.
struct sock_common
{
union
{
// Выровненное объединение skc_daddr и skc_rcv_saddr.
__addrpair skc_addrpair;
struct
{
// IPv4-адрес источника.
__be32 skc_daddr;
// Связанный локальный IPv4-адрес.
__be32 skc_rcv_saddr;
};
};
union
{
// Значение хеша, используемое с таблицами поиска протоколов.
unsigned int skc_hash;
// Два u16 со значениями хеша, используемые в таблицах поиска UDP.
__u16 skc_u16hashes[2];
};
// Сгруппированные skc_dport и skc_num.
union
{
// __u32-объединение skc_dport и skc_num.
__portpair skc_portpair;
struct
{
// Плейсхолдер для inet_dport/tw_dport
__be16 skc_dport;
// Плейсхолдер для inet_num/tw_num
__u16 skc_num;
};
};
// Семейство адресов.
unsigned short skc_family;
Некоторые флаги также хранятся в данной структуре:
// Состояние соединения.
volatile unsigned char skc_state;
// Значение параметра SO_REUSEADDR.
unsigned char skc_reuse:4;
// Значение параметра SO_REUSEPORT.
unsigned char skc_reuseport:1;
// Флаг указывает, что это исключительно IPV6-сокет.
unsigned char skc_ipv6only:1;
// Сокет использует подсчет ссылок net.
unsigned char skc_net_refcnt:1;
// Индекс связанного устройства, если не равен 0.
int skc_bound_dev_if;
Оставшаяся часть нам интересна полем skc_prot. Это структура, которая вносит особенности транспортных протоколов в сокет. Как это делается, будет рассмотрено в следующем разделе.
Структура может содержать IPv6-адреса, а также пространство для флагов и куки сокета:
// Обработчики протоколов сетевого уровня.
struct proto *skc_prot;
// Ссылка на сетевой неймспейс для этого сокета.
possible_net_t skc_net;
#if IS_ENABLED(CONFIG_IPV6)
// IPV6-адрес назначения.
struct in6_addr skc_v6_daddr;
// IPV6-адрес источника.
struct in6_addr skc_v6_rcv_saddr;
#endif
// Значение куки сокета.
atomic64_t skc_cookie;
// Поля используются как заполнение для 64-битных архитектур.
union
{
// Плейсхолдер для sk_flags SO_LINGER (l_onoff),
// SO_BROADCAST, SO_KEEPALIVE, SO_OOBINLINE,
// SO_TIMESTAMPING.
unsigned long skc_flags;
// Сокет прослушивателя запроса на соединение (rsk_listener).
struct sock *skc_listener; /* request_sock */
// Указатель на &struct inet_timewait_death_row.
struct inet_timewait_death_row *skc_tw_dr; /* inet_timewait_sock */
};
Прочие атрибуты — это номера очередей для TCP, привязка к CPU, счетчик ссылок:
// Номер очереди передачи для этого соединения.
unsigned short skc_tx_queue_mapping;
#ifdef CONFIG_SOCK_RX_QUEUE_MAPPING
// Номер очереди приема для этого соединения.
unsigned short skc_rx_queue_mapping;
#endif
union
{
// Привязка к процессору, обрабатывающему входящие пакеты.
int skc_incoming_cpu;
// Размер окна приема TCP (может изменяться, rsk_rcv_wnd).
u32 skc_rcv_wnd;
// Номер следующего ожидаемого сегмента в окне TCP (tw_rcv_nxt).
u32 skc_tw_rcv_nxt; /* struct tcp_timewait_sock */
};
// Количество ссылок на сокет.
refcount_t skc_refcnt;
// Далее идут закрытые поля, которые тут не показаны.
// В них хранятся, например, служебные хеши.
Видим, что структура содержит некоторые специфичные для конкретных протоколов атрибуты, хотя и является общей.
Ядро создается множеством разработчиков, старые и простые решения постепенно заменяются более новыми, однако важно сохранять их совместимость. В результате атрибуты постепенно переходят в другие структуры.
В части структуры, которую мы не показали, содержатся служебные данные: хеши, служебные поля, определяющие порядок копирования, списки хешей для обработчиков протоколов и др.
Многие вызовы setsockopt() просто изменяют данные параметры, делая запросы к ядру.
Структура proto, которую хранит sk_prot в структуре sock_common, является главным связующим звеном между общими структурами и структурами для конкретных протоколов.
Помимо некоторых служебных атрибутов, основное содержимое этой структуры — указатели на функции:
struct proto
{
void (*close)(struct sock *sk, long timeout);
// Подключение и отключение сокета.
int (*pre_connect)(struct sock *sk, struct sockaddr *uaddr,
int addr_len);
int (*connect)(struct sock *sk, struct sockaddr *uaddr, int addr_len);
int (*disconnect)(struct sock *sk, int flags);
struct sock* (*accept)(struct sock *sk, int flags, int *err, bool kern);
// Установка некоторых опций.
int (*ioctl)(struct sock *sk, int cmd, unsigned long arg);
// Создание и уничтожение сокета (конструктор и деструктор).
int (*init)(struct sock *sk);
void (*destroy)(struct sock *sk);
void (*shutdown)(struct sock *sk, int how);
// Установка и получение опций.
int (*setsockopt)(struct sock *sk, int level, int optname,
sockptr_t optval, unsigned int optlen);
int (*getsockopt)(struct sock *sk, int level, int optname,
char __user *optval, int __user *option);
void (*keepalive)(struct sock *sk, int valbool);
Мы видим как уже известные названия — эти функции будут вызваны системными вызовами, — так и неизвестные, такие как keepalive.
Функция, адрес которой записан в указатель keepalive, например, может вызываться ядром, чтобы проверить, работоспособно ли соединение.
Для системных вызовов, отвечающих за передачу данных, также есть свои указатели на функции:
#ifdef CONFIG_COMPAT
int (*compat_ioctl)(struct sock *sk, unsigned int cmd,
unsigned long arg);
#endif
// Отправка и получение данных.
int (*sendmsg)(struct sock *sk, struct msghdr *msg, size_t len);
int (*recvmsg)(struct sock *sk, struct msghdr *msg, size_t len,
int flags, int *addr_len);
int (*sendpage)(struct sock *sk, struct page *page, int offset,
size_t size, int flags);
int (*bind)(struct sock *sk, struct sockaddr *addr, int addr_len);
...
Данная структура содержит «методы» обработки сокета. Эти методы различаются для разных типов сокетов и применяются к различным экземплярам структур, которые передаются в параметре sk, по сути — this, если провести аналогию с C++.
Сами параметры могут быть структурами разных типов. Но все они содержат в начале структуру sock, которую можно называть «базовым классом».
Эта структура используется для регистрации функций, создающих необходимые структуры для семейства адресов:
struct net_proto_family
{
// Семейство адресов.
int family;
// Вызов для создания нового сокета.
int (*create)(net *net, struct socket *sock, int protocol, int kern);
// Модуль-владелец, поддерживающий семейство протоколов.
struct module *owner;
};
...
// Список семейств.
static const struct net_proto_family __rcu *net_families[NPROTO];
Видно, что существует список семейств — net_families, куда могут добавляться новые семейства — как ядром, так и модулями. Для их добавления используется функция sock_register().
Любой интернет-сокет, работающий поверх IP, описан структурой inet_sock, определенной в net/inet_sock.h.
Видим, что в заголовке он включает базовую структуру sock и несколько макросов для работы с ней:
// struct inet_sock — представление INET-сокетов.
struct inet_sock
{
// "Класс"-предок.
struct sock sk;
#if IS_ENABLED(CONFIG_IPV6)
// Указатель на блок управления IPv6.
struct ipv6_pinfo *pinet6;
#endif
// Удаленный IPv4-адрес.
#define inet_daddr sk.__sk_common.skc_daddr
// Связанный локальный IPv4-адрес.
#define inet_rcv_saddr sk.__sk_common.skc_rcv_saddr
// Порт назначения.
#define inet_dport sk.__sk_common.skc_dport
// Локальный порт.
#define inet_num sk.__sk_common.skc_num
В этой же структуре хранятся различные адреса, порты, значения TTL и другие параметры, специфичные для большинства протоколов, работающих поверх IP:
// Атомарные флаги: IP_RECVERR, отложенного соединения, того, что
// сокет ориентирован на соединение, а также флаги управляющих данных.
unsigned long inet_flags;
// Адрес отправителя.
__be32 inet_saddr;
// TTL для unicast-передачи.
__s16 uc_ttl;
// Порт источника.
__be16 inet_sport;
// Структура, которая содержит опции заголовка IP.
struct ip_options_rcu __rcu *inet_opt;
// Индикатор счетчика для пакетов DF, то есть таких, которые
// не фрагментируются.
__u16 inet_id;
// Поле IP-заголовка Type Of Service.
__u8 tos;
// Минимальный TTL.
__u8 min_ttl;
// TTL многоадресной рассылки.
__u8 mc_ttl;
// Состояние механизма Path MTU discovery.
// Принимает разные значения: IP_PMTUDISC_DO, IP_PMTUDISC_WANT,
// IP_PMTUDISC_OMIT, IP_PMTUDISC_INTERFACE, IP_PMTUDISC_PROBE.
__u8 pmtudisc;
Структура в конце содержит флаги, индексы сетевых интерфейсов, а также другую служебную информацию:
// ToS, принятый от удаленного абонента.
__u8 rcv_tos;
// Используется для расчета контрольной суммы заголовка.
__u8 convert_csum;
// Индекс устройства для одноадресной передачи.
int uc_index;
// Индекс устройства для многоадресной передачи.
int mc_index;
// Адрес для многоадресной рассылки.
__be32 mc_addr;
// Значение опции IP_LOCAL_PORT_RANGE.
u32 local_port_range;
// Массив групп многоадресной передачи.
struct ip_mc_socklist __rcu *mc_list;
// Информация для построения ip hdr для каждого IP-фрагмента,
// сокета, на котором включен алгоритм Нейгла.
struct inet_cork_full cork;
};
Данная структура, «унаследованная» от sock, является «базовым классом» для протоколов, работающих в стеке TCP/IP. Схема наследования показана на рис. 7.1.
Рис. 7.1. Отношения между структурами Internet-сокета
Сокеты транспортных протоколов содержат структуру inet_sock внутри своей, что хорошо видно на примере UDP:
struct udp_sock
{
// inet_sock — описание сокета IPv4.
struct inet_sock inet;
// Макросы для удобного доступа к полям.
#define udp_port_hash inet.sk.__sk_common.skc_u16hashes[0]
#define udp_portaddr_hash inet.sk.__sk_common.skc_u16hashes[1]
#define udp_portaddr_node inet.sk.__sk_common.skc_portaddr_node
// Флаги UDP.
unsigned long udp_flags;
// Количество ожидающих кадров.
int pending;
// Тип инкапсуляции, если она используется.
__u8 encap_type;
/*
* Следующие поля требуются для создания UDP-заголовка, когда
* сокет имеет снятый флаг UDP_CORK.
* В этом случае данные не объединяются в одну дейтаграмму.
*/
// Полный размер ожидающих кадров.
__u16 len;
__u16 gso_size;
// Дальше идут поля, специфичные для UDP Lite, указатели на функции
// для туннелированных сокетов и пр.
...
};
Опции для UDP представляют собой перечисление:
enum
{
// Включен флаг UDP_CORK.
UDP_FLAGS_CORK,
// Отправлять нулевые контрольные суммы UDP6.
UDP_FLAGS_NO_CHECK6_TX,
// Разрешено принимать нулевые контрольные суммы.
UDP_FLAGS_NO_CHECK6_RX,
// Включен Generic Receive Offload,
// то есть обработка прямо на сетевом устройстве.
UDP_FLAGS_GRO_ENABLED,
UDP_FLAGS_ACCEPT_FRAGLIST,
UDP_FLAGS_ACCEPT_L4,
// На сокете включена инкапсуляция, например, для туннелирования.
UDP_FLAGS_ENCAP_ENABLED,
// Опции для расчета контрольных сумм UDP Lite.
UDP_FLAGS_UDPLITE_SEND_CC,
UDP_FLAGS_UDPLITE_RECV_CC,
};
Флаги также перечислены в структуре sock_shared, но там мы это перечисление не показывали.
Структура proto для обработчиков UDP инициализируется следующим образом:
struct proto udp_prot =
{
.name = "UDP",
.owner = THIS_MODULE,
.close = udp_lib_close,
.pre_connect = udp_pre_connect,
// UDP поверх IPv6 — другой протокол, UDP6, который описывается
// структурой udpv6_prot.
.connect = ip4_datagram_connect,
.disconnect = udp_disconnect,
// Сюда перенаправляется вызов ioctl() для UDP.
.ioctl = udp_ioctl,
// Эта функция инициализирует сокет (struct sock).
.init = udp_init_sock,
.destroy = udp_destroy_sock,
// Функции, которые обслуживают API для установки и получения опций
// сокета.
.setsockopt = udp_setsockopt,
.getsockopt = udp_getsockopt,
// Обмен данными осуществляется через sendmsg()/recvmsg().
.sendmsg = udp_sendmsg,
.recvmsg = udp_recvmsg,
//
.sendpage = udp_sendpage,
.release_cb = ip4_datagram_release_cb,
.hash = udp_lib_hash,
.unhash = udp_lib_unhash,
.rehash = udp_v4_rehash,
.get_port = udp_v4_get_port,
.put_port = udp_lib_unhash,
...
.obj_size = sizeof(struct udp_sock),
.h.udp_table = NULL,
.diag_destroy = udp_abort,
};
Например, поле sendmsg() в структуре udp_prot ссылается на функцию udp_sendmsg(), которая формирует UDP-дейтаграмму:
int udp_sendmsg(struct sock *sk, struct msghdr *msg, size_t len);
Функция updv6_sendmsg(), которая используется для формирования UDPv6-дейтаграммы, в определенных случаях вызывает udp_sendmsg().
Но с точки зрения сетевого стека в ядре Linux UDPv4 и UDPv6 — разные протоколы.
Функция достаточно объемная. При желании ее можно посмотреть в исходном коде ядра в файле net/ipv4/udp.c.
А вот протокол UDP Lite, хотя и описан структурой udplite_prot, использует большинство функций из протокола UDP, то есть вызов «методов предка», когда это требуется, может выполняться явно. Однако есть и другой механизм, который мы рассмотрим чуть позже.
Для приема данных используется поле recvmsg, которое в экземплярах udp_prot инициализируется адресом функции udp_recvmsg().
В tcp_prot эти поля инициализируются адресами функций tcp_sendmsg() и tcp_recvmsg() соответственно:
struct proto tcp_prot =
{
.name = "TCP",
.owner = THIS_MODULE,
.close = tcp_close,
.pre_connect = tcp_v4_pre_connect,
.connect = tcp_v4_connect,
.disconnect = tcp_disconnect,
.accept = inet_csk_accept,
.ioctl = tcp_ioctl,
.init = tcp_v4_init_sock,
.destroy = tcp_v4_destroy_sock,
.shutdown = tcp_shutdown,
.setsockopt = tcp_setsockopt,
.getsockopt = tcp_getsockopt,
...
.diag_destroy = tcp_abort,
};
Теперь, когда мы рассмотрели часть структур, начнем разбираться, как работает стек, — в основном на примере интернет-протоколов.
В функции inet_init(), которая вызывается при инициализации сетевой подсистемы ядра, вызывается функция proto_register(), создающая в памяти экземпляры данных структур для каждого протокола:
static int __init inet_init(void)
{
struct inet_protosw *q;
struct list_head *r;
int rc;
// Регистрация протоколов.
...
rc = proto_register(&tcp_prot, 1);
if (rc) goto out;
rc = proto_register(&udp_prot, 1);
if (rc) goto out_unregister_tcp_proto;
rc = proto_register(&raw_prot, 1);
...
rc = proto_register(&ping_prot, 1);
// Регистрация AF_INET-семейства.
(void)sock_register(&inet_family_ops);
...
Видим, что после регистрации протоколов выполняется регистрация семейства адресов. Для AF_INET обработчиком создания нового сокета является функция inet_create():
static const struct net_proto_family inet_family_ops =
{
.family = PF_INET,
// Функция для создания нового сокета.
.create = inet_create,
.owner = THIS_MODULE,
};
Затем описатели протоколов добавляются в массив с помощью следующей функции:
struct net_protocol __rcu *inet_protos[MAX_INET_PROTOS];
...
int inet_add_protocol(const struct net_protocol *prot, unsigned char protocol)
{
return !cmpxchg((const struct net_protocol **)&inet_protos[protocol],
NULL, prot) ? 0 : -1;
}
В отличие от далее описанных функций эти обработчики вызываются не «сверху вниз» из пользовательского пространства, а «снизу вверх», нижележащим уровнем стека, при наступлении какого-либо события:
struct net_protocol
{
// Обработчик SDU протокола нижележащего уровня.
int (*handler)(struct sk_buff *skb);
// Возвращает ошибку, если не получается ее устранить.
int (*err_handler)(struct sk_buff *skb, u32 info);
unsigned int no_policy:1,
icmp_strict_tag_validation:1;
u32 secret;
};
В функции inet_init() добавление выглядит так:
// Добавление базовых протоколов.
// ICMP.
if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
pr_crit("%s: Cannot add ICMP protocol\n", __func__);
// UDP.
net_hotdata.udp_protocol = (struct net_protocol)
{
// Обработчик, который получает SDU, то есть IP-пакет
.handler = udp_rcv,
.err_handler = udp_err,
.no_policy = 1,
};
if (inet_add_protocol(&net_hotdata.udp_protocol, IPPROTO_UDP) < 0)
pr_crit("%s: Cannot add UDP protocol\n", __func__);
// TCP.
net_hotdata.tcp_protocol = (struct net_protocol)
{
.handler = tcp_v4_rcv,
.err_handler = tcp_v4_err,
.no_policy = 1,
.icmp_strict_tag_validation = 1,
};
if (inet_add_protocol(&net_hotdata.tcp_protocol, IPPROTO_TCP) < 0)
pr_crit("%s: Cannot add TCP protocol\n", __func__);
Процесс добавления изображен на рис. 7.2. Каждый описатель в этом массиве содержит указатель на обработчики SDU и ошибки.
Завершается функция inet_init() вызовом инициализации протоколов:
...
// Регистрация информации сокета для inet_create().
for (r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++r)
INIT_LIST_HEAD(r);
for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
inet_register_protosw(q);
// Инициализация ARP. Создается кэш соседей. Регистрируются свойства
// в SysFS.
arp_init();
// Инициализация IP. Настраивается маршрутизация.
ip_init();
// Инициализация IPv4 mibs для каждого процессора.
if (init_ipv4_mibs()) panic("%s: Cannot init ipv4 mibs\n", __func__);
// Установка кэша TCP slab, настройка таймеров, умолчаний и т.п.
tcp_init();
// Установка ограничений памяти для UDP.
udp_init();
udplite4_register();
raw_init();
ping_init();
...
if (icmp_init() < 0)
panic("Failed to create the ICMP control socket.\n");
...
// Добавление параметров в ProcFS.
ipv4_proc_init();
...
}
Это настройка разных параметров, специфичных для конкретных протоколов, что выходит за рамки рассматриваемой темы.
Обработчик udp_recv(), формирующий PDU, должен получить заголовок пакета, рассчитать и проверить контрольную сумму пакета и вернуть дейтаграмму, если не было ошибок. Он вызывается функцией ip_local_deliver_finish(), которая заканчивает обработку PDU сетевого уровня, в данном случае IPv4.
Обработчик ошибки должен вернуть ее корректный код.
Но даже в случае UDP эти обработчики достаточно сложны: они могут отправлять сообщения ICMP, работать с памятью и т.п.
Рис. 7.2. Регистрация обработчиков
Структура inet_protosw объединяет свойства протокола и его операции.
Для каждой из структур в массиве inetsw_array в функции inet_init(), разной для каждого семейства адресов, будет вызвана функция регистрации inet_register_protosw().
В массиве сначала описаны транспортные протоколы:
/* При запуске мы вставляем все элементы из inetsw_array[] в
* связанный список inetsw.
*/
static struct inet_protosw inetsw_array[] =
{
{
.type = SOCK_STREAM,
.protocol = IPPROTO_TCP,
// Операции транспортного протокола, описанные выше.
.prot = &tcp_prot,
// Операции нижележащего сетевого протокола, в данном случае IPv4.
// Например, connect здесь будет установлен в адрес
// inet_stream_connect().
.ops = &inet_stream_ops,
.flags = INET_PROTOSW_PERMANENT | INET_PROTOSW_ICSK,
},
{
.type = SOCK_DGRAM,
.protocol = IPPROTO_UDP,
.prot = &udp_prot,
// Здесь connect будет установлен в адрес inet_dgram_connect().
.ops = &inet_dgram_ops,
.flags = INET_PROTOSW_PERMANENT,
},
Затем служебные — протокол ICMP и raw-протокол, то есть raw-сокеты без транспортного протокола:
{
.type = SOCK_DGRAM,
.protocol = IPPROTO_ICMP,
.prot = &ping_prot,
.ops = &inet_sockraw_ops,
.flags = INET_PROTOSW_REUSE,
},
{
.type = SOCK_RAW,
.protocol = IPPROTO_IP,
.prot = &raw_prot,
.ops = &inet_sockraw_ops,
.flags = INET_PROTOSW_REUSE,
}
};
Протоколов UDP Lite или протокола IGMP в этой структуре нет. Они добавляются отдельно. Для UDP Lite вызывается функция updlite4_register(), которая внутри себя вызывает proto_register(), inet_add_protocol() и inet_register_protosw().
Структура inet_protosw достаточно проста:
// Используется для регистрации сокетных интерфейсов IP-стека.
struct inet_protosw
{
// Структура — элемент списка.
struct list_head list;
// Два поля ниже используются для поиска.
// Второй аргумент сокета.
unsigned short type;
// Номер протокола транспортного уровня.
unsigned short protocol;
// Операции протокола.
struct proto* prot;
// Операции нижележащего протокола.
const struct proto_ops* ops;
// Флаги протокола: невозможность его удалить (выгрузить модуль),
// возможность переиспользовать порты протокола автоматически,
// ориентация на соединение.
unsigned char flags;
};
Единственный атрибут, который требует пояснения, — ops. Он содержит указатель на операции нижележащего уровня:
struct proto_ops
{
int family;
struct module* owner;
int (*release)(socket *sock);
int (*bind)(socket *sock, sockaddr *myaddr, int sockaddr_len);
int (*connect)(socket *sock, struct sockaddr *vaddr,
int sockaddr_len, int flags);
int (*socketpair)(socket *sock1, struct socket *sock2);
int (*accept)(socket *sock, struct socket *newsock, int flags, bool kern);
int (*getname) (socket *sock, struct sockaddr *addr, int peer);
__poll_t (*poll)(file *file, struct socket *sock,
poll_table_struct *wait);
int (*ioctl)(socket *sock, unsigned int cmd, unsigned long arg);
...
int (*sendmsg)(socket *sock, msghdr *m, size_t total_len);
int (*recvmsg)(socket *sock, msghdr *m, size_t total_len, int flags);
int (*mmap)(file *file, socket *sock, vm_area_struct * vma);
ssize_t (*sendpage)(socket *sock, page *page, int offset, size_t size,
int flags);
...
};
Для транспортных протоколов, UDP и TCP, это будет уровень IP, а также указатели на функции, которые реализованы для конкретного протокола. Например, для TCP-сокета вызов poll() свой.
Для сокетного уровня это интерфейс системных вызовов. Когда пользователь выполняет системный вызов, сначала вызывается функция из этой структуры, а затем, как правило, функция из структуры prot.
Пример структуры inet_stream_ops:
const struct proto_ops inet_stream_ops =
{
.family = PF_INET,
.owner = THIS_MODULE,
.release = inet_release,
.bind = inet_bind,
.connect = inet_stream_connect,
.socketpair = sock_no_socketpair,
.accept = inet_accept,
.getname = inet_getname,
.poll = tcp_poll,
.ioctl = inet_ioctl,
.gettstamp = sock_gettstamp,
.listen = inet_listen,
.shutdown = inet_shutdown,
.setsockopt = sock_common_setsockopt,
.getsockopt = sock_common_getsockopt,
.sendmsg = inet_sendmsg,
.recvmsg = inet_recvmsg,
...
}
Вызов функции socket() в Linux приводит к системному вызову sys_socket(), запрашивая у ядра создание новой структуры для сокета и привязывая ее к определенному дескриптору в запросившем создание процессе.
Сначала осуществляется вызов функции __sys_socket_create(), которая устанавливает некоторые флаги и вызывает функцию __sock_create(). Внутри нее создается структура сокета и его файловый дескриптор, что подробнее описано в главе 13.
Далее сетевой стек создает описанные выше структуры, которые специфичны для семейства протоколов, используя вызов create() структуры net_protofamily, выбранной из массива net_families по индексу семейства:
pf = rcu_dereference(net_families[family]);
...
err = pf->create(net, sock, protocol, kern);
Для AF_INET это будет функция inet_create(), которая делает следующее:
1. Вызывает sk_alloc() — функцию, создающую все сокетные объекты для протокола.
2. Вызывает sock_init_data(), инициализирующую различные структуры.
3. Вызывает sk->sk_prot->init() для завершения инициализации структур протокола.
4. Выполняет множество служебных действий, таких как установка некоторых флагов и умолчаний.
Функция sk_alloc() использует вызовы из структуры proto для создания, а также инициализации нужных для работы протокола структур:
struct sock *sk_alloc(net *net, int family, gfp_t priority, proto *prot,
int kern)
{
struct sock *sk;
// Выделение памяти для структур протокола.
sk = sk_prot_alloc(prot, priority | __GFP_ZERO, family);
if (sk)
{
sk->sk_family = family;
// Вызов функции для создания необходимых протоколу структур.
sk->sk_prot = sk->sk_prot_creator = prot;
sk->sk_kern_sock = kern;
...
// Установка пространства имен.
sock_net_set(sk, net);
// Затем установка контрольной группы и т.п.
...
// Создание и очистка очереди на передачу.
sk_tx_queue_clear(sk);
}
return sk;
}
Таким образом все структуры, нужные протоколу, созданы и правильно инициализированы, а также назначены обработчики, которые могут работать с данными протокола.
К структурам применяются вызовы из структур операций, выбираемых на основе протокола. На рис. 7.3 показаны структуры tcp_prot и udp_prot.
Рис. 7.3. Работа стека Linux
Например, вызовы приема и отправки данных заканчиваются функциями ip_rcv() или ip_output(), которые формируют дейтаграмму и вызывают функцию отправки данных устройством.
Запускаться функции могут системным вызовом, который выполняет приложение, или прерыванием, поступившим с устройства, принявшего данные, или, наоборот, готового к дальнейшей отправке.
С вызовами также связаны файлы в специальной файловой системе ProcFS.
«Снизу» сетевой подсистемы находятся драйверы сетевых адаптеров, которые представлены модулями ядра.
При загрузке такой модуль вызывает функцию register_netdevice(), передавая ей структуру net_device:
struct net_device
{
// Сначала идут параметры GSO, которые не показаны.
// Флаги интерфейса.
unsigned int flags;
// Внутренние флаги.
unsigned long long priv_flags;
// Операции.
const struct net_device_ops *netdev_ops;
// Обратные вызовы для создания, анализа, кэширования и т.п.
// заголовков уровня 2.
const struct header_ops *header_ops;
// Массив очередей передачи.
struct netdev_queue *_tx;
...
// MTU интерфейса.
unsigned int mtu;
...
// Индекс данного интерфейса.
int ifindex;
В этой структуре хранится большинство параметров устройств:
// Имя сетевого интерфейса.
char name[IFNAMSIZ];
// Имя узла хеш-списка.
struct netdev_name_node *name_node;
// SNMP-маркировка интерфейсов.
struct dev_ifalias __rcu *ifalias;
// Адреса для ввода-вывода DMA.
unsigned long mem_end;
unsigned long mem_start;
unsigned long base_addr;
// Устройство добавляется в различные списки:
// — Глобальный список устройств.
// — Список опроса NAPI.
struct list_head dev_list;
struct list_head napi_list;
// Нужно, чтобы добавить устройство в список для удаления,
// например, когда выгружается модуль.
struct list_head unreg_list;
// Список для устройств, ожидающих закрытия.
struct list_head close_list;
// Списки обработчиков различных протоколов для этого устройства.
struct list_head ptype_all;
struct list_head ptype_specific;
...
// Назначенный устройству номер прерывания.
int irq;
// Основная структура для описания устройства.
struct device dev;
...
// Структура, описывающая устройство в подсистеме PHY.
struct phy_device * phydev;
...
};
Ядро и драйвер сетевого устройства взаимодействуют через прерывание, вызываемое пришедшими или отправленными устройством данными. Они записываются в буфер — структуру sk_buff, которая нам уже не раз встречалась.
Прием и передачу данных мы уже кратко рассмотрели в главе 3. Когда данные приняты, устройство также генерирует прерывание. Вызванный им обработчик добавляет задачу планировщику. В задаче будет выполнен вызов функций конкретных протоколов стека.
В случае, когда данные переданы, также генерируется прерывание, сигнализирующее о завершении передачи.
Номер прерывания, который назначен устройству, тоже хранится как атрибут структуры net_device.
Мы привели структуру в очень сокращенном виде. Она помогает лучше понять некоторые особенности функций, описанные в следующих главах. Например, почему функция ioctl(), работая с устройством, все равно получает дескриптор сокета.
В ОС Linux вызов функции socket() приводит к системному вызову sys_socket(), запрашивая у ядра создание нового экземпляра структуры и привязывая его к определенному дескриптору в запросившем создание процессе.
Внутри ядра сетевой стек создает описанную структуру socket, которая является точкой входа для хранения информации о сокете и содержит еще несколько структур:
• socket — структура является представлением сокета в ядре и доступна системным вызовам.
• sock — внутреннее представление сокета, которое не зависит от протокола. Используется внутри ядра, ее детали могут изменяться в разных версиях ядра.
• sock_common — хранит адреса и порты.
• proto — содержит методы обработки сокета.
Структуры сокета в ядре Linux обладают высокой степенью сложности и гибкости, что позволяет им поддерживать широкий спектр сетевых протоколов и устройств. Они постоянно развиваются, что обеспечивает постепенное внедрение новых решений при сохранении обратной совместимости.
Любой интернет-сокет, работающий поверх IP, описан структурой inet_sock. Структуры транспортных протоколов содержат эту структуру внутри своей, то есть «наследуются» от сетевого протокола. И далее в этом сокете применяются вызовы, описанные в этих структурах.
При инициализации сетевой подсистемы ядра вызывается функция proto_register(), создающая экземпляры структур sock и proto для каждого протокола. Затем — функции inet_add_protocol() и inet_register_protosw(), которые инициализируют «методы» конкретных сетевых протоколов, добавляя их в массив inetsw_array. Этот массив содержит структуры inet_protosw — связующее звено между структурами протоколов и функциями их обработки.
Создание нового сокета приводит к вызову функции create(), выбранной по семейству адресов из массива структур net_families, в котором свои функции предварительно зарегистрировали модули, поддерживающие разные семейства. Эта функция создает необходимые структуры и inode в специальной файловой системе SockFS, что позволяет работать через read() и write().
При приеме данных стек, начиная с нижнего уровня, вызывает задачи, которые были запланированы прерыванием от сетевого адаптера. К моменту выполнения прерывания данные уже записаны устройством в буфер и передаются стеку через структуру sk_buff.
Передача работает примерно так же: если возникает прерывание и в очереди на передачу есть данные, они проходят стек, диспетчеризуются в различные очереди и передаются устройству, либо стек инициирует передачу самостоятельно.
Изучение исходного кода ядра Linux помогает лучше понять механизмы работы сетевого стека операционной системы и решать специфические задачи, а также диагностировать проблемы, связанные со средами передачи данных.
1. Для чего полезно изучать исходный код ядра Linux?
2. За что отвечает структура sock?
3. Внутри структуры sock есть указатель sk_socket на структуру socket. Для чего он нужен?
4. Что хранит структура sock_common?
5. Зачем при инициализации сетевой подсистемы ядра ОС создаются экземпляры структур sock и proto для каждого протокола?
6. Почему в ядре Linux структура сокета содержит специфичные атрибуты для разных протоколов?
7. Как взаимосвязаны структуры для IP и для TCP?
8. Каким образом можно управлять параметрами сокета в ядре Linux?
9. Для чего функции ioctl() передается дескриптор сокета при работе с устройством?
10. Где хранится информация о номере прерывания, назначенном сетевому адаптеру?
11. Какая структура сетевого стека объединяет свойства протокола и его операции?
12. В какую структуру записываются данные при обмене между ядром и драйвером сетевого устройства?