OS
Введение: сбор информации о технологиях, связанных с JDK Tools, Linux Tools, Git и т. д.!
[TOC]
В Linux/Unix обычно используются следующие модели ввода-вывода: блокирующий ввод-вывод (Blocking I/O), неблокирующий ввод-вывод (Non-Blocking I/O), мультиплексированный ввод-вывод (I/O Multiplexing), сигналоуправляемый ввод-вывод (Signal Driven I/O) (используется редко) и асинхронный ввод-вывод (Asynchronous I/O). Основные операции сетевого ввода-вывода включают в себя ядро и процессы, и их можно разделить на два этапа:
Запрос ввода-вывода можно разделить на две фазы: фазу вызова и фазу выполнения.
Блокирование и неблокирование происходят, когда ядро ожидает готовности данных. Они указывают, нужно ли ждать ответа во время ожидания.
Синхронизация и асинхронность происходят при взаимодействии ядра и процесса. Они определяют, должен ли процесс ожидать или опрашивать результаты после запуска операции ввода-вывода.
При использовании блокирующего ввода-вывода пользовательский поток блокируется после вызова read. Он будет ждать, пока ядро скопирует данные из дискового буфера в пользовательский буфер, прежде чем read вернёт результат. Блокировка включает в себя два этапа:
Приложение инициирует запрос ввода-вывода к ядру. Поток, инициировавший запрос, блокируется до тех пор, пока не получит ответ от ядра. Один запрос ввода-вывода соответствует одному потоку. Однако ресурсы потоков ограничены и ценны, поэтому создание слишком большого количества потоков увеличивает накладные расходы на переключение контекста.
Неблокирующий режим позволяет read немедленно возвращаться, даже если данные ещё не готовы. В этом случае приложение постоянно опрашивает ядро, чтобы узнать, готовы ли данные. Если данные не готовы, ядро немедленно возвращает ошибку EWOULDBLOCK. Как только данные становятся доступными, ядро копирует их в буфер приложения, и read получает результат.
Обратите внимание, что последний вызов read для получения данных является синхронным процессом, который требует ожидания. Здесь синхронизация относится к процессу копирования данных из ядра в буфер приложения.
По сравнению с блокирующим вводом-выводом неблокирующий ввод-вывод значительно повышает производительность, но большое количество системных вызовов во время опроса приводит к высоким накладным расходам на переключение контекста. Поэтому использование неблокирующего ввода-вывода отдельно не очень эффективно, и его производительность ухудшается по мере увеличения параллелизма.
Если данные недоступны при использовании неблокирующего режима, приложение тратит ресурсы процессора на опрос ядра. Можно ли избежать опроса и получать уведомления, когда данные готовы? Мультиплексированный ввод-выход решает эту проблему. Select, poll и epoll являются реализациями мультиплексированного ввода-вывода.
Мультиплексирование позволяет одному потоку обрабатывать несколько дескрипторов ввода-вывода одновременно. Многоканальный означает наличие нескольких каналов данных, а мультиплексирование означает использование одного или нескольких фиксированных потоков для обработки каждого сокета. Select, poll и epoll — это конкретные реализации мультиплексирования ввода-вывода. Один вызов select может получить статус данных в нескольких каналах ядра. Мультиплексирование ввода-вывода решает проблемы синхронного блокирующего и синхронного неблокирующего режимов и является высокопроизводительной моделью ввода-вывода.
Независимо от того, используется ли блокирующий или неблокирующий режим, read и send являются синхронными вызовами. Во время read ядро копирует данные из своего буфера в буфер приложения; этот процесс требует ожидания и называется синхронизацией. Если ядро неэффективно копирует данные, read будет долго ждать в процессе синхронизации.
Сигналоуправляемый ввод-вывод — это полуасинхронная модель ввода-вывода. Когда данные готовы, ядро отправляет сигнал SIGIO, чтобы уведомить приложение о начале чтения данных.
Асинхронный ввод-вывод отличается тем, что оба процесса — ожидание готовности данных и копирование данных из ядра в приложение — выполняются без блокировки. После запуска aio_read приложение немедленно возвращается, и ядро автоматически копирует данные в буфер приложения. Этот процесс также выполняется асинхронно, и приложению не нужно активно инициировать копирование.
Основное отличие асинхронного ввода-вывода от сигналоуправляемого заключается в том, что асинхронный ввод-вывод уведомляет приложение, когда операция ввода-вывода завершена, в то время как сигналоуправляемый ввод-вывод сообщает приложению, когда операцию ввода-вывода можно начать.
Режим Reactor — это модель, которая отслеживает события ввода-вывода и распределяет их соответствующим образом. Основными компонентами являются Reactor и пул обработки ресурсов:
Reactor является гибким и может адаптироваться к различным сценариям использования. Его гибкость заключается в следующем:
Теоретически существует четыре возможных комбинации:
При увеличении fd
эффективность IO
снижается, так как каждый вызов требует линейного сканирования и перебора всех элементов. Это приводит к замедлению работы по мере увеличения количества fd
.
Преимущества:
select()
обладает лучшей переносимостью. В некоторых системах Unix не поддерживается poll()
.select()
обеспечивает более высокую точность значений тайм-аута: микросекунды вместо миллисекунд у poll
.Poll
По сути, poll
и select
похожи. Poll
копирует массив, переданный пользователем, в пространство ядра, затем проверяет состояние каждого fd
, связанного с устройством. Если устройство готово, оно добавляется в очередь устройств ожидания. После обхода всех fd
и отсутствия готовых устройств процесс приостанавливается до готовности устройства или истечения времени ожидания. Затем процесс возобновляется и снова проходит по всем fd
. Этот цикл может повторяться несколько раз без необходимости. Особенностью poll
является «горизонтальный триггер»: если после уведомления о состоянии fd
оно не обрабатывается, то при следующем вызове poll
уведомление будет выдано повторно.
Недостатки:
fd
массивов полностью копируются между пользовательским пространством и пространством ядра, независимо от того, имеет ли это смысл.poll
необходимо опросить pollfd
для получения информации о готовых дескрипторах.Достоинства:
select
, poll
работает быстрее при обработке большого количества дескрипторов файлов.Epoll
поддерживает горизонтальный и вертикальный триггеры. Его основная особенность — вертикальный триггер, который сообщает процессу только о тех fd
, которые недавно стали готовыми, и делает это только один раз. Ещё одна особенность заключается в использовании механизма уведомлений, основанного на событиях, через epoll_ctl
. Когда fd
готов, ядро использует механизм обратного вызова, похожий на callback
, чтобы активировать fd
, и epoll_wait
может получать уведомления.
Достоинства:
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) без изменений. Без оптимизации чтение данных с диска, их передача через сеть и запись в буфер будут сопровождаться четырьмя копиями данных и четырьмя переключениями контекста, как показано на рисунке ниже.
Если оптимизация не применяется, процесс чтения данных с диска, передачи через сеть и записи в буфер будет выглядеть следующим образом:
read
;read
;write
;write
.Проблема заключается в том, что ЦП постоянно занят перемещением данных, а также в том, что скорость перемещения данных между диском, сетью и памятью значительно ниже скорости работы ЦП.
Технология нулевого копирования предполагает, что при выполнении операций данные не копируются многократно, а управление ими осуществляется напрямую. Однако это не означает, что данные вообще не копируются: если данные изначально находятся не в памяти, они должны быть скопированы туда перед использованием.
Существует несколько способов реализации технологии нулевого копирования, таких как:
sendfile
;mmap
;splice
;Direct I/O
).Каждый из этих методов подходит для разных сценариев использования. Ниже подробно рассматриваются методы sendfile
, mmap
и прямого I/O.
sendfile
: заменяет операции read
и write
одним системным вызовом, используя DMA и передачу файловых дескрипторов для реализации нулевого копирования.mmap
: заменяет только операцию read
, отображая адресное пространство ядра в адресное пространство пользователя через DMA. Операция write
выполняется непосредственно в пространстве ядра.Метод sendfile
подходит для случаев, когда данные считываются с диска и сразу передаются по сети без дополнительной обработки. Типичным примером является использование очередей сообщений. Традиционный подход требует четырёх копий данных и четырёх переключений контекста. Метод sendfile
использует две технологии:
Использование DMA сокращает количество переключений контекста с четырёх до двух, как показано на следующем рисунке.
Благодаря DMA данные перемещаются напрямую из дискового пространства в память ядра и из памяти ядра в сетевое пространство. Перевод текста на русский язык:
Пользователю необходимо дождаться, пока данные из сокета будут считаны в буфер, прежде чем продолжить обработку полученных данных. В течение всего процесса запроса ввода-вывода пользовательский поток блокируется, что приводит к невозможности выполнения каких-либо действий при инициировании запроса ввода-вывода и недостаточному использованию ресурсов процессора.
Особенности: Процесс ввода-вывода состоит из двух этапов блокировки процесса.
Преимущества:
Недостатки:
На стороне сервера сохраняется список соединений сокетов, который затем опрашивается:
Это позволяет эффективно использовать ресурсы сервера, значительно повышая эффективность. При выполнении запроса ввода-вывода используется отдельный поток для обработки. Это «запрос на поток». В Java используются Selector, Channel и Buffer для достижения этого эффекта.
Пользователь должен постоянно вызывать read, пытаясь прочитать данные из сокета, пока чтение не завершится успешно, после чего он может продолжить обработку полученных данных. Хотя пользовательский поток может немедленно вернуться после каждого инициирования запроса ввода-вывода, ему всё равно приходится постоянно опрашивать и повторять запросы, потребляя значительные ресурсы процессора.
Особенности: В режиме неблокирующего ввода-вывода требуется постоянно запрашивать у ядра, готовы ли данные.
Преимущества:
Недостатки:
Используя Reactor, работа по опросу состояния операций ввода-вывода может быть объединена и передана обработчику событий handle_events для обработки. После регистрации обработчика событий пользовательский поток может продолжать выполнение других задач (асинхронно), в то время как поток Reactor отвечает за вызов функции select в ядре для проверки состояния сокета. Когда сокет активирован, он уведомляет соответствующий пользовательский поток (или функцию обратного вызова выполняемого пользовательского потока) для выполнения handle_event и обработки данных чтения и обработки.
Особенности: Механизм позволяет одновременно ожидать готовности нескольких файловых дескрипторов, и когда любой из этих файловых дескрипторов (дескрипторы сокетов) становится доступным для чтения, функция select()/poll() возвращает значение.
Преимущества:
Недостатки:
AIO (NIO.2, асинхронный неблокирующий ввод-вывод). В модели асинхронного ввода-вывода пользовательский поток напрямую использует асинхронный API ввода-вывода ядра для инициирования запроса на чтение и сразу же возвращается к выполнению кода пользовательского потока. Однако в этот момент пользовательский поток уже зарегистрировал AsynchronousOperation и CompletionHandler в ядре, после чего операционная система запускает независимый поток ядра для обработки операций ввода-вывода. Когда данные для чтения становятся доступными, ядро считывает данные из сокета и записывает их в указанный пользователем буфер. Наконец, ядро распределяет данные чтения и обработчик завершения, зарегистрированный пользовательским потоком, среди внутренних Proactor. Proactor уведомляет пользовательский поток (обычно путём вызова функции завершения, зарегистрированной пользовательским потоком) о завершении асинхронного ввода-вывода.
Особенности: Первая и вторая фазы выполняются ядром.
Преимущества:
Недостатки:
Сигнальный ввод-вывод означает, что процесс заранее сообщает ядру о том, что произошло изменение в одном из файловых дескрипторов. Ядро использует сигнал для уведомления процесса. В модели сигнального ввода-вывода процесс использует сокет для сигнального ввода-вывода и устанавливает функцию обработки сигнала SIGIO. Когда процесс вызывает системную функцию ввода-вывода через эту функцию обработки сигналов, ядро не готово предоставить данные, а вместо этого возвращает сигнал процессу. В этом случае процесс может продолжить выполнение других операций. То есть в первой фазе, когда ядро подготавливает данные, процесс не блокируется и может продолжать выполняться. Когда данные готовы, ядро отправляет сигнал SIGIO, чтобы уведомить пользовательское пространство о функции обработки сигнала, указывая, что данные готовы; в этом случае процесс вызовет системную функцию recvfrom, которая аналогична блокирующему вводу-выводу. То есть во второй фазе, когда данные копируются из ядра в пространство пользователя, процесс также блокируется.
Весь процесс сигнального ввода-вывода можно представить следующим образом:
Первая фаза (неблокирующая):
Вторая фаза (блокирующая):
Особенности: Используется сокет для реализации сигнального ввода-вывода и настройки функции обработки сигнала SIGIO.
Преимущества:
Недостатки:
Существует два механизма уведомления о сигналах:
Порядковый номер: при установлении соединения компьютер генерирует случайное число, которое становится его начальным значением. Каждый раз, когда данные отправляются, к «количеству байтов данных» добавляется единица. Это помогает избежать проблемы с нарушением порядка пакетов в сети.
Номер подтверждения: указывает порядковый номер, который ожидается получить следующим. После получения этого номера подтверждения отправитель может считать, что все данные до этого номера были успешно получены. Это позволяет избежать потери пакетов.
Управляющие биты: * 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 — это набор протоколов, используемых для передачи данных в компьютерных сетях. Он состоит из четырёх уровней:
В модели TCP/IP используются протоколы IP (Internet Protocol) для адресации и маршрутизации и TCP (Transmission Control Protocol) для обеспечения надёжной передачи данных.
Состояния TCP
Состояния TCP описывают этапы установления и поддержания соединения между клиентом и сервером. Вот основные состояния:
Чтобы просмотреть состояние TCP-соединения в Linux, можно использовать команду netstat -napt
.
Состояние TIME_WAIT
Состояние TIME_WAIT используется для предотвращения проблем с повторным использованием номеров портов и обеспечением корректного завершения соединения. Если соединение закрывается активно, одна из сторон переходит в состояние TIME_WAIT. Это состояние длится около 60 секунд.
Если состояние TIME_WAIT занимает слишком много ресурсов, это может привести к проблемам с созданием новых соединений. Чтобы оптимизировать использование ресурсов, можно настроить параметры TCP. Однако изменение этих параметров требует доступа к исходному коду операционной системы.
Процесс установления TCP-соединения
Процесс установления соединения включает три этапа:
После того как клиент получает сообщение от сервера, он должен отправить ответное сообщение. В этом сообщении:
— в поле TCP заголовка ACK (подтверждение) устанавливается значение 1;
— поле «номер подтверждения» заполняется значением server_isn + 1.
Затем сообщение отправляется серверу. После этого клиент переходит в состояние ESTABLISHED.
Сервер после получения сообщения от клиента также переходит в состояние ESTABLISHED.
Третий этап квитирования: можно ли передавать данные?
Третий этап квитирования может передавать данные, в то время как первые два этапа не могут. Как только три этапа квитирования завершены, обе стороны находятся в состоянии ESTABLISHED, и соединение установлено. Клиент и сервер могут отправлять данные друг другу.
Предположим, что номер последовательности третьего этапа квитирования равен x + 1:
[Рисунок TCP-сервер — поток SYN-RECV]
[Рисунок TCP-клиент — поток SYN-SEND]
Сценарии:
sk->sk_write_pending != 0. Значение по умолчанию равно 0, но при каких условиях оно становится ненулевым? Ответ заключается в том, что когда функция отправки данных в стеке протоколов сталкивается с состоянием, отличным от ESTABLISHED для сокета, она увеличивает это значение на 1 и пытается отправить данные через небольшой промежуток времени.
icsk->icsk_accept_queue.rskq_defer_accept != 0. Клиент сначала привязывается к порту и IP-адресу, затем устанавливает опцию TCP_DEFER_ACCEPT, а затем подключается к серверу. В этот момент rskq_defer_accept будет установлен в 1, и ядро установит таймер для ожидания данных перед отправкой ответа ACK.
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 и выделять ресурсы без необходимости.
Этапы:
Процесс четырёхэтапного рукопожатия:
Почему требуется четыре этапа рукопожатия?
Повторный просмотр процесса отправки четырёх сообщений FIN показывает причину необходимости четырёх этапов.
Когда клиент хочет закрыть соединение, отправка сообщения FIN указывает только на то, что клиент больше не будет отправлять данные, но всё ещё может получать данные.
Получив сообщение FIN от клиента, сервер сначала отвечает подтверждающим сообщением, но сервер может иметь данные для отправки и обработки. Только после завершения обработки данных сервер отправляет сообщение FIN, соглашаясь закрыть соединение.
Из этого процесса видно, что сервер обычно должен ждать завершения обработки и отправки данных, поэтому сервер обычно отправляет ACK и FIN отдельно, что приводит к дополнительному этапу по сравнению с трёхэтапным рукопожатием.
Правильное и эффективное использование параметров TCP может повысить производительность TCP. Стратегии оптимизации будут рассмотрены с трёх точек зрения:
Оптимизация трёхэтапного рукопожатия
Оптимизация четырёхэтапного рукопожатия
Оптимизация передачи данных TCP
Здесь представлен список часто задаваемых вопросов, связанных с TCP, без предоставления подробных ответов. TCP и UDP: в чём разница?
Соединение:
Объект обслуживания:
Надёжность:
Управление перегрузкой и потоком:
Длина заголовка:
Способ передачи:
Разделение данных:
Почему идентификаторы начального номера у клиента и сервера разные?
Если уже использованное соединение повторно используется, старые сообщения могут остаться в сети. Если номера будут одинаковыми, невозможно будет определить, являются ли сообщения старыми или новыми. Это может вызвать путаницу в данных. Поэтому перед каждым новым соединением номер инициализируется заново, чтобы можно было отбросить сообщения, не принадлежащие текущему соединению. Также это делается для защиты от подделки номеров TCP-сообщений злоумышленниками.
Как генерируется идентификатор начального номера?
Начальный номер ISN основан на времени и обновляется каждые 4 мс + 1. Полный цикл занимает около 4,5 часов. RFC1948 предлагает алгоритм генерации ISN.
ISN = M + F (localhost, localport, remotehost, remoteport)
UDP и TCP обеспечивают различные уровни надёжности и скорости передачи данных, в зависимости от требований приложения.
Для приложений, требующих высокой надёжности, используется TCP. Он обеспечивает контроль ошибок, управление потоком и гарантированную доставку данных. Однако это может замедлить передачу данных.
В приложениях, где важна скорость передачи и допускается потеря данных, используется UDP. Он не обеспечивает надёжность, но позволяет быстро передавать данные.
Надёжность в TCP обеспечивается следующими механизмами:
Контрольная сумма: данные разбиваются на сегменты, и каждый сегмент получает контрольную сумму. При получении данных контрольная сумма проверяется, чтобы убедиться в отсутствии ошибок.
Порядковые номера: каждому байту данных присваивается порядковый номер. Это позволяет упорядочить данные при их получении и устранить дублирование.
Подтверждение приёма: после получения данных получатель отправляет подтверждение (ACK), содержащее порядковый номер следующего ожидаемого сегмента.
Перезагрузка по таймауту: если отправитель не получает ACK в течение определённого времени, он предполагает потерю данных и повторно отправляет их.
Управление соединением: включает в себя трёхэтапное рукопожатие и четырёхэтапное закрытие соединения.
Контроль перегрузки: отправитель регулирует свою скорость передачи в соответствии с возможностями получателя.
Управление перегрузкой: предотвращает перегрузку сети путём ограничения скорости отправки данных при возникновении проблем с перегрузкой.
Чтобы повысить эффективность 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选项并保存到传输控制块中 */ 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:
В противном случае (без 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.
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 из этого сегмента.
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 )