Мы можем рассчитать время выполнения нашей неблокируемой версии, используя тот же файл из 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
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
медленнее версии с неблокируемым вводом- выводом. Тем не менее, учитывая сложность кода неблокируемого ввода-вывода по сравнению с кодом функции