UNIX: разработка сетевых приложений — страница 15 из 42

Процессы-демоны и суперсервер inetd

13.1. Введение

Демон (daemon) — это процесс, выполняющийся в фоновом режиме и не связанный с управляющим терминалом. Системы Unix обычно имеют множество процессов (от 20 до 50), которые являются демонами, работают в фоновом режиме и выполняют различные административные задачи.

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

Существует несколько способов запустить демон:

1. Во время запуска системы многие демоны запускаются сценариями инициализации системы. Эти сценарии часто находятся в каталоге

/etc
  или в каталоге, имя которого начинается с
/etc/rc
, но их расположение и содержание зависят от реализации. Такие демоны запускаются с правами привилегированного пользователя.

Некоторые сетевые серверы часто запускаются из сценариев инициализации: суперсервер

inetd
(следующий пункт, который мы рассмотрим), веб-сервер и почтовый сервер (обычно это программа
sendmail
). Демон
syslogd
, обсуждаемый в разделе 13.2, тоже обычно запускается одним из этих сценариев.

2. Многие сетевые серверы запускаются суперсервером

inetd
, который мы опишем далее в этой главе. Сам
inetd
запускается в одном из сценариев на этапе 1. Суперсервер
inetd
прослушивает сетевые порты (Telnet, FTP и т.д.), и когда приходит запрос, активизирует требуемый сервер (сервер Telnet, сервер FTP и т.д.).

3. За периодические процессы в системе отвечает демон

cron
, и программы, которые он активизирует, выполняются как демоны. Сам демон
cron
запускается на этапе 1 во время загрузки системы.

4. Если программа должна быть выполнена однократно в определенный момент времени в будущем, применяется команда

at
. Демон
cron
обычно инициирует эти программы, когда приходит время их выполнения, поэтому они выполняются как демоны.

5. Демоны можно запускать с пользовательских терминалов, как в основном, так и в фоновом режимах. Это часто осуществляется при тестировании демона или перезапуске демона, завершенного по некоей причине.

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

syslog
— стандартный способ вывода таких сообщений. Эта функция посылает сообщения демону
syslogd
.

13.2. Демон syslogd

Системы Unix обычно запускают демон

syslogd
в одном из сценариев инициализации системы, и он функционирует, пока система работает. Реализации
syslogd
, происходящие от Беркли, выполняют при запуске следующие действия:

1. Считывается файл конфигурации, обычно

/etc/syslog.conf
, в котором указано, что делать с каждым типом сообщений, получаемых демоном. Эти сообщения могут добавляться в файл (особой разновидностью такого файла является
/dev/console
, который записывает сообщение на консоль), передаваться определенному пользователю (если этот пользователь вошел в систему) или передаваться демону
syslogd
на другом узле.

2. Создается доменный сокет Unix и связывается с полным именем

/var/run/log
(в некоторых системах
/dev/log
).

3. Создается сокет UDP и связывается с портом 514 (служба

syslog
).

4. Открывается файл (устройство)

/dev/klog
. Любые сообщения об ошибках внутри ядра появляются как входные данные на этом устройстве.

Демон

syslogd
выполняется в бесконечном цикле, в котором вызывается функция
select
, ожидающая, когда один из трех его дескрипторов (из п. 2, 3 и 4) станет готов для чтения. Этот демон считывает сообщение и выполняет то, что предписывает делать с этим сообщением файл конфигурации. Если демон получает сигнал
SIGHUP
, он заново считывает файл конфигурации.

ПРИМЕЧАНИЕ

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

Между реализациями демона syslogd существуют различия. Например, доменные сокеты Unix используются Беркли-реализациями, а реализации System V используют потоковый драйвер (streams log driver). Различные реализации, происходящие от Беркли, используют для доменных сокетов Unix различные полные имена. Мы можем игнорировать все эти тонкости, если используем функцию syslog.

Мы можем отправлять сообщения о событиях для записи в журнал (log messages) демону

syslogd
из наших демонов, создав дейтаграммный доменный сокет Unix и указывая при отправке полное имя, с которым связан демон, но более простым интерфейсом является функция
syslog
, которую мы описываем в следующем разделе. В качестве альтернативы мы можем создать сокет UDP и отправлять наши сообщения на адрес закольцовки и порт 514.

13.3. Функция syslog

Поскольку у демона нет управляющего терминала, он не может просто вызвать функцию

fprintf
для вывода в стандартный поток сообщений об ошибках (
stderr
). Обычная техника записи в журнал сообщений для демона — это вызов функции
syslog
.

#include 


void syslog(int priority, const char *message, ...);

Хотя эта функция изначально разрабатывалась для BSD, в настоящее время она предоставляется большинством производителей систем Unix. Описание

syslog
в POSIX соответствует тому, что мы пишем здесь. RFC 3164 содержит документацию, касающуюся протокола
syslog
BSD.

Аргумент

