Структура ifreq также используется в следующих вызовах:
• SIOCGIFMETRIC, SIOCSIFMETRIC — получить или установить метрику интерфейса. Используется поле ifr_metric. В ядре Linux версии 6.x работа с метрикой не реализована. При чтении поля ifr_metric будет возвращен 0, а при установке — ошибка EOPNOTSUPP или EINVAL, в зависимости от стека протоколов.
• SIOCGIFMTU, SIOCSIFMTU — получить или установить MTU. Используется поле ifr_mtu.
• SIOCGIFTXQLEN, SIOCSIFTXQLEN — получить или установить длину очереди передачи устройства с помощью ifr_qlen. Установка длины очереди передачи — привилегированная операция.
• SIOCGIFMAP, SIOCSIFMAP — получить или установить аппаратные параметры интерфейса, используя ifr_map.
Поле ifr_map имеет тип ifmap, который является структурой:
struct ifmap
{
unsigned long mem_start;
unsigned long mem_end;
unsigned short base_addr;
unsigned char irq;
unsigned char dma;
unsigned char port;
};
Интерпретация данной структуры зависит от драйвера устройства и архитектуры.
В большинстве Unix-подобных ОС для настройки параметров Ethernet-адаптеров и подобных им существует утилита ethtool.
Также она позволяет устанавливать параметры виртуальных устройств TUN/TAP, описанных в главе 23.
Эта утилита позволяет выполнять очень низкоуровневые операции с адаптерами:
• получать состояние EEPROM адаптера;
• получать статистику;
• проводить самотестирование;
• запускать тестирование кабеля;
• перепрошивать адаптер;
и так далее.
Большинство операций выполняются с правами суперпользователя.
Для работы утилиты используется ioctl SIOCETHTOOL. Семантика вызова сильно зависит от используемой ОС. Заголовочные файлы, которые требуется включить для его работы, также различаются.
В Linux это linux/sockios.h и linux/ethtool.h.
Вызов SIOCETHTOOL принимает структуру ifreq как параметр. В ее поле ifr_name содержится имя адаптера, в поле ifr_data — указатель на структуру ethtool команды.
В каждой структуре команды первым идет 32-битное поле cmd, по которому ядро определяет, что это за команда, и соответствующим образом интерпретирует оставшуюся часть структуры.
Команд достаточно много:
• Работа с EEPROM:
• ethtool_eeprom — дамп EEPROM. Команды ETHTOOL_GEEPROM, ETHTOOL_GMODULEEEPROM, ETHTOOL_SEEPROM.
• ethtool_modinfo — информация о модуле EEPROM плагина. Команда ETHTOOL_GMODULEINFO.
• ethtool_dump — получить дамп устройства. Команды ETHTOOL_GET_DUMP_FLAG, ETHTOOL_GET_DUMP_DATA, ETHTOOL_SET_DUMP. В структуре первыми идут поля версии, флага и длины полученного дампа, которые устанавливает драйвер устройства. Затем поле data, содержащее массив данных указанной длины.
• Информация об устройстве и его проверка:
• ethtool_drvinfo — общая информация о драйвере и устройстве. Команда ETHTOOL_GDRVINFO.
• ethtool_test — вызов самотестирования устройства. Команда ETHTOOL_TEST.
• ethtool_stats — статистика для конкретного устройства. Команда ETHTOOL_GSTATS.
• ethtool_perm_addr — постоянный аппаратный адрес. Команда ETHTOOL_GPERMADDR.
• ethtool_ts_info — содержит временну́ю метку устройства и ассоциацию PHC. Команда ETHTOOL_GET_TS_INFO.
• Управление масштабированием на принимающей стороне — Receive Side Scaling, или RSS:
• ethtool_rxfh_indir — получение или установка косвенности хеш-потока RX. Команды ETHTOOL_GRXFHINDIR и ETHTOOL_SRXFHINDIR. Принимают таблицу косвенности, показанную на рис. 10.4.
• ethtool_rxfh — получение и установка хеш-адреса потока RX или хеш-ключа. Команды ETHTOOL_GRSSH или ETHTOOL_SRSSH.
• ethtool_rx_ntuple — установка или очистка фильтра потока RX. Команда ETHTOOL_SRXNTUPLE.
RReceive Side Scaling
Масштабирование на принимающей стороне — технология, обеспечивающая распределение обработки принятых данных по нескольким процессорам.
Рис. 10.5. Таблица косвенности и обработка данных
RSS гарантирует, что одно соединение всегда обрабатывается на одном процессоре. Для этого процессор выбирается не сразу по хешу данных, а по записям из таблицы косвенности, выбранных по хешу, как это показано на рис. 10.5.
Изначально эта технология была разработана Microsoft, и подробнее о ней можно прочитать в разделе .
• Управление потоками данных на устройстве:
• ethtool_pauseparam — параметры управления потоком данных Ethernet. Команды ETHTOOL_GPAUSEPARAM и ETHTOOL_SPAUSEPARAM. Они позволяют управлять автосогласованием, а также вставкой кадров паузы в поток приема и передачи, как описано в стандарте IEEE 802.3x «Flow Control».
• ethtool_rxnfc — получение или установка правил классификации потока RX. Команды ETHTOOL_GRXRINGS, ETHTOOL_GRXCLSRLCNT, ETHTOOL_GRXCLSRULE, ETHTOOL_GRXCLSRLALL, ETHTOOL_SRXCLSRLDEL, ETHTOOL_SRXCLSRLINS.
• ethtool_gstrings — набор строк для тегирования данных. Команда ETHTOOL_GSTRINGS.
• ethtool_sset_info — информация о наборе строк. Команда ETHTOOL_GSSET_INFO.
• ethtool_fecparam — параметры FEC, исправления ошибок Ethernet. Команды ETHTOOL_GFECPARAM и ETHTOOL_SFECPARAM.
• Управление соединением:
• ethtool_link_settings — управление соединением и его состояние. Команды ETHTOOL_GLINKSETTINGS и ETHTOOL_SLINKSETTINGS.
• ethtool_channels — настройка количества сетевых каналов. Команды ETHTOOL_GCHANNELS и ETHTOOL_SCHANNELS.
• ethtool_ringparam — параметры кольца RX/TX. Команды ETHTOOL_GRINGPARAM или ETHTOOL_SRINGPARAM.
• Управление питанием и энергопотреблением:
• ethtool_wolinfo — конфигурация Wake-On-Lan. Команды ETHTOOL_GWOL и ETHTOOL_SWOL.
• ethtool_eee — информация об энергоэффективности Ethernet. Команды ETHTOOL_GEEE и ETHTOOL_SEEE.
• Настройка любых параметров и вызов произвольных функций адаптера:
• ethtool_regs — получить содержимое аппаратного регистра. Команда ETHTOOL_GREGS.
• ethtool_coalesce — параметры объединения для IRQ и обновлений статистики. Команды ETHTOOL_GCOALESCE и ETHTOOL_SCOALESCE.
• ethtool_gfeatures — получение состояния функций устройства. Команда ETHTOOL_GFEATURES.
• ethtool_sfeatures — запрос на изменение функций устройства. Команда ETHTOOL_SFEATURES.
• ethtool_per_queue_op — применить подкоманду к очередям в маске. Команда ETHTOOL_PERQUEUE.
Все команды и структуры определены в linux/ethtool.h. Рассматривать подробно мы их не будем, но для примера получим скорость и тип передачи адаптера — дуплексный или нет.
Ethtool в Linux постепенно тоже переходит на интерфейс Netlink. Поэтому описание большинства команд ethtool можно посмотреть в документе «».
Данные команды, используемой для этого, описываются такой структурой ethtool_link_settings:
#include <linux/ethtool.h>
struct ethtool_link_settings
{
// Команда, в данном случае ETHTOOL_GLINKSETTINGS.
__u32 cmd;
// Заданная скорость: 10 Mбит, 100 Mбит, 1 Гбит...
__u16 speed;
// Дуплекс: half, full.
__u8 duplex;
// Тип порта адаптера, например PORT_MII.
__u8 port;
// MDIO-адрес трансивера.
__u8 phy_address;
// Флаг включения автоматического согласования Ethernet.
__u8 autoneg;
// Битовая маска флагов ETH_MDIO_SUPPORTS_* для MDIO-протоколов,
// поддерживаемых интерфейсом. Только для чтения.
__u8 mdio_support;
// Поддержка MDI-X. Поле для чтения.
__u8 eth_tp_mdix;
// Поле для установки режима MDI-X.
__u8 eth_tp_mdix_ctrl;
// Число слов для каждой из поддерживаемых битовых карт
// advertising, lp_advertising.
__s8 link_mode_masks_nwords;
// Используемый трансивер. Позволяет различать типы PHY,
// о которых сообщает PHYLIB. Только для чтения.
__u8 transceiver;
// Режим главного/подчиненного порта.
__u8 master_slave_cfg;
// Состояние главного/подчиненного порта.
__u8 master_slave_state;
__u8 reserved1[1];
__u32 reserved[7];
// Поле, содержащее битовые карты переменной длины:
// __u32 map_supported[link_mode_masks_nwords];
// __u32 map_advertising[link_mode_masks_nwords];
// __u32 map_lp_advertising[link_mode_masks_nwords];
__u32 link_mode_masks[0];
};
Для решения задачи не требуются все поля структуры, и прокомментированы они для справки. Нам достаточно полей ifr_name, speed и duplex.
Поле eth_tp_mdix описывает режим включения кроссовера.
Если этот режим установлен в ETH_TP_MDI_AUTO, интерфейс сам определяет, какие пары в кабеле включены на прием, а какие на передачу, и работает с обычным или кроссоверным кабелем.
Также кроссовер может быть активирован вручную или выключен.
Кроме обычных заголовочных файлов, требуется включить еще некоторые:
extern "C"
{
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <net/if.h>
// Идентификаторы команд.
#include <linux/sockios.h>
// Структура команды.
#include <linux/ethtool.h>
#include <unistd.h>
}
Печать состояния будет производиться в функции print_adapter_params(), которой передается имя адаптера.
Сначала необходимо создать новый сокет, инициализировать структуры команды и сделать первый вызов для корректного заполнения структуры:
void print_adapter_params(const std::string &name)
{
const int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
throw std::system_error(errno, std::system_category(), "socket");
}
// Команда.
ethtool_link_settings cmd = { .cmd = ETHTOOL_GLINKSETTINGS };
ifreq ifr = {0};
// Команда.
ifr.ifr_data = reinterpret_cast<caddr_t>(&cmd);
// Название интерфейса.
std::copy_n(name.c_str(), std::min(IF_NAMESIZE, name.size()),
ifr.ifr_name);
try
{
// Первый вызов.
if (ioctl(sock, SIOCETHTOOL, &ifr) < 0)
{
throw std::system_error(errno, std::system_category(),
"SIOCETHTOOL IOCtl");
}
// От ядра должно прийти отрицательное число.
if (cmd.link_mode_masks_nwords >= 0 ||
cmd.cmd != ETHTOOL_GLINKSETTINGS)
{
throw std::logic_error("Incorrect speed!");
}
Если вызов прошел корректно, флаги корректируются и делается еще один вызов, после которого требуется несколько проверок:
// Получено реальное значение cmd.link_mode_masks_nwords.
// После этого отправляется реальный запрос.
cmd.cmd = ETHTOOL_GLINKSETTINGS;
cmd.link_mode_masks_nwords = -cmd.link_mode_masks_nwords;
// Еще один вызов.
// Ту же информацию можно получить за один вызов команды
// ETHTOOL_GSET, использующей структуру ethtool_cmd.
// Но в Linux эта команда помечена как устаревшая.
if (ioctl(sock, SIOCETHTOOL, &ifr) < 0)
{
throw std::system_error(errno, std::system_category(),
"Cannot read speed on interface: " + name);
}
// Проверка скорости на корректность.
if (!ethtool_validate_speed(cmd.speed))
{
throw std::logic_error("Incorrect speed!");
}
// Проверка дуплекса.
if (!ethtool_validate_duplex(cmd.duplex))
{
throw std::logic_error("Incorrect duplex!");
}
Остается только вывести полученные данные на экран:
const std::map<int, std::string> s_map =
{
{SPEED_10, "10 Mb/s"}, {SPEED_100, "100 Mb/s"},
{SPEED_1000, "1 Gb/s"}, {SPEED_2500, "2.5 Gb/s"},
{SPEED_10000, "10 Gb/s"}, {SPEED_UNKNOWN, "Unknown"}
};
const auto speed = s_map.find(cmd.speed);
if (s_map.end() == speed)
{
throw std::logic_error("Incorrect speed!");
}
// Вывести на экран.
std::cout
<< "Iface = " << ifr.ifr_name << "\n"
<< "Speed = " << speed->second << "\n"
<< "Duplex = "
<< (DUPLEX_HALF == cmd.duplex ? "half" :
(DUPLEX_FULL == cmd.duplex ? "full" : "unknown"))
<< std::endl;
}
catch (const std::exception& e)
{
...
}
close(sock);
}
Вызовем реализованную функцию в main():
int main(int argc, const char* const argv[])
{
if (argc < 2)
{
std::cerr << argv[0] << " <interface>" << std::endl;
return EXIT_FAILURE;
}
print_adapter_params(argv[1]);
return EXIT_SUCCESS;
}
Запустим пример:
➭ build/bin/b01-ch10-ethtool enp0s20f0u1u3
Iface = enp0s20f0u1u3
Speed = 10 Mb/s
Duplex = half
➭ build/bin/b01-ch10-ethtool tun0
Iface = tun0
Speed = 10 Mb/s
Duplex = full
Вызов ethtool показывает тот же результат:
➭ sudo ethtool tun0|grep -i 'speed\|dup'
Speed: 10Mb/s
Duplex: Full
Данные параметры можно получить не для всех адаптеров: адаптер должен быть активен и поддерживать операцию.
Хотя мы и не будем рассматривать данный интерфейс подробно, мы будем упоминать его в этой и следующих книгах по мере необходимости.
Сетевой адаптер — сложное устройство, которое состоит из нескольких блоков:
• MAC — блок нижнего подуровня канального уровня, который управляет доступом к среде передачи.
• PHY — реализует физический уровень Ethernet.
Для соединения этих блоков между собой используется стандартный интерфейс MII, или Media Independent Interface, показанный на рис. 10.6. И этим интерфейсом также можно управлять.
Рис. 10.6. Интерфейс MII
Как правило, он используется для соединения двух микросхем на одной плате и дает возможность использовать любые реализации PHY без замены блока MAC.
Также данный интерфейс может быть использован для подключения внешнего трансивера.
Интерфейс MII синхронный и состоит из двух частей:
• канал MDIO — для приема и передачи данных;
• канал MDC — служебный канал управления.
MII определяет, какими сигналами и по каким линиям обмениваются блок PHY и MAC для передачи и приема данных, тактовые частоты, линии, по которым PHY информирует о коллизиях и ошибках, а также управляющие линии PHY трансивера.
Конкретно MII используется для Fast Ethernet. Существуют его расширения, такие как GMII для гигабитного Ethernet, XGMII для 10-гигабитного Ethernet, который вводит еще один подуровень между PHY и MAC — слой XGXS, 10 Gigabit Ethernet Extended Sublayer и новый протокол XAUI для взаимодействия этого слоя и PHY.
Также существуют расширения для различных задач:
• RMII — Reduced media-independent interface для микроконтроллеров.
• SMII — Serial media-independent interface для последовательной передачи, когда количество линий на порт ограниченно.
• HSGMII — High serial gigabit media-independent interface для увеличения скорости на SGMII до 2,5 Гбит/с.
• QSGMII — Quad serial gigabit media-independent interface для увеличения скорости на SGMII до 5 Гбит/с.
И прочие.
Но интерфейс, предоставляемый ОС, достаточно ограничен, и в большинстве случаев при работе через него различия не будут заметны.
Для работы с MII в Linux есть утилита mii-tool, в основном позволяющая выбирать скорость адаптера: 100baseTx-FD, 100baseTx-HD, 10baseT-FD, 10baseT-HD и переключать автосогласование.
Обычно адаптеры выполняют автоматическое согласование скорости обмена данными.
Для работы с MII существуют три основных ioctl:
• SIOCGMIIPHY — получить адрес используемого PHY-блока.
• SIOCGMIIREG — прочитать регистр PHY-блока.
• SIOCSMIIREG — записать регистр PHY-блока.
Чтобы ими пользоваться, требуется подключить заголовочный файл linux/mii.h.
Используемая в ifr_data структура для работы с этими командами — mii_ioctl_data:
struct mii_ioctl_data
{
// Адрес блока PHY.
__u16 phy_id;
// Номер регистра PHY.
__u16 reg_num;
// Значение для записи в регистр.
__u16 val_in;
// Значение, прочитанное из регистра.
__u16 val_out;
};
Рассмотрим пример чтения регистров. Сначала создадим сокет, инициализируем структуру ifreq именем заданного интерфейса:
extern "C"
{
#include <linux/mii.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <linux/sockios.h>
#include <linux/types.h>
#include <netinet/in.h>
#include <unistd.h>
}
int main(int argc, const char* const argv[])
{
...
ifreq ifr = {0};
std::copy_n(iface_name.c_str(),
std::min(static_cast<size_t>(IF_NAMESIZE — 1),
iface_name.size()), ifr.ifr_name);
...
Затем выполним ioctl для получения номера PHY-блока, а в цикле прочитаем его регистры и выведем их содержимое на экран:
// Прочитать адрес PHY-блока адаптера.
if (ioctl(sock, SIOCGMIIPHY, &ifr) < 0)
{
close(sock);
throw std::system_error(errno, std::system_category(), "SIOCGMIIPHY");
}
mii_ioctl_data *mii_data = reinterpret_cast<mii_ioctl_data *>(
&ifr.ifr_data);
// Цикл чтения регистров: регистры до 0x1c — основные регистры MII.
for(int i = 0 ; i <= 0x1с; ++i)
{
// Установить номер регистра.
mii_data->reg_num = i;
// Выполнить ioctl для чтения значения.
if (ioctl(sock, SIOCGMIIREG, &ifr) < 0)
{
std::array<char, 256> buffer{};
// Ошибка чтения одного регистра, но другие регистры читать можно.
std::cerr
<< "Ioctl error: "
<< ::strerror_r(errno, buffer.data(), buffer.size())
<< std::endl;
continue;
}
std::cout
<< std::hex
<< "PHY addr: 0x" << mii_data->phy_id
<< ", reg: 0x" << mii_data->reg_num
<< ", value: 0x" << mii_data->val_out
<< std::endl;
}
close(sock);
return EXIT_SUCCESS;
}
Запустим пример:
➭ sudo build/bin/b01-ch10-mii-example eno2
Interface: eno2
PHY addr: 0x2, reg: 0x0, value: 0x1140
PHY addr: 0x2, reg: 0x1, value: 0x7909
PHY addr: 0x2, reg: 0x2, value: 0x154
PHY addr: 0x2, reg: 0x3, value: 0xa0
PHY addr: 0x2, reg: 0x4, value: 0xde1
PHY addr: 0x2, reg: 0x5, value: 0x0
PHY addr: 0x2, reg: 0x6, value: 0x4
Ioctl error: Input/output error
Ioctl error: Input/output error
PHY addr: 0x2, reg: 0x9, value: 0x200
PHY addr: 0x2, reg: 0xa, value: 0x0
...
Видим содержимое регистров блока PHY. Некоторые регистры прочитать не удалось.
Утилита mii-tool работает так же: использует вызовы ioctl, то есть читает регистры, а затем интерпретирует результат.
Для интерпретации в заголовочном файле linux/mii.h определен набор констант, задающих назначение регистров, флаги, маски полей. Например, MII_BCMR — основной регистр управляющего режима, через который можно осуществить сброс адаптера, если записать в него флаг BCMR_RESET. А флаг BMCR_SPEED100 позволяет определить скорость — 100 или 10 Мбит/с. Установка этого флага приведет к переходу на заданную скорость.
За подробностями читатель может обратиться , которая содержится в пакете net-tools.
ARP — протокол разрешения адресов, который используется для преобразования между аппаратными адресами уровня 2 и адресами протокола IPv4 в сетях с прямым подключением узлов.
PDU данного протокола имеет простую структуру, в которой содержится код операции, аппаратные и сетевые адреса. Она показана на рис. 10.7.
Рис. 10.7. Структура ARP-пакета
На рис. 10.7 видно, как протокол работает в случае наличия общей шины. В случае моста все работает аналогично.
Если узел 1 хочет отправить данные узлу 2, зная его IP, но его аппаратный адрес ему неизвестен, он отправит запрос, содержащий IP узла 2 и свой MAC на широковещательный MAC-адрес.
Рис. 10.8. Работа ARP
Узел 2 ответит узлу 1 PDU, содержащим свой MAC. Когда узел 1 получит ответ, он добавит MAC-адрес узла 2 в свой кэш и после этого сможет обмениваться с этим узлом как обычно.
ARP описан в RFC 826 «An Ethernet Address Resolution Protocol». Задача ARP — найти канальный адрес, или MAC адаптера по IP-адресу.
Если этот компьютер доступен в общем широковещательном домене, он пришлет ответ на широковещательный ARP-запрос. Его MAC-адрес будет помещен в кэш и в дальнейшем использован для обмена с этим узлом, когда запрос производится к его IP-адресу. Широковещательный домен — это, например, общая шина, VLAN или физические подсети, соединенные коммутаторами.
Если же адрес узла находится за границами пространства адресов подсети, что определяется по маске сети, узлу ответит шлюз и все обращения к узлам из других сетей будут переадресованы шлюзу, который уже отправит их по назначению.
Протокол в современных сетях может быть полезен в основном для определения адреса шлюза, а в IPv6-сетях не используется вообще. Там его задачи выполняет ICMPv6.
Система поддерживает статическую ARP-таблицу, содержащую записи сопоставления IP-адресов с MAC-адресами. Пользователь может добавлять или удалять записи в этой таблице.
Кэш сопоставлений между аппаратными адресами и адресами протоколов сетевого уровня:
• Динамический ARP-кэш, записи в который помещаются, когда устройство узнает MAC-адрес устройства из ARP-запроса или ответа.
• Результат обнаружения соседей по IPv6.
Размер кэша ограничен: старые записи удаляются сборщиком мусора. Записи, помеченные как постоянные, не удаляются никогда.
Запись считается устаревшей, если в течение некоторого периода для нее отсутствует ответ.
Ответ может прийти:
• С более высокого уровня, например, после успешного TCP ACK.
• От протокола, уведомляющего о ходе пересылки. Для этого используется флаг MSG_CONFIRM в функциях send().
Если ответа нет, ARP пытается повторить попытку, сначала запрашивая новый MAC у локального демона ARP.
Если это не удается и известен старый MAC, отправляется реальный запрос к узлу. Если и это не удается, ARP отправляет в сеть новый запрос.
Кэшем можно управлять с помощью ioctl на дескрипторе сокета из семейства AF_INET:
• SIOCSARP — добавить запись кэша;
• SIOCGARP — получить запись кэша;
• SIOCDARP — удалить запись кэша.
Параметром данных ioctl является структура, определенная в net/if_arp.h:
// Структура — параметр ioctl.
struct arpreq
{
// Сетевой адрес — "Protocol Address".
struct sockaddr arp_pa;
// Аппаратный адрес.
struct sockaddr arp_ha;
// Флаги.
int arp_flags;
// Сетевая маска протокола.
// Действительна, только если ATF_NETMASK установлен.
struct sockaddr arp_netmask;
// Имя адаптера.
char arp_dev[16];
};
Флаги могут принимать следующие значения:
// Запись используется, старый флаг.
#define ATF_INUSE 0x01
// Нормальная запись с корректным аппаратным адресом.
#define ATF_COM 0x02
// Постоянная запись.
#define ATF_PERM 0x04
// Опубликованная другим узлом запись, полученная из сети через ARP.
#define ATF_PUBL 0x08
// Устаревший флаг.
#define ATF_USETRAILERS 0x10
// Использовать маску сети для записей прокси.
#define ATF_NETMASK 0x20
// Не отвечать этим адресом.
#define ATF_DONTPUB 0x40
// Запись добавлена автоматически.
#define ATF_MAGIC 0x80
Узнать подробнее о поддержке ARP можно в man 7 arp.
В Linux существует возможность управления кэшем ARP с помощью сокетов Netlink.
Внимание! Данные вызовы устарели. Используйте сокеты RTNetlink в Linux, как это делает команда ip route. В Solaris и BSD-системах также есть сокеты AF_ROUTE, которые описаны Стивенсом. Упоминание данных ioctl нужно для того, чтобы понимать, как организовать работу с маршрутами в других, как правило, более старых ОС, а также как это было сделано в исторических утилитах, которые могут до сих пор использоваться.
Для выбора соответствующего сетевого интерфейса при отправке данных используется таблица маршрутизации. Она содержит одну запись для каждого маршрута к определенной сети или узлу, содержащую адрес назначения и интерфейс, через который следует отправить пакет данных до следующего маршрутизатора.
Таблица может использоваться разными протоколами, не только разными версиями IP. За работу с таблицей отвечали следующие вызовы:
• SIOCADDRT — добавить одну запись в таблицу маршрутизации;
• SIOCDELRT — удалить одну запись из таблицы маршрутизации.
Структура записи таблицы маршрутизации очень сильно зависит от платформы. Например, в Linux структура определена в net/route.h таким образом:
// Передавать эту структуру надо в SIOCADDRT и SIOCDELRT.
struct rtentry
{
unsigned long int rt_pad1;
// Адрес назначения.
struct sockaddr rt_dst;
// Адрес шлюза.
struct sockaddr rt_gateway;
// Маска сети назначения.
struct sockaddr rt_genmask;
// Различные флаги.
unsigned short int rt_flags;
short int rt_pad2;
unsigned long int rt_pad3;
unsigned char rt_tos;
unsigned char rt_class;
#if __WORDSIZE == 64
short int rt_pad4[3];
#else
short int rt_pad4;
#endif
// Метрика, требуемая для совместимости.
short int rt_metric;
// Имя устройства: к нему привязан маршрут.
// Пользователь не может указывать поле интерфейса.
// Оно заполняется процедурами маршрутизации.
char *rt_dev;
// MTU для маршрута.
unsigned long int rt_mtu;
// Ограничение окна для маршрута.
unsigned long int rt_window;
// Начальное RTT для маршрута.
unsigned short int rt_irtt;
};
// Необходимо для совместимости.
#define rt_mss rt_mtu
В Solaris или BSD-системах данная структура будет отличаться. Общие значения флагов маршрута:
// Значения флагов для поля rt_flags.
// Маршрут работоспособен.
#define RTF_UP 0x0001
// Пункт назначения — шлюз.
#define RTF_GATEWAY 0x0002
// Запись в таблице адресует узел.
// Если флаг не установлен, запись адресует сеть.
#define RTF_HOST 0x0004
// Восстановить маршрут после тайм-аута.
#define RTF_REINSTATE 0x0008
// Динамически создан, то есть через редирект.
#define RTF_DYNAMIC 0x0010
// Динамически изменен, то есть через редирект.
#define RTF_MODIFIED 0x0020
// Для этого маршрута задан специальный MTU.
#define RTF_MTU 0x0040
// Нужно для совместимости.
#define RTF_MSS RTF_MTU
// Ограничение размера окна для маршрута.
#define RTF_WINDOW 0x0080
// Для маршрута задан начальный RTT.
#define RTF_IRTT 0x0100
// Отклонить маршрут.
#define RTF_REJECT 0x0200
// Маршрут добавлен вручную.
#define RTF_STATIC 0x0400
// Внешний резолвер.
#define RTF_XRESOLVE 0x0800
// Запрещено использовать маршрут для пересылки данных.
#define RTF_NOFORWARD 0x1000
// Перейти к следующему классу.
#define RTF_THROW 0x2000
// Не отправлять пакеты, которые не могут быть фрагментированы.
// Например, с установленным DF в IP-заголовке.
#define RTF_NOPMTUDISC 0x4000
Флаги IPv6 и прочие:
// Флаги для IPv6.
// default — выучен через обнаружение соседей (IPv6 Neighbor Discovery).
#define RTF_DEFAULT 0x00010000
// Резервный вариант: нет маршрутизаторов в канале.
#define RTF_ALLONLINK 0x00020000
// Маршрут addrconf, полученный через уведомления от маршрутизатора
// через Router Advertisement.
#define RTF_ADDRCONF 0x00040000
// Специфичный для соединения.
#define RTF_LINKRT 0x00100000
// Маршрут без следующего узла.
#define RTF_NONEXTHOP 0x00200000
// Запись кэша.
#define RTF_CACHE 0x01000000
// flow significant route
#define RTF_FLOW 0x02000000
// Маршрутизация на основе политик.
#define RTF_POLICY 0x04000000
#define RTCF_VALVE 0x00200000
#define RTCF_MASQ 0x00400000
#define RTCF_NAT 0x00800000
#define RTCF_DOREDIRECT 0x01000000
#define RTCF_LOG 0x02000000
#define RTCF_DIRECTSRC 0x04000000
#define RTF_LOCAL 0x80000000
#define RTF_INTERFACE 0x40000000
#define RTF_MULTICAST 0x20000000
#define RTF_BROADCAST 0x10000000
#define RTF_NAT 0x08000000
В таблице маршрутизации существует три типа записей:
• для узла;
• для всех узлов в определенной сети;
• для любого пункта назначения, не совпадающего с записями первых двух типов, также называемые маршрутом с подстановочными знаками.
Каждый сетевой интерфейс при инициализации устанавливает запись в таблице маршрутизации. Обычно интерфейс указывает, является ли маршрут через него прямым соединением с целевым узлом или сетью. Если маршрут прямой, транспортный уровень запрашивает отправку пакета на тот же узел, который указан в пакете, иначе через интерфейс пакет будет пересылаться, если пересылка не запрещена.
Запись маршрутизации с подстановочными знаками указывается с нулевым значением адреса назначения. Маршруты с подстановочными знаками используются, когда системе не удается найти другой маршрут к узлу или сети.
Настройки сокета не ограничиваются опциями, перечисленными в главе 8, и не существует единого вызова для установки всех параметров. Сокет поддерживает несколько «своих» ioctl.
Данные вызовы действуют только на конкретный сокет и не меняют системные настройки, поэтому не являются привилегированными. К структуре ifreq они также не имеют никакого отношения: каждый вызов использует свой тип данных.
Рассмотрим эти вызовы подробнее:
• SIOCGSTAMP — возвращает структуру timeval с временем получения последнего пакета, отправленного пользователю. Это полезно для точного измерения RTT — времени круговой задержки. Данный ioctl используется, когда параметры сокета SO_TIMESTAMP и SO_TIMESTAMPNS не установлены на сокете, иначе возвращается временна́я метка последнего полученного пакета до их установки. Если же такой пакет не был получен, ioctl() возвращает –1 и ошибку ENOENT.
• SIOCSPGRP — установить процесс или группу процессов, которые должны получать сигналы SIGIO, когда становится возможным ввод-вывод, или SIGURG, когда доступны внеполосные данные, описанные в главе 6. Аргумент ioctl — указатель на pid_t. Для указания PID процесса значения положительные, для указания группы — отрицательные, 0 сбрасывает получателя.
• SIOCGPGRP — получить идентификатор процесса или группы процессов, которые получают сигналы SIGIO или SIGURG. Вернет положительный идентификатор для процесса, отрицательный для группы или 0, если идентификатор не установлен.
• FIOASYNC — включить или отключить режим асинхронного ввода-вывода. В асинхронном режиме, когда ввод или вывод станет возможным, процесс получит сигнал SIGIO. В Linux сигнал может быть переопределен через операцию F_SETSIG, выполняемую функцией fcntl(). Данный ioctl просто изменяет флаг O_ASYNC.
• FIONBIO — включить или отключить неблокирующий режим сокета. Устанавливает значение флага O_NONBLOCK.
FIOASYNC включает уведомления при асинхронном вводе-выводе. Эта команда заставляет ядро отправлять сигнал SIGIO или SIGPOLL процессу или группе процессов, когда дескриптор готов к вводу-выводу. Эту функциональность реализуют только сокеты, tty и псевдо-tty.
FIONBIO разрешает неблокирующий ввод-вывод. Установка O_NONBLOCK не уведомляет пользовательский процесс о том, готов ли дескриптор к чтению или записи.
Он изменяет поведение вызовов чтения, записи и подобных. После его установки вызовы перестанут блокироваться и всегда будут возвращать управление немедленно.
O_NONBLOCK обычно используется в сочетании с вызовами select() или poll() и аналогами, чтобы реализовывать асинхронные системы, рассмотренные в книге 2.
Внимание! Использование ioctl FIOASYNC — нестандартный вариант установки опции O_ASYNC, поэтому лучше его не использовать. Установку флага в Unix-подобных системах лучше производить через рассмотренную далее функцию fcntl().
Для TCP существует несколько ioctl. В скобках для некоторых из них указаны их синонимы:
• FIONREAD или SIOCINQ — получить количество непрочитанных данных в очереди приема, то есть сколько данных можно прочитать за один вызов recv(). Если сокет находится в состоянии LISTEN, будет возвращена ошибка EINVAL. SIOCINQ определен в linux/sockios.h. FIONREAD определен в sys/ioctl.h. Вероятно, в переносимом коде предпочтительнее использовать FIONREAD, так как в ОС Windows он также присутствует.
• SIOCATMARK — возвращает значение, не равное нулю, если входящий поток данных находится на отметке срочности. Если параметр сокета SO_OOBINLINE установлен, а SIOCATMARK возвращает ненулевое значение, при следующем чтении из сокета будут возвращены внеполосные данные. Если параметр сокета SO_OOBINLINE не установлен, а SIOCATMARK возвращает ненулевое значение, при следующем чтении из сокета будут возвращены байты, следующие за срочными данными, если в recv() не был указан флаг MSG_OOB. О внеполосных данных подробнее см. в главе 6.
• TIOCOUTQ или SIOCOUTQ — возвращает количество неотправленных данных в очереди отправки, если сокет находится в состоянии LISTEN, в противном случае будет возвращена ошибка EINVAL. SIOCOUTQ определен в linux/sockios.h. Альтернативный TIOCOUTQ определен в sys/ioctl.h.
Вызовы ioctl постепенно стандартизуются в POSIX. Часто вместо них предлагается использовать функции. Поэтому и для вызова SIOCATMARK, вероятно, стоит использовать функцию sockatmark().
С другой стороны, в ОС Windows эта функция еще отсутствует, и в ней придется использовать SIOCATMARK.
FIONREAD можно использовать, например, для того, чтобы выделить пользовательский буфер заранее:
unsigned long bytes_available;
if (ioctl(socket_fd, FIONREAD, &bytes_available) < 0)
{
throw std::system_error(errno, std::system_category(), "FIONREAD");
}
if (bytes_available > 0)
{
std::vector<char> buffer(bytes_available);
...
Для UDP доступны следующие вызовы:
• FIONREAD или SIOCINQ — вернуть размер в байтах следующей ожидающей в очереди приема дейтаграммы или 0, если ожидающих дейтаграмм нет.
• TIOCOUTQ или SIOCOUTQ — вернуть количество байтов данных в очереди отправки.
Внимание! При использовании FIONREAD невозможно отличить случай, когда ожидающих дейтаграмм нет, от случая, когда следующая ожидающая дейтаграмма имеет нулевой размер данных. Лучше использовать асинхронные вызовы типа select() или poll(), которые будут рассмотрены в книге 2.
Для Unix-сокетов в Linux поддерживается единственный вызов — SIOCINQ, получающий количество непрочитанных данных в очереди приема. Он поддерживается только на сокетах типа SOCK_STREAM и работает аналогично тому же вызову, описанному для TCP.
Для работы с дескрипторами в Linux существуют два нестандартных ioctl, работающих с флагом CLOEXEC:
• FIOCLEX — установить флаг CLOEXEC на дескриптор;
• FIONCLEX — сбросить флаг.
Используются они так:
int r = ioctl(fd, FIOCLEX);
Конечно, мы рассмотрели не все «сетевые» ioctl. В BSD-стеке, например, существует достаточно много вызовов, общих между системами, но отсутствующих в Linux. Например, SIOCGIFSTATS для получения статистики интерфейса или SIOCGIFTYPE для получения его типа. Систем достаточно много, существуют еще AIX, Solaris, Haiku — наследница BeOS, и каждая включает свои собственные вызовы, даже если некоторые системы используют BSD-стек.
Задачи написать всеобъемлющий справочник не ставилось, поэтому мы не рассматривали данные системы.
В этой главе мы изучили работу с функцией ioctl(), которая предоставляет низкоуровневый доступ к устройствам и сетевым интерфейсам, позволяя напрямую обращаться к драйверу и устанавливать параметры.
При установке или получении параметров сетевых интерфейсов обычно указывается, с чем работать, путем передачи имени сетевого интерфейса ifr_name, его индекса либо индекса для IPv6 в ifr6_ifindex. Вызовы ioctl можно использовать для того, чтобы получить индекс некоторого интерфейса по его имени, а также решить обратную задачу. Затем через ioctl можно получить различные характеристики интерфейса, такие как его тип и тип среды передачи. Также ioctl позволяет изменять параметры интерфейса: переименовать интерфейс, менять его адреса, маски, метрики, флаги и пр.
Флаги интерфейса — одна из характеристик сетевого интерфейса. Они позволяют отслеживать работу интерфейса, менять его режим, связывать интерфейсы в группу и т.п.
Широко известная утилита для работы с сетевыми параметрами — ethtool — использует ioctl-вызов SIOCETHTOOL и берет основные данные о сетевом интерфейсе из этого вызова.
Для более низкоуровневой работы существует утилита mii-tool. Адаптер состоит из блоков MAC и PHY, и для их соединения используется Media Independent Interface. Эта утилита позволяет управлять данным интерфейсом, для чего тоже использует ioctl-вызовы.
В сетях с прямым подключением узлов для преобразования между аппаратными и сетевыми адресами используется ARP. Система поддерживает кэш сопоставлений между адресами разных уровней соседних узлов. Работа с кэшем ARP производится также с помощью ioctl.
С помощью ioctl можно также работать напрямую с сокетом, менять настройки работы для сетевых протоколов TCP, UDP и т.п. Ранее в Unix-подобных системах этот вызов был основным интерфейсом, позволяющим взаимодействовать с аппаратным обеспечением и ресурсами системы на низком уровне. Он до сих пор широко используется в драйверах и системном программировании, но сейчас постепенно вытесняется интерфейсами, подобными сокетам Netlink в Linux.
1. В чем отличие сетевого интерфейса от сетевого адаптера?
2. Каковы основные параметры, которые принимает вызов ioctl()?
3. Какие проблемы могут возникнуть при осуществлении неправильного вызова ioctl?
4. Имеет ли код ioctl структуру? Почему?
5. Зачем нужен индекс интерфейса, если в вызове ioctl() можно использовать название интерфейса?
6. Как получить адрес сетевого интерфейса?
7. Какие есть способы перечисления сетевых интерфейсов в Linux?
8. Для чего может потребоваться переименование сетевого интерфейса?
9. Почему операционная система после переименования интерфейса не удаляет старое название?
10. Какие типы состояния есть у сетевого интерфейса? Флаги какого состояния можно устанавливать, а какого нет? Почему?
11. Как обычно передаются флаги в вызовах ioctl()?
12. За что отвечает флаг IFF_AUTOMEDIA?
13. Для чего нужны флаги расширенного состояния?
14. Что случится, если размер буфера недостаточен при вызове SIOCGIFCONF? Как выделить буфер нужного размера?
15. Для выполнения каких задач служит вызов SIOCETHTOOL?
16. Что такое PHY- и MAC-блоки?
17. Что такое MII? Для чего используется управление MII?
18. Для чего используется ARP? Используется ли он в IPv6-сетях?
19. Какие параметры ARP возможно изменять?
20. Для чего используется вызов FIOASYNC и стоит ли его использовать?
21. Для чего используется вызов FIONREAD и стоит ли его использовать?
22. Какие еще вызовы для сокета кажутся вам полезными для решения своих задач при написании сетевых приложений?
23. Чем заменяется ioctl в современных ядрах Linux, например устаревшие вызовы для работы с маршрутами?
24. Установите с помощью вызова ioctl() SIOCSIFMTU MTU на сетевом устройстве в значение 500 байт. Убедитесь что новое значение установлено с помощью ioctl().
25. Напишите программу, которая использует ioctl SIOCGIFADDR, чтобы получить и вывести IP-адрес заданного сетевого интерфейса.
26. Создайте приложение, которое использует ioctl SIOCGIFHWADDR, чтобы получить и поменять MAC-адрес заданного сетевого интерфейса.