1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/yu120-lemon-guide

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
OS.md 140 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
gitlife-traslator Отправлено 30.11.2024 18:34 ec8a065

OS

Введение: сбор информации о технологиях, связанных с JDK Tools, Linux Tools, Git и т. д.!

[TOC]

I/O

В Linux/Unix обычно используются следующие модели ввода-вывода: блокирующий ввод-вывод (Blocking I/O), неблокирующий ввод-вывод (Non-Blocking I/O), мультиплексированный ввод-вывод (I/O Multiplexing), сигналоуправляемый ввод-вывод (Signal Driven I/O) (используется редко) и асинхронный ввод-вывод (Asynchronous I/O). Основные операции сетевого ввода-вывода включают в себя ядро и процессы, и их можно разделить на два этапа:

  • Ядро ожидает готовности данных для чтения или записи — блокирование и неблокирование.
  • Копирование данных между ядром и процессом.

Базовые концепции

Запрос ввода-вывода можно разделить на две фазы: фазу вызова и фазу выполнения.

  • Первая фаза — это фаза вызова ввода-вывода, когда пользовательский процесс инициирует системный вызов.
  • Вторая фаза — фаза выполнения ввода-вывода. На этом этапе ядро ожидает завершения обработки запроса ввода-вывода и возвращает результат. Эта фаза делится на два процесса: сначала ядро ожидает готовности данных, а затем копирует данные из буфера ядра в буфер пользователя.

Блокирующий и неблокирующий режимы

Блокирование и неблокирование происходят, когда ядро ожидает готовности данных. Они указывают, нужно ли ждать ответа во время ожидания.

  • Блокирование: если ядро обнаруживает, что данные недоступны для операций чтения или записи, оно не возвращается немедленно.
  • Неблокирование: если ядро определяет, что данные недоступны для операций чтения или записи, оно немедленно возвращается.

Синхронный и асинхронный режимы

Синхронизация и асинхронность происходят при взаимодействии ядра и процесса. Они определяют, должен ли процесс ожидать или опрашивать результаты после запуска операции ввода-вывода.

  • Синхронизация: запуск операции ввода-вывода → ожидание или опрос результатов.
  • Асинхронность: запуск операции ввода-вывода → немедленное возвращение к выполнению других задач; ядро уведомляет процесс об окончании операции ввода-вывода после её завершения.

Блокирующий ввод-вывод (BIO)

При использовании блокирующего ввода-вывода пользовательский поток блокируется после вызова read. Он будет ждать, пока ядро скопирует данные из дискового буфера в пользовательский буфер, прежде чем read вернёт результат. Блокировка включает в себя два этапа:

  1. Ожидание, пока ядро скопирует данные с диска в свой буфер.
  2. Ожидание копирования данных из буфера ядра в пользовательский буфер.

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

Неблокирующий ввод-вывод (NIO)

Неблокирующий режим позволяет read немедленно возвращаться, даже если данные ещё не готовы. В этом случае приложение постоянно опрашивает ядро, чтобы узнать, готовы ли данные. Если данные не готовы, ядро немедленно возвращает ошибку EWOULDBLOCK. Как только данные становятся доступными, ядро копирует их в буфер приложения, и read получает результат.

Обратите внимание, что последний вызов read для получения данных является синхронным процессом, который требует ожидания. Здесь синхронизация относится к процессу копирования данных из ядра в буфер приложения.

По сравнению с блокирующим вводом-выводом неблокирующий ввод-вывод значительно повышает производительность, но большое количество системных вызовов во время опроса приводит к высоким накладным расходам на переключение контекста. Поэтому использование неблокирующего ввода-вывода отдельно не очень эффективно, и его производительность ухудшается по мере увеличения параллелизма.

Мультиплексированный ввод-вывод

Если данные недоступны при использовании неблокирующего режима, приложение тратит ресурсы процессора на опрос ядра. Можно ли избежать опроса и получать уведомления, когда данные готовы? Мультиплексированный ввод-выход решает эту проблему. Select, poll и epoll являются реализациями мультиплексированного ввода-вывода.

Мультиплексирование позволяет одному потоку обрабатывать несколько дескрипторов ввода-вывода одновременно. Многоканальный означает наличие нескольких каналов данных, а мультиплексирование означает использование одного или нескольких фиксированных потоков для обработки каждого сокета. Select, poll и epoll — это конкретные реализации мультиплексирования ввода-вывода. Один вызов select может получить статус данных в нескольких каналах ядра. Мультиплексирование ввода-вывода решает проблемы синхронного блокирующего и синхронного неблокирующего режимов и является высокопроизводительной моделью ввода-вывода.

Сигналоуправляемый ввод-вывод

Независимо от того, используется ли блокирующий или неблокирующий режим, read и send являются синхронными вызовами. Во время read ядро копирует данные из своего буфера в буфер приложения; этот процесс требует ожидания и называется синхронизацией. Если ядро неэффективно копирует данные, read будет долго ждать в процессе синхронизации.

Сигналоуправляемый ввод-вывод — это полуасинхронная модель ввода-вывода. Когда данные готовы, ядро отправляет сигнал SIGIO, чтобы уведомить приложение о начале чтения данных.

Асинхронный ввод-вывод

Асинхронный ввод-вывод отличается тем, что оба процесса — ожидание готовности данных и копирование данных из ядра в приложение — выполняются без блокировки. После запуска aio_read приложение немедленно возвращается, и ядро автоматически копирует данные в буфер приложения. Этот процесс также выполняется асинхронно, и приложению не нужно активно инициировать копирование.

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

Режим Reactor

Режим Reactor — это модель, которая отслеживает события ввода-вывода и распределяет их соответствующим образом. Основными компонентами являются Reactor и пул обработки ресурсов:

  • Reactor отвечает за мониторинг и распределение событий. Типы событий включают соединения и чтение-запись.
  • Пул обработки ресурсов обрабатывает события. Например, read → бизнес-логика → send.

Reactor является гибким и может адаптироваться к различным сценариям использования. Его гибкость заключается в следующем:

  • Количество Reactor может быть один или несколько.
  • Пулы обработки могут быть одним потоком или несколькими потоками.

Теоретически существует четыре возможных комбинации:

  • Один Reactor, один поток.
  • Один Reactor, несколько потоков.
  • Несколько Reactor, один поток (эту комбинацию можно игнорировать, поскольку она не имеет преимуществ в производительности по сравнению с одним Reactor, одним потоком).
  • Несколько Reactor, несколько потоков. Для fd при сканировании используется линейный способ.

При увеличении fd эффективность IO снижается, так как каждый вызов требует линейного сканирования и перебора всех элементов. Это приводит к замедлению работы по мере увеличения количества fd.

Преимущества:

  • select() обладает лучшей переносимостью. В некоторых системах Unix не поддерживается poll().
  • select() обеспечивает более высокую точность значений тайм-аута: микросекунды вместо миллисекунд у poll.

Poll

По сути, poll и select похожи. Poll копирует массив, переданный пользователем, в пространство ядра, затем проверяет состояние каждого fd, связанного с устройством. Если устройство готово, оно добавляется в очередь устройств ожидания. После обхода всех fd и отсутствия готовых устройств процесс приостанавливается до готовности устройства или истечения времени ожидания. Затем процесс возобновляется и снова проходит по всем fd. Этот цикл может повторяться несколько раз без необходимости. Особенностью poll является «горизонтальный триггер»: если после уведомления о состоянии fd оно не обрабатывается, то при следующем вызове poll уведомление будет выдано повторно.

Недостатки:

  • Множество fd массивов полностью копируются между пользовательским пространством и пространством ядра, независимо от того, имеет ли это смысл.
  • После возврата из poll необходимо опросить pollfd для получения информации о готовых дескрипторах.

Достоинства:

  • Разработчику не нужно вычислять максимальный дескриптор файла плюс один.
  • По сравнению с select, poll работает быстрее при обработке большого количества дескрипторов файлов.
  • Нет ограничения на максимальное количество подключений, поскольку он основан на списке.

Epoll

Epoll поддерживает горизонтальный и вертикальный триггеры. Его основная особенность — вертикальный триггер, который сообщает процессу только о тех fd, которые недавно стали готовыми, и делает это только один раз. Ещё одна особенность заключается в использовании механизма уведомлений, основанного на событиях, через epoll_ctl. Когда fd готов, ядро использует механизм обратного вызова, похожий на callback, чтобы активировать fd, и epoll_wait может получать уведомления.

Достоинства:

  • Поддержка большого количества сокетов (Socket) в одном процессе. У select есть ограничение на количество открытых FD: оно задаётся FD_SETSIZE и по умолчанию равно 1024 или 2048. Для серверов, которым требуется поддерживать тысячи соединений, этого явно недостаточно. Можно изменить этот макрос и перекомпилировать ядро. Однако у epoll нет такого ограничения: количество поддерживаемых FD ограничено только максимальным количеством файлов, которое обычно намного больше 2048 (например, около 10 000 на машине с 1 ГБ памяти).
  • Эффективность IO не линейно снижается с увеличением количества FD. Ещё одним недостатком традиционных select/poll является то, что когда у процесса открыто большое количество сокетов, но из-за сетевых задержек только часть из них активна в данный момент, select/poll всё равно линейно сканирует весь набор при каждом вызове, что снижает эффективность. Epoll решает эту проблему, работая только с активными сокетами благодаря реализации на основе функций обратного вызова для каждого fd. Только активные сокеты вызывают функцию обратного вызова, а неактивные — нет. Таким образом, epoll реализует псевдо-AIO, где движущая сила находится в ядре Linux.
  • Использование mmap ускоряет обмен сообщениями между ядром и пользовательским пространством. Это связано с конкретной реализацией epoll. Независимо от того, используются ли select, poll или epoll, ядро должно уведомлять процессы о готовности FD, и важно избежать ненужного копирования памяти. В случае epoll ядро и пользовательское пространство используют одну и ту же область памяти через mmap.

Нулевое копирование (Zero-Copy)

Данные и четыре копии с четырьмя переключениями контекста

Многие приложения, сталкиваясь с запросами клиентов, могут быть эквивалентны следующим системным вызовам:

  • File.read(file, buf, len);
  • Socket.send(socket, buf, len).

Например, промежуточное ПО для обмена сообщениями Kafka делает именно это: после чтения сообщений с диска они отправляются через сетевой интерфейс (NIC) без изменений. Без оптимизации чтение данных с диска, их передача через сеть и запись в буфер будут сопровождаться четырьмя копиями данных и четырьмя переключениями контекста, как показано на рисунке ниже.

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

  1. Четыре копии:
    • ЦП отвечает за перемещение данных из диска в кэш страниц ядра;
    • ЦП отвечает за перемещение данных из буфера сокета в сетевом пространстве в сеть;
    • ЦП отвечает за перемещение данных из кэша страниц ядра в буфер пользователя;
    • ЦП отвечает за перемещение данных из сетевого пространства в буфер сокета.
  2. Четыре переключения контекста:
    • переключение из пользовательского режима в режим ядра при выполнении системного вызова read;
    • возвращение из режима ядра в пользовательский режим после выполнения системного вызова read;
    • переключение из пользовательского режима в режим ядра при выполнении системного вызова write;
    • возвращение из режима ядра в пользовательский режим после выполнения системного вызова write.

