Я пытаюсь понять нижние детали веб-серверов. Мне интересно, если сервер, скажем, Apache, постоянно опрашивает новые запросы или работает ли он какой-то системой прерываний. Если это прерывание, что вызывает прерывание, это драйвер сетевой карты?
2 ответа
Короткий ответ: какая-то система прерываний. По сути, они используют блокирующий ввод / вывод, что означает, что они спят (блокируются), ожидая новых данных.
Сервер создает сокет прослушивания, а затем блокирует при ожидании новых подключений. В течение этого времени ядро переводит процесс в состояние прерывистого сна и запускает другие процессы. Это важный момент: непрерывный опрос процесса может привести к бесполезной загрузке процессора. Ядро может использовать системные ресурсы более эффективно, блокируя процесс до тех пор, пока не будет выполнено работы.
Когда новые данные поступают в сеть, сетевая карта выдает прерывание.
Видя, что есть прерывание от сетевой карты, ядро через драйвер сетевой карты считывает новые данные с сетевой карты и сохраняет их в памяти. (Это должно быть сделано быстро и обычно обрабатывается внутри обработчика прерываний.)
Ядро обрабатывает вновь поступившие данные и связывает их с сокетом. Процесс, который блокирует этот сокет, будет помечен как работоспособный, что означает, что он теперь может быть запущен. Он не обязательно запускается немедленно (ядро может решить запустить другие процессы).
На досуге ядро разбудит заблокированный процесс веб-сервера. (Так как теперь он работает.)
Процесс веб-сервера продолжает выполняться, как будто времени не прошло. Его системный вызов блокировки возвращается и обрабатывает любые новые данные. Тогда ... перейдите к шагу 1.
Здесь довольно много "нижних" деталей.
Во-первых, учтите, что в ядре есть список процессов, и в любой момент времени некоторые из этих процессов работают, а некоторые нет. Ядро позволяет каждому выполняющемуся процессу некоторое время процессора, затем прерывает его и переходит к следующему. Если нет запущенных процессов, то ядро, вероятно, выдаст команду типа HLT для процессора, которая приостанавливает работу процессора до тех пор, пока не произойдет аппаратное прерывание.
Где-то на сервере есть системный вызов, который говорит «дай мне что-нибудь сделать». Есть две широкие категории способов сделать это. В случае Apache он вызывает accept
на сокете, который Apache ранее открыл, вероятно, прослушивая порт 80. Ядро поддерживает очередь попыток соединения и добавляет в эту очередь каждый раз при получении TCP SYN . Как ядро узнает, что получен TCP SYN, зависит от драйвера устройства; для многих сетевых карт возможно получение аппаратного прерывания при получении сетевых данных.
accept
просит ядро вернуть мне следующую инициализацию соединения. Если очередь не была пуста, то accept
просто сразу возвращает. Если очередь пуста, то процесс (Apache) удаляется из списка запущенных процессов. Когда позднее соединение инициируется, процесс возобновляется. Это называется "блокировкой", потому что для вызывающего его процесса accept()
выглядит как функция, которая не возвращает, пока не получит результат, который может пройти через некоторое время. За это время процесс больше ничего не может сделать.
Как только accept
возврат, Apache знает, что кто-то пытается установить соединение. Затем он вызывает fork, чтобы разделить процесс Apache на два идентичных процесса. Один из этих процессов продолжает обрабатывать HTTP-запрос, другие вызовы снова accept
чтобы установить следующее соединение. Таким образом, всегда есть главный процесс, который ничего не делает, кроме вызова accept
и порождения, а затем есть один подпроцесс для каждого запроса.
Это упрощение: это можно сделать с потоками, а не с процессами, а также можно предварительно fork
чтобы рабочий процесс был готов к работе при получении запроса, что снижает накладные расходы при запуске. В зависимости от того, как настроен Apache, он может выполнять одно из следующих действий.
Это первая широкая категория того, как это сделать, и она называется блокировкой ввода-вывода, потому что системные вызовы, такие как accept
read
и write
работающие с сокетами, приостановят процесс до тех пор, пока им не будет что возвращать.
Другой широкий способ сделать это называется неблокирующим, основанным на событиях или асинхронным вводом-выводом. Это реализовано с помощью системных вызовов, таких как select
или epoll
. Каждый из них делает одно и то же: вы даете им список сокетов (или вообще файловых дескрипторов) и того, что вы хотите с ними делать, и ядро блокируется, пока не будет готово выполнить одну из этих вещей.
С этой моделью вы могли бы сказать ядру (с epoll
):«Скажите, когда будет новое соединение на порту 80 или новые данные для чтения на любом из этих 9471 других соединений, которые у меня открыты». epoll
блокирует, пока одна из этих вещей не будет готова, тогда вы делаете это. Тогда вы повторяете. Системные вызовы, такие как accept
read
и write
никогда не блокируются, отчасти потому, что всякий раз, когда вы их вызываете, epoll
просто сообщает вам, что они готовы, поэтому не будет причин для блокировки, а также потому, что при открытии сокета или указанного вами файла что вы хотите, чтобы они были в неблокирующем режиме, поэтому эти вызовы завершатся с EWOULDBLOCK
вместо блокировки.
Преимущество этой модели в том, что вам нужен только один процесс. Это означает, что вам не нужно выделять стек и структуры ядра для каждого запроса. Nginx и HAProxy используют эту модель, и это большая причина, по которой они могут иметь дело с гораздо большим количеством соединений, чем Apache на аналогичном оборудовании.