Різниця між nginx і apache з прикладами

Під час співбесід на роль linux/unix адміністратора у багатьох IT-компаніях запитують, що таке load average, ніж nginx відрізняється від apache httpd і що таке fork. У цій статті я постараюся пояснити, що розраховують почути відповідь на ці питання, і чому.
Тут дуже важливо добре розуміти основи адміністрування. В ідеальній ситуації при постановці завдання системного адміністратора виставляють ряд вимог. Якщо ж ситуація не ідеальна, то, по суті, вимога до адміністратора одне: «Хочу, щоб все працювало». Іншими словами, сервіс повинен бути доступний 24/7 і, якщо якесь рішення не задовольняє цим вимогам (масштабування і відмовостійкість відносяться до доступності), то можна сказати, що адміністратор погано зробив свою роботу. Але якщо різні рішення двох адміністраторів працюють 24/7, як зрозуміти, яка з них краще?
Хороший системний адміністратор при виборі рішення при заданих вимогах орієнтується на дві умови: мінімальне споживання ресурсів та їх збалансований розподіл.
Варіант, коли одному фахівцю потрібно 10 серверів для виконання завдання, а другого всього 2, ми розглядати не будемо, що тут краще – очевидно. Далі під ресурсами я буду розуміти ПРОЦЕСОР (cpu), ОЗУ (ram) і диск (hdd).
Давайте розглянемо ситуацію: один адміністратор створив рішення, яке вимагає 10% cpu, 5% ram і 10% hdd від усього вашого обладнання, а другий використовував для цього 1% cpu, 40% ram і 20% hdd. Яке з цих рішень краще? Тут все стає вже не так очевидно. Тому хороший адміністратор завжди повинен вміти грамотно підібрати рішення, виходячи з наявних ресурсів.

Уявімо, що ми програмісти початкового рівня, і нас просять написати елементарну програму по роботі з мережею. Вимоги прості: потрібно обробити два з'єднання одночасно по протоколу tcp і записати те, що ми прийняли, в файл.
До розробки програми потрібно згадати, які кошти нам надає операційна система Linux (далі в статті всі приклади тільки на основі цієї ОС). В Linux у нас є набір системних викликів (тобто функцій в ядрі ОС, які ми можемо викликати безпосередньо з нашої програми, тим самим примусово віддаючи процесорний час ядру):
1) socket — виділяє місце в буфері ядра ОС під наш сокет. Адреса виділеного місця повертається з функції в програму;
2) bind — дозволяє змінювати інформацію в структурі сокета, яку нам виділила ОС linux по команді socket;
3) listen – так само як і bind міняє дані в нашій структурі, дозволяючи вказувати ОС, що ми хочемо брати підключення з цього сокету;
4) connect – каже нашої ОС, що вона повинна підключитися до іншого віддаленого сокету;
5) accept – каже нашої ОС, що ми хочемо прийняти нове підключення від іншого сокета;
6) read – ми просимо ОС видати нам зі свого буфера певну кількість байт, яке вона отримала від віддаленого сокета;
7) write – ми просимо ОС послати певну кількість байт на віддалений сокет.
Для того, щоб встановити з'єднання, нам потрібно створити сокет в пам'яті linux, прописати в ньому потрібні дані і виконати підключення до віддаленої стороні.
socket → bind → connect → read/write
Але якщо ви довіряєте ОС зробити вибір вихідного порту за вас (а так само і ip-адреси), то bind робити необов'язково:
socket → connect → read/write
Для того, щоб приймати вхідні повідомлення, нам потрібно виконати:
socket → bind → listen → accept → read/write
Тепер ми знаємо достатньо для того, щоб написати програму. Приступаємо безпосередньо до написання, використовуючи сі. Чому сі? Тому що в цій мові команди називаються так само, як системні виклики (за рідкісним винятком, типу fork).
Програма differ1.c
//Порт, який ми слухаємо
#define PORT_NO 2222

#include < stdio.h>
#include <stdlib.h>
#include < string.h>
#include <netinet/in.h>

int main(int argc, char *argv[])
{
//Буфер, куди ми будемо зчитувати дані з сокета
long buffersize=50;
int sockfd, newsockfd;
socklen_t clilen;
// Змінна, в якій буде зберігатися адреса нашого буфера
char *buffer;
struct sockaddr_in serv_addr, cli_addr;
FILE * resultfile;
// виділяємо пам'ять
buffer = malloc (buffersize+1);
//відкриваємо файл для запису наших повідомлень 
resultfile = fopen("/tmp/nginx_vs_apache.log","a");
bzero((char *) &serv_addr, sizeof(serv_addr));
bzero(buffer,buffersize+1);
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(PORT_NO);
//створюємо структуру (сокет), тут SOCK_STREAM це tcp/ip сокет.
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) error("ERROR opening socket");
//визначаємо структуру нашого сокета, будемо слухати порт 2222 на всіх ip адреси
if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) error("ERROR on binding");
// говоримо нашої ОС, щоб вона брала входять коннекти для нашого сокета, максимум 50
listen(sockfd,50);
while (1) {
//в замкнутому циклі обробляємо вхідні підключення і читаємо з них
newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);
if (newsockfd < 0) error("ERROR on accept");
read(newsockfd,buffer,buffersize);
fprintf(resultfile, buffer);
fflush (resultfile);
}
close(sockfd);
return 0;
}

Компілюємо і запускаємо наш демон:
[tolik@ localhost]$ 
[tolik@localhost]$ ./differ