Проблема заключается в том, что ЦП постоянно занят перемещением данных, а также в том, что скорость перемещения данных между диском, сетью и памятью значительно ниже скорости работы ЦП.

Технология нулевого копирования

Технология нулевого копирования предполагает, что при выполнении операций данные не копируются многократно, а управление ими осуществляется напрямую. Однако это не означает, что данные вообще не копируются: если данные изначально находятся не в памяти, они должны быть скопированы туда перед использованием.

Существует несколько способов реализации технологии нулевого копирования, таких как:

  • sendfile;
  • mmap;
  • splice;
  • прямое I/O (Direct I/O).

Каждый из этих методов подходит для разных сценариев использования. Ниже подробно рассматриваются методы sendfile, mmap и прямого I/O.

  • DMA: технология DMA позволяет копировать данные между памятью и другими компонентами без участия ЦП, который отвечает только за управление процессом.
  • sendfile: заменяет операции read и write одним системным вызовом, используя DMA и передачу файловых дескрипторов для реализации нулевого копирования.
  • mmap : заменяет только операцию read, отображая адресное пространство ядра в адресное пространство пользователя через DMA. Операция write выполняется непосредственно в пространстве ядра.
  • Прямое I/O без использования кэша страниц: операции чтения и записи выполняются непосредственно на диске или в сети без использования механизма кэширования страниц. Обычно используется вместе с пользовательским кешем. Прямое взаимодействие с диском или сетью через DMA обеспечивает нулевое копирование.

Sendfile

Метод sendfile подходит для случаев, когда данные считываются с диска и сразу передаются по сети без дополнительной обработки. Типичным примером является использование очередей сообщений. Традиционный подход требует четырёх копий данных и четырёх переключений контекста. Метод sendfile использует две технологии:

  • DMA;
  • передачу файлового дескриптора вместо копирования данных.

Использование DMA сокращает количество переключений контекста с четырёх до двух, как показано на следующем рисунке.

Благодаря DMA данные перемещаются напрямую из дискового пространства в память ядра и из памяти ядра в сетевое пространство. Перевод текста на русский язык:

Пользователю необходимо дождаться, пока данные из сокета будут считаны в буфер, прежде чем продолжить обработку полученных данных. В течение всего процесса запроса ввода-вывода пользовательский поток блокируется, что приводит к невозможности выполнения каких-либо действий при инициировании запроса ввода-вывода и недостаточному использованию ресурсов процессора.

Особенности: Процесс ввода-вывода состоит из двух этапов блокировки процесса.

  • Использование отдельного потока для поддержания соединения с сокетом приводит к увеличению нагрузки на виртуальную машину по мере увеличения количества соединений.
  • Поток используется для чтения данных, и поток блокируется при отсутствии доступных для чтения/записи данных, что может привести к потере ресурсов.

Преимущества:

  • Возможность немедленного возврата данных без задержек.
  • Простота программы, при этом блокировка процесса обычно не требует значительных затрат процессорного времени.

Недостатки:

  • Блокировка ввода-вывода оказывает значительное влияние на производительность.
  • Для каждого соединения требуется отдельный процесс/поток обработки, что приводит к значительным накладным расходам на память, потоки и переключение контекста процессора при большом количестве одновременных запросов. Это делает его менее популярным в среде разработки.

NIO (синхронный неблокирующий ввод-вывод)

На стороне сервера сохраняется список соединений сокетов, который затем опрашивается:

  • Если обнаружено, что в сокете есть доступные для чтения данные, это означает, что чтение готово, и вызывается соответствующий метод чтения для этого соединения сокета.
  • Если в сокете обнаружены доступные для записи данные, это указывает на то, что запись готова, и вызывается соответствующий метод записи для этого соединения сокета.
  • Если соединение сокета было прервано, вызывается соответствующий деструктор для закрытия порта.

Это позволяет эффективно использовать ресурсы сервера, значительно повышая эффективность. При выполнении запроса ввода-вывода используется отдельный поток для обработки. Это «запрос на поток». В Java используются Selector, Channel и Buffer для достижения этого эффекта.

  • Selector: Selector позволяет одному потоку обрабатывать несколько каналов. Если приложение открывает несколько подключений (каналов), но объём трафика каждого подключения невелик, использование Selector будет удобным. Чтобы использовать Selector, необходимо зарегистрировать канал в Selector, а затем вызвать его метод select, который будет блокироваться до тех пор, пока один из зарегистрированных каналов не будет готов. Как только этот метод вернётся, поток сможет обработать эти события, такие как новое подключение или получение данных.
  • Channel: Практически весь ввод-вывод в NIO начинается с канала. Канал похож на поток, данные могут быть прочитаны из канала в буфер или записаны из буфера в канал.
  • Buffer: Буфер — это, по сути, блок памяти, который можно читать и записывать данные. Его можно рассматривать как контейнерный объект (включая массив), предоставляющий набор методов для более удобного использования блока памяти. Буферный объект имеет встроенные механизмы для отслеживания и регистрации изменений состояния буфера. Channel обеспечивает доступ к файлам и сетевым данным, но все операции чтения и записи должны проходить через Buffer.

Пользователь должен постоянно вызывать read, пытаясь прочитать данные из сокета, пока чтение не завершится успешно, после чего он может продолжить обработку полученных данных. Хотя пользовательский поток может немедленно вернуться после каждого инициирования запроса ввода-вывода, ему всё равно приходится постоянно опрашивать и повторять запросы, потребляя значительные ресурсы процессора.

Особенности: В режиме неблокирующего ввода-вывода требуется постоянно запрашивать у ядра, готовы ли данные.

Преимущества:

  • Процесс может выполнять другие задачи во время ожидания завершения текущей задачи, избегая блокировки в ядре при ожидании данных, и каждый инициированный запрос ввода-вывода немедленно возвращается, обеспечивая хорошую оперативность.

Недостатки:

  • Постоянное опрашивание занимает много процессорного времени, снижая эффективность использования системных ресурсов и влияя на производительность, уменьшая общий объём данных, передаваемых через систему.
  • Эта модель не подходит для веб-серверов.

IO мультиплексирование (асинхронная блокировка ввода-вывода)

Используя Reactor, работа по опросу состояния операций ввода-вывода может быть объединена и передана обработчику событий handle_events для обработки. После регистрации обработчика событий пользовательский поток может продолжать выполнение других задач (асинхронно), в то время как поток Reactor отвечает за вызов функции select в ядре для проверки состояния сокета. Когда сокет активирован, он уведомляет соответствующий пользовательский поток (или функцию обратного вызова выполняемого пользовательского потока) для выполнения handle_event и обработки данных чтения и обработки.

Особенности: Механизм позволяет одновременно ожидать готовности нескольких файловых дескрипторов, и когда любой из этих файловых дескрипторов (дескрипторы сокетов) становится доступным для чтения, функция select()/poll() возвращает значение.

Преимущества:

  • Можно использовать один блокирующий объект для одновременного ожидания доступности чтения на нескольких дескрипторах вместо использования отдельных потоков (по одному на каждый дескриптор), что позволяет обрабатывать больше соединений.
  • Может сэкономить больше системных ресурсов.

Недостатки:

  • Если количество обрабатываемых соединений невелико, использование select/poll для веб-сервера не обязательно улучшит производительность по сравнению с использованием многопоточного подхода с блокирующим вводом-выводом.
  • Задержка может быть ещё больше, поскольку для обработки одного соединения необходимо выполнить два системных вызова.

AIO (асинхронный неблокирующий ввод-вывод)

AIO (NIO.2, асинхронный неблокирующий ввод-вывод). В модели асинхронного ввода-вывода пользовательский поток напрямую использует асинхронный API ввода-вывода ядра для инициирования запроса на чтение и сразу же возвращается к выполнению кода пользовательского потока. Однако в этот момент пользовательский поток уже зарегистрировал AsynchronousOperation и CompletionHandler в ядре, после чего операционная система запускает независимый поток ядра для обработки операций ввода-вывода. Когда данные для чтения становятся доступными, ядро считывает данные из сокета и записывает их в указанный пользователем буфер. Наконец, ядро распределяет данные чтения и обработчик завершения, зарегистрированный пользовательским потоком, среди внутренних Proactor. Proactor уведомляет пользовательский поток (обычно путём вызова функции завершения, зарегистрированной пользовательским потоком) о завершении асинхронного ввода-вывода.

Особенности: Первая и вторая фазы выполняются ядром.

Преимущества:

  • Позволяет эффективно использовать DMA, совмещая операции ввода-вывода с вычислениями, повышая производительность, эффективность использования ресурсов и параллелизм.

Недостатки:

  • Реализация в программе довольно сложна.
  • Чтобы реализовать настоящий асинхронный ввод-вывод, операционной системе необходимо проделать большую работу. На данный момент Windows реализует истинный асинхронный ввод-вывод через IOCP. В Linux 2.6 появился асинхронный ввод-вывод (AIO), который ещё не полностью разработан, поэтому высококонкурентное сетевое программирование в Linux в основном основано на модели мультиплексирования ввода-вывода (Reactor).

Сигнальный ввод-вывод

Сигнальный ввод-вывод означает, что процесс заранее сообщает ядру о том, что произошло изменение в одном из файловых дескрипторов. Ядро использует сигнал для уведомления процесса. В модели сигнального ввода-вывода процесс использует сокет для сигнального ввода-вывода и устанавливает функцию обработки сигнала SIGIO. Когда процесс вызывает системную функцию ввода-вывода через эту функцию обработки сигналов, ядро не готово предоставить данные, а вместо этого возвращает сигнал процессу. В этом случае процесс может продолжить выполнение других операций. То есть в первой фазе, когда ядро подготавливает данные, процесс не блокируется и может продолжать выполняться. Когда данные готовы, ядро отправляет сигнал SIGIO, чтобы уведомить пользовательское пространство о функции обработки сигнала, указывая, что данные готовы; в этом случае процесс вызовет системную функцию recvfrom, которая аналогична блокирующему вводу-выводу. То есть во второй фазе, когда данные копируются из ядра в пространство пользователя, процесс также блокируется.

Весь процесс сигнального ввода-вывода можно представить следующим образом:

Первая фаза (неблокирующая):

  • ①: Процесс использует сокет для сигнального ввода-вывода и настраивает функцию обработки сигнала SIGIO. Он вызывает системную функцию в ядре, которое, если данные не готовы, возвращает сигнал процессу, позволяя процессу продолжить выполнение других действий.
  • ②: Ядро загружает данные с диска в свою внутреннюю буферную область и затем отправляет сигнал SIGIO в пользовательское пространство для функции обработки сигнала.