priority
— это комбинация аргументов
level
и
facility
, которые мы показываем в табл. 13.1 и 13.2. Дополнительные сведения об этом аргументе можно найти в RFC 3164. Аргумент
message
аналогичен строке формата функции
printf
с добавлением спецификации
%m
, которая заменяется сообщением об ошибке, соответствующим текущему значению переменной
errno
. Символ перевода строки может появиться в конце строки
message
, но он не является обязательным.

Сообщения для журнала имеют значение

level
(уровень) от 0 до 7, что мы показываем в табл. 13.1. Это упорядоченные значения. Если отправитель не задает значение
level
, используется значение по умолчанию
LOG_NOTICE
.


Таблица 13.1. Аргумент level журнальных сообщений

LevelЗначениеОписание
LOG_EMERG0Система не может функционировать, экстренная ситуация (наивысший приоритет)
LOG_ALERT1Следует немедленно принять меры, срочная ситуация
LOG_CRIT2Критическая ситуация
LOG_ERR3Состояние ошибки
LOG_WARNING4Предупреждение
LOG_NOTICE5Необычное, хотя и не ошибочное состояние (значение аргумента level по умолчанию)
LOG_INFO6Информационное сообщение
LOG_DEBUG7Отладочные сообщения (низший приоритет)

Сообщения также содержат аргумент

facility
для идентификации типа процесса, посылающего сообщение. Мы показываем его различные значения в табл. 13.2. Если не задано значение аргумента
facility
, используется его значение по умолчанию —
LOG_USER
.


Таблица 13.2. Аргумент facility журнальных сообщений

facilityОписание
LOG_AUTHСообщения no безопасности/авторизации
LOG_AUTHPRIVСообщения по безопасности/авторизации (частные)
LOG_CRONДемон cron
LOG_DAEMONСистемные демоны
LOG_FTPДемон FTP
LOG_KERNСообщения ядра
LOG_LOCAL0Локальное использование
LOG_LOCAL1Локальное использование
LOG_LOCAL2Локальное использование
LOG_LOCAL3Локальное использование
LOG_LOCAL4Локальное использование
LOG_LOCAL5Локальное использование
LOG_LOCAL6Локальное использование
LOG_LOCAL7Локальное использование
LOG_LPRДемон принтера
LOG_MAILПочтовая система
LOG_NEWSСистема телеконференций
LOG_SYSLOGВнутренние сообщения системы syslog
LOG_USERСообщения пользовательского уровня (значение аргумента facility по умолчанию)
LOG_UUCPСистема UUCP

Например, демон может сделать следующий вызов, когда вызов функции

rename
неожиданно оказывается неудачным:

syslog(LOG_INFO|LOG_LOCAL2, "rename(%s, %s): %m", file1, file2);

Назначение аргументов

facility
и
level
в том, чтобы все сообщения, которые посылаются процессами определенного типа (то есть с одним значением аргумента
facility
), могли обрабатываться одинаково в файле
/etc/syslog.conf
или чтобы все сообщения одного уровня (с одинаковым значением аргумента
level
) обрабатывались одинаково. Например, файл конфигурации может содержать строки

kern.* /dev/console

local7.debug /var/log/cisco.log

для указания, что все сообщения ядра направляются на консоль, а сообщения относительно отладки со значением аргумента

facility
, равным
local7
, добавляются в файл
/var/log/cisco.log
.

Когда приложение впервые вызывает функцию

syslog
, она создает дейтаграммный доменный сокет Unix и затем вызывает функцию
connect
для сокета с заранее известным полным именем, которое создано демоном
syslogd
(например,
/var/run/log
). Этот сокет остается открытым, пока процесс не завершится. Другим вариантом является вызов процессом функций
openlog
и
closelog
.

#include 


void openlog(const char *ident, int options, int facility);

void closelog(void);

Функция

openlog
может быть вызвана перед первым вызовом функции
syslog
, а функция
closelog
— когда приложение закончит отправлять сообщения в журнал.

Аргумент

ident
— это строка, которая будет добавлена в начало каждого журнального сообщения функцией
syslog
. Часто это имя программы.

Обычно аргумент

options
формируется путем применения операции логического ИЛИ к константам из табл. 13.3.


Таблица 13.3. Аргумент options (параметр) для функции openlog

ПараметрОписание
LOG_CONSВыводить журнал на консоль, если невозможно послать сообщение демону syslogd
LOG_NDELAYНе откладывать создание сокета, открыть его сейчас
LOG_PERRORЗаписывать сообщение в stderr, а также посылать его демону syslogd
LOG_PIDВключать идентификатор процесса (PID) в каждую запись журнала

Обычно доменный сокет Unix не создается при вызове функции

openlog
. Вместо этого сокет открывается при первом вызове функции
syslog
. Параметр
LOG_NDELAY
указывает, что сокет должен создаваться при вызове функции
openlog
.

Аргумент

