Если мы запустим наш новый сервер, а затем запустим клиент на том же узле, то дважды соединившись с сервером, мы получим от клиента следующий вывод:
solaris % <b>daytimetcpcli 127.0.0.1</b>
Thu Sep 11 12:44:00 2003
solaris % <b>daytimetcpcli 192.168.1.20</b>
Thu Sep 11 12:44:09 2003
Сначала мы задаем IP-адрес сервера как адрес закольцовки на себя (loopback address) (127.0.0.1), а затем как его собственный IP-адрес (192.168.1.20). Вот соответствующий вывод сервера:
solaris # <b>daytimetcpsrv1</b>
connection from 127.0.0.1, port 43388
connection from 192.168.1.20, port 43389
Обратите внимание на то, что происходит с IP-адресом клиента. Поскольку наш клиент времени и даты (см. листинг 1.1) не вызывает функцию
bind
, как сказано в разделе 4.4, ядро выбирает IP-адрес отправителя, основанный на используемом исходящем интерфейсе. В первом случае ядро задает IP-адрес равным адресу закольцовки, во втором случае — равным IP-адресу интерфейса Ethernet. Кроме того, мы видим, что динамически назначаемый порт, выбранный ядром Solaris, — это 33 188, а затем 33 189 (см. рис. 2.10).
Наконец, заметьте, что приглашение интерпретатора команд изменилось на знак
#
— это приглашение к вводу команды для привилегированного пользователя. Наш сервер должен обладать правами привилегированного пользователя, чтобы с помощью функции
bind
связать зарезервированный порт 13. Если у нас нет прав привилегированного пользователя, вызов функции
bind
оказывается неудачным:
solaris % <b>daytimetcpsrv1</b>
bind error: Permission denied
4.7. Функции fork и exec
Прежде чем рассматривать создание параллельного сервера (что мы сделаем в следующем разделе), необходимо описать функцию Unix
fork
. Эта функция является единственным способом создания нового процесса в Unix.
#include <unistd.h>
pid_t fork(void);
<i>Возвращает: 0 в дочернем процессе, идентификатор дочернего процесса в родительском процессе, -1 в случае ошибки</i>
Если вы никогда не встречались с этой функцией, трудным для понимания может оказаться то, что она вызывается один раз, а возвращает два значения. Одно значение эта функция возвращает в вызывающем процессе (который называется родительским процессом) — этим значением является идентификатор созданного процесса (который называется дочерним процессом). Второе значение (нуль) она возвращает в дочернем процессе. Следовательно, по возвращаемому значению можно определить, является ли данный процесс родительским или дочерним.
Причина того, что функция
fork
возвращает в дочернем процессе нуль, а не идентификатор родительского процесса, заключается в том, что у дочернего процесса есть только один родитель, и дочерний процесс всегда может получить идентификатор родительского, вызвав функцию
getppid
. У родителя же может быть любое количество дочерних процессов, и способа получить их идентификаторы не существует. Если родительскому процессу требуется отслеживать идентификаторы своих дочерних процессов, он должен записывать возвращаемые значения функции
fork
.
Все дескрипторы, открытые в родительском процессе перед вызовом функции
fork
, становятся доступными дочерним процессам. Вы увидите, как это свойство используется сетевыми серверами: родительский процесс вызывает функцию
accept
, а затем функцию
fork
. Затем присоединенный сокет совместно используется родительским и дочерним процессами. Обычно дочерний процесс использует присоединенный сокет для чтения и записи, а родительский процесс только закрывает присоединенный сокет.
Существует два типичных случая применения функции
fork
:
1. Процесс создает свои копии таким образом, что каждая из них может обрабатывать одно задание. Это типичная ситуация для сетевых серверов. Далее в тексте вы увидите множество подобных примеров.
2. Процесс хочет запустить другую программу. Поскольку единственный способ создать новый процесс — это вызвать функцию
fork
, процесс сначала вызывает функцию
fork
, чтобы создать свою копию, а затем одна из копий (обычно дочерний процесс) вызывает функцию
exec
(ее описание следует за описанием функции
fork
), чтобы заменить себя новой программой. Этот сценарий типичен для таких программ, как интерпретаторы командной строки.
Единственный способ запустить в Unix на выполнение какой-либо файл — вызвать функцию
exec
. (Мы будем часто использовать общее выражение «функция
exec
», когда неважно, какая из шести функций семейства
exec
вызывается.) Функция
exec
заменяет копию текущего процесса новым программным файлом, причем в новой программе обычно запускается функция
main
. Идентификатор процесса при этом не изменяется. Процесс, вызывающий функцию
exec
, мы будем называть
вызывающим процессом, а выполняемую при этом программу —
новой программой.
ПРИМЕЧАНИЕ
В старых описаниях и книгах новая программа ошибочно называется «новым процессом». Это неверно, поскольку новый процесс не создается.
Различие между шестью функциями
exec
заключается в том, что они допускают различные способы задания аргументов:
■ выполняемый программный файл может быть задан или именем файла (filename), или полным именем (pathname);
■ аргументы новой программы либо перечисляются один за другим, либо на них имеется ссылка через массив указателей;
■ новой программе либо передается окружение вызывающего процесса, либо задается новое окружение.
#include <unistd.h>
int execl(const char *<i>pathname</i>, const char *<i>arg0</i>, ... /* (char*)0 */ );
int execv(const char *<i>pathname</i>, char *const <i>argv</i>[]);
int execle(const char *<i>pathname</i>, const char *<i>arg0</i> ... /* (char*)0,
char *const <i>envp</i>[] */ );
int execve(const char *<i>pathname</i>, char *const <i>argv</i>[], char *const <i>envp</i>[]);
int execlp(const char *<i>filename</i>, const char *<i>arg0</i>, .... /* (char*)0 */ );