Вторая фаза (блокирующая):

  • ③: После получения сигнала SIGIO процесс вызывает системную функцию (recvfrom).
  • ④: Ядро копирует данные из своей внутренней буферной области в буферную область процесса пользователя (фактическая операция ввода-вывода).
  • ⑤: Ядро возвращает успешное сообщение об обработке данных процессу; процесс обрабатывает данные после получения сообщения; после обработки процесс выходит из спящего режима и выполняет следующий запрос ввода-вывода.

Особенности: Используется сокет для реализации сигнального ввода-вывода и настройки функции обработки сигнала SIGIO.

Преимущества:

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

Недостатки:

  • Довольно сложно реализовать в программе.
  • Сигнальный ввод-вывод может столкнуться с переполнением очереди сигналов при обработке большого количества операций ввода-вывода, что затрудняет уведомление. Хотя сигнальный ввод-вывод полезен для обработки UDP-соединений, где сигналы указывают на прибытие данных или возврат асинхронной ошибки, для TCP он практически бесполезен, так как существует множество условий, вызывающих уведомления, каждое из которых требует больших ресурсов для определения, что снижает эффективность по сравнению с другими методами.

Существует два механизма уведомления о сигналах:

  • Горизонтальное срабатывание: после того как данные подготовлены в буферной зоне ядра, ядро уведомляет процесс. Если процесс занят, ядро повторно отправит уведомление. Этот процесс будет повторяться до тех пор, пока процесс не запросит системную функцию. Очевидно, что этот подход потребляет слишком много системных ресурсов.
  • Краевое срабатывание: ядро отправит только одно уведомление. TCP-заголовок

Порядковый номер: при установлении соединения компьютер генерирует случайное число, которое становится его начальным значением. Каждый раз, когда данные отправляются, к «количеству байтов данных» добавляется единица. Это помогает избежать проблемы с нарушением порядка пакетов в сети.

Номер подтверждения: указывает порядковый номер, который ожидается получить следующим. После получения этого номера подтверждения отправитель может считать, что все данные до этого номера были успешно получены. Это позволяет избежать потери пакетов.

Управляющие биты: * ACK — если бит установлен в 1, поле «номер подтверждения» становится активным. В TCP-соединении этот бит должен быть установлен в 1 за исключением первоначального SYN-пакета. * RST — если установлен в 1, это означает, что в соединении произошла ошибка, и оно должно быть принудительно разорвано. * SYN — если равен 1, указывает на желание установить соединение и устанавливает начальное значение порядкового номера в поле «порядковый номер». * FIN — если 1, означает, что данные больше не будут отправляться, и соединение должно быть закрыто. Когда обе стороны хотят закрыть соединение, они могут обмениваться пакетами с установленным в 1 битом FIN.

Модель OSI

OSI (Open System Interconnect) — это модель взаимодействия открытых систем. Обычно её называют моделью OSI или эталонной моделью OSI. Она была разработана Международной организацией по стандартизации (ISO) в 1985 году для улучшения совместимости сетей. Модель OSI определяет семь уровней, через которые проходят данные при передаче между компьютерами.

На физическом уровне происходит передача битов по каналу связи. Канальный уровень отвечает за передачу данных между двумя устройствами в одной сети. Сетевой уровень обеспечивает маршрутизацию пакетов между сетями. Транспортный уровень гарантирует надёжную передачу данных между конечными точками. Сеансовый уровень управляет сеансами связи между приложениями. Представления данных преобразуются на сеансовом уровне. На прикладном уровне работают приложения, которые обмениваются данными.

TCP/IP-модель

TCP/IP — это набор протоколов, используемых для передачи данных в компьютерных сетях. Он состоит из четырёх уровней:

  1. Уровень приложений: отвечает за взаимодействие между приложениями и предоставляет услуги более высоким уровням.
  2. Транспортный уровень: обеспечивает надёжную и упорядоченную передачу данных.
  3. Межсетевой уровень: отвечает за маршрутизацию и доставку пакетов между сетями.
  4. Канал передачи данных: определяет физический канал передачи и электрические сигналы.

В модели TCP/IP используются протоколы IP (Internet Protocol) для адресации и маршрутизации и TCP (Transmission Control Protocol) для обеспечения надёжной передачи данных.

Состояния TCP

Состояния TCP описывают этапы установления и поддержания соединения между клиентом и сервером. Вот основные состояния:

  • CLOSED: исходное состояние.
  • LISTEN: сервер ожидает подключения клиентов.
  • SYN_RCVD: сервер получил SYN-пакет от клиента.
  • SYN_SENT: клиент отправил SYN-пакет серверу.
  • ESTABLISHED: соединение установлено.
  • TIME_WAIT: клиент или сервер ожидают закрытия соединения.
  • CLOSE_WAIT: одна сторона ожидает закрытия от другой.

Чтобы просмотреть состояние TCP-соединения в Linux, можно использовать команду netstat -napt.

Состояние TIME_WAIT

Состояние TIME_WAIT используется для предотвращения проблем с повторным использованием номеров портов и обеспечением корректного завершения соединения. Если соединение закрывается активно, одна из сторон переходит в состояние TIME_WAIT. Это состояние длится около 60 секунд.

Если состояние TIME_WAIT занимает слишком много ресурсов, это может привести к проблемам с созданием новых соединений. Чтобы оптимизировать использование ресурсов, можно настроить параметры TCP. Однако изменение этих параметров требует доступа к исходному коду операционной системы.

Процесс установления TCP-соединения

Процесс установления соединения включает три этапа:

  1. Клиент отправляет SYN-пакет на сервер.
  2. Сервер отвечает SYN-ACK-пакетом.
  3. Клиент подтверждает получение ответа ACK-пакетом. Состояние
  • [Рисунок SYN+ACK сообщение]*

После того как клиент получает сообщение от сервера, он должен отправить ответное сообщение. В этом сообщении:

— в поле TCP заголовка ACK (подтверждение) устанавливается значение 1;

— поле «номер подтверждения» заполняется значением server_isn + 1.

Затем сообщение отправляется серверу. После этого клиент переходит в состояние ESTABLISHED.

Сервер после получения сообщения от клиента также переходит в состояние ESTABLISHED.

Третий этап квитирования: можно ли передавать данные?

Третий этап квитирования может передавать данные, в то время как первые два этапа не могут. Как только три этапа квитирования завершены, обе стороны находятся в состоянии ESTABLISHED, и соединение установлено. Клиент и сервер могут отправлять данные друг другу.

Предположим, что номер последовательности третьего этапа квитирования равен x + 1:

  1. Если есть передача данных: следующий отправленный клиентом пакет имеет номер последовательности, равный номеру, который сервер вернул в сообщении ACK.
  2. Если нет передачи данных: третий этап квитирования не использует номер последовательности. Следующий отправленный клиентом пакет будет иметь номер последовательности x + 1.

[Рисунок TCP-сервер — поток SYN-RECV]

[Рисунок TCP-клиент — поток SYN-SEND]

Сценарии:

  1. sk->sk_write_pending != 0. Значение по умолчанию равно 0, но при каких условиях оно становится ненулевым? Ответ заключается в том, что когда функция отправки данных в стеке протоколов сталкивается с состоянием, отличным от ESTABLISHED для сокета, она увеличивает это значение на 1 и пытается отправить данные через небольшой промежуток времени.

  2. icsk->icsk_accept_queue.rskq_defer_accept != 0. Клиент сначала привязывается к порту и IP-адресу, затем устанавливает опцию TCP_DEFER_ACCEPT, а затем подключается к серверу. В этот момент rskq_defer_accept будет установлен в 1, и ядро установит таймер для ожидания данных перед отправкой ответа ACK.

  3. icsk->icsk_ack.pingpong != 0. pingpong на самом деле является опцией сокета, которая указывает, является ли соединение интерактивным потоком данных. Когда его значение равно 1, это означает, что это интерактивный поток данных, и используется механизм подтверждения с задержкой.

Почему именно три этапа квитирования?

Не два и не четыре этапа?

Три этапа квитирования позволяют предотвратить установление старых соединений, уменьшить ненужное использование ресурсов обеих сторон и помочь синхронизировать начальные номера последовательности обеих сторон.

Причины:

  • Три этапа квитирования могут предотвратить повторное установление старых соединений (основная причина).
  • Три этапа квитирования помогают синхронизировать начальный номер последовательности каждой стороны.
  • Три этапа квитирования предотвращают ненужную трату ресурсов.

Причина 1: предотвращение повторного установления старых соединений

Если клиент отправляет несколько сообщений SYN подряд, то в случае перегрузки сети:

— старое сообщение SYN достигнет сервера раньше, чем последнее сообщение SYN;

— сервер ответит сообщением SYN + ACK;

— клиент, получив сообщение, может определить, является ли это старым соединением (истек срок действия номера последовательности или произошла задержка), и отправит сообщение RST на сервер, чтобы прервать текущее соединение.

[Рисунок Три этапа квитирования для предотвращения старых соединений]

Если используется двухэтапное квитирование, невозможно определить, является ли текущее соединение старым. Однако при трёхэтапном квитировании клиент может определить текущее соединение как старое при подготовке к отправке третьего сообщения.

Таким образом, основная причина использования трёхэтапного квитирования заключается в предотвращении установления старых соединений.

Причина 2: синхронизация начального номера последовательности

Обе стороны TCP должны поддерживать номер последовательности для надёжной передачи данных. Функции номера последовательности включают:

— удаление дублированных данных;

— упорядоченное получение пакетов данных;

— определение, какие данные были получены другой стороной.

Поэтому, когда клиент отправляет сообщение SYN с начальным номером последовательности, сервер отвечает сообщением ACK, указывая, что сообщение SYN было получено. Аналогично, когда сервер отправляет начальный номер последовательности клиенту, клиент также отвечает сообщением ACK. Таким образом, обе стороны могут надёжно синхронизировать свои начальные номера последовательностей.

Четырехэтапное рукопожатие также может надёжно синхронизировать номера последовательностей обеих сторон, но поскольку второй и третий этапы могут быть объединены в один этап, остаётся только три этапа.

Причина 3: избежать ненужного использования ресурсов

Если есть только два этапа квитирования, если клиентское сообщение SYN блокируется в сети, клиент не получит сообщение ACK и повторно отправит SYN. Сервер не знает, получил ли клиент сообщение ACK, поэтому он вынужден устанавливать новое соединение каждый раз, когда получает SYN. Это приводит к ненужному использованию ресурсов, так как сервер будет создавать множество ненужных подключений.

[Рисунок Избегание ненужного использования ресурсов]

То есть, при использовании двухэтапного квитирования сервер будет многократно принимать ненужные запросы на подключение SYN и выделять ресурсы без необходимости.

TCP четырёхэтапное рукопожатие

Этапы:

  1. Первый этап: FIN = 1, seq = u. После завершения передачи данных клиент переходит в состояние FIN_WAIT_1.
  2. Второй этап: ACK = 1, seq = v, ack = u + 1. После завершения сервер переходит в состояние CLOSE_WAIT. Клиент переходит в состояние FIN_WAIT_2 после получения пакета.
  3. Третий этап: FIN = 1, ACK = 1, seq = w, ack = u + 1. Сервер переходит в состояние LAST_ACK после завершения. Клиент переходит в состояние TIME_WAIT после получения пакета.
  4. Четвёртый этап: ACK = 1, seq = u + 1, ack = w + 1. Клиент получает запрос на закрытие соединения от сервера и отправляет подтверждающее сообщение. Затем клиент переходит в состояние TIME_WAIT и ожидает фиксированный период времени (два максимальных интервала жизни сегмента, 2MSL). Если клиент не получает подтверждающее сообщение от сервера в течение этого периода, считается, что сервер нормально закрыл соединение, и клиент также закрывает соединение и переходит в состояние CLOSED. Сервер получает подтверждающее сообщение и также переходит в состояние CLOSED.

Процесс четырёхэтапного рукопожатия:

  1. Клиент хочет закрыть соединение и отправляет сообщение с флагом FIN, установленным в 1 (сообщение FIN). Затем клиент переходит в состояние FIN_WAIT_1.
  2. Сервер получает сообщение и отвечает подтверждающим сообщением (ACK). Затем сервер переходит в состояние CLOSE_WAIT.
  3. Клиент получает подтверждающее сообщение и переходит в состояние FIN_WAIT_2.
  4. Сервер завершает обработку данных и отправляет сообщение FIN. Затем сервер переходит в состояние LAST_ACK.
  5. Клиент получает сообщение FIN и отвечает подтверждающим сообщением. Затем клиент переходит в состояние TIME_WAIT.
  6. Сервер получает подтверждающее сообщение и завершает закрытие соединения, переходя в состояние CLOSED.
  7. Через фиксированный интервал времени клиент автоматически переходит в состояние CLOSED, завершая закрытие соединения.

Почему требуется четыре этапа рукопожатия?

Повторный просмотр процесса отправки четырёх сообщений FIN показывает причину необходимости четырёх этапов.

Когда клиент хочет закрыть соединение, отправка сообщения FIN указывает только на то, что клиент больше не будет отправлять данные, но всё ещё может получать данные.

Получив сообщение FIN от клиента, сервер сначала отвечает подтверждающим сообщением, но сервер может иметь данные для отправки и обработки. Только после завершения обработки данных сервер отправляет сообщение FIN, соглашаясь закрыть соединение.

Из этого процесса видно, что сервер обычно должен ждать завершения обработки и отправки данных, поэтому сервер обычно отправляет ACK и FIN отдельно, что приводит к дополнительному этапу по сравнению с трёхэтапным рукопожатием.

Оптимизация TCP

Правильное и эффективное использование параметров TCP может повысить производительность TCP. Стратегии оптимизации будут рассмотрены с трёх точек зрения:

Оптимизация трёхэтапного рукопожатия

Оптимизация четырёхэтапного рукопожатия

Оптимизация передачи данных TCP

Часто задаваемые вопросы

Здесь представлен список часто задаваемых вопросов, связанных с TCP, без предоставления подробных ответов. TCP и UDP: в чём разница?

Соединение:

  • TCP — это транспортный протокол, ориентированный на соединение. Перед передачей данных необходимо установить соединение.
  • UDP не требует соединения и может передавать данные немедленно.

Объект обслуживания:

  • В TCP используется модель «один-к-одному», где каждое соединение имеет только два конечных пункта.
  • UDP поддерживает взаимодействие «один-к-одному», «один-ко-многим» и «многие-ко-многим».

Надёжность:

  • TCP обеспечивает надёжную доставку данных без ошибок, потерь, дублирования и с доставкой по требованию.
  • UDP старается обеспечить надёжность, но не гарантирует её.

Управление перегрузкой и потоком:

  • У TCP есть механизмы управления перегрузкой и потоком для обеспечения безопасности передачи данных.
  • У UDP таких механизмов нет, и даже при сильной перегрузке сети скорость отправки данных не снижается.

Длина заголовка:

  • Заголовок TCP имеет большую длину и связан с определёнными накладными расходами. Без использования поля опций длина заголовка составляет 20 байт, а с использованием — больше.
  • Длина заголовка UDP фиксирована и составляет всего 8 байт.

Способ передачи:

  • Данные в TCP передаются непрерывно, обеспечивая последовательность и надёжность.
  • В UDP данные передаются пакетами с границами, что может привести к потере пакетов и нарушению порядка.

Разделение данных:

  • Если размер данных в TCP превышает MSS (максимальный размер сегмента), они разделяются на уровне передачи. После получения данные также собираются на уровне передачи, и если один из сегментов потерян, требуется передать только его.
  • Если данные в UDP превышают MTU (максимальная единица передачи) по размеру, они разделяются на IP-уровне. После приёма данные собираются на IP-уровне, затем передаются на уровень передачи. Если сегмент потерян, необходимо повторно передать все данные, что снижает эффективность передачи. Обычно размер сообщений UDP должен быть меньше MTU.

ISN (идентификатор начального номера)

Почему идентификаторы начального номера у клиента и сервера разные?

Если уже использованное соединение повторно используется, старые сообщения могут остаться в сети. Если номера будут одинаковыми, невозможно будет определить, являются ли сообщения старыми или новыми. Это может вызвать путаницу в данных. Поэтому перед каждым новым соединением номер инициализируется заново, чтобы можно было отбросить сообщения, не принадлежащие текущему соединению. Также это делается для защиты от подделки номеров TCP-сообщений злоумышленниками.

Как генерируется идентификатор начального номера?

Начальный номер ISN основан на времени и обновляется каждые 4 мс + 1. Полный цикл занимает около 4,5 часов. RFC1948 предлагает алгоритм генерации ISN.

ISN = M + F (localhost, localport, remotehost, remoteport)

  • M — счётчик, который увеличивается на 1 каждые 4 миллисекунды.
  • F — хэш-функция, которая использует IP-адреса источника и назначения, а также порты источника и назначения для создания случайного числа. Для обеспечения безопасности хеш-функции используется MD5.

UDP

UDP и TCP обеспечивают различные уровни надёжности и скорости передачи данных, в зависимости от требований приложения.

Для приложений, требующих высокой надёжности, используется TCP. Он обеспечивает контроль ошибок, управление потоком и гарантированную доставку данных. Однако это может замедлить передачу данных.

В приложениях, где важна скорость передачи и допускается потеря данных, используется UDP. Он не обеспечивает надёжность, но позволяет быстро передавать данные.

Надёжность TCP

Надёжность в TCP обеспечивается следующими механизмами:

  1. Контрольная сумма: данные разбиваются на сегменты, и каждый сегмент получает контрольную сумму. При получении данных контрольная сумма проверяется, чтобы убедиться в отсутствии ошибок.

  2. Порядковые номера: каждому байту данных присваивается порядковый номер. Это позволяет упорядочить данные при их получении и устранить дублирование.

  3. Подтверждение приёма: после получения данных получатель отправляет подтверждение (ACK), содержащее порядковый номер следующего ожидаемого сегмента.

  4. Перезагрузка по таймауту: если отправитель не получает ACK в течение определённого времени, он предполагает потерю данных и повторно отправляет их.

  5. Управление соединением: включает в себя трёхэтапное рукопожатие и четырёхэтапное закрытие соединения.

  6. Контроль перегрузки: отправитель регулирует свою скорость передачи в соответствии с возможностями получателя.

  7. Управление перегрузкой: предотвращает перегрузку сети путём ограничения скорости отправки данных при возникновении проблем с перегрузкой.

Повышение эффективности TCP

Чтобы повысить эффективность TCP, используются следующие методы:

  • Сдвиговое окно: позволяет отправлять несколько сегментов данных без ожидания подтверждения каждого из них. Это ускоряет передачу данных.

  • Быстрая перезагрузка: при потере данных отправитель быстро повторно отправляет только потерянные сегменты.

  • Задержка подтверждения: получатель задерживает отправку подтверждения, чтобы увеличить размер окна отправки и ускорить передачу данных.

  • «Натравливание» подтверждения: подтверждение отправляется вместе с данными в одном пакете, что также ускоряет процесс.

Обработка перегрузок в TCP

TCP обрабатывает перегрузки с помощью следующих этапов:

  • Медленный старт: начальная фаза, когда отправитель медленно увеличивает скорость отправки.

  • Избегание перегрузок: отправитель следит за перегрузками и регулирует скорость отправки соответственно.

  • Быстрая перезагрузка: при потере данных отправитель быстро повторно отправляет потерянные сегменты.

  • Быстрое восстановление: после потери данных отправитель временно уменьшает размер окна отправки, чтобы избежать повторной перегрузки.

Работа на основе TCP

Работа на основе TCP включает следующие шаги:

  • Инициализация сокета на клиенте и сервере.

  • Привязка сервера к IP-адресу и порту.

  • Запуск сервера в режиме прослушивания.

  • Ожидание клиентом подключения к серверу.

  • Подключение клиента к серверу через соединение.

  • Получение сервером сокета для обмена данными.

  • Чтение и запись данных между клиентом и сервером.

  • Закрытие соединения при завершении работы клиента. accept отправляет в каком шаге трёхэтапного рукопожатия?

Мы сначала посмотрим, что было отправлено при подключении клиента к серверу:

  • Клиентский протокол стека отправляет на сервер SYN-пакет и сообщает серверу текущий номер отправки client_isn. Клиент переходит в состояние SYNC_SENT.

  • После получения пакета сервер отвечает клиенту ACK-ответом, значение которого равно client_isn + 1, что подтверждает SYN-пакет клиента. Сервер также отправляет SYN-пакет, сообщая клиенту текущий номер отправки server_isn, и переходит в состояние SYNC_RCVD.

  • Когда клиент получает ACK, приложение выходит из вызова connect, указывая на успешное установление однонаправленного соединения с сервером. Состояние клиента — ESTABLISHED. Клиентский протокол стека также отвечает на SYN-пакет сервера, значение ответа — server_isn + 1.

  • Ответный пакет достигает сервера, и серверный протокол стека возвращает блокирующий вызов accept. В этот момент однонаправленное соединение с клиентом также успешно установлено, и сервер переходит в состояние ESTABLISHED.

Из описанного процесса мы можем сделать вывод, что успешное возвращение клиентского вызова connect происходит во время второго рукопожатия, а успешное возвращение серверного вызова accept — после завершения третьего рукопожатия.

Что происходит, когда клиент вызывает close и соединение разрывается?

Рассмотрим ситуацию, когда клиент активно вызывает функцию close:

  • Клиент вызывает close, указывая, что данные больше не нужно отправлять. Затем он переходит в состояние FIN_WAIT_1.

  • Сервер получает FIN-пакет. TCP-протокол стека сервера добавляет файл-терминатор EOF в буфер приёма. Приложение может обнаружить этот FIN-пакет через вызов read. Этот EOF будет помещён в очередь других принятых данных, ожидающих обработки. Это означает, что сервер должен обработать эту исключительную ситуацию, поскольку EOF указывает на то, что дополнительных данных по этому соединению больше не будет. Сервер переходит в состояние CLOSE_WAIT.

  • Затем, после обработки данных, сервер естественным образом считывает EOF и также вызывает close для закрытия своего сокета. Это приводит к отправке FIN-пакета, и сервер переходит в состояние LAST_ACK.

  • Получив FIN-пакет от сервера, клиент отправляет ACK-подтверждение и переходит в состояние TIME_WAIT.

  • После того как сервер получает ACK-подтверждение, он переходит в конечное состояние CLOSE.

  • Через 2 MSL (максимальное время жизни сегмента) клиент также переходит в состояние CLOSE. ``` /* 运行到这里,说明有新连接到来,则等待新的传输控制块 */ error = wait_for_connect(sk, timeo); if (error) goto out; }

req = tp->accept_queue; if ((tp->accept_queue = req->dl_next) == NULL) tp->accept_queue_tail = NULL;

newsk = req->sk; sk_acceptq_removed(sk); tcp_openreq_fastfree(req); ...

return newsk; }


**Три раза «рукопожатия»**

* **Клиент отправляет SYN-сегмент:**
  * Из tcp_v4_connect() -> tcp_connect() -> tcp_transmit_skb().
  * Устанавливается в TCP_SYN_SENT.
  * Код:
   ```c
   /* Построение и отправка SYN-сегмента */
   int tcp_connect(struct sock *sk) {
       struct tcp_sock *tp = tcp_sk(sk);
       struct sk_buff *buff;

       tcp_connect_init(sk); /* Инициализация членов, связанных с подключением, в блоке управления передачей */

       /* Выделяем буфер для сегмента SYN */
       buff = alloc_skb(MAX_TCP_HEADER + 15, sk->sk_allocation);
       if (unlikely(buff == NULL))
           return -ENOBUFS;

       /* Резервируем место для заголовков */
       skb_reserve(buff, MAX_TCP_HEADER);

       TCP_SKB_CB(buff)->flags = TCPCB_FLAG_SYN;
       TCP_ECN_send_syn(sk, tp, buff);
       TCP_SKB_CB(buff)->sacked = 0;
       skb_shinfo(buff)->tso_segs = 1;
       skb_shinfo(buff)->tso_size = 0;
       buff->csum = 0;
       TCP_SKB_CB(buff)->seq = tp->write_seq++;
       TCP_SKB_CB(buff)->end_seq = tp->write_seq;
       tp->snd_nxt = tp->write_seq;
       tp->pushed_seq = tp->write_seq;
       tcp_ca_init(tp);

       /* Отправляем его */
       TCP_SKB_CB(buff)->when = tcp_time_stamp;
       tp->retrans_stamp = TCP_SKB_CB(buff)->when;

       /* Добавляем буфер в очередь отправки */
       __skb_queue_tail(&sk->sk_write_queue, buff);
       sk_charge_skb(sk, buff);
       tp->packets_out += tcp_skb_pcount(buff);

       /* Передаём SYN-сегмент */
       tcp_transmit_skb(sk, skb_clone(buff, GFP_KERNEL));
       TCP_INC_STATS(TCP_MIB_ACTIVEOPENS);

       /* Запускаем таймер повторной передачи */
       tcp_reset_xmit_timer(sk, TCP_TIME_RETRANS, tp->rto);
       return 0;
   }
  • Сервер получает SYN и отправляет SYN+ACK:

    • Через tcp_v4_do_rcv() -> tcp_rcv_state_process() -> tcp_v4_conn_request() -> tcp_v4_send_synack().

    • tcp_v4_send_synack():

    • tcp_make_synack(sk, dst, req): создание SYN+ACK на основе маршрута, блока управления передачей и запроса на подключение.

    • ip_build_and_send_pkt(): создание IP-пакета и отправка.

    • Код:

    /* Отправка SYN+ACK клиенту */
    static int tcp_v4_send_synack(struct sock *sk, struct open_request *req,
                         struct dst_entry *dst) {
        int err = -1;
        struct sk_buff * skb;
    
        /* Сначала получаем маршрут */
        if (!dst && (dst = tcp_v4_route_req(sk, req)) == NULL)
            goto out;
    
        /* Создаём SYN+ACK */
        skb = tcp_make_synack(sk, dst, req);
    
        if (skb) { /* Создание SYN+ACK успешно */
            struct tcphdr *th = skb->h.th;
    
            /* Вычисляем контрольную сумму */
            th->check = tcp_v4_check(th, skb->len,
                        req->af.v4_req.loc_addr,
                        req->af.v4_req.rmt_addr,
                        csum_partial((char *)th, skb->len,
                                skb->csum));
    
            /* Создаём IP-пакет и отправляем */
            err = ip_build_and_send_pkt(skb, sk, req->af.v4_req.loc_addr,
                             req->af.v4_req.rmt_addr,
                             req->af.v4_req.opt);
            if (err == NET_XMIT_CN)
                err = 0;
        }
    
        out:
            dst_release(dst);
            return err;
    }
  • Клиент отвечает ACK:

    • Через tcp_v4_do_rcv() -> tcp_rcv_state_process(). Клиент находится в состоянии TCP_SYN_SENT.
    • tcp_rcv_synsent_state_process(): обработка сегментов, полученных в состоянии SYN_SENT.
    • tcp_ack(): обработка полученного ACK.
    • tcp_send_ack(): отправка ACK серверу при активном подключении и обновление окна.
    • tcp_urg(sk, skb, th): обработка внеполосных данных после второго рукопожатия.
    • tcp_data_snd_check(sk): проверка наличия данных для отправки.
    • tcp_write_xmit(): отправка сегментов из очереди отправки. sock *sk, struct sk_buff *skb, struct tcphdr *th, unsigned len) { struct tcp_sock *tp = tcp_sk(sk); int saved_clamp = tp->rx_opt.mss_clamp;

    /* 解析TCP选项并保存到传输控制块中 */ tcp_parse_options(skb, &tp->rx_opt, 0);

    if (th->ack) {/* 处理ACK标志 / / rfc793: * "If the state is SYN-SENT then * first check the ACK bit * If the ACK bit is set * If SEG.ACK =< ISS, or SEG.ACK > SND.NXT, send * a reset (unless the RST bit is set, if so drop * the segment and return)" * * We do not send data with SYN, so that RFC-correct * test reduces to: */ if (TCP_SKB_CB(skb)->ack_seq != tp->snd_nxt) goto reset_and_undo;

    if (tp->rx_opt.saw_tstamp && tp->rx_opt.rcv_tsecr &&
        !between(tp->rx_opt.rcv_tsecr, tp->retrans_stamp,
             tcp_time_stamp)) {
        NET_INC_STATS_BH(LINUX_MIB_PAWSACTIVEREJECTED);
        goto reset_and_undo;
    }
    
    /* Now ACK is acceptable.
     *
     * "If the RST bit is set
     *    If the ACK was acceptable then signal the user "error:
     *    connection reset", drop the segment, enter CLOSED state,
     *    delete TCB, and return."
     */
    
    if (th->rst) {/* 收到ACK+RST段,需要tcp_reset设置错误码,并关闭套接口 */
        tcp_reset(sk);
        goto discard;
    }
    
    /* rfc793:
     *   "fifth, if neither of the SYN or RST bits is set then
     *    drop the segment and return."
     *
     *    See note below!
     *                                        --ANK(990513)
     */
    if (!th->syn)/* 在SYN_SENT状态下接收到的段必须存在SYN标志,否则说明接收到的段无效,丢弃该段 */
        goto discard_and_undo;
    
    /* rfc793:
     *   "If the SYN bit is on ...
     *    are acceptable then ...
     *    (our SYN has been ACKed), change the connection
     *    state to ESTABLISHED..."
     */
    
    /* 从首部标志中获取显示拥塞通知的特性 */
    TCP_ECN_rcv_synack(tp, th);
    if (tp->ecn_flags&TCP_ECN_OK)/* 如果支持ECN,则设置标志 */
        sk->sk_no_largesend = 1;
    
    /* 设置与窗口相关的成员变量 */
    tp->snd_wl1 = TCP_SKB_CB(skb)->seq;
    tcp_ack(sk, skb, FLAG_SLOWPATH);
    
    /* Ok.. it's good. Set up sequence numbers and
     * move to established.
     */
    tp->rcv_nxt = TCP_SKB_CB(skb)->seq + 1;
    tp->rcv_wup = TCP_SKB_CB(skb)->seq + 1;
    
    /* RFC1323: The window in SYN & SYN/ACK segments is
     * never scaled.
     */
    tp->snd_wnd = ntohs(th->window);
    tcp_init_wl(tp, TCP_SKB_CB(skb)->ack_seq, TCP_SKB_CB(skb)->seq);
    
    if (!tp->rx_opt.wscale_ok) {
        tp->rx_opt.snd_wscale = tp->rx_opt.rcv_wscale = 0;
        tp->window_clamp = min(tp->window_clamp, 65535U);
    }
    
    if (tp->rx_opt.saw_tstamp) {/* 根据是否支持时间戳选项来设置传输控制块的相关字段 */
        tp->rx_opt.tstamp_ok       = 1;
        tp->tcp_header_len =
            sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED;
        tp->advmss      -= TCPOLEN_TSTAMP_ALIGNED;
        tcp_store_ts_recent(tp);
    } else {
        tp->tcp_header_len = sizeof(struct tcphdr);
    }
    
    /* 初始化PMTU、MSS等成员变量 */
    if (tp->rx_opt.sack_ok && sysctl_tcp_fack)
        tp->rx_opt.sack_ok |= 2;
    
    tcp_sync_mss(sk, tp->pmtu_cookie);
    tcp_initialize_rcv_mss(sk);
    
    /* Remember, tcp_poll() does not lock socket!
     * Change state from SYN-SENT only after copied_seq
     * is initialized. */
    tp->copied_seq = tp->rcv_nxt;
    mb();
    tcp_set_state(sk, TCP_ESTABLISHED);
    
    /* Make sure socket is routed, for correct metrics.  */
    tp->af_specific->rebuild_header(sk);
    
    tcp_init_metrics(sk);
    
    /* Prevent spurious tcp_cwnd_restart() on first data
     * packet.
     */
    tp->lsndtime = tcp_time_stamp;
    
    tcp_init_buffer_space(sk);
    
    /* 如果启用了连接保活,则启用连接保活定时器 */
    if (sock_flag(sk, SOCK_KEEPOPEN))
        tcp_reset_keepalive_timer(sk, keepalive_time_when(tp));
    
    if (!tp->rx_opt.snd_wscale)/* 首部预测 */
        __tcp_fast_path_on(tp, tp->snd_wnd);
    else
        tp->pred_flags = 0;
    
    if (!sock_flag(sk,

}} sock_dead)) {

/* Если состояние сокета не SOCK_DEAD, то пробудить ожидающий данный интерфейс процесс */ sk->sk_state_change(sk); sk_wake_async(sk, 0, POLL_OUT); }

/* Соединение установлено, в зависимости от ситуации переходим в режим отложенного подтверждения / if (sk->sk_write_pending || tp->defer_accept || tp->ack.pingpong) { / Сохраняем один ACK. Данные будут готовы через несколько тиков, если установлен sk_write_pending. * * Его можно удалить, но с этой функцией tcpdumps выглядят так замечательно, что я не смог устоять перед искушением 8) --ANK */ tcp_schedule_ack(tp); tp->ack.lrcvtime = tcp_time_stamp; tp->ack.ato = TCP_ATO_MIN; tcp_incr_quickack(tp); tcp_enter_quickack_mode(tp); tcp_reset_xmit_timer(sk, TCP_TIME_DACK, TCP_DELACK_MAX);

discard: __kfree_skb(skb); return 0; } else { /* Не нужно отложенное подтверждение, немедленно отправляем ACK */ tcp_send_ack(sk); } return -1; }

/* В сегменте нет ACK */

if (th->rst) { /* Получен сегмент RST, отбрасываем блок управления передачей / / rfc793:

  • "Если установлен бит RST
  •  В противном случае (без ACK) отбросьте сегмент и вернитесь."

*/ goto discard_and_undo; }

/* Проверка PAWS. / / Проверка PAWS не удалась, также отбрасываем блок управления передачей */ if (tp->rx_opt.ts_recent_stamp && tp->rx_opt.saw_tstamp && tcp_paws_check(&tp->rx_opt, 0)) goto discard_and_undo;

/* Получен SYN-сегмент без ACK в состоянии SYN_SENT / if (th->syn) { / Мы видим SYN без ACK. Это попытка одновременного подключения с перекрещенными SYNs.

  • В частности, это может быть подключение к себе. / tcp_set_state(sk, TCP_SYN_RECV); / Устанавливаем состояние TCP_SYN_RECV */

if (tp->rx_опт.saw_tstamp) { /* Устанавливаем поля, связанные с отметкой времени */ tp->rx_opt.tstamp_ok = 1; tcp_store_ts_recent(tp); tp->tcp_header_len = sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED; } else { tp->tcp_header_len = sizeof(struct tcphdr); }

/* Инициализируем переменные, связанные с окном */ tp->rcv_nxt = TCP_SKB_CB(skb)->seq + 1; tp->rcv_wup = TCP_SKB_CB(skb)->seq + 1;

/* RFC1323: Окно в сегментах SYN & SYN/ACK никогда не масштабируется. */ tp->snd_wnd = ntohs(th->window); tp->snd_wl1 = TCP_SKB_CB(skb)->seq; tp->max_window = tp->snd_wnd;

TCP_ECN_rcv_syn(tp, th); /* Извлекаем характеристики явного уведомления о перегрузке из заголовка. */ if (tp->ecn_flags&TCP_ECN_OK) sk->sk_no_largesend = 1;

/* Инициализация переменных, связанных с MSS */ tcp_sync_mss(sk, tp->pmtu_cookie); tcp_initialize_rcv_mss(sk);

/* Отправляем SYN+ACK и отбрасываем полученный SYN / tcp_send_synack(sk); #if 0 / Обратите внимание, мы могли бы принять данные и URG из этого сегмента.

  • Нет никаких препятствий для этого.
  • Однако, если иногда мы игнорируем данные в сегментах без подтверждения, у нас нет причин принимать их иногда.
  • Кроме того, кажется, что код, делающий это на шаге 6 tcp_rcv_state_process, не безупречен. Поэтому отбросьте пакет для здравомыслия.
  • Раскомментируйте это возвращение, чтобы обработать данные. / return -1; #else goto discard; #endif } / «пятое, если ни бит SYN, ни RST не установлены, отбросьте сегмент и вернитесь». */

discard_and_undo: tcp_clear_options(&tp->rx_opt); tp->rx_opt.mss_clamp = saved_clamp; goto discard;