Дивимося, що вийшло:
[root@ localhost]# ps axuf | grep [d]iffer
tolik 45409 0.0 0.0 4060 460 pts/12 S+ 01:14 0:00 | \_ ./differ
[root@localhost ]# netstat -tlnp | grep 2222
tcp 0 0 0.0.0.0:2222 0.0.0.0:* LISTEN 45409/./differ
[root@localhost ]# ls -lh /proc/45409/fd
разом 0
lrwx------ 1 tolik tolik 64 Кві 19 01:16 0 -> /dev/pts/12
lrwx------ 1 tolik tolik 64 Кві 19 01:16 1 -> /dev/pts/12
lrwx------ 1 tolik tolik 64 Кві 19 01:16 2 -> /dev/pts/12
l-wx------ 1 tolik tolik 64 Кві 19 01:16 3 -> /tmp/nginx_vs_apache.log
lrwx------ 1 tolik tolik 64 Кві 19 01:16 4 -> socket:[42663416]
[root@localhost ]# netstat -apeen | grep 42663416
tcp 0 0 0.0.0.0:2222 0.0.0.0:* LISTEN 500 42663416 45409/./differ
[root@localhost ]# strace -p 45409
Process 45409 attached - interrupt to quit
accept(4, ^C <unfinished ...>
Process 45409 detached
[root@localhost ]#

Процес знаходиться в стані sleep (S+ в команді ps).
Ця програма продовжуватиме виконуватися (отримає процесорний час) тільки при появі нового з'єднання на порт 2222. У всіх інших випадках програма ніколи не отримає процесорний час: вона навіть не буде вимагати його від ОС і, отже, не буде впливати на load avarage (далі LA), споживаючи тільки пам'ять.
З іншого консолі запускаємо першого клієнта:
[tolik@localhost ]$ telnet localhost 2222
Connected to localhost.
Escape character is '^]'.
test client 1

Дивимося файл:
[root@localhost ]# cat /tmp/nginx_vs_apache.log
test client 1

Відкриваємо друге з'єднання:
[tolik@localhost ]$ telnet localhost 2222
Connected to localhost.
Escape character is '^]'.
test client 2

Дивимося результат:
[root@localhost ]# cat /tmp/nginx_vs_apache.log
test client 1

По вмісту файлу видно, що прийшло тільки перше повідомлення від першого клієнта. Але при цьому друге повідомлення ми вже відправили, і де воно знаходиться. Всі мережеві підключення здійснює ОС, значить і повідомлення test client 2 зараз у буфері операційної системи, в пам'яті, яка нам недоступна. Єдиний спосіб забрати ці дані – опрацювати нове з'єднання командою accept, потім викликати read.
Спробуємо що-небудь написати в першому клієнта:
[tolik@localhost ]$ telnet localhost 2222
Connected to localhost.
Escape character is '^]'.
test client 1
blablabla

Перевіряємо лог:
[root@localhost ]# cat /tmp/nginx_vs_apache.log
test client 1

Нове повідомлення не потрапило в лог. Це відбувається з-за того, що ми викликаємо команду read тільки один раз, отже, в лог потрапляє тільки перше повідомлення.
Спробуємо закрити наше перше з'єднання:
[tolik@localhost ]$ telnet localhost 2222
Connected to localhost.
Escape character is '^]'.
test client 1
bla bla bla
^]
telnet> quit
Connection closed.

У цей момент наша програма запускає по циклу наступний accept і read, отже, приймає повідомлення з другого з'єднання:
[root@localhost ]# cat /tmp/nginx_vs_apache.log
test client 1
test client 2

Наше повідомлення bla bla bla ніде не з'явилося, ми вже закрили сокет, і ОС чистить буфер, тим самим видаливши наші дані. Потрібно модернізувати програму — читати з сокета до тих пір, поки звідти надходить інформація.
Програма з нескінченним читанням з сокета differ2.c
#define PORT_NO 2222

#include < stdio.h>
#include < string.h>
#include <netinet/in.h>

int main(int argc, char *argv[])
{
int sockfd, newsockfd;
socklen_t clilen;
char buffer;
char * pointbuffer = &buffer;
struct sockaddr_in serv_addr, cli_addr;
FILE * resultfile;
resultfile = fopen("/tmp/nginx_vs_apache.log","a");
bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(PORT_NO);
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) error("ERROR opening socket");
if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) error("ERROR on binding");
listen(sockfd,50);
while (1) {
newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);
if (newsockfd < 0) error("ERROR on accept");
while (read(newsockfd, pointbuffer,1)) {
fprintf(resultfile, pointbuffer);
fflush (resultfile);
}
}
close(sockfd);
return 0;
}

Програма не сильно відрізняється від попередньої. Ми додали ще один цикл перед командою read для того, щоб забирати дані з сокета до тих пір, поки вони туди надходять. Перевіряємо.
Очищаємо файл:
[root@localhost ]# > /tmp/nginx_vs_apache.log

Компілюємо і запускаємо:
[tolik@localhost ]$ gcc -o differ differ2.c
[tolik@localhost ]$ ./differ

Перший клієнт:
[tolik@localhost ]$ telnet localhost 2222
Connected to localhost.
Escape character is '^]'.
client test 1
yoyoyo

Другий клієнт:
[tolik@localhost ]$ telnet localhost 2222
Connected to localhost.
Escape character is '^]'.
client test 2
yooyoy

Перевіряємо, що вийшло:
[root@localhost ]# cat /tmp/nginx_vs_apache.log
client test 1
yoyoyo

На цей раз все добре, ми забрали всі дані, але проблема залишилася: два з'єднання обробляються послідовно, по черзі, а це не підходить під наші вимоги. Якщо ми закриємо перше з'єднання (ctrl + ]), то дані з другого з'єднання потраплять відразу в лог:
[root@localhost ]# cat /tmp/nginx_vs_apache.log
client test 1
yoyoyo
client test 2
yooyoy

