Книга: Сетевое программирование. От основ до приложений
Назад: Глава 6. Внеполосные данные. Пространства имен
Дальше: Глава 8. Управление сокетами

Глава 7. Сокет в ядре Linux

Линуксоиды делают то, что делают, потому что ненавидят 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.

Структура socket

Структура является представлением сокета в ядре, 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 был добавлен в структуру относительно недавно с целью оптимизации. Это очередь, в которой могут храниться, например, идентификаторы процессов, ожидающих готовности сокета.

Структура sock

Рассмотрим внутреннюю структуру. В ней также есть вложенные поля-структуры. Для упрощения доступа к ним определены некоторые макросы.

Прежде всего мы увидим очереди для отправки и приема данных, счетчики, опции и несколько служебных атрибутов:

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_common

Адреса и, если требуется, порты хранятся в отдельной структуре, «заголовке» в структуре 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

Структура 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, которую можно называть «базовым классом».

Структура net_proto_family

Эта структура используется для регистрации функций, создающих необходимые структуры для семейства адресов:

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().

INET-сокет

Любой интернет-сокет, работающий поверх 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. В какую структуру записываются данные при обмене между ядром и драйвером сетевого устройства?

Назад: Глава 6. Внеполосные данные. Пространства имен
Дальше: Глава 8. Управление сокетами