Існує досить багато "нижчих" деталей.
Спочатку врахуйте, що в ядрі є список процесів, і в будь-який момент часу деякі з цих процесів запущені, а деякі - ні. Ядро дозволяє кожному запущеному процесу деякий фрагмент часу процесора, потім перериває його і переходить до наступного. Якщо немає запущених процесів, ядро, ймовірно, видасть ЦП до інструкції, як HLT, яка призупиняє процесор, поки не буде апаратне переривання.
Десь на сервері стоїть системний виклик, який говорить "дай мені щось зробити". Існує дві широкі категорії способів зробити це. У випадку Apache, він викликає accept
сокет Apache, який раніше відкрився, ймовірно, прослуховуючи порт 80. Ядро підтримує чергу спроб з'єднання і додає до цієї черги щоразу, коли приймається TCP SYN . Наскільки ядро знає, що отримано TCP SYN, залежить від драйвера пристрою; для багатьох НІК, ймовірно, відбувається апаратне переривання при отриманні мережевих даних.
accept
просить ядро повернути мені наступне ініціювання з'єднання. Якщо черга не була порожньою, accept
просто повертається негайно. Якщо черга порожня, процес (Apache) видаляється зі списку запущених процесів. Коли пізніше ініціюється з'єднання, процес відновляється. Це називається "блокуванням", тому що процес, що викликає його, accept()
виглядає як функція, яка не повертається, поки не отримає результат, який може пройти через деякий час. За цей час процес не може більше нічого робити.
Після accept
повернення Apache знає, що хтось намагається ініціювати з'єднання. Потім він закликає форк розділити процес Apache на два однакові процеси. Один із цих процесів продовжується обробкою HTTP-запиту, інший accept
знову викликає наступне з'єднання. Таким чином, завжди є головний процес, який не робить нічого, крім виклику accept
та нерестовини підпроцесів, і тоді є один підпроцес для кожного запиту.
Це спрощення: це можна зробити за допомогою потоків замість процесів, а також fork
заздалегідь можливо, щоб під час отримання запиту був готовий робочий процес, таким чином зменшуючи накладні витрати. Залежно від налаштування Apache, він може робити будь-яку з цих речей.
Це перша широка категорія того, як це зробити, і називається блокуванням IO, оскільки виклики системи, як accept
і read
і write
які працюють на сокетах, призупиняють процес, поки їм щось не повернути.
Інший широкий спосіб зробити це називається неблокуючим або на основі подій або асинхронним IO . Це реалізується за допомогою системних викликів типу select
або epoll
. Вони роблять те саме: ви даєте їм список сокетів (або взагалі дескрипторів файлів) і те, що ви хочете зробити з ними, і ядро блокує, поки не буде готове зробити одну з цих речей.
У цій моделі ви можете сказати ядру (з epoll
): "Скажіть мені, коли на порту 80 з'явиться нове з'єднання або нові дані для читання на будь-якому з цих 9471 інших з'єднань, які я відкрив". epoll
блокує, поки одна з цих речей не буде готова, тоді ви це зробите. Потім ви повторите. Системні виклики , як accept
і read
і write
НЕ блок, почасти тому , що всякий раз , коли ви їх називаєте, epoll
просто сказали, що вони готові , так що не було б ніяких підстав для блокування, а також тому , що при відкритті сокета або файлу ви вказуєте , що ви хочете їх в режимі, що не блокує, тож ці дзвінки не вдасться EWOULDBLOCK
замість блокувати.
Перевага цієї моделі полягає в тому, що вам потрібен лише один процес. Це означає, що вам не потрібно виділяти стек і структури ядра для кожного запиту. Nginx і HAProxy використовують цю модель, і це велика причина, що вони можуть мати справу з набагато більшою кількістю підключень, ніж Apache на аналогічному обладнанні.