Дані прийшли. Але як обробити два з'єднання паралельно? Тут нам на допомогу приходить команда fork. Що робить системний виклик fork в linux? Правильну відповідь на це запитання на будь-якій співбесіді – нічого. Fork – застарілий виклик, і в linux присутня тільки для зворотної сумісності. Насправді, викликаючи команду fork, ви викликаєте системний виклик clone. Функція clone створює копію процесу і ставить обидва процесу в чергу на процесор. Різниця між ними в тому, що fork копіює дані (змінні, буфери тощо) відразу в область пам'яті дочірнього процесу, а clone копіює дані в дочірній процес тільки при спробі їх змінити (дивіться обмеження прав доступу до пам'яті в MMU). Тобто, якщо ви викликаєте fork 10 разів, а дані використовуєте тільки для читання, то ви отримаєте 10 однакових копій даних в пам'яті. І це явно не те, що вам потрібно, особливо в мультитредовых додатках. Clone запускає копію вашого додатки, але не копіює дані відразу. Якщо ви запустите clone 10 разів, то у вас буде 10 виконуваних процесів з одним блоком пам'яті, і пам'ять буде копіюватися лише при спробі її змінити дочірнім процесом. Погодьтеся, другий алгоритм набагато ефективніше.
Програма c fork differ3.c
#define PORT_NO 2222

#include < stdio.h>
#include < string.h>
#include <netinet/in.h>

int main(int argc, char *argv[])
{
int sockfd, newsockfd;
socklen_t clilen;
char buffer;
char * pointbuffer = &buffer;
struct sockaddr_in serv_addr, cli_addr;
FILE * resultfile;
int pid=1;
resultfile = fopen("/tmp/nginx_vs_apache.log","a");
bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(PORT_NO);
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) error("ERROR opening socket");
if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) error("ERROR on binding");
listen(sockfd,50);
while (pid!=0) {
newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);
if (newsockfd < 0) error("ERROR on accept");
pid=fork();
if (pid!=0) {
close(newsockfd);
fprintf(resultfile,"New process was started with pid=%d\n",pid);
fflush (resultfile);
}
}
while (read(newsockfd, pointbuffer,1)) {
fprintf(resultfile, pointbuffer);
fflush (resultfile);
}
close(sockfd);
return 0;
}

У цій програмі все те ж саме — ми робимо accept, приймаємо нове з'єднання. Далі ми запускаємо fork. І якщо це майстер процес (fork повернув pid створеного процесу), то ми закриваємо поточне з'єднання в батьківському процесі (воно доступно і в батьків, і в дочірньому процесі). Якщо це дочірній процес (fork повернув 0), то ми починаємо робити read з відкритого сокета, який ми відкрили командою accept в батьківському процесі. За фактом виходить, що батьківський процес у нас тільки приймає з'єднання, а read/write ми робимо в дочірніх процесах.
Компілюємо і запускаємо:
[tolik@localhost ]$ gcc -o differ differ3.c
[tolik@localhost ]$ ./differ

Очищаємо наш лог файл:
[root@localhost ]# > /tmp/nginx_vs_apache.log

Дивимося процеси:
[root@localhost ]# ps axuf | grep [d]iffer
tolik 45643 0.0 0.0 4060 460 pts/12 S+ 01:40 0:00 | \_ ./differ

Клиент1:
[tolik@localhost ]$ telnet localhost 2222
Connected to localhost.
Escape character is '^]'.
client 1 test
megatest

Клиент2:
[tolik@localhost ]$ telnet localhost 2222
Connected to localhost.
Escape character is '^]'.
client2 test
yoyoyoy

Дивимося процеси:
[root@localhost ]# ps axuf | grep [d]iffer
tolik 45643 0.0 0.0 4060 504 pts/12 S+ 01:40 0:00 | \_ ./differ
tolik 45663 0.0 0.0 4060 156 pts/12 S+ 01:41 0:00 | \_ ./differ
tolik 45665 0.0 0.0 4060 160 pts/12 S+ 01:41 0:00 | \_ ./differ

Ми не закриваємо обидва з'єднання і можемо туди ще щось дописувати, дивимося наш лог:
[root@localhost ]# cat /tmp/nginx_vs_apache.log
New process was started with pid=44163
New process was started with pid=44165
client 1 test
megatest
client2 test
yoyoyoy

Два з'єднання обробляються одночасно — ми отримали бажаний результат.
Програма працює, але недостатньо швидко. Вона спочатку приймає з'єднання, а тільки потім запускає команду fork, і з'єднання обробляє тільки один процес. Виникає питання: чи можуть кілька процесів в ОС Linux працювати з одним і тим же tcp портом? Пробуємо.
Програма pre c fork differ_prefork.c
#define PORT_NO 2222

#include < stdio.h>
#include < string.h>
#include <netinet/in.h>

int main(int argc, char *argv[])
{
int sockfd, newsockfd, startservers, count ;
socklen_t clilen;
char buffer;
char * pointbuffer = &buffer;
struct sockaddr_in serv_addr, cli_addr;
FILE * resultfile;
int pid=1;
resultfile = fopen("/tmp/nginx_vs_apache.log","a");
bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(PORT_NO);
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) error("ERROR opening socket");
if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) error("ERROR on binding");
listen(sockfd,50);
startservers=2;
count = 0;
while (pid!=0) {
if (count < startservers)
{
pid=fork();
if (pid!=0) {
close(newsockfd);
fprintf(resultfile,"New process was started with pid=%d\n",pid);
fflush (resultfile);
}
count = count + 1;
}
//sleep (1);
}
newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);
if (newsockfd < 0) error("ERROR on accept");
while (read(newsockfd, pointbuffer,1)) {
fprintf(resultfile, pointbuffer);
fflush (resultfile);
}
close(sockfd);
return 0;
}

Як бачите, програма все ще не сильно змінилася, ми просто запускаємо fork по циклу. В даному випадку ми створюємо два дочірніх процесу, а тільки потім в кожному з них робимо accept на прийом нового з'єднання. Перевіряємо.
Компілюємо і запускаємо:
[tolik@localhost ]$ gcc -o differ differ_prefork.c
[tolik@localhost ]$ ./differ

Дивимося, що у нас в процесах:
[root@localhost ]# ps axuf | grep [d]iffer
tolik 44194 98.0 0.0 4060 504 pts/12 R+ 23:35 0:07 | \_ ./differ
tolik 44195 0.0 0.0 4060 152 pts/12 S+ 23:35 0:00 | \_ ./differ
tolik 44196 0.0 0.0 4060 156 pts/12 S+ 23:35 0:00 | \_ ./differ