reset_and_undo: tcp_clear_options(&tp->rx_opt); tp->rx_opt.mss_clamp = saved_clamp; return 1; } ``` NET_INC_STATS_BH(LINUX_MIB_PAWSESTABREJECTED); tcp_send_dupack(sk, skb); goto discard; } /* Reset is accepted even if it did not pass PAWS. */ }

  /* step 1: check sequence number */
  if (!tcp_sequence(tp, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq)) {/* TCP段序号无效 */
      if (!th->rst)/* 如果TCP段无RST标志,则发送DACK给对方 */
          tcp_send_dupack(sk, skb);
      goto discard;
  }

  /* step 2: check RST bit */
  if(th->rst) {/* 如果有RST标志,则重置连接 */
      tcp_reset(sk);
      goto discard;
  }

  /* 如果有必要,则更新时间戳 */
  tcp_replace_ts_recent(tp, TCP_SKB_CB(skb)->seq);

  /* step 3: check security and precedence [ignored] */

  /*  step 4:
   *
   *  Check for a SYN in window.
   */
  if (th->syn && !before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) {/* 如果有SYN标志并且序号在接收窗口内 */
      NET_INC_STATS_BH(LINUX_MIB_TCPABORTONSYN);
      tcp_reset(sk);/* 复位连接 */
      return 1;
  }

  /* step 5: check the ACK field */
  if (th->ack) {/* 如果有ACK标志 */
      /* 检查ACK是否为正常的第三次握手 */
      int acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH);

      switch(sk->sk_state) {
      case TCP_SYN_RECV:
          if (acceptable) {
              tp->copied_seq = tp->rcv_nxt;
              mb();
              /* 正常的第三次握手,设置连接状态为TCP_ESTABLISHED */
              tcp_set_state(sk, TCP_ESTABLISHED);
              sk->sk_state_change(sk);

              /* Note, that this wakeup is only for marginal
               * crossed SYN case. Passively open sockets
               * are not waked up, because sk->sk_sleep ==
               * NULL and sk->sk_socket == NULL.
               */
              if (sk->sk_socket) {/* 状态已经正常,唤醒那些等待的线程 */
                  sk_wake_async(sk,0,POLL_OUT);
              }

              /* 初始化传输控制块,如果存在时间戳选项,同时平滑RTT为0,则需计算重传超时时间 */
              tp->snd_una = TCP_SKB_CB(skb)->ack_seq;
              tp->snd_wnd = ntohs(th->window) <<
                        tp->rx_opt.snd_wscale;
              tcp_init_wl(tp, TCP_SKB_CB(skb)->ack_seq,
                      TCP_SKB_CB(skb)->seq);

              /* tcp_ack considers this ACK as duplicate
               * and does not calculate rtt.
               * Fix it at least with timestamps.
               */
              if (tp->rx_opt.saw_tstamp && tp->rx_opt.rcv_tsecr &&
                  !tp->srtt)
                  tcp_ack_saw_tstamp(tp, 0);

              if (tp->rx_opt.tstamp_ok)
                  tp->advmss -= TCPOLEN_TSTAMP_ALIGNED;

              /* Make sure socket is routed, for
               * correct metrics.
               */
              /* 建立路由,初始化拥塞控制模块 */
              tp->af_specific->rebuild_header(sk);

              tcp_init_metrics(sk);

              /* Prevent spurious tcp_cwnd_restart() on
               * first data packet.
               */
              tp->lsndtime = tcp_time_stamp;/* 更新最近一次发送数据包的时间 */

              tcp_initialize_rcv_mss(sk);
              tcp_init_buffer_space(sk);
              tcp_fast_path_on(tp);/* 计算有关TCP首部预测的标志 */
          } else {
              return 1;
          }
          break;
      .....
      }
  } else
      goto discard;
  .....

  /* step 6: check the URG bit */
  tcp_urg(sk, skb, th);/* 检测带外数据位 */

  /* tcp_data could move socket to TIME-WAIT */
  if (sk->sk_state != TCP_CLOSE) {/* 如果tcp_data需要发送数据和ACK则在这里处理 */
      tcp_data_snd_check(sk);
      tcp_ack_snd_check(sk);
  }

  if (!queued) { /* 如果段没有加入队列,或者前面的流程需要释放报文,则释放它 */

discard: __kfree_skb(skb); } return 0; }


**HTTP 缓存**

HTTP 缓存的好处?

- 减少亢余的数据传输,节约资源。
- 缓解服务器压力,提高网站性能。
- 加快客户加载网页的速度。

Нежелательные способы использования без кэширования:

- Ctrl + F5 принудительно обновляет и всегда напрямую извлекает данные с сервера. **Кэш браузера: классификация и обновление**

*Нажмите F5 для обновления или используйте кнопку обновления в браузере, добавив Cache-Control: max-age=0 по умолчанию.*

**Классификация кэша браузера:**

— Строгий кэш 200 (from memory cache) и 200 (from disk cache).

— Согласительный кэш 304 (Not Modified).

**Стратегии обновления операций кэша:**

1. Обычная операция: строгий кэш эффективен, согласительный кэш эффективен.

2. Ручное обновление: строгий кэш неэффективен, согласительный кэш эффективен.

3. Принудительное обновление: строгий кэш неэффективен, согласительный кэш неэффективен.

**Процесс кэширования:**

Строгий кэш имеет приоритет над согласительным кэшем. Если строгий кэш (Expires и Cache-Control) действует, то данные берутся из кэша напрямую. Если нет, то происходит согласительное кэширование (Last-Modified / If-Modified-Since и Etag / If-None-Match), решение об использовании кэша принимает сервер. Если согласительное кэширование не работает, это означает, что запрос кэша не удался, и необходимо повторно получить результат запроса и сохранить его в кэше браузера; если оно работает, возвращается 304, и продолжается использование кэша. Основной процесс выглядит следующим образом:

!

**Строгий кэш**

Если включено строгое кэширование, при запросе ресурсов не будет отправляться запрос на сервер, а данные будут считываться непосредственно из кэша. В консоли Chrome можно увидеть статус 200 с указанием from disk cache или from memory cache. Разница между ними заключается в том, где находится кэш.

*Pragma:* в HTTP1.1 больше не используется.

*Cache-Control:* заголовок информации, появившийся в HTTP1.1. Устанавливает срок действия (абсолютное время, точка времени), и после истечения этого срока ресурс считается устаревшим. Однако местное время может быть изменено пользователем, поэтому могут возникнуть проблемы.

  *max-age = x (в секундах):* содержимое кэша становится недействительным через x секунд, например, Cache-Control:max-age=36000.

  *no-cache:* клиент кэширует содержимое, но решение о том, использовать ли кэш, принимается после согласования кэширования.

  *no-store:* все содержимое не кэшируется, т. е. не используется ни строгое, ни согласительное кэширование.

  *private:* всё содержимое может кэшироваться только клиентом, значение по умолчанию для Cache-Control.

  *public:* всё содержимое будет кэшировано (клиент и прокси-сервер могут кэшировать).

*Expires:* спецификация HTTP1.0, её значение представляет собой строку времени GMT в формате абсолютного времени. Устанавливается срок действия (относительное время, временной интервал), который определяет продолжительность, не зависящую от местного времени, в течение которого кэш действителен.

  *То же самое в Response Headers.*

  *Также контролирует срок действия кэша.*

  *Уже заменён Cache-Control.*

*Примечание:* порядок приоритета для вступления в силу: Pragma > Cache-Control > Expires.

**Pragma**

В HTTP1.1 больше не используется.

**Cache-Control**

!

*Шаг 1:* браузер инициирует запрос впервые, кэш пуст, сервер отвечает:

!

Браузер кэширует этот ответ, срок действия составляет 100 секунд с момента получения этого ответа.

*Шаг 2:* через 10 секунд браузер снова инициирует запрос, обнаруживает, что кэш не истёк, браузер вычисляет Age: 10, затем использует кэш напрямую, здесь непосредственно берёт кэш из памяти, from disk берёт кэш с диска:

!

*Шаг 3:* через 100 секунд браузер снова инициирует запрос, обнаруживая, что кэш истёк, отправляет запрос на проверку кэша на сервер. Если сервер считает, что файл изменился, он возвращает 1; в противном случае он не возвращает данные файла, а сразу возвращает 304:

!

**Expires**

Expires — это поле, которое управляет сроком действия веб-кэша в HTTP/1.0. Его значение — это время, когда сервер возвращает результат этого запроса, и при повторной отправке запроса, если местное время клиента меньше значения Expires, данные кэша используются напрямую.

Expires является полем HTTP/1.0, но современные браузеры по умолчанию используют HTTP/1.1, так что в HTTP/1.1 кеш всё ещё управляется Expires? В HTTP/1.1 Expires был заменён на Cache-Control, потому что принцип работы Expires заключается в сравнении местного времени клиента с временем, возвращаемым сервером, и если из-за некоторых факторов (разница часовых поясов, неточное местное или серверное время) возникает ошибка, то строгое кэширование становится бессмысленным.

**Согласительный кэш**

Согласительное кэширование (сравнительное кэширование) контролируется сервером для определения доступности кэша ресурсов, поэтому клиент и сервер должны общаться через определённые идентификаторы, чтобы сервер мог определить, можно ли использовать кэшированный ресурс, что включает в себя следующие два набора заголовков. Эти две группы появляются парами, то есть первый запрос имеет определённый заголовок (Last-Modified или Etag), а последующий запрос будет иметь соответствующий заголовок (If-Modified-Since или If-None-Match). Если в ответе нет заголовка Last-Modified или Etag, то в запросе также не будет соответствующего заголовка.

*Обратите внимание:* Last-Modified и ETag можно использовать вместе, сервер сначала проверит ETag, и только если они совпадают, проверит Last-Modified, и наконец решит, возвращать ли 304.

**ETag / If-None-Match**

!

*ETag:* сервер возвращает уникальный идентификатор текущего файла ресурсов при ответе на запрос (сгенерированный сервером). Например:

!

*If-None-Match:* клиент снова инициирует тот же запрос и добавляет Etag, полученный в предыдущем запросе, к запросу, сообщая серверу, что ресурс был возвращён в последнем запросе Etag. Сервер получает этот запрос и обнаруживает, что он содержит If-None-Match, сравнивает значение поля If-None-Match с Etag ресурса сервера, и если они совпадают, возвращает 304, указывая, что ресурс не обновлён и продолжает использовать данные кэша; если они не совпадают, ресурс возвращается заново, статус 200:

!

*Обратите внимание:* Etag/If-None-Match имеет более высокий приоритет, чем Last-Modified/If-Modified-Since, и оба существуют, только Etag/If-None-Match вступает в силу.

**Last-Modified / If-Modified-Since**

!

*Last-Modified:* сервер отвечает на запрос и возвращает время последнего изменения файла ресурсов на сервере. Например:

!

*If-Modified-Since:* клиент инициирует тот же запрос снова и добавляет Last-Modified из предыдущего запроса к запросу, информируя сервер о времени последнего изменения ресурса в предыдущем запросе. Сервер получает запрос и обнаруживает наличие If-Modified-Since. Он сравнивает время в If-Modified-Since с последним изменённым временем ресурса на сервере. Если последнее изменённое время больше, чем время в If-Modified-Since, ресурс будет возвращён заново со статусом 200; в противном случае будет возвращено 304, указывающее, что ресурс не был изменён и можно продолжать использовать данные кэша:

!

**Общие вопросы**

Вопрос 1: зачем нужен Etag?

Вы можете подумать, что использования Last-Modified достаточно, чтобы браузер знал, достаточно ли свежи локальные данные кэша, зачем нужен Etag? Основная причина появления Etag в HTTP1.1 заключается в решении нескольких проблем, которые трудно решить с помощью Last-Modified:

* Некоторые файлы могут периодически изменяться, но их содержимое остаётся неизменным (изменяется только время модификации), в этом случае мы не хотим, чтобы клиент думал, что файл был изменён, и повторно получал его;
* некоторые файлы изменяются очень часто, например, изменяются N раз за 1 секунду (точность сравнения If-Modified-Since составляет секунды);
* некоторые серверы не могут точно получить время последнего изменения файлов.

Вопрос 2: Что произойдёт, если не установлена никакая стратегия кэширования?

Если не установлены никакие стратегии кэширования, нет Cache-Control и нет Expires, браузер будет использовать алгоритм эвристики (LM-Factor), обычно беря 10% разницы между Date и Last-Modified в качестве времени кэширования. **Описание**

GET — GET-запрос отображает указанный ресурс. Обычно метод GET используется только для чтения данных и не должен использоваться для операций, которые могут вызвать побочные эффекты и являются неэквивалентными. Предполагается, что запрос будет безопасным и эквивалентным. Здесь безопасность означает, что запрос не влияет на состояние ресурса.

POST — отправляет данные на указанный ресурс для обработки запроса (например, отправка формы или загрузка файла). Данные содержатся в теле запроса. POST-запросы могут привести к созданию нового ресурса или изменению существующего.

PUT — PUT-запрос заменяет содержимое указанного ресурса своим последним содержимым. Метод PUT является эквивалентным методом. С помощью этого метода клиент может отправить последние данные указанного ресурса на сервер, чтобы заменить содержимое ресурса.

PATCH — метод PATCH появился позже, он был определён в стандарте RFC 5789 в 2010 году. PATCH-запросы похожи на PUT-запросы, но используются для обновления ресурсов. Они отличаются следующим:
* PATCH обычно используется для частичного обновления ресурса, а PUT — для полного обновления.
* Если ресурс не существует, PATCH создаст новый ресурс, а PUT обновит существующий.

DELETE — DELETE-запрос используется для запроса удаления сервера ресурса, указанного URI (унифицированный идентификатор ресурса, Uniform Resource Identifier). После DELETE-запроса указанный ресурс будет удалён. Метод DELETE также является эквивалентным.

OPTIONS — позволяет клиенту просматривать возможности сервера.

CONNECT — зарезервирован для прокси-серверов, способных преобразовывать соединения в режим канала в протоколе HTTP/1.1.

HEAD — похож на GET-запрос, но возвращает только заголовки ответа, без содержимого. Используется для получения заголовков.

TRACE — отображает полученный запрос, в основном используется для тестирования и диагностики.

**Заголовки запросов**
| Название | Назначение |
| --- | --- |
| Authorization | Используется для установки информации аутентификации. |
| User-Agent | Идентификатор пользователя, например, тип ОС и версия браузера. |
| If-Modified-Since | Значение равно значению Last-Modified, возвращённому сервером при предыдущем запросе. Используется для проверки, был ли ресурс изменён с момента последнего запроса, и если нет изменений (304), то данные берутся из кэша. |
| If-None-Match | Значение равно ETag, возвращённому сервером при последнем запросе. Обычно используется вместе с If-Modified-Since. |
| Cookie | Существующие файлы cookie. |
| Referer | Указывает на адрес, с которого был сделан запрос. Например, если вы переходите с одной страницы на другую, значение будет адресом первой страницы. |
| Host | Адрес и порт запрашиваемого сервера. |

**Заголовки ответов**
| Название | Назначение |
| --- | --- |
| Date | Дата сервера. |
| Last-Modified | Время последнего изменения ресурса. |
| Transfer-Encoding | Обычно имеет значение chunked. Используется, когда размер данных ответа не может быть определён заранее, обычно также присутствует заголовок Content-Encoding. |
| Set-Cookie | Устанавливает файлы cookie. |
| Location | Перенаправляет на другой URL. Например, ввод адреса в браузере приведёт к автоматическому переходу на https://www.baidu.com, который контролируется этим заголовком ответа. |
| Server | Сервер, обрабатывающий запрос. |

**Коды состояния**
HTTP-коды состояния состоят из трёх десятичных цифр. Первая цифра определяет тип кода состояния, а две другие цифры не имеют классификации. HTTP-коды состояния делятся на пять типов:

| Тип | Описание |
| ---- | -------- |
| 1xx | Информация, сервер получил запрос и требует от клиента продолжить операцию. |
| 2xx | Успех, операция была успешно принята и обработана. |
| 3xx | Перенаправление, требуется дополнительная операция для завершения запроса. |
| 4xx | Ошибка клиента, запрос содержит синтаксическую ошибку или не может завершить запрос. |
| 5xx | Ошибка сервера, сервер столкнулся с ошибкой во время обработки запроса. |
Обычно достаточно знать несколько распространённых кодов, таких как 200, 400, 401, 403, 404, 500, 502.

**Процесс запроса**
Процесс запроса веб-страницы браузером включает следующие шаги:
1. Сначала DNS-сервер преобразует доменное имя в IP-адрес, используя IP и маску подсети, чтобы определить, принадлежит ли он той же подсети.
2. Создаётся HTTP-сообщение запроса, добавляется TCP/UDP-заголовок на транспортном уровне, IP-заголовок на сетевом уровне и Ethernet-заголовок на канальном уровне.
3. Данные проходят через маршрутизаторы и коммутаторы, пока не достигнут целевого сервера. Целевой сервер также анализирует данные и получает HTTP-сообщение. Затем он отвечает соответствующим образом.

**Общие вопросы**
**HTTP 1.1 и HTTP 2**
* HTTP 1.1:
    * Постоянное соединение.
    * Запрос конвейеризации.
    * Улучшенное управление кешем (новые поля, такие как cache-control).
    * Добавление поля Host и поддержка фрагментации передачи.
* HTTP 2:
    * Двоичное разделение на кадры.
    * Мультиплексирование (или совместное использование соединения).
    * Сжатие заголовка.
    * Серверная push-технология.

**HTTP и HTTPS**
(1) Для HTTPS-протокола необходимо получить сертификат у центра сертификации (CA), бесплатные сертификаты встречаются редко, поэтому это требует определённых затрат.
(2) HTTP — это протокол передачи гипертекста, информация передаётся в виде открытого текста, а HTTPS — это защищённый протокол SSL-шифрования, обеспечивающий безопасную передачу данных.
(3) HTTP и HTTPS используют совершенно разные способы подключения, и используемые порты также различаются: первый — 80, второй — 443.
(4) Соединение HTTP простое и без сохранения состояния; HTTPS-протокол представляет собой сетевой протокол, построенный на основе SSL+HTTP, который обеспечивает зашифрованную передачу и аутентификацию личности, более безопасный, чем HTTP.

**Симметричное и асимметричное шифрование**
Симметричное шифрование использует один и тот же ключ для шифрования и дешифрования данных, и основная проблема этого метода заключается в том, как безопасно передать ключ другой стороне.
Асимметричное шифрование использует пару ключей: открытый ключ и закрытый ключ. Открытый ключ можно свободно распространять, но закрытый ключ известен только владельцу. Отправитель использует открытый ключ получателя для шифрования данных, а получатель использует свой закрытый ключ для расшифровки данных.
Поскольку асимметричное шифрование не требует отправки закрытого ключа, оно обеспечивает большую безопасность; однако по сравнению с симметричным шифрованием оно медленнее, поэтому мы всё ещё используем симметричное шифрование для передачи сообщений, но можем использовать асимметричное шифтирование для отправки используемых ключей.

**Распространённые коды состояния**
1××: обработка запроса продолжается, запрос принят и обрабатывается.
2××: запрос выполнен успешно, запрос успешно обработан (200 OK).
3××: перенаправление, для выполнения запроса требуется дальнейшая обработка (301: постоянное перенаправление, 302: временное перенаправление, 304: данные уже кэшированы).
4××: ошибка клиента, запрос недействителен (400: Bad Request, запрос имеет синтаксические ошибки, 403: отказ в доступе, 404: клиент пытается получить доступ к странице, которой не существует).
5××: ошибка сервера, сервер не может обработать действительный запрос (500: внутренняя ошибка сервера, 503: сервер недоступен, пожалуйста, повторите попытку позже).

**Сеансы и файлы cookie**
* Сеанс находится на сервере, файл cookie — на клиенте (браузере).
* По умолчанию сеанс хранится в файле на сервере (не в памяти).
* Работа сеанса зависит от идентификатора сеанса, который содержится в файле cookie, то есть, если браузер отключил файлы cookie, и одновременно

В этом тексте не было обнаружено ошибок перевода. **Сеанс также может быть недействительным (но это можно реализовать другими способами, например, передав session_id в URL)**

- Сеанс можно разместить в файле, базе данных или памяти.
- Для этой ситуации обычно используется сеанс проверки подлинности пользователя.

# OS

## Процессор

Существует четыре распространённых типа процессоров: X86, ARM, MIPS и PowerPC.

### X86

X86 — это архитектура микропроцессора, разработанная Intel. Если вы не понимаете, о чём идёт речь, то, когда я скажу слова вроде 8086 или 80286, вы сразу поймёте, что такое X86. Сегодня большинство наших ПК используют архитектуру X86. Это связано с доминирующим положением Intel на рынке.

Архитектура X86 использует CISC (Complex Instruction Set Computer) — сложную систему команд. В отличие от RISC, в архитектуре X86 инструкции выполняются последовательно, а операции внутри каждой инструкции также выполняются последовательно. Преимущество последовательного выполнения заключается в простоте управления, но коэффициент использования компонентов компьютера невысок, а скорость выполнения низкая.

**Преимущества:**
* Скорость: одна инструкция выполняет множество функций, и количество инструкций относительно невелико.
* Низкая пропускная способность: даже при высокой частоте работы не требуется большая пропускная способность для передачи инструкций в CPU.
* Простота управления: поскольку инструкции в архитектуре X86 выполняются последовательно, управление проще.
* Промышленное применение: процессоры, используемые крупными производителями данных, такими как AMD и Intel, основаны на архитектуре X86.

**Недостатки:**
* Общий регистр: в наборе инструкций X86 всего восемь общих регистров. Поэтому выполнение большинства операций в системе происходит с использованием данных из памяти, а не регистров. Это замедляет работу всей системы. Архитектура RISC обычно имеет много общих регистров и использует такие технологии, как перекрывающиеся окна регистров и регистровые стеки, чтобы эффективно использовать ресурсы регистров.
* Декодирование: это уникальная функция процессора X86. Его роль заключается в преобразовании длинных инструкций X86 в фиксированные по длине инструкции, подобные RISC, и передаче их в ядро RISC. Декодирование делится на аппаратное декодирование и микродекодирование. Простые инструкции X86 требуют только аппаратного декодирования, которое выполняется быстро, в то время как сложные инструкции требуют микродекодирования, которое разбивает их на несколько простых инструкций и выполняется медленно и сложно.
* Небольшой диапазон адресов: ограничивает потребности пользователей.
* Коэффициент использования компонентов компьютера невелик, а скорость выполнения низка.

### ARM

ARM расшифровывается как Advanced RISC Machine (усовершенствованная машина с сокращённым набором команд). Это 32-битная архитектура с упрощённым набором инструкций, которая также поддерживает 16-битный набор инструкций. По сравнению с эквивалентным 32-битным кодом, он экономит около 35% и сохраняет все преимущества 32-битной системы.

**Преимущества:**
* Компактный размер, низкое энергопотребление, низкая стоимость и высокая производительность — основные причины широкого применения ARM во встраиваемых системах.
* Поддержка Thumb (16 бит) / ARM (32 бита), возможность хорошо поддерживать 8-битные / 16-битные устройства.
* Большое количество регистров, быстрая скорость выполнения инструкций.
* Большинство операций с данными выполняются в регистрах.
* Гибкий и простой способ адресации, высокая эффективность выполнения.
* Конвейерная обработка.
* Фиксированная длина инструкций, большинство инструкций могут быть выполнены за один такт, легко проектируются суперскалярные и конвейерные процессоры.

**Недостатки:**
* Производительность немного хуже, чем у X86. Чтобы достичь производительности X86, частота должна быть намного выше, но более высокая частота приводит к значительному увеличению энергопотребления, что сводит на нет преимущества ARM.

### MIPS

MIPS (Microprocessor without interlocked piped stages architecture) — это процессорная архитектура, использующая упрощённый набор команд (RISC). Она была разработана в 1981 году компанией MIPS Technologies и широко используется в различных электронных устройствах, сетевых устройствах, персональных развлекательных устройствах и коммерческих устройствах. Оригинальная архитектура MIPS была 32-разрядной, а последние версии стали 64-разрядными. Основные характеристики включают большое количество регистров, инструкций и символов, а также видимые задержки в конвейере. Эти характеристики позволяют архитектуре MIPS обеспечивать максимальную производительность на квадратный миллиметр и самое низкое энергопотребление в современных SoC-дизайнах.

**Преимущества:**
* Поддержка 64-битных инструкций и операций, в отличие от ARM, который пока ограничен 32-битными.
* Наличие специального делителя, способного выполнять инструкции деления.
* Больше внутренних регистров по сравнению с ARM, что позволяет MIPS потреблять меньше энергии при той же производительности или обеспечивать более высокую производительность при том же энергопотреблении.
* Инструкции немного сложнее, чем в ARM, но немного более гибкие.
* MIPS является открытым исходным кодом.

**Недостатки:**
* Проблемы с начальной загрузкой в памяти и кэше, которые ограничивают поддержку больших объёмов памяти в текущих процессорах MIPS.
* Будущее направление развития MIPS — параллельные потоки, аналогичные Intel Hyperthreading, в то время как будущее направление развития ARM — физические ядра. На данный момент кажется, что физическое ядро имеет преимущество перед параллельными потоками.
* Хотя структура MIPS проще, она всё ещё однонаправленная, в то время как ARM уже эволюционировала до неупорядоченной двунаправленной.

### PowerPC

## Управление памятью

### Виртуальная память

Однокристальный микрокомпьютер не имеет операционной системы, поэтому после написания программы её необходимо записать с помощью инструмента, прежде чем программа сможет работать. Кроме того, центральный процессор (CPU) однокристального микрокомпьютера напрямую управляет физической памятью («физический адрес»).

В этом случае невозможно одновременно запускать две программы. Если первая программа записывает новое значение в позицию 2000, это перезапишет все данные второй программы, хранящиеся в этой позиции, и обе программы немедленно завершатся сбоем.

**Решение операционной системы**

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

Операционные системы вводят концепцию виртуальной памяти, и каждый процесс имеет свой собственный виртуальный адрес, который обрабатывается блоком управления памятью (MMU). Блок управления памятью преобразует виртуальный адрес в физический адрес, а затем получает доступ к памяти через этот физический адрес. Существуют два типа адресов:

* Мы используем адрес памяти в программе, называемый виртуальным адресом памяти (Virtual Memory Address).
* Реальное пространство, существующее в оборудовании, называется физическим адресом памяти (Physical Memory Address).

Способ, которым операционная система управляет отношениями между виртуальными и физическими адресами, включает:
* Разделение сегментов (сегментация) (представлено ранее).
* Разделение страниц.

### Разделение сегментов

Программа состоит из нескольких логических сегментов, таких как код сегмента, сегмент данных, стек и куча. Различные сегменты имеют разные атрибуты, поэтому они разделены (сегментированы).

**① Отображение виртуальных и физических адресов в сегментации**

Отображение виртуальных адресов в сегментации состоит из двух частей: фактора выбора сегмента и смещения внутри сегмента.

Фактор выбора сегмента хранится в регистре сегмента. Наиболее важным элементом в факторе выбора сегмента является номер сегмента, который служит индексом таблицы сегментов. Таблица сегментов содержит информацию о сегменте, такую как базовый адрес сегмента, предел сегмента и уровень привилегий.

Смещение внутри сегмента должно находиться между 0 и пределом сегмента. Если смещение внутри сегмента допустимо, базовый адрес сегмента добавляется к смещению внутри сегмента для получения физического адреса памяти.

**② Отображение виртуального адреса на физический**

Как упоминалось ранее, известно, что виртуальный адрес разделяется на четыре сегмента в сегментации, и каждый сегмент имеет запись в таблице сегментов. В таблице сегментов есть элемент, содержащий базовый адрес сегмента и другую информацию. Используя базовый адрес сегмента плюс смещение внутри сегмента, можно получить физический адрес памяти. Например, если мы хотим получить доступ к виртуальному адресу в сегменте 3 со смещением 500, мы можем вычислить физический адрес как базовый адрес сегмента 3 (7000) плюс смещение 500 (500), что даёт нам 7500. Сегментация хороша тем, что она позволяет программам не заботиться о конкретных физических адресах памяти, но у неё есть некоторые недостатки:
* **Создание фрагментации памяти.**
* **Низкая эффективность обмена памятью.**

**③ Фрагментация памяти**

Фрагментация памяти включает две проблемы:
* Внешняя фрагментация: образуется множество несмежных небольших блоков физической памяти, что делает невозможным загрузку новых программ.
* Внутренняя фрагментация: хотя вся память программы загружена, часть памяти программы может использоваться редко, что также приводит к потере памяти.

**Решения:**
* Решение внешней фрагментации — обмен памятью. Сначала запишите программу, занимающую память, на диск, а затем загрузите её обратно в память (переместите в другое место).

**④ Низкая эффективность обмена памяти**

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

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/yu120-lemon-guide.git
git@api.gitlife.ru:oschina-mirror/yu120-lemon-guide.git
oschina-mirror
yu120-lemon-guide
yu120-lemon-guide
main