KnigaRead.com/
KnigaRead.com » Компьютеры и Интернет » Программное обеспечение » Уильям Стивенс - UNIX: разработка сетевых приложений

Уильям Стивенс - UNIX: разработка сетевых приложений

На нашем сайте KnigaRead.com Вы можете абсолютно бесплатно читать книгу онлайн Уильям Стивенс, "UNIX: разработка сетевых приложений" бесплатно, без регистрации.
Перейти на страницу:

Листинг 16.5. Отсортированный вывод функции tcpdump и данных диагностики

solaris % tcpdump -r tcpd -N | sort diag -

10:18:34.486392 solaris.33621 > linux.echo: S 1802738644:1802738644(0) win 8760 <mss 1460>

10:18:34.488278 linux.echo > solaris.33621: S 3212986316 3212986316(0) ack 1802738645 win 8760 <mss 1460>

10:18:34.488490 solaris.33621 > linux.echo: . ack 1 win 8760

10:18:34.491482: read 4096 bytes from stdin

10:18:34.518663 solaris.33621 > linux.echo: P 1461(1460) ack 1 win 8760

10:18:34.519016: wrote 4096 bytes to socket

10:18:34.528529 linux echo > solaris.33621. P 1:1461(1460) ack 1461 win 8760

10:18:34 528785 solaris.33621 > linux.echo: . 1461 2921(1460) ack 1461 win 8760

10:18:34.528900 solaris.33621 > linux echo: P 2921:4097(1176) ack 1461 win 8760

10:18:34.528958 solaris 33621 > linux.echo: ack 1461 win 8760

10:18:34.536193 linux echo: > solaris.33621: . 1461:2921(1460) ack 4097 win 8760

10:18:34.536697 linux.echo: > solaris.33621: P 2921.3509(588) ack 4097 win 8760

10:18.34.544636: read 4096 bytes from stdin 10:18:34.568505: read 3508 bytes from socket

10:18:34.580373 solaris 33621 > linux.echo: . ack 3509 win 8760

10:18:34.582244 linux.echo > solaris.33621: P 3509.4097(588) ack 4097 win 8760

10:18:34.593354: wrote 3508 bytes to stdout

10:18:34.617272 solaris.33621 > linux.echo: P 4097.5557(1460) ack 4097 win 8760

10:18:34.617610 solaris 33621 > linux.echo: P 5557:7017(1460) ack 4097 win 8760

10:18:34.617908 solaris.33621 > linux.echo: P 7017.8193(1176) ack 4097 win 8760

10:18:34.618062: wrote 4096 bytes to socket

10:18:34.623310 linux.echo > solaris.33621: . ack 8193 win 8760

10:18:34.626129 linux.echo > solaris.33621: . 4097.5557(1460) ack 8193 win 8760

10:18:34.626339 solaris.33621 > linux.echo: . ack 5557 win 8760

10:18:34.626611 linux.echo > solaris.33621: P 5557:6145(588) ack 8193 win 8760

10:18:34.628396 linux.echo > solaris.33621: 6145:7605(1460) ack 8193 win 8760

10:18:34.643524: read 4096 bytes from stdin 10:18:34.667305. read 2636 bytes from socket

10:18:34.670324 solaris.33621 > linux echo: . ack 7605 win 8760

10:18:34.672221 linux.echo > solaris.33621: P 7605.8193(588) ack 8193 win 8760

10:18:34.691039: wrote 2636 bytes to stdout

Мы удалили записи (DF) из сегментов, отправленных Solaris, означающие, что устанавливается бит DF (он используется для определения величины транспортной MTU).

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

Рис. 16.3. Временная диаграмма событий для примера неблокируемого ввода

На этом рисунке мы не показываем сегменты ACK. Также помните, что если программа выводит сообщение wrote N bytes to stdout (записано N байт в стандартное устройство вывода), это означает, что завершилась функция write, возможно, заставившая TCP отправить один или более сегментов данных.

По этому рисунку мы можем проследить динамику обмена между клиентом и сервером. Использование неблокируемого ввода-вывода позволяет программе использовать преимущество этой динамики, считывая или записывая данные, когда операция ввода или вывода может иметь место. Ядро сообщает нам, когда может произойти операция ввода-вывода, при помощи функции select.

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

Перейти на страницу:
Прокомментировать
Подтвердите что вы не робот:*