Ми ще не підключилися ні одним клієнтом, а програма вже два рази зробила fork. Що ж зараз відбувається з системою? Для початку майстер процес: він знаходиться в замкнутому циклі і перевіряє, чи треба форкать ще процеси. Якщо ми будемо робити це без зупинки, то, по суті, будемо постійно вимагати від ОС процесорний час, так як наш цикл повинен виконуватися завжди. Це означає, що ми споживаємо 100% одного ядра – в команді ps значення 98.0%. Це ж можна побачити в команді top:
[root@localhost ]# top -n 1 | head
top - 23:39:22 up 141 days, 21 min, 8 users, load average: 1.03, 0.59, 0.23
Завдання: 195 total, 2 running, 193 sleeping, 0 stopped, 0 zombie
Cpu(s): 0.3%us, 0.2%sy, 0.0%ni, 99.3%id, 0.2%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 1896936k total, 1876280k used, 20656k free, 151208k buffers
Swap: 4194296k total, 107600k used, 4086696k free, 1003568k cached

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
44194 tolik 20 0 4060 504 420 R 98.9 0.0 4:10.54 differ
44255 root 20 0 15028 1256 884 R 3.8 0.1 0:00.03 top
1 root 20 0 19232 548 380 S 0.0 0.0 2:17.17 init

Якщо ми підключимося командою strace до батьків, то нічого не побачимо, так як наш процес не викликає жодних функції ядра:
[root@localhost ]# strace -p 44194
Process 44194 attached - interrupt to quit
^CProcess 44194 detached
[root@localhost ]#

