Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Назад Сервер TCP/IP ... много серверов хороших и разных Вперед

Классический параллельный сервер

Ниже показан (файл ech1.cc) код такого сервера, который несколько десятилетий считался классической реализацией параллельного сервера: когда по поступлению запроса (accept()) порождается (вызовом fork()) новый обслуживающий процесс (клон родительского процесса). Родительский процесс при этом возвращается в режим прослушивания следующих поступающих запросов:

#include "common.h" 
// ретранслятор c fork 
int main( int argc, char *argv[] ) { 
   int ls = getsocket( FORK_PORT ), rs; 
   setv( argc, argv ); 
   while( true ) { 
      if( ( rs = accept( ls, NULL, NULL ) ) < 0 ) errx( "accept error" ); 
      pid_t pid = fork(); 
      if( pid < 0 ) errx( "fork error" ); 
      if( pid == 0 ) { 
         close( ls ); 
         retrans( rs ); 
         close( rs ); 
         if( debug ) cout < "*" < flush; 
         exit( EXIT_SUCCESS ); 
      } 
      else close( rs ); 
   }; 
   exit( EXIT_SUCCESS ); 
}; 

После выхода из accept() (получение запроса connect() от клиента) – порождается отдельный обслуживающий процесс, который тут же закрывает свою копию прослушивающего сокета, производит ретрансляцию через соединённый сокет, завершает соединение и завершается сам. Родительский же процесс закрывает свою копию соединённого сокета и продолжает прослушивание канала. Вот результаты выполнения такого сервера в тех же, что и ранее, условиях:

$ sudo nice -n19 ./ech1 
waiting on port 51001 ... 
$ ./cli -a 192.168.1.5 -p 51001 -n 20 
host: 192.168.1.5, TCP port = 51001, number of echoes = 20 
time of reply - Cycles [usec.] : 
1535986[500]   1223094[398]    1594498[519]    2174443[708]       1590898[518] 
1193516[388]    865628[282]    1203671[392]     805564[262]       1141375[371] 
 895183[291]    857498[279]     816719[266]     949854[309]        852196[277] 
1137442[370]   1131899[368]     988333[321]    1106507[360]       1125275[366] 

Что произошло? Время реакции сервера по сравнению с простым последовательным возросло вдвое или чуть больше. Это плата за fork(), предшествующий началу обработки. Но не будем забывать, что при этом сервер уже во время обработки текущего готов обрабатывать всё новые и новые приходящие запросы!

Добавим в код сервера одну строку – перед точкой main опишем достаточно большую статическую область данных (файл ech10.cc, и изменён порт):

static long MEM[ 2500000 ];
...
$ sudo nice -n19 ./ech10 
waiting on port 51002 ... 
$ ./cli -a 192.168.1.5 -p 51002 -n 20 
host: 192.168.1.5, TCP port = 51002, number of echoes = 20 
time of reply - Cycles [usec.] : 
1521530[495]   1698090[553]  3454336[1125]	1847440[601]       1657460[539] 
1082966[352]   1264862[412]    762312[248]	1121848[365]       1152530[375] 
 937986[305]   1149793[374]    864869[281]       838316[273]        830622[270] 
 903727[294]    924036[301]   1116133[363]      1144215[372]       1003777[327] 

Цифры практически не изменились! Это говорит о многом:

  • при выполнении fork(), в порождённом процессе должна быть создана копия адресного пространства памяти родительского, в том числе, и статической области в 10Mb...
  • и копирование производится, кроме того, не простейшими функциями типа memcpy(), а копированием между различными защищёнными адресными пространствами;
  • но в данном случае работает техника COW («copy on write»), при которой реально страницы памяти будут дублированы и копироваться только при внесении в них изменений.

Для сравнения: в операционной системе реального времени QNX, которая не имеет права на отсроченные операции типа «copy on write» (требование детерминированности поведения) добавление такой строки в код увеличивало время реакции в 50 раз! Но не нужно упускать из виду и то, что такие временные затраты будут затребованы и в Linux (с COW) при первейших операциях записи в такие области данных, только произойдёт это в непредсказуемые моменты времени в будущем (в этом и есть не детерминированность).


Назад Сервер TCP/IP ... много серверов хороших и разных Вперед