facility
функции
openlog
задает значение
facility
, используемое по умолчанию для любого последующего вызова функции
syslog
, при котором не задается аргумент
facility
. Некоторые демоны вызывают функцию
openlog
и задают значение аргумента
facility
(которое обычно не изменяется для данного демона) и затем в каждом вызове функции
syslog
задают только аргумент
level
(поскольку
level
может изменяться в зависимости от ошибки).

Сообщения для записи в журнал могут также генерироваться командой

logger
. Это может использоваться в сценариях интерпретатора команд, например для отправки сообщений демону
syslogd
.

13.4. Функция daemon_init

В листинге 13.1[1] показана функция, называемая

daemon_init
, которую мы можем вызвать (обычно с сервера), чтобы придать процессу свойства демона.

Листинг 13.1. Функция daemon_init: придание процессу свойств демона

//daemon _init.с

 1 #include "unp.h"

 2 #include 


 3 #define MAXFD 64


 4 extern int daemon_proc; /* определен в error.с */


 5 int

 6 daemon_init(const char *pname, int facility)

 7 {

 8  int i;

 9  pid_t pid;


10  if ((pid = Fork()) < 0)

11   return (-1);

12  else if (pid)

13   _exit(0); /* родитель завершается */


14  /* 1-й дочерний процесс продолжает работу... */


15  if (setsid() < 0) /* становится главным процессом сеанса */

16   return (-1);


17  Signal(SIGHUP, SIG_IGN);

18  if ((pid = Fork()) < 0)

19   return (-1);

20  else if (pid)

21   _exit(0); /* 1-й дочерний процесс завершается */


22  /* 2-й дочерний процесс продолжает работу */


23  daemon_proc = 1; /* для функций err_XXX() */


24  chdir("/"); /* смена текущего каталога */


25  /* закрытие дескрипторов файлов*/

26  for (i = 0; i < MAXFD; i++)

27   close(i);


28  /* перенаправление stdin, stdout и stderr в /dev/null */

29  open("/dev/null", O_RDONLY);

30  open("/dev/null", O_RDWR);

31  open("/dev/null", O_RDWR);


32  openlog(pname, LOG_PID, facility);


33  return (0); /* успешное завершение */

34 }

Вызов функции fork

10-13
 Сначала мы вызываем функцию
fork
, после чего родительский процесс завершается, а дочерний продолжается. Если процесс был запущен из интерпретатора команд в фоновом режиме, то, когда родительский процесс завершается, оболочка считает, что команда выполнена. Это автоматически запускает дочерний процесс в фоновом режиме. Дочерний процесс наследует идентификатор группы процессов от родительского процесса, но получает свой собственный идентификатор процесса. Это гарантирует, что дочерний процесс не является главным в группе процессов, что требуется для следующего вызова функции
setsid
.

Вызов функции setsid

15-16
 Функция
setsid
— это функция POSIX, создающая новый сеанс. (В главе 9 [110] подробно рассказывается о взаимоотношениях процессов.) Процесс становится главным в новом сеансе, становится главным в новой группе процессов и не имеет управляющего терминала.

Игнорирование сигнала SIGHUP и новый вызов функции fork

17-21
 Мы игнорируем сигнал
SIGHUP
и снова вызываем функцию
fork
. Когда эта функция завершается, родительский процесс на самом деле является первым дочерним процессом, и он завершается, оставляя выполняться второй дочерний процесс. Назначение второй функции
fork
— гарантировать, что демон не сможет автоматически получить управляющий терминал, если потом он откроет устройство терминала. В SVR4, когда главный процесс сеанса без управляющего терминала открывает устройство терминала (которое в этот момент не является управляющим терминалом для другого сеанса), терминал становится управляющим терминалом главного процесса сеанса. Но вызывая второй раз функцию
fork
, мы гарантируем, что второй дочерний процесс больше не является главным в сеансе, поэтому он не может получить управляющий терминал. Сигнал
SIGHUP
приходится игнорировать, поскольку, когда главный процесс сеанса завершает работу (первый дочерний процесс), всем процессам в сеансе (нашему второму дочернему процессу) посылается сигнал SIGHUP.

Установка флага для функций ошибок

23
 Мы присваиваем глобальной переменной
daemon_proc
ненулевое значение. Эта внешняя переменная задается нашими функциями
err_XXX
(см. раздел Г.4), и ее ненулевое значение сообщает этим функциям, что нужно вызвать функцию
syslog
вместо функции
fprintf
(которая выводит сообщение об ошибке в стандартный поток сообщений об ошибках). Это спасает нас от необходимости проходить через весь наш код и вызывать одну из наших функций ошибок, если сервер не работает как демон (то есть когда мы проверяем сервер), а при работе в режиме демона заменять все вызовы на вызовы
syslog
.

Изменение рабочего каталога и сброс всех битов в маске режима создания файла

24
 Мы изменяем рабочий каталог на корневой каталог, хотя у некоторых демонов могут быть причины изменить рабочий каталог на какой-либо другой. Например, демон печати может изменить его на каталог, в котором накапливается содержимое заданий для принтера и происходит вся работа по выводу данных на печать. Если демоном сбрасывается дамп (файл
core
), он появляется в текущем рабочем каталоге. Другой причиной для изменения рабочего каталога является то, что демон мог быть запущен в любой файловой системе, и если он там останется, эту систему нельзя будет размонтировать, во всяком случае, без жестких мер.

Закрытие всех открытых дескрипторов

25-27
 Мы закрываем все открытые дескрипторы, которые наследуются от процесса, запустившего демон (обычно этим процессом бывает интерпретатор команд). Проблема состоит в определении наибольшего используемого дескриптора: в Unix нет ни одной функции, предоставляющей это значение. Есть способы определения максимального числа дескрипторов, которое может открыть процесс, но даже это достаточно сложно [110, с. 43], поскольку предел может быть бесконечным. Наше решение — закрыть первые 64 дескриптора, даже если большинство из них, возможно, не было открыто.

ПРИМЕЧАНИЕ

Solaris предоставляет функцию closefrom, позволяющую демонам решать эту проблему.

Перенаправление stdin, stdout и stderr в /dev/null

29-31
 Некоторые демоны открывают
/dev/null
для чтения и записи и подключают к нему дескрипторы стандартных потоков ввода, вывода и сообщений об ошибках. Это гарантирует, что наиболее типичные дескрипторы открыты и операция чтения из любого из них возвращает 0 (конец файла), а ядро игнорирует все, что записано в любой из этих трех дескрипторов. Причина, по которой требуется открыть эти дескрипторы, заключается в том, что любая библиотечная функция, вызываемая демоном и считающая, что она может читать из стандартного потока ввода или записывать либо в стандартный поток вывода, либо в стандартный поток сообщений об ошибках, не должна завершиться с ошибкой. Отказ был бы потенциально опасен: если демон открывает сокет для связи с клиентом, дескриптор сокета воспринимается как стандартный поток вывода, поэтому ошибочный вызов какой-нибудь функции типа
perror
может привести к отправке клиенту нежелательных данных.

Использование демона syslogd для вывода сообщений об ошибках

32
 Вызывается функция
openlog
. Первый ее аргумент берется из вызывающего процесса и обычно является именем программы (например,
argv[0]
). Мы указываем, что идентификатор процесса должен добавляться к каждому сообщению. Аргумент
facility
также задается вызывающим процессом, и его значением может быть константа из табл. 13.2 либо, если приемлемо значение по умолчанию
LOG_USER
, нулевое значение.

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

SIGHUP
от ядра. Следовательно, многие демоны используют этот сигнал в качестве уведомления от администратора, что файл конфигурации демона изменился и демон должен еще раз считать файл. Два других сигнала, которые демон никогда не должен получать, — это сигналы
SIGINT
и
SIGWINCH
, и они также могут использоваться для уведомления демона о некоторых изменениях.

Пример: сервер времени и даты в качестве демона

В листинге 13.2 представлено изменение нашего сервера времени и даты, не зависящего от протокола. В отличие от сервера, показанного в листинге 11.8, в нем вызывается функция

daemon_init
, чтобы этот сервер мог выполняться в качестве демона.

Листинг 13.2. Не зависящий от протокола сервер времени и даты, работающий в качестве демона

//inetd/daytimetcpsrv2.c

 1 #include "unp.h"

 2 #include 


 3 int

 4 main(int argc, char **argv)

 5 {

 6  int listenfd, connfd;

 7  socklen_t addrlen, len;

 8  struct sockaddr *cliaddr;

 9  char buff[MAXLINE];

10  time_t ticks;


11  daemon_init(argv[0], 0);


12  if (argc == 2)

13   listenfd = Tcp_listen(NULL, argv[1], &addrlen);

14  else if (argc == 3)

15   listenfd = Tcp_listen(argv[1], argv[2], &addrlen);

16  else

17   err_quit("usage: daytimetcpsrv2 [  ] ");


18  cliaddr = Malloc(addrlen);


19  for (;;) {

20   len = addrlen;

21   connfd = Accept(listenfd, cliaddr, &len);

22   err_msg("connection from %s", Sock_ntop(cliaddr, len));


23   ticks = time(NULL);

24   snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));

25   Write(connfd, buff, strlen(buff));


26   Close(connfd);

27  }

28 }

Изменений всего два: мы вызываем нашу функцию

daemon_init
, как только программа запускается, а затем вызываем нашу функцию
err_msg
вместо
printf
, чтобы вывести IP-адрес и порт клиента. На самом деле, если мы хотим, чтобы наши программы могли выполняться как демоны, мы должны исключить вызов функций
printf
и
fprintf
и вместо них использовать нашу функцию
err_msg
.

Обратите внимание, что мы проверяем argc и выводим соответствующее сообщение до вызова

daemon_init
. Таким образом пользователь, запустивший демона, получает немедленное уведомление о недопустимом количестве аргументов. После вызова
daemon_init
все сообщения направляются в системный журнал.

Если мы запустим эту программу на нашем узле

linux
и затем проверим файл
/var/log/messages
(куда мы отправляем все сообщения
LOG_USER
) после соединения с тем же узлом, мы получим:

Jul 10 09:54:37 linux daytimetcpsrv2[24288]: connection from 127.0.0.1.55862

Дата, время и имя узла автоматически ставятся в начале сообщения демоном

syslogd
.

13.5. Демон inetd

В типичной системе Unix может существовать много серверов, ожидающих запроса клиента. Примерами являются FTP, Telnet, Rlogin, TFTP и т.д. В системах, предшествующих 4.3BSD, каждая из этих служб имела связанный с ней процесс. Этот процесс запускался во время загрузки из файла

/etc/rc
, и каждый процесс выполнял практически идентичные задачи запуска: создание сокета, связывание при помощи функции
bind
заранее известного порта с сокетом, ожидание соединения (TCP) или получения дейтаграммы (UDP) и последующее выполнение функции
fork
. Дочерний процесс выполнял обслуживание клиента, а родительский процесс ждал, когда поступит следующий запрос клиента. Эта модель характеризуется двумя недостатками.

1. Все демоны содержали практически идентичный код запуска, направленный сначала на создание сокета, а затем на превращение процесса в процесс демона (аналогично нашей функции

daemon_init
).

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

Реализация 4.3BSD упростила ситуацию, предоставив суперсервер (superserver) Интернета — демон

inetd
. Этот демон может применяться серверами, использующими TCP или UDP, и не поддерживает других протоколов, таких как доменные сокеты Unix. Демон
inetd
решает две вышеупомянутые проблемы.

1. Он упрощает написание процессов демонов, поскольку обрабатывает большинство подробностей запуска. Таким образом устраняется необходимость вызова нашей функции

daemon_init
для каждого сервера.

2. Этот демон позволяет одиночному процессу (

inetd
) ждать входящие клиентские запросы ко множеству служб (вместо одного процесса для каждой службы). Это сокращает общее число процессов в системе.

Процесс

inetd
сам становится демоном, используя технологии, которые мы изложили при описании функции
daemon_init
. Затем он считывает и обрабатывает файл конфигурации, обычно файл
/etc/inetd.conf
. Этот файл задает, какие службы должен обрабатывать суперсервер, а также что нужно делать, когда приходит запрос к одной из этих служб. Каждая строка содержит поля, показанные в табл. 13.4. Вот несколько строк в качестве примера:

ftp    stream tcp nowait root   /usr/bin/ftpd ftpd -l

telnet stream tcp nowait root   /usr/bin/telnetd telnetd

login  stream tcp nowait root   /usr/bin/rlogind rlogind -s

tftp   dgram  udp wait   nobody /usr/bin/tftpd tftpd -s /tftpboot

Действительное имя сервера всегда передается в качестве первого аргумента программе, выполняемой с помощью функции

exec
.


Таблица 13.4. Поля файла inetd.conf

ПолеОписание
service-nameДолжен быть в /etc/services
socket-typestream (TCP) или dgram (UDP)
ProtocolДолжен быть в /etc/protocols; либо tcp, либо udp
wait-flagОбычно nowait для TCP и wait для UDP
login-nameИз /etc/password; обычно root
server-programПолное имя программы для вызова exec
server-program-argumentsАргументы программы для вызова exec
ПРИМЕЧАНИЕ

Таблица и приведенные строки — это только пример. Большинство производителей добавили демону inetd свои собственные функции. Примером может служить возможность обрабатывать серверы вызовов удаленных процедур (RPC) в дополнение к серверам TCP и UDP, а также возможность обрабатывать другие протоколы, отличные от TCP и UDP. Полное имя для функции exec и аргументы командной строки сервера, очевидно, зависят от приложения.

Флаг wait-flag может быть достаточно труден для понимания. Он указывает, собирается ли демон, запускаемый inetd, взять на себя работу с прослушиваемым сокетом. Сервисы UDP лишены деления на прослушиваемые и принятые сокеты, и потому практически всегда создаются с флагом wait-flag, равным wait. Сервисы TCP могут вести себя по-разному, но чаще всего для них указывается флаг wait-flag со значением nowait.

Взаимодействие IPv6 с файлом /etc/inetd.conf зависит от производителя. Иногда в качестве поля protocol указывается tcp6 или udp6, чтобы подчеркнуть, что для сервера должен быть создан сокет IPv6. Некоторые разрешают использовать значения protocol, равные tcp46 и udp46, если сервер готов принимать соединения по обоим протоколам. Специальные названия протоколов обычно не включаются в файл /etc/protocols.

Иллюстрация действий, выполняемых демоном

inetd
, представлена на рис. 13.1.

Рис. 13.1. Действия, выполняемые демоном inetd

1. При запуске демон читает файл

/etc/inetd.conf
и создает сокет соответствующего типа (потоковый или дейтаграммный сокет) для всех служб, заданных в файле. Максимальное число серверов, которые может обрабатывать демон
inetd
, зависит от максимального числа дескрипторов, которые он может создать. Каждый новый сокет добавляется к набору дескрипторов, который будет использован при вызове функции
select
.

2. Для каждого сокета вызывается функция

bind
, задающая заранее известный порт для сервера и универсальный IP-адрес. Этот номер порта TCP или UDP получается при вызове функции
getservbyname
с полями
service
-name и
protocol
из файла конфигурации в качестве аргументов.

3. Для сокетов TCP вызывается функция

listen
, так что принимаются входящие запросы на соединение. Этот шаг не выполняется для дейтаграммных сокетов.

4. После того как созданы все сокеты, вызывается функция

select
, ожидающая, когда какой-либо из сокетов станет готов для чтения. Вспомните (раздел 6.3), что прослушиваемый сокет TCP становится готов для чтения, когда новое соединение готово быть принятым с помощью функции
accept
, а сокет UDP становится готов для чтения, когда приходит дейтаграмма. Демон
inetd
большую часть времени блокирован в вызове функции
select
, ожидая, когда сокет станет готов для чтения.

5. При указании флага

nowait
для сокетов TCP вызывается функция
accept
сразу же, как только дескриптор сокета становится готов для чтения.

6. Демон

inetd
запускает функцию
fork
, и дочерний процесс обрабатывает запрос клиента. Это аналогично стандартному параллельному серверу (см. раздел 4.8).

Дочерний процесс закрывает все дескрипторы, кроме дескриптора, который он обрабатывает: новый присоединенный сокет, возвращаемый функцией

accept
для сервера TCP, или исходный сокет UDP. Дочерний процесс трижды вызывает функцию
dup2
, подключая сокет к дескрипторам 0, 1 и 2 (стандартные потоки ввода, вывода и сообщений об ошибках). Исходный дескриптор сокета затем закрывается. При этом в дочернем процессе открытыми остаются только дескрипторы 0, 1 и 2. Если дочерний процесс читает из стандартного потока ввода, он читает из сокета, и все, что он записывает в стандартный поток вывода или стандартный поток сообщений об ошибках, записывается в сокет. Дочерний процесс вызывает функцию
getpwnam
, чтобы получить значение поля
login-name
, заданного в файле конфигурации. Если это не поле root, дочерний процесс становится указанным пользователем при помощи функций
setgid
и
setuid
. (Поскольку процесс
inetd
выполняется с идентификатором пользователя, равным 0, дочерний процесс наследует этот идентификатор пользователя при выполнении функции
fork
, поэтому он имеет возможность стать любым пользователем по своему выбору.)

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

exec
, чтобы выполнить соответствующую программу сервера (поле
server-program
) для обработки запроса, передавая аргументы, указанные в файле конфигурации.

7. Если сокет является потоковым сокетом, родительский процесс должен закрыть присоединенный сокет (как наш стандартный параллельный сервер). Родительский процесс снова вызывает функцию

select
, ожидая, когда следующий сокет станет готов для чтения.

Чтобы рассмотреть более подробно, что происходит с дескрипторами, на рис. 13.2 показаны дескрипторы демона

inetd
в момент прихода нового запроса на соединение от клиента FTP.

Рис. 13.2. Дескрипторы демона inetd в тот момент, когда приходит запрос на порт 21 TCP

Запрос на соединение направляется на порт 21 TCP; новый присоединенный сокет создается функцией

accept
.

На рис. 13.3 показаны дескрипторы в дочернем процессе после вызова функции

fork
, после того как дочерний процесс закрывает все остальные дескрипторы, кроме дескрипторов присоединенного сокета.

Рис. 13.3. Дескрипторы демона inetd в дочернем процессе

Следующий шаг для дочернего процесса — подключение присоединенного сокета к дескрипторам 0, 1 и 2 и последующее закрытие присоединенного сокета. При этом мы получаем дескрипторы, изображенные на рис. 13.4.

Рис. 13.4. Дескрипторы демона inetd после выполнения функции dup2

Затем дочерний процесс вызывает функцию

exec
, и, как сказано в разделе 4.7, во время выполнения функции
exec
все дескрипторы обычно остаются открытыми, поэтому реальный сервер, на котором выполняется функция
exec
, использует любой из дескрипторов 0, 1 и 2 для взаимодействия с клиентом. Эти дескрипторы должны быть единственными открытыми на стороне сервера дескрипторами.

Описанный нами сценарий относится к ситуации, при которой файл конфигурации задает в поле

wait-flag
значение
nowait
для сервера. Это типично для всех служб TCP и означает, что демону
inetd
не нужно ждать завершения его дочернего процесса, перед тем как он примет другое соединение для данной службы. Если приходит другой запрос на соединение для той же службы, он возвращается родительскому процессу, как только тот снова вызовет функцию
select
. Шаги 4, 5 и 6, перечисленные выше, выполняются снова, и новый запрос обрабатывается другим дочерним процессом.

Задание флага

wait
для дейтаграммного сервиса изменяет шаги, выполняемые родительским процессом. Флаг указывает на то, что демон
inetd
должен ждать завершения своего дочернего процесса, прежде чем снова вызвать функцию
select
для определения готовности этого сокета UDP для чтения. Происходят следующие изменения:

1. После выполнения функции

fork
в родительском процессе сохраняется идентификатор дочернего процесса. Это дает возможность родительскому процессу узнать, когда завершается определенный дочерний процесс, анализируя значение, возвращаемое функцией
waitpid
.

2. Родительский процесс отключает способность сокета выполнять последующие функции

select
, сбрасывая соответствующий бит в наборе дескрипторов с помощью макроса
FD_CLR
. Это значит, что дочерний процесс завладевает сокетом до своего завершения.

3. Когда завершается дочерний процесс, родительский процесс уведомляется об этом с помощью сигнала

SIGCHLD
, и обработчик сигналов родительского процесса получает идентификатор завершающегося дочернего процесса. Он снова включает функцию
select
для соответствующего сокета, устанавливая бит для этого сокета в своем наборе дескрипторов.

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

inetd
возможности выполнять функцию
select
на этом сокете для проверки готовности его для чтения (в ожидании другой дейтаграммы клиента), в том, что для сервера дейтаграмм существует только один сокет, в отличие от сервера TCP, у которого имеется прослушиваемый сокет и по одному присоединенному сокету для каждого клиента. Если демон
inetd
не отключил чтение на сокете дейтаграмм и, допустим, родительский процесс (
inetd
) завершил выполнение перед дочерним, дейтаграмма от клиента все еще будет находиться в приемном буфере сокета. Это приводит к тому, что функция
select
снова сообщает, что сокет готов для чтения, и демон
inetd
снова выполняет функцию
fork
, порождая другой (ненужный) дочерний процесс. Демон
inetd
должен игнорировать дейтаграммный сокет до тех пор, пока он не узнает, что дочерний процесс прочитал дейтаграмму из приемного буфера сокета. Демон
inetd
узнает, что дочерний процесс закончил работу с сокетом, путем получения сигнала
SIGCHLD
, указывающего на то, что дочерний процесс завершился. Подобный пример мы показываем в разделе 22.7.

Пять стандартных служб Интернета, описанных в табл. 2.1, обеспечиваются самим демоном

inetd
(см. упражнение 13.2).

Поскольку функцию

accept
для сервера TCP вызывает демон
inetd
(а не сам сервер), реальный сервер, запускаемый демоном
inetd
, обычно вызывает функцию
getpeername
для получения IP-адреса и номера порта клиента. Вспомните рис. 4.9, где мы показывали, что после выполнения вызовов
fork
и
exec
(что выполняет демон
inetd
) у реального сервера есть единственный способ получить идентификацию клиента — вызвать функцию
getpeername
.

Демон

inetd
обычно не используется для серверов, работающих с большими объемами данных, в особенности почтовыми серверами и веб-серверами. Например, функция
sendmail
обычно запускается как стандартный параллельный сервер, как мы отмечали в разделе 4.8. В этом режиме стоимость порождения процесса для каждого клиентского соединения равна стоимости функции
fork
, тогда как в случае сервера TCP, активизированного демоном
inetd
, — стоимости функций
fork
и
exec
. Веб-серверы используют множество технологий для минимизации накладных расходов при порождении процессов для обслуживания клиентов, как мы покажем в главе 30.

13.6. Функция daemon_inetd

В листинге 13.3 показана функция

daemon_inetd
, которую мы можем вызвать с сервера, запущенного демоном
inetd
.

Листинг 13.3. Функция daemon_inetd для придания свойств демона процессу, запущенному демоном inetd

//daemon_inetd.c

1 #include "unp.h"

2 #include 


3 extern int daemon_proc; /* определено в error.c */


4 void

5 daemon_inetd(const char *pname, int facility)

6 {

7  daemon_proc = 1; /* для наших функций err_XXX() */

8  openlog(pname, LOG_PID, facility);

9 }

Эта функция тривиальна по сравнению с

daemon_init
, потому что все шаги выполняются демоном
inetd
при запуске. Все, что мы делаем, — устанавливаем флаг
daemon_proc
для наших функций ошибок (см. табл. Г.1) и вызываем функцию
openlog
с теми же аргументами, что и при вызове функции
daemon_init
, представленной в листинге 13.1.

Пример: сервер времени и даты, активизированный демоном inetd

Листинг 13.4 представляет собой модификацию нашего сервера времени и даты, показанного в листинге 13.2, который может быть активизирован демоном

inetd
.

Листинг 13.4. Не зависящий от протокола сервер времени и даты, который может быть активизирован демоном inetd

//inetd/daytimetcpsrv3.c

 1 #include "unp.h"

 2 #include 


 3 int

 4 main(int argc, char **argv)

 5 {

 6  socklen_t len;

 7  struct sockaddr *cliaddr;

 8  char buff[MAXLINE];

 9  time_t ticks;


10  daemon_inetd(argv[0], 0);


11  cliaddr = Malloc(MAXSOCKADDR);

12  len = MAXSOCKADDR;

13  Getpeername(0, cliaddr, &len);

14  err_msg("connection from %s", Sock_ntop(cliaddr, len));


15  ticks = time(NULL);

16  snprintf(buff, sizeof(buff), "%.24s\r\n\", ctime(&ticks));

17  Write(0, buff, strlen(buff));


18  Close(0); /* закрываем соединение TCP */

19  exit(0);

20 }

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

tcp_listen
и
accept
. Эти шаги выполняются демоном
inetd
, и мы ссылаемся на соединение TCP, используя нулевой дескриптор (стандартный поток ввода). Во-вторых, исчез бесконечный цикл
for
, поскольку сервер активизируется по одному разу для каждого клиентского соединения. После предоставления сервиса клиенту сервер завершает свою работу.

Вызов функции getpeername

11-14
 Поскольку мы не вызываем функцию
tcp_listen
, мы не знаем размера структуры адреса сокета, которую она возвращает, а поскольку мы не вызываем функцию
accept
, то не знаем и адреса протокола клиента. Следовательно, мы выделяем буфер для структуры адреса сокета, используя нашу константу
MAXSOCKADDR
и вызываем функцию
getpeername
с нулевым дескриптором в качестве первого аргумента.

Чтобы выполнить этот пример в нашей системе Solaris, сначала мы присваиваем службе имя и порт, добавляя следующую строку в

/etc/services
:

mydaytime 9999/tcp

Затем добавляем строку в

/etc/inetd.conf
:

mydaytime stream tcp nowait andy

/home/andy/daytimetcpsrv3 daytimetcpsrv3

(Мы разбили длинную строку на более короткие.) Мы помещаем выполняемый код в заданный файл и отправляем демону

inetd
сигнал
SIGHUP
, сообщающий ему, что нужно заново считать файл конфигурации. Следующий шаг — выполнить программу
netstat
, чтобы проверить, что на порте TCP 9999 создан прослушиваемый сокет:

solaris % netstat -na | grep 9999

*.9999 *.* 0 0 49152 0 LISTEN

Затем мы запускаем сервер с другого узла:

linux % telnet solaris 9999

Trying 192.168.1.20...

Connected to solaris.

Escape character is '^]'.

Tue Jun 10 11:04:02 2003

Connection closed by foreign host.

Файл

/var/amd/messages
(в который, как указано в нашем файле
/etc/syslog.conf
, должны направляться наши сообщения с аргументом
facility=LOG_USER
) содержит запись:

Jun 10 11:04:02 solaris daytimetcpsrv3[28724]: connection from 192.168.1.10.58145

13.7. Резюме

Демоны — это процессы, выполняемые в фоновом режиме независимо от управления с терминалов. Многие сетевые серверы работают как демоны. Все выходные данные демона обычно отправляются демону

syslogd
при помощи вызова функции
syslog
. Администратор полностью контролирует все, что происходит с этими сообщениями, основываясь на том, какой демон отправил данное сообщение и насколько оно серьезно.

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

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

Многие серверы Unix запускаются демоном

inetd
. Он осуществляет все необходимые шаги по превращению процесса в демон, и при запуске действительного сервера открывается сокет для стандартных потоков ввода, вывода и сообщений об ошибках. Это позволяет нам опустить вызовы функций
socket
,
bind
,
listen
и
accept
, поскольку все эти шаги выполняются демоном
inetd
.

Упражнения

1. Что произойдет в листинге 13.2, если мы отложим вызов функции

daemon_init
до завершения обработки аргументов командной строки и функция
err_quit
будет вызвана до того, как программа станет демоном?

2. Как вы думаете, какие из 10 серверов, перечисленных в табл. 2.1 (учитываются версии TCP и UDP для каждой из пяти служб, управляемых демоном

inetd
), реализуются с помощью вызова функции fork, а какие не требуют этой функции?

3. Что произойдет, если мы создадим сокет UDP, свяжем порт 7 с сокетом (стандартный эхо-сервер в табл. 2.1) и отправим дейтаграмму UDP-серверу

chargen
?

4. В руководстве Solaris 2.x для демона

inetd
описывается флаг
-t
, заставляющий демон
inetd
вызывать функцию
syslog
(с аргументами
facility=LOG_DAEMON
и
level=LOG_NOTICE
) для протоколирования клиентского IP-адреса и порта любой службы TCP, которые обрабатывает демон
inetd
. Как демон
inetd
получает эту информацию?

В этом же руководстве сказано, что демон

inetd
не может выполнить это для сокета UDP. Почему?

Есть ли способ обойти эти ограничения для служб UDP?

Глава 14