Що роблять дочірні процеси? Тут починається найцікавіше. Судячи з кодом, всі вони після форк повинні висіти в стані accept і чекати нових сполук з одного і того ж порту, в нашому випадку 2222. Перевіряємо:
[root@localhost ]# strace -p 44195
Process 44195 attached - interrupt to quit
accept(4, ^C <unfinished ...>
Process 44195 detached
[root@localhost ]# strace -p 44196
Process 44196 attached - interrupt to quit
accept(4, ^C <unfinished ...>
Process 44196 detached

На даний момент вони не вимагають від ОС процесорного часу і споживають тільки пам'ять. Але ось у чому питання: хто з них візьме моє з'єднання, якщо я зроблю telnet? Перевіряємо:
[tolik@localhost ]$ telnet localhost 2222
Connected to localhost.
Escape character is '^]'.
client 1 test
hhh

[root@localhost ]# strace -p 44459
Process 44459 attached - interrupt to quit
read(5, ^C <unfinished ...>
Process 44459 detached
[root@localhost ]# strace -p 44460
Process 44460 attached - interrupt to quit
accept(4, ^C <unfinished ...>
Process 44460 detached

Ми бачимо, що процес, який був створений раніше (з меншим pid), обробив з'єднання першим, і тепер перебуває в стані read. Якщо ми запустимо другий telnet, то наше з'єднання обробить наступний процес. Після того, як ми закінчили працювати з сокетом, ми можемо його закрити і знову перейти в стан accept (я цього робити не став, щоб не ускладнювати програму).
Залишається останнє питання: що нам робити з батьківським процесом, щоб він не пив стільки cpu і при цьому продовжував працювати? Нам потрібно віддати час інших процесів у добровільному порядку, тобто «сказати» нашої ОС, що якийсь час cpu нам не потрібно. Для цієї мети підійде команда sleep 1: якщо ви її раскомментіруете, то побачите в strace приблизно таку, що повторюється раз в секунду, картину:
[root@localhost ]# strace -p 44601
.....
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
nanosleep({1, 0}, 0x7fff60a15aa0) = 0
....
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
nanosleep({1, 0}, 0x7fff60a15aa0) = 0
...

і т. д.
Наш процес буде отримувати процесор приблизно раз в секунду або, принаймні, вимагати його від ОС.
Якщо ви все ще не розумієте, до чого ця довга стаття, то подивитеся на apache httpd працює в режимі prefork:
[root@www /]# ps axuf | grep [h]ttpd
root 12730 0.0 0.5 271560 11916 ? Ss Feb25 3:14 /usr/sbin/httpd
apache 19832 0.0 0.3 271692 7200 ? S Apr17 0:00 \_ /usr/sbin/httpd
apache 19833 0.0 0.3 271692 7212 ? S Apr17 0:00 \_ /usr/sbin/httpd
apache 19834 0.0 0.3 271692 7204 ? S Apr17 0:00 \_ /usr/sbin/httpd
apache 19835 0.0 0.3 271692 7200 ? S Apr17 0:00 \_ /usr/sbin/httpd

Дочірні процеси в accept:
[root@www /]# strace -p 19832
Process 19832 attached
accept4(3, ^CProcess 19832 detached
<detached ...>
[root@www /]# strace -p 19833
Process 19833 attached
accept4(3, ^CProcess 19833 detached
<detached ...>

Майстер процес з секундною паузою:
[root@www /]# strace -p 12730
Process 12730 attached
select(0, NULL, NULL, NULL, {0, 629715}) = 0 (Timeout)
wait4(-1, 0x7fff4c9e3fbc, WNOHANG|WSTOPPED, NULL) = 0
select(0, NULL, NULL, NULL, {1, 0}) = 0 (Timeout)
wait4(-1, 0x7fff4c9e3fbc, WNOHANG|WSTOPPED, NULL) = 0
select(0, NULL, NULL, NULL, {1, 0}) = 0 (Timeout)
wait4(-1, 0x7fff4c9e3fbc, WNOHANG|WSTOPPED, NULL) = 0
...

При старті httpd майстер процес плодить дочірні процеси, це легко перевірити, якщо запустити strace на майстер процес в момент старту:
Запустимо веб сервер з такими параметрами:
StartServers 1
MinSpareServers 9
MaxSpareServers 10
ServerLimit 10
MaxClients 10
MaxRequestsPerChild 1

Ці налаштування говорять про те, що кожний дочірній процес буде обробляти тільки один запит, потім процес буде побиватися. Мінімальна кількість процесів в accept дорівнює 9 і максимальне одно 10.
Якщо запустити strace на майстер процес в момент старту, то ми побачимо як майстер викликає clone до тих пір, поки не досягне MinSpareServers.
Трасуванняrt_sigaction(SIGSEGV, {0x7f9991933c20, [], SA_RESTORER|SA_RESETHAND, 0x7f99901dd500}, NULL, 8) = 0
rt_sigaction(SIGBUS, {0x7f9991933c20, [], SA_RESTORER|SA_RESETHAND, 0x7f99901dd500}, NULL, 8) = 0
rt_sigaction(SIGABRT, {0x7f9991933c20, [], SA_RESTORER|SA_RESETHAND, 0x7f99901dd500}, NULL, 8) = 0
rt_sigaction(SIGILL, {0x7f9991933c20, [], SA_RESTORER|SA_RESETHAND, 0x7f99901dd500}, NULL, 8) = 0
rt_sigaction(SIGFPE, {0x7f9991933c20, [], SA_RESTORER|SA_RESETHAND, 0x7f99901dd500}, NULL, 8) = 0
rt_sigaction(SIGTERM, {0x7f999193de50, [], SA_RESTORER, 0x7f99901dd500}, NULL, 8) = 0
rt_sigaction(SIGWINCH, {0x7f999193de50, [], SA_RESTORER, 0x7f99901dd500}, NULL, 8) = 0
rt_sigaction(SIGINT, {0x7f999193de50, [], SA_RESTORER, 0x7f99901dd500}, NULL, 8) = 0
rt_sigaction(SIGXCPU, {SIG_DFL, [], SA_RESTORER, 0x7f99901dd500}, NULL, 8) = 0
rt_sigaction(SIGXFSZ, {SIG_IGN, [], SA_RESTORER, 0x7f99901dd500}, NULL, 8) = 0
rt_sigaction(SIGPIPE, {SIG_IGN, [], SA_RESTORER, 0x7f99901dd500}, NULL, 8) = 0
rt_sigaction(SIGHUP, {0x7f999193de80, [HUP USR1], SA_RESTORER, 0x7f99901dd500}, NULL, 8) = 0
rt_sigaction(SIGUSR1, {0x7f999193de80, [HUP USR1], SA_RESTORER, 0x7f99901dd500}, NULL, 8) = 0
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f99918eeab0) = 13098
write(2, "[Wed Jan 25 13:24:39 2017] [noti"..., 114) = 114
wait4(-1, 0x7fffae295fdc, WNOHANG|WSTOPPED, NULL) = 0
select(0, NULL, NULL, NULL, {1, 0}) = 0 (Timeout)
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f99918eeab0) = 13099
wait4(-1, 0x7fffae295fdc, WNOHANG|WSTOPPED, NULL) = 0
select(0, NULL, NULL, NULL, {1, 0}) = 0 (Timeout)
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f99918eeab0) = 13100
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f99918eeab0) = 13101
wait4(-1, 0x7fffae295fdc, WNOHANG|WSTOPPED, NULL) = 0
select(0, NULL, NULL, NULL, {1, 0}) = 0 (Timeout)
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f99918eeab0) = 13102
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f99918eeab0) = 13103
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f99918eeab0) = 13104
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f99918eeab0) = 13105
wait4(-1, 0x7fffae295fdc, WNOHANG|WSTOPPED, NULL) = 0
select(0, NULL, NULL, NULL, {1, 0}) = 0 (Timeout)
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f99918eeab0) = 13106
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f99918eeab0) = 13107
wait4(-1, 0x7fffae295fdc, WNOHANG|WSTOPPED, NULL) = 0
select(0, NULL, NULL, NULL, {1, 0}) = 0 (Timeout)
Дивимося як стартує апач – для цього можна просто дивитися ps axuf | grep [h]ttp кожну секунду, відразу після старту.
Старт апача[root@www /]# date; ps axuf | grep [h]ttp
Wed Jan 25 14:12:10 EST 2017
root 13342 2.5 0.4 271084 9384? Ss 14:12 0:00 /usr/sbin/httpd
apache 13344 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
[root@www /]# date; ps axuf | grep [h]ttp
Wed Jan 25 14:12:11 EST 2017
root 13342 1.6 0.4 271084 9384? Ss 14:12 0:00 /usr/sbin/httpd
apache 13344 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13348 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
[root@www /]# date; ps axuf | grep [h]ttp
Wed Jan 25 14:12:11 EST 2017
root 13342 2.0 0.4 271084 9384? Ss 14:12 0:00 /usr/sbin/httpd
apache 13344 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13348 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13352 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13353 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
[root@www /]# date; ps axuf | grep [h]ttp
Wed Jan 25 14:12:12 EST 2017
root 13342 1.7 0.4 271084 9384? Ss 14:12 0:00 /usr/sbin/httpd
apache 13344 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13348 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13352 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13353 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13357 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13358 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13359 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13360 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
[root@www /]# date; ps axuf | grep [h]ttp
Wed Jan 25 14:12:13 EST 2017
root 13342 1.4 0.4 271084 9384? Ss 14:12 0:00 /usr/sbin/httpd
apache 13344 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13348 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13352 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13353 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13357 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13358 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13359 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13360 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13364 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13365 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
[root@www /]#
Отже, у нас кілька дочірніх процесів, які готові прийняти наш http запит. Давайте спробуємо надіслати запит:
[root@www /]# wget -O /dev/null http://localhost
--2017-01-25 14:04:00-- http://localhost/
Resolving localhost... ::1, 127.0.0.1
Connecting to localhost|::1|:80... failed: Connection refused.
Connecting to localhost|127.0.0.1|:80... connected.
HTTP request sent, awaiting response... 403 Forbidden
2017-01-25 14:04:00 ERROR 403: Forbidden.

