Чтение онлайн

ЖАНРЫ

UNIX: разработка сетевых приложений
Шрифт:

Мы можем рассчитать время выполнения нашей неблокируемой версии, используя тот же файл из 2000 строк и тот же сервер (с периодом RTT, равным 175 мс), что и в разделе 6.7. Теперь время оказалось равным 6,9 с по сравнению с 12,3 с в версии из раздела 6.7. Следовательно, неблокируемый ввод-вывод сокращает общее время выполнения этого примера, в котором файл отправляется серверу.

Более простая версия функции str_cli

Неблокируемая версия функции

str_cli
, которую мы только что показали, нетривиальна: около 135 строк кода по сравнению с 40 строками версии, использующей функцию
select
с блокируемым вводом-выводом (см.
листинг 6.2), и 20 строками начальной версии, работающей в режиме остановки и ожидания (см. листинг 5.4). Мы знаем, что эффект от удлинения кода в два раза, с 20 до 40 строк оправдывает затраченные усилия, поскольку в пакетном режиме скорость возрастает почти в 30 раз, а применение функции
select
с блокируемыми дескрипторами осуществляется не слишком сложно. Но будут ли оправданы затраченные усилия при написании приложения, использующего неблокируемый ввод-вывод, с учетом усложнения итогового кода? Нет, ответим мы. Если нам необходимо использовать неблокируемый ввод-вывод, обычно бывает проще разделить приложение либо на процессы (при помощи функции
fork
), либо на потоки (см. главу 26).

В листинге 16.6 показана еще одна версия нашей функции

str_cli
, разделяемая на два процесса при помощи функции
fork
.

Эта функция сразу же вызывает функцию

fork
для разделения на родительский и дочерний процессы. Дочерний процесс копирует строки от сервера в стандартный поток вывода, а родительский процесс — из стандартного потока ввода серверу, как показано на рис. 16.4.

Рис. 16.4. Функция str_cli, использующая два процесса

Мы показываем, что соединения TCP являются двусторонними и что родительский и дочерний процессы совместно используют один и тот же дескриптор сокета: родительский процесс записывает в сокет, а дочерний процесс читает из сокета. Есть только один сокет, один буфер приема сокета и один буфер отправки, но на этот сокет ссылаются два дескриптора: один в родительском процессе и один в дочернем.

Листинг 16.6. Версия функции str_cli, использующая функцию fork

//nonblock/strclifork.c

1 #include "unp.h"

2 void

3 str_cli(FILE *fp, int sockfd)

4 {

5 pid_t pid;

6 char sendline[MAXLINE], recvline[MAXLINE];

7 if ((pid = Fork) == 0) { /* дочерний процесс: сервер -> stdout */

8 while (Readline(sockfd, recvline, MAXLINE) > 0)

9 Fputs(recvline, stdout);

10 kill(getppid, SIGTERM); /* в случае, если родительский процесс

все еще выполняется */

11 exit(0);

12 }

13 /* родитель: stdin -> сервер */

14 while (Fgets(sendline, MAXLINE, fp) != NULL)

15 Writen(sockfd, sendline, strlen(sendline));

16 Shutdown(sockfd, SHUT_WR); /* конец файла на stdin, посылаем FIN */

17 pause;

18 return;

19 }

Нам нужно снова вспомнить о последовательности завершения соединения. Обычное завершение происходит, когда

в стандартном потоке ввода встречается конец файла. Родительский процесс считывает конец файла и вызывает функцию
shutdown
для отправки сегмента FIN. (Родительский процесс не может вызвать функцию
close
, см. упражнение 16.1.) Но когда это происходит, дочерний процесс должен продолжать копировать от сервера в стандартный поток вывода, пока он не получит признак конца файла на сокете.

Также возможно, что процесс сервера завершится преждевременно (см. раздел 5.12), и если это происходит, дочерний процесс считывает признак конца файла на сокете. В таком случае дочерний процесс должен сообщить родительскому, что нужно прекратить копирование из стандартного потока ввода в сокет (см. упражнение 16.2). В листинге 16.6 дочерний процесс отправляет родительскому процессу сигнал

SIGTERM
, в случае, если родительский процесс еще выполняется (см. упражнение 16.3). Другим способом обработки этой ситуации было бы завершение дочернего процесса, и если родительский процесс все еще выполнялся бы к этому моменту, он получил бы сигнал
SIGCHLD
.

Родительский процесс вызывает функцию

pause
, когда заканчивает копирование, что переводит его в состояние ожидания того момента, когда будет получен сигнал. Даже если родительский процесс не перехватывает никаких сигналов, он все равно переходит в состояние ожидания до получения сигнала
SIGTERM
от дочернего процесса. По умолчанию действие этого сигнала — завершение процесса, что вполне устраивает нас в этом примере. Родительский процесс ждет завершения дочернего процесса, чтобы измерить точное время для этой версии функции
str_cli
. Обычно дочерний процесс завершается после родительского, но поскольку мы измеряем время, используя команду оболочки
time
, измерение заканчивается, когда завершается родительский процесс.

Отметим простоту этой версии по сравнению с неблокируемым вводом-выводом, представленным ранее в этом разделе. Наша неблокируемая версия управляла четырьмя различными потоками ввода-вывода одновременно, и поскольку все четыре были неблокируемыми, нам пришлось иметь дело с частичным чтением и частичной записью для всех четырех потоков. Но в версии с функцией

fork
каждый процесс обрабатывает только два потока ввода-вывода, копируя из одного в другой. В применении неблокируемого ввода-вывода не возникает необходимости, поскольку если нет данных для чтения из потока ввода, то и в соответствующий поток вывода записывать нечего.

Сравнение времени выполнения различных версий функции str_cli

Итак, мы продемонстрировали четыре различных версии функции

str_cli
. Для каждой версии мы покажем время, которое потребовалось для ее выполнения, в том числе и для версии, использующей программные потоки (см. листинг 26.1). В каждом случае было скопировано 2000 строк от клиента Solaris к серверу с периодом RTT, равным 175 мс:

354,0 с, режим остановки и ожидания (см. листинг 5.4);

12,3 с, функция

select
и блокируемый ввод-вывод (см. листинг 6.2);

6,9 с, неблокируемый ввод-вывод (см. листинг 16.1);

8,7 с, функция

fork
(см. листинг 16.6);

8,5 с, версия с потоками (см. листинг 26.1).

Наша версия с неблокируемым вводом-выводом почти вдвое быстрее версии, использующей блокируемый ввод-вывод с функцией

select
. Наша простая версия с применением функции
fork
медленнее версии с неблокируемым вводом- выводом. Тем не менее, учитывая сложность кода неблокируемого ввода-вывода по сравнению с кодом функции
fork
, мы рекомендуем более простой подход.

Поделиться с друзьями: