Интерфейс «гнезд»

Рассмотрев ранее такие средства межпроцессного взаимодействия (англ. inter-process communication, IPC), как сигналы, каналы (англ. pipes, FIFOs) и псевдотерминалы (англ. pseudo-ttys, ptys), мы переходим к т. н. гнездам Беркли (англ. Berkley sockets), используемых для организации обмена данными между процессами не только в рамках одной системы, но и выполняющимися на разных — как, например, через Internet.

История

Гнезда Беркли впервые были реализованы в 4.2BSD Unix () — варианте системы Unix компании AT&T, распространяемом Калифорнийским университетом в Беркли.

Впоследствии данный интерфейс, с некоторыми изменениями, вошел в стандарт IEEE Std 1003.1 (POSIX.1; см., например, описание заголовка sys/socket.h в тексте стандарта) и в настоящее время поддерживается практически всеми существующими системами общего назначения (например, Sockets // The GNU C Library Referene Manual.)

Безымянные гнезда; socketpair

Начнем с повторения основных особенностей каналов и псевдотерминалов как средств межпроцессного взаимодействия:

Гнезда Беркли, как и псевдотерминалы, двунаправлены, но в отличии от них не реализуют собственно интерфейс терминала.

Для изучения интерфейса воспользуемся программой Socat — название которой образовано от названия программы cat и англ. socket — гнездо.

Вспомним, что команда cat без аргументов копирует без каких-либо изменений данные со стандартного ввода на стандартный вывод — что делает ее (подчеркнем: при использовании без аргументов; или, нередко — с единственным «файловым» аргументом) сравнительно бесполезной в конвейерах, как показывает следующий пример.

Пример: «бесполезное» использование cat (англ. useless use of cat, UUoC) — на примере двух командных строк (с cat и без нее, соответственно), дающих одинаковый результат.
$ cat /etc/passwd | grep -F john 
$ grep -F john /etc/passwd 
$ 

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

Начнем с примера «бесполезного» в том смысле, что в данном случае ни гнезда в общем, ни socat в частности ни коим образом не необходимы.

Пример: взаимодействие с программой nl через гнезда Беркли.
$ socat  STDIO  EXEC:nl 
Первая строка ввода.

Третья строка ввода.
^D
     1  Первая строка ввода.
       
     2  Третья строка ввода.
$ 

Обратим внимание на то, что в отличие от использования nl непосредственно на терминале, нажатие клавиши Ввод (Enter) не приводит к немедленной обработке данных программой. (Иное поведение можно получить выбрав вариант командной строки выше, использующей псевдотерминал: socat STDIO EXEC:nl,pty,echo=0.)

Выполним теперь команду выше под отладчиком strace и разберем части результирующего вывода.

  1. socketpair(AF_UNIX, SOCK_STREAM, 0, [7, 8]) = 0
    

    Использование гнезд начинается с создания их связанной пары системным вызовом socketpair. Передача данных в файловый дескриптор 7 позволит получить их из связанного с ним файлового дескриптора 8 — и наоборот.

  2. clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f1939659190) = 20086
    

    Для выполнения программы nl создается дочерний процесс 20086, для чего используется системный вызов clone — являющийся, по-сути, более гибким аналогом ранее рассмотренного fork.

  3. close(8)                                = 0
    [pid 20086] close(7)                    = 0
    [pid 20086] dup2(8, 0)                  = 0
    [pid 20086] dup2(8, 1)                  = 1
    [pid 20086] close(8)                    = 0
    

    Дочерний процесс наследует все файловые дескрипторы родительского — включая оба гнезда созданной пары. Один из них, 7, закрывается; другой, 8, замещает дескрипторы 0, 1 (стандартный ввод и вывод, соответственно), после чего также закрывается.

    В родительском процессе, наоборот, закрывается «отданный» дочернему процессу 8.

  4. [pid 20086] execve("/usr/bin/nl", ["nl"], 0x55da5eb923f0 /* 38 vars */) = 0
    

    Программа дочернего процесса замещается исполнимым файлом программы nl. (До этого момента выполнение socat продолжалось как в родительском, так и в дочернем процессах.)

  5. read(0, "\320\237\320\265\321\200\320\262\320\260\321\217 \321\201\321\202\321\200\320\276\320\272\320\260 \320\262\320\262\320\276", 8192) = 77
    write(7, "\320\237\320\265\321\200\320\262\320\260\321\217 \321\201\321\202\321\200\320\276\320\272\320\260 \320\262\320\262\320\276", 77) = 77
    

    Программа socat (родительский процесс) читает стандартный ввод 0 и передает данные в гнездо 7.

  6. [pid 20086] read(0, "\320\237\320\265\321\200\320\262\320\260\321\217 \321\201\321\202\321\200\320\276\320\272\320\260 \320\262\320\262\320\276", 4096) = 77
    [pid 20086] fstat(1, {st_mode=S_IFSOCK|0777, st_size=0, ...}) = 0
    [pid 20086] read(0, "", 4096)           = 0
    [pid 20086] lseek(0, 0, SEEK_CUR)       = -1 ESPIPE (Illegal seek)
    [pid 20086] close(0)                    = 0
    [pid 20086] write(1, "     1\t\320\237\320\265\321\200\320\262\320\260\321\217 \321\201\321\202\321\200\320\276\320\272\320\260", 98 <unfinished >
    

    Программа nl читает стандартный ввод, обрабатывает данные и выводит результат на стандартный вывод. Напомним, что как стандартный ввод, так и стандартный вывод являются, в данном случае, гнездом, парным гнезду 7 родительского процесса.

  7. read(7, "     1\t\320\237\320\265\321\200\320\262\320\260\321\217 \321\201\321\202\321\200\320\276\320\272\320\260"..., 8192) = 98
    write(1, "     1\t\320\237\320\265\321\200\320\262\320\260\321\217 \321\201\321\202\321\200\320\276\320\272\320\260"..., 98) = 98
    

    Программа socat читает сформированный nl результат из гнезда 7 и выводит его без изменений на стандартный вывод (например — терминал пользователя.)

Наконец, можно попробовать прочитать вывод strace «в обратную сторону»:

  1. мы хотим передавать (read, write) данные между некоторым процессом и «ничего не подозревающей» программой nl;
  2. для этого мы «подменяем» (dup) стандартный ввод—вывод последней гнездом Беркли;
  3. гнездо, в свою очередь, является одним из созданной (socketpair) нами связанной пары.

Домашнее задание

Для данного задания остается в силе обычный регламент, однако для подтверждения «присутствия» желательно отправить отчет в течение двух—трех дней.

  1. (1 балл) Найдите в исходном коде какой-либо программы фрагмент, в котором используются функции гнезд Беркли.

    Обратите внимание, что в «сетевых» программах используются «непарные» гнезда, создаваемые функцией socket. Кроме того, появляется необходимость использовать такие функции, как установление соединения connect (или же ожидания и принятия соединения, listen, accept, в случае сервера), а равно функции привязки к адресу bind.

  2. (2 балла) Объясните работу найденного фрагмента, его значение для программы в целом.

При выполнении задания могут оказаться полезными статья Berkley sockets Википедии и следующие ресурсы Всемирной паутины:

http://git.savannah.gnu.org/cgit/
исходный код программ проекта GNU, в частности — комплекта «сетевых» программ GNU Inetutils;
http://sources.debian.org/
исходный код пакетов проекта Debian, в частности — apache2, HTTP-сервера Apache;
http://git.busybox.net/busybox/tree/
исходный код BusyBox, включающий сравнительно простые реализации ряда сетевых протоколов.