Апач нам відповів 403, дивимося процеси:
root 13342 0.0 0.4 271084 9384 ? Ss 14:12 0:00 /usr/sbin/httpd
apache 13348 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13352 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13353 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13357 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13358 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13359 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13360 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13364 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13365 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd

Як бачимо, процес з мінімальним pid обробив запит і завершив свою роботу:
apache 13344 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd

Дочірніх процесів у нас залишилося 9, що вписується в наш ліміт MinSpareServers.
Пробуємо знову відправити запит:
[root@www /]# wget -O /dev/null http://localhost
--2017-01-25 14:15:47-- http://localhost/
Resolving localhost... ::1, 127.0.0.1
Connecting to localhost|::1|:80... failed: Connection refused.
Connecting to localhost|127.0.0.1|:80... connected.
HTTP request sent, awaiting response... 403 Forbidden
2017-01-25 14:15:47 ERROR 403: Forbidden.
[root@www /]# ps axuf | grep [h]ttp
root 13342 0.0 0.4 271084 9384 ? Ss 14:12 0:00 /usr/sbin/httpd
apache 13352 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13353 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13357 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13358 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13359 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13360 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13364 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13365 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13373 0.0 0.2 271084 5232 ? S 14:15 0:00 \_ /usr/sbin/httpd

На цей раз наш запит обробив процес
apache 13348 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd

так як тепер у нього мінімальний pid.
Але у нас залишилося 8 вільних дочірніх процесів в accept, одного не вистачає до MinSpareServers, тому майстер процес нам створив новий процес:
apache 13373 0.0 0.2 271084 5232 ? S 14:15 0:00 \_ /usr/sbin/httpd

Давайте скажемо нашої ОС, щоб вона не давала процесорний час майстер процесу апача:
[root@www /]# kill -SIGSTOP 13342

Дивимося:
[root@www /]# ps axuf | grep [h]ttp | grep ^root
root 13342 0.0 0.4 271084 9384 ? Ts 14:12 0:00 /usr/sbin/httpd

Статус процесу змінився, тепер він не працює.
Перевіряємо, чи працює у нас веб сервер:
[root@www /]# wget -O /dev/null http://localhost
--2017-01-25 14:20:12-- http://localhost/
Resolving localhost... ::1, 127.0.0.1
Connecting to localhost|::1|:80... failed: Connection refused.
Connecting to localhost|127.0.0.1|:80... connected.
HTTP request sent, awaiting response... 403 Forbidden
2017-01-25 14:20:12 ERROR 403: Forbidden.

О, так, все ще працює, веб сервер ще відповідає.
Дивимося що у нас з процесами:
root 13342 0.0 0.4 271084 9384 ? Ts 14:12 0:00 /usr/sbin/httpd
apache 13352 0.0 0.0 0 0 ? Z 14:12 0:00 \_ [httpd] <defunct>
apache 13353 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13357 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13358 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13359 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13360 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13364 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13365 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13373 0.0 0.2 271084 5232 ? S 14:15 0:00 \_ /usr/sbin/httpd

Наш черговий запит був оброблений черговим дочірнім процесом, який відпрацював і вийшов. Але він залишив код виходу, який повинен бути оброблений майстер процесом. Так як майстер процес у нас зупинено, код виходу поки знаходиться в ядрі в таблиці процесів, і хоч у нас вже і немає, але в таблиці він є, позначений як зомбі.
apache 13352 0.0 0.0 0 0 ? Z 14:12 0:00 \_ [httpd] <defunct>

Природно дочірніх процесів у нас залишилося 8, так як новий 9й плодити нікому, майстер зупинений.
Давайте для експерименту відправимо ще один запит http:
[root@www /]# wget -O /dev/null http://localhost
--2017-01-25 14:25:03-- http://localhost/
Resolving localhost... ::1, 127.0.0.1
Connecting to localhost|::1|:80... failed: Connection refused.
Connecting to localhost|127.0.0.1|:80... connected.
HTTP request sent, awaiting response... 403 Forbidden
2017-01-25 14:25:03 ERROR 403: Forbidden.
[root@www /]# ps axuf | grep [h]ttp
root 13342 0.0 0.4 271084 9384 ? Ts 14:12 0:00 /usr/sbin/httpd
apache 13352 0.0 0.0 0 0 ? Z 14:12 0:00 \_ [httpd] <defunct>
apache 13353 0.0 0.0 0 0 ? Z 14:12 0:00 \_ [httpd] <defunct>
apache 13357 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13358 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13359 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13360 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13364 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13365 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13373 0.0 0.2 271084 5232 ? S 14:15 0:00 \_ /usr/sbin/httpd

Логічно, що ситуація повторюється.
Давайте скажемо нашої ОС, що майстер процес може знову продовжити роботу:
[root@www /]# kill -SIGCONT 13342
[root@www /]# ps axuf | grep [h]ttp
root 13342 0.0 0.4 271084 9384 ? Ss 14:12 0:00 /usr/sbin/httpd
apache 13357 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13358 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13359 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13360 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13364 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13365 0.0 0.2 271084 5232 ? S 14:12 0:00 \_ /usr/sbin/httpd
apache 13373 0.0 0.2 271084 5232 ? S 14:15 0:00 \_ /usr/sbin/httpd
apache 13388 0.0 0.2 271084 5232 ? S 14:26 0:00 \_ /usr/sbin/httpd
apache 13389 0.0 0.2 271084 5232 ? S 14:26 0:00 \_ /usr/sbin/httpd
apache 13390 0.0 0.2 271084 5232 ? S 14:26 0:00 \_ /usr/sbin/httpd

Майстер процес тут же вважав exit code дочірніх процесів, і згадки про них пішли з таблиці процесів, а відсутні процеси майстер процес нам знову склонировал — тепер у нас 10 вільних процесів в accept, що вмістилося в рамки наших змінних з конфіги.
Як влаштований nginx? Як ви вже зрозуміли, системний виклик accept блокує виконання нашої програми до тих пір, поки не прийде нове з'єднання. Виходить, що ми не можемо очікувати нове з'єднання і обробляти вже відкрите з'єднання в одному процесі. Або?
Поглянемо на код:
Код з select
#define PORT 2222
#include < stdio.h>
#include < string.h> 
#include <stdlib.h>
#include <errno.h>
#include <unistd.h> 
#include <arpa/inet.h> 
#include < sys/types.h>
#include < sys/socket.h>
#include <netinet/in.h>
#include < sys/time.h>

int main(int argc , char *argv[])
{
int opt = 1;
int master_socket , addrlen , new_socket , client_socket[30] , max_clients = 30 , activity, i , valread , sd;
int max_sd;
FILE * resultfile;
struct sockaddr_in address;
char buffer[50];
fd_set readfds;
resultfile = fopen("/tmp/nginx_vs_apache.log","a");
//Заповнюємо наш масив сокетів нулями
for (i = 0; i < max_clients; i++) client_socket[i] = 0;
if( (master_socket = socket(AF_INET , SOCK_STREAM , 0)) == 0) error("socket failed");
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons( PORT );
if (bind(master_socket, (struct sockaddr *)&address, sizeof(address))<0) error("bind failed");
if (listen(master_socket, 3) < 0) error("listen");
addrlen = sizeof(address);
while(1) //В нескінченному циклі обробляємо запити
{
FD_ZERO(&readfds);
FD_SET(master_socket, &readfds);
max_sd = master_socket;
for ( i = 0 ; i < max_clients ; i++)
{
sd = client_socket[i];
if(sd > 0) FD_SET( sd , &readfds);
if(sd > max_sd) max_sd = sd;
}
//Чекаємо подій на будь-якому з цікавлять нас сокетів
activity = select( max_sd + 1 , &readfds , NULL , NULL , NULL);
if ((activity < 0) && (errno!=EINTR)) printf("select error");
//Обробка нового з'єднання
if (FD_ISSET(master_socket, &readfds))
{
if ((new_socket = accept(master_socket, (struct sockaddr *)&address, (socklen_t*)&addrlen))<0) error("accept");
for (i = 0; i < max_clients; i++)
if( client_socket[i] == 0 ) { client_socket[i] = new_socket; break; }
}

//Читаємо дані з кожного сокета, так як не знаємо які події змусив ОС дати нам CPU
for (i = 0; i < max_clients; i++)
{
sd = client_socket[i];
if (FD_ISSET( sd , &readfds))
{
if ((valread = read( sd , buffer, 1024)) == 0) { close( sd ); client_socket[i] = 0; }
else
{
buffer[valread] = '\0';
fprintf(resultfile, buffer);
fflush (resultfile);
}
}
}
}

return 0;
}

Цей код виглядає трохи складніше, ніж попередні, але його досить легко пояснити. Припустимо, в процесі потрібно обробляти максимум 30 сполук. Ми створюємо масив з нулів. Як тільки до нас прийде нове з'єднання, ми його опрацьовуємо, а адреса сокету записуємо в цей масив. Перебираючи весь масив і всі наші сокети, ми можемо послідовно зчитувати з них інформацію. Але як нам дізнатися про новому з'єднанні без використання виклику accept? В linux для цього є як мінімум 3 функції: select, poll і epoll. А в freebsd для цього є аналог функції epoll під назвою kqeueu (kernel queue). Що роблять ці команди? select – найстаріша функція, яка до цих пір використовується для того, щоб віддавати всі процесорний час ядру, запитуючи його тільки при певних умовах (за аналогією з accept). Різниця в тому, що ядро поверне нам cpu, коли на зазначених нами сокетах почнеться будь-яка активність. Так як при запуску програми відкритий тільки один сокет, то і в select ми вказуємо один. Якщо ми підключимося телнетом до нашого демону, то в select ми повинні вказувати вже два сокету: майстер сокет на порт 2222 і той, який до нас підключився. Щоб було зрозуміліше, продемонструю:
[tolik@101host nginx_vs_apache]$ ./differ &
[1] 44832
[tolik@101host nginx_vs_apache]$ ps axuf | grep [.]/differ
tolik 44832 0.0 0.0 4060 448 pts/0 S 22:47 0:00 \_ ./differ
[root@localhost ]# strace -p 44832
Process 44832 attached - interrupt to quit
select(5, [4], NULL, NULL, NULL) = 1 (in [4])

У цей момент ми з іншої консолі робимо telnet на порт 2222 в наш демон і дивимося на трейс:
accept(4, {sa_family=AF_INET, sin_port=htons(41130), sin_addr=inet_addr("127.0.0.1")}, [16]) = 5
select(6, [4 5], NULL, NULL, NULL^C <unfinished ...>
Process 44832 detached
[root@localhost ]# ls -lh /proc/44832/fd
разом 0
lrwx------ 1 tolik tolik 64 Кві 19 00:26 0 -> /dev/pts/12
lrwx------ 1 tolik tolik 64 Кві 19 00:26 1 -> /dev/pts/12
lrwx------ 1 tolik tolik 64 Кві 19 00:21 2 -> /dev/pts/12
l-wx------ 1 tolik tolik 64 Кві 19 00:26 3 -> /tmp/nginx_vs_apache.log
lrwx------ 1 tolik tolik 64 Кві 19 00:26 4 -> socket:[42651147]
lrwx------ 1 tolik tolik 64 Кві 19 00:26 5 -> socket:[42651320] 
[root@localhost ]# netstat -apeen | grep 42651147
tcp 0 0 0.0.0.0:2222 0.0.0.0:* LISTEN 500 42651147 44832/./differ
[root@localhost ]# netstat -apeen | grep 42651320
tcp 0 0 127.0.0.1:2222 127.0.0.1:41130 ESTABLISHED 500 42651320 44832/./differ

Спочатку команді select ми вказували сокет 4 (дивіться у квадратних дужках). З /proc ми дізналися, що 4й файл-дескриптор — це сокет з номером 42651147. За netstat ми дізналися, що сокет з таким номером — це наш сокет в стані listen порту 2222. Як тільки ми підключилися до цього сокету, ОС справила tcp handshake з нашим telnet клієнтом і встановила нове з'єднання, про що сповістила додаток через select. Наша програма отримала процесорний час і почала обробляти порожній масив з сполуками. Побачивши, що це нове з'єднання, ми запустили кнопку accept, знаючи, що вона точно не перерве виконання програми, так як з'єднання вже присутня. Тобто фактично ми використовуємо той же accept, тільки в неблокирующем режимі.
Після того, як ми виконали з'єднання, ми знову віддали управління ядра linux, але сказали йому, що тепер ми хочемо отримувати повідомлення по двом сокетам — під номером 4 і 5, що дуже добре видно в команді strace ([4 5]). Саме так працює nginx: він здатний обробляти велику кількість сокетів одним процесом. За існуючими сокетам ми можемо проводити операції read/write, за новим можемо викликати accept. У select є великий недолік — ми не можемо знати, яка саме подія сталося і з яким саме сокетом. Кожен раз, коли ми отримуємо процесорний час, нам доводиться обробляти всі наші коннекти та перевіряти їх на отримання даних, роблячи з них read. Якщо у нас буде 1000 сполук, а дані прийдуть тільки по одному з них, то ми опрацюємо всі 1000 з'єднань, щоб знайти потрібний. Select — дуже старий системний виклик, має ряд обмежень: наприклад, на максимальну кількість коннектов (файл дескрипторів). Йому на зміну прийшов спершу більш досконалий системний виклик poll, позбавлений цих лімітів і працює швидше. Згодом з'явилися epoll і kqeueu (freebsd). Більш сучасні функції дозволяють більш ефективно працювати з коннектами. Наприклад, при використанні kqeueu ви будете точно знати, за яким сокету у вас сталася подія, і зможете відразу його обробити.
Які з цих функцій підтримує nginx? Nginx вміє працювати з усіма цими функціями.
Посилання на документацію. У цій статті я не буду описувати, чим відрізняються всі ці функції, оскільки обсяг тексту вже досить великий.
Nginx використовує fork для того, щоб створювати процеси і завантажувати всі ядра на сервері. Але кожен окремо взятий дочірній процес nginx працює з безліччю з'єднань так само, як у прикладі з select, тільки використовують для цього сучасні функції (для linux за замовчуванням це epoll). Дивимося:
[root@localhost ]# ps axuf| grep [n]ginx
root 232753 0.0 0.0 96592 556 ? Ss Feb25 0:00 nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf
nginx 232754 0.0 0.0 97428 1400 ? S Feb25 5:20 \_ nginx: worker process
nginx 232755 0.0 0.0 97460 1364 ? S Feb25 5:02 \_ nginx: worker process
[root@localhost ]# strace -p 232754
Process 232754 attached - interrupt to quit
epoll_wait(12, ^C <unfinished ...>
Process 232754 detached
[root@localhost ]# strace -p 232755
Process 232755 attached - interrupt to quit
epoll_wait(14, {}, 512, 500) = 0
epoll_wait(14, ^C <unfinished ...>
Process 232755 detached

Що робить батьківський майстер процес nginx?
[root@localhost ]# strace -p 232753
Process 232753 attached - interrupt to quit
rt_sigsuspend([]^C <unfinished ...>
Process 232753 detached

Він не приймає вхідні з'єднання, а тільки чекає сигнал з боку ОС. За сигналом nginx вміє багато цікавого, наприклад, перевідкривати файл дескриптори, що корисно при ротації логів, або ж перечитувати конфігураційний файл.
Все взаємодія між процесами nginx здійснює через unix сокети:
[root@localhost ]# ls -lh /proc/232754/fd
разом 0
lrwx------ 1 nginx nginx 64 Apr 8 13:20 0 -> /dev/null
lrwx------ 1 nginx nginx 64 Apr 8 13:20 1 -> /dev/null
lrwx------ 1 nginx nginx 64 Apr 8 13:20 10 -> socket:[25069547]
lrwx------ 1 nginx nginx 64 Apr 8 13:20 11 -> socket:[25069551]
lrwx------ 1 nginx nginx 64 Apr 8 13:20 12 -> anon_inode:[eventpoll]
lrwx------ 1 nginx nginx 64 Apr 8 13:20 13 -> anon_inode:[eventfd]
l-wx------ 1 nginx nginx 64 Apr 8 13:20 2 -> /var/log/nginx/error.log
lrwx------ 1 nginx nginx 64 Apr 8 13:20 3 -> socket:[25069552]
l-wx------ 1 nginx nginx 64 Apr 8 13:20 5 -> /var/log/nginx/error.log
l-wx------ 1 nginx nginx 64 Apr 8 13:20 6 -> /var/log/nginx/access.log
lrwx------ 1 nginx nginx 64 Apr 8 13:20 9 -> socket:[25069546]
[root@localhost ]# netstat -apeen | grep 25069547
tcp 0 0 172.16.0.1:80 0.0.0.0:* LISTEN 0 25069547 232753/nginx
[root@localhost ]# netstat -apeen | grep 25069551
unix 3 [ ] STREAM CONNECTED 25069551 232753/nginx

Підсумок
Перед тим як вибирати ті чи інші інструменти важливо розуміти, як саме вони працюють. Так в деяких випадках вигідніше використовувати тільки apache httpd без nginx – і навпаки. Але найчастіше ці продукти використовуються разом, тому що розпаралелюванням обробки сокетів в апачі займається ОС (різні процеси), а розпаралелюванням обробки сокетів в nginx займається сам nginx.
p.s.
Якщо у вас після компіляції програми не запускаються, переконайтеся, що не відкрито з'єднання на порту 2222. В програмі я не обробляв ситуації щодо закриття сокетів, та вони можуть бути якийсь час ще відкриті в різних станах від старих демонів. Якщо програма не запускається, просто почекайте, поки всі сокети закриються по таймауту.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.