Разработка приложений в среде Linux. Второе издание

Джонсон Майкл К.

Троан Эрик В.

Часть III

Системное программирование

 

 

Глава 10

Модель процессов

 

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

 

10.1. Определение процесса

 

Что такое процесс? В исходной реализации Unix процессом была любая выполняющаяся программа. Для каждой программы ядро системы отслеживает перечисленные ниже аспекты.

• Текущая точка выполнения (такая как ожидание возврата системного вызова из ядра), часто называемая программным контекстом.

• К каким файлам имеет доступ программа.

• Сертификаты (credentials) программы (например, какой пользователь и группа владеют процессом).

• Текущий каталог программы.

• К какому пространству памяти имеет доступ программа и как оно распределено.

Процесс также является базовой единицей планирования для операционной системы. Только процессам разрешено выполняться в центральном процессоре.

 

10.1.1. Усложнение концепции — потоки

Хотя определение процесса может показаться очевидным, концепция потока (thread) делает все это несколько менее ясным. Поток позволяет единственной программе выполняться во многих местах одновременно. Все потоки, созданные одной программой, разделяют большинство характеристик, которые отличают процессы друг от друга. Например, множество потоков, порожденных от одной программы, разделяют информацию об открытых файлах, правах доступа, текущем каталоге и образе памяти. Как только один из потоков модифицирует глобальную переменную, все потоки увидят новое значение, а не только тот, что это сделал.

Многие реализации Unix (включая каноническую версию AT&T System V) были перепроектированы, чтобы сделать потоки фундаментальным элементом планирования для ядра, и процесс превратился в коллекцию потоков, разделяющих ресурсы. Поскольку множество ресурсов разделяется между потоками, ядро может быстрее переключаться между потоками одного процесса, чем оно это делает при полноконтекстном переключении между процессами. В результате в большинстве ядер Unix существует двухуровневая модель процессов, которая различает потоки и процессы.

 

10.1.2. Подход Linux

В Linux, однако, все идет другим путем. Переключение контекстов в Linux всегда было исключительно быстрым (примерно на том же уровне, как новые "переключатели потоков", представленные в двухуровневом подходе), что стимулировало разработчиков ядра вместо смены подхода к планированию процессов позволить процессам разделять ресурсы более либерально.

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

К счастью, разница между моделью процессов Linux и двухуровневым подходом проявляется редко. В настоящей книге мы используем термин процесс для обозначения набора из (обычно, одной) планируемых сущностей, которые разделяют основные ресурсы. Когда процесс состоит из одного потока, мы используем эти термины как взаимозаменяемые. Чтобы не усложнять, в большей части этой главы мы будем игнорировать потоки полностью. До ее завершения мы обсудим системный вызов clone(), который используется для создания потоков (и может также создавать нормальные процессы).

 

10.2 Атрибуты процессов

 

10.2.1. Идентификатор процесса и происхождение

Два из наиболее фундаментальных атрибутов — это идентификатор процесса (process ID), или pid, а также идентификатор его родительского процесса. Идентификатор pid — это положительное целое число, которое уникально идентифицирует работающий процесс и сохраняется в переменной типа pid_t. Когда создается новый процесс, исходный процесс, известный как родитель нового процесса, будет уведомляться, когда этот дочерний процесс будет завершен.

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

Если родитель процесса завершается (делая дочерний процесс висячим), такой процесс становится дочерним для начального процесса (init). Начальный процесс — это первый процесс, который запускается при загрузке машины и которому присваивается значение pid, равное 1. Одной из основных задач начального процесса является сбор кодов завершения процессов, чьи родители исчезли, позволяя ядру удалять такие дочерние процессы из таблицы процессов системы. Процессы могут получать свой pid и pid родителя с помощью функций getpid() и getppid().

pid_t getpid(void) Возвращает pid текущего процесса.
pid_t getppid(void) Возвращает pid родительского процесса.

 

10.2.2. Сертификаты

В Linux используются традиционные механизмы обеспечения безопасности Unix для пользователей и групп. Идентификаторы пользователя (uid) и группы (gid) — это целые числа, которые отображаются на символические имена пользователей и групп в файлах /etc/passwd и /etc/group, соответственно (более подробную информацию о базах данных пользователей и групп можно получить в главе 28). Однако ядро ничего не знает об именах — оно имеет дело только с целочисленными представлениями. Идентификатор uid, равный 0, зарезервирован за системным администратором, обычно имеющим имя root. Все обычные проверки безопасности отключаются для процессов, запущенных от имени root (то есть с uid, равным 0), что дает администратору полный контроль над системой.

В большинстве случаев процесс имеет единственный uid и единственный gid, ассоциированный с ним. Это идентификаторы, которые используются для большинства целей обеспечения безопасности (как, например, назначение прав владения вновь созданным файлам). Системные вызовы, которые могут модифицировать принадлежность процессов, обсуждаются далее в настоящей главе.

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

Дополнительные группы были представлены в BSD 4.3 для решения этой проблемы. Хотя каждый процесс по-прежнему имеет собственный первичный gid (который используется, например, как gid для вновь создаваемых файлов), он также связан с набором дополнительных групп. Проверки безопасности, которые используются для обеспечения того, что процесс относится к определенной группе (и только этой группе), теперь позволяет обеспечить доступ и в случае, когда данная группа является одной из дополнительных групп, к которым процесс относится. Макрос sysconf() по имени _SC_NGROUPS_МАХ специфицирует, к скольким дополнительным группам может относиться процесс. (Подробно о sysconf() см. главу 6.) В Linux 2.4 и более ранних версиях _SC_NGROUPS_MAX был равен 32. В Linux 2.6 и последующих версиях _SC_NGROUPS_MAX равен 65536. Не используйте статические массивы для хранения дополнительных групп. Вместо этого выделяйте память динамически, принимая во внимания значение, возвращаемое sysconf(_SC_NGROUPS_MAX). Старый код может пользоваться макросом NGROUPS_MAX для определения количества поддерживаемых групп, установленных в системе. Этот макрос не обеспечивает корректную работу, когда код компилируется в одной среде, а используется в другой.

Установка списка групп для процесса осуществляется системным вызовом setgroups() и может быть выполнена процессом, имеющим полномочия root.

int setgroups(size_t num, const gid_t * list);

Параметр list указывает на массив из num идентификаторов групп gid. Дополнительная группа процесса устанавливается этим списком идентификаторов групп, переданным в массиве list.

Функция getgroups() позволяет получить список дополнительных групп, установленных для процесса.

int getgroups(size_t num, gid_t * list);

list должен указывать на массив элементов типа gid_t, который наполняется идентификаторами дополнительной группы процесса, a num определяет, сколько элементов может типа gid_t содержать list. В случае ошибки системный вызов getgroups() возвращает -1 (обычно это происходит, когда list недостаточно велик, чтобы вместить дополнительный список групп процесса), или же количество дополнительных групп. В особом случае, когда num равно 0, getgroups() просто возвращает количество дополнительных групп процесса.

Ниже показан пример использования getgroups().

gid_t *groupList;

int numGroups;

numGroups = getgroups(0, groupList);

if (numGroups) {

 groupList = alloca(numGroups * sizeof(gid_t));

 getgroups(numGroups, groupList);

}

Более сложный пример getgroups() приведен в главе 28.

Таким образом, процесс имеет uid, первичный gid и набор дополнительных групп, ассоциированных с ним. К счастью, это все, о чем нужно знать большинству программистов. Существуют два класса программ, которым необходимо очень гибкое управление идентификаторами пользователей и групп — это программы setuid/setgid и системные демоны.

Системные демоны — это программы, которые всегда запущены в системе и выполняют определенные действия в ответ на внешние воздействия. Например, большинство демонов World Wide Web (http) функционируют всегда, ожидая подключения к ним клиента, чтобы обрабатывать клиентские запросы. Другие демоны, такие как cron (которые запускаются периодически), пребывают в спящем состоянии до тех пор, пока не наступает время, когда они должны выполнить какие-то действия. Большинство демонов должны быть запущены с полномочиями root, но выполняют действия по запросу пользователя, который может попытаться нарушить системную безопасность с помощью демонов.

ftp — хороший пример демона, который нуждается в гибком управлении uid. Изначально он запускается с правами root и затем переключает свой uid на uid пользователя, который подключился к нему (большинство систем запускают дополнительный процесс для обработки каждого ftp-запроса, поэтому такой подход работает достаточно хорошо). Это оставляет работу по проверке доступа к файлам ядру, к которому он относится. Однако в некоторых случаях демон ftp должен открывать сетевое подключение таким способом, который разрешен только root, поскольку пользовательские процессы не могут выдать сами себе административные полномочия (по вполне ясной причине), но сохранение идентификатора uid пользователя root вместо переключения на пользовательский uid должен потребовать от демона ftp самостоятельной проверки всего доступа к файловой системе. Решение этой дилеммы применяется симметрично — к обоим uid и первичным gid, поэтому мы и говорим здесь об uid.

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

Сохраненный и действительный идентификаторы uid проверяются только тогда, когда процесс пытается изменить его эффективный uid. Любой процесс может изменять свой эффективный uid на сохраненный или действительный. Только процессы с эффективным uid, равным 0 (процессы, запущенные от имени root), могут изменять свой эффективный uid на произвольное значение.

Обычно эффективный, реальный и действительный uid процесса совпадают. Однако этот механизм решает дилемму демона ftp. Когда он запускается, все его идентификаторы устанавливаются в 0, что предоставляет ему полномочия root. Когда подключается пользователь, демон устанавливает свой эффективный uid равным uid пользователя, оставляя сохраненный и действительный uid равными 0. Когда демону ftp требуется выполнить действие, разрешенное только root, он устанавливает свой эффективный uid в 0, выполняет действие, а затем переустанавливает эффективный uid в значение uid подключенного пользователя.

Хотя демон ftp вообще не нуждается в сохраненном uid, другие классы программ, применяющие этот механизм — двоичные модули setuid и setgid — используют его.

Программа passwd — это простой пример того, зачем нужна функциональность setuid и setgid. Программа passwd позволяет пользователям изменять свои пароли. Пользовательские пароли обычно хранятся в файле /etc/passwd. Выполнять запись в этот файл может только пользователь root, что предотвращает изменение информации о пользователях другими пользователями. Но пользователи должны иметь возможность изменять свои собственные пароли, поэтому необходим какой-то способ предоставить программе passwd права на изменение /etc/passwd.

Чтобы обеспечить эту гибкость, пользователь программы может устанавливать специальные биты в группе бит прав доступа этой программы (см. главу 11). Это сообщает ядру, что всякий раз, когда программа запускается, она должна выполняться с тем же эффективным uid (или gid), как у пользователя, который владеет файлом программы, независимо от того, какой пользователь запустил программу. Такие программы называются setuid- или setgid-программами.

Принадлежность программы passwd пользователю root и установка бита setuid в наборе битов доступа программы позволяют всем пользователям изменять свои пароли. Когда пользователь запускает passwd, эта программа выполняется с эффективным идентификатором пользователя 0, что позволяет ей модифицировать /etc/passwd и изменять пользовательский пароль. Конечно, passwd должна быть реализована очень тщательно, дабы исключить побочные эффекты. Программы setuid — это популярная цель для злоумышленников, проникающих в систему, поэтому плохо написанная программа подобного рода дает простую возможность получить неавторизованный доступ.

Существуют много случаев, когда программы setuid требуют специальных прав доступа на короткий период времени и должны переключаться на uid действительного пользователя в остальное время (как это делает демон ftp). Программы setuid имеют установленный эффективный uid равным uid ее владельца, но они также "знают" uid того пользователя, который их запустил (сохраненный uid), что упрощает обратное переключение. В дополнение они могут устанавливать свой действительный uid в значение setuid (не затрагивая сохраненный uid), при необходимости повторно получая эти специальные полномочия. В этой ситуации эффективный, сохраненный и действительный идентификаторы пользователя работают вместе, насколько возможно, упрощая систему безопасности.

К несчастью, применение этого механизма может сбивать с толку, поскольку в POSIX и BSD применяются слегка отличающиеся подходы, a Linux поддерживает оба. Решение BSD более полнофункционально, чем метод POSIX. Оно использует функцию setreuid().

int setreuid(uid_t ruid, uid_t euid);

Действительный uid процесса устанавливается в ruid, а эффективный — в euid. Если любой из параметров равен -1, идентификатор вызовом не затрагивается.

Если эффективный uid процесса равен 0, такой вызов всегда выполняется успешно. В противном случае идентификаторы могут быть установлены равными либо сохраненному uid, либо реальному uid процесса. Следует отметить, что этот вызов никогда не изменяет сохраненный uid или реальный uid текущего процесса. Чтобы сделать это, используйте функцию POSIX setuid(), которая может модифицировать сохраненный uid.

int setuid(uid_t euid);

Как и в случае setreuid() эффективный uid процесса устанавливается в euid, если euid равен действительному uid процесса либо эффективный uid процесса на момент вызова равен 0.

Когда setuid() используется процессом, чей эффективный uid установлен в 0, все uid процесса изменяются на euid. К сожалению, это делает невозможным использование setreuid() в setuid-программах, которым нужно временное использование другого uid, поскольку после вызова setreuid() процесс не может восстановить свои полномочия root.

Хотя способность переключать uid упрощает написание кода, с помощью которого нельзя нарушить безопасность системы, все же это не панацея. Существует очень много популярных методов обманного проникновения в выполняющийся код [18]. До тех пор пока либо сохраненный, либо действительный uid процесса равен 0, такие атаки легко могут устанавливать эффективный uid процесса в 0. Это не дает возможности переключению uid эффективно предотвращать серьезную уязвимость системных программ. Однако если процесс может передать любой доступ к полномочиям root, устанавливая эффективный, сохраненный и действительный идентификаторы в ненулевые значения, это ограничивает эффективность любых атак против него.

 

10.2.3. Идентификатор uid файловой системы

В очень специальных случаях программе может понадобиться сохранять свои права root для всего, кроме доступа к файловой системе, при котором она использует пользовательский uid. Изначально использовавшийся в Linux NFS-сервер пространства пользователя может служить иллюстрацией проблемы, которая возникает, когда процесс предполагает применение пользовательского uid. Хотя NFS-сервер в прошлом применял setreuid() для переключения uid при доступе к файловой системе, такое поведение позволяло пользователю, чей uid совпадает с uid NFS-сервера, уничтожать NFS-сервер. В конечном итоге, пользователь получал владение процессом NFS-сервера. Чтобы предотвратить проблемы подобного рода, Linux использует uid файловой системы (fsuid) для контроля доступа к файловой системе.

Всякий раз когда изменяется эффективный uid процесса, его fsuid устанавливается равным новому эффективному идентификатору пользователя, что делает fsuid прозрачным для большинства приложений. Те приложения, которые нуждаются в дополнительных возможностях, предоставляемых отличающимся значением fsuid, должны применять вызов setfsuid() для явной установки fsuid.

int setfsuid(uid_t uid);

Значение fsuid может быть установлено равным текущим эффективному, сохраненному или действительному идентификаторам пользователя. В дополнение следует сказать, что setfsuid() выполняется успешно, если fsuid остается неизменным или эффективный uid процесса равен 0.

 

10.2.4. Резюме по идентификаторам пользователей и групп

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

Все эти функции возвращают -1 в случае ошибки и 0 — в случае успеха, если только не указано иначе. Большинство их прототипов находятся в . Те, что расположены где-то еще, отмечены ниже.

int setreuid(uid_t ruid, uid_t euid); Устанавливает действительный uid текущего процесса в ruid и эффективный uid процесса в euid . Если оба параметра равны -1 , то uid остаются неизменными.
int setregid(gid_t rgid, gid_t egid); Устанавливает действительный gid текущего процесса в rgid и эффективный gid процесса в egid. Если оба параметра равны -1 , то gid остаются неизменными.
int setuid(uid t uid); Если применяется обычным пользователем, то устанавливает эффективный uid текущего процесса в значение параметра uid . Если используется процессом с эффективным uid, равным 0, то устанавливает действительный, эффективный и сохраненный uid в значение параметра uid .
int setgid(gid_t gid); Если применяется обычным пользователем, то устанавливает эффективный gid текущего процесса в значение параметра gid . Если используется процессом с эффективным gid, равным 0, то устанавливает действительный, эффективный и сохраненный gid в значение параметра gid .
int seteuid(uid_t uid); Эквивалент setreuid(-1, uid) .
int setegid(gid_t gid); Эквивалент setregid(-1, gid) .
int setfsuid(uid_t fsuid); Устанавливает fsuid текущего процесса в значение параметра fsuid . Прототип находится в <sys/fsuid.h> . Возвращает предшествующий fsuid.
int setfsgid(gid_t fsgid); Устанавливает fsgid текущего процесса в значение параметра fsgid . Прототип находится в <sys/fsuid.h> . Возвращает предшествующий fsgid.
int setgroups(size_t num, const gid_t * list); Устанавливает дополнительные группы текущего процесса из списка, переданного в массиве list , который должен содержать num элементов. Макрос SC_NGROUPS_MAX указывает, сколько групп может быть в списке (от 32 до 65536, в зависимости от работающей у вас версии Linux).
uid_t getuid(); Возвращает действительный uid процесса.
uid_t geteuid(); Возвращает эффективный uid процесса.
gid_t getgid(); Возвращает действительный gid процесса.
gid_t getegid(); Возвращает эффективный gid процесса.
size_t getgroups (size_t size, gid_t list[]); Возвращает текущий набор дополнительных групп процесса в массиве list . Параметр size сообщает, сколько элементов типа gid_t может содержать list . Если размер list недостаточен, чтобы вместить все группы, возвращается -1 , а errno устанавливается в EINVAL . В противном случае возвращается фактическое количество групп в list . Если size равен 0 , возвращается количество групп, но list не затрагивается. Прототип функции getgroups() находится в <grp.h> .

 

10.3. Информация о процессе

 

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

 

10.3.1. Аргументы программы

Есть два типа значений, передаваемых новым программам при их запуске: аргументы командной строки и переменные окружения. Для их использования установлено множество соглашений, но система сама по себе не придерживается их автоматически. Однако хорошим тоном считается придерживаться этих соглашений, чтобы помочь вашим программам попасть в мир Unix.

Аргументы командной строки — это набор строк, передаваемый программе. Обычно они представляют собой текст, набранный вслед за именем команды в оболочке, с необязательными аргументами, начинающимися с символа "минус" (-).

Переменные окружения — это набор пар "имя-значение". Каждая пара представляет отдельную строку в форме ИМЯ = ЗНАЧЕНИЕ , и набор таких строк образует окружение (environment) программы. Например, домашний каталог текущего пользователя обычно указан в переменной окружения HOME, поэтому программы, скажем, пользователя Joe часто запускаются, имея в своем окружении HOME=/home/joe.

И аргументы, и окружение становятся доступными программе при запуске. Аргументы командной строки передаются в виде параметров главной функции программы — main(), в то время как указатель на окружение помещается в глобальную переменную environ, которая определена в .

Ниже представлен полный прототип функции main() в мире Linux, Unix и языка ANSI/ISO С.

int main(int argc, char *argv[]);

Возможно, вас удивит, что main() возвращает значение (отличное от void). Это значение, возвращаемое функцией main(), передается родительскому процессу после завершения данного. По соглашению 0 означает, что процесс завершен успешно, а ненулевое значение означает возникновение сбоя. При этом принимаются во внимание только младшие 8 бит из этого кода возврата. Отрицательные значения от -1 до -128 зарезервированы для ненормального завершения процессов по инициативе другого процесса или ядра системы. Код выхода 0 сигнализирует об успешном завершении, а значения от 1 до 127 говорят о том, что программа завершена по ошибке.

Первый параметр, argc, содержит количество аргументов командной строки, переданных программе, тогда как argv — массив указателей на строки — хранит сами аргументы. Первый элемент в массиве, argv[0], содержит имя вызванной программы (хотя и не обязательно полный путь к ней). В элементе argv[argc-1] расположен указатель на завершающий аргумент командной строки, а argv[argc] содержит NULL.

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

extern char *environ[];

Это представляет environ как массив указателей на каждый элемент программного окружения (помните, каждый элемент — это пара ИМЯ = ЗНАЧЕНИЕ ), и финальный элемент массива содержит NULL. Это объявление находится в , поэтому вам не обязательно объявлять его самостоятельно.

Наиболее общий способ проверки элементов окружения — это вызов getenv, который исключает непосредственное обращение к переменной environ.

const char *getenv(const char * name);

Единственный параметр getenv() — это имя переменной окружения, значение которой интересует. Если переменная существует, getenv() вернет указатель на ее значение. Если переменная не существует в текущем окружении (то есть окружении, на которое указывает environ), функция вернет NULL.

Linux предоставляет два способа добавления строк в программное окружение: setenv() и putenv(). POSIX определяет только putenv(), что делает его более переносимым.

int putenv(const char * string);

Переданный функции параметр string должен иметь форму ИМЯ = ЗНАЧЕНИЕ . putenv() добавляет переменную по имени ИМЯ к текущему окружению и присваивает ей значение ЗНАЧЕНИЕ . Если окружение уже содержит переменную ИМЯ , ее значение изменяется на ЗНАЧЕНИЕ .

BSD определяет функцию setenv(), которую Linux также поддерживает. Это более гибкий и удобный способ добавления переменных к окружению.

int setenv(const char * name, const char * value, int overwrite);

Здесь имя и новое значение переменной окружения передаются раздельно, что обычно программам делать проще. Если overwrite равно 0, окружение не модифицируется, если оно уже содержит переменную по имени name. В противном случае значение переменной модифицируется, как и в putenv().

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

putenv("PATH=/bin:/usr/bin");

setenv("PATH","/bin:/usr/bin", 1);

 

10.3.2 Использование ресурсов

Ядро Linux отслеживает, сколько ресурсов использует каждый процесс. Хотя отслеживается только небольшое их число, их измерения могут быть полезными разработчикам, администраторам и пользователям. В табл. 10.1 перечислены ресурсы, использование которых отслеживается ядром Linux версии 2.6.7.

Таблица 10.1. Ресурсы процессов, отслеживаемые Linux

Тип Член Описание
struct timeval ru_utime Общее время, затраченное на выполнение кода в режиме пользователя. Это включает в себя все время, потраченное на выполнение инструкций приложения, но исключая время, потраченное ядром на обслуживание запросов приложения.
struct timeval ru_stime Общее время, потраченное ядром на выполнение запросов процесса. Это не включает времени блокировки процесса в период ожидания выполнения системных вызовов.
long ru_minflt Количество второстепенных сбоев (minor faults), вызванных данным процессом. Второстепенные сбои — это попытки доступа к памяти, переключающие процессор в режим ядра, но не вызывающих обращений к диску. Это случается, когда процесс пытается писать за пределами стека, что вынуждает ядро распределить больше пространства стека, прежде чем продолжить выполнение процесса.
long ru_majflt Количество первостепенных сбоев (major faults), вызванных данным процессом. Первостепенные сбои — это обращения к памяти, заставляющие ядро обратиться к диску, прежде чем программа сможет продолжить работу. Одной из частых причин этого может быть обращение к части исполняемой памяти, которая еще не была загружена в ОЗУ с диска либо была временно выгружена на диск.
long ru_nswap Количество страниц памяти, для которых был выполнен обмен с диском при обращении к памяти из процесса.

Процесс может проверять использование ресурсов им самим, общее использование ресурсов его дочерними процессами либо сумму того и другого.

Системный вызов getrusage() возвращает структуру struct rusage (определенную в ), содержащую информацию о текущем использовании ресурсов.

int getrusage(int who, struct rusage * usage);

Первый параметр, who, сообщает, какой из трех счетчиков ресурсов должен быть возвращен. RUSAGE_SELF возвращает использование ресурсов текущим процессом, RUSAGE_CHILDREN — его дочерними процессами, a RUSAGE_BOTH — общее использование ресурсов текущим процессом и всеми его дочерними процессами. Второй параметр getrusage() — это указатель на struct rusage, куда помещается информация об использовании ресурсов. Хотя struct rusage и содержит относительно немного членов (список унаследован из BSD), большинство этих членов пока не используются Linux). Ниже представлено полное определение этой структуры. В табл. 10.1 описаны члены, используемые в настоящее время Linux.

#include

struct rusage {

 struct timeval ru_utime;

 struct timeval ru_stime;

 long intru_maxrss;

 long intru_ixrss;

 long intru_idrss;

 long intru_isrss;

 long intru_minflt;

 long intru_majfit;

 long intru_nswap;

 long intru_inblock;

 long intru_oublock;

 long intru_msgsnd;

 long intru_msgrcv;

 long intru_nsignals;

 long intru_nvcsw;

 long intru_nivcsw;

};

 

10.3.3. Применение ограничений использования ресурсов

Чтобы помочь в предотвращении неконтролируемого снижения производительности процессами, Unix отслеживает многие ресурсы, которые может использовать процесс, и позволяет системному администратору и самим пользователям накладывать ограничения на расход ресурсов процессами.

Предусмотрены два класса доступных ограничений: жесткие и мягкие ограничения. Жесткие обычно установлены при запуске системы в RLIM_INFINITY, что означает отсутствие каких-либо ограничений. Единственное исключение из этого — RLIMIT_CORE (максимальный размер дампа памяти), который Linux инициирует нулем, чтобы предотвратить неожиданный сброс дампов ядра. Многие дистрибутивы сбрасывают этот лимит при запуске, однако, большинство технических пользователей ожидают появления дампов памяти при некоторых условиях (информацию о дампах памяти можно найти далее в главе). Мягкие ограничения — это те ограничения, которые установлены в ядре в данный момент. Любой процесс может наложить мягкое ограничение на использование ресурса на определенном уровне — равном или более низком, чем установленное жесткое ограничение.

Таблица 10.2. Ограничения ресурсов

Значение Лимит
RLIMIT_AS Максимальный объем памяти, доступный процессу. Включает память для стека, глобальных переменных и динамически выделенную память.
RLIMIT_CORE Максимальный размер дампа памяти, генерируемого ядром (если файл дампа получается слишком большим, он не создается).
RLIMIT_CPU Общее используемое время процессора (в секундах). Более подробно об этом ограничении рассказывается при описании SIGXCPU в главе 12.
RLIMIT_DATA Максимальный объем памяти данных (в байтах). Это не включает динамически выделенную память.
RLIMIT_FSIZE Максимальный размер открытого файла (проверяется при записи). Более подробно об этом ограничении рассказывается при описании SIGXFSZ в главе 12.
RLIMIT_MEMLOCK Максимальный объем памяти, которая может быть блокирована с помощью mlock() . Функция mlock() рассматривается в главе 13.
RLIMIT_NOFILE Максимальное количество открытых файлов.
RLIMIT_NPROC Максимальное количество дочерних процессов, которые может породить данный процесс. Это ограничивает только количество дочерних процессов, которые могут существовать одновременно. Это не ограничивает количества наследников дочерних процессов — каждый из них может иметь до RLIMIT_NPROC потомков.
RLIMIT_RSS Максимальный объем ОЗУ, использованный в любой момент (всякое превышение этого объема используемой памяти вызывает страничную подкачку). Это также известно под названием размера резидентной части (resident set size).
RLIMIT_STACK Максимальный размер памяти стека (в байтах), включая все локальные переменные.

Различные ограничения, которые могут быть установлены, перечислены в табл. 10.2 и определены в . Системные вызовы getrlimit() и setrlimit() устанавливают и получают ограничения для отдельного ресурса.

int getrlimit(int resource, struct rlimit *rlim);

int setrlimit(int resource, const struct rlimit *rlim);

Обе эти функции используют структуру struct rlimit, определенную следующим образом:

struct rlimit {

 long int rlim_cur; /* мягкое ограничение */

 long int rlim_max; /* жесткое ограничение */

};

Второй член структуры — rlim_max, указывает жесткое ограничение лимита, переданного в параметре resource, a rlim_cur — мягкое ограничение. Это те же наборы лимитов, которыми манипулируют команды ulimit и limit, одна из которых встроена в большинство командных оболочек.

 

10.4. Примитивы процессов

 

Несмотря на относительно длинную дискуссию, необходимую для описания процесса, создание и уничтожение процессов в Linux достаточно просто.

 

10.4.1. Создание дочерних процессов

В Linux предусмотрены два системных вызова, которые создают новые процессы: fork() и clone(). Как упоминалось ранее, clone() используется для создания потоков, и этот вызов будет кратко описан далее. А сейчас мы сосредоточимся на fork() — наиболее популярном методе создания процессов.

#include

pid_t fork(void);

Этот системный вызов имеет уникальное свойство возвращать управление не один раз, а дважды: один раз в родительском процессе и другой — в дочернем. Обратите внимание, что мы не говорим "первый — в родительском" — написание кода, который делает какие-то предположения относительно предопределенного порядка — очень плохая идея.

Каждый из двух возвратов системного вызова fork() имеет разные значения. В родительский процесс этот системный вызов возвращает pid вновь созданного дочернего процесса, а в дочернем он возвращает 0.

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

#include

#include

#include

int main(void) {

 pid_t child;

 if (!(child = fork())) {

  printf("в дочернем\n");

  exit (0);

 }

 printf("в родительском - дочерний: %d\n", child);

 return 0;

}

 

10.4.2. Наблюдение за уничтожением дочерних процессов

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

pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);

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

pid < -1 Ожидать завершения любого дочернего процесса, чей pgid равен абсолютному значению pid.

pid = -1 Ожидать прерывания любого дочернего процесса.

pid = 0 Ожидать завершения дочернего из той же группы процессов, что и текущий.

pid > 0 Ожидать выхода процесса pid.

Второй параметр — это указатель на целое, которое устанавливается в значение, равное соду возврата того процесса, который заставляет wait4() вернуть управление (мы будем зазывать его "проверяемым" процессом). Формат возвращенного состояния довольно закрученный, и для того, чтобы сделать его осмысленным, существует набор макросов.

Три события заставляют wait4() вернуть состояние проверяемого процесса. Процесс может завершиться, он может быть прерван вызовом kill() (получит фатальный сигнал) либо он может быть остановлен по какой-либо причине. Вы можете узнать, что именно случилось, с помощью описанных ниже макросов, каждый из которых принимает возвращаемое состояние wait4() в качестве единственного параметра.

WIFEXITED(status) Возвращает true , если процесс завершился нормально. Процесс завершается нормально, когда его функция main() выходит из программы посредством вызова exit() . Если WIFEXITED истинно, то WEXITSTATUS(status) возвращает код возврата процесса.
WIFSIGNALED(status) Возвращает true , если процесс был прерван сигналом (это происходит, когда он прерывается вызовом kill() ). В этом случае WTERMSIG(status) возвращает номер сигнала, прервавшего процесс.
WIFSTOPPED(status) Если процесс приостановлен сигналом, WIFSTOPPED() возвращает true , a WSTOPSIG(status) возвращает номер сигнала, приостановившего процесс. wait4() возвращает информацию только о приостановленных процессах, если указана опция WUNTRACED .

Аргумент options управляет поведением вызова. WHOHANG заставляет функцию немедленно вернуть управление. Если в данный момент нет ни одного процесса, готового сообщить свое состояние, то возвращается 0 вместо допустимого pid. WUNTRACED заставляет wait4() возвратить соответствующий остановленный дочерний процесс. Более подробно о приостановленных процессах рассказывается в главе 15. Оба флажка могут быть объединены вместе битовой операцией "или".

Финальный параметр wait4(), указатель на struct rusage, наполняется информацией об использовании ресурсов проверяемым процессом и всеми его потомками. Более подробная информация об этом давалась при обсуждении getrusage() и RUSAGE_BOTH ранее в главе. Если этот параметр равен NULL, информация о состоянии не возвращается.

Существуют три других интерфейса к wait4(), каждый из которых представляет подмножество его функциональности.

pid_t wait(int *status) Единственный параметр wait() — это указатель на место, куда следует поместить код возврата прерванного процесса. Эта функция всегда блокирует выполнение до тех пор, пока дочерний процесс не будет прерван.
pid_t waitpid (pid_t pid, int *status, int options) Функция waitpid() подобна wait4() . Единственное отличие в том, что она не возвращает информации об использовании ресурсов прерванным процессом.
pid_t wait3(int *status, int options, struct rusage *rusage) Эта функция также подобна wait4() , но не позволяет специфицировать дочерний процесс, который должен быть проверен.

 

10.4.3. Запуск новых программ

Хотя доступно целых шесть способов запустить одну программу из другой, все они делают почти одно и то же — заменяют текущую выполняющуюся программу другой программой. Обратите внимание на слово "заменяет" — все следы текущей выполняющейся программы при этом исчезают. Если вы хотите оставить исходную программу работающей, вы должны создать новый процесс вызовом fork(), а затем запустить новую программу из дочернего процесса.

Эти шесть функций лишь слегка отличаются по интерфейсу. Только одна из них — execve() — является системным вызовом Linux. Остальные реализованы в библиотеках пользовательского пространства и вызывают execve() для запуска новой программы. Ниже представлены прототипы семейства функций exec().

int execl(const char *path, const char *arg0, ...);

int execlp(const char *file, const char *arg0, ...);

int execle(const char *path, const char *arg0, ...);

int execv(const char *path, const char **argv);

int execvp(const char *file, const char **argv);

int execve(const char *file, const char **argv, const char **envp);

Как уже упоминалось, все эти программы пытаются заменить текущую программу новой. Если это удается, то управление не возвращается (то есть программа, которая вызвала другую программу, уже не выполняется). Если не удается, то возвращается значение -1 и устанавливается код ошибки в errno, как при любом другом системном вызове. Когда новая программа запускается, она принимает массив аргументов (argv) и массив переменных окружения (envp). Каждый элемент envp имеет форму ПЕРЕМЕННАЯ = значение .

Основная разница между функциями семейства exec() состоит в том, как новой программе передаются аргументы командной строки. Функции execl() передают каждый элемент в отдельном аргументе argv, причем список завершается NULL. Традиционно первый элемент argv — это команда, использованная для запуска программы. Например, команда оболочки /bin/cat /etc/passwd /etc/group обычно получается в результате следующей вызова exec:

execl("/bin/cat", "/bin/cat", "/etc/passwd", "/etc/group", NULL);

Первый аргумент — это полный путь к программе, которую требуется выполнить, а остальные аргументы передаются программе в виде argv. Заключительный параметр execl() должен быть равен NULL — это служит признаком конца списка параметров. Если вы пропустите NULL, то, скорее всего, функция завершится ошибкой сегментации либо вернет EINVAL. Окружение, переданное новой программе — это то, на что указывает глобальная переменная environ, как упоминалось ранее в настоящей главе.

Функциям execv аргументы командной строки передаются как массив С строк, имеющих тот же формат, что применяется для передачи argv новой программе.

Последним элементом должен быть NULL для обозначения конца массива, а первый элемент (argv[0]) должен содержать имя вызываемой программы.

Наш пример с bin/cat /etc/passwd /etc/group может быть закодирован, используя execv, следующим образом:

char *argv[] = { "/bin/cat", "/bin/cat", "/etc/passwd", "/etc/group", NULL }; execv("/bin/cat", argv);

Если нужно передать специфическое окружение новой программе, для этого подойдут execle() и execve(). Они в точности похожи на execl() и execv(), но принимают указатель на окружение в качестве последнего аргумента. Окружение устанавливается так же, как argv.

Например, ниже показан один способ запуска /usr/bin/env (эта программа печатает окружение, которое ей передано) с небольшим набором переменных окружения:

char *newenv[] = { "PATH=/bin:/usr/bin", "HOME=/home/sweethome", NULL };

execle("/usr/bin/env", "/usr/bin/env", NULL, newenv);

Вот та же идея, реализованная с помощью execve():

char *argv[] = { "/usr/bin/env", NULL };

char *newenv[] = { "PATH=/bin:/usr/bin", "HOME=/home/sweethome", NULL };

execve("/usr/bin/env", argv, newenv);

Последние две функции, execlp() и execvp(), отличаются от первых двух тем, что выполняют поиск программы, которую нужно запустить, в текущем пути (установленном переменной окружения PATH). Аргументы программы, однако, не модифицируются, поэтому argv[0] не содержит полного пути к запускаемой программе. Ниже показана модифицированная версия нашего первого примера, который ищет cat в текущем PATH.

execlp("cat", "cat", "/etc/passwd", "/etc/group", NULL);

char *argv[] = { "cat", "/etc/passwd", "/etc/group", NULL };

execvp("cat", argv);

Если вместо этого воспользоваться execl() или execv(), этот фрагмент кода завершится ошибкой, если только cat не окажется в текущем каталоге.

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

Обработчики сигналов предохраняются внутри функций exec() несколько неочевидным образом. Этот механизм рассматривается в главе 12.

 

10.4.4. Ускоренное создание процессов с помощью vfork()

Обычно процессы, в которых вызывается fork(), немедленно вызывают exec() для другой программы (это то, что оболочка делает всякий раз, когда вы вводите команду), что делает полную семантику fork() более расточительной по вычислительным ресурсам, чем это необходимо.

Чтобы оптимизировать этот общий случай, существует vfork().

#include

pid_t vfork(void);

Вместо создания совершенно новой среды выполнения для нового процесса vfork() создает новый процесс, который разделяет память с исходным процессом. Ожидается, что новый процесс запустит другой процесс посредством exit() или exec() очень быстро, но его поведение непредсказуемо, если он модифицирует память, возвратит управление из функции vfork(), содержащейся в нем, либо вызовет любую новую функцию. В дополнение к этому исходный процесс приостанавливается, до тех пор, пока новый либо не будет прерван, либо вызовет функцию exec(). Однако не все системы обеспечивают семантику разделения памяти и приостановки родительского процесса vfork(), поэтому приложения не должны полагаться на такое поведение.

 

10.4.5. Уничтожение процессом самого себя

Процессы прерывают себя вызовом либо exit(), либо _exit(). Когда функция процесса main() возвращает управление, стандартная библиотека С вызывает exit() со значением, возвращаемым main() в качестве параметра.

void exit(int exitCode);

void _exit(int exitCode);

Две формы, exit() и _exit(), отличаются тем, что exit() — функция из библиотеки С, a _exit() — системный вызов. Системный вызов _exit() прерывает программу немедленно, и exitCode сохраняется в качестве кода возврата процесса. Когда используется exit(), то перед тем, как запустить системный вызов _exit(exitCode), вызываются функции, зарегистрированные в atexit(). Помимо всего прочего, это позволяет стандартной библиотеке ввода-вывода ANSI/ISO сбросить все свои буферы.

Регистрация функций, которые должны быть запущены при вызове exit(), выполняется с помощью функции atexit():

int atexit(void (*function) (void));

Единственный параметр, переданный atexit() — это указатель на функцию. Когда вызывается exit(), все функции, зарегистрированные через atexit(), вызываются в порядке, обратном тому, в котором они регистрировались. Следует отметить, что если используется _exit() либо процесс прерывается сигналом (подробно о сигналах читайте в главе 12), то функции, зарегистрированные atexit(), не вызываются.

 

10.4.6. Уничтожение других процессов

Разрушение другого процесса почти столь же просто, как создание нового — нужно просто уничтожить его:

int kill(pid_t pid, int signum);

pid должен быть идентификатором процесса, который требуется уничтожить, а signum описывает, как это нужно сделать. Доступны два варианта выполнения операции прерывания дочернего процесса. Вы можете применить SIGTERM, чтобы прервать его "вежливо". Это означает, что процесс при этом может сообщить ядру о том, что кто-то пытается его уничтожить; в результате появляется возможность завершить его корректно (сохранив файлы, например). Процесс может в этом случае игнорировать запрос на прерывание такого типа и продолжать выполняться. Применение значения SIGKILL в качестве параметра signum вызывает немедленное прерывание процесса без каких-либо вопросов. Если signum равно 0, то kill() проверяет, имеет ли тот процесс, что вызвал kill(), соответствующие полномочия, возвращает ноль, если это так, либо ненулевое значение, если полномочий недостаточно. Это обеспечивает процессу возможность проверки корректности pid.

Параметр pid в среде Linux может принимать перечисленные ниже значения.

pid > 0 Сигнал отправляется процессу с идентификатором pid . Если такого процесса нет, возвращается ESRCH .
pid < -1 Сигнал посылается всем процессам, принадлежащим группе с pgid, равным -pid . Например, kill(-5316, SIGKILL) немедленно прерывает все процессы из группы 5316. Такая возможность используется оболочками управления заданиями, как описано в главе 15.
pid = 0 Сигнал отправляется всем процессам группы, к которой относится текущий процесс.
pid = -1 Сигнал посылается всем процессам системы за исключением инициализирующего процесса (init). Это применяется для полного завершения системы.

Процессы могут нормально уничтожать вызовом kill() только те процессы, которые разделяют тот же эффективный идентификатор пользователя, что и у них самих. Существуют два исключения из этого правила. Во-первых, процессы с эффективным uid, равным 0, могут уничтожать любые процессы в системе. Во-вторых, любой процесс может посылать сигнал SIGCONT любому процессу в том же сеансе.

 

10.4.7. Дамп ядра

Хотя мы уже упоминали, что передача SIGTERM и SIGKILL функции kill() прерывает процесс, вы также можете использовать несколько других значений (все они описаны в главе 12). Некоторые из них, такие как SIGABRT, заставляют программу перед уничтожением сбрасывать дамп ядра (dump core).

Дамп ядра программы содержит полную хронологию состояния программы перед ее уничтожением. Большинство отладчиков, включая gdb, могут анализировать файл дампа и рассказывать, что программа делала непосредственно перед тем, как была уничтожена, а также поможет исследовать образ памяти процесса. Дамп ядра выгружается в файл по имени core, расположенный в текущем каталоге процесса.

Когда процесс нарушает какие-то системные требования (например, пытается обратиться к памяти, доступ к которой запрещен), ядро прерывает процесс, вызывая встроенную версию kill() с параметром, который заставляет выгрузить дамп ядра. Ядро может уничтожать процессы по разным причинам, включая арифметические ошибки, такие как деление на ноль, либо по причине выполнения программой некорректных инструкций, либо при попытке доступа к запрещенной области памяти. Последняя причина вызывает ошибку сегментации, что выражается в сообщении segmentation fault (core dumped) (ошибка сегментации (дамп ядра сброшен)). Если вы обладаете хоть каким-нибудь опытом программирования в Linux, то наверняка неоднократно получали это сообщение.

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

 

10.5. Простые дочерние процессы

 

Хотя функции fork(), exec() и wait() позволяют программам в полной мере использовать модель процессов Linux, многим приложениям не нужен такой контроль дочерних процессов. Существуют две библиотечных функции, которые упрощают создание дочерних процессов: system() и popen().

 

10.5.1. Запуск и ожидание с помощью

system()

Программам часто требуется запускать другие программы и ожидать их завершения, прежде чем продолжать свою работу. Функция system() позволяет это делать достаточно просто.

int system (const char* cmd);

system() порождает дочерний процесс, который выполняет exec() для /bin/sh, который, в свою очередь, запускает cmd. Исходный процесс ожидает завершения дочерней оболочки и возвращает тот же код, что wait(). Если вам не нужно оставлять в памяти оболочку (что случается редко), cmd должна включать предшествующее слово "exec", которое заставляет оболочку вызывать exec() вместо запуска cmd как подпроцесса.

Поскольку cmd запускается из оболочки /bin/sh, то здесь применимы все обычные правила расширения команд. Ниже показан пример вызова system(), который отображает исходные тексты С из текущего каталога.

#include

#include

int main() {

int result;

result = system("exec ls *.c");

if (!WIFEXITED(result))

 printf("(аварийный выход)\n");

 exit(0);

}

Команда system() должна применяться с большой осторожностью в программах, которые запускаются со специальными полномочиями. Поскольку системная оболочка предоставляет множество мощных средств и сильно зависит от переменных окружения, system() является уязвимым местом в плане безопасности, которым могут воспользоваться злоумышленники для проникновения в систему. Однако до тех пор, пока приложение не является демоном или программой setuid/setgid, вызов system() совершенно безопасен.

 

10.5.2. Чтение и запись из процесса

Хотя system() отображает результат работы команды на устройство стандартного вывода и позволяет дочерним программам читать стандартный ввод, это не всегда идеально. Часто процесс желает читать вывод другого процесса либо отправлять текст на стандартный ввод. popen() облегчает процессам решение этой задачи.

FILE * popen(const char *cmd, const char *mode);

cmd выполняется через оболочку, как и в system(). Параметр mode должен быть "r", если родительский процесс желает читать командный вывод, и "w" — для записи в стандартный ввод дочернего процесса. Следует отметить, что с помощью popen() делать одновременно чтение и запись нельзя.

Два процесса, которые читают и пишут друг в друга, достаточно сложны и выходят за рамки возможностей popen().

popen() возвращает FILE* (как это определено в стандартной библиотеке ввода-вывода ANSI/ISO), который может быть прочитан и записан подобно любому другому потоку stdio, либо NULL, если операция не удается. Когда завершается родительский процесс, он может воспользоваться pclose() для закрытия потока и прерывания дочернего процесса, если он все еще выполняется. Подобно system(), pclose() возвращает состояние дочернего процесса из wait4().

int pclose(FILE *stream);

Ниже приведен пример простой программы-калькулятора, которая использует программу bc для выполнения всей реальной работы. Важно сбрасывать поток, полученный от popen(), после записи в него, чтобы предотвратить буферизацию stdio от задержки вывода (подробности о буферизации стандартных функций библиотеки stdio можно найти в [15]).

 1: /*calc.c*/

 2:

 3: /* Это очень простой калькулятор, который использует внешнюю команду bc

 4:    для выполнения всей работы. Открывает канал к bc, читает команду,

 5:    передает ее bc и завершается. */

 6: #include

 7: #include

 8: #include

 9:

10: int main(void) {

11:  char buf[1024];

12:  FILE *bc;

13:  int result;

14:

15:  /* открыть канал на bc и выйти в случае неудачи */

16:  bc = popen("bc", "w");

17:  if (!bc) {

18:   perror("popen");

19:   return 1;

20:  }

21:

22:  /* пригласить ввести выражение, и прочитать его */

23:  printf("expr:"); fflush(stdout);

24:  fgets(buf, sizeof(buf), stdin);

25:

26:  /* послать выражение bc для вычисления */

27:  fprintf(bc, "%s\n", buf);

28:  fflush(bc);

29:

30:  /* закрыть канал на bc и ожидать выхода из нее */

31:  result = pclose(bc);

32:

33:  if (!WIFEXITED(result))

34:   printf("(аварийный выход)\n");

35:

36:  return 0;

37: }

Подобно system(), popen() запускает команды через системную оболочку и должна использоваться с большой осторожностью, если вызывается из программы со специальными полномочиями.

 

10.6. Сеансы и группы процессов

 

В Linux, как и в других системах Unix, пользователи обычно взаимодействуют с группами взаимосвязанных процессов. Хотя изначально они входят через единственный терминал и используют единственный процесс (а именно — оболочку, предоставляющую интерфейс командной строки), пользователи затем запускают множество процессов в результате перечисленных ниже действий.

• Запуск неинтерактивных заданий в фоновом режиме.

• Переключение между интерактивными заданиями с помощью управления заданиями (job control), которое более подробно обсуждается в главе 15.

• Запуск множества процессов, взаимодействующих через программные каналы.

• Запуск оконной системы, вроде X Window System, которая позволяет открывать несколько терминальных окон.

Чтобы управлять всеми этими процессами, ядру необходимо группировать процессы более сложным образом, чем простое отношение "родительский-дочерний", которое мы описали. Этот способ группировки называется сеансами и группами процессов. На рис. 10.1 показано отношение между сеансами, группами процессов и процессами.

Рис. 10.1. Сеансы, группы процессов и процессы

 

10.6.1. Сеансы

Когда пользователь выходит из системы, ядро должно прервать все процессы, которые пользователь запустил (иначе может остаться множество процессов, которые будут ожидать ввода, а тот никогда не последует). Чтобы упростить эту задачу, процессы организуются в наборы сеансов. Идентификатор сеанса — это то же, что pid процесса, который создает сеанс с помощью системного вызова setsid(). Этот процесс называют лидером сеанса (session leader) для данной группы процессов. Все потомки процесса являются членами сеанса, если только явно не будут удалены из него. Вызов функции setsid() не принимает аргументов, а возвращает идентификатор нового сеанса.

#include

pid_t setsid(void);

 

10.6.2. Управление терминалом

Каждый сеанс привязывается к терминалу, от которого процессы и сеансы получают ввод и куда отправляют свой вывод. Терминал может быть локальной консолью машины, терминальным подключением через последовательный порт или псевдотерминалом, который отображается на окно X либо на сетевое подключение (см. главу 16). Терминал, к которому относится сеанс, называется управляющим терминалом (или управляющим tty) данного сеанса. Терминал может быть управляющим одновременно только для одного сеанса.

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

 

10.6.3. Группы процессов

Одной из главных целей Unix было создание набора простых инструментов, которые могут быть использованы вместе сложными способами (с помощью механизмов, подобных программным каналам). Большинство пользователей Linux делали нечто вроде следующего практического примера этой философии:

ls | grep "^[аА].*\.gz" | more

Другое популярное средство, появившееся в Unix достаточно давно — управление заданиями (job control). Управление заданиями дает возможность пользователям прерывать текущее задание (известное как задание переднего плана (foreground task)) в то время, пока они уходят и делают на терминале что-то другое. Когда приостановленное задание представляет собой последовательность процессов, работающих вместе, система должна отслеживать, какие именно процессы должны быть приостановлены, когда пользователь желает приостановить задание переднего плана. Группы процессов позволяют системе видеть, какие процессы работают вместе, а потому должны управляться совместно средствами управления заданиями.

Процессы добавляются в группы с помощью setpgid().

int setpgid(pid_t pid, pid_t pgid);

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

Правила применения setpgid() несколько сложны.

1. Процесс может устанавливать группу для себя или одного из своих потомков. Он не может изменять группу для любого другого процесса в системе, даже если процесс, вызвавший setpgid(), имеет административные полномочия.

2. Лидер сеанса не может изменить свою группу.

3. Процесс не может быть перемещен в группу, чей лидер представляет другой сеанс, чем он сам. Другими словами, все процессы в группе должны относиться к одному и тому же сеансу.

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

Полный пример групп процессов будет приведен при обсуждении системы управления заданиями в главе 15.

Когда соединение с терминалом теряется, ядро посылает сигнал (SIGHUP; подробнее о сигналах рассказывается в главе 12) лидеру сеанса, содержащему группу процессов переднего плана данного терминала. Обычно это командная оболочка. Это позволит оболочке безусловно прерывать пользовательские процессы, извещая их о том, что пользователь выходит из системы (обычно посредством SIGHUP), либо выполнить некоторые другие действия (или бездействие). Хотя это все может показаться усложненным, это дает возможность лидеру группы сеанса принимать решения о том, как управлять закрывающимися терминалами, вместо того, чтобы возлагать эту обязанность на ядро. Это также дает возможность администраторам гибко управлять политиками пользовательских учетных записей.

Определение группы процесса может быть выполнено просто, с помощью функций getpgid() и getpgrp().

pid_t getpgid(pid_t pid) Возвращает pgid процесса pid . Если pid равен 0 , возвращается pgid текущего процесса. Для вызова не требуется никаких специальных полномочий. Любой процесс может определять группу, к которой принадлежит любой другой процесс.
pid_t getpgrp(void) Возвращает pgid текущего процесса pid (эквивалентно getprgid(0) )

 

10.6.4. Висячие группы процессов

Механизм прерывания процессов (либо возобновления их работы после приостановки) при исчезновении их сеанса довольно сложен. Представьте себе сеанс со многими группами процессов в нем (см. рис. 10.1). Сеанс запущен на терминале, и обычная системная оболочка является его лидером.

Когда лидер сеанса (оболочка) завершается, ее группы процессов оказываются в сложной ситуации. Если они активно работают, то лишаются возможности использовать стандартные потоки stdin и stdout, поскольку терминал закрыт. Если они приостановлены, то вероятно, никогда не будут запущены снова, поскольку пользователь терминала не имеет возможности перезапустить их, к тому же то, что они не могут быть запущены, означает также, что они не могут быть и прерваны.

В этой ситуации каждая такая группа процессов получает название висячей (orphaned). Стандарт POSIX определяет ее как группу процессов, чей родитель является также членом этой группы либо не является членом сеанса этой группы. Другими словами, группа процессов не является висячей до тех пор, пока у нее есть родительский процесс, принадлежащий тому же сеансу, но другой группе.

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

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

Программы, которые не прерываются по сигналу SIGHUP, получают сигнал SIGCONT, который продолжает выполнение приостановленных процессов. Такая последовательность прерывает большинство процессов и обеспечивает оставшимся возможность работать (то есть гарантирует, что они не будет в приостановленном состоянии).

Как только процесс становится висячим, он принудительно отключается от своего управляющего терминала (позволяя новому пользователю при необходимости применять этот терминал). Если продолжающие работать программы пытаются получить доступ к терминалу, эти попытки вызывают ошибки, устанавливающие errno в значение EIO. Процессы остаются в том же сеансе, и идентификатор сеанса не используется для новых идентификаторов процессов до тех пор, пока не завершатся все процессы данного сеанса.

 

10.7. Введение в

ladsh

 

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

• Простые встроенные команды.

• Запуск внешних команд.

• Перенаправление ввода-вывода (>, | и так далее).

• Управление заданиями.

Полный исходный текст окончательной версии этой оболочки, ladsh4.с, представлен в приложении Б. По мере добавления в ladsh новых средств, изменения исходного текста описываются в тексте книги. Чтобы уменьшить количество изменений, которые мы вносим между версиями, некоторые ранние версии несколько более сложны, чем было бы нужно. Эти небольшие усложнения, однако, далее в книге упрощают разработку оболочки, поэтому будьте терпеливы. Просто пока поверьте, что эти фрагменты кода необходимы; все они будут объяснены позднее.

 

10.7.1. Запуск внешних программ с помощью ladsh

Вот первая (и самая простая) версия ladsh, называемая ladsh1.

  1: /*ladsh1.c*/

  2:

  3: #include

  4: #include

  5: #include

  6: #include

  7: #include

  8: #include

  9: #include

 10: #include

 11: #include

 12: #include

 13:

 14: #define MAX_COMMAND_LEN 250 /* максимальная длина отдельной

 15:                                командной строки */

 16: #define JOB_STATUS_FORMAT "[%d]%-22s%.40s\n"

 17:

 18: struct jobSet {

 19:  struct job *head; /* заголовок списка запущенных заданий */

 20:  struct job *fg;   /* текущее задание переднего плана */

 21: };

 22:

 23: struct childProgram {

 24:  pid_t Pid;   /* 0 на выходе */

 25:  char **argv; /* имя программы с аргументами */

 26: };

 27:

 28: struct job {

 29:  int job Id;       /* номер задания */

 30:  int numProgs;     /* общее кол-во программ в задании */

 31:  int runningProgs; /* кол-во работающих программ */

 32:  char *text;       /* имя задания */

 33:  char *cmdBuf;     /* буфер различных argv */

 34:  pid_t pgrp;       /* идентификатор группы процессов задания */

 35:  struct childProgram *progs; /* массив программ в задании */

 36:  struct job *next; /* для слежения за фоновыми программами */

 37: };

 38:

 39: void freeJob(struct job *cmd) {

 40:  int i;

 41:

 42:  for (i=0; inumProgs; i++) {

 43:   free (cmd->progs[i].argv);

 44:  }

 45:  free(cmd->progs);

 46:  if (cmd->text) free(cmd->text);

 47:   free(cmd->cmdBuf);

 48:  }

 49:

 50:  int getCommand(FILE *source, char *command) {

 51:  if (source == stdin) {

 52:   printf("#");

 53:   fflush(stdout);

 54:  }

 55:

 56:  if (!fgets(command, MAX_COMMAND_LEN, source)) {

 57:   if (source==stdin) printf("\n");

 58:   return 1;

 59:  }

 60:

 61:  /* удалить завершающий перевод строки */

 62:  command[strlen(command) - 1] = '\0';

 63:

 64:  return 0;

 65: }

 66:

 67: /* Возвратить cmd->numProgs как 0, если нет никаких команд (то есть пустая

 68:    строка). Если найдена правильная команда, commandPtr устанавливается в

 69:    указатель на начало следующей команды (если исходная команда имеет более

 70:    одного задания, ассоциированного с ней) или NULL, если

 71:    больше нет команд.*/

 72: int parseCommand(char **commandPtr, struct job *job, int *isBg) {

 73:  char *command;

 74:  char *returnCommand = NULL;

 75:  char *src, *buf;

 76:  int argc = 0;

 77:  int done = 0;

 78:  int argvAlloced;

 79:  char quote = '\0';

 80:  int count;

 81:  struct childProgram *prog;

 82:

 83:  /* Пропустить ведущие пробелы */

 84:  while(**commandPtr && isspace(**commandPtr)) (*commandPtr)++;

 85:

 86:  /* здесь обрабатываются пустые строки и ведущие символы '#' */

 87:  if (!**commandPtr || (**commandPtr=='#')) {

 88:   job->numProgs = 0;

 89:   *commandPtr = NULL;

 90:   return 0;

 91:  }

 92:

 93:  *isBg = 0;

 94:  job->numProgs = 1;

 95:  job->progs = malloc(sizeof(*job->progs));

 96:

 97:  /* Мы устанавливаем элементы argv в указатели внутри строки.

 98:     Память освобождается freeJob().

 99:

100:     Получение чистой памяти позволяет далее иметь дело с

101:     NULL-завершающимися вещами и делает все остальное немного

102:     яснее (к тому же, это добавляет эффективности) */

103:  job->cmdBuf = command = calloc(1, strlen(*commandPtr) + 1);

104:  job->text = NULL;

105:

106:  prog = job->progs;

107:

108:  argvAlloced = 5;

109:  prog->argv = malloc(sizeof(*prog->argv) * argvAlloced);

110:  prog->argv[0] = job->cmdBuf;

111:

112:  buf = command;

113:  src = *commandPtr;

114:  while (*src && !done) {

115:   if (quote==*src) {

116:    quote='\0';

117:   } else if (quote) {

118:    if (*src == '\\') {

119:     src++;

120:     if (!*src) {

121:      fprintf(stderr,

122:       "ожидается символ после\\\n");

123:      freeJob(job);

124:      return 1;

125:     }

126:

127:     /* в оболочке, "\'" должно породить \' */

128:     if (*src != quote) *buf++='\\';

129:    }

130:    *buf++ = *src;

131:   } else if (isspace(*src)) {

132:    if (*prog->argv[argc]) {

133:     buf++, argc++;

134:     /* +1 здесь оставляет место для NULL,

135:        которым завершается argv */

136:     if ((argc+1) == argvAlloced) {

137:      argvAlloced += 5;

138:      prog->argv = realloc(prog->argv,

139:       sizeof(*prog->argv)*argvAlloced);

140:     }

141:     prog->argv[argc]=buf;

142:    }

143:   } else switch(*src) {

144:   case '"':

145:   case '\'':

146:    quote = *src;

147:    break;

148:

149:   case '#' : /* комментарий */

150:    done=1;

151:    break;

152:

153:   case '&': /* фоновый режим */

154:    *isBg = 1;

155:   case ';': /* множественные команды */

156:    done=1;

157:    return Command = *commandPtr + (src - *commandPtr) + 1;

158:    break;

159:

160:   case '\\' :

161:    src++;

162:    if (!*src) {

163:     freeJob(job);

164:     fprintf(stderr, "ожидается символ после \\\n");

165:     return 1;

166:    }

167:    /* двигаться дальше */

168:   default:

169:    *buf++=*src;

170:   }

171:

172:   src++;

173:  }

174:

175:  if (*prog->argv[argc]) {

176:   argc++;

177:  }

178:  if (!argc) {

179:   freeJob(job);

180:   return 0;

181:  }

182:  prog->argv[argc]=NULL;

183:

184:  if (!returnCommand) {

185:   job->text = malloc(strlen(*commandPtr) + 1);

186:   strcpy(job->text,*commandPtr);

187:  } else {

188:   /* Это оставляет хвостовые пробелы, что несколько излишне */

189:

190:   count = returnCommand - *commandPtr;

191:   job->text = malloc(count + 1);

192:   strncpy(job->text,*commandPtr,count);

193:   job->text[count] = '\0';

194:  }

195:

196:  *commandPtr = returnCommand;

197:

198:  return 0;

199: }

200:

201: int runCommand(struct jobnewJob, struct jobSet *jobList,

202:  intinBg) {

203:  struct job *job;

204:

205:  /* обходной путь "вручную" - мы не используем fork(),

206:     поэтому не можем легко реализовать фоновый режим */

207:  if (!strcmp(newJob.progs[0].argv[0], "exit")) {

208:   /* это должно вернуть реальный код возврата */

209:   exit(0);

210:  } else if(!strcmp(newJob.progs[0].argv[0], "jobs")) {

211:   for (job = jobList->head; job; job = job->next)

212:    printf(JOB_STATUS_FORMAT, job->jobId, "Работаю",

213:     job->text);

214:   return 0;

215:  }

216:

217:  /* у нас пока только одна программа на дочернее задание,

218:     потому это просто */

219:  if (!(newJob.progs[0].pid = fork())) {

220:   execvp(newJob.progs[0].argv[0],newJob.progs[0].argv);

221:   fprintf(stderr, "exec() для %s потерпела неудачу: %s\n",

222:    newJob.progs[0].argv[0],

223:   strerror(errno));

224:   exit(1);

225:  }

226:

227:  /* поместить дочернюю программу в отдельную группу процессов */

228:  setpgid(newJob.progs[0].pid,newJob.progs[0].pid);

229:

230:  newJob.pgrp = newJob.progs[0].pid;

231:

232:  /* найти идентификатор для задания */

233:  newJob.jobld = 1;

234:  for (job = jobList->head; job; job = job->next)

235:   if (job->jobId >= newJob.jobId)

236:    newJob.jobId = job->jobId+1;

237:

238:  /* задание для списка заданий */

239:  if (!jobList->head) {

240:   job = jobList->head = malloc(sizeof(*job));

241:  } else {

242:   for (job = jobList->head; job->next; job = job->next);

243:   job->next = malloc(sizeof(*job));

244:   job = job->next;

245:  }

246:

247:  *job = newJob;

248:  job->next = NULL;

249:  job->runningProgs = job->numProgs;

250:

251:  if (inBg) {

252:   /* мы не ждем завершения фоновых заданий - добавить

253:      в список фоновых заданий и оставить в покое */

254:

255:   printf("[%d]%d\n", job->jobId,

256:    newJob.progs[newJob.numProgs-1].pid);

257:  } else {

258:   jobList->fg=job;

259:

260:   /* переместить новую группу процессов на передний план */

261:

262:   if (tcsetpgrp(0,newJob.pgrp))

263:    perror("tcsetpgrp");

264:  }

265:

266:  return 0;

267: }

268:

269: void removeJob(struct jobSet *jobList, struct job *job) {

270:  struct job *prevJob;

271:

272:  freeJob(job);

273:  if (job == jobList->head) {

274:   jobList->head=job->next;

275:  } else {

276:   prevJob = jobList->head;

277:   while (prevJob->next != job) prevJob = prevJob->next;

278:   prevJob->next=job->next;

279:  }

280:

281:  free(job);

282: }

283:

284: /* Проверить, завершился ли какой-то из фоновых процессов -

285:    если да, выяснить, почему и определить, завершилось ли задание */

286: void checkJobs(struct jobSet *jobList) {

287:  struct job *job;

288:  pid_t childpid;

289:  int status;

290:  int progNum;

291:

292:  while ((childpid = waitpid(-1, &status, WNOHANG))>0) {

293:   for (job = jobList->head;job;job = job->next) {

294:    progNum = 0;

295:    while (progNumnumProgs &&

296:     job->progs[progNum].pid != childpid)

297:     progNum++;

298:    if (progNumnumProgs) break;

299:   }

300:

301:   job->runningProgs--;

302:   job->progs[progNum].pid = 0;

303:

304:   if (!job->runningProgs) {

305:    printf(JOB_STATUS_FORMAT,job->jobId,"Готово",

306:     job->text);

307:    removeJob(jobList, job);

308:   }

309:  }

310:

311:  if (childpid == -1 && errno!= ECHILD)

312:   perror("waitpid");

313:  }

314:

315:  int main(int argc, const char **argv) {

316:   char command [MAX_COMMAND_LEN + 1];

317:   char *nextCommand = NULL;

318:   struct jobSetjobList = {NULL, NULL};

319:   struct jobnewJob;

320:   FILE *input = stdin;

321:   int i;

322:   int status;

323:   int inBg;

324:

325:   if (argc>2) {

326:    fprintf(stderr,"Непредвиденные аргументы; использование: ladsh1 "

327:     "<команды>\n");

328:    exit(1);

329:   } else if (argc == 2) {

330:    input = fopen(argv[1], "r");

331:    if (!input) {

332:     perror("fopen");

333:     exit(1);

334:    }

335:   }

336:

337:   /* не обращать внимания на этот сигнал; он только вводит

338:      в заблуждение и не имеет особого значения для оболочки */

339:   signal(SIGTTOU, SIG_IGN);

340:

341:   while(1) {

342:   if (!jobList.fg) {

343:    /* нет заданий переднего плана */

344:

345:    /* проверить, завершились ли какие-то фоновые процессы */

346:    checkJobs(&jobList);

347:

348:    if (!nextCommand) {

349:     if (getCommand(input, command)) break;

350:     nextCommand=command;

351:    }

352:

353:    if (!parseCommand(&nextCommand, &newJob, &inBg) &&

354:     newJob.numProgs) {

355:     runCommand(newJob,&jobList,inBg);

356:    }

357:   } else {

358:    /* задание выполняется на переднем плане; ждать завершения */

359:    i = 0;

360:    while (!jobList.fg->progs[i].pid) i++;

361:

362:    waitpid(jobList.fg->progs[i].pid,&status,0);

363:

364:    jobList.fg->runningProgs--;

365:    jobList.fg->progs[i].pid=0;

366:

367:    if (!jobList.fg->runningProgs) {

368:     /* дочернее завершилось */

369:

370:     removeJob(&jobList, jobList.fg);

371:     jobList.fg = NULL;

372:

373:     /* переместить оболочку на передний план */

374:     if (tcsetpgrp(0, getpid()))

375:      perror("tcsetpgrp");

376:    }

377:   }

378:  }

379:

380:  return 0;

381: }

Эта версия не делает ничего, кроме запуска внешней программы с аргументами, поддержки комментариев стиля # (все, что следует за символом #, игнорируется), и позволяет программам выполняться в фоновом режиме. Она работает как интерпретатор простых сценариев оболочки, написанных в нотации #!, но ничего сверх этого не делает. Она разработана в качестве имитации обычного интерпретатора оболочки, используемого в системах Linux, несмотря на то, что в значительной степени упрощена.

Прежде всего, взглянем на структуры данных, которые здесь используются. На рис. 10.2 показаны структуры данных, используемые в ladsh1.с для отслеживания запускаемых дочерних процессов, на примере применения программы grep в фоновом режиме и links — в режиме переднего плана, struct jobSet описывает набор функционирующих заданий. Он содержит связный список заданий и указатель на текущее задание, выполняемое на переднем плане. Если такового нет, то указатель равен NULL, ladsh1.с использует struct jobSet для того, чтобы отслеживать задания, выполняемые в данный момент в фоновом режиме.

Рис. 10.2. Структуры данных, описывающие задания для ladsh1.с

struct childProgram описывает отдельную выполняемую программу. Это не совсем то же самое, что задание — в конце концов, каждое задание может состоять из нескольких программ, связанных по программным каналам. Для каждой дочерней программы ladsh отслеживает pid, имя программы и аргументы командной строки. Первый элемент argv, argv[0], содержит имя запущенной программы, которое передается также потомку в виде первого аргумента.

Множество программ объединяется в одно задание с помощью struct job. Каждое задание имеет уникальный идентификатор в оболочке, соответствующее количество программ, составляющих задание (хранимых в progs, указателе на массив struct childProgram), а также указатель на другое (следующее) задание, что позволяет объединять их вместе в связный список (который описывает struct jobSet). Задание также отслеживает, сколько отдельных программ составляет его, и сколько их них все еще выполняются (поскольку не все компоненты задания могут завершаться одновременно). Остальные два члена — text и cmdBuf — служат в качестве буферов для хранения различных строк, которые используются структурами struct childProgram, содержащимися в задании.

Большая часть struct jobSet состоит из динамически распределенной памяти, которая должна быть освобождена по завершении задания. Первая функция в ladsh1.с, freeJob(), освобождает память, использованную заданием.

Следующая функция, getCommand(), получает команду, введенную пользователем, и возвращает строку. Если команды читаются из файла, то никакого приглашения не выводится (вот почему код сравнивает входной файловый поток со stdin).

parseCommand() разбивает строку команды в структуру struct job для использования в ladsh. Первый аргумент — это указатель на указатель на команду. Если в строке множество команд, он переставляется на начало следующей команды. Он устанавливается в NULL, когда завершается разбор последней команды в строке. Это позволяет parseCommand() разбирать только одну команду при каждом вызове и дает возможность вызывающей функции просто разбирать строку за несколько вызовов. Следует отметить, что несколько программ, объединенных каналами, не рассматриваются как отдельные команды — независимыми друг от друга считаются только команды, разделенные символами ; или &. Поскольку parseCommand() — это просто пример разбора строк, мы не будем углубляться в детали ее работы.

Функция runCommand() отвечает за запуск отдельного задания. Она принимает структуру struct job, описывающую запускаемое задание, список заданий, выполняющихся в данный момент, а также флаг, указывающий, должно ли задание выполняться в фоновом режиме или же на переднем плане.

Пока ladsh не поддерживает каналов, поэтому каждое задание может состоять только из одной программы (хотя большая часть инфраструктуры, поддерживающей каналы, уже присутствует в ladsh1.с). Если пользователь запускает exit, происходит немедленный выход из программы. Это пример встроенной команды, которую выполняет сама оболочка для обеспечения правильного поведения. Другая встроенная команда — jobs — также здесь реализована.

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

219: if (!(newJob.progs[0].pid = fork())) {

220:  execvp(newJob.progs[0].argv[0], newJob.progs[0].argv);

221:  fprintf(stderr, "exec() для %s потерпела неудачу: %s\n",

222:   newJob.progs[0].argv[0],

223:  strerror(errno));

224:  exit(1);

225: }

Во-первых, с помощью fork() порождается дочерний процесс. Родитель сохраняет идентификатор pid дочернего процесса в newJob.progs[0].pid, тогда как дочерний процесс сохраняет там 0 (помните, что родитель и потомок имеют разные образы памяти, хотя изначально они и содержат одинаковую информацию). В результате управление в дочернем процессе входит в тело оператора if, в то время как родитель пропускает его. Дочерний немедленно запускает новую программу с помощью вызова execvp(). Если ему этот вызов не удается, печатается сообщение об ошибке и работа завершается. Это все необходимо, чтобы породить простой дочерний процесс.

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

Следующая функция, checkJobs(), ищет фоновые задания, которые были завершены, и соответствующим образом чистит список работающих заданий. Для каждого процесса, который был завершен (помните, что waitpid() возвращает только информацию о завершенных процессах, если только не было указано WUNTRACED), оболочка делает следующие вещи.

1. Ищет задание, частью которого является процесс.

2. Помечает программу как завершенную (устанавливая сохраненный pid равным 0) и уменьшает количество работающих программ в задании на единицу.

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

Процедура main() из ladsh1.с контролирует поток управления оболочки. Если при ее запуске ей передан аргумент, он трактуется как имя файла, из которого нужно читать последовательность команд. В противном случае в качестве источника команд используется stdin. Затем программа игнорирует сигнал SIGTTOU. Это элемент "магии" управления заданиями, который обеспечивает, что все происходит гладко. Смысл этого будет пояснен в главе 15. Пока что это только скелет.

Остаток функции main() составляет главный цикл программы. Условие выхода из цикла не предусмотрено. Программа завершается вызовом exit() внутри функции runCommand().

Переменная nextCommand указывает на исходное (не разобранное) строковое представление следующей команды, которая должна быть выполнена, либо NULL, если команда должна быть прочитана из входного файла, коим обычно является stdin. Когда никакое задание не выполняется на переднем плане, ladsh вызывает checkJobs() для проверки выполняющихся фоновых заданий, читает следующую команду из входного файла, если nextCommand равно NULL, затем разбирает и выполняет следующую команду.

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

 

10.8. Создание клонов

Хотя fork() является традиционным способом создания новых процессов в Unix, Linux также предлагает системный вызов call(), позволяющий процессам дублироваться с указанием ресурсов, которые родительский процесс должен разделять со своими потомками.

int clone(int flags);

Это ненамного отличается от fork(). Единственная разница в наличии параметра flags. Он должен быть установлен равным сигналу, который посылается родительскому процессу, когда потомок завершает работу (обычно это SIGCHLD), объединенному логическим "или" с любым сочетанием перечисленных ниже флагов, определенных в .

CLONE_VM Два процесса разделяют пространство виртуальной памяти (включая стек).
CLONE_FS Разделяется информация файловой системы (такая как текущий каталог).
CLONE_FILES Разделяются открытые файлы.
CLONE_SIGHAND Обработчики сигналов разделяются двумя процессами.

Когда ресурсы разделяется двумя процессами, оба они видят эти ресурсы идентично. Если указан CLONE_SIGHAND, то когда один процесс заменяет обработчик определенного сигнала, оба начинают использовать новый обработчик (подробности об обработчиках сигналов представлены в главе 12). Когда используется CLONE_FILES, разделяются не только наборы открытых файлов, но также текущие позиции в каждом файле. Значения возврата для clone() те же самые, что и у fork().

Если для доставки родительскому процессу специфицирован сигнал, отличный от SIGCHLD, то семейство функций wait() по умолчанию не будет возвращать информацию об этих процессах. Если вы хотите получать информацию об этих процессах, как и в случае процессов, использующих нормальный механизм SIGCHLD, то флаг __WCLONE должен быть объединен с помощью логического "или" с параметром flags вызова wait(). Хотя такое поведение может показаться странным, оно обеспечивает большую гибкость. Если бы функция wait() возвращала информацию о клонированных процессах, было бы сложнее построить стандартные библиотеки потоков вокруг clone(), потому что wait() должна возвращать информацию о других потоках, а также о дочерних процессах.

Хотя и не рекомендуется, чтобы приложения непосредственно использовали clone(), доступно множество библиотек пространства пользователя, которые применяют clone() и предоставляют полностью POSIX-совместимую реализацию потоков. Библиотека glibc включает libthread — наиболее популярную реализацию потоков. Теме программирования потоков POSIX посвящено несколько хороших книг, среди которых [4] и [23].

 

Глава 11

Простое управление файлами

 

Файлы — это наиболее распространенная абстракция ресурсов, используемая в мире Unix. Такие ресурсы, как память, дисковое пространство, устройства и каналы межпроцессного взаимодействия (IPC), могут быть представлены в виде файлов. Поддерживая унифицированную абстракцию для этих ресурсов, Unix уменьшает количество программных интерфейсов, которые обязан знать программист. Ниже перечислены ресурсы, доступные через файловые операции.

• Обычные файлы. Это то, о чем большинство пользователей компьютеров думают как о файлах. Они служат репозиториями данных, которые могут расти до необходимых размеров, и обеспечивают произвольный доступ. Файлы Unix являются байт-ориентированными — любое другое логическое представление является результатом программных преобразований; ядро ничего не знает о них.

• Каналы (pipes). Простейший механизм IPC в Unix. Обычно один процесс пишет информацию в канал в то время как другой читает из него. Каналы — это то, что командные оболочки используют для перенаправления ввода-вывода (например, ls -LR | grep notes или ls | more), и многие программы применяют каналы для того, чтобы передавать свой ввод программам, запущенным в виде их подпроцессов. Существуют два типа каналов: именованные и неименованные. Неименованные каналы создаются по мере необходимости и исчезают, как только читатель и писатель на концах канала закрывают его. Неименованные каналы называются так, потому что они не существуют в файловой системе и потому не имеют файловых имен. Именованные каналы обладают именами файлов, и имя файла используется для того, чтобы позволить двум независимым процессам общаться через канал (подобно тому, как работают сокеты доменов Unix (см. главу 17)). Каналы также известны как FIFO (first-in-first-out), потому что данные упорядочены в манере "первым вошел — первым вышел".

• Каталоги. Специальные файлы, которые содержат списки файлов, хранящихся внутри них. Старые реализации Unix позволяли программам читать и писать их в той же манере, что и обычные файлы. Чтобы обеспечить большую степень абстракции, добавлен специальный набор системных вызовов для обеспечения манипуляций каталогами, хотя каталоги по-прежнему открываются и закрываются подобно обычным файлам. Эти функции рассматриваются в главе 14.

• Файлы устройств. Большинство физических устройств представлены в виде файлов. Есть два типа файлов устройств: блочные устройства и символьные устройства. Файлы блочных устройств представляют аппаратные устройства, которые не могут быть прочитаны побайтно; они должны читаться блоками определенного размера. В Linux блочные устройства принимают специальное управление от ядраи могут содержать файловые системы. Дисковые приводы, включая CD-ROM и RAM-диски, являются наиболее часто используемыми блочными устройствами. Символьные устройства могут быть прочитаны по одному символу за раз, и ядро не представляет для них никаких средств кэширования или упорядочивания. Модемы, терминалы, принтеры, звуковые карты и мыши — все это символьные устройства. Традиционно к каждому из них привязана некая сущность в каталоге /dev, что позволяет пользовательским процессам получать доступ к ресурсам устройств как к файлам.

• Символические ссылки. Специальный тип файла, который содержит путь к другому файлу. Когда открывается символическая ссылка, система распознает ее как ссылку, читает ее значение и открывает файл, на который она ссылается, вместо самой ссылки. Когда используется значение, сохраняемое в символической ссылке, говорят, что система следует по ссылке. Если не указано другое, предполагается, что системные вызовы следуют по ссылкам, которые переданы им.

• Сокеты. Подобно каналам, сокеты представляют собой каналы IPC. Они более гибки, чем каналы, и могут создавать IPC-каналы между процессами, запущенными на разных машинах. Сокеты обсуждаются в главе 17.

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

Единственной уникальной отличительной чертой файла является его inode (от information node — информационный узел). Информационный узел файла содержит всю информацию о файле, включая права доступа, ассоциированные с ним, его текущий размер, количество имен, которые он имеет (оно может быть равно нулю, одному, двадцати или больше). Существуют два типа информационных узлов, in-core inode (информационный узел в ядре) — единственный тип, о котором нам нужно заботиться; каждый открытый файл в системе имеет его. Ядро отслеживает такие узлы в памяти, и они одинаковы для файловых систем всех типов. Другой тип узлов — on-disk inode (информационный узел на диске). Каждый файл в файловой системе имеет такой узел, и его точная структура зависит от типа файловой системы, в которой хранится файл.

Когда процесс открывает файл в файловой системе, on-disk inode загружается в память и превращается в in-core inode. Когда последний модифицируется, он трансформируется обратно в on-disk inode и сохраняется в файловой системе.

in-core inode и on-disk inode не содержат абсолютно одинаковую информацию. Так, например, только in-core inode отслеживает, сколько процессов в системе в данный момент используют файл, ассоциированный с ним.

Когда in-core inode и on-disk inode синхронизируются ядром, большинство системных вызовов завершаются обновлением этих узлов. Когда такое происходит, мы просто будем говорить об обновлении узла; это подразумевает, что изменением затронуты как in-core inode, так и on-disk inode. Некоторые файлы (такие как неименованные каналы), не имеют on-disk inode. В этом случае обновляется только in-core inode.

Имя файла существует только в каталоге, который связывает имя с on-disk inode. Вы можете воспринимать об именах файлов, как об указателях на дисковые узлы для файлов, ассоциированных с ними. Дисковый узел содержит в счетчике ссылок количество имен файлов, которые на него ссылаются. Когда файл удаляется, счетчик ссылок уменьшается на единицу, и если достигает 0, и ни один процесс не держит его открытым, то занятое файлом пространство освобождается. Если же другие процессы держат файл открытым, дисковое пространство освобождается тогда, когда последний из них закрывает файл.

Все это делает доступными следующие возможности.

• Можно иметь множество процессов, имеющих доступ к файлу, который не существует в файловой системе (такому, например, как канал).

• Можно создать файл на диске, удалить его вход в каталоге и продолжать выполнять чтение и запись файла.

• Можно изменить /tmp/foo и немедленно увидеть изменения в /tmp/bar, если оба имени файла ссылаются на один узел.

Система Unix всегда работала описанным образом, хотя эти операции и могут привести в замешательство новых пользователей и программистов. До тех пор, пока вы помните, что имя файла — это всего лишь указатель на дисковый узел, а сам узел — реальный ресурс, с вами все будет в порядке.

 

11.1. Режим файла

 

Каждый файл в системе имеет как тип (вроде неименованного канала или символьного устройства), так и набор прав доступа, определяющих, какие процессы могут иметь доступ к файлу. Тип файла и права доступа комбинируются в 16-битное значение (тип short в С), называемое режимом файла (file mode).

Младшие 12 бит режима файла представляют права, регламентирующие доступ к файлу, или модификаторы доступа. Они служат для множества функций. Наиболее важной функцией является возможность изменять идентификатор эффективного пользователя и идентификаторы групп файла при его выполнении.

Режим файла обычно записывается в виде шести восьмеричных разрядов. Представленные в восьмеричном виде, три младших разряда содержат модификаторы доступа файла, а два старших разряда указывают на его тип. Например, файл с режимом 0041777 имеет тип 04, модификатор прав 1 и биты доступа 0777. Аналогично, файл с режимом 0100755 имеет тип 010, не имеет установленного модификатора доступа, а правами доступа к нему являются 0755.

 

11.1.1. Права доступа к файлу

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

Попробуем немного конкретизировать последний абзац с помощью нескольких примеров. Команда Linux chmod дает возможность пользователю специфицировать режим доступа в восьмеричном виде и затем применить его к одному или более файлам. Если имеется файл somefile, который мы хотим сделать доступным для записи только его владельцу, а всем пользователям (включая владельца) разрешить его чтение, мы должны использовать режим 0644 (помните, это восьмеричные цифры). Ведущая цифра 6 — это в двоичном виде 110, а это означает, что тип пользователя, к которому она относится (в данном случае — владелец), имеет право как читать, так и писать в файл; 4 в двоичном виде выглядит как 010, что дает остальным пользователям (членам группы и прочим) права только для чтения.

$ chmod 0644 somefile

$ ls -l somefile

-rw-r--r-- 1 ewt devel 31 Feb 15 15:12 somefile

Если мы хотим позволить любому члену группы devel писать в файл, то должны использовать режим 0664.

$ chmod 0664 somefile

$ ls -l somefile

-rw-rw-r-- 1 ewt devel 31 Feb 15 15:12 somefile

Если somefile — сценарий оболочки (программы, которые используют #! в начале для указания командного интерпретатора), который мы хотим запускать на выполнение, необходимо сообщить системе, что файл является исполняемым, включив бит выполнения — в данном случае мы позволяем владельцу читать, писать и запускать файл, а членам группы devel — читать и запускать этот файл. Всем другим пользователям запрещено манипулировать файлом любым образом.

$ chmod 0750 somefile

$ ls -l somefile

-rwxr-x--- 1 ewt devel 31 Feb 15 15:12 somefile

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

Большинство системных каталогов на машинах Linux имеют права доступа 0755 и принадлежат пользователю root. Это дает возможность пользователям системы просматривать файлы в каталоге и получать доступ к ним по имени, но разрешает запись в каталоги только пользователю root. Анонимные ftp-сайты, которые позволяют любому пользователю отправлять файлы, но не дают возможность им загружать их до тех пор, пока администратор не просмотрит их содержимое, обычно устанавливают права на входящие каталоги в значение 0722. Это позволяет всем пользователям создавать новые файлы в каталоге, не предоставляя им возможность ни видеть содержимое каталога, ни получать доступа к файлам.

Дополнительную информацию о правах доступа к файлам можно найти в любой книге по Linux или Unix.

 

11.1.2. Модификаторы прав доступа к файлам

Модификаторы прав доступа файлов — это также битовые маски, значения которых представляют биты setuid, setgid и sticky-бит ("липкий" бит). Если бит setuid установлен для исполняемого файла, то эффективный идентификатор пользователя процесса устанавливается равным идентификатору владельца файла, когда программы выполняется (в главе 10 можно найти информацию о том, почему это удобно). Бит setgid ведет себя аналогичным образом, но устанавливает эффективный идентификатор группы в значение группы файла. Бит setuid не имеет значения для неисполняемых файлов, но если бит setgid устанавливается для неисполняемого файла, любая блокировка, выполняемая над файлом, носит обязательный, а не рекомендательный характер (см. главу 13). В Linux биты setuid и setgid игнорируются для сценариев оболочки, поскольку устанавливать setuid для сценариев было бы опасно.

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

Sticky-бит — последний значащий бит в разряде модификатора доступа к файлу, имеет интересную историю, связанную с его наименованием. Старые реализации Unix должны были загружать в память всю программу целиком, прежде чем начать выполнять ее. Это означало, что крупные программы отнимали значительное время на запуск, что было довольно-таки неприятно. Если же программа имела установленный sticky-бит, то операционная система пыталась сохранить ее "привязанной" в памяти настолько долго, насколько возможно, даже когда эта программа не запущена, чтобы уменьшить время запуска. Хотя это было немного некрасиво, но работало достаточно хорошо с часто используемыми программами, такими как компилятор С. Современные реализации Unix, включая Linux, используют загрузку по требованию — кусочек за кусочком, что сделало sticky-бит излишним, поэтому Linux игнорирует его для обычных файлов.

Sticky-бит по-прежнему используется для каталогов. Обычно любой пользователь с правами записи в каталог может удалить любой файл в этом каталоге. Однако если sticky-бит каталога установлен, файлы могут быть удалены только пользователем-владельцем либо пользователем root. Такое поведение удобно, если каталог служит репозиторием для файлов, созданных многими пользователями, например, /tmp.

Последний раздел режима файла указывает тип файла. Он содержится в старших восьмеричных разрядах и не является битовой маской. Вместо этого значение этих разрядов равно специфическому типу файлов (04 означает каталог, 06 — блочное устройство). Тип файла устанавливается при его создании. Он никогда не может быть изменен, кроме как посредством удаления файла.

Включаемый файл представляет символические константы для всех этих битов доступа, что делает код более читабельным. Пользователи Unix и Linux обычно чувствуют себя увереннее с восьмеричными представлениями режимов файла, поэтому обычно в программах используются восьмеричные представления непосредственно. В табл. 11.1 перечислены символические имена прав и модификаторов доступа к файлам.

Таблица 11.1. Константы прав доступа к файлам

Имя Значение Описание
S_ISUID 0004000 Программа является setuid-программой.
S_ISGID 0002000 Программа является setgid-программой.
S_ISVTX 0001000 Sticky-бит.
S_IRWXU 00700 Владелец файла имеет права на чтение, запись и выполнение.
S_IRUSR 00400 Владелец файла имеет права на чтение.
S_IWUSR 00200 Владелец файла имеет права на запись.
S_IXUSR 00100 Владелец файла имеет права на выполнение.
S_IRWXG 00070 Группа файла имеет права на чтение, запись и выполнение.
S_IRGRP 00040 Группа файла имеет права на чтение.
S_IWGRP 00020 Группа файла имеет права на запись.
S_IXGRP 00010 Группа файла имеет права на выполнение.
S_IRWXO 00007 Прочие пользователи имеют права на чтение, запись и выполнение.
S_IROTH 00004 Прочие пользователи имеют права на чтение.
S_IWOTH 00002 Прочие пользователи имеют права на запись.
S_IXOTH 00001 Прочие пользователи имеют права на выполнение.

 

11.1.3. Типы файлов

Старшие четыре бита режима файла указывают тип файла. В табл. 11.2 перечислены константы, имеющие отношение к типам файлов. Объединение с помощью битовой операции "И" любых этих констант с режимом файла порождает ненулевое значение, если бит установлен.

Таблица 11.2. Константы типов файлов

Имя Значение (восьмеричное) Описание
S_IFMT 00170000 Это значение, побитно объединенное с режимом с помощью операции "И", дает тип файла (который эквивалентен одному из остальных значений S_IF ).
S_IFSOCK 0140000 Файл является сокетом.
S_IFLNK 0120000 Файл является символической ссылкой.
S_IFREG 0100000 Файл является обычным файлом.
S_IFBLK 0060000 Файл представляет блочное устройство.
S_IFDIR 0040000 Файл является каталогом.
S_IFCHR 0020000 Файл представляет символьное устройство.
S_IFIFO 0010000 Файл представляет коммуникационный канал "первый вошел — первый вышел".

Описанные ниже макросы принимают в качестве аргумента режим файла и возвращают true или false.

S_ISLINK(m) Истинно, если файл является символической ссылкой.
S_ISREC(m) Истинно, если файл является обычным файлом.
S_ISDIR(m) Истинно, если файл является каталогом.
S_ISCHR(m) Истинно, если файл представляет символьное устройство.
S_ISBLK(m) Истинно, если файл представляет блоковым устройство.
S_ISFIFO(m) Истинно, если файл является каналом "первый вошел — первый вышел"
S_ISSOCK(m) Истинно, если файл является сокетом.

 

11.1.4. Маска umask процесса

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

Текущая установка umask для процесса выполняется системным вызовом umask().

#include

int umask(int newmask);

Возвращается старое значение и устанавливается новое значение umask процесса. Для файла могут быть указаны только права на чтение, запись и исполнение — вы не можете использовать umask для запрещения установки setuid, setgid или sticky-бита. Команда umask представлена в большинстве командных процессоров и позволяет пользователю устанавливать umask для самой командной оболочки и всех его последующих дочерних процессов.

В качестве примера, команда touch создает новые файлы с правами 0666 (общие права на чтение и запись). Так как пользователю подобное редко подходит, он может заставить команду touch отключать общие и групповые права записи для файла с помощью команды umask 022, как показано ниже.

$ umask 022

$ touch foo

$ ls -l foo

-rw-r--r-- 1 ewt ewt 0 Feb 24 21:24 foo

Если он предпочитает давать права на запись группе, то может вместо этого назначит umask 002.

$ umask 002

$ touch foo

$ ls -l foo

-rw-rw-r-- 1 ewt ewt 0 Feb 24 21:24 foo

Если же он хочет, чтобы его файлы были доступны только ему, это обеспечит umask 077.

$ umask 077

$ touch foo

$ ls -l foo

-rw------- 1 ewt ewt 0 Feb 24 21:24 foo

umask процесса влияет на системные вызовы open(), creat(), mknod() и mkdir().

 

11.2. Основные файловые операции

 

Поскольку значительная часть системных вызовов Linux манипулирует файлами, начнем с демонстрации наиболее широко используемых функций. Более специализированные функции обсудим далее в настоящей главе. Функции, применяемые для чтения каталогов, представлены в главе 14, чтобы сделать настоящую главу более краткой.

 

11.2.1. Файловые дескрипторы

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

Первые три файловых дескриптора для процессов (0, 1 и 2) имеют стандартное назначение. Первый, 0, известен как стандартный ввод (stdin) и является местом, откуда программы должны получать свой интерактивный ввод. Файловый дескриптор 1 называется стандартным выводом (stdout), и большая часть вывода программ должна быть направлена в него. Сообщения об ошибках должны направляться в стандартный поток ошибок (stderr), который имеет файловый дескриптор 2. Стандартная библиотека С следует этим правилам, поэтому gets() и printf() используют stdin и stdout соответственно, и это соглашение дает возможность командным оболочкам правильно перенаправлять ввод и вывод процессов.

Заголовочный файл представляет макросы STDIN_FILENO, STDOUT_FILENO и STDERR_FILENO, которые вычисляются как файловые дескрипторы stdin, stdout и stderr соответственно. Использование этих символических имен делает код более читабельным.

Многие из файловых операций, которые манипулируют файловыми узлами inode, доступны в двух формах. Первая форма принимает в качестве аргумента имя файла. Ядро использует этот аргумент для поиска inode файла и выполняет соответствующую операцию над ним (обычно это включает следование символическим ссылкам). Вторая форма принимает файловый дескриптор в качестве аргумента и выполняет операцию над inode, на который он ссылается. Эти два набора системных вызовов используют похожие имена, но системные вызовы, работающие с файловыми дескрипторами, имеют префикс f. Например, системный вызов chmod() изменяет права доступа для файла, ссылка на который осуществляется по имени; fchmod() устанавливает права доступа к файлу, ссылаясь на него по указанному файловому дескриптору.

Чтобы меньше тратить слов, мы представим обе версии системных вызовов, если они существуют, а обсуждать будет только первую из их (та, которая использует имена файлов).

 

11.2.2. Закрытие файлов

Одной из операций, которые одинаковы для файлов всех типов, является закрытие файла. Ниже показано, как закрыть файл.

#include

int close(int fd);

Очевидно, что это базовая операция. Однако есть один важный момент, касающийся закрытия файлов, о котором следует помнить — она может завершиться сбоем. Некоторые системы (в первую очередь, следует вспомнить сетевые файловые системы вроде NFS) не пытаются поместить последнюю порцию записываемых данных в файл до тех пор, пока он не будет закрыт. Если такая операция вызовет сбой (например, по причине недоступности удаленного хоста), то close() вернет ошибку. Если ваше приложение пишет данные, но не синхронизирует записи (см. обсуждение O_SYNC в следующем разделе), то вы всегда должны проверять результат закрытия файла. Если close() дает сбой, то это значит, что обновленный файл поврежден самым непредсказуемым образом! К счастью подобное случается достаточно редко.

 

11.2.3. Открытие файлов в файловой системе

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

#include

#include

int open(char *pathname, int flags, mode_t mode);

int creat(char *pathname, mode_t mode);

Функция open() возвращает файловый дескриптор, указывающий на pathname. Если возвращенное значение меньше нуля, значит, произошла ошибка (как всегда, errno содержит код ошибки). Аргумент flags описывает тип доступа, который нужен вызывающему процессу, а также управляет различными атрибутами открытия и манипулирования файлом. Режим доступа всегда должен быть указан, и он может быть одним из следующих: O_RDONLY, O_RDWR либо O_WRONLY, что запрашивает доступ, соответственно, только по чтению, по чтению и записи либо только по записи. С этим режимом может быть объединены логическим "И" следующие значения для управления прочей семантикой файлов.

O_CREAT Если файл еще не существует, создать его как обычный файл.
O_EXCL Этот флаг должен использоваться только с O_CREAT . Если он указан, то open() дает сбой в случае существования файла. Этот флаг позволяет реализовать простую блокировку, но не надежен при использовании в сетевых файловых системах типа NFS (подробно о блокировке файлов рассказывается в главе 13).
O_NOCTTY Открываемый файл не становится управляющим терминалом процесса (см. главу 10). Этот флаг имеет значение только тогда, когда процесс, не имеющий управляющего терминала, открывает устройство tty. Если же он указан в любом другом случае, этот флаг игнорируется.
O_TRUNC Если файл уже существует, его содержимое отбрасывается, и его размер устанавливается равным 0.
O_APPEND Все операции записи выполняются в конец файла, хотя произвольный доступ по чтению также разрешен.
O_NONBLOCK Файл открывается в неблокирующем режиме. Операции с нормальными файлами всегда блокируются, потому что они работают с локальными жесткими дисками, имеющими предсказуемое время отклика, но операции на некоторых типах файлов требуют непредсказуемого времени для завершения. Например, чтение из канала, в котором нет данных, блокирует процесс чтения до тех пор, пока данные в нем не появятся. Если же специфицирован флаг O_NONBLOCK , вызов read() вместо блокирования вернет ноль байт. Файлы, на операции с которыми может понадобиться непредсказуемый объем времени, называются медленными файлами . ( Примечание . O_NDELAY — оригинальное имя O_NONBLOCK , теперь устаревшее.)
O_SYNC Обычно ядро перехватывает операции записи и сбрасывает их на физическое устройство тогда, когда это удобно. Хотя такая реализация значительно повышает производительность, появляется также возможность потери данных, чем в том случае, когда они немедленно пишутся на диск. Если при открытии файла указан флаг O_SYNC , то все изменения в файле сохраняются на диске перед тем, как ядро возвращает управления процессу, выполняющему запись. Это очень важно для некоторых приложений, таких как системы управления базами данных, в которых принудительная запись используется для предотвращения повреждения данных в случае сбоя системы.

Параметр mode указывает права доступа для файла, если он создается и если он модифицируется текущей установкой umask процесса. Если не указано O_CREAT, то mode игнорируется.

Функция creat() в точности эквивалентна следующему вызову:

open(pathname, O_CREAT | O_WRONLY | O_TRUNC, mode)

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

 

11.2.4. Чтение, запись и перемещение

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

#include

size_t read(int fd, void * buf, size_t length);

size_t read(int fd, const void * buf, size_t length);

Обе функции принимают файловый дескриптор fd, указатель на буфер buf и длину буфера length, read() читает из файлового дескриптора и помещает полученные данные в буфер; write() пишет length байт из буфера в файл. Обе функции возвращают количество переданных байт, или -1 в случае ошибки (это означает, что ничего не было прочитано или записано).

Теперь, когда мы описали эти системные вызовы, рассмотрим простой пример, создающий файл hw в текущем каталоге и записывающий в него строку "Добро пожаловать!".

 1: /* hwwrite.с */

 2:

 3: #include

 4: #include

 5: #include

 6: #include

 7: #include

 8:

 9: int main(void) {

10:  int fd;

11:

12:  /* открыть файл, создавая его, если он не существовал, и удаляя

13:     его содержимое в противном случае */

14:  if ((fd = open("hw", O_TRUNC | O_CREAT | O_WRONLY, 0644)) < 0) {

15:   perror("open");

16:   exit(1);

17:  }

18:

19:  /* магическое число 18 - это кол-во символов, которые

20:     будут записаны */

21:  if (write(fd, "Добро пожаловать!\n", 18) != 18) {

22:   perror("write");

23:   exit(1);

24:  }

25:

26:  close(fd);

27:

28:  return 0;

29: }

Ниже показано, что получится, если запустить hwwrite.

$ cat hw

cat: hw: No such file or directory

cat: hw: Файл или каталог не существует

$ ./hwwrite

$ cat hw

Добро пожаловать!

$

Для изменения этой программы, чтобы она читала файл, нужно просто изменить open(), как показано ниже, и заменить write() статической строки на read() в буфер.

open("hw", O_RDONLY);

Файлы Unix можно разделить на две категории: просматриваемые (seekable) и непросматриваемые (nonseekable). Непросматриваемые файлы представляют собой каналы, работающие в режиме "первый вошел — первый вышел" (FIFO), которые не поддерживают произвольное чтение или запись, их данные не могут быть перечитаны или перезаписаны. Просматриваемые файлы позволяют читать и писать в произвольное место файла. Каналы и сокеты являются не просматриваемыми файлами; блоковые устройства и обычные файлы являются просматриваемыми.

Поскольку FIFO — это непросматриваемые файлы, то, очевидно, что read() читает их с начала, a write() пишет в конец. С другой стороны, просматриваемые файлы не имеют очевидной точки для этих операций. Вместо этого здесь вводится понятие "текущего" положения, которое перемещается вперед после операции. Когда просматриваемый файл изначально открывается, текущее положение устанавливается в его начало, со смещением 0. Если затем из него читается 10 байт, то текущее положение перемещается в точку со смещением 10 от начала, а запись 5 байт переписывает данные, начиная с одиннадцатого байта в файле (то есть, со смещения 10, где расположена текущая позиция после чтения). После такой записи текущая позиция находится в позиции, смещенной на 15 относительно начала файла — сразу после перезаписанных данных.

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

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

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

#include

int lseek(int fd, off_t offset, int whence);

Текущая позиция для файла fd перемещается на offset байт относительно whence, где whence принимает одно из следующих значений:

SEEK_SET Начало файла.

SEEK_CUR Текущая позиция в файле.

SEEK_END Конец файла.

Для SEEK_CUR и SEEK_END значение offset может быть отрицательным. В этом случае текущая позиция перемещается в сторону начала файла (от whence), а не в сторону конца. Например, приведенный ниже код перемещает текущую позицию на 5 байт назад от конца файла.

lseek(fd, -5, SEEK_END);

Системный вызов lseek() возвращает новую текущую позицию в файле относительно его начала, либо -1 в случае ошибки. То есть lseek(fd, 0, SEEK_END) — это просто способ определения размера файла, но следует убедиться, что вы сбросили текущую позицию, прежде чем читать из fd.

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

Процесс A                Процесс B

lseek(fd, 0, SEEK_END);

                        lseek(fd, 0, SEEK_END);

                        write (fd, buf, 10);

write(fd, buf, 5);

В этом случае процесс А перепишет первые 5 байт данных, которые запишет процесс В, а это наверняка не то, чего вы хотели. Если множеству процессов нужно совместно писать данные в конец файла, должен быть использован флаг O_APPEND, который делает эту операцию атомарной.

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

Части файлов, которые "создаются" подобным образом, называют "дырками" (holes). Чтение из такой "дырки" в файле возвращает буфер, полный двоичных нулей, а запись в них может завершиться ошибкой по причине отсутствия свободного пространства на диске. Все это значит, что вызов lseek() не должен применяться для резервирования дискового пространства для позднейшего использования, поскольку такое пространство может быть и не выделено. Если ваше приложение нуждается в выделении некоторого дискового пространства для последующего использования, вы должны применять write(). Файлы с "дырками" часто используют для хранения редко расположенных в них данных, таких как файлы, представляющие хеш-таблицы.

Для простой демонстрации "дырок" в файлах, основанной на командной оболочке, рассмотрим следующий пример (/dev/zero — это символьное устройство, которое возвращает столько двоичных нулей, сколько процесс пытается прочитать из него).

$ dd if=/dev/zero of=foo bs=1k count=10

10+0 records in

10+0 records out

$ ls -l foo

-rw-rw-r-- 1 ewt ewt 10240 Feb 6 21:50 foo

$ du foo

10 foo

$ dd if=/dev/zero of=bar bs=1k count=1 seek=9

1+0 records in

1+0 records out

$ ls -l bar

-rw-rw-r-- 1 ewt ewt 10240 Feb 6 21:50 foo

$ du bar

1 bar

$

Хотя оба файла — и foo, и bar — имеют длину в 10 Кбайт, файл bar занимает только 1 Кбайт дискового пространства, потому что остальные 9 Кбайт были пропущены seek(), когда файл был создан или записан.

 

11.2.5. Частичное чтение и запись

Хотя обе функции — и read(), и write() — принимают параметр, указывающий, сколько байт нужно прочитать или записать, ни одна из них не гарантирует, что обработает указанное количество байт, даже если не случается никаких ошибок. Простейший пример этого — попытки чтения из обычного файла, который уже позиционирован в конце. Система не может прочитать ни одного байта, но это в то же время не является ошибкой. Вместо этого read() возвращает 0 байт. Точно так же, если текущая позиция находилась в 10 байт от конца файла, и была выполнена попытка прочитать из файла более 10 байт, то прочитано будет ровно 10 байт и вызов read() вернет число 10. Опять-таки это не рассматривается как ошибочная ситуация.

Поведение read() также зависит от того, был ли файл открыт с флагом O_NONBLOCK. Для файлов многих типов O_NONBLOCK не влияет ни на что. Файлы, для которых система может гарантировать завершенность операции в пределах разумного периода времени, всегда блокирует чтение и запись; такие файлы часто называют быстрыми файлами. Это множество файлов включает локальные блочные устройства и обычные файлы. Для других типов файлов, таких как каналы, и символьных устройств вроде терминалов процесс может ожидать другого процесса (или человека), чтобы тот либо выполнил чтение, либо освободил ресурсы системы при обработке запроса на write(). В обоих случаях система не имеет способа знать — будет ли вообще возможно дождаться завершения системного вызова. Когда такие файлы открываются с флагом O_NONBLOCK, то для каждой операции с файлом система просто делает максимум того, что удается сделать немедленно, а затем возвращает управление вызывающему процессу.

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

Чтобы показать конкретный пример чтения и записи файлов, приведем простую реализацию cat. Она копирует стандартный поток ввода (stdin) на стандартный вывод (stdout) до тех пор, пока есть что копировать.

 1: /* cat.с */

 2:

 3: #include

 4: #include

 5:

 6: /* Пока есть данные на стандартном входе (fd0), копировать их в

 7:    стандартный выход (fd1). Выйти, когда не будет доступных данных. */

 8:

 9: int main(void) {

10:  char buf[1024];

11:  int len;

12:

13:  /* len будет >= 0, пока доступны данные

14:     и read() успешен */

15:  while ((len = read(STDIN_FILENO, buf, sizeof(buf))) > 0) {

16:   if (write(1, buf, len) != len) {

17:    perror("write");

18:    return 1;

19:   }

20:  }

21:

22:  /* len будет <= 0; если len = 0, больше нет

23:     доступных данных. Иначе - ошибка. */

24:  if (len < 0) {

25:   perror("read");

26:   return 1;

27:  }

28:

29:  return 0;

30: }

 

11.2.6. Сокращение файлов

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

#include

int truncate(const char *pathname, size_t length);

int ftruncate(int fd, size_t length);

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

Если length больше текущего размера файла, то файл увеличивается до заданного размера (по возможности используя "дырки"), хотя такое поведение и не гарантируется POSIX, поэтому на него нельзя полагаться при написании переносимых программ.

 

11.2.7. Синхронизация файлов

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

Есть несколько механизмов, которые может использовать приложение, чтобы дождаться записи данных на физический носитель. Флаг O_SYNC, описанный ранее в этой главе, при каждой операции записи в файл вызывает блокирование вызывающего процесса до тех пор, пока носитель не будет действительно обновлен. Хотя это, конечно, работает, все же такой подход не является достаточно аккуратным. Обычно приложения не нуждаются в том, чтобы все операции были синхронизированы, гораздо чаще они нуждаются в том, чтобы гарантировать, что некий набор операций завершился перед тем, как может быть начат другой набор операций. Системные вызовы fsync() и fdatasync() обеспечивают такую семантику.

#include

int fsync(int fd);

int fdatasync(int fd);

Оба системных вызова приостанавливают приложение до тех пор, пока в файл fd не будут записаны все данные, fsync() также ожидает обновления информации в inode файла, подобной времени доступа (информация inode для файлов перечислена в табл. 11.3). Однако ни один из этих вызовов не гарантирует записи на неразрушимое устройство хранения. Современные дисковые приводы имеют большие собственные кэши, поэтому сбой питания может привести к тому, что некоторые данные, сохраненные в кэше, будут потеряны.

 

11.2.8. Прочие операции

Файловая модель Linux достаточно хорошо поддерживает стандартизацию большинства файловых операций через обобщенные функции наподобие read() и write() (например, запись в программный канал выполняется так же, как запись в файл на диске). Однако некоторые устройства поддерживают операции, которые плохо моделируются такой абстракцией. Например, терминальные устройства, представленные как устройства символьные, нуждаются в представлении метода изменения скорости терминала, и приводы CD-ROM, представленные как блочные устройства, нуждаются в том, чтобы знать, кода они должны воспроизводить аудиодорожки, чтобы помочь увеличить производительность работы программистов.

Все эти разнообразные операции доступны через единственный системный вызов — ioctl() (сокращение для "I/O control" — управление вводом-выводом), прототип которого показан ниже.

#include

int ioctl(int fd, int request, ...);

Хотя часто он применяется следующим образом:

int ioctl (int fd, int request, void *arg);

Всякий раз когда используется ioctl(), его первый аргумент — это файл, с которым выполняются манипуляции, а второй аргумент указывает операцию, которая должна быть выполнена. Последний аргумент обычно представляет собой указатель на нечто, но на что именно, а так же точная семантика возвращаемого кода зависит от типа файла fd и типа запрошенной операции. Для некоторых операций arg — длинное целое вместо указателя; в этих случаях обычно применяется приведение типов. В нашей книге есть множество примеров применения ioctl(), и вам нет нужды заботиться об ioctl() до тех пор, пока вы не доберетесь до них.

 

11.3. Запрос и изменение информации inode

 

11.3.1. Поиск информации inode

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

#include

int stat(const char *pathname, struct stat *statbuf);

int lstat (const char *pathname, struct stat *statbuf);

int fstat(int fd, struct stat *statbuf);

Первая версия, stat() возвращает информацию inode для файла, на который осуществляется ссылка через pathname, следуя всем символическим ссылкам, которые она представляет. Если вы не хотите следовать символическим ссылкам (например, чтобы проверить, не является ли само имя такой ссылкой), то используйте вместо этого lstat(). Последняя версия, fstat(), возвращает inode, на который ссылается текущий открытый файловый дескриптор. Все три системных вызова заполняют структуру struct stat, на которую указывает параметр statbuf, информацией о файловом inode. В табл. 11.3 описана информация, доступная в struct stat.

Таблица 11.3. Члены структуры struct stat

Тип Поле Описание
dev_t st_dev Номер устройства, на котором находится файл.
ino_t st_ino Номер файлового on-disk inode. Каждый файл имеет номер on-disk inode, уникальный в пределах устройства, на котором он расположен. То есть пара ( st_dev , st_ino ) представляет собой уникальный идентификатор файла.
mode_t st mode Режим файла. Сюда включена информация о правах доступа и типе файла.
nlink_t st_nlink Количество путевых имен, ссылающихся на данный inode. Сюда не включаются символические ссылки, потому что они ссылаются на другие имена, а не на inode.
uid_t st_uid Идентификатор пользователя, владеющего файлом.
gid_t st_gid Идентификатор группы, владеющей файлом.
dev_t st_rdev Если файл — символьное или блочное устройство, это задает старший (major) и младший (minor) номера файла. Чтобы получить информацию о членах и макросах, которые манипулируют этим значением, обратитесь к обсуждению mknod() далее в этой главе.
off_t st size Размер файла в байтах. Это определено только для обычных файлов.
unsigned long st_blksize Размер блока в файловой системе, хранящей файл.
unsigned long st_blocks Количество блоков, выделенных файлу. Обычно st_blksize * st_blocks — это немного больше, чем st_size , потому что некоторое пространство в конечном блоке не используется. Однако для файлов с "дырками" st_blksize * st_blocks может быть заметно меньше, чем st_size .
time_t st_atime Время последнего доступа к файлу. Обновляется при каждом открытии файла или модификации его inode.
time_t st_mtime Время последней модификации файла. Обновляется при изменении данных файла.
time_t st_ctime Последнее время изменения файла или его inode, включая владельца, группу, счетчик связей и так далее.

 

11.3.2. Простой пример

stat()

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

 1: /* statsamp.с */

 2:

 3: /* Для каждого имени файла, переданного в командной строке, отображаем

 4:    всю информацию, которую возвращает lstat() для файла. */

 5:

 6: #include

 7: #include

 8: #include

 9: #include

10: #include

11: #include

12: #include

13: #include

14:

15: #define TIME_STRING_BUF 50

16:

17: /* Пользователь передает buf (минимальной длины TIME_STRING_BUF) вместо

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

19:    локальных статических переменных и динамической памяти. Никаких ошибок

20:    происходить не должно, поэтому никакой проверки ошибок не делаем. */

21: char *time String (time_t t, char *buf) {

22:  struct tm *local;

23:

24:  local = localtime(&t);

25:  strftime(buf, TIME_STRING_BUF, "%c", local);

26:

27:  return buf;

28: }

29:

30: /* Отобразить всю информацию, полученную от lstat() по имени

31:    файла как единственному параметру. */

32: int statFile(const char *file) {

33:  struct stat statbuf;

34:  char timeBuf[TIME_STRING_BUF];

35:

36:  if (lstat(file, &statbuf)) {

37:   fprintf(stderr, "не удалось lstat %s: %s\n", file,

38:    strerror(errno));

39:   return 1;

40:  }

41:

42:  printf("Имя файла : %s\n", file);

43:  printf("На устройстве: старший %d/младший %d Inode номер: %ld\n" ,

44:   major(statbuf.st_dev), minor(statbuf.st_dev),

45:   statbuf.st_ino);

46:  printf ("Размер : %-101d Тип: %07o"

47:   "Права доступа : %05o\n", statbuf.st_size,

48:   statbuf.st_mode & S_IFMT, statbuf.st_mode &~(S_IFMT));

49:  printf("Владелец : %d Группа : %d"

50:   " Количество ссылок : %d\n",

51:   statbuf.st_uid, statbuf.st_gid, statbuf.st_nlink);

52:  printf("Время создания : %s\n",

53:   timeString(statbuf.st_ctime, timeBuf));

54:  printf("Время модификации : %s\n",

55:   timeString(statbuf.st_mtime, timeBuf));

56:  printf("Время доступа : %s\n",

57:   timeString (statbuf.st_atime, timeBuf));

58:

59:  return 0;

60: }

61:

62: int main(int argc, const char **argv) {

63:  int i;

64:  int rc = 0 ;

65:

66:  /* Вызвать statFile() для каждого имени файла,

67:     переданного в командной строке. */

68:  for (i = 1; i < argc; i++) {

69:   /* Если statFile() сбоит, rc будет содержать не ноль.*/

70:   rc |= statFile(argv[i]);

71:

72:   /* это печатает пробел между позициями,

73:      но не за последней */

74:   if ((argc - i) > 1) printf ("\n");

75:  }

76:

77:  return rc;

78: }

 

11.3.3. Простое определение прав доступа

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

#include

int access(const char *pathname, int mode);

mode — это маска, которая содержит одно или более перечисленных ниже значений.

F_OK Файл существует. Это требует прав на выполнение по всем каталогам, составляющим путь, поэтому может закончиться сбоем, даже если файл существует.
R_OK Процесс может читать файл.
W_OK Процесс может писать файл.
X_OK Процесс может исполнять файл (или искать в каталоге).

access() возвращает 0, если указанный режим доступа разрешен, в противном случае возвращает ошибку EACCESS.

 

11.3.4. Изменение прав доступа к файлу

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

#include

int chmod(const char *pathname, mode_t mode);

int fchmod(int fd, mode_t mode);

Хотя chmod() позволяет указать путь, помните, что права доступа к файлу определяет inode, а не имя файла. Если у файла есть множество жестких ссылок, то изменение прав доступа по одному из имен файла изменяет права доступа к нему везде, где он встречается в файловой системе. Параметр mode может быть любой комбинацией прав доступа и модификаторов прав доступа, объединенных по логическому "И". Хотя это достаточно нормально — специфицировать по несколько этих значений за раз, общей практикой для программ является указание новых прав доступа непосредственно в восьмеричном виде. Только пользователь root и владелец файла могут изменять права доступа к файлу — все остальные, кто попытается это сделать, получат ошибку EPERM.

 

11.3.5. Смена владельца и группы файла

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

#include

int chown(const char *pathname, uid_t owner, gid_t group);

int fchown(int fd, uid_t owner, gid_t group);

Параметры owner и group указывают нового владельца и группу для файла. Если любой из них равен -1, соответствующее значение не изменяется. Только пользователь root имеет право сменить владельца файла. Когда владелец файла меняется или файл записывается, то бит setuid для этого файла всегда очищается из соображений безопасности. Как root, так и владелец файла могут менять группу, которая владеет файлом, но при условии, что владелец сам является членом этой группы. Если у файла установлен бит выполнения для группы, то бит setgid очищается из тех же соображений безопасности. Если же бит выполнения для группы не установлен, то у файла включена принудительная блокировка и режим предохраняется.

 

11.3.6. Изменение временных меток файла

Владелец файла может изменять mtime и atime файла на любое желаемое значение. Это делает такие метки бесполезными для целей аудита, но позволяет инструментам архивирования вроде tar и cpio сбрасывать временные метки файлов в то значение, когда они были архивированы. Метка ctime изменяется, когда обновляются mtime и atime, поэтому tar и cpio не могут восстановить их.

Существуют два способа изменения этих меток: utime() и utimes(). utime() появилась в System V, после чего была адаптирована POSIX, в то время как utimes() пришла из BSD. Обе функции эквивалентны; они отличаются только способом, каким указываются новые временные метки.

#include

int utime(const char *pathname, struct utimbuf *buf);

#include

int utimes(const char *pathname, struct timeval *tvp);

Версия POSIX, utime(), принимает struct utimbuf, которая определена в , как показано ниже.

struct utimbuf {

 time_t асtime;

 time_t modtime;

};

utimes() из BSD вместо этого передает новое значение atime и mtime через struct timeval, которая определена в .

struct timeval {

 long tv_sec;

 long tv_usec;

};

Элемент tv_sec содержит новое значение atime; tv_usec содержит новое значение mtime для utimes().

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

 

11.3.7. Расширенные атрибуты Ext3

Главная файловая система, используемая в Linux — это Third Extended File System (третья расширенная файловая система), обычно упоминаемая как ext3. Хотя она поддерживает все традиционные функциональные средства файловых систем Unix, такие как значение отдельных бит в режиме файла, она также позволяет хранить некоторые дополнительные атрибуты для каждого файла. В табл. 11.4 описаны поддерживаемые в настоящее время дополнительные атрибуты. Эти флаги могут быть установлены и просмотрены с помощью программ chattr и lsattr.

Таблица 11.4. Расширенные атрибуты файла

Атрибут Определение
EXT3_APPEND_FL Если файл открыт для записи, должен быть указан флаг O_APPEND .
EXT3_IMMUTABLE_FL Файл не может быть модифицирован или удален ни одним пользователем, включая root.
EXT3_NODUMP Файл должен быть проигнорирован командой dump.
EXT3_SYNC_FL Файл должен обновляться синхронно, как если бы при открытии был указан флаг O_SYNC

Поскольку расширенные атрибуты ext3 выходят за пределы стандартного интерфейса файловых систем, они не могут модифицироваться с помощью chmod(), как все остальные атрибуты. Вместо этого используется ioctl(). Вспомним, как определен вызов ioctl().

#include

#include

int ioctl(int fd, int request, void *arg);

Файл, атрибуты которого меняются, должен быть открыт, как для fchmod(). Запрос (параметр request) на получение текущего состояния флагов — EXT3_IOC_GETFLAGS, а для установки их — EXT3_IOC_SETFLAGS. В обоих случаях arg должен быть указателем на int. Если используется EXT3_IOC_GETFLAGS, то long устанавливается в текущее значение программных флагов. Если применяется EXT3_IOC_SETFLAGS, то новое значение файловых флагов берется из int, на который указывает arg.

Это дополнение и неизменяемые флаги могут быть изменены только пользователем root, поскольку это связано с операциями, которые может выполнять только root.

Другие флаги могут быть модифицированы либо пользователем root, либо владельцем файла.

Приведем пример небольшой программы, которая отображает флаги для любого файла, переданного в командной строке. Она работает только с файлами из файловой системы ext3. Вызов ioctl() завершится неудачей, если применить его к файлам из любой другой файловой системы.

 1: /* checkflags.c */

 2:

 3: /* Для каждого имени файла, переданного в командной строке, отобразить

 4:    информацию об атрибутах этого файла в файловой системе ext3. */

 5:

 6: #include

 7: #include

 8: #include

 9: #include

10: #include

11: #include

12: #include

13:

14: int main(int argc, const char **argv) {

15:  const char **filename = argv + 1;

16:  int fd;

17:  int flags;

18:

19:  /* Пройти по каждому имени файла, переданному в командной строке. Последний

20:     указатель в argv[] равен NULL, поэтому такие циклы while() корректны. */

21:  while(*filename) {

22:   /* В отличие от нормальных атрибутов, атрибута ext3 можно опрашивать только

23:      если есть файловый дескриптор (имя файла не годится).

24:      Для выполнения запроса атрибутов ext3 нам не нужен доступ на запись,

25:      поэтому O_RDONLY подойдет. */

26:   fd = open(*filename, O_RDONLY);

27:   if (fd<0) {

28:    fprintf(stderr, "не открывается %s: %s\n", *filename,

29:     strerror(errno));

30:    return 1;

31:   }

32:

33:   /* Этот вызов получает атрибуты, и помещает их в flags */

34:   if (ioctl(fd, EXT3_IOC_GETFLAGS, &flags)) {

35:    fprintf(stderr, "ioctl завершился ошибкой на %s: %s\n", *filename,

36:     strerror(errno));

37:    return 1;

38:   }

39:

40:   printf("%s: ", *filename++);

41:

42:   /* Проверить каждый атрибут, и отобразить сообщение для каждого,

43:      который включен. */

44:   if (flags & EXT3_APPEND_FL) printf("Append");

45:   if (flags & EXT3_IMMUTABLE_FL) printf("Immutable");

46:   if (flags & EXT3_SYNC_FL) printf("Sync");

47:   if (flags & EXT3_NODUMP_FL) printf("Nodump");

48:

49:   printf("\n");

50:   close(fd);

51:  }

52:

53:  return 0;

54: }

Ниже приведена похожая программа, которая устанавливает расширенные атрибуты ext3 для указанного списка файлов. Первый параметр должен быть списком флагов, которые нужно установить. Каждый флаг представляется в списке в виде одной буквы: А — только для добавления (append only), I — неизменяемый (immutable), S — синхронизированный (sync), N — недампированный (nodump). Эта программа не модифицирует существующие флаги файла; она только устанавливает флаги, переданные в командной строке.

 1: /* setflags.c */

 2:

 3: /* Первый параметр этой программы — строка, состоящая из

 4:    0 (допускается пустая) или более букв из набора I, A, S,

 5:    N. Эта строка указывает, какие из атрибутов ext3 должны

 6:    быть включены для файлов, указанных в остальных

 7:    параметрах командной строки — остальные атрибуты выключаются

 8:    буквы обозначают соответственно: immutable, append-only, sync и nodump.

 9:

10:    Например, команда "setflags IN file1, file2" включает

11:    флаги immutable и nodump для файлов file1 и file2, но отключает

12:    флаги sync и append-only для этих файлов. */

13:

14: #include

15: #include

16: #include

17: #include

18: #include

19: #include

20: #include

21:

22: int main(int argc, const char **argv) {

23:  const char **filename = argv + 1;

24:  int fd;

25:  int flags = 0;

26:

27:  /* Убедиться, что указаны устанавливаемые флаги, вместе

28:     с именами файлов. Позволить установить "0", как признак

29:     того, что все флаги должны быть сброшены. */

30:  if (argc<3){

31:   fprintf(stderr, "Использование setflags: [0][I][A][S][N]"

32:    "\n");

33:   return 1;

34:  }

35:

36:  /* каждая буква представляет флаг; установить

37:     флаг, которые указаны */

38:  if (strchr(argv[1], 'I') ) flags |= EXT3_IMMUTABLE_FL;

39:  if (strchr(argv[1], 'A') ) flags |= EXT3_APPEND_FL;

40:  if (strchr(argv[1], 'S') ) flags |= EXT3_SYNC_FL;

41:  if (strchr(argv[1], 'N') ) flags |= EXT3_NODUMP_FL;

42:

43:  /* пройти по всем именам в argv[] */

44:  while (*(++filename)) {

45:   /* В отличие от нормальных атрибутов, атрибута ext3 можно опрашивать,

46:      только если есть файловый дескриптор (имя файла не годится).

47:      Для выполнения запроса атрибутов ext3 нам не нужен доступ на запись,

48:      поэтому O_RDONLY подойдет. */

49:   fd = open(*filename, O_RDONLY);

50:   if (fd < 0) {

51:    fprintf(stderr, "невозможно открыть %s:%s\n", *filename,

52:     strerror(errno));

53:    return 1;

54:   }

55:

56:   /* Установить атрибуты в соответствии с переданными

57:      флагами. */

58:   if (ioctl(fd, EXT3_IOC_SETFLAGS, &flags)) {

59:    fprintf(stderr, "Сбой ioctl в %s:%s\n", *filename,

60:     strerror(errno));

61:    return 1;

62:   }

63:   close(fd);

64:  }

65:

66:  return 0;

67: }

 

11.4. Манипулирование содержимым каталогов

 

Вспомните, что компоненты каталогов (имена файлов) — это ни что иное, как указатели на дисковые информационные узлы (on-disk inodes); почти вся важная информация, касающаяся файла, хранится в его inode. Вызов open() позволяет процессу создавать компоненты каталогов, которые являются обычными файлами, но для создания файлов других типов и для манипулирования компонентами каталогов могут понадобиться другие функции. Функции, которые позволяют создавать, удалять и выполнять поиск каталогов, описаны в главе 14; файлы сокетов — в главе 17. В настоящем разделе раскрываются символические ссылки, файлы устройств и FIFO.

 

11.4.1. Создание входных точек устройств и именованных каналов

Процессы создают файлы устройств и именованных каналов в файловой системе с помощью вызова mknod().

#include

#include

int mknod(const char *pathname, mode_t mode, dev_t dev);

pathname — это имя файла, который нужно создать, mode — это и режим доступа (который модифицируется текущим umask), и тип нового файла (S_IFIFO, S_IFBLK, S_IFCHR). Последний параметр, dev, содержит старший (major) и младший (minor) номера создаваемого устройства. Тип устройства (символьное или блочное) и старший номер устройства сообщают ядру, какой драйвер устройств отвечает за операции с этим файлом устройства. Младший номер используется внутри драйвером устройства, чтобы различать отдельные устройства среди многих, которыми он управляет. Только пользователю root разрешено создавать файлы устройств; именованные же каналы могут создавать все пользователи.

Заголовочный файл представляет три макроса для манипулирования значениями типа dev_t. Макрос makedev() принимает старшие номера в первом аргументе, младшие — во втором и возвращает значение dev_t, ожидаемое mknod(). Макросы major() и minor() принимают значение типа dev_t в качестве единственного аргумента и возвращают, соответственно, старший и младший номер устройства.

Программа mknod, доступная в Linux, предоставляет пользовательский интерфейс к системному вызову mknod() (подробности см. в man 1 mknod). Ниже приведена упрощенная реализация mknod для иллюстрации системного вызова mknod(). Следует отметить, что программа создает файл с режимом доступа 0666 (предоставляя право на чтение и запись всем пользователям) и зависит от системной установки umask процесса для получения прав доступа.

 1: /* mknod.с */

 2:

 3: /* Создать устройство или именованный канал, указанный в командной строке.

 4:    См. подробности о параметрах командной строки

 5:    на man-странице mknod(1). */

 6:

 7: #include

 8: #include

 9: #include

10: #include

11: #include

12: #include

13: #include

14:

15: void usage(void) {

16:  fprintf (stderr, "использование: mknod <путь> [b | с | u | p]"

17:   "<старший> <младший>\n");

18:  exit(1);

19: }

20:

21: int main(int argc, const char **argv) {

22:  int major = 0, minor = 0;

23:  const char *path;

24:  int mode = 0666;

25:  char *end;

26:  int args;

27:

28:  /* Всегда необходимы, как минимум, тип создаваемого inode

29:     и путь к нему. */

30:  if (argc < 3) usage();

31:

32:  path = argv[1];

33:

34:  /* второй аргумент указывает тип создаваемого узла */

35:  if (!strcmp(argv[2], "b")) {

36:   mode | = S_IFBLK;

37:   args = 5;

38:  } else if (!strcmp(argv[2] , "с") || !strcmp(argv[2], "u")) {

39:   mode |= S_IFCHR;

40:   args = 5;

41:  } else if(!strcmp(argv[2], "p")) {

42:   mode |= S_IFIFO;

43:   args = 3;

44:  } else {

45:   fprintf(stderr, "неизвестный тип узла %s\n", argv[2]);

46:   return 1;

47:  }

48:

49:  /* args сообщает, сколько аргументов ожидается, поскольку нам нужно

50:    больше информации при создания устройств, чем именованных каналов*/

51:  if (argc != args) usage();

52:

53:  if (args == 5) {

54:   /* получить старший и младший номера файла устройств,

55:      который нужно создать */

56:   major = strtol(argv[3], &end, 0);

57:   if (*end) {

58:    fprintf(stderr,"неверный старший номер %s\n", argv[3]);

59:    return 1;

60:   }

61:

62:   minor = strtol(argv[4], &end, 0);

63:   if (*end) {

64:    fprintf(stderr, "неверный младший номер %s\n", argv[4]);

65:    return 1;

66:   }

67:  }

68:

69:  /* если создается именованный канал, то финальный параметр

70:     игнорируется */

71:  if (mknod(path, mode, makedev(major, minor))) {

72:   fprintf(stderr, "вызов mknod не удался : %s\n", strerror(errno));

73:   return 1;

74:  }

75:

76:  return 0;

77: }

 

11.4.2. Создание жестких ссылок

Когда множество имен файлов в файловой системе ссылаются на единственный inode, такие файлы называют жесткими ссылками (hard links) на него. Все эти имена должны располагаться на одном физическом носителе (обычно это значит, что они должны быть на одном устройстве). Когда файл имеет множество жестких ссылок, все они равны — нет способа узнать, с каким именем первоначально был создан файл. Одно из преимуществ такой модели заключается в том, что удаление одной жесткой ссылки не удаляет файл с устройства — он остается до тех пор, пока все ссылки на него не будут удалены. Системный вызов link() связывает новое имя файла с существующим inode.

#include

int link(const char *origpath, const char *newpath);

Параметр origpath ссылается на существующее путевое имя, a newpath представляет собой путь для новой жесткой ссылки. Любой пользователь может создавать ссылку на файл, к которому у него есть доступ по чтению, до тех пор, пока он имеет право записи в каталоге, в котором ссылка создается, и право выполнения в каталоге, в котором находится origpath. Только пользователь root имеет право создавать жесткие ссылки на каталоги, но поступать так — обычно плохая идея, поскольку большинство файловых систем и некоторые утилиты не работают с ними достаточно хорошо — они полностью их отвергают.

 

11.4.3. Использование символических ссылок

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

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

• chown()

• lstat()

• readlink()

• rename()

• unlink()

Символически ссылки создаются почти так же, как жесткие, но при этом используется системный вызов symlink().

#include

int symlink(const char *origpath, const char *newpath);

Если вызов успешен, создается файл newpath как символическая ссылка, указывающая на oldpath (часто говорят, что newpath содержит в качестве своего значения oldpath).

Поиск значения символической ссылки немного сложнее.

#include

int readlink(const char *pathname, char *buf, size_t bufsiz);

Буфер, на который указывает buf, наполняется содержимым символической ссылки pathname до тех пор, пока хватает длины buf, указанной в bufsize в байтах. Обычно константы PATH_MAX применяется в качестве размера буфера, поскольку она должна быть достаточно большой, чтобы уместить содержимое любой символической ссылки. Одна странность функции readlink() связана с тем, что она не завершает строку, которую записывает в buf, символом '\0', поэтому buf не содержит корректную строку С, даже если readlink() выполняется успешно. Вместо этого она возвращает количество байт, записанных в buf в случае успеха и -1 — при неудаче. Из-за этой особенности код, использующий readlink(), часто выглядит так, как показано ниже.

char buf[PATH_MAX + 1];

int bytes;

if ( (bytes = readlink (pathname, buf, sizeof (buf) - 1)) < 0) {

 perror("ошибка в readlink");

} else {

 buf[bytes]= '\0';

}

 

11.4.4. Удаление файлов

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

#include

int unlink(char *pathname);

 

11.4.5. Переименование файлов

Имя файла может быть изменено на любое другое до тех пор, пока оба имени относятся к одному и тому же физическому носителю (это то же ограничение, что и касается создания жестких ссылок). Если новое имя уже ссылается на файл, то такое имя разъединяется перед тем, как произойдет переименование. Атомарность системного вызова rename() гарантируется. Другие процессы в системе всегда видят существование файла под тем или иным именем, но не под обеими сразу. Поскольку открытые файлы не связаны с именами (а только с inode), то переименование файла, который открыт в других процессах, никак не влияет на их работу. Ниже показано, как выглядит системный вызов для переименования файлов.

#include

int rename(const char *oldpath, const char *newpath);

После вызова файл, на который ссылалось имя oldpath, получает ссылку newpath вместо oldpath.

 

11.5. Манипуляции файловыми дескрипторами

 

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

#include

int fcntl (int fd, int command, long arg);

Для многих команд arg не используется. Ниже мы обсудим большую часть применений fcntl(). Этот вызов используется для блокировки файлов, аренды файлов, неблокирующего ввода-вывода, который рассматривается в главе 13, а также уведомления об изменениях каталогов, представленного в главе 14.

 

11.5.1. Изменение режима доступа к открытому файлу

Режим добавления (указываемый флагом O_APPEND при открытии файла) и неблокирующий режим (флаг O_NONBLOCK), могут быть включены и отключены уже после того, как файл был открыт, с помощью команды F_SETFL в fcntl(). Параметр arg при этом должен содержать флаги, которые нужно установить — если какой-то из флагов не указан для fd, он отключается.

F_GETFL можно использовать для запроса текущих установленных флагов файла. Это возвращает все флаги, включая режим чтения/записи для открытого файла. F_SETFL позволяет только устанавливать упомянутые выше флаги — любые другие флаги, представленные в аргументе arg, игнорируются.

fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_RDONLY);

Такой вызов абсолютно правильный, но он не делает ничего. Включение режима добавления для открытого файлового дескриптора выглядит так, как показано ниже.

fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_APPEND);

Следует отметить, что это предохраняет установку O_NONBLOCK. Отключение режима добавления выглядит похоже.

fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_APPEND);

 

11.5.2. Модификация флага "закрыть при выполнении"

Во время системного вызова exec() дескрипторы файлов обычно остаются открытыми для использования в новых программах. В некоторых случаях может потребоваться, чтобы файлы закрывались, когда вызывается exec(). Вместо закрытия их вручную вы можете попросить систему закрыть соответствующий файловый дескриптор при вызове exec() с помощью команд F_GETFD и F_SETFD в fcntl(). Если флаг "закрыть при выполнении" (close-on-exec) установлен, когда применяется F_GETFD, возвращается ненулевое значение, в противном случае возвращается ноль. Флаг "закрыть при выполнении" устанавливается командой F_SETFD; он отключается, если arg равно 0, в противном случае он включается.

Ниже показано, как можно заставить fd закрываться, когда процесс вызывает exec().

fcntl(fd, F_SETFD, 1);

 

11.5.3. Дублирование файловых дескрипторов

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

#include

int dup(int oldfd);

dup() возвращает файловый дескриптор, который ссылается на тот же inode, что и oldfd, или -1 в случае ошибки, oldfd остается корректным дескриптором, по-прежнему ссылающимся на исходный файл. Новый файловый дескриптор — это всегда наименьший доступный файловый дескриптор. Если процессу нужно получить новый файловый дескриптор с определенным значением (например, 0, чтобы перенаправить стандартный ввод), то он должен использовать dup2().

#include

int dup2(int oldfd, int newfd);

Если newfd ссылается на уже открытый дескриптор, то он закрывается. Если вызов завершен успешно, он возвращает новый файловый дескриптор и newfd ссылается на тот же файл, что oldfd. Системный вызов fcntl() представляет почти ту же функциональность командой F_DUPFD. Первый аргумент — fd — это уже открытый файловый дескриптор. Новый файловый дескриптор — это первый доступный дескриптор, равный или больший, чем значение последнего аргумента fcntl(). (В этом состоит отличие от работы dup2().) Вы можете реализовать dup2() через fcntl() следующим образом.

int dup2(int oldfd, int newfd) {

 close(newfd); /* ensure new fd is available */

 return fcntl(oldfd, F_DUPFD, newfd);

}

Создание двух файловых дескрипторов, ссылающихся на один и тот же файл — это не то же самое, что открытие файла дважды. Почти все атрибуты дублированных дескрипторов разделяются: они разделяют текущую позицию, режим доступа и блокировки. (Все это записывается в файловой структуре, которая создается при каждом открытии файла. Файловый дескриптор ссылается на файловую структуру, и дескриптор, возвращенный dup(), ссылается на одну и ту же структуру.) Единственный атрибут, который может независимо управляться в этих двух дескрипторах — это состояние "закрыть при выполнении". После того, как процесс вызывает fork(), то файлы, открытые в родительском процессе, наследуются дочерним, и эти пары файловых дескрипторов (один в родительском процессе, другой — в дочернем) ведут себя так, будто файловые дескрипторы были дублированы с текущей позицией и другими разделенными атрибутами.

 

11.6. Создание неименованных каналов

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

Оболочки применяют неименованные каналы для выполнения команд вроде ls | head. Процесс ls пишет в тот канал, из которого head читает свой ввод, выдавая ожидаемый пользователем результат.

Создание неименованного канала выполняется по двум файловым дескрипторам, один из которых доступен только для чтения, а второй — только для записи.

#include

int pipe(int fds[2]);

Единственный параметр-массив включает два файловых дескриптора — fd[0] для чтения и fd[1] для записи.

 

11.7. Добавление перенаправления для ladsh

 

Теперь, когда мы рассмотрели основные манипуляции с файлами, мы можем научить ladsh перенаправлению ввода и вывода через файлы и каналы. ladsh2.с, который мы представим здесь, работает с каналами (описанными символом | в командах ladsh, как это делается в большинстве командных оболочек) и перенаправление ввода и вывода в файловые дескрипторы. Мы покажем только модифицированные части кода здесь — полный исходный текст ladsh2.с доступен по упомянутым в начале книги адресам. Изменения в parseCommand() — это простое упражнение по разбору строк, поэтому мы не будем надоедать дискуссией об этом.

 

11.7.1. Структуры данных

Хотя код в ladsh1.с поддерживает концепцию задания как множества процессов (предположительно, объединенных вместе каналами), он не предоставляет способа указания того, какие файлы использовать для ввода и вывода. Чтобы позволить это, добавляются новые структуры данных и модифицируются существующие.

24:  REDIRECT_APPEND};

25:

26: struct redirectionSpecifier {

27:  enum redirectionTypetype; /* тип перенаправления */

28:  int fd; /*перенаправляемый файловый дескриптор*/

29:  char * filename; /* файл для перенаправления fd */

30: };

31:

32: struct childProgram {

33:  pid_t pid; /* 0 если завершен */

34:  char **argv; /* имя программы и аргументы */

35:  int numRedirections; /* элементы в массиве перенаправлений */

36:  struct redirectionSpecifier *redirections; /* перенаправления ввода-вывода*/

37: } ;

Структура struct redirectionSpecifier сообщает ladsh2.с о том, как установить отдельный файловый дескриптор. Она содержит enum redirectionTypetype, который указывает, является ли это перенаправление перенаправлением ввода, перенаправлением вывода, который должен быть добавлен к существующему файлу, либо перенаправлением вывода, которое заменяет существующий файл. Она также включает перенаправляемый файловый дескриптор и имя файла. Каждая дочерняя программа (struct childProgram) теперь специфицирует нужное ей количество перенаправлений.

Эти новые структуры данных не связаны с установкой каналов между процессами. Поскольку задание определено как множество дочерних процессов с каналами, связывающими их, нет необходимости в более подробной информации, описывающей каналы. На рис. 11.1 показано, как эти новые структуры должны выглядеть для команды tail < input-file | sort > output-file.

Рис. 11.1. Структуры данных, описывающие задание для ladsh2.с

 

11.7.2. Изменения в коде

Как только в parseCommand() будут правильно отражены структуры данных, то запуск команд в правильном порядке становится довольно простым при достаточном внимании к деталям. Прежде всего, мы добавляем цикл в parseCommand() для запуска дочерних процессов, поскольку теперь их может быть множество. Прежде чем войти в цикл, мы устанавливаем nextin и nextout, которые являются файловыми дескрипторами, используемыми в качестве стандартных потоков ввод и вывода для следующего запускаемого процесса. Для начала мы используем те же stdin и stdout, что и оболочка.

Теперь посмотрим, что происходит внутри цикла. Основная идея описана ниже.

1. Если это финальный процесс в задании, убедиться, что nextout указывает на stdout. В противном случае нужно подключить вывод этого задания к входному концу неименованного канала.

2. Породить новый процесс. Внутри дочернего перенаправить stdin и stdout, как указано с помощью nextin, nextout и всех специфицированных ранее перенаправлений.

3. Вернувшись обратно в родительский процесс, закрыть nextin и nextout, используемые только что запущенным дочерним процессом (если только они не являются потоками ввода и вывода самой оболочки).

4. Теперь настроить следующий процесс в задании для приема его ввода из вывода процесса, который мы только что создали (через nextin).

Вот как эти идеи перевести на С.

365: nextin=0, nextout=1;

366: for (i=0; i

367:  if ((i+1) < newJob.numProgs) {

368:   pipe(pipefds);

369:   nextout = pipefds[1];

370:   } else {

371:    nextout = 1;

372:   }

373:

374:   if (!(newJob.progs[i].pid = fork())) {

375:    if (nextin != 0) {

376:     dup2(nextin, 0);

377:     close(nextin);

378:    }

379:

380:    if (nextout != 1) {

381:     dup2(nextout, 1);

382:     close(nextout);

383:    }

384:

385:    /* явное перенаправление перекрывает каналы */

386:    setupRedirections(newJob.progs+i);

387:

388:    execvp(newJob.progs[i].argv[0], newJob.progs[i].argv);

389:    fprintf(stderr, "exec() of %s failed: %s\n",

390:     newJob.progs[i].argv[0],

391:    strerror(errno));

392:    exit(1);

393:   }

394:

395:   /* поместить наш дочерний процесс в группу процессов,

396:      чей лидер - первый процесс канала */

397:   setpgid(newJob.progs[i].pid, newJob.progs[0].pid);

398:

399:   if (nextin != 0) close(nextin);

400:   if (nextout != 1) close (nextout);

401:

402:   /* Если больше нет процессов, то nextin - мусор,

403:      но это не имеет значения */

404:   nextin = pipefds[0];

Единственный код, добавленный в ladsh2.с для обеспечения перенаправлений — это функция setupRedirections(), код которой останется неизменным во всех последующих версиях ladsh. Ее задача состоит в обработке спецификаторов struct redirectionSpecifier для дочерних заданий и соответствующей модификации дочерних файловых дескрипторов. Мы рекомендуем просмотреть код этой функции в приложении Б.

 

Глава 12

Обработка сигналов

 

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

Сигналы всегда были неотъемлемой частью Unix. Ядро использует их для извещения процессов о разнообразных событиях, включая перечисленные ниже.

• Уничтожение одного из дочерних процессов.

• Установка предупреждений устаревшим процессам.

• Изменение размеров окна терминала.

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

• Проигнорировать сигнал.

• Позволить ядру запустить специальную часть процесса, прежде чем продолжить выполнение основной его части (это называется перехватом сигнала).

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

Концептуально это довольно просто. Однако история развития средств работы с сигналами видна, когда вы сравните различные интерфейсы сигналов, которые поддерживаются различными реализациями Unix. BSD, System V и System 3 поддерживают различные и несовместимые программные интерфейсы сигналов. POSIX определил стандарт, теперь поддерживаемый почти всеми версиями Unix (включая Linux), который был тогда расширен для обработки новой семантики сигнала (вроде формирования очереди сигналов) как части определения сигнала в режиме реального времени POSIX (POSIX Real Time Signal). В этой главе обсуждается исходное выполнение сигналов Unix перед объяснением основ программного интерфейса POSIX и их расширений Real Time Signal, поскольку появление многих возможностей POSIX API было мотивировано недостатками в более ранних реализациях системы сигналов.

 

12.1. Концепция сигналов

 

12.1.1. Жизненный цикл сигнала

Сигналы имеют четко определенный жизненный цикл: они создаются, сохраняются до тех пор, пока ядро не выполнит определенное действие на основе сигнала, а затем вызывают совершение этого действия. Создание сигнала называют по-разному: поднятие (raising), генерация или посылка сигнала. Обычно процесс посылает сигнал другому процессу, в то время как ядро генерирует сигналы для отправки процессу. Когда процесс посылает сигнал самому себе, говорят, что он поднимает его. Однако эти термины используются не особо согласованно.

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

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

 

12.1.2. Простые сигналы

Изначально обработка сигналов была проста. Системный вызов signal() использовался для того, чтобы сообщить ядру, как доставить процессу определенный сигнал.

#include

void * signal(int signum, void *handler);

Здесь signum — это сигнал, который нужно обработать, a handler определяет действия, которое должно быть выполнено при доставке сигнала. Обычно handler — это указатель на функцию-обработчик сигнала, которая не принимает параметров и не возвращает значения. Когда сигнал доставлен процессу, ядро как можно скорее запускает функцию-обработчик. Когда функция возвращает управление, ядро возобновляет выполнение процесса с того места, где он был прерван. Системные инженеры распознают в этом механизме обработки сигналов аналог доставки аппаратных прерываний. Прерывания и сигналы очень похожи и у них возникают сходные проблемы.

Доступно множество номеров сигналов. В табл. 12.1 перечислены все сигналы, поддерживаемые в настоящее время Linux, за исключением сигналов реального времени. Они имеют символические имена, начинающиеся с SIG, и мы будем использовать SIG ЧТО-ТО , говоря о каком-то из них.

Параметр handler может иметь два специальных значения — SIG_IGN и SIG_DFL (оба определены в ). Если указано SIG_IGN, сигнал игнорируется, SIG_DFL сообщает ядру, что нужно выполнить действие по умолчанию, как правило, уничтожив процесс либо проигнорировав сигнал. Два сигнала — SIGKILL и SIGSTOP — не могут быть перехвачены. Ядро всегда выполняет действие по умолчанию для этих сигналов, соответственно, уничтожая процесс и приостанавливая его.

Функция signal() возвращает предыдущий обработчик сигнала (который мог быть SIG_IGN или SIG_DFL). Обработчики сигналов резервируются при создании новых процессов вызовом fork(), и все сигналы, которые установлены в SIG_IGN, игнорируются и после вызова exec(). Все не игнорируемые сигналы после exec() устанавливаются в SIG_DFL.

Все это выглядит достаточно простым, пока вы не спросите себя: что произойдет, если сигнал SIG ЧТО-ТО будет отправлен процессу, который уже исполняет обработчик сигнала для SIG ЧТО-ТО .

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

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

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

Первое решение этой проблемы было неудачным. Прежде чем вызывается обработчик сигнала, для программы значение этого обработчика устанавливалось в SIG_DFL, и ожидалось, что правильное его значение будет восстановлено немедленно, как только возможно. Хотя это упрощало написание обработчиков сигналов, но также делало невозможным для программиста обрабатывать сигналы надежным образом. Если два экземпляра одного и того же сигнала поступали быстро друг за другом, то ядро обрабатывало второй из них способом, принятым по умолчанию. Это означало, что сигнал, пришедший вторым, игнорировался (и пропадал навсегда), или же процесс прерывался. Эта реализация известна под названием ненадежных сигналов, потому что не позволяла написать обработчики, ведущие себя надежным, правильным образом.

К сожалению, именно такая модель сигналов используется в ANSI/ISO-стандарте С. Хотя программные интерфейсы надежных сигналов, в которых исправлен этот недостаток, уже широко распространены, стандартизация ненадежных сигналов в ANSI/ISO, видимо, останется навсегда.

 

12.1.3. Надежные сигналы

Реализация BSD для решения проблемы множества сигналов полагается на простое ожидание завершения работы каждого обработчика сигналов в процессе перед доставкой следующего. Это гарантирует то, что каждый сигнал будет рано или поздно обработан, а также исключает риск переполнения стека. Вспомним, что когда ядро удерживает сигнал для отложенной доставки, сигнал называется ожидающим (pending).

Однако если процессу отправлен сигнал SIG ЧТО-ТО , в то время, как SIG ЧТО -ТО уже находится в состоянии ожидания, то в этом случае процессу доставляется только первый из них. У процесса нет никакой возможности узнать, сколько раз один и тот же сигнал был отправлен ему, поскольку множество одинаковых сигналов подряд воспринимаются как один. Обычно это не представляет собой проблему. Поскольку сигнал не несет в себе никакой информации помимо собственно номера сигнала, двойная посылка сигнала за очень короткий отрезок времени может быть воспринята как одиночная, потому если программа примет сигнал только однажды, это не имеет особого значения. Это отличается от варианта с обработкой второго сигнала по умолчанию (что делается при ненадежной схеме обработки сигналов).

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

Хотя BSD представляет адаптированную версию модели сигналов POSIX, комитет по стандартизации POSIX упростил ее для системных вызовов, с тем чтобы модифицировать диспозицию групп сигналов, предлагая новые системные вызовы для оперирования наборами сигналов. Наборы сигналов представлены типом данных sigset_t, и для манипулирования ими предусмотрен набор макросов.

 

12.1.4. Сигналы и системные вызовы

Часто сигналы доставляются процессу, который находится состоянии ожидания наступления некоторого внешнего события. Например, текстовый редактор часто ожидает завершения read(), чтобы возвратить ввод терминала. Когда системный администратор посылает процессу сигнал SIGTERM (нормальный сигнал, посылаемый командой kill, позволяющий процессу завершиться чисто), то процесс может обработать его, как описано ниже.

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

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

3. Перехватить сигнал, установить флаг, обозначающий, что сигнал получен, и каким-то образом обеспечить выход из блокирующего системного вызова (в данном случае read()) с индикацией ошибки — в знак того, что произошло что-то необычное. Нормальный порядок выполнения затем должен проверить флаг и обработать его соответствующим образом.

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

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

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

Чтобы "упростить" ситуацию, 4.2BSD автоматически перезапускает такие системные вызовы (особенно read() и write()). Поэтому для большинства операций программы более не должны беспокоиться об EINTR, поскольку выполнение системных вызовов продолжится после того, как процесс обработает сигнал. В последних версиях Unix изменен перечень системных вызовов, которые автоматически перезапускаются, a 4.3BSD позволяет вам выбрать, какие системные вызовы перезапускать. Стандарт обработки сигналов POSIX не указывает, какое поведение должно применяться, но все популярные системы согласны в том, как обрабатывать этот случай. По умолчанию системные вызовы не перезапускаются, но для каждого сигнала процесс может установить флаг, который указывает, что система должна перезапускать системные вызовы, прерванные этим сигналом.

 

12.2. Программный интерфейс сигналов Linux и POSIX

 

12.2.1. Посылка сигналов

Посылка сигналов от одного процесса другому обычно осуществляется с помощью системного вызова kill(). Этот системный вызов подробно обсуждался в главе 10. Вариантом kill() является tkill(), который не предназначен для прямого использования в программах.

int tkill(pid_t pid, int signum);

Существуют два отличия между kill() и tkill(). Первое: pid должен быть положительным числом; tkill() не может использоваться для отправки сигналов группам процессов, как это может kill(). Другое отличие позволяет обработчикам сигналов определять, применялся ли вызов kill() или tkill() для генерации сигнала: подробности см. далее в главе.

Функция raise(), которая представляет собой способ генерации сигналов, указанный ANSI/ISO, использует системный вызов tkill() для генерации сигналов в системах Linux.

int raise(int signum);

Функция raise() посылает текущему процессу сигнал, указанный в signum.

 

12.2.2. Использование

sigset_t

Большинство функций сигналов POSIX принимают набор сигналов в качестве одного из своих параметров (или части одного из параметров). Тип данных sigset_t служит для представления набора сигналов и определен в . POSIX определяет пять функций для манипулирования наборами сигналов.

#include

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set, int signum);

int sigdelset(sigset_t *set, int signum);

int sigismember(const sigset_t *set, int signum);

int sigemptyset(sigset_t *set); Делает пустым набор сигналов, на который указывает set (никаких сигналов в set представлено не будет).
int sigfillset(sigset_t *set); Включает все доступные сигналы в set .
int sigaddset(sigset_t *set, int signum); Добавляет сигнал signum в набор set .
int sigdelset(sigset_t *set, int signum); Удаляет сигнал signum из набора set .
int sigismember(const sigset_t *set, int signum); Возвращает не 0, если сигнал signum содержится в set . В противном случае возвращает 0.

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

 

12.2.3. Перехват сигналов

Вместо использования функции signal() (чья семантика в процессе эволюции стала неправильной) POSIX-программы регистрируют обработчики сигналов с помощью sigaction().

#include

int sigaction(int signum, struct sigaction *act, struct sigaction *oact);

Этот системный вызов устанавливает обработчик сигнала signum, как определено с помощью act. Если oact не равен NULL, он принимает расположение обработчика перед вызовом sigaction(). Если act равен NULL, текущая установка обработчика остается неизменной, позволяя программе получить текущее расположение, не изменяя его. sigaction() возвращает 0 в случае успеха и ненулевое значение в случае ошибки. Ошибки случаются только если один или несколько параметров, переданных sigaction(), не верны.

Обработка сигнала ядром полностью описывается структурой struct sigaction.

#include

struct sigaction {

 __sighandler_t sa_handler;

 sigset_t sa_mask;

 int sa_flags;

};

sa_handler — это указатель на функцию со следующим прототипом:

void handler(int signum);

Здесь signum устанавливается равным номеру сигнала, который является причиной вызова функции, sa_handler может указывать на функцию этого типа либо быть равным SIG_IGN или SIG_DFL.

Программа также специфицирует набор сигналов, которые должны блокироваться во время функционирования обработчика сигнала. Если обработчик предназначен для обработки нескольких различных сигналов (что легко сделать благодаря параметру signum), это средство существенно для предотвращения возникновения условия состязаний. sa_mask — это набор сигналов, включающий все сигналы, которые должны блокироваться при вызове обработчика. Однако доставленный сигнал блокируется независимо от того, что содержит sa_mask — если вы не хотите, чтобы он блокировался, укажите это флагом sa_flags — членом структуры struct sigaction.

Член sa_flags позволяет процессу модифицировать различные поведения сигнала. Он содержит один или более флагов, объединенных битовой операцией "ИЛИ".

SA_NOCLDSTOP Обычно SIGCHLD генерируется, когда один из потомков процесса прерван или приостановлен (то есть всякий раз, когда wait4() должен вернуть информацию о состоянии процесса). Если флаг SA_NOCLDSTOP указан для сигнала SIGCHLD , то сигнал генерируется лишь в случае прерывания дочернего процесса; приостановка дочернего процесса не приводит к генерации каких-либо сигналов. SA_NOCLDSTOP не оказывает влияния ни на какой другой сигнал.
SA_NODEFER Когда вызывается обработчик сигнала, сигнал автоматически не блокируется. Применение этого флага приводит к ненадежным сигналам, и он должен использоваться только для эмуляции ненадежных сигналов в приложениях, зависящих от такого поведения. Это идентично флагу SA_NOMASK в System V.
SA_RESETHAND Когда присылается сигнал, обработчик сбрасывается в SIG_DFL. Этот флаг позволяет эмулировать функцию ANSI/ISO signal() в библиотеке пользовательского пространства. Идентично флагу SA_ONESHOT в System V.
SA_RESTART Когда сигнал посылается процессу во время выполнения медленного системного вызова, системный вызов перезапускается после возврата управления из обработчика. Если флаг не указан, то системный вызов в этом случае возвращает ошибку и устанавливает errno равным EINTR .

 

12.2.4. Манипулирование маской сигналов процесса

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

Пример немного прояснит эту проблему. Ниже показан простой обработчик SIGHUP, изменяющий значение строки, на которую указывает глобальная переменная someString.

void handleHup(int signum) {

 free(someString);

 someString = strdup("другая строка");

}

В реальных программах новое значение someString вероятно, будет читаться из внешнего источника (такого как FIFO), но некоторые концепции актуальны и так. Теперь предположим, что основная часть программы копирует строку (этот код аналогичен реализации strcpy(), хотя и не очень оптимизирован), когда поступает сигнал SIGHUP.

src = someString;

while(*src)

*dest++ = *src++;

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

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

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

#include

int sigprocmask(int what, sigset_t *set, sigset_t *oldest);

Первый параметр, what, описывает, как должна выполняться манипуляция. Если set равно NULL, то what игнорируется.

SIG_BLOCK Сигналы в set добавляются к текущей маске сигналов.
SIG_UNBLOCK Сигналы в set исключаются из текущей маски сигналов.
SIG_SETMASK Блокируются сигналы из набора set , остальные разблокируются.

Во всех трех случаях параметр oldset типа sigset_t указывает на исходную маску сигналов, если только он не равен NULL — в этом случае oldset игнорируется. Следующий вызов ищет текущую маску сигналов для запущенных процессов.

sigprocmask(SIG_BLOCK, NULL, ¤tSet);

Системный вызов sigprocmask позволяет исправить код, представленный выше, который мог вызвать состояние состязаний. Все, что потребуется сделать — это блокировать SIGHUP перед копированием строки и разблокировать после копирования. Следующее усовершенствование делает код более безопасным.

sigset_t hup;

sigemptyset(&hup);

sigaddset(&hup, SIGHUP);

sigprocmask(SIG_BLOCK, &hyp, NULL);

src = someString;

while(*src)

 *dest++ = *src++;

sigprocmask(SIG_UNBLOCK, &hup, NULL);

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

 

12.2.5. Нахождение набора ожидающих сигналов

Очень легко найти сигналы, находящиеся в состоянии ожидания (сигналы, которые должны быть доставлены, но в данный момент заблокированы).

#include

int sigpending(sigset_t *set);

Эта функция записывает по адресу, указанному set, набор сигналов, которые в данный момент находятся в состоянии ожидания.

 

12.2.6. Ожидание сигналов

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

#include

int pause(void);

Функция pause() не возвращает управления до тех пор, пока сигнал не будет доставлен процессу. Если зарегистрирован обработчик для этого сигнала, то он запускается до того, как pause() вернет управление, pause() всегда возвращает -1 и устанавливает errno равным EINTR.

Системный вызов sigsuspend() предлагает альтернативный метод ожидания вызова сигнала.

#include

int sigsuspend(const sigset_t *mask);

Как и pause(), sigsuspend() временно приостанавливает процесс до тех пор, пока не будет получен сигнал (и обработан связанным с ним обработчиком, если таковой предусмотрен), возвращая -1 и устанавливая errno в EINTR.

В отличие от pause(), sigsuspend() временно устанавливает маску сигналов процесса в значение, находящееся по адресу, указанному в mask, на период ожидания появления сигнала. Как только сигнал поступает, маска сигналов восстанавливается в то значение, которое она имела до вызова sigsuspend(). Это позволяет процессу ожидать появления определенного сигнала за счет блокирования всех остальных сигналов.

 

12.3. Доступные сигналы

 

Linux предоставляет в распоряжение процессов сравнительно немного сигналов, и все они собраны в табл. 12.1.

Таблица 12.1. Сигналы

Сигнал Описание Действие по умолчанию
SIGABRT Доставляется вызовом abort() . Прервать, сбросить дамп
SIGALRM Истек срок действия alarm() . Прервать
SIGBUS Ошибка, зависящая от оборудования. Прервать, сбросить дамп
SIGCHLD Дочерний процесс прерван. Игнорировать
SIGCONT Выполнение процесса продолжается после приостановки. Игнорировать
SIGFPE Арифметическая ошибка. Прервать, сбросить дамп
SIGHUP Закрыт процесс, управляющий терминалом. Прервать
SIGILL Обнаружена недопустимая инструкция. Прервать
SIGINT Пользователь послал символ прерывания (^C). Прервать
SIGIO Принят асинхронный ввод-вывод. Прервать
SIGKILL Не перехватываемое прерывание процесса. Прервать
SIGPIPE Процесс пишет в канал при отсутствии читателя. Прервать
SIGPROF Закончился сегмент профилирования. Прервать
SIGPWR Обнаружен сбой питания. Прервать
SIGQUIT Пользователь послал символ выхода (^\). Прервать, сбросить дамп
SIGSEGV Нарушение памяти. Прервать, сбросить дамп
SIGSTOP Приостановка процесса без его прерывания. Процесс приостановить
SIGSYS Неверный системный вызов. Прервать, сбросить дамп
SIGTERM Перехватываемый запрос на прерывание процесса. Прервать
SIGTRAP Получена инструкция точки прерывания. Прервать, сбросить дамп
SIGTSTP Пользователь послал символ приостановки (^Z). Процесс приостановить
SIGTTIN Фоновый процесс читает с управляющего терминала. Процесс приостановить
SIGTTOU Фоновый процесс пишет на управляющий терминал. Процесс приостановить
SIGURG Условие срочного ввода-вывода. Игнорировать
SIGUSR1 Определяемый процессом сигнал. Прервать
SIGUSR2 Определяемый процессом сигнал. Прервать
SIGVTALRM Таймер, установленный с помощью setitimer() , устарел. Прервать
SIGWINCH Размер управляющего терминала изменился. Игнорировать
SIGXCPU Достигнуто ограничение ресурсов центрального процессора. Прервать, сбросить дамп
SIGXFSZ Достигнуто ограничение размера файла. Прервать, сбросить дамп

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

SIGABRT Функция abort() посылает сигнал процессу, который ее вызвал, прерывая процесс со сбросом файла дампа ядра. Под Linux библиотека С вызывает abort() , когда происходит сбой утверждения (assertion). Примечание . Утверждения описаны в книгах по С начального уровня, например, [15].
SIGALRM Вызывается, когда предупреждение, установленное alarm() , устаревает. Предупреждения (alarms) — это основа функции sleep() , описанной в главе 18.
SIGBUS Когда процесс нарушает ограничения, накладываемые оборудованием, но не связанные с защитой памяти, посылается этот сигнал. Обычно это случается на традиционных платформах Unix, когда выполняется попытка "невыровненного" доступа, но ядро Linux исправляет такие попытки и продолжает выполнять процесс. Выравнивание памяти обсуждается в главе 7.
SIGCHLD Этот сигнал посылается процессу, когда один из его дочерних процессов устаревает или остановлен. Это позволяет процессу избежать появления "зомби" за счет вызова одной из функций wait() из обработчика сигнала. Если родитель всегда ожидает завершения дочерних процессов, прежде чем продолжить работу, этот сигнал может быть проигнорирован. Это отличается от сигнала SIGCHLD , представленного в ранних версиях System V. SIGCHLD устарел и более не должен применяться.
SIGCONT Этот сигнал перезапускает приостановленный процесс. Также он может быть вызван процессом, позволяющим выполнить действие после перезапуска. Большинство редакторов перехватывают этот сигнал и обновляют терминал после перезапуска. В главе 15 дана более подробная информация об останове и перезапуске процесса.
SIGFPE Этот сигнал посылается, когда процесс вызывает арифметическое исключение. Все исключения плавающей точки, такие как переполнение и потеря значимости, вызывают этот сигнал, как это происходит при делении на 0.
SIGHUP Когда терминал отсоединяется, лидер сеанса, ассоциированного с терминалом, получает этот сигнал, если только на терминале не выставлен флаг CLOCAL . Если лидер сеанса завершается, SIGHUP отправляется лидеру каждой группы процессов в данном сеансе. Большинство процессов прерываются при получении SIGHUP , поскольку это значит, что пользователя уже нет в системе. Многие процессы-демоны интерпретируют SIGHUP как запрос на закрытие и повторное открытие журнальных файлов, а также на перечитывание конфигурационных файлов.
SIGILL Процесс пытается запустить некорректную аппаратную команду.
SIGINT Этот сигнал посылается всем процессам в группе процессов переднего плана, когда пользователь нажимает клавиатурную комбинацию прерывания (обычно ^C).
SIGIO Произошло асинхронное событие ввода-вывода. Асинхронный ввод-вывод редко используется и в этой книге не описан. По вопросам асинхронного ввода-вывода обращайтесь к соответствующим источникам, например, [35].
SIGKILL Этот сигнал генерируется только вызовом kill() и разрешает пользователю безусловно прервать процесс.
SIGPIPE Процесс выполнил запись в канал, который не имеет читателя.
SIGPROF Завершилось действие таймера профилирования. Это сигнал обычно используется профилировщиками, которые проверяют другие характеристики процесса времени выполнения. Профилировщики обычно используются для оптимизации времени выполнения программ, помогая программистам находить узкие места. Простейшим профилировщиком является утилита gprof , входящая в состав всех дистрибутивов Linux.
SIGPWR Система обнаружила надвигающуюся потерю питания. Обычно этот сигнал отправляется процессу init демоном, отслеживающим источники питания машины, позволяя корректно завершить работу до отключения питания.
SIGQUIT Этот сигнал посылается всем процессам в группе процессов переднего плана, когда пользователь нажимает клавиатурную комбинацию завершения (обычно ^/).
SIGSEGV Этот сигнал посылается, когда процесс пытается прочитать неотображаемую память, выполнить страницу памяти, которая не была отображена с привилегиями на выполнение, или же выполнить запись в память, к которой не имеет прав доступа на запись.
SIGSTOP Этот сигнал генерируется только вызовом kill() , и дает возможность пользователю безусловно остановить процесс. Более подробно о приостановке процессов можно почитать в главе 15.
SIGSYS Когда программа пытается выполнить несуществующий системный вызов, ядро прерывает программу с помощью этого сигнала. Это никогда не должно происходить в программах, которые осуществляют системные вызовы посредством системой библиотеки С.
SIGTERM Этот сигнал генерируется только вызовом kill() и дает возможность пользователю элегантно прервать процесс. Процесс должен прекратиться насколько возможно быстро, немедленно после получения сигнала.
SIGTRAP Когда программа проходит через точку прерывания, этот сигнал посылается процессу. Обычно он перехватывается процессом отладчика, который установил точку прерывания.
SIGTSTP Этот сигнал посылается всем процессам в группе процессов переднего плана, когда пользователь нажимает клавиатурную комбинацию прерывания (обычно ^Z).
SIGTTIN Этот сигнал посылается фоновому процессу, который пытается осуществить чтение из контролируемого им терминала. Об управлении заданиями подробнее читайте в главе 15.
SIGTTOU Этот сигнал посылается фоновому процессу, который пытается осуществить запись на контролируемый им терминал. Об управлении заданиями подробнее читайте в главе 15.
SIGURG Этот сигнал посылается, когда по сокету принимается экстренное сообщение. Экстренные данные — тема, касающаяся сетевых технологий, которая выходит за рамки освещаемых в настоящей книге. В [33] это рассматривается более подробно.
SIGUSR1 Для этого сигнала нет предопределенного назначения; процессы могут использовать его для собственных нужд.
SIGUSR2 Для этого сигнала нет предопределенного назначения; процессы могут использовать его для собственных нужд.
SIGVTALRM Отправляется, когда истекает период действия таймера, установленного вызовом settimer() . Информацию о применении таймеров можно найти в главе 18.
SIGWINCH Когда окно терминала изменяет размер, например, когда пользователь растягивает окно xterm , все процессы в группе процессов переднего плана получают этот сигнал. В главе 16 представлена информация об определении текущего размера управляющего терминала.
SIGXCPU Процесс превысил свой мягкий лимит использования ресурсов процессора. Этот сигнал посылается раз в секунду до тех пор, пока данный процесс не превысит жесткий лимит использования ресурсов процессора. Как только это произойдет, процесс прерывается сигналом SIGKILL . Информацию о лимитах ресурса процессора можно найти в главе 10.
SIGXFSZ Когда программа превышает лимит максимального размера файла, ей посылается этот сигнал, что обычно уничтожает процесс. Если сигнал перехвачен, то системный вызов, который послужил причиной превышения лимита на размер файла, возвращает ошибку EFBIG . Информацию о лимитах ресурса процессора можно найти в главе 10.

 

12.3.1. Описание сигналов

Иногда приложения нуждаются в описании сигнала для отображения пользователю или помещения в журнал. Существуют три способа сделать это (см. главу 9). К сожалению, ни один из них не стандартизован.

Самый старый метод предусматривает применение sys_siglist — массива строк, описывающих каждый сигнал, проиндексированного номерами самих сигналов. Он включает описания всех сигналов за исключением сигналов реального времени. Применение sys_siglist более переносимо, чем прочие методы, описанные ниже. В системах BSD предусмотрена функция psignal(), которая является сокращенным способом отображения сообщений. Вот как выглядит версия psignal().

#include

#include

void psignal(int signum, const char *msg) {

 printf("%s: %s\n", msg, sys_siglist[signum]);

}

Следует отметить, что эта функция использует тот же список сигналов, что и sys_siglist, поэтому сигналы реального времени также исключаются.

Библиотека GNU С, используемая Linux, предлагает еще один метод — strsignal(). Эта функция не входит ни в какой стандарт, поэтому для доступа к файлу прототипа нужно определить _GNU_SOURCE.

#define _GNU_SOURCE

#include

char *strsignal(int signum);

Подобно sys_siglist, strsignal() также представляет описание сигнала по номеру signum. Он использует sys_siglist для большинства сигналов и конструирует описания для сигналов реального времени. Например, SIGRTMIN + 5 будет описан как "Real-time signal 5". Пример использования strsignal() можно найти в строках 639–648 и 717 файла ladsh4.с, приведенного в приложении Б.

 

12.4. Написание обработчиков сигналов

Хотя обработчик сигнала выглядит подобно обычной функции С, он не вызывается так, как она. Вместо того чтобы быть частью нормальной последовательности вызовов программы, обработчик вызывается ядром. Ключевое различие между этими двумя вещами заключается в том, что обработчик может быть вызван почти в любое время, даже во время выполнения отдельного оператора С! Есть только несколько ограничений того, когда система может вызвать обработчик сигнала, на который вы полагаетесь.

1. Семантика некоторых сигналов ограничивает, когда они могут быть посланы. Так, например, SIGCHLD обычно посылается программам, у которых нет дочерних процессов. Большинство сигналов, подобных SIGHUP, посылаются в непредсказуемые моменты.

2. Если процесс находится в процессе обработки некоторого сигнала, то обработчик сигнала не вызывается повторно для обработки того же сигнала, если только не была задана опция SA_NODEFER. Процесс также может блокировать дополнительные сигналы, если сигнал, который обрабатывается, указан в члене sa_mask структуры struct sigaction.

3. Процесс может блокировать сигналы, когда выполняется часть кода, используя sigprocmask(). Ранее в этой главе был дан пример использования этого средства для обеспечения атомарного обновления структур данных.

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

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

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

Если вам требуется доступ к глобальным данным из обработчика сигналов (что и делает большинство обработчиков), оставляйте структуры данных простыми. Хотя достаточно просто безопасно модифицировать отдельный элемент данных, такой как int, более сложные структуры обычно требуют блокировки сигналов. Любые глобальные переменные, которые могут быть модифицированы обработчиками сигналов, должны быть объявлены с ключевым словом volatile. Это сообщает компилятору, что переменная может быть изменена вне нормального потока программы, и он не должен пытаться оптимизировать доступ к этой переменной.

Другая вещь, с которой нужно соблюдать осторожность в обработчиках сигналов — это вызов других функций, потому что они тоже могут изменять глобальные данные! Библиотека С stdio пытается облегчить это и не допускает вызовов своих функций из обработчиков сигналов. В табл. 12.2 перечислены функции, которые гарантированно являются безопасными для вызова из обработчиков сигналов; вызовов всех прочих функций следует избегать.

Таблица 12.2. Реентерабельные функции

abort() accept() access()
aio_error() aio_return() aio_suspend()
alarm() bind() cfgetispeed()
cfgetospeed() cfsetispeed() cfsetospeed()
chdir() chmod() chown()
close() connect() creat()
dup() dup2() execle()
execve() _exit() fchmod()
fchown() fcntl() fdatasync()
fork() fpathconf() fstat()
fsync() getegid() geteuid()
getgid() getgroups() getpeername()
getpgrp() getpid() getppid()
getuid() kill() link()
listen() lseek() lstat()
mkdir() mkfifo() open()
pathconf() pause() pipe()
poll() posix_trace_event() pselect()
raise() read() readlink()
recv() recvfrom() recvmsg()
rename() rmdir() select()
sem_post() send() sendmsg()
sendto() setgid() setpgid()
setsid() setsockopt() setuid()
shutdown() sigaction() sigaddset()
sigdelset() sigemptyset() sigfillset()
sigismember() signal() sigpause()
sigpending() sigprocmask() sigqueue()
sigset() sigsuspend() sleep()
socket() socketpair() stat()
symlink() sysconf() tcdrain()
tcflow() tcflush() tcgetattr()
tcgetpgrp() tcsendbreak() tcsetattr()
tcsetpgrp() time() timer_getoverrun()
timer_gettime() timer_settime() times()
umask() uname() unlink()
utime() wait() wait3()
wait4() waitpid() write()

 

12.5. Повторное открытие журнальных файлов

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

dd /var/log

mv messages messages.old

killall -HUP syslogd

Logrotate (ftp://ftp.redhat.com/pub/redhat/code/logrotate/) — одна из программ, которая использует преимущество такого метода для выполнения безопасной ротации журналов.

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

Затем обработчик сигнала SIGHUP в своем вызове устанавливает эту переменную, и главная часть программы проверяет эту переменную насколько можно часто. Ниже приведен пример соответствующей программы.

 1: /*sighup.c*/

 2:

 3: #include

 4: #include

 5: #include

 6: #include

 7: #include

 8:

 9: volatile int reopenLog = 0; /* volatile - поскольку модифицируется

10:                                обработчиком сигнала */

11:

12: /* записать строку в журнал */

13: void logstring(int logfd, char *str) {

14:  write(logfd, str, strlen(str));

15: }

16:

17: /* когда приходит SIGHUP, сделать запись об этом и продолжить */

18: void hupHandler(int signum) {

19:  reopenLog = 1;

20: }

21:

22: int main() {

23:  int done = 0;

24:  struct sigaction sa;

25:  int rc;

26:  int logfd;

27:

28:  logfd = STDOUT_FILENO;

29:

30:  /* Установить обработчик сигнала SIGHUP. Использовать memset() для

31:     инициализации структуры sigaction чтобы обеспечить очистку

32:     всего. */

33:  memset(&sa, 0, sizeof(sa));

34:  sa.sa_handler = hupHandler;

35:

36:  if (sigaction(SIGHUP, &sa, NULL)) perror("sigaction");

37:

38:  /* Записывать сообщение в журнал каждые две секунды, и

39:     повторно открывать журнальный файл по требованию SIGHUP */

40:  while (!done) {

41:   /*sleep() возвращает не ноль, если не спит достаточно долго*/

42:   rc = sleep(2);

43:   if (rc) {

44:    if (reopenLog) {

45:     logstring(logfd,

46:      "* повторное открытие журналов по запросу SIGHUP\n");

47:     reopenLog = 0;

48:    } else {

49:     logstring(logfd,

50:      "* sleep прервано неизвестным сигналом "

51:      "--dying\n");

52:     done=1;

53:    }

54:   } else {

55:    logstring(logfd, "Периодическое сообщение\n");

56:   }

57:  }

58:

59:  return 0;

60: }

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

 

12.6. Сигналы реального времени

 

Учитывая некоторые ограничения модели сигналов POSIX, например, недостающую возможность присоединения к сигналам никаких данных и вероятность того, что множество сигналов сольются в одной доставке, было разработано расширение POSIX Real Time Signals (сигналы реального времени POSIX). Системы, которые поддерживают сигналы реального времени, включая Linux, также поддерживают описанный ранее традиционный механизм сигналов POSIX. Для обеспечения наивысшего уровня переносимости между системами, мы советуем использовать стандартные интерфейсы POSIX, если только не возникает необходимости в некоторых дополнительных средствах, предоставляемых расширением реального времени.

 

12.6.1. Очередность и порядок сигналов

Два из ограничений стандартной модели сигналов POSIX заключаются в том, что когда сигнал перебивает сигнал, это не приводит к множественной доставке этих сигналов, и отсутствуют гарантии упорядоченной доставки множества разнородных сигналов (если вы пошлете SIGTERM, а следом SIGKILL, то нет способа узнать, какой из них придет первым). Расширение POSIX Real Time Signals добавляет новый набор сигналов, которые не подпадают под упомянутые ограничения.

Существует множество доступных сигналов реального времени, и они не используются ядром ни для каких предопределенных целей. Все сигналы между SIGRTMIN и SIGRTMAX являются сигналами реального времени, хотя точные номера их в POSIX не специфицированы (на момент написания этой книги Linux предоставляет 32 таких сигнала, но в будущем их количество может увеличиться).

Сигналы реального времени всегда ставятся в очередь; каждый такой сигнал, посланный приложению, доставляется ему (если только приложение не прервано перед тем, как такой сигнал будет доставлен). Упорядочение сигналов реального времени также хорошо определено. Сигналы с меньшими номерами всегда доставляются перед сигналами с большими номерами, и когда множество сигналов с одинаковым номером поставлены в очередь, то они доставляются в порядке постановки. Порядок доставки сигналов, не относящихся к расширению реального времени, не определен, как и порядок доставки смеси сигналов реального времени и не относящихся к ним.

Ниже показан пример кода, иллюстрирующий постановку сигналов в очередь и их упорядочивание.

 1: /* queued.с */

 2:

 3: /* получить определение strsignal() из string.h */

 4: #define _GNU_SOURCE1

 5:

 6: #include

 7: #include

 8: #include

 9: #include

10: #include

11:

12: /* Глобальные переменные для построения списка сигналов */

13: int nextSig = 0;

14: int sigOrder[10];

15:

16: /* Перехватить сигнал и записать, что он был обработан */

17: void handler(int signo) {

18:  sigOrder[nextSig++] = signo;

19: }

20:

21: int main() {

22:  sigset_t mask;

23:  sigset_t oldMask;

24:  struct sigaction act;

25:  int i;

26:

27:  /* Обрабатываемые в программе сигналы */

28:  sigemptyset(&mask);

29:  sigaddset(&mask, SIGRTMIN);

30:  sigaddset(&mask, SIGRTMIN+1);

31:  sigaddset(&mask, SIGUSR1);

32:

33:  /* Отправить сигнал handler() и сохранять их блокированными,

34:     чтобы handler() был сконфигурирован во избежание

35:     состязаний при манипулировании глобальными переменными */

36:  act.sa_handler = handler;

37:  act.sa_mask = mask;

38:  act.sa_flags = 0;

39:

40:  sigaction(SIGRTMIN, &act, NULL);

41:  sigaction(SIGRTMIN+1, &act, NULL);

42:  sigaction(SIGUSR1, &act, NULL);

43:

44:  /* Блокировать сигналы, с которыми мы работаем, чтобы

45:     была видна очередность и порядок */

46:  sigprocmask(SIG_BLOCK, &mask, &oldMask);

47:

48:  /* Генерировать сигналы */

49:  raise(SIGRTMIN+1);

50:  raise(SIGRTMIN);

51:  raise(SIGRTMIN);

52:  raise(SIGRTMIN+1);

53:  raise(SIGRTMIN);

54:  raise(SIGUSR1);

55:  raise(SIGUSR1);

56:

57:  /* Разрешить доставку этих сигналов. Все они будут доставлены

58:     прямо перед возвратом этого вызова (для Linux; это

59:     НЕПЕРЕНОСИМОЕ поведение). */

60:  sigprocmask(SIG_SETMASK, &oldMask, NULL);

61:

62:  /* Отобразить упорядоченный список перехваченных сигналов */

63:  printf("Принятые сигналы:\n");

64:  for (i = 0; i < nextSig; i++)

65:   if (sigOrder[i] < SIGRTMIN)

66:    printf("\t%s\n", strsignal(sigOrder[i]));

67:   else

68:    printf("\tSIGRTMIN + %d\n", sigOrder[i] - SIGRTMIN);

69:

70:  return 0;

71: }

Эта программа посылает себе некоторое количество сигналов и выводит на дисплей порядок их получения. Когда сигналы отправляются, она блокирует их, чтобы предотвратить немедленную доставку. Также она блокирует сигналы всякий раз, когда вызывается обработчик, устанавливая значение члена sa_mask структуры struct sigaction при настройке обработчика для каждого сигнала. Это предотвращает возможное состояние состязаний при обращении к глобальным переменным nextSig и sigOrder изнутри обработчика.

Запуск этой программы выдаст показанные ниже результаты.

Принятые сигналы:

        User defined signal1

        SIGRTMIN + 0

        SIGRTMIN + 0

        SIGRTMIN + 0

        SIGRTMIN + 1

        SIGRTMIN + 1

Это показывает, что все сигналы реального времени были доставлены, в то же время, был доставлен только один экземпляр сигнала SIGUSR1. Вы также видите изменение порядка сигналов реального времени — все сигналы SIGRTMIN были доставлены перед SIGRTMIN + 1.

 

12.7. Дополнительные сведения о сигналах

 

Сигналы, которые мы обсуждали до сих пор, не несли в себе никаких данных; появление сигнала — это единственная информация, которую получает приложение. В некоторых случаях было бы неплохо знать, что послужило причиной отправки сигнала (как, например, неправильная адресация памяти, генерирующая SIGSEGV), или же иметь возможность включить данные в сигналы, генерируемые приложением. Расширение реального времени Real Time Signals позволяет решить обе эти задачи.

 

12.7.1. Получение контекста сигнала

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

void handler(int signum, siginfo_t *siginfo, void *context);

Приложение должно указать ядру на необходимость передачи полной информации о контексте, устанавливая флаг SA_SIGINFO члена sa_mask структуры struct sigaction, применяемой для регистрации обработчика сигнала. Член sa_handler также не используется, потому что он является указателем на функцию с другим прототипом. Вместо этого новый член, sa_sigaction, указывает на обработчик сигнала с правильным прототипом. Чтобы снизить потребление памяти, sa_handler и sa_sigaction разрешено использовать один и тот же участок памяти, поэтому только один из двух должен применяться в одно и то же время. Чтобы сделать это прозрачным, библиотека С определяет struct sigaction следующим образом.

#include

struct sigaction {

 union {

  __sighandler_t sa_handler;

  __sigaction_t sa_sigaction;

 } __sigaction_handler;

 sigset_t sa_mask;

 unsigned long sa_flags;

};

#define sa_handler __sigaction_handler.sa_handler

#define sa_sigaction __sigaction_handler.sa_sigaction

Использование представленной комбинации объединений и макросов позволяет этим двум членам разделять одну и ту же память без необходимости усложнения с точки зрения приложений.

Структура siginfo_t содержит информацию о том, где и почему был сгенерирован сигнал. Всем сигналам доступны два члена: sa_signo и si_code. Какие другие члены доступны — зависит от конкретного сигнала, и эти члены разделяют память подобно тому, как это делают члены sa_handler и sa_sigaction структуры struct sigaction. Член sa_signo содержит номер доставленного сигнала и всегда равен значению первого параметра, переданного обработчику сигнала, в то время как si_code указывает, почему сигнал был сгенерирован, и изменяется в зависимости от номера сигнала. Для большинства сигналов он может принимать перечисленные ниже значения.

SI_USER

Приложение пространства пользователя вызвало kill() для отправки сигнала. Примечание. Функция sigsend(), включенная в Linux для совместимости с некоторыми системами Unix, также выдает SI_USER.

SI_QUEUE

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

SI_TKILL

Приложение пространства пользователя вызвало tkill(). В то время как ядро Linux использует SI_TKILL, его значение не специфицировано в текущей версии библиотеки С.

Если вам нужно проверить SI_TKILL, используйте следующий сегмент кода для определения этого значения:

#ifndef SI_TKILL

#define SI_TKILL -6

#endif

SI_TKILL не специфицирован ни в каком стандарте (хотя допускается ими), поэтому его следует применять осторожно в переносимых программах.

SI_KERNEL

Сигнал сгенерирован ядром.

Когда SIGILL, SIGFPE, SIGSEGV, SIGBUS и SIGCHLD посылаются ядром, то si_code вместо si_kernel принимает значения, перечисленные в табл. 12.3.

Таблица 12.3. Значения si_code для специальных сигналов

Сигнал si_code Описание
SIGILL ILL_ILLOPC Неправильный код операции (opcode).
ILL_ILLOPC Неправильный операнд.
ILL_ILLOPC Неправильный режим адресации.
ILL_ILLOPC Неправильная ловушка (trap).
ILL_ILLOPC Привилегированный код операции.
ILL_ILLOPC Привилегированный регистр.
ILL_ILLOPC Внутренняя ошибка стека.
ILL_ILLOPC Ошибка сопроцессора.
SIGFPE FPE_INTDIV Деление целого на ноль.
FPE_INTOVF Переполнение целого.
FPE_FLTDIV Деление числа с плавающей точкой на ноль.
FPE_FLTOVF Переполнение числа с плавающей точкой.
FPE_FLTUND Потеря значимости числа с плавающей точкой.
FPE_FLTRES Неточный результат числа с плавающей точкой.
FPE_FLTINV Неверная операция с плавающей точкой.
FPE_FLTSUB Число с плавающей точкой вне диапазона.
SIGSEGV SEGV_MAPPER Адрес не отображается на объект.
SEGV_ACCERR Неверные права доступа для адреса.
SIGBUS BUS_ADRALN Неверное выравнивание адреса.
BUS_ADRERR Несуществующий физический адрес.
BUS_OBJERR Специфичный для объекта сбой оборудования.
SIGCHLD CLD_EXITED Дочерний процесс завершен.
CLD_KILLED Дочерний процесс уничтожен.
CLD_DUMPED Дочерний процесс уничтожен с выводом дампа памяти в файл.
CLD_TRAPPED Дочерний процесс достиг точки останова.
CLD_STOPPED Дочерний процесс приостановлен.

Чтобы помочь прояснить разные значения, которые может принимать si_code, рассмотрим пример, в котором SIGCHLD генерируется четырьмя разными способами: kill(), sigqueue(), raise() (использует системный вызов tkill()) и созданием дочернего процесса, который немедленно прерывается.

 1: /* sicode.с */

 2:

 3: #include

 4: #include

 5: #include

 6: #include

 7:

 8: #ifndef SI_TKILL

 9: #define SI_TKILL -6

10: #endif

11:

12: void handler(int signo, siginfo_t *info, void *f ) {

13:  static int count = 0;

14:

15:  printf("перехвачен сигнал, отправленный ");

16:  switch(info->si_code) {

17:  case SI_USER:

18:   printf("kill()\n"); break;

19:  case SI_QUEUE:

20:   printf("sigqueue()\n"); break;

21:  case SI_TKILL:

22:   printf("tkill() или raise()\n"); break;

23:  case CLD_EXITED:

24:   printf ("ядро сообщает, что дочерний процесс завершен\n"); exit(0);

25:  }

26:

27:  if (++count == 4) exit(1);

28: }

29:

30: int main() {

31:  struct sigaction act;

32:  union sigval val;

33:  pid_t pid = getpid();

34:

35:  val.sival_int = 1234;

36:

37:  act.sa_sigaction = handler;

38:  sigemptyset(&act.sa_mask);

39:  act.sa_flags = SA_SIGINFO;

40:  sigaction(SIGCHLD, &act, NULL);

41:

42:  kill(pid, SIGCHLD);

43:  sigqueue(pid, SIGCHLD, val);

44:  raise(SIGCHLD);

45:

46:  /* Чтобы получить SIGCHLD от ядра, мы создаем дочерний процесс

47:     и немедленно завершаем его. Обработчик сигнала выйдет после

48:     получения сигнала от ядра, поэтому мы просто засыпаем

49:     на время и позволяем программе прерваться подобным образом. */

50:

51:  if (!fork()) exit(0);

52:  sleep(60);

53:

54:  return 0;

55: }

Если si_code равно SI_USER, SI_QUEUE или SI_TKILL, то доступны два дополнительных члена siginfo_t: si_pid и si_uid, которые представляют идентификатор процесса, пославшего сигнал и действительный идентификатор пользователя этого процесса.

Когда ядром посылается SIGCHLD, доступны члены si_pid, si_status, si_utime и si_stime. Первый из них, si_pid, задает идентификатор процесса, состояние которого изменилось. Информация о новом состоянии доступна как в si_code (как показано в табл. 12.3) и в si_status, что идентично целому значению состояния, возвращаемому семейством функций wait().

Последние два члена, si_utime и si_stime, определяют период времени, которое потрачено дочерним приложением на работу в пользовательском режиме и в режиме ядра, соответственно (это подобно тому, что возвращают вызовы wait3() и wait4() в структуре struct rusage). Это время измеряется в тиках часов, заданных целым числом. Количество тиков в секунду задает макрос _SC_CLK_TCK, определенный в .

SIGSEGV, SIGBUS, SIGILL и SIGFPE — все они представляют si_addr, специфицирующий адрес, который вызвал сбой, описанный si code.

Ниже приведен простой пример проверки контекста сигнала. Он устанавливает обработчик сигнала для SIGSEGV, который печатает контекст сигнала и прерывает процесс. Нарушение сегментации генерируется попыткой обращения к NULL.

 1: /* catch-segv.c */

 2:

 3: #include

 4: #include

 5: #include

 6:

 7: void handler(int signo, siginfo_t *info, void *f) {

 8:  printf("перехват");

 9:  if (info->si_signo == SIGSEGV)

10:   printf("segv accessing %p", info->si_addr);

11:  if (info->si_code == SEGV_MAPERR)

12:   printf("SEGV_MAPERR");

13:  printf("\n");

14:

15:  exit(1);

16: }

17:

18: int main() {

19:  struct sigactin act;

20:

21:  act.sa_sigaction = handler;

22:  sigemptyset(&act.sa_mask);

23:  act.sa_flags = SA_SIGINFO;

24:  sigaction(SIGSEGV, &act, NULL);

25:

26:  *((int *)NULL) = 1 ;

27:

28:  return 0;

29: }

 

12.7.2. Отправка данных с сигналом

Механизм siginfo_t также позволяет сигналам, которые посылают программы, присоединять к себе один элемент данных (этот элемент может быть указателем, что позволяет неявно передавать любой необходимый объем данных). Чтобы отправить данные, используется union sigval.

#include

union sigval {

 int sival_int;

 void *sival_ptr;

};

Любой из членов объединения — sival_int или sival_ptr — может быть установлен в требуемое значение, которое включается в siginfo_t, доставляемое вместе с сигналом. Чтобы сгенерировать сигнал с union sigval, должна использоваться функция sigqueue().

#include

void *sigqueue(pid_t pid, int signum, const union sigval value);

В отличие от kill(), pid должен быть корректным идентификатором процесса (отрицательные значения не допускаются), signum указывает номер посылаемого сигнала. Подобно kill(), sigqueue() допускает нулевое значение signum нулю, чтобы проверить, позволяет ли вызывающий процесс посылать целевому сигналы, в действительности не выполняя такой посылки. Последний параметр, value, представляет собой элемент данных, передаваемый вместе с сигналом.

Чтобы принять union sigval, процесс, перехватывающий сигнал, должен использовать SA_SIGINFO при регистрации обработчика сигналов с помощью sigaction(). Когда член si_code структуры siginfo_t равен SI_QUEUE, то siginfo_t представляет член si_value, который содержит значение value, переданное sigqueue.

Ниже приведен пример отправки элемента данных с сигналом. Он устанавливает в очередь три сигнала SIGRTMIN с разными элементами данных. Он демонстрирует, что сигналы доставляются в том же порядке, что были отправлены, как мы и ожидаем при работе с сигналами реального времени. Более сложный пример, использующий сигналы для отслеживания изменений в каталогах, можно найти в главе 14.

 1: /* sigval.с */

 2:

 3: #include

 4: #include

 5: #include

 6: #include

 7: #include

 8:

 9: /* Захватить сигнал и зарегистрировать факт его обработки */

10: void handler(int signo, siginfo_t *si, void *context) {

11:  printf("%d\n", si->si_value.sival_int);

12: }

13:

14: int main() {

15:  sigset_t mask;

16:  sigset_t oldMask;

17:  struct sigaction act;

18:  int me = getpid();

19:  union sigval val;

20:

21:  /* Отправить сигналы handler() и сохранять все сигналы заблокированными,

22:     чтобы handler() был сконфигурирован для перехвата с исключением

23:     состязаний при манипулировании глобальными переменными */

24:  act.sa_sigaction = handler;

25:  act.sa_mask = mask;

26:  act.sa_flags = SA_SIGINFO;

27:

28:  sigaction(SIGRTMIN, &act, NULL);

29:

30:  /* Блокировать SIGRTMIN, чтобы можно было увидеть очередь и упорядочение*/

31:  sigemptyset(&mask);

32:  sigaddset(&mask, SIGRTMIN);

33:

34:  sigprocmask(SIG_BLOCK, &mask, &oldMask);

35:

36:  /* Сгенерировать сигналы */

37:  val.sival_int = 1;

38:  sigqueue(me, SIGRTMIN, val);

39:  val.sival_int++;

40:  sigqueue(me, SIGRTMIN, val);

41:  val.sival_int++;

42:  sigqueue(me, SIGRTMIN, val);

43:

44:  /* Разрешить доставку сигналов */

45:  sigprocmask(SIG_SETMASK, &oldMask, NULL);

46:

47:  return 0;

48: }

 

Глава 13

Расширенная обработка файлов

 

В Linux файлы применяются при решении большого количества задач, среди которых, например, хранение долговременных данных, организация сетей с помощью сокетов и доступ к устройствам посредством файлов устройств. Разнообразие приложений, работающих с файлами, привело к созданию множества специальных способов управления файлами. В главе 11 рассматривались наиболее распространенные действия с файлами; в настоящей же главе исследуются специализированные файловые операции. В частности, мы рассмотрим следующие вопросы: использование одновременно нескольких файлов, отображение файлов на системную память, блокировка файлов, чтение и запись вразброс.

 

13.1. Мультиплексирование входных и выходных данных

 

Многим клиент-серверным приложениям необходимо считывать входные данные или записывать выходные данные с помощью одновременно нескольких файловых дескрипторов. Например, современные Web-браузеры открывают одновременно несколько сетевых подключений, чтобы уменьшить время загрузки Web-страницы. Это позволяет им загружать множество изображений, имеющихся на большинстве Web-страниц, быстрее, чем с помощью последовательных подключений. Кроме канала межпроцессных взаимодействий (IPC), используемого графическими браузерами для связи с X-сервером, на котором они отображаются, браузеры работают с множеством файловых дескрипторов.

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

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

Для иллюстрации этих проблем рассмотрим короткую программу, считывающую из двух файлов, p1 и p2. Для ее испытания откройте три сеанса работы с X-терминалом (или воспользуйтесь тремя виртуальными консолями). Создайте каналы под именами p1 и p2 (с помощью команды mknod), затем запустите cat > p1 и cat > p2 в двух терминалах, одновременно запустив mpx-blocks в третьем. После этого набирайте любой текст в каждом окно cat и смотрите, как он появляется. Помните, что две команды cat не будут записывать данные в каналы до конца строки.

 1: /* mpx-blocks.с */

 2:

 3: #include

 4: #include

 5: #include

 6:

 7: int main(void) {

 8:  int fds[2];

 9:  char buf[4096];

10:  int i;

11:  int fd;

12:

13:  if ((fds[0] = open("p1", O_RDONLY) ) < 0) {

14:   perror("open p1");

15:   return 1;

16:  }

17:

18:  if ( (fds[1] = open("p2", O_RDONLY)) < 0) {

19:   perror("open p2");

20:   return 1;

21:  }

22:

23:  fd = 0;

24:  while (1) {

25:   /* если данные доступны, прочитать и отобразить их */

26:   i = read (fds[fd], buf, sizeof (buf) - 1);

27:   if (i < 0) {

28:    perror("read");

29:    return 1;

30:   } else if (!i) {

31:    printf("канал закрыт\n");

32:    return 0;

33:   }

34:

35:   buf[i] = '\0';

36:   printf ("чтение: %s", buf);

37:

38:   /* читать из другого файлового дескриптора */

39:   fd = (fd + 1) % 2;

40:  }

41: }

Хотя программа mpx-blocks может считывать одновременно из обоих каналов, это не является особо эффективным. Она считывает из каждого канала по очереди. После запуска программа читает из первого файла, пока в нем доступны данные, второй файл игнорируется вплоть до возврата из read() для первого файла. Как только произошел возврат, первый файл игнорируется вплоть до чтения данных из второго файла. Этот метод не поддерживает гладкое мультиплексирование данных. На рис. 13.1 показана программа mpx-blocks во время выполнения.

Рис. 13.1. Примеры запуска мультиплексной передачи

 

13.1.1. Неблокируемый ввод-вывод

Как упоминалось в главе 11, неблокируемый файл можно определить с помощью системного вызова fcntl. Если медленный файл неблокируемый, read() сразу же возвращается. Если данные недоступны, она просто возвращает 0. Неблокируемый ввод- вывод предоставляет простое решение мультиплексирования, предотвращая блокирование файловых операций.

Показанная ниже модифицированная версия mpx-blocks пользуется преимуществом неблокируемого ввода-вывода для более гладкого переключения между p1 и p2.

 1: /* mpx-nonblock.c */

 2:

 3: #include

 4: #include

 5: #include

 6: #include

 7:

 8: int main(void) {

 9:  int fds[2];

10:  char buf[4096];

11:  int i;

12:  int fd;

13:

14:  /* открыть оба канала в неблокирующем режиме */

15:  if ((fds[0] = open("p1", O_RDONLY | O_NONBLOCK)) < 0) {

16:   perror("open p1");

17:   return 1;

18:  }

19:

20:  if ((fds[1] = open("p2", O_RDONLY | O_NONBLOCK)) < 0) {

21:   perror("open p2");

22:   return 1;

23:  }

24:

25:  fd = 0;

26:  while (1) {

27:   /* если данные доступны, прочитать и отобразить их */

28:   i = read(fds[fd], buf, sizeof (buf) - 1);

29:   if ((i < 0) && (errno ! = EAGAIN)) {

30:    perror("read");

31:    return 1;

32:   } else if (i > 0) {

33:    buf[i] = '\0';

34:    printf("чтение: %s", buf);

35:   }

36:

37:   /* читать из другого файлового дескриптора */

38:   fd = (fd + 1) % 2;

39:  }

40: }

Важное различие между mpx-nonblock и mpx-blocks состоит в том, что программа mpx-nonblock не закрывается, когда один из каналов, из которого она считывает, закрыт. Неблокируемый read() из канала без записывающих устройств возвращает 0 байт, из канала с таковыми, но без доступных данных read() возвращает EAGAIN.

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

 

13.1.2. Мультиплексирование с помощью

poll()

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

#include

int poll(struct pollfd * fds, int numfds, int timeout);

Последние два параметра очень просты; numfds задает количество элементов в массиве, на который указывает первый параметр, a timeout определяет, насколько долго poll() должна ожидать события. Если в качестве тайм-аута задается 0, poll() никогда не входит в состояние тайм-аута.

Первый параметр, fds, описывает, какие файловые дескрипторы следует контролировать, и для каких типов ввода-вывода. Это указатель на массив структур struct pollfd.

struct pollfd {

 int fd;        /* файловый дескриптор */

 short events;  /* ожидаемые события ввода-вывода */

 short revents; /* происшедшие события ввода-вывода */

};

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

POLLIN Нормальные данные доступны для считывания из файлового дескриптора.
POLLPRI Приоритетные (внешние) данные доступны для считывания.
POLLOUT Файловый дескриптор может принимать записываемые на него данные.

Элемент revents структуры struct pollfd заполняется системным вызовом poll() и отражает состояние файлового дескриптора fd. Это похоже на элемент events, но вместо определения интересующих приложение событий ввода-вывода он определяет доступные такие типы. Например, если приложение контролирует канал как для чтения, так и для записи (events установлено в POLLIN | POLLOUT), после успешного вызова poll() в revents устанавливается бит POLLIN, если канал готов для чтения, и бит POLLOUT, если в канале имеется пространство для записи дополнительных данных. Если верно и то, и другое, устанавливаются оба бита.

Существует несколько битов, которые ядро может установить в revents, но которые невозможно установить в events.

POLLERR В дескрипторе файла имеется ожидающая ошибка; выполнение системного вызова на файловом дескрипторе приведет к установке errno в подходящий код.
POLLHUP Файл был отключен; в него больше невозможно ничего записывать (хотя могут остаться данные для считывания). Это происходит в случае отключения терминала либо закрытия удаленного конца канала или сокета.
POLLNVAL Файловый дескриптор недоступен (он не относится к открытому файлу).

Возвращаемое значение poll() равно нулю в случае тайм-аута вызова, -1 в случае ошибки (например, fds — неверный указатель; ошибки в самих файлах вызывают установку POLLERR), или же положительное число, описывающее количество файлов с ненулевыми элементами revents.

В отличие от неэффективного метода мультиплексирования входных и выходных данных из каналов, используемого ранее, poll() довольно легко решает ту же проблему. Применяя poll() к файловым дескрипторам одновременно для обоих каналов, мы знаем, что когда poll() возвращается, один из каналов готов для чтения либо закрыт. Мы проверяем элемент revents для обоих файловых дескрипторов, чтобы узнать, какие действия предпринять, и по завершении возвращаемся в вызов poll(). Теперь большая часть времени тратится на блокирование вызова poll(), а не на постоянную проверку файловых дескрипторов, использующих неблокируемый ввод-вывод, что значительно уменьшает нагрузку на систему. Ниже показан код mpx-poll.

 1: /* mpx-poll.с */

 2:

 3: #include

 4: #include

 5: #include

 6: #include

 7:

 8: int main(void) {

 9:  struct pollfdfds[2];

10:  char buf [4096];

11:  int i, rc;

12:

13:  /* открыть оба канала */

14:  if ( (fds[0].fd = open("p1", O_RDONLY | O_NONBLOCK)) < 0) {

15:   perror("open p1");

16:   return 1;

17:  }

18:

19:  if ((fds[1].fd = open("p2", O_RDONLY | O_NONBLOCK)) < 0) {

20:   perror("open p2");

21:   return 1;

22:  }

23:

24:  /* начать чтение из обоих файловых дескрипторов */

25:  fds[0].events = POLLIN;

26:  fds[1].events = POLLIN;

27:

28:  /* пока наблюдаем за одним из fds[0] или fds[1] */

29:  while (fds[0].events || fds[1].events ) {

30:   if (poll(fds, 2, 0) < 0) {

31:    perror("poll");

32:    return 1;

33:   }

34:

35:   /* проверить, какой из файловых дескрипторов

36:      готов для чтения из него */

37:   for (i = 0; i < 2; i++) {

38:    if (fds[i].revents) {

39:     /* fds[i] готов для чтения, двигаться дальше... */

40:     rc = read(fds[i].fd, buf, sizeof(buf) - 1);

41:     if (rc < 0) {

42:      perror("read");

43:      return 1;

44:     } else if (!rc) {

45:      /* этот канал закрыт, не пытаться

46:         читать из него снова */

47:      fds[i].events = 0;

48:     } else {

49:      buf[rc] = '\0';

50:      printf("чтение : %s", buf);

51:     }

52:    }

53:   }

54:  }

55:

56:  return 0;

57: }

 

13.1.3. Мультиплексирование с помощью

select()

Системный вызов poll() был изначально представлен как часть Unix-дерева System V. Усилиями разработчиков BSD та же основная проблема была решена похожим способом — предоставлением системного вызова select().

#include

int select(int numfds, fd_set * readfds, fd_set * writefds,

 fd_set * exceptfds, struct timeval * timeout);

Три промежуточных параметра — readfds, writefds и exceptfds — определяют, за какими файловыми дескрипторами необходимо следить. Каждый параметр — это указатель на fd_set, структуру данных, позволяющую процессу определить произвольное количество файловых дескрипторов. Ею манипулируют с помощью перечисленных ниже макросов.

FD_ZERO(fd_set * fds); Очищает fds — в наборе не содержатся файловые дескрипторы. Этот макрос используется для инициализации структур fd_set .
FD_SET(intfd, fd_set * fds); Добавляет fd к fd_set .
FD_CLR(intfd, fd_set * fds); Удаляет fd из fd_set .
FD_ISSET(int fd, fd_set * fds); Возвращает true , если fd содержится в установленном fds .

Первый набор файловых дескрипторов select(), readfds, содержит перечень файловых дескрипторов, вызывающих возврат вызова select(), когда они готовы для чтения или (для каналов и сокетов) когда процесс на другом конце файла закрыл его. Когда любой файловый дескриптор в writefds готов к записи, select() возвращается, exceptfds содержит файловые дескрипторы для слежения за исключительными условиями. В Linux (так же, как и в Unix) это происходит только при поступлении внешних данных в сетевое подключение. В качестве любого из них можно указать NULL, если тот или иной тип события вас не интересует.

Окончательный параметр, timeout, определяет, насколько долго (в миллисекундах) вызову select() необходимо ожидать какого-либо события. Это указывает на struct timeval, которая выглядит следующим образом.

#include

struct timeval {

 int tv_sec; /* секунды */

 int tv_usec; /* микросекунды */

};

Первый элемент — tv_sec — это количество оставшихся секунд, a tv_usec — это количество оставшихся микросекунд. Если значением timeout является NULL, select() блокируется до следующего события. Если он указывает на struct timeval, содержащую 0 в обоих элементах, вызов select() не блокируется. Он обновляет наборы файловых дескрипторов, чтобы определить, какой файловый дескриптор в настоящее время готов для чтения или записи, а затем немедленно возвращается.

Первый параметр, numfds, вызывает наибольшие трудности. Он задает количество файловых дескрипторов (начиная с файлового дескриптора 0), которое может быть определено с помощью fd_sets. Еще один (и, возможно, более легкий) способ поведения numfds намного лучше максимального файлового дескриптора select().

Поскольку Linux обычно позволяет каждому процессу иметь до 1024 файловых дескрипторов, numfds избавляет ядро от необходимости просмотра всех 1024 файловых дескрипторов, которые может содержать каждая структура fd_set, что улучшает показатели производительности.

После возврата три структуры fd_set содержат файловые дескрипторы с задержкой входных данных, на которые можно произвести запись или которые находятся в исключительном состоянии. Вызов select() в Linux возвращает общее количество элементов, установленных в трех структурах fd_set, 0 в случае тайм-аута вызова либо -1 в случае ошибки. Однако многие системы Unix считают определенные файловые дескрипторы в возвращаемом значении только один раз, даже если они находятся как в readfds, так и в writefds, поэтому в целях переносимости лучше совершать проверку только тогда, когда возвращаемое значение больше 0. Если возвращаемое значение равно -1, не думайте, что структуры fd_set остаются незатронутыми. Linux обновляет их только в случае, если select() возвращает значение больше 0, однако некоторые системы Unix демонстрируют иное поведение.

Еще одним параметром, связанным с переносимостью, является timeout. Ядра Linux обновляют его, чтобы отобразить количество времени, оставшегося до тайм-аута вызова select(), но большинство других систем Unix его не обновляют. Однако другие системы не обновляют тайм-аут с целью соответствия более привычной реализации.

Для переносимости устраните зависимость от поведения и явно настройте структуру timeout перед вызовом select().

Теперь рассмотрим несколько примеров применения select(). Для начала используем select() без связи с файлами, создав вторичный вызов sleep().

#include

#include

int usecsleep(int usees) {

 struct timeval tv;

 tv.tv_sec = 0;

 tv.tv_usec = useсs;

 return select(0, NULL, NULL, NULL, &tv);

}

Этот код разрешает переносимые паузы длительностью менее секунды (это обеспечивает также библиотечная функция BSD usleep(), но select() намного более переносима). Например, usecsleep(500000) вызывает паузу минимум на полсекунды.

Вызов select() также используется для решения примера мультиплексирования каналов, с которым мы работали. Решение очень похоже на решение при использовании poll().

 1: /* mpx-select.c */

 2:

 3: #include

 4: #include

 5: #include

 6: #include

 7:

 8: int main(void) {

 9:  int fds[2];

10:  char buf[4096];

11:  int i, rc, maxfd;

12:  fd_set watchset; /* fds для чтения */

13:  fd_set inset; /* обновляется select() */

14:

15:  /* открыть оба канала */

16:  if ((fds[0] = open("p1", O_RDONLY | O_NONBLOCK)) < 0) {

17:   perror("open p1");

18:   return 1;

19:  }

20:

21:  if ((fds[1] = open("p2", O_RDONLY | O_NONBLOCK)) < 0) {

22:   perror("open p2");

23:   return 1;

24:  }

25:

26:  /* начать чтение из обоих файловых дескрипторов */

27:  FD_ZERO(&watchset);

28:  FD_SET(fds[0], &watchset);

29:  FD_SET(fds[1], &watchset);

30:

31:  /* найти максимальный файловый дескриптор */

32:  maxfd = fds[0] > fds[1] ? fds[0] : fds[1];

33:

34:  /* пока наблюдаем за одним из fds[0] или fds[1] */

35:  while (FD_ISSET(fds[0], &watchset) ||

36:   FD_ISSET(fds[1], &watchset)) {

37:   /* здесь копируем watchset, потому что select() обновляет его */

38:   inset = watchset;

39:   if (select(maxfd + 1, &inset, NULL, NULL, NULL) < 0) {

40:    perror("select");

41:    return 1;

42:   }

43:

44:   /* проверить, какой из файловых дескрипторов

45:      готов для чтения из него */

46:   for (i = 0; i < 2; i++) {

47:    if (FD_ISSET(fds[i], &inset )) {

48:     /* fds[i] готов для чтения, двигаться дальше... */

49:     rc = read(fds[i], buf, sizeof (buf) - 1);

50:     if (rc < 0) {

51:      perror("read");

52:      return 1;

53:     } else if (!rc) {

54:      /* этот канал закрыт, не пытаться

55:         читать из него снова */

56:      close(fds[i]);

57:      FD_CLR(fds[i], &watchset);

58:     } else {

59:      buf[rc] = '\0';

60:      printf("чтение: %s", buf);

61:     }

62:    }

63:   }

64:  }

65:

66:  return 0;

67: }

 

13.1.4. Сравнение

poll()

и

select()

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

Более важное отличие связано с производительностью. Интерфейс poll() обладает несколькими свойствами, делающими его намного эффективнее, чем select().

1. При использовании select() ядру необходимо проверить все файловые дескрипторы между 0 и numfds - 1, чтобы убедиться, заинтересовано ли приложение в событиях ввода-вывода для этого файлового дескриптора. Для приложений с большим количеством открытых файлов это может привести к существенным затратам, поскольку ядро проверяет, какие именно файловые дескрипторы являются объектом интереса.

2. Набор файловых дескрипторов передается ядру как битовая карта для select() и как список для poll(). Сложные битовые операции, необходимые для проверки и установки структур данных fd_set, менее эффективны, чем простые проверки, требуемые для struct pollfd.

3. Поскольку ядро переписывает структуры данных, передаваемые select(), приложение вынуждено сбрасывать эти структуры каждый раз перед вызовом select(). С poll() результаты ядра ограничены элементом revents, что устраняет потребность в восстановлении структур данных после каждого вызова.

4. Использование структуры, основанной на множествах (например, fd_set) не масштабируется по мере увеличения количества доступных процессу файловых дескрипторов. Поскольку ее размер статичен, а не динамичен (обратите внимание на отсутствие соответствующего макроса, например, FD_FREE), она не может расширяться или сжиматься в соответствии с потребностями приложения (или возможностями ядра). В Linux максимальный файловый дескриптор, который можно установить в fd_set, равен 1023. Если понадобится больший файловый дескриптор, select() работать не будет.

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

Следующая короткая программа, подсчитывающая количество системных вызовов в секунду, демонстрирует, насколько poll() эффективнее select().

 1: /* select-vs-poll.с */

 2:

 3: #include

 4: #include

 5: #include

 6: #include

 7: #include

 8: #include

 9:

10: int gotAlarm;

11:

12: void catch(int sig) {

13:  gotAlarm = 1;

14: }

15:

16: #define HIGH_FD 1000

17:

18: int main(int argc, const char ** argv) {

19:  int devZero;

20:  int count;

21:  fd_set select Fds;

22:  struct pollfd pollFds;

23:

24:  devZero = open("/dev/zero", O_RDONLY);

25:  dup2(devZero, HIGH_FD);

26:

27:  /* с помощью signal выяснить, когда время истекло */

28:  signal(SIGALRM, catch);

29:

30:  gotAlarm =0;

31:  count = 0;

32:  alarm(1);

33:  while (!gotAlarm) {

34:   FD_ZERO(&selectFds);

35:   FD_SET(HIGH_FD, &selectFds);

36:

37:   select(HIGH_FD + 1, &selectFds, NULL, NULL, NULL);

38:   count++;

39:  }

40:

41:  printf("Вызовов select() в секунду: %d\n", count);

42:

43:  pollFds.fd = HIGH_FD;

44:  pollFds.events = POLLIN;

45:  count = 0;

46:  gotAlarm = 0;

47:  alarm(1);

48:  while (!gotAlarm) {

49:   poll(&pollFds, 0, 0);

50:   count++;

51:  }

52:

53:  printf("Вызовов poll() в секунду: %d\n", count);

54:

55:  return 0;

56: }

Здесь используется устройство /dev/zero, предоставляющее бесконечное количество нулей, что обеспечивает немедленный возврат системных вызовов. Значение HIGH_FD можно изменить, чтобы посмотреть, как деградирует select() по мере роста значений файловых дескрипторов.

В определенной системе при не очень высоком значении HIGH_FD, равном 2, программа показала, что ядро за секунду может обрабатывать в четыре раза больше вызовов poll(), чем вызовов select(). При увеличении HIGH_FD до 1000 эффективность poll() становится в 40 раз выше, чем у select().

 

13.1.5. Мультиплексирование с помощью

epoll

В версии 2.6 ядра Linux был предложен третий метод для мультиплексированного ввода-вывода по имени epoll. Будучи более сложным, чем poll() или select(), epoll ликвидирует узкие места, связанные с производительностью, которые характерны для обоих методов.

Оба системных вызова poll() и select() передают на проверку полный список файловых дескрипторов при каждом вызове. Каждый из этих дескрипторов должен быть обработан системным вызовом, даже если только один из них готов к чтению или записи. Когда проверяются десятки, сотни или тысячи файловых дескрипторов, эти системные вызовы превращаются в узкие места; ядро тратит много времени на выяснение того, какие именно файловые дескрипторы приложению необходимо проверить.

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

Преимущества в плане производительности epoll требуют более сложного, чем у poll() или select(), интерфейса системных вызовов. В то время как poll() использует массив struct pollfd для предоставления набора файловых дескрипторов, a select() с той же целью — три разных структуры fd_set, epoll перемещает эти наборы файловых дескрипторов в ядро, а не хранит их в адресном пространстве программы. На каждый из этих наборов ссылаются с помощью дескриптора epoll, являющегося файловым дескриптором, который можно применять только для системных вызовов epoll. Новые дескрипторы epoll распределяются системным вызовом epoll_create().

#include

int epoll_create (int numDescriptors);

Единственный параметр numDescriptors — это наилучшее предположение программы о том, на какое количество файловых дескрипторов будет ссылаться заново созданный дескриптор epoll. Это не жесткий предел, это просто подсказка ядру для более точной инициализации его внутренних структур. epoll_create() возвращает дескриптор epoll, а когда программа заканчивает работу с дескриптором, его следует передать close(), чтобы позволить ядру освободить память, используемую этим дескриптором.

Хотя дескриптор epoll является файловым дескриптором, его следует применять только с двумя системными вызовами.

#include

int epoll_ctl(int epfd, int op, int fd, struct epoll_event * event);

int epoll_wait(int epfd, struct epoll_event * events, int maxevents,

 int timeout);

Большинство этих параметров используют структуру struct epoll_event, которая определяется, как показано ниже.

#include

struct epoll_event {

 int events;

 union {

  void * ptr;

  int fd;

  unsigned int u32;

  unsigned long long u64;

 } data;

};

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

EPOLLIN Определяет, что операция read() не блокируется; данные или уже готовы, или их уже не осталось для считывания.
EPOLLOUT Связанный файл готов для записи.
EPOLLPRI Файл имеет внешние данные, готовые для чтения.

Второй элемент struct epoll_event, data, представляет собой объединение, содержащее целое число (для хранения файлового дескриптора), указатель, а также 32- и 64-битные целые числа. Этот элемент данных хранится в epoll и возвращается в программу всякий раз, когда происходит событие подходящего типа. Элемент data — это единственный способ, с помощью которого программе нужно выяснить, какой файловый дескриптор необходимо обслужить; интерфейс epoll не передает файловый дескриптор программе, в отличие от poll() и select() (если data не содержит файловый дескриптор). Этот метод обеспечивает дополнительную гибкость приложениям, которые отслеживают файлы как нечто, более сложное, чем простые файловые дескрипторы.

Системный вызов epoll_ctl() добавляет файловые дескрипторы к набору, на который ссылается дескриптор epfdepoll, и удаляет их из него.

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

EPOLL_CTL_ADD Файловый дескриптор fd добавляется к набору файловых дескрипторов набором событий events . Если файловый дескриптор уже присутствует, он возвращает EEXIST . (Несколько потоков могут добавлять тот же файловый дескриптор к набору epoll более одного раза, но это действие ничего не меняет.)
EPOLL_CTL_DEL Файловый дескриптор fd удаляется из контролируемого набора файловых дескрипторов. Параметр events должен указывать на struct epoll_event , но содержимое этой структуры игнорируется. (Это еще раз доказывает, что events должен быть допустимым указателем; он не может быть NULL .)
EPOLL_CTL_MOD Системный вызов struct epoll_event для fd обновляется на основе информации, на которую указывает events . Это позволяет контролировать набор событий и обновлять элемент данных, ассоциируемый с файловым дескриптором, не создавая условий состязания.

Последним системным вызовом epoll является epoll_wait(), который блокирует до тех пор, пока один или несколько контролируемых файловых дескрипторов не будут иметь данные для чтения или же не будут готовы к записи. Первым аргументом является дескриптор epoll, а последний — тайм-аутом в секундах. Если файловые дескрипторы не готовы к обработке до истечения тайм-аута, epoll_wait() возвращает 0.

Два промежуточных параметра определяют буфер для ядра, в который можно копировать структуры struct epoll_event. Параметр events указывает на буфер, maxevents определяет, какое количество структур struct epoll_event помещается в буфер, а возвращаемое значение сообщает программе количество структур, помещенных в этот буфер (пока вызов не попадет в состояние тайм-аута либо не произойдет ошибка).

Каждый системный вызов struct epoll_event сообщает программе полное состояние контролируемого файлового дескриптора. Элемент events может иметь установленные флаги EPOLLIN, EPOLLOUT или EPOLLPRI, а также два новых флага, которые описаны ниже.

EPOLLERR С файлом связано ожидающее состояние ошибки; это случается, если ошибка происходит в сокете, когда приложение не считывает из него или не записывает в него.
EPOLLHUP Файловый дескриптор завис; в главе 10 дана информация о том, когда это обычно происходит.

На первый взгляд это все может показаться сложным, но на самом деле это очень похоже на работу poll(). Вызов epoll_create() — это то же, что и распределение массива struct pollfd, a epoll_ctl() — это то же, что и инициализация элементов этого массива. Главный цикл, обрабатывающий файловые дескрипторы, использует epoll_wait() вместо системного вызова poll(), а close() аналогичен освобождению памяти, занимаемой массивом struct pollfd. Эти параллели помогают переписывать с применением epoll программы мультиплексирования, которые изначально были реализованы с помощью poll() или select().

Интерфейс epoll предлагает еще одну возможность, которую невозможно сравнить с poll() или select(). Поскольку дескриптор epoll в действительности является файловым дескриптором (вот почему его можно передавать close()), имеется возможность контролировать дескриптор epoll как часть еще одного дескриптора epoll либо через poll() или select(). Дескриптор epoll будет готов к чтению из любого места, а вызов epoll_wait() вернет события.

В окончательном решении проблемы мультиплексирования каналов, предложенном в данном разделе, используется epoll. Оно очень похоже на другие примеры, вот только определенная часть кода инициализации перемещена в новую функцию addEvent() для предотвращения нежелательного удлинения программы.

 1: /* mpx-epoll.c */

 2:

 3: #include

 4: #include

 5: #include

 6: #include

 7: #include

 8:

 9: #include

10:

11: void addEvent(int epfd, char * filename) {

12:  int fd;

13:  struct epoll_event event;

14:

15:  if ((fd = open (filename, O_RDONLY | O_NONBLOCK)) < 0) {

16:   perror("open");

17:   exit(1);

18:  }

19:

20:  event.events = EPOLLIN;

21:  event.data.fd = fd;

22:

23:  if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event)) {

24:   perror("epoll_ctl(ADD)");

25:   exit(1);

26:  }

27: }

28:

29: int main(void) {

30:  char buf[4096];

31:  int i, rc;

32:  int epfd;

33:  struct epoll_event events[2];

34:  int num;

35:  int numFds;

36:

37:  epfd = epoll_create(2);

38:  if (epfd < 0) {

39:   perror("epoll_create");

40:   return 1;

41:  }

42:

43:  /* открыть оба канала и добавить их в набор epoll */

44:  addEvent(epfd, "p1");

45:  addEvent(epfd, "p2");

46:

47:  /* продолжать, пока есть один или более файловых дескрипторов

48:     для слежения */

49:  numFds = 2;

50:  while (numFds) {

51:   if ((num = epoll_wait(epfd, events,

52:    sizeof(events) / sizeof(* events),

53:    -1)) <= 0) {

54:   perror("epoll_wait");

55:   return 1;

56:  }

57:

58:  for (i = 0; i < num; i++) {

59:   /* events[i].data.fd готов для чтения */

60:

61:   rc = read(events[i].data.fd, buf, sizeof(buf) - 1);

62:   if (rc < 0) {

63:    perror("read");

64:    return 1;

65:   } else if (!rc) {

66:    /* этот канал закрыт, не пытаться

67:       читать из него снова */

68:    if (epoll_ctl(epfd, EPOLL_CTL_DEL,

69:     events[i].data.fd, &events[i])) {

70:     perror("epoll_ctl (DEL)");

71:     return 1;

72:    }

73:

74:    close(events[i].data.fd);

75:

76:    numFds--;

77:   } else {

78:    buf[rc] = '\0';

79:    printf("чтение: %s", buf);

80:

81:   }

82:  }

83:

84:  close(epfd);

85:

86:  return 0;

87: }

 

13.1.6 Сравнение

poll()

и

epoll

Методы poll() и epoll существенно отличаются; poll() хорошо стандартизован, но плохо масштабируется, в то время как epoll существует только в Linux, но очень хорошо масштабируется. Приложения, наблюдающие за небольшим количеством файловых дескрипторов и переносимости величин, должны использовать poll(), но любому приложению, которому необходимо контролировать большое количество дескрипторов, лучше применять epoll, даже если ему нужно поддерживать poll() для других платформ.

Отличия в производительности двух методов поразительны. Чтобы продемонстрировать, насколько лучше масштабируется epoll, в коде poll-vs-epoll.с измеряется количество системных вызовов poll() и epoll_wait(), которые можно создать за одну секунду для наборов файловых дескрипторов разных размеров (количество файловых дескрипторов для помещения в набор задается в командной строке). Каждый файловый дескриптор ссылается на считывающую часть канала, и они создаются с помощью dup2().

В табл. 13.1 суммируются результаты запуска poll-vs-epoll.с для установленных размеров диапазоном от одного до 100 000 файловых дескрипторов. В то время как количество системных вызовов в секунду резко падает для poll(), оно остается почти постоянным для epoll. Как поясняет эта таблица, epoll добавляет в систему намного меньше нагрузки, чем poll(), и в результате гораздо лучше масштабируется.

Таблица 13.1. Результаты сравнения poll() и epoll()

Файловые дескрипторы poll() epoll()
1 310063 714848
10 140842 726108
100 25866 726659
1000 3343 729072
5000 612 718424
10000 300 730483
25000 108 717097
50000 38 729746
100000 18 712301

  1: /* poll-vs-epoll.с */

  2:

  3: #include

  4: #include

  5: #include

  6: #include

  7: #include

  8: #include

  9: #include

 10: #include

 11: #include

 12: #include

 13:

 14: #include

 15:

 16: int gotAlarm;

 17:

 18: void catch(int sig) {

 19:  gotAlarm = 1;

 20: }

 21:

 22: #define OFFSET 10

 23:

 24: int main(int argc, const char ** argv) {

 25:  int pipeFds[2];

 26:  int count;

 27:  int numFds;

 28:  struct pollfd * pollFds;

 29:  struct epoll_event event;

 30:  int epfd;

 31:  int i;

 32:  struct rlimit lim;

 33:  char * end;

 34:

 35:  if (!argv[1]) {

 36:   fprintf(stderr, "ожидалось число\n");

 37:   return 1;

 38:  }

 39:

 40:  numFds = strtol(argv[1], &end, 0);

 41:  if (*end) {

 42:   fprintf(stderr, "ожидалось число\n");

 43:   return 1;

 44:  }

 45:

 46:  printf("Запуск теста для %d файловых дескрипторов.\n", numFds);

 47:

 48:  lim.rlim_cur = numFds + OFFSET;

 49:  lim.rlim_max = numFds + OFFSET;

 50:  if (setrlimit(RLIMIT_NOFILE, &lim)) {

 51:   perror("setrlimit");

 52:   exit(1);

 53:  }

 54:

 55:  pipe(pipeFds);

 56:

 57:  pollFds = malloc(sizeof (*pollFds) * numFds);

 58:

 59:  epfd = epoll_create(numFds);

 60:  event.events = EPOLLIN;

 61:

 62:  for (i = OFFSET; i < OFFSET + numFds; i++) {

 63:   if (dup2(pipeFds[0], i) != i) {

 64:    printf("сбой в %d: %s\n", i, strerror(errno));

 65:    exit(1);

 66:   }

 67:

 68:   pollFds[i - OFFSET].fd = i;

 69:   pollFds[i - OFFSET].events = POLLIN;

 70:

 71:   event.data.fd = i;

 72:   epoll_ctl(epfd, EPOLL_CTL_ADD, i, &event);

 73:  }

 74:

 75:  /* с помощью signal выяснить, когда время истекло */

 76:  signal(SIGALRM, catch);

 77:

 78:  count = 0;

 79:  gotAlarm = 0;

 80:  alarm(1);

 81:  while (!gotAlarm) {

 82:   poll(pollFds, numFds, 0);

 83:   count++;

 84:  }

 85:

 86:  printf("Вызовов poll() в секунду: %d\n", count);

 87:

 88:  alarm(1);

 89:

 90:  count = 0;

 91:  gotAlarm = 0;

 92:  alarm(1);

 93:  while (!gotAlarm) {

 94:   epoll_wait(epfd, &event, 1, 0);

 95:   count++;

 96:  }

 97:

 98:  printf("Вызовов epoll() в секунду: %d\n", count);

 99:

100:  return 0;

101: }

 

13.2. Отображение в памяти

 

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

Высокоскоростной доступ к файлам. Нормальные механизмы ввода-вывода, такие как read() и write(), вынуждают ядро копировать данные через буфер ядра, а не непосредственно между файлом, содержащим устройство, и процессом пространства пользователя. Карты памяти устраняют этот промежуточный буфер, сохраняя копию памяти.

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

Новую память можно распределить отображением части /dev/zero, специального устройства, состоящего из нулей, или же через анонимное отображение. Средство Electric Fence, описанное в главе 7, использует этот механизм для распределения памяти.

Новую память, распределенную посредством карт памяти, можно сделать исполняемой, наполняя ее машинными командами, которые затем запускаются. Это свойство используется оперативными (just-in-time) компиляторами.

Файлы могут рассматриваться как память и читаться с использованием указателей, а не системных вызовов. Это существенно упрощает программы, избавляя от необходимости применения вызовов read(), write() и seek().

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

 

13.2.1. Выравнивание по страницам

Системная память делится на порции под названием страницы. Размер страницы изменяется в зависимости от архитектуры, и на некоторых процессорах размер страницы может изменяться ядром. Функция getpagesize() возвращает размер (в байтах) каждой страницы системы.

#include

size_t getpagesize(void);

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

Адрес памяти должен быть выровнен по страницам, если это адрес начала страницы. Иначе говоря, адрес должен быть целым, кратным размеру страницы архитектуры. В системе со страницами в 4 Кбайт адреса 0, 4 096, 16 384 и 32 768 являются выровненными по страницам (конечно, это далеко не весь список), потому что первая, вторая, пятая и девятая страницы системы начинаются с указанных адресов.

 

13.2.2. Установка отображения в памяти

Новые карты памяти создаются с помощью системного вызова mmap().

#include

caddr_tmmap(caddr_t address, size_t length , int protection, int flags,

 int fd, off_t offset);

Параметр address указывает, где именно в памяти необходимо отображать данные. Обычно address — это NULL, который означает, что для процесса не имеет значения местонахождение новой карты, и позволяет ядру выбрать любой адрес. Если адрес указан, он должен быть выровнен по страницам и в данный момент не использоваться. Если запрашиваемая карта будет конфликтовать с другой картой или не будет выровнена по страницам, mmap() может дать сбой.

Второй параметр, length, сообщает ядру, какую часть файлов следует отображать в памяти. Можно успешно отобразить больше памяти, чем количество данных в наличии у файла, но попытка доступа к нему может привести к SIGSEGV.

Процесс проверяет, какие типы доступа разрешены новой области памяти. Это должно быть одно или несколько значений из табл. 13.2, объединенных с помощью битового "ИЛИ", либо PROT_NONE, если доступ к отображаемой области запрещен. Файл может отображаться только для типов доступа, которые также были запрошены при изначальном открытии файла. Например, файл, открытый как O_RDONLY, не может быть отображен для записи с помощью PROT_WRITE.

Таблица 13.2. Флаги защиты mmap()

Флаг Описание
PROT_READ Из отображаемой области можно читать.
PROT_WRITE В отображаемую область можно записывать.
PROT_EXEC Отображаемую область можно выполнять.

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

По этой причине на флаги защиты памяти, передаваемые в mmap(), следует полагаться лишь как на обеспечивающие минимальную защиту.

В flags определяются другие атрибуты отображаемой области. В табл. 13.3 описаны все флаги. Многие флаги, поддерживаемые Linux, нестандартны, но могут быть полезны при особых условиях. В табл. 13.3 приведены различия между стандартными флагами mmap() и дополнительными флагами Linux. Во всех вызовах mmap() должен быть специфицирован MAP_PRIVATE или MAP_SHARED; остальные флаги устанавливать необязательно.

Таблица 13.3. Флаги mmap()

Флаг POSIX? Описание
MAP_ANONYMOUS Да Игнорировать fd , создать анонимную карту.
MAP_FIXED Да Сбой в случае недопустимого адреса ( address ).
MAP_PRIVATE Да Запись приватна для процесса.
MAP_SHARED Да Запись копируется в файл.
MAP_DENYWRIТЕ Нет Не разрешать нормальную запись в файл.
MAP_GROWSDOWN Нет Расширить область памяти сверху вниз.
MAP_LOCKED Нет Блокировать страницы в памяти.
MAP_ANONYMOUS Вместо отображения файла возвращается анонимное отображение . Оно ведет себя подобно обычному отображению, но без участия физического файла. Хотя эту область памяти нельзя ни использовать совместно с другими процессами, ни автоматически сохранять в файле, анонимное отображение позволяет процессам распределять новую память для индивидуального использования. Такое отображение часто применяется реализациями malloc() , а также еще несколькими специализированными приложениями. Параметр fd игнорируется при использовании этого флага.
MAP_FIXED Если карту нельзя поместить по запрашиваемому адресу, mmap() завершается неудачей. Если этот флаг не определен, ядро попытается разместить карту по указанному адресу, но если это не удастся, то отобразит ее на альтернативный адрес. Если адрес, переданный в address , уже использовался mmap() , элемент, отображаемый в этой области, будет замещен новой картой памяти. Это означает, что лучше передавать только те адреса, которые были возвращены предыдущими вызовами в mmap() ; если применяются произвольные адреса, может быть перезаписана область памяти, используемая системными библиотеками.
MAP_PRIVATE Модификации области памяти должны быть индивидуальными для процесса. Их не следует совместно использовать с другими процессами, которые отображают этот же файл (процессами, отличающимися от связанных процессов, которые ответвляются после создания карты памяти), а также отражать в самом файле. Должен использоваться флаг MAP_SHARED или MAP_PRIVATE . Если область памяти незаписываемая, тип используемого флага не имеет значения.
MAP_SHARED Изменения в области памяти копируются обратно в файл, который был отображен и использован совместно с другими процессами, отображающими этот же файл. (Для записи изменений в область памяти следует установить PROT_WRITE ; иначе область памяти будет постоянной). Должен использоваться флаг MAP_SHARED или MAP_PRIVATE .
MAP_DENYWRITE Обычно системные вызовы для нормального доступа к файлам (например, write() ) могут модифицировать отображенный файл. Однако если область запускается, это будет не самым лучшим решением. Указание MAP_DENYWRITE приводит к тому, что операции записи файлов, отличные от тех, что совершаются через карту памяти, будут возвращать etxtbsy .
MAP_GROWSDOWN Попытка немедленного доступа к памяти, расположенной непосредственно перед отображаемой областью, обычно вызывает SIGSEGV . Этот флаг заставляет ядро расширять область для младших адресов памяти по страницам, если процесс пытается получить доступ к памяти на младшей смежной странице, и продолжает процесс обычным образом. Это разрешает ядру автоматически расширять стеки процессов на платформах, на которых стеки расширяются сверху вниз (наиболее распространенный случай). Это специфичный для платформы флаг, применяемый обычно только для системного кода. Единственным ограничением для MAP_GROWSDOWN является ограничение размеров стека, рассматриваемое в главе 10. Если ограничение не установлено, ядро расширит отображенный сегмент, несмотря на то, выгодно ли это. Однако оно не будет расширять сегмент поверх остальных отображаемых областей.
MAP_GROWSUP Этот флаг работает так же, как и MAP_GROWSDOWN , но предназначен для тех редких платформ, на которых стеки расширяются снизу вверх, что означает расширение области со старших, а не младших адресов. (В ядре версии 2.6.7 только архитектура parisc имеет стеки, расширяющиеся снизу вверх.) Как и MAP_GROWSDOWN , этот флаг зарезервирован для системного кода с установленным ограничением на размер стека.
MAP_LOCKED Область блокируется в памяти. Это означает, что она никогда не будет подлежать страничному обмену. Это важно для систем реального времени ( mlock() , рассматриваемый далее в этой главе, предоставляет еще один метод блокирования памяти). Обычно это может установить только привилегированный пользователь; обычным пользователям не разрешено блокировать страницы в памяти. Некоторые системы Linux допускают ограниченное распределение заблокированной памяти непривилегированными пользователями, и эта возможность, вероятно, вскоре будет добавлена к стандартному ядру Linux.

За флагами следует файловый дескриптор, fd, для файла, который предстоит отобразить в памяти. Если применялся флаг MAP_ANONYMOUS, его значение игнорируется. Последний параметр определяет, где именно в файле должно начаться отображение. Он должен быть целым числом, кратным размеру страницы. Большинство приложений начинают отображение с начала файла, указывая в качестве offset ноль.

Системный вызов mmap() возвращает адрес, который должен храниться в указателе. Если произошла ошибка, он возвращает адрес, эквивалентный -1. Для проверки этого необходимо привести тип константы -1 к caddr_t, а не к int. Это гарантирует, что результат будет верным независимо от размеров указателей и целых чисел.

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

 1: /* map-cat.с */

 2:

 3: #include

 4: #include

 5: #include

 6: #include

 7: #include

 8: #include

 9: #include

10:

11: int main(int argc, const char ** argv) {

12:  int fd;

13:  struct stat sb;

14:  void * region;

15:

16:  if ( fd = open(argv[1], O_RDONLY)) < 0) {

17:   perror("open");

18:   return 1;

19:  }

20:

21:  /* Вызвать fstat для файла, чтобы узнать, сколько необходимо памяти для его отображения */

22:  if (fstat(fd, &sb)) {

23:   perror("fstat");

24:   return 1;

25:  }

26:

27:  /* можно было бы также отобразить как MAP_PRIVATE, поскольку

28:     запись в эту память не планируется */

29:  region = mmap(NULL, sb.st_size, PROT_READ, MAP_SHARED, fd, 0);

30:  if (region == ((caddr_t) -1)) {

31:   perror("mmap");

32:   return 1;

33:  }

34:

35:  close(fd);

36:

37:  if (write(1, region, sb.st_size) != sb.st_size) {

38:   perror("write");

39:   return 1;

40:  }

41:

42:  return 0;

43: }

 

13.2.3. Отмена отображения областей

После окончания отображения в памяти процесс может отменить отображение памяти с помощью munmap(). Это приводит к тому, что последующие доступы к этому адресу будут генерировать SIGSEGV (если только память не будет перераспределена) и сохраняет некоторые системные ресурсы. Отображение всех областей памяти отменяется, когда процесс заканчивает или начинает новую программу с помощью системного вызова exec().

#include

int munmap(caddr_t addr, int length);

Параметр addr — это адрес начала области памяти для отмены отображения, а length определяет, отображение какой части области памяти должно быть отменено. Обычно отображение каждой области отменяется отдельным вызовом munmap(). Linux может фрагментировать карты, если отменено отображение только части области, но такой код будет непереносимым.

 

13.2.4. Синхронизация областей памяти на диск

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

#include

int msync(caddr_t addr, size_t length, int flags);

Первые два параметра, addr и length, устанавливают область для синхронизации с диском. Параметр flags устанавливает, каким образом должны синхронизироваться память и диск. Он состоит из одного или нескольких перечисленных ниже флагов, объединенных с помощью битового "ИЛИ".

MS_ASYNC Модифицированные версии области памяти запланированы на "скорую" синхронизацию. Использовать можно только либо MS_ASYNC , либо MS_SYNC .
MS_SYNC Модифицированные страницы в области памяти записываются на диск до возврата системного вызова msync() . Использовать можно только либо MS_ASYNC , либо MS_SYNC .
MS_INVALIDATE Эта опция позволяет ядру выяснять, записываются ли изменения на диск. Хотя эта опция не дает гарантию того, что они не будут записаны, она сообщает ядру, что необходимость сохранения изменений отсутствует. Этот флаг применяется только при особых условиях.
0 Передача 0 в msync() работает в ядрах Linux, хотя она не очень хорошо документирована. Она похожа на MS_ASYNC , но означает, что страницы должны записываться на диск при любом удобном случае. Обычно это значит, что они будут сбрасываться на диск при каждом следующем запуске потока ядра bdflush (обычно он запускается каждые 30 секунд), в то время как MS_ASYNC записывает страницы более интенсивно.

 

13.2.5. Блокировка областей памяти

В Linux и многих других современных операционных системах для областей памяти можно организовать страничный обмен с диском (или отклонять, если их невозможно заменить каким-либо другим способом), когда возникает дефицит памяти. На приложения, чувствительные к ограничениям внешней синхронизации, может неблагоприятно повлиять задержка, к которой приводит подкачка страниц обратно в ОЗУ, когда это необходимо процессу. Для улучшения надежности таких приложений Linux позволяет процессу блокировать области памяти в ОЗУ, чтобы сделать эти синхронизации более предсказуемыми. В целях безопасности блокировка памяти разрешена только процессам с полномочиями привилегированного пользователя. Если блокировать области памяти сможет любой процесс, то какой-то неисправный процесс может заблокировать все ОЗУ системы и привести ее к краху. Общее количество памяти, блокируемой процессом, не может превышать предел использования RLIMIT_MEMLOCK (см. главу 10).

Для блокирования и разблокирования областей памяти применяются перечисленные ниже вызовы.

#include

int mlock(caddr_t addr, size_t length);

int mlockall(int flags);

int munlock(caddr_t addr, size_t length);

int munlockall(void);

Первый вызов, mlock(), блокирует length байт, начиная с адреса addr. За один раз должна блокироваться полная страница памяти, поэтому mlock() фактически блокирует все страницы между страницей, содержащей первый адрес, и страницей, содержащей последний адрес, включительно. После завершения mlock() все страницы, на которые распространился вызов, окажутся в ОЗУ.

Если процессу необходимо заблокировать все свое адресное пространство, применяется mlосkall(). Аргумент flags принимает значение одного или обоих описанных ниже флагов, объединенных с помощью битового "ИЛИ".

MCL_CURRENT Все страницы, в данный момент находящиеся в адресном пространстве процесса, блокируются в ОЗУ. После завершения вызова mlockall() они все будут в ОЗУ.
MCL_FUTURE Все страницы, добавленные к адресному пространству процесса, будут заблокированы в ОЗУ.

Разблокирование памяти — это почти то же, что ее блокирование. Если процесс больше не нуждается в блокировании памяти, munlockall() разблокирует все его страницы. munlock() принимает те же аргументы, что и mlock(), и разблокирует страницы, относящиеся к указанной области.

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

 

13.3. Блокирование файлов

 

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

Существует два типа блокирования файлов. Наиболее распространенное — рекомендательное блокирование, которое ядром принудительно не осуществляется. Это просто соглашение, которому должны следовать все процессы, имеющие доступ к файлу. Второй тип, обязательное блокирование, принудительно выполняется ядром. Когда процесс блокирует файл для записи, другие процессы, пытающиеся прочитать или записать в файл, приостанавливаются до снятия блокировки. Хотя этот метод может показаться более очевидным, обязательное блокирование вынуждает ядро совершать проверку наличия блокировок при каждом вызове read() и write(), существенно снижая производительность этих системных вызовов.

Операционная система Linux предоставляет два метода блокирования файлов: блокировочные файлы и блокирование записей.

 

13.3.1. Блокировочные файлы

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

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

fd = open("somefile.lck", O_RDONLY, 0644);

if (fd >= 0) {

 close(fd);

 printf("файл уже заблокирован");

 return 1;

} else {

 /* блокировочный файл не существует, мы можем заблокировать его

    и получить доступ */

 fd = open("somefile.lck", O_CREAT | O_WRONLY, 0644");

 if (fd < 0) {

  perror("ошибка при создании блокировочного файла");

  return 1;

 }

 /* можем записать pid в файл */

 close(fd);

}

Когда процесс заканчивает обработку файла, он вызывает unlink("somefile.lck") для снятия блокировки.

Несмотря на то что показанный выше фрагмент кода выглядит корректным, он позволяет при некоторых обстоятельствах нескольким процессам блокировать один файл, а именно этого и следует избегать в блокировании. Если процесс проверяет существование блокировочного файла, убеждается в том, что блокировочный файл не существует, и прерывается ядром, чтобы позволить выполняться прочим процессам, то какой-то другой процесс сможет заблокировать файл до того, как исходный процесс создаст блокировочный файл. Флаг O_EXCL для open() может сделать создание блокировочного файла атомарным и, следовательно, защищенным от условия состязаний. После установки O_EXCL вызов open() завершается неудачей, если файл уже существует. Это упрощает создание блокировочных файлов, которое происходит так, как показано ниже.

fd = open("somefile.lck", O_WRONLY | O_CREAT | O_EXCL, 0644);

if (fd < 0 && errno == EEXIST) {

 printf("файл уже заблокирован");

 return 1;

} else if (fd < 0) {

 perror("непредвиденная ошибка при проверке блокировки");

 return 1;

}

/* можем записать pid в файл */

close(fd);

Блокировочные файлы используются для блокирования широкого ряда стандартных файлов Linux, включая последовательные порты и файл /etc/passwd. Хотя они хорошо работают со многими приложениями, им присущи и несколько серьезных недостатков.

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

• Флаг O_EXCL надежен только в локальных файловых системах. Ни одна из сетевых файловых систем, поддерживаемых Linux, не сохраняет семантику O_EXCL между несколькими машинами, блокирующими общий файл.

• Блокирование является только рекомендательным; процессы могут обновить файл, несмотря на существование блокировки.

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

 

13.3.2. Блокировка записей

С целью преодоления проблем, присущих блокировочным файлам, в System V и BSD 4.3 была добавлена блокировка записей, реализуемая с помощью системных вызовов lockf() и flock() соответственно. Стандарт POSIX определил третий механизм для блокировки записей, который использует системный вызов fcntl(). Хотя Linux поддерживает все три интерфейса, мы обсудим только интерфейс POSIX, поскольку сейчас его поддерживают почти все платформы Unix. Кроме того, функция lockf() реализована как интерфейс для fcntl(), поэтому оставшаяся часть данного обсуждения касается обоих методов.

Существуют два значительных отличия между блокировками записей и блокировочными файлами. Во-первых, блокировки записей применяются к произвольной части файла. Например, процесс А может заблокировать байты с 50-го по 200-й файла, в то время как другой процесс блокирует байты с 2500-го по 3000-й без конфликта двух блокировок. Мелкомодульное блокирование полезно, когда нескольким процессам необходимо обновить один файл. Еще одно преимущество блокирования записей заключается в том, что блокировки удерживаются в ядре, а не в файловой системе. По окончании процесса все блокировки, которые он содержит, освобождаются.

Как и блокировочные файлы, блокировки POSIX также являются рекомендательными. Linux, как и System V, предоставляет обязательный вариант блокирования записей, который можно использовать, но нарушая при этом переносимость. Блокирование файлов может работать или не работать в сетевой файловой системе (NFS). В последних версиях Linux блокирование файлов работает в NFS, если на всех машинах, участвующих в блокировке, выполняется демон блокировки NFS lockd.

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

Множество блокировок одного процесса никогда не конфликтуют друг с другом.

Если процесс имеет блокировку чтения на байтах 200—250 и пытается установить блокировку записи на байты 200–225, ему это удастся. Исходная блокировка смещается и становится блокировкой чтения на байтах 226–250, а новая блокировка записи устанавливается на байты 200–225. Это позволяет предотвратить взаимоблокировку одного процесса (хотя ситуация взаимоблокировки нескольких процессов по-прежнему возможна).

Блокирование записей POSIX осуществляется с помощью системного вызова fcntl(). В главе 11 было показано, что fcntl() выглядит следующим образом.

#include

int fcntl (int fd, int command, long arg);

Для всех операций блокировки третий параметр (arg) указывает на структуру struct flock, представленную ниже.

#include

struct flock {

 short l_type;

 short l_whence;

 off_t l_start;

 off_t l_len;

 pid_t l_pid;

};

Первый элемент, l_type, определяет тип установленной блокировки.

F_RDLCK Устанавливается блокировка чтения (разделяемая).
F_WRLCK Устанавливается блокировка записи (эксклюзивная).
F_UNLCK Снимается существующая блокировка.

Следующие два элемента, l_whence и l_start, определяют начало области тем же способом, что и файловые смещения, передаваемые в lseek(). l_whence сообщает о способе интерпретации l_start и принимает одно из значений SEEK_SET, SEEK_CUR или SEEK_END; более подробно эти значения рассматривались в главе 11. Следующий элемент, l_len, сообщает размер блокировки в байтах. Если l_len равно 0, считается, что блокировка распространяется до конца файла. Последний элемент, l_pid, используется только тогда, когда запрашиваются блокировки. Он устанавливается в идентификатор процесса, владеющего запрашиваемой блокировкой.

Существуют три команды fcntl(), относящиеся к блокировке файла. Они передаются fcntl() во втором аргументе, fcntl() возвращает -1 в случае ошибки и 0 — в противном случае. Ниже перечислены допустимые значения параметра command.

F_SETLK Устанавливает блокировку, описанную в arg . Если блокировку невозможно выдать из-за конфликта с блокировками других процессов, возвращается EAGAIN . Если l_type устанавливается в F_UNLCK , существующая блокировка снимается.
F_SETLKW Подобно F_SETLK , но блокирует только при условии предоставления блокировки. Если сигнал поступает во время блокирования процесса, вызов fcntl() возвращает EAGAIN .
F_GETLK Проверяет возможность выдачи описанной в arg блокировки. Если блокировка предоставляется, содержимое struct flock не меняется, кроме l_type , который устанавливается в F_UNLCK . Если блокировка не выдается, l_pid устанавливается в идентификатор процесса, содержащего конфликтующую блокировку. Значение 0 возвращается независимо от того, будет ли предоставлена блокировка.

Хотя F_GETLK позволяет процессу проверить, будет ли выдана блокировка, следующий код все еще не сможет получить блокировку.

fcntl(fd, F_GETLK, &lockinfo);

if (lockinfо.l_type != F_UNLCK) {

 fprintf(stderr, "конфликт блокировок\n");

 return 1;

}

lockinfо.l_type = F_RDLCK;

fcntl(fd, F_SETLK, &lockinfo);

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

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

 1: /* lock.с */

 2:

 3: #include

 4: #include

 5: #include

 6: #include

 7:

 8: /* выводит сообщение и ожидает нажатия

 9:    пользователем клавиши */

10: void waitforuser(char * message) {

11:  char buf[10];

12:

13:  printf("%s", message);

14:  fflush(stdout);

15:

16:  fgets(buf, 9, stdin);

17: }

18:

19: /* Получает блокировку заданного типа на файловом дескрипторе fd.

20:    Типом блокировки может быть F_UNLCK, F_RDLCK или F_WRLCK */

21: void getlock(int fd, int type) {

22:  struct flock lockinfo;

23:  char message[80];

24:

25:  /* будет блокироваться весь файл */

26:  lockinfo.l_whence = SEEK_SET;

27:  lockinfo.l_start = 0;

28:  lockinfo.l_len = 0;

29:

30:  /* продолжать попытки, пока того желает пользователь */

31:  while (1) {

32:   lockinfo.l_type = type;

33:   /* если блокировка получена, немедленно возвратиться */

34:   if (!fcntl(fd, F_SETLK, &lockinfo)) return;

35:

36:   /* найти, кто удерживает конфликтующую блокировку */

37:   fcntl(fd, F_GETLK, &lockinfo);

38:

39:   /* есть шанс, что блокировка освобождена между F_SETLK

40:      и F_GETLK; проверить, существует ли еще конфликт

41:      перед тем, как сообщать об этом */

42:   if (lockinfo.l_type != F_UNLCK) {

43:    sprintf (message, "конфликт с процессом %d... нажмите "

44:     " для повторения:", lockinfo.l_pid);

45:    waitforuser(message);

46:   }

47:  }

48: }

49:

50: int main(void) {

51:  int fd;

52:

53:  /* подготовить файл для блокировки */

54:  fd = open("testlockfile", O_RDWR | O_CREAT, 0666);

55:  if (fd < 0) {

56:   perror("open");

57:   return 1;

58:  }

59:

60:  printf("получение блокировки чтения\n");

61:  getlock(fd, F_RDLCK);

62:  printf("блокировка чтения получена\n");

63:

64:  waitforuser("\nдля продолжения нажмите :");

65:

66:  printf("освобождение блокировки\n");

67:  getlock(fd, F_UNLCK);

68:

69:  printf("получение блокировки записи\n");

70:  getlock(fd, F_WRLCK);

71:  printf("блокировка записи получена\n");

72:

73:  waitforuser("\nдля завершения нажмите :");

74:

75:  /* при закрытии файла блокировки освобождаются */

76:

77:  return 0;

78: }

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

1. Открытие одного файла дважды, что дает два разных файловым дескриптора.

2. Поучение блокировки чтения на одной области в обоих файловых дескрипторах.

3. Закрытие одного из файловых дескрипторов.

Выдана лишь одна блокировка чтения, потому что была задействована только одна пара (pid, inode) (вторая попытка блокировки удалась, поскольку блокировки одного и того же процесса никогда не конфликтуют), и после закрытия одного из файловых дескрипторов у процесса больше нет блокировок на файле.

После fork() родительский процесс сохраняет свои файловые блокировки, но дочерний процесс — нет. Если бы дочерние процессы наследовали блокировки, два процесса пришли бы, в конечном счете, к блокировке записи на одной области файла.

Однако файловые блокировки наследуются в exec(). Поскольку в POSIX не определено, что происходит с блокировками после exec(), все варианты Unix сохраняют их.

 

13.3.3. Обязательные блокировки

И Linux, и System V поддерживают как обычные, так и обязательные блокировки. Обязательные блокировки устанавливаются и реализуются с помощью того же механизма fcntl(), который используется для рекомендательной блокировки записей. Блокировки считаются обязательными, если бит setgid заблокированного файла установлен, но его бит группового выполнения — нет. В противном случае применяется рекомендательное блокирование.

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

Обязательное блокирование записи приводит к большей потере производительности, чем рекомендательное блокирование, поскольку каждый вызов read() и write() должен быть проверен на предмет конфликтов с блокировками. Оно также не настолько переносимо, как рекомендательное блокирование POSIX, поэтому в большинстве приложений обязательное блокирование применять не следует.

 

13.3.4. Аренда файла

И рекомендательное, и обязательное блокирование предназначены для предотвращения доступа процесса к файлу или его части, которая используется другим процессом. Когда блокировка установлена, процесс, которому необходим доступ к файлу, должен подождать завершения процесса, владеющего блокировкой. Эта структура подходит для большинства применений, но иногда программа использует файл до тех пор, пока он не понадобится другой программе, и желает получить при необходимости эксклюзивный доступ к файлу. Для этого Linux предлагает механизм аренды файлов (в других системах это называется периодическими блокировками (oplocks)).

Взятие файла в аренду позволяет процессу получать уведомления (через сигнал) о доступе к файлу со стороны другого процесса. Существуют два типа аренды: аренда чтения и аренда записи. Аренда чтения вызывает передачу сигнала при открытии файла для записи, открытии с указанием O_TRUNC или вызове truncate(). Аренда записи также посылает сигнал при открытии файла для чтения22. Аренды файлов работают только для модификаций, внесенных в файл той же системой, которая владеет арендой. Если файл локальный (не файл, доступ к которому возможен через сеть), любой подходящий доступ к файлу инициирует сигнал. Если доступ к файлу возможен через сеть, передачу сигнала вызывают только процессы на одной машине с процессо-арендатором; доступ с любой другой машины удается в случае отсутствия аренды.

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

F_SETLEASE

Аренда создается или освобождается в зависимости от значения последнего параметра, передаваемого в fcntl(); F_RDLCK создает аренду чтения, F_WRLCK — аренду записи, a F_UNLCK освобождает любую аренду, которая может существовать. Если запрашивается новая аренда, она заменяет любую существующую аренду. В случае ошибки возвращается отрицательное число; ноль или положительное число свидетельствуют об успехе операции.

F_GETLEASE

Возвращается тип аренды, существующей в настоящий момент для файла (F_RDLCK, F_WRLCK или F_UNLCK).

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

Использование F_SETSIG дает один значительный эффект. По умолчанию siginfo_t не передается обработчику при доставке SIGIO. Если используется F_SETSIG, даже когда сигналом, передаваемым в ядро, является SIGIO, a SA_SIGINFO был установлен при регистрации обработчика сигнала, файловый дескриптор, аренда которого инициировала событие, передается в обработчик сигналов одновременно с элементом siginfo_t по имени si_fd. Это позволяет применять отдельный сигнал к аренде множества файлов, в то время как si_fd сообщает сигналу, какому файлу необходимо уделить внимание.

Единственные два системных вызова, которые могут инициировать передачу сигнала для арендуемого файла — это open() и truncate(). Когда они вызываются процессом для арендуемого файла, они блокируются, и процессу-владельцу передается сигнал, open() или truncate() завершаются после удаления аренды с файла (или его закрытия процессом-владельцем, что вызывает удаление аренды). Если процесс, удерживающий аренду, не отменяет снятие в течение времени, указанного в файле /proc/sys/fs/lease-break-time, ядро прерывает аренду и позволяет завершиться запускающему системному вызову.

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

 1: /* leases.с */

 2:

 3: #define GNU_SOURCE

 4:

 5: #include

 6: #include

 7: #include

 8: #include

 9: #include

10:

11: const char ** fileNames;

12: int numFiles;

13:

14: void handler (int sig, siginfo_t * siginfo, void * context) {

15:  /* Когда аренда истекает, вывести сообщение и закрыть файл.

16:     Предполагается, что первый открываемый файл получит файловый

17:     дескриптор 3, следующий - 4 и так далее. */

18:

19:  write(1, "освобождение", 10);

20:  write(1, fileNames[siginfo->si_fd - 3],

21:  strlen(fileNames[siginfo->si_fd - 3]));

22:  write(1, "\n", 1);

23:  fcntl(siginfo->si_fd, F_SETLEASE, F_UNLCK);

24:  close(siginfo->si_fd);

25:  numFiles--;

26: }

27:

28: int main(int argc, const char ** argv) {

29:  int fd;

30:  const char ** file;

31:  struct sigaction act;

32:

33:  if (argc < 2) {

34:   fprintf(stderr, "использование: %s +\n", argv[0]);

35:   return 1;

36:  }

37:

38:  /* Зарегистрировать обработчик сигналов. Указав SA_SIGINFO, предоставить

39:     обработчику возможность узнать, какой файловый дескриптор имеет

40:     истекшую аренду. */

41:  act.sa_sigaction = handler;

42:  act.sa_flags = SA_SIGINFO;

43:  sigemptyset(&act.sa_mask);

44:  sigaction(SIGRTMIN, &act, NULL);

45:

46:  /* Сохранить список имен файлов в глобальной переменной, чтобы

47:     обработчик сигналов мог иметь доступ к нему. */

48:  fileNames = argv + 1;

49:  numFiles = argc - 1;

50:

51:  /* Открыть файлы, установить используемые сигнал

52:     и создать аренду */

53:  for (file = fileNames; *file; file++) {

54:   if ((fd = open(* file, O_RDONLY)) < 0) {

55:    perror("open");

56:    return 1;

57:   }

58:

59:   /* Для правильного заполнения необходимо использовать F_SETSIG

60:      для структуры siginfo */

61:   if (fcntl(fd, F_SETSIG, SIGRTMIN) < 0) {

62:    perror("F_SETSIG");

63:    return 1;

64:   }

65:

66:   if (fcntl(fd, F_SETLEASE, F_WRLCK) < 0) {

67:    perror("F_SETLEASE");

68:    return 1;

69:   }

70:  }

71:

72:  /* Пока файлы остаются открытыми, ожидать поступления сигналов. */

73:  while (numFiles)

74:   pause();

75:

76:  return 0;

77: }

 

13.4. Альтернативы

read()

и

write()

 

Несмотря на то что системные вызовы read() и write() как нельзя лучше подходят приложениям для извлечения и хранения данных в файле, все же они не всегда являются самыми быстрыми методами. Они допускают управление отдельными порциями данных; для записи же нескольких порций данных требуется несколько системных вызовов. Подобным образом, если приложению необходим доступ к данным в разных частях файла, оно должно вызывать lseek() между каждым read() или write(), удваивая количество необходимых системных вызовов. Для улучшения эффективности существуют другие системные вызовы.

 

13.4.1. Разбросанное/сборное чтение и запись

Приложениям часто требуется читать и записывать данные различных типов в последовательные области файла. Несмотря на то что это можно делать сравнительно легко с помощью множества вызовов read() и write(), такое решение не является особо эффективным. Вместо этого приложения могут перемещать все данные в последовательную область памяти, делая возможным один системный вызов. Однако эти действия приводят к множеству ненужных операций с памятью.

Linux предлагает системные вызовы readv() и writev(), реализующие разбросанное/сборное чтение и запись. В отличие от стандартных элементов своего уровня, получающих по одному указателю и размеру буфера, эти системные вызовы получают массивы записей, каждая запись которых описывает буфер. Буферы читаются или записываются в том порядке, в каком они приведены в массиве. Каждый буфер описывается с помощью структуры struct iovec.

#include

struct iovec {

 void * iov_base; /* адрес буфера */

 size_t iov_len; /* длина буфера */

};

Первый элемент, iov_base, указывает на буферное пространство. Элемент iov_len — это количество символов в буфере. Эти элементы представляют собой то же, что и второй и третий параметры, передаваемые read() и write().

Ниже показаны прототипы readv() и writev().

#include

int readv(int fd, const struct iovec * vector, size_t count);

int writev(int fd, const struct iovec * vector, size_t count);

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

Ниже приведена простая программа-пример, использующая writev() для отображения простого сообщения на стандартном устройстве вывода.

 1: /* gather.с */

 2:

 3: #include

 4:

 5: int main(void) {

 6:  struct iovec buffers[3];

 7:

 8:  buffers[0].iov_base = "hello";

 9:  buffers[0].iov_len = 5;

10:

11:  buffers[1].iov_base = " ";

12:  buffers[1].iov_len = 1;

13:

14:  buffers[2].iov_base = "world\n";

15:  buffers[2].iov_len = 6;

16:

17:  writev(1, buffers, 3);

18:

19:  return 0;

20: }

 

13.4.2. Игнорирование указателя файла

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

lseek(fd, SEEK_SET, offset1);

read(fd, buffer, bufferSize);

offset2 = someOperation(buffer);

lseek(fd, SEEK_SET, offset2);

read(fd, buffer2, bufferSize2);

offset3 = someOperation(buffer2);

lseek(fd, SEEK_SET, offset3);

read(fd, buffer3, bufferSize3);

Необходимость поиска нового расположения с помощью lseek() перед каждым read() удваивает количество системных вызовов, поскольку указатель файла никогда не располагается правильно после read() из-за непоследовательной природы хранения данных в файле. Существуют альтернативы read() и write(), принимающие смещение файла в качестве параметра, и ни одна из альтернатив не использует указатель файла, чтобы выяснить, к какой части файла можно получить доступ, или какую его часть можно обновить. Обе функции работают только применительно к просматриваемым файлам, поскольку непросматриваемые файлы можно читать или записывать только в текущем расположении.

#define XOPEN_SOURCE 500

#include

size_tpread(int fd, void * buf, size_t count, off_t offset);

size_t pwrite(int fd, void * buf, size_t count, off_t offset);

#endif

Это выглядит подобно прототипам read() и write() с четвертым параметром, offset. offset определяет, с какой точки файла следует читать, а в какую — записывать. Как и их "тезки", эти функции возвращают количество переданных байтов. Ниже приведена версия pread(), реализованная с помощью read() и lseek(), что облегчает понимание ее функции.

int pread (int fd, void * data, int size, int offset) {

 int oldOffset;

 int rc;

 int oldErrno;

 /* переместить указатель файла в новое расположение */

 oldOffset = lseek(fd, SEEK_SET, offset);

 if (oldOffset < 0) return -1;

 rc = read(fd, data, size);

 /* восстановить указатель файла, предварительно сохранив errno */

 oldErrno = errno;

 lseek(fd, SEEK_SET, oldOffset);

 errno = oldErrno;

 return rc;

}

 

Глава 14

Операции с каталогами

 

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

 

14.1. Текущий рабочий каталог

 

14.1.1. Поиск текущего рабочего каталога

Функция getcwd() позволяет процессу найти имя своего текущего каталога относительно корневого каталога системы.

#include

char * getcwd(char * buf, size_t size);

Первый параметр, buf, указывает на буфер, хранящий путь к текущему каталогу. Если длина текущего пути превышает size - 1 байт (-1 позволяет пути завершаться символом '\0'), функция возвращает ошибку ЕRANGE. Если вызов удается, возвращается buf; в случае ошибки возвращается NULL. Несмотря на то что в большинстве современных оболочек поддерживается переменная окружения PWD, хранящая путь в текущий каталог, ее значение необязательно равняется значению, возвращаемому getcwd(). PWD часто содержит элементы путей, являющиеся символическими ссылками на другие каталоги, но getcwd() всегда возвращает путь, свободный от символических ссылок.

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

char * buf;

int len = 50;

buf = malloc(len);

while (!getcwd(buf, len) && errno == ERANGE) {

 len += 50;

 buf = realloc(buf, len);

}

Как и многие другие Unix-подобные системы, Linux предоставляет полезное расширение POSIX-спецификации getcwd(). Если buf является NULL, функция распределяет буфер, размер которого достаточен для содержания текущего пути, с помощью нормального механизма malloc(). Несмотря на то что вызывающий код должен позаботиться о надлежащем освобождении памяти, используемой результатом, это расширение обеспечивает лучшую очистку, нежели цикл, как показано в предыдущем примере.

Функция BSD по имени getwd() является наиболее распространенной альтернативой getcwd(), но ее определенные дефекты привели к разработке getcwd().

#include

char * getwd(char * buf);

Как и getcwd(), getwd() заполняет buf текущим путем, хотя функция не имеет представления о размере buf. getwd() никогда не записывает в буфер больше, чем PATH_MAX (определенная в ), что позволяет программам избегать переполнения буферов, но не предоставляет программе механизма поиска правильного пути, если он превышает PATH_MAX байт. Эта функция поддерживается Linux только для унаследованных приложений и не может использоваться новыми приложениями. Вместо этого применяйте правильную и более переносимую функцию getcwd().

Если для пользователей необходимо отображать текущий путь каталога, хорошим решением будет проверка переменной окружения PWD. Если она установлена, она содержит путь, который применяется пользователем и который может содержать символические ссылки на некоторые элементы пути. Этот путь обычно и должен отображаться приложением по желанию пользователя. Для облегчения задачи библиотека С в Linux предоставляет функцию get_current_dir_name(), реализуемую следующим образом.

char * get_current_dir_name() {

char * env = getenv("PWD");

if (env)

 return strdup(env);

else

 return getcwd(NULL, 0);

}

 

14.1.2. Специальные файлы

.

и

..

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

Еще одно специальное имя файла, .., является родительским каталогом текущего каталога. В случае корневого каталога .. относится к самому корневому каталогу (поскольку у корневого каталога нет родительского каталога).

И ., и .. можно применять везде, где можно использовать имя каталога. Нормально то, что отношение символических ссылок к путям вроде ../include/mylib и именам файлов наподобие /./foo/.././bar/./fubar/../../usr/bin/less является законным (хотя эти названия довольно запутаны).

 

14.1.3. Смена текущего каталога

Предусмотрено два системных вызова, меняющих текущий каталог процесса: chdir() и fchdir().

#include

int chdir(const char * pathname);

int fchdir(int fd);

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

 

14.2. Смена корневого каталога

Хотя в системе имеется один корневой каталог, значение / может меняться для каждого процесса в системе. Это обычно делается для предотвращения доступа к файловой системе со стороны сомнительных процессов (например, демоны ftp, обрабатывающие запросы ненадежных пользователей). Например, если в качестве корневого каталога процесса определен /home/ftp, запуск chdir("/") сделает текущий каталог процесса /home/ftp, a getcwd() вернет / для поддержания последовательности данного процесса. С целью обеспечения безопасности, если процесс пытается выполнить chdir("/.."), он остается в своем каталоге / (каталог /home/ftp в масштабах всей системы), так же как и нормальные процессы, выполняющие chdir("/..") остаются в корневом каталоге в масштабах всей системы. Процесс может легко изменять свой текущий корневой каталог с помощью системного вызова chroot(). Но путь нового корневого каталога процесса интерпретируется с помощью текущего установленного корневого каталога, поэтому chroot("/") не модифицирует текущий корневой каталог процесса.

#include

int chroot(const char * path);

Здесь path определяет новый корневой каталог для процесса. Этот системный вызов, однако, не изменяет текущий каталог процесса. У процесса все еще есть доступ к файлам в текущем каталоге, а также в родственном ему каталоге (../../directory/file). Большинство процессов, выполняющих chroot(), немедленно меняют свои текущие каталоги, чтобы находиться внутри новой корневой иерархии, с помощью chdir("/") или чего-либо подобного. Отмена этого действия может вызвать проблемы с безопасностью в некоторых приложениях.

 

14.3. Создание и удаление каталогов

 

14.3.1. Создание новых каталогов

Создание новых каталогов выполняется очень просто.

#include

#include

int mkdir(const char * dirname, mode_t mode);

Путь, определенный в dirname, создается как новый каталог с полномочием mode (что модифицируется umask процесса). Если dirname определяет существующий файл, или некоторые элементы dirname не являются каталогом или символической ссылкой на него, системный вызов не удается.

 

14.3.2. Удаление каталогов

Удаление каталога — это практически то же, что и удаление файла; меняется разве что имя системного вызова.

#include

int rmdir(char * pathname);

Для успешного выполнения rmdir() каталог должен быть пустым (он не должен содержать ничего, кроме вездесущих . и ..); в противном случае возвращается ENOTEMPTY.

 

14.4. Чтение содержимого каталога

 

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

#include

DIR * opendir(const char * pathname);

int closedir(DIR * dir);

Системный вызов opendir() возвращает указатель на тип данных DIR, который является абстрактным (как и структура stdio по имени FILE) и которым не следует манипулировать вне библиотеки С. Поскольку каталоги можно открывать только для чтения, нет необходимости определять, в каком режиме открывается каталог, opendir() срабатывает только в случае существования каталога — этот вызов нельзя использовать для создания новых каталогов (для этого служит mkdir()). Закрытие каталога может не сработать только в случае некорректного значения аргумента dir.

После открытия каталога его элементы читаются последовательно до конца каталога.

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

#include

struct dirent * readdir (DIR * dir);

Вызывающему коду возвращается указатель на структуру struct dirent. Несмотря на то что struct dirent содержит несколько элементов, единственным переносимым элементом является d_name, содержащий имя файла элемента каталога. Остальные элементы struct dirent зависят от системы. Однако интересным является элемент d_ino, содержащий inode-номер файла.

Самой сложной частью этого процесса является определение ошибки. К сожалению, readdir() возвращает NULL, и когда происходит ошибка, и когда в каталоге больше нет элементов. Чтобы различать эти две ситуации, необходимо проверять errno. Эта задача усложняется тем, что readdir() не меняет errno, пока не произойдет ошибка. Это означает, что для корректной проверки ошибок errno необходимо установить перед вызовом readdir() в заранее известное значение (обычно 0). Ниже показана простая программа, записывающая имена файлов текущего каталога в stdout.

 1: /* dircontents.с */

 2:

 3: #include

 4: #include

 5: #include

 6:

 7: int main(void) {

 8:  DIR * dir;

 9:  struct dirent * ent;

10:

11:  /* "." - текущий каталог */

12:  if (!(dir = opendir("."))) {

13:   perror("opendir");

14:   return 1;

15:  }

16:

17:  /* установить errno в 0, чтобы можно было выяснить, когда readdir() даст сбой*/

18:  errno = 0;

19:  while ((ent = readdir(dir))) {

20:   puts (ent->d_name);

21:   /* сбросить errno, поскольку puts() может модифицировать ее */

22:   errno = 0;

23:  }

24:

25:  if (errno) {

26:   perror("readdir");

27:   return 1;

28:  }

29:

30:  closedir(dir);

31:

32:  return 0;

33: }

 

14.4.1. Прохождение по каталогу

Если требуется перечитать содержимое каталога, уже открытого opendir(), с помощью rewinddir() структура DIR сбрасывается, чтобы следующий вызов readdir() мог вернуть первый файл в каталоге.

#include

int rewinddir(DIR * dir);

 

14.5. Универсализация файловых имен

 

Большинство пользователей Linux принимают как должное то, что запуск ls *.с не сообщает сведения о файле в текущем каталоге, именем которого является *.с. Вместо этого они ожидают увидеть список всех файлов в текущем каталоге, имена которых заканчиваются на .с. Это расширение имени файла от *.с до ladsh.с dircontents.с (например) обычно обрабатывается оболочкой, которая универсализирует все параметры для программ, выполняющихся под ее управлением. Программы, помогающие пользователям манипулировать файлами, тоже часто нуждаются в универсализации файловых имен. Существуют два распространенных способа универсализации имен файлов внутри программ.

 

14.5.1. Использование подпроцесса

Самый старый метод универсализации предусматривает запуск оболочки в качестве дочернего процесса и указание ей универсализировать файловые имена. Стандартная функция popen() (см. главу 10) упрощает этот метод — просто запустите команду ls *.с с помощью popen() и прочитайте результат. Этот подход может показаться несколько упрощенным, но все же он обеспечивает переносимое решение проблемы универсализации (вот почему приложения вроде Perl используют его).

Ниже приведена программа, которая универсализирует все аргументы и отображает все совпадения.

 1: /* popenglob.c */

 2:

 3: #include

 4: #include

 5: #include

 6: #include

 7:

 8: int main(int argc, const char ** argv)

 9:  char buf[1024];

10:  FILE * ls;

11:  int result;

12:  int i;

13:

14:  strcpy(buf, "ls");

15:

16:  for (i = 1; i < argc; i++) {

17:   strcat(buf, argv[i]);

18:   strcat(buf, " ");

19:  }

20:

21:  ls = popen(buf, "r");

22:  if (!ls) {

23:   perror("popen");

24:   return 1;

25:  }

26:

27:  while (fgets(buf, sizeof(buf), ls))

28:   printf("%s", buf);

29:

30:  result = pclose(ls);

31:

32:  if (!WIFEXITED(result)) return 1;

33:

34:  return 0;

35: }

 

14.5.2. Внутренняя универсализация

Если необходимо универсализировать несколько файловых имен, запуск нескольких подоболочек с помощью popen() будет неэффективным. Функция glob() позволяет универсализировать имена файлов без запуска каких-либо подпроцессов, однако за счет увеличения сложности и снижения переносимости. Несмотря на то что вызов glob() описан в стандарте POSIX.2, многие варианты Unix до сих пор его не поддерживают.

#include

int glob(const char * pattern, int flags,

int (*errfunc)(const char * epath, int eerrno), glob_t* pglob);

Первый параметр, pattern, определяет шаблон, которому должны соответствовать имена файлов. В нем допускается применение операций универсализации *, ? и [], а также необязательно {, } и ~ которые трактуются так же, как в стандартных оболочках. Последний параметр указывает на структуру, которая заполняется результатами универсализации. Эта структура определена следующим образом.

#include

typedef struct {

 int gl_pathc; /* количество путей в gl_pathv */

 char **gl_pathv; /* список gl_pathc, соответствующих именам путей */

 int gl_offs; /* пространство, зарезервированное в gl_pathv для GLOB_DOOFFS*/

} glob_t;

flags — это одно или несколько перечисленных ниже значений, объединенных с помощью битового "ИЛИ".

GLOB_ERR Возвращается в случае ошибки (если функция не может прочесть оглавление каталога, например, из-за проблем с доступом).
GLOB_MARK Если шаблон соответствует имени каталога, при возврате к этому имени будет добавлен символ / .
GLOB_NOSORT Обычно возвращаемые имена путей сортируются в алфавитном порядке. Если этот флаг установлен, они не сортируются.
GLOB_DOOFFS При установке первые строки pglob->gl_offs в возвращаемом списке имен путей оставляются пустыми. Это позволяет использовать glob() во время выстраивания ряда аргументов, которые будут переданы прямо в execv() .
GLOB_NOCHECK Если ни одно из файловых имен не соответствует шаблону, в качестве единственного совпадения возвращается сам шаблон (обычно не возвращается ни одного совпадения). В обоих случаях шаблон возвращается, если он не содержит операций универсализации.
GLOB_APPEND pglob предположительно является действительным результатом предыдущего вызова glob() , и любые результаты этого вызова добавляются к результатам предыдущего вызова. Это облегчает универсализацию множества шаблонов.
GLOB_NOESCAPE Обычно если операции универсализации предшествует символ \ , она воспринимается как обычный символ. Например, шаблон а\* обычно соответствует только файлу по имени а* . Если устанавливается GLOB_NOESCAPE , символ \ теряет свое особое значение, aa\* соответствует любому имени файла, начинающемуся с символов а\ . В таком случае имена а\. и a\bcd будут соответствовать, но arachnid — нет, поскольку оно не содержит \ .
GLOB_PERIOD Большинство оболочек не позволяют применять операции универсализации для файловых имен, начинающихся с . (запустите ls * в своем домашнем каталоге и сравните полученное с результатом ls - а . ). Функция glob() обычно ведет себя подобным образом, но GLOB_PERIOD позволяет операциям универсализации работать с ведущим символом. Значение GLOB_PERIOD в POSIX не определено.
GLOB_BRACE Многие оболочки (следуя примеру csh ) разворачивают последовательности с фигурными скобками как альтернативы; например, шаблон {a, b} разворачивается до a b , а шаблон a {, b, c} — до a ab ас . GLOB_BRACE делает возможным такое поведение. Значение GLOB_BRACE в POSIX не определено.
GLOB_NOMAGIС Действует подобно GLOB_NOCHECK за исключением того, что он добавляет шаблон к списку результатов только в том случае, если она не содержит специальных знаков. Значение GLOB_NOMAGIC в POSIX не определено.
GLOB_TILDE Включает расширение с тильдой, в котором ~ или подстрока ~/ разворачиваются до пути к домашнему каталогу текущего пользователя, а ~user — до пути к домашнему каталогу пользователя user. Значение GLOB_TILDE в POSIX не определено.
GLOB_ONLYDIR Совпадает только с каталогами, а не с другими типами файлов. Значение GLOB_ONLYDIR в POSIX не определено.

Часто glob() наталкивается на каталоги, к которым у процесса нет доступа, что вызывает ошибки. Хотя ошибку можно каким-то образом обработать, однако если glob() возвращает ошибку (GLOB_ERR), операцию универсализации нельзя перезапустить там, где предыдущая операция универсализации столкнулась с ошибкой. Поскольку сложно одновременно устранять ошибки, происходящие во время выполнения glob(), и завершать универсализацию, glob() позволяет передать ошибку в специально предусмотренную для этого функцию, которая определяется в третьем параметре glob().

Прототип этой функции показан ниже.

int globerr(const char * pathname, int globerrno);

Функции передается путевое имя, вызвавшее ошибку, и значение errno, возвращенное одним из системных вызовов opendir(), readdir() или stat(). Если функция ошибки возвращает величину больше нуля, glob() возвращается с ошибкой. В противном случае операция универсализации продолжается.

Результаты универсализации хранятся в структуре glob_t, на которую ссылается pglob. Она включает описанные ниже элементы, позволяющие абоненту найти согласованные имена файлов.

gl_pathc Количество путевых имен, соответствующих шаблону.
gl_pathv Массив путевых имен, соответствующих шаблону.

После использования возвращенного результата glob_t занимаемую им память следует освободить, передав его в globfree().

void globfree(glob_t * pglob);

Системный вызов glob() возвращает GLOB_NOSPACE в случае нехватки памяти, GLOB_ABEND, если ошибка чтения привела к неудачному выполнению функции, GLOB_NOMATCH, если соответствия не были найдены, или 0, если функция выполнилась удачно и нашла соответствия.

Для иллюстрации работы glob() ниже приведена программа globit, которая принимает множество шаблонов в качестве аргументов, универсализирует их и отображает результат. В случае ошибки отображается сообщение, описывающее ошибку, а операция универсализации продолжается.

 1: /* globit.с */

 2:

 3: #include

 4: #include

 5: #include

 6: #include

 7: #include

 8:

 9: /* Это функция ошибки, которая передается в glob(). Она просто отображает

10:    сообщение об ошибке и возвращает состояние успеха, что позволяет glob()

11:    продолжить работу. */

12: int errfn(const char * pathname, int theerr) {

13:  fprintf(stderr, "ошибка при доступе к %s: %s\n", pathname,

14:  strerror(theerr));

15:

16:  /* Операция универсализации должна продолжаться, поэтому вернуть 0 */

17:  return 0;

18: }

19:

20: int main(int argc, const char ** argv) {

21:  glob_t result;

22:  int i, rc, flags;

23:

24:  if (argc < 2) {

25:   printf("необходимо передать хотя бы один аргумент\n") ;

26:   return 1;

27:  }

28:

29:  /* установить flags в 0; позже он будет изменен на GLOB_APPEND */

30:  flags = 0;

31:

32:  /* совершить проход по всем аргументам командной строки */

33:  for (i = 1; i < argc; i++) {

34:   rc = glob(argv[i], flags, errfn, &result);

35:

36:   /* благодаря errfn, GLOB_ABEND не происходит */

37:   if (rc == GLOB_NOSPACE) {

38:    fprintf(stderr, "не хватает памяти для выполнения универсализации\n");

39:    return 1;

40:   }

41:

42:   flags |= GLOB_APPEND;

43:  }

44:

45:  if (!result.gl_pathc) {

46:   fprintf(stderr, "соответствий нет\n");

47:   rc = 1;

48:  } else {

49:   for (i = 0; i < result.gl_pathc; i++)

50:    puts(result.gl_pathv[i]);

51:   rc = 0;

52:  }

53:

54:  /* структура glob_t занимает память из пула malloc(),

55:     которая должна быть освобождена */

56:  globfree(&result);

57:

58:  return rc;

59: }

 

14.6. Добавление к

ladsh

возможностей работы с каталогами и универсализацией

 

Продолжим эволюцию ladsh, добавив к ladsh3.с четыре новых возможности.

1. Встроенная команда cd для смены каталогов.

2. Встроенная команда pwd для отображения текущего каталога.

3. Универсализация файловых имен.

4. Отображение ряда новых сообщений, позволяющее воспользоваться преимуществами strsignal(). Эти изменения обсуждались в главе 12.

 

14.6.1. Добавление встроенных команд

cd

и

pwd

Добавление встроенных команд является прямым применением вызовов chdir() и getcwd(). Код соответствует runProgram() как раз там, где обрабатываются другие встроенные команды. Ниже показан раздел обработки встроенных команд в ladsh3.с.

422: if (!strcmp(newJob.progs[0].argv[0], "exit")) {

423:  /* здесь должен возвращаться реальный код завершения */

424:  exit(0);

425: } else if (!strcmp(newJob.progs[0].argv[0], "pwd")) {

426:  len = 50;

427:  buf = malloc(len);

428:  while (!getcwd(buf, len) && errno == ERANGE) {

429:   len += 50;

430:   buf = realloc(buf, len);

431:  }

432:  printf("%s\n", buf);

433:  free(buf);

434:  return 0;

435: } else if (!strcmp(newJob.progs[0].argv[0], "cd")) {

436:  if (!new Job.progs[0].argv[1] == 1)

437:   newdir = getenv("HOME");

438:  else

439:   newdir = newJob.progs[0].argv[1];

440:  if (chdir(newdir))

441:   printf("сбой при смене текущего каталога: %s\n",

442:  strerror(errno));

443:  return 0;

444: } else if (!strcmp(newJob.progs[0].argv[0], "jobs")) {

445:  for (job = jobList->head; job; job = job->next)

446:   printf(JOB_STATUS_FORMAT, job->jobId, "Выполняется",

447:    job -> text);

448:  return 0;

449: }

 

14.6.2. Добавление универсализации файловых имен

Универсализацию файловых имен, при которой оболочка разворачивает символы *, [] и ? в соответствующие файловые имена, в определенной мере сложно реализовать из-за разнообразных методов применения кавычек. Первая модификация заключается в построении каждого аргумента в виде строки, подходящей для передачи в glob(). Если символ универсализации помещен в кавычки, принятые в оболочке (например, двойные кавычки), тогда символу универсализации предшествует \ с целью предотвращения его разворачивания в glob(). Этот процесс реализуется легко, хотя с первого взгляда может показаться сложным.

Две части синтаксического разбора команд в parseCommand() необходимо слегка изменить. Последовательности " и ' обрабатываются ближе к началу цикла, что обеспечивает разделение командной строки на аргументы. Если во время синтаксического разбора мы находимся в середине строки в кавычках и сталкиваемся с символом универсализации, мы заключаем его в кавычки с предваряющим символом \, что выглядит следующим образом.

189: } else if (quote) {

190:  if (*src == '\\') {

191:   src++;

192:   if (!*src) {

193:    fprintf(stderr,

194:     "после \\ ожидался символ\n");

195:    freeJob(job);

196:    return 1;

197:   }

198:

199:   /* в оболочке "\'" должен дать \' */

200:   if (* src ! = quote) *buf++ = '\\';

201:  } else if (*src = '*' | | *src == '?' || *src == '[' ||

202:   *src == ']')

203:   *buf++ = '\\';

204:  *buf++ = *src;

205: } else if (isspace(*src)) {

В код были добавлены только средний else if и оператор присваивания в его теле. Похожий код потребуется предусмотреть для обработки символов \, встречающихся вне строк в кавычках. Это реализовано в конце главного цикла parseCommand(). Ниже приведен измененный код.

329: case '\\':

330:  src++;

331:  if (!*src) {

332:   freeJob(job);

333:   fprintf(stderr, "после \\ ожидался символ\n");

334:   return 1;

335:  }

336:  if (* src == '*' || *srс == '[' | | *src == ']'

337:   || *srс == '?')

338:   *buf++ = '\\';

339:  /* сквозная обработка */

340: default:

341:  *buf++ = *src;

Для заключения знаков универсализации в кавычки здесь был добавлен тот же самый код.

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

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

Для облегчения управления памяти к struct childProgram добавляется элемент globResult типа glob_t, используемый для хранения результатов всех операций универсализации. Кроме того, добавляется целочисленный элемент freeGlob, не равный нулю, если freeJob() должна освободить globResult. Ниже показано полное описание struct childProgram в ladsh3.c.

35: struct childProgram {

36:  pid; /* 0, если завершена */

37:  char ** argv; /* имя и аргументы программы */

38:  int numRedirections; /* элементы в массиве перенаправлений */

39:  struct redirection Specifier * redirections; /* перенаправления ввода-вывода */

40:  glob_t globResult; /* результат универсализации параметров */

41:  int freeGlob; /* нужно ли освобождать globResult? */

42: };

Во время первого запуска для командной строки функция globLastArgument() (когда argc для текущей дочерней оболочки равно 1) инициализирует globResult. Для остальных аргументов она пользуется преимуществом GLOB_APPEND для добавления новых совпадений к существующим. Это избавляет от необходимости распределения собственной памяти для целей универсализации, поскольку одиночный glob_t при необходимости автоматически расширяется.

Если globLastArgument() не находит совпадений, символы \ с кавычками удаляются из аргумента. В противном случае все новые совпадения копируются в список аргументов, создаваемый для дочерней программы.

Ниже приведена полная реализация globLastArgument(). Все сложные ее части относятся к управлению памятью; фактическая универсализация похожа на реализованную в программе globit.с, которая представлена ранее в главе.

 87: void globLastArgument(struct childProgram * prog, int * argcPtr,

 88:  int * argcAllocedPtr) {

 89:  int argc = *argcPtr;

 90:  int argcAlloced = *argcAllocedPtr;

 91:  int rc;

 92:  int flags;

 93:  int i;

 94:  char * src, * dst;

 95:

 96:  if (argc >1) {/* cmd->globResult уже инициализирован */

 97:   flags = GLOB_APPEND;

 98:   i = prog->globResult.gl_pathc;

 99:  } else {

100:   prog->freeGlob = 1;

101:   flags = 0;

102:   i = 0;

103:  }

104:

105:  rc = glob(prog->argv[argc - 1], flags, NULL, &prog->globResult);

106:  if (rc == GLOB_NOSPACE) {

107:   fprintf (stderr, "не хватает памяти для выполнения универсализации\n");

108:   return;

109:  } else if (rc == GLOB_NOMATCH ||

110:   (!rc && (prog->globResult.gl_path - i) == 1 &&

111:   !strcmp(prog->argv[argc - 1],

112:   prog->globResult.gl_pathv[i]))) {

113:    /* необходимо удалить кавычки в \, если они все еще присутствуют */

114:    src = dst = prog->argv[argc - 1];

115:    while (*src) {

116:     if (*src ! = '\\') *dst++ = *src;

117:     src++;

118:    }

119:    *dst = '\0';

120:   } else if (!rc) {

121:   argcAlloced += (prog->globResult.gl_pathc - i);

122:   prog->argv = realloc(prog->argv,

123:   argcAlloced * sizeof(*prog->argv));

124:   memcpy(prog->argv + (argc - 1),

125:    prog->globResult.gl_pathv + i,

126:    sizeof(*(prog->argv)) *

127:    (prog->globResult.gl_pathc - i));

128:   argc += (prog->globResult.gl_pathc - i - 1);

129:  }

130:

131:  *argcAllocedPtr = argcAlloced;

132:  *argcPtr = argc;

133: }

Последними изменениями касаются вызовов globLastArgument(), которые должны совершаться после синтаксического разбора нового аргумента. Вызовы добавляются в двух местах: когда за пределами строки в кавычках найдены пробелы и когда вся командная строка разобрана. Оба вызова выглядят следующим образом:

globLastArgument(prog, &argc, &argvAlloced);

Полный код ladsh3.с доступен на Web-сайте издательства, а также на сайте, посвященном книге.

 

14.7. Обход деревьев файловых систем

 

Существуют две функции, которые облегчают приложениям просмотр всех файлов каталога, включая файлы в подкаталогах. Рекурсивный просмотр всех элементов древовидной структуры (например, файловой системы) часто называется обходом (walk) дерева и он реализуется функциями ftw() и nftw(). nftw() представляет собой усовершенствованную версию ftw.

 

14.7.1. Использование

ftw()

#include

int ftw(const char *dir, ftwFunctionPointer callback, int depth);

Функция ftw() начинает с каталога dir и вызывает указанную в callback функцию для каждого файла в этом каталоге и его подкаталогах. Функция вызывается для всех типов файлов, включая символические ссылки и каталоги. Реализация ftw() открывает каждый найденный каталог (с помощью файлового дескриптора) и для увеличения производительности не закрывает их, пока не закончит чтение всех элементов каталога. Это означает, что он использует количество файловых дескрипторов, равное количеству уровней подкаталогов. Чтобы предотвратить недостаток файловых дескрипторов в приложении, параметр depth ограничивает количество файловых дескрипторов ftw(), остающихся одновременно открытыми. Если этот предел достигается, производительность снижается, поскольку каталоги необходимо часто открывать и закрывать.

Функция, на которую указывает callback, определяется следующим образом:

int ftwCallbackFunction(const char * file, const struct stat * sb, int flag);

Эта функция вызывается один раз для каждого файла в дереве каталогов, а первый параметр, file, представляет собой имя файла, начинающееся с dir, которое передается ftw(). Например, если бы аргумент dir принимал значение ., одним из файловых имен было бы ./bashrc. Если бы вместо этого использовалось /etc, имя файла выглядело бы как /etc/hosts.

Следующий аргумент обратного вызова, sb, указывает на структуру struct stat как на результат применения stat() к файлу. Аргумент flag предоставляет информацию о файле и принимает одно из следующих значений.

FTW_F Файл не является символической ссылкой или каталогом.
FTW_D Файл является каталогом либо символической ссылкой, указывающей на каталог.
FTW_DNR Файл является каталогом, полномочий на чтение которого у приложения нет (то есть его обход невозможен).
FTW_SL Файл является символической ссылкой.
FTW_NS Файл является объектом, к которому не удалось применить stat() . Примером может служить файл в каталоге, права на чтение которого приложение имеет (приложение может получить список файлов этого каталога), но не имеет права на выполнение (что предотвращает успешный вызов stat() применительно к файлам этого каталога).

Когда файл является символической ссылкой, ftw() пытается последовать за этой ссылкой и вернуть информацию о файле, на который она указывает (ftw() проходит один и тот же каталог несколько раз, если на него имеется несколько символических ссылок). Однако для поврежденной ссылки не определено, вернется FTW_SL или FTW_NS. Это хорошая причина, чтобы использовать nftw().

Если функция обратного вызова возвращает ноль, обход каталога продолжается. Если возвращается ненулевое значение, обход дерева файлов завершается и эта величина возвращается функцией ftw(). При удачном завершении обхода ftw() возвращает ноль, а в случае ошибки возвращается -1.

 

14.7.2. Обход дерева файлов с помощью

nft()

Новая версия ftw() — nftw() — решает неоднозначность символических ссылок, присущих ftw(), и содержит несколько дополнительных свойств. С целью правильного определения nftw() заголовочными файлами значение _XOPEN_SOURCE в приложении должно быть определено 500 или больше. Ниже показан прототип nftw().

#define _XOPEN_SOURCE 600

#include

int nftw(const char * dir, ftwFunctionPointer callback, int depth, int flags);

int nftwCallbackFunction(const char *file, const struct stat * sb,

 int flag, struct FTW * ftwInfo);

Сравнивая nftw() с ftw(), легко заметить новый параметр — flags. Это может быть один или несколько следующих флагов, объединенных с помощью логического "ИЛИ".

FTW_CHDIR Функция nftw() обычно не меняет текущий каталог программы. Если определен флаг FTW_CHDIR функция nftw() меняет текущий каталог на любой другой каталог, читаемый в данный момент. Иначе говоря, при активизации обратного вызова имя файла, передаваемое ему, всегда относится к текущему каталогу.
FTW_DEPTH По умолчанию nftw() выводит имя каталога перед списком имен файлов в этом каталоге. Этот флаг вызывает изменение порядка на обратный, то есть содержимое каталога выводится перед его именем. ( Примечание . Этот флаг заставляет nftw() выполнять поиск в глубину. Подобного флага для поиска в ширину не существует.)
FTW_MOUNT Это флаг запрещает nftw() переходить границу файловой системы во время обхода. Более подробно файловые системы описаны в [32].
FTW_PHYS Вместо следования символическим ссылкам nftw() сообщает о ссылках, но не следует по ним. Побочный эффект заключается в том, что обратный вызов получает результат вызова lstat() , а не stat() .

Аргумент обратного вызова flag может принимать два новых значения для nftw() вдобавок к значениям, уже упомянутым для ftw().

FTW_DP Этот элемент является каталогом, об оглавлении которого уже сообщили (это может произойти только в случае установки FTW_DEPTH ).
FTW_SLN Этот элемент является символической ссылкой, указывающей на несуществующий файл (поврежденная ссылка). Это происходит только в том случае, если FTW_PHYS не был установлен; если же он был установлен, передается FTW_SL .

Эти дополнительные значения flag надлежащим образом определяют nftw() для символических ссылок. При использовании FTW_PHYS все символические ссылки возвращают FTW_SL. Без nftw() поврежденные ссылки выдают FTW_NS, а другие символические ссылки дают тот же результат, что и цель ссылки.

Обратный вызов для nftw() принимает еще один аргумент, ftwInfо. Это указатель на struct FTW, которая определяется следующим образом.

#define _XOPEN_SOURCE 600

#include

struct FTW {

 int base;

 int level;

};

Элемент base — это смещение имени файла в полном пути, передаваемое обратному вызову. Например, если переданный полный путь выглядит как /usr/bin/ls, base будет равно 9, a file + ftwInfo->base даст имя файла ls. level — это количество каталогов под текущим каталогом. Если ls был найден в nftw(), начинающемся с /usr, уровень будет равен 1. Если поиск начался с /usr/bin, уровень будет равен 0.

 

14.7.3. Реализация

find

Команда find выполняет в одном или нескольких деревьях каталогов поиск файлов, соответствующих определенным характеристикам. Ниже приведена простая реализация find, реализованная на основе nftw(). Она использует fnmatch() (см. главу 23) для реализации переключателя -name и иллюстрирует многие флаги, воспринимаемые nftw().

 1: /* find.с */

 2:

 3: #define _XOPEN_SOURCE 600

 4:

 5: #include

 6: #include

 7: #include

 8: #include

 9: #include

10: #include

11:

12: const char * name = NULL;

13: int minDepth = 0, maxDepth = INT_MAX;

14:

15: int find (const char * file, const struct stat * sb, int flags,

16:  struct FTW * f) {

17:  if (f->level < minDepth) return 0;

18:  if (f->level > maxDepth) return 0;

19:  if (name && fnmatch(name, file + f->base, FNM_PERIOD)) return 0;

20:

21:  if (flags == FTW_DNR) {

22:   fprintf(stderr, "find: %s: недопустимые полномочия\n", file);

23:  } else {

24:   printf("%s\n", file);

25:  }

26:

27:  return 0;

28: }

29:

30: int main(int argc, const char ** argv) {

31:  int flags = FTW_PHYS;

32:  int i;

33:  int problem = 0;

34:  int tmp;

35:  int rc;

36:  char * chptr;

37:

38:  /* поиск первого параметры командной строки (который должен

39:     находиться после списка путей */

40:  i = 1;

41:  while (i < argc && *argv[i] != '-') i++;

42:

43:  /* обработать опции командной строки */

44:  while (i < argc && !problem) {

45:   if (!strcmp(argv[i], "-name")) {

46:    i++;

47:    if (i == argc)

48:     problem = 1;

49:    else

50:     name = argv[i++];

51:   } else if (!strcmp(argv[i], "-depth")) {

52:    i++;

53:    flags |= FTW_DEPTH;

54:   } else if (!strcmp (argv[i], "-mount") ||

55:    !strcmp(argv[i], "-xdev")) {

56:    i++;

57:    flags |= FTW_MOUNT;

58:   } else if (!strcmp (argv[i], "-mindepth") ||

59:    !strcmp (argv[i], "-maxdepth")) {

60:    i++;

61:    if (i == argc)

62:     problem = 1;

63:    else {

64:     tmp = strtoul(argv[i++], &chptr, 10);

65:     if (*chptr)

66:      problem = 1;

67:     else if (!strcmp(argv[i - 2], "-mindepth"))

68:      minDepth = tmp;

69:     else

70:      maxDepth = tmp;

71:    }

72:   }

73:  }

74:

75:  if (problem) {

76:   fprintf(stderr, "использование: find <пути> [-name <строка>]"

77:    "[-mindepth <целое>] [-maxdepth <целое>]\n");

78:   fprintf(stderr, " [-xdev] [-depth]\n");

79:   return 1;

80:  }

81:

82:  if (argc == 1 || *argv[1] == '-') {

83:   argv[1] = ".";

84:   argc = 2;

85:  }

86:

87:  rc = 0;

88:  i = 1;

89:  flags = 0;

90:  while (i < argc && *argv[i] != '-')

91:   rc |= nftw (argv [i++], find, 100, flags);

92:

93:  return rc;

94: }

 

14.8. Уведомление о смене каталога

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

Системный вызов fcntl() используется для регистрации уведомлений об обновлениях каталога. В главе 11 уже говорилось о том, что этот системный вызов принимает три аргумента. Первый аргумент — это интересующий файловый дескриптор, второй — это команда, которую необходимо выполнить fcntl(), а последний — это целое число, специфическое для этой команды. Для уведомлений каталогов первый аргумент является файловым дескриптором, относящимся к интересующему каталогу. Это единственный случай, при котором каталог следует открывать с помощью нормального системного вызова open() вместо opendir(). Командой регистрации уведомлений является F_NOTIFY, а последний аргумент определяет, какие типы событий вызывают отправку сигнала. Это должен быть один или несколько перечисленных ниже флагов, объединенных по логическому "ИЛИ".

DN_ACCESS Файл в каталоге, который читается.
DN_ATTRIB Права владения или доступа к файлу в каталоге были изменены.
DN_CREATE В каталоге создан новый файл (включая новые жесткие ссылки на уже существующие файлы).
DN_DELETE Файл удален из каталога.
DN_MODIFY Файл в каталоге был модифицирован (тип модификации — усечение).
DN_RENAME Файл в каталоге был переименован.

Для отмены уведомления о событии вызовите fcntl() с командой F_NOTIFY и последним аргументом, равным нулю.

Обычно уведомление каталога автоматически отменяется после передачи одного сигнала. Для эффективного уведомления каталога окончательный аргумент для fcntl() должен быть объединен операцией "ИЛИ" с DN_MULTISHOT, что вызывает отправку сигналов для всех подходящих событий до отмены уведомления.

По умолчанию для уведомления каталога передается SIGIO. Если приложение желает использовать для этого другой сигнал (например, для разных каталогов могут понадобиться разные сигналы), можно применить команду F_SETSIG в fcntl(), а в качестве последнего аргумента определить нужный сигнал. Если используется F_SETSIG (даже если установлен сигнал SIGIO), ядро также помещает файловый дескриптор на каталог в элементе si_fd аргумента обработчика сигналов siginfo_t, позволяя приложению узнать, какие из контролируемых каталогов обновились.

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

Ниже приведена программа, использующая уведомление о смене каталога для вывода сообщений об удалении либо добавлении файлов в любые контролируемые ею каталоги (их количество указывается в командной строке). Она отказывается принять SIGRTMIN при смене каталога и использует si_fd, чтобы обнаружить, какой именно каталог был изменен. С целью предотвращения условий состязаний программа использует сигналы с очередизацией и блокирование сигналов. Сигнал может быть доставлен только один раз — при вызове sigsuspend() в строке 203. Это обеспечивает повторное сканирование каталога в случае внесения изменений в каталог во время его сканирования; иначе эти изменения останутся незамеченными. Использование сигналов с очередизацией разрешает любые изменения каталога во время работы программы; эти сигналы доставляется при каждом новом вызове sigsuspend(), гарантируя, что ничего не пропущено.

  1: /* dirchange.с */

  2:

  3: #define _GNU_SOURCE

  4: #include

  5: #include

  6: #include

  7: #include

  8: #include

  9: #include

 10: #include

 11: #include

 12:

 13: /* Для сохранения имен файлов из каталога используется связный

 14:    список. Поле exists служит для хранения служебной информации

 15:    при проверке изменений. */

 16: struct fileInfo {

 17:  char * name;

 18:  struct fileInfo * next;

 19:  int exists;

 20: };

 21:

 22: /* Это глобальный массив. Он отображает файловые дескрипторы на пути

 23:    каталогов, сохраняет список файлов в каталоге и предоставляет

 24:    обработчику сигналов место для отображения того факта, что каталог

 25:    должен сканироваться повторно. Последний элемент имеет path,

 26:    равный NULL, обозначающий конец массива. */

 27:

 28: struct directoryInfo {

 29:  char * path;

 30:  int fd;

 31:  int changed;

 32:  struct fileInfo * contents;

 33: } * directoryList;

 34:

 35: /* Это никогда не возвращает пустой список; любой каталог содержит,

 36:    по крайней мере, "." и ".." */

 37: int buildDirectoryList(char * path, struct fileInfo ** listPtr) {

 38:  DIR * dir;

 39:  struct dirent * ent;

 40:  struct fileInfo * list = NULL;

 41:

 42:  if (!(dir = opendir(path))) {

 43:   perror("opendir");

 44:   return 1;

 45:  }

 46:

 47:  while ((ent = readdir(dir))) {

 48:   if (!list) {

 49:    list = malloc(sizeof(*list));

 50:    list->next = NULL;

 51:    *listPtr = list;

 52:   } else {

 53:    list->next = malloc(sizeof(*list));

 54:    list = list->next;

 55:   }

 56:

 57:   list->name = strdup(ent->d_name);

 58:  }

 59:

 60:  if (errno) {

 61:   perror("readdir");

 62:   closedir(dir);

 63:   return 1;

 64:  }

 65:

 66:  closedir(dir);

 67:

 68:  return 0;

 69: }

 70:

 71: /* Сканирует путь каталога в поисках изменений предыдущего

 72:    содержимого, как указано *listPtr. Связанный список

 73:    обновляется новым содержимым, и выводятся сообщения,

 74:    описывающие произошедшие изменения. */

 75: int updateDirectoryList(char * path, struct fileInfo ** listPtr) {

 76:  DIR * dir;

 77:  struct dirent * ent;

 78:  struct fileInfo * list = *listPtr;

 79:  struct fileInfo * file, * prev;

 80:

 81:  if (!(dir = opendir(path))) {

 82:   perror("opendir");

 83:   return 1;

 84:  }

 85:

 86:  for (file = list; file; file = file->next)

 87:   file->exists = 0;

 88:

 89:  while ((ent = readdir(dir))) {

 90:   file = list;

 91:   while (file && strcmp(file->name, ent->d_name))

 92:    file = file->next;

 93:

 94:   if (!file) {

 95:    /* новый файл, добавить его имя в список */

 96:    printf("%s создан в %s\n", ent->d_name, path);

 97:    file = malloc(sizeof(*file));

 98:    file->name = strdup(ent->d_name);

 99:    file->next = list;

100:    file->exists = 1;

101:    list = file;

102:   } else {

103:    file->exists = 1;

104:   }

105:  }

106:

107:  closedir(dir);

108:

109:  file = list;

110:  prev = NULL;

111:  while (file) {

112:   if (!file->exists) {

113:    printf("%s удален из %s\n", file->name, path);

114:    free(file->name);

115:

116:    if (!prev) {

117:     /* удалить головной узел */

118:     list = file->next;

119:     free(file);

120:     file = list;

121:    } else {

122:     prev->next = file->next;

123:     free(file);

124:     file = prev->next;

125:    }

126:   } else {

127:    prev = file;

128:    file = file->next;

129:   }

130:  }

131:

132:  *listPtr = list;

133:

134:  return 0;

135: }

136:

137: void handler(int sig, siginfo_t * siginfo, void * context) {

138:  int i;

139:

140:  for (i = 0; directoryList[i].path; i++) {

141:   if (directoryList[i].fd == siginfo->si_fd) {

142:    directoryList[i].changed = 1;

143:    return;

144:   }

145:  }

146: }

147:

148: int main(int argc, char ** argv) {

149:  struct sigaction act;

150:  sigset_t mask, sigio;

151:  int i;

152:

153:  /* Блокировать SIGRTMIN. Мы не хотим получать его нигде,

154:     кроме как внутри системного вызова sigsuspend(). */

155:  sigemptyset(&sigio);

156:  sigaddset(&sigio, SIGRTMIN);

157:  sigprocmask(SIG_BLOCK, &sigio, &mask);

158:

159:  act.sa_sigaction = handler;

160:  act.sa_flags = SA_SIGINFO;

161:  sigemptyset(&act.sa_mask);

162:  sigaction(SIGRTMIN, &act, NULL);

163:

164:  if (!argv[1]) {

165:   /* ни одного аргумента не передано, привести argc/argv

166:      к виду ".", как будто передается единственный аргумент */

167:   argv[1] = ".";

168:   argc++;

169:  }

170:

171:  /* каждый аргумент представляет собой отслеживаемый каталог */

172:  directoryList = malloc(sizeof(*directoryList) * argc);

173:  directoryList[argc - 1].path = NULL;

174:

175:  for (i = 0; i < (argc - 1); i++) {

176:   directoryList[i].path = argv[i + 1];

177:   if ((directoryList[i].fd =

178:    open(directoryList[i].path, O_RDONLY)) < 0) {

179:    fprintf(stderr, "ошибка при открытии %s: %s\n",

180:    directoryList[i].path, strerror(errno));

181:    return 1;

182:   }

183:

184:   /* Отслеживание каталога перед первым сканированием;

185:      это гарантирует, что мы захватим файлы, созданные кем-то

186:      во время сканирования каталога. Если кто-то изменит его,

187:      будет сгенерирован сигнал (и заблокирован, пока

188:      мы не будем готовы принять его) */

189:   if (fcntl(directoryList[i].fd, F_NOTIFY, DN_DELETE |

190:    DN_CREATE | DN_RENAME | DN_MULTISHOT) ) {

191:    perror("fcntl F_NOTIFY");

192:    return 1;

193:   }

194:

195:   fcntl(directoryList[i].fd, F_SETSIG, SIGRTMIN);

196:

197:   if (build DirectoryList(directoryList[i].path,

198:    &directoryList[i].contents))

199:    return 1;

200:  }

201:

202:  while (1) {

203:   sigsuspend(&mask);

204:

205:   for (i = 0; directoryList[i].path; i++)

206:    if (directoryList[i].changed)

207:     if (updateDirectoryList(directoryList[i].path,

208:      &directoryList[i].contents))

209:      return 1;

210:  }

211:

212:  return 0;

213: }

 

Глава 15

Управление заданиями

 

Управление заданиями — возможность, стандартизованная в POSIX.1 и предоставляемая многими другими стандартами — позволяет одному терминалу выполнять несколько заданий. Задание (job) — это один процесс или группа процессов, обычно соединенных каналами. Для перемещения заданий между передним планом и фоном и предотвращения доступа к терминалу фоновых заданий предусмотрены специальные механизмы.

 

15.1. Основы управления заданиями

 

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

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

 

15.1.1. Перезапуск процессов

Каждый процесс может пребывать в трех состояниях: выполнение, останов и "зомби". Выполняющиеся процессы завершаются системным вызовом exit() или отправкой сигнала фатального завершения. Процессы перемещаются между состояниями работы и остановки исключительно посредством сигналов, сгенерированных другим процессом, ядром либо ими самими.

Когда процесс получает SIGCONT, ядро перемещает его из состояния останова в состояние выполнения; если процесс уже работает, сигнал не влияет на его состояние. Процесс может захватить сигнал; ядро в это время будет перемещать процесс в рабочее состояние перед передачей сигнала.

 

15.1.2. Остановка процессов

Четыре сигнала перемещают работающий процесс в состояние останова. SIGSTOP никогда не генерируется ядром. Он предназначен для остановки произвольных процессов. Его невозможно захватить или проигнорировать; он всегда останавливает целевой процесс. Остальные три сигнала, останавливающие процессы — SIGTSTP, SIGTTIN и SIGTTOU — могут генерироваться терминалом, на котором работает процесс, или другим процессом. Хотя эти сигналы ведут себя похожим образом, они генерируются при разных обстоятельствах.

SIGTSTP

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

SIGTTIN

Когда фоновый процесс пытается считывать из терминала, ему передается SIGTTIN.

SIGTTOU

Этот сигнал обычно генерируется фоновым процессом, пытающимся выполнить запись в свой терминал. Сигнал генерируется только в случае установки атрибута терминала TOSTOP, как рассматривается в главе 16.

Данный сигнал генерируется также фоновым процессом, вызывающим tcflush(), tcflow(), tcsetattr(), tcsetpgrp(), tcdrain() или tcsendbreak().

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

 

15.1.3. Обработка сигналов управления заданиями

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

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

При получении SIGTSTP процесс должен немедленно выполнить все необходимые ему действия, чтобы разрешить приостановку (например, восстановление исходного состояния терминала) и приостановиться самому. Самый простой способ приостановки процесса — передача самому себе сигнала SIGSTOP. Однако большинство оболочек отображают сообщения с типом сигнала, вызвавшего остановку процесса, и если процесс передаст себе sigstop, он будет отличаться от большинства приостановленных процессов. Во избежание этого неудобства многие приложения сбрасывают свой обработчик SIGTSTP в SIG_DFL и передают себе SIGTSTP.

Процессам, которые требуют специальный код для правильных приостановок, обычно необходимо выполнять специальные действия при перезапуске. Это легко делается предоставлением обработчика сигнала для SIGCONT, который выполняет такие действия. Если процесс приостанавливается с помощью SIGTSTP, такие специальные действия, возможно, включают установку обработчика сигнала для SIGTSTP.

Ниже приведен код простого обработчика сигналов SIGCONT и SIGTSTP. Когда пользователь приостанавливает или перезапускает процесс, последний отображает сообщение перед остановкой или продолжением.

 1: /* monitor.с */

 2:

 3: #include

 4: #include

 5: #include

 6: #include

 7:

 8: void catchSignal(int sigNum, int useDefault);

 9:

10: void handler(int signum) {

11:  if (signum == SIGTSTP) {

12:   write(STDOUT_FILENO, "получен SIGTSTP\n", 12);

13:   catchSignal(SIGTSTP, 1);

14:   kill(getpid(), SIGTSTP);

15:  } else {

16:   write(STDOUT_FILENO, "получен SIGCONT\n", 12);

17:   catchSignal(SIGTSTP, 0);

18:  }

19: }

20:

21: void catchSignal(int sigNum, int useDefault) {

22:  struct sigaction sa;

23:

24:  memset(&sa, 0, sizeof(sa));

25:

26:  if (useDefault)

27:   sa.sa_handler = SIG_DFL;

28:  else

29:   sa.sa_handler = handler;

30:

31:  if (sigaction(sigNum, &sa, NULL)) perror("sigaction");

32: }

33:

34: int main() {

35:  catchSignal(SIGTSTP, 0);

36:  catchSignal(SIGCONT, 0);

37:

38:  while(1);

39:

40:  return 0;

41: }

 

15.2. Управление заданиями в

ladsh

Добавление управления заданиями к ladsh — это последнее добавление к простой оболочке, окончательный исходный код которой можно найти в приложении Б. Для начала потребуется добавить по элементу в структуры struct childProgram, struct job и struct jobSet. Поскольку ladsh некоторое время не рассматривался, вернитесь в главу 10, где были впервые представлены эти структуры данных. Ниже показано окончательное определение struct childProgram.

35: struct childProgram {

36:  pid_t pid;           /* 0 на выходе */

37:  char ** argv;        /* имя программы с аргументами * /

38:  int numRedirections; /* элементы в массиве переадресации */

39:  struct redirectionSpecifier * redirections; /* переадресации ввода-вывода */

40:  glob_t globResult;   /* результат универсализации параметров */

41:  int freeGlob;        /* должен ли освобождаться globResult? */

42:  int isStopped;       /* выполняется ли программа в данный момент?*/

43: };

Мы уже различаем работающие и завершенные дочерние программы с помощью элемента pid структуры struct childProgram, равного нулю в случае завершения дочерней программы, а в противном случае содержащего действительный идентификатор процесса. Новый элемент, isStopped, не равен нулю, если процесс был остановлен, в ином же случае он равен нулю. Обратите внимание, что его значение неважно, если pid равен нулю.

Аналогичные изменения потребуется внести и в struct job. Ранее эта структура отслеживала определенное количество программ в задании и количество выполняющихся процессов. Ее новый элемент, stoppedProgs, записывает количество процессов задания, остановленных в данный момент. Он может быть вычислен на основе элементов isStopped дочерних программ, содержащихся в задании, но лучше отслеживать его отдельно. После этого изменения получаем окончательную форму структуры struct job.

45: struct job {

46:  int jobld;         /* номер задания */

47:  int numProgs;      /* количество программ в задании */

48:  int runningProgs;  /* количество выполняющихся программ */

49:  char * text;       /* имя задания */

50:  char * cmdBuf;     /* буфер различных argv */

51:  pid_t pgrp;        /* идентификатор группы процессов задания */

52:  struct childProgram* progs; /* массив программ в задании */

53:  struct job * next; /* для слежения за фоновыми программами */

54:  int stopped Progs; /* количество активных, однако остановленных программ */

55: };

Как и предыдущие версии ladsh, код ladsh4.с игнорирует SIGTTOU. Это делается, чтобы позволить использовать tcsetpgrp() даже тогда, когда оболочка не является процессом переднего плана. Однако поскольку оболочка уже будет поддерживать правильное управление заданиями, дочерним процессам не следует игнорировать сигнал. Как только новый процесс разветвляется с помощью runCommand(), он устанавливает обработчик для SIGTTOU в SIG_DFL. Это позволяет драйверу терминала приостановить фоновые процессы, пытающиеся выполнить запись в терминал или провести с ним еще какие-то действия. Ниже приведен код, который начинается с создания дочернего процесса, где сбрасывается SIGTTOU и выполняется дополнительная работа по синхронизации.

514: pipe(controlfds);

515:

516: if (!(newJob.progs[i].pid = fork())) {

517:  signal(SIGTTOU, SIG_DFL);

518:

519:  close(controlfds[1]);

520:  /* это чтение вернет 0, когда закроется записывающая сторона*/

521:  read(controlfds[0], &len, 1);

522:  close(controlfds[0]);

Канал controlfds используется для приостановки дочернего процесса до того, как оболочка переместит этот процесс в подходящую группу процессов. Закрытием записывающей стороны канала и чтением из считывающей стороны дочерний процесс останавливается до тех пор, пока родительский процесс закроет записывающую сторону, что происходит после вызова setpgid() в строке 546. Этот тип механизма необходим для гарантии того, что дочерний процесс перемещается в группу процессов до происшествия exec(). Если подождать до exec(), то не будет уверенности, что процесс попадет в правильную группу процессов, пока он не начнет доступ к терминалу (который может быть запрещен).

Завершенные дочерние процессы проверяются ladsh два раза. Первый раз это происходит во время ожидания процессов в группе процессов переднего плана. После завершения либо остановки процесса переднего плана ladsh проверяет изменения в состояниях своих фоновых процессов с помощью функции checkJobs(). Обе этих кодовых цепочки необходимо модифицировать с целью обработки остановленных и завершенных дочерних процессов.

Добавление флага WUNTRACED к вызову waitpid(), ожидающему на процессах переднего плана, позволяет заметить также остановленные процессы. Когда процесс скорее останавливается, чем завершается, устанавливается флаг дочернего процесса isStopped и увеличивается номер задания stoppedProgs. Если все программы задания были остановлены, ladsh снова перемещается на передний план и ожидает команды пользователя. Вот как выглядит часть главного цикла ladsh, ожидающая на процессе переднего плана.

708: /* задание выполняется на переднем плане; ожидать его */

709: i = 0;

710: while (!jobList.fg->progs[i].pid ||

711:  jobList.fg->progs[i].isStopped) i++;

712:

713: waitpid(jobList.fg->progs[i].pid, &status, WUNTRACED);

714:

715: if (WIFSIGNALED(status) &&

716:  (WTERMSIG(status) != SIGINT)) {

717:  printf("%s\n", strsignal(status));

718: }

719:

720: if (WIFEXITED(status) || WIFSIGNALED(status)) {

721:  /* дочерний процесс завершен */

722:  jobList.fg->runningProgs--;

723:  jobList.fg->progs[i].pid = 0;

724:

725:  if (!jobList.fg->runningProgs) {

726:   /* дочерний процесс завершен */

727:

728:   removeJob(&jobList, jobList.fg);

729:   jobList. fg = NULL;

730:

731:   /* переместить оболочку на передний план */

732:   if (tcsetpgrp (0, getpid()))

733:    perror("tcsetpgrp");

734:   }

735:  } else {

736:   /* дочерний процесс остановлен */

737:   jobList.fg->stoppedProgs++;

738:   jobList.fg->progs[i].isStopped = 1;

739:

740:   if (jobList.fg->stoppedProgs ==

741:    jobList.fg->runningProgs) {

742:    printf ("\n" JOB_STATUS_FORMAT,

743:     jobList.fg->jobId,

744:     "Остановлен", jobList.fg->text);

745:    jobList.fg = NULL;

746:   }

747:  }

748:

749:  if (!jobList.fg) {

750:   /* переместить оболочку на передний план */

751:   if (tcsetpgrp (0, getpid()))

752:    perror("tcsetpgrp");

753:  }

754: }

Подобным образом фоновые задания могут прерываться с помощью сигналов. Мы снова добавляем WUNTRACED к waitpid(), что проверяет состояния фоновых процессов. После остановки фонового процесса обновляются флаг isStopped и счетчик stoppedProgs, а в случае остановки всего задания выводится сообщение.

Последняя возможность, требуемая для ladsh — перемещение задания между состоянием выполнения на переднем плане, состоянием выполнения в фоне и остановом. Это делается с помощью двух встроенных команд: fg и bg. Они являются ограниченными версиями нормальных команд оболочки, носящих те же имена. Оба принимают один параметр, являющийся номером задания, которому предшествует знак % (для совместимости со стандартными оболочками). Команда fg перемещает определенное задание на передний план, a bg запускает его в фоне.

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

461: } else if (! strcmp(newJob.progs[0].argv[0], "fg") ||

462:  !strcmp(newJob.progs[0].argv[0], "bg")) {

463:  if (!newJob.progs[0].argv[1] || newJob.progs[0].argv[2]) {

464:   fprintf(stderr,

465:    "%s: ожидался в точности один аргумент\n",

466:   newJob.progs[0].argv[0]);

467:   return 1;

468:  }

469:

470:  if (sscanf(newJob.progs[0].argv[l], "%%%d", &jobNum) != 1)

471:   fprintf(stderr, "%s: ошибочный аргумент '%s'\n",

472:   newJob.progs[0].argv[0],

473:   newJob.progs[0].argv[1]);

474:   return 1;

475:  }

476:

477:  for (job = jobList->head; job; job = job->next)

478:   if (job->jobId == jobNum) break;

479:

480:  if (!job) {

481:   fprintf(stderr, "%s: неизвестное задание %d\n",

482:    newJob.progs[0].argv[0], jobNum);

483:   return 1;

484:  }

485:

486:  if (* new Job.progs[0].argv [0] == 'f') {

487:   /* Перевести это задание на передний план */

488:

489:   if (tcsetpgrp(0, job->pgrp))

490:    perror("tcsetpgrp");

491:   jobList->fg = job;

492:  }

493:

494:  /* Перезапустить процессы в задании */

495:  for (i = 0; i < job->numProgs; i++)

496:   job->progs[i].isStopped = 0;

497:

498:  kill (-job->pgrp, SIGCONT);

499:

500:  job->stoppedProgs = 0;

501:

502:  return 0;

503: }

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

 

Глава 16

Терминалы и псевдотерминалы

 

Устройства, предназначенные для интерактивного использования, обладают сходным интерфейсом, который был выведен десятилетия назад для последовательных терминалов TeleType и получил название tty. Интерфейс tty используется для доступа к последовательным терминалам, консолям, терминалам xterm, сетевым регистрационным именам и тому подобному.

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

К сожалению, разработчикам Unix пришлось предпринять несколько попыток совершенствования интерфейса. Они оставили пользователям три разных интерфейса для соединения с устройствами tty. Интерфейсы sgtty (BSD) и termio (System V) теперь вытеснены интерфейсом termios (POSIX), который представляет собой супермножество команд интерфейса termio. Так как все существующие системы поддерживают интерфейс termios, и поскольку это самый мощный интерфейс, мы документируем только termios, а не ранние интерфейсы. (Ради поддержки унаследованного исходного кода Linux поддерживает termio, а также termios. Ранее он также ограниченно поддерживал интерфейс sgtty, но эта поддержка впоследствии была изъята, поскольку этот интерфейс никогда не был идеален, и в нем уже не было существенной потребности.)

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

Устройство tty имеет два конца. Если рассуждать упрощенно, один конец присоединяется к программе, а второй — к аппаратному устройству. Это верно для последовательного порта; в данном случае драйвер последовательного устройства присоединяет последовательный порт (и, таким образом, терминал или модем) к оболочке, редактору или другой программе. Это также верно для консоли; драйвер консоли соединяет клавиатуру и экран с такими же типами программ. Но в некоторых случаях на каждом конце находится по программе; при этом один из концов занимает место оборудования.

Например, при сетевом подключении один конец устройства tty соединяется с программой, предоставляющей сетевое подключение, а второй — с оболочкой, редактором или другой потенциально интерактивной программой. Если программы находятся на каждом конце, вы должны ясно понимать, на каком конце эмулируется оборудование; при сетевом подключении к сети подключается аппаратная сторона.

Устройства tty с программным обеспечением на обоих концах называются псевдотерминалами (pseudo-tty, или же просто pty). В первой части главы они рассматриваться не будут, поскольку программный конец pty обрабатывается так же, как и любое другое устройство tty. Позже мы поговорим о программировании аппаратного конца pty.

 

16.1. Операции tty

 

Устройства tty предоставляют огромное количество опций обработки данных; они относятся к наиболее сложным устройствам ядра. Настраивать можно опции обработки входных и выходных данных, а также потока данных. Также можно контролировать ограниченное манипулирование данными на уровне драйвера устройства.

Интерфейсы tty работают в двух основных режимах: неформатируемый режим и режим обработки. Неформатируемый режим передает данные в приложение без изменений. Режим обработки, известный также как канонический режим, поддерживает ограниченный построчный редактор внутри драйвера устройства и пересылает отредактированные входные данные в приложение по одной строке за раз. Этот режим изначально произрастает из универсальных систем, в которых специализированные устройства обработки входных данных предоставляли режим обработки без прерывания ЦП.

Режим обработки обрабатывает определенные управляющие символы; например, по умолчанию ^U уничтожает (стирает) текущую строку, ^W стирает текущее слово, забой (^Н) или Delete — предыдущий символ, a ^R стирает и затем повторно набирает текущую строку. Каждое из этих управляющих действий может быть повторно назначено другом символу. Например, на многих терминалах символу DEL (код 127) назначается действие забоя.

 

16.1.1. Служебные функции терминалов

Иногда невозможно узнать, соответствует ли файловый дескриптор tty. Чаще всего это связано со стандартным дескриптором выходного файла. Программы, выводящие текст на стандартные устройства, часто форматируются иначе при записи в канал, чем при отображении информации для пользователей. Например, в случае применения команды ls для получения списка файлов, она отобразит несколько колонок при простом запуске (удобнее читать человеку), но когда вы перенаправите ее вывод в другую программу, она отобразит по одному файлу в строке (удобнее читать программе). Запустите ls и ls | cat и почувствуйте разницу.

Определить, соответствует ли файловый дескриптор tty, можно с помощью функции isatty(), принимающей файловый дескриптор в качестве своего аргумента и возвращающей 1, если дескриптор соответствует tty, и 0 в противном случае.

#include

int isatty(int fd);

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

#include

char *ttyname(int fd);

Поскольку поток символов расположен в статическом пространстве, потребуется сделать копию возвращенной строки перед повторным вызовом ttyname(); эта функция не реентерабельна. ttyname() возвращает NULL при любой ошибке, включая случай передачи файлового дескриптора, не ассоциированного с tty.

 

16.1.2. Управляющие терминалы

Каждый сеанс (см. главу 10) привязан к терминалу, с которого процессы сеанса получают свои входные данные и в который пересылают свои выходные данные. Этот терминал может быть локальной консолью машины, терминалом, подключенным через последовательный канал, либо псевдотерминалом, устанавливающим соответствия во внешнем окне или по всей сети (подробнее о псевдотерминалах читайте в конце этой главы). Терминал, к которому относится сеанс, называется управляющим терминалом (или управляющим tty) сеанса. Терминал может быть управляющим терминалом только в одном сеансе за раз.

Нормальные процессы не могут менять свои управляющие терминалы; это может делать только лидер сеанса. В Linux изменение управляющего терминала лидера сеанса не распространяется на другие процессы того же сеанса. Лидеры сеансов почти всегда устанавливают управляющий терминал при запуске до создания каких-либо дочерних процессов, чтобы гарантировать, что все процессы сеанса совместно используют один управляющий терминал.

Существуют два интерфейса для смены управляющего tty лидера сеанса. Первый реализуется с помощью нормальных системных вызовов open() и close().

1. Закройте все файловые дескрипторы, относящиеся к текущему управляющему терминалу.

2. Откройте новый терминал без установки флага O_NOCTTY.

Второй метод включает вызовы ioctl() на отдельных файловых дескрипторах, ссылающихся на старые и новые терминалы.

1. Установите флаг TIOCNOTTY на файловый дескриптор, привязанный к исходному управляющему tty (обычно ioctl(0, TIOCNOTTY, NULL) нормально работает). Это разрывает соединение между сеансом и tty.

2. Установите флаг TIOCSCTTY на файловый дескриптор, привязанный к новому управляющему tty. Это устанавливает новый управляющий tty.

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

Функция tcsetpgrp() позволяет процессу, работающему на терминале, сменить группу процессов переднего плана для этого терминала.

int tcsetpgrp(int ttyfd, pid_t pgrp);

Первый параметр определяет tty, управляющая группа процессов которого изменяется, a pgrp является группой процессов, которую необходимо переместить на передний план. Процессы могут менять группу процессов переднего плана только для своего управляющего терминала. Если процесс, совершающий изменение, не принадлежит к группе процессов переднего плана на этом терминале, генерируется сигнал SIGTTOU, если только он не игнорируется или заблокирован.

 

16.1.3. Принадлежность терминала

Существуют две системные базы данных, используемые для отслеживания зарегистрированных пользователей; utmp применяется для пользователей, зарегистрированных в данный момент, a wtmp является записью всех предыдущих регистраций со времени создания файла. Команда who использует базу данных utmp для отображения списка зарегистрированных пользователей, а команда last — базу данных wtmp для отображения списка пользователей, зарегистрированных в системе после регенерации базы данных wtmp. В системах Linux база данных utmp хранится в файле /var/run/utmp, а база данных wtmp — в файле /var/log/wtmp.

Программы, использующие tty для сеансов регистрации пользователей (независимо от того, ассоциируются ли они с графической регистрацией), должны обновлять эти две системные базы данных, пока пользователь явно не сделает иной запрос; например, некоторые пользователи не хотят, чтобы каждый сеанс оболочки, запускаемый ими в эмуляторе терминала в системе X Window, перечислялся как процесс входа. Добавляйте только интерактивные сеансы, поскольку utmp и wtmp не предназначены для регистрации автоматизированных программ. Любые tty, не являющиеся контролирующими терминалами, обычно в базы данных utmp и wtmp не добавляются.

 

16.1.4. Запись с помощью

utempter

Приложения со встроенными средствами безопасности, использующие pty, имеют недостаточно полномочий для модификации файлов баз данных. Эти приложения должны предоставлять опцию для использования простой вспомогательной программы, доступной в большинстве систем Linux и в некоторых других системах, но не стандартизованной — утилиты utempter. Утилита utempter является setgid (или, при необходимости, setuid) с достаточными полномочиями для модификации баз данных utmp и wtmp. Доступ к ней можно получить через простую библиотеку. Утилита utempter проверяет, владеет ли процесс tty, который пытается войти в базу данных utmp до разрешения операции, utempter предназначена только для pty; другие tty обычно открываются демонами с достаточными полномочиями для модификации файлов системных баз данных.

#include

void addToUtmp(const char *pty, const char *hostname, int ptyfd);

void removeLineFromUtmp(const char *pty, int ptyfd);

void removeFromUtmp(void);

Функция addToUtmp() принимает три аргумента. Первый, pty, является полным путем к добавляемому pty. Второй, hostname, может быть NULL или сетевым именем системы, из которой сетевое подключение использует этот порожденный pty (что запускает ut_host, рассматриваемый в следующем разделе главы). Третий, ptyfd, должен быть открытым файловым дескриптором, ссылающимся на устройство, названное в аргументе pty.

Функция removeLineFromUtmp() принимает два аргумента; они определяются в точности как аргументы с таким же именем, передаваемые функции addToUtmp().

Некоторые существующие приложения записываются с помощью структуры, усложняющей хранение имени и файлового дескриптора для очистки элемента utmp. Из-за этого библиотека utempter поддерживает кэш самого позднего имени устройства и файлового дескриптора, передаваемого addToUtmp(), и удобную функцию removeFromUtmp(), не принимающую никаких аргументов и действующую как removeLineFromUtmp() на кэшированную информацию. Это подходит только для приложений, добавляющих лишь один элемент utmp; более сложные приложения, использующие более одного pty, должны вместо этого применять removeLineFromUtmp().

 

16.1.5. Запись вручную

Область обработки utmp и wtmp является одной из тех противоречивых областей, где механизмы различаются между системами и меняются на протяжении лет; даже определение информации, доступной в utmp и wtmp, до сих пор различается между системами. Изначально utmp и wtmp были просто массивами структур, записанных на диск; через некоторое время были созданы программные интерфейсы приложений (API) для надежной обработки записей.

По крайней мере, два таких интерфейса были официально стандартизованы; исходный интерфейс utmp (описанный в XSI, XPG2 и SVID2) и расширенный интерфейс utmpx (описанный в XPG4.2 и в поздних версиях POSIX). В Linux доступны оба интерфейса (utmp и utmpx). Интерфейс utmp, широко варьирующийся между машинами, имеет набор определений, которые делают возможной запись переносимого кода. Этот код пользуется преимуществом расширений, предоставляемых glibc. Более строго стандартизованный интерфейс utmpx в данный момент не предоставляет эти определения, но все еще поддерживает расширения.

Интерфейс Linux utmp был изначально задуман как супермножество других существующих интерфейсов utmp, a utmpx был стандартизован как супермножество других существующих интерфейсов utmp; к счастью, оба набора во многом одинаковы. В Linux различие между структурами данных utmp и utmpx заключается лишь в букве x.

Если вы не хотите применять расширения, мы рекомендуем использовать интерфейс utmpx, поскольку он наиболее переносим, пока вы не используете расширения, и строго стандартизован.

Однако если вы хотите применять расширения, мы рекомендуем использовать интерфейс utmp, поскольку glibc предоставляет определения, позволяющие записать переносимый код, пользующийся преимуществами расширений.

Существует также смешанный подход — включите оба заголовочных файла и используйте определения, предоставляемые glibc для интерфейса utmp, чтобы решить, применять ли расширения в интерфейсе utmpx. Этого мы не рекомендуем, поскольку нет гарантии, что заголовочные файлы utmp.h и utmpx.h не будут конфликтовать с системами, не относящимися к Linux. Если ожидается максимальная переносимость и функциональность, в одной из этих областей придется записать некоторые коды дважды — первую версию с использованием utmpx для легкого переноса в новые системы, а вторую с применением #ifdef — для максимальной функциональности в каждой новой системе, в которую вы перемещаетесь.

Здесь документируются лишь наиболее распространенные расширения; документация glibc покрывает все поддерживаемые расширения. Функции utmp работают в терминах struct utmp; мы игнорируем некоторые расширения. Структура и функции utmpx работают точно так же, как структура и функции utmp, поэтому мы не документируем их отдельно. Обратите внимание, что такая же структура используется и для utmp, и для wtmp, поскольку обе базы данных очень похожи.

struct utmp {

 short int ut_type;          /* тип входа */

 pid_t ut_pid;               /* идентификатор процесса входа */

 char ut_line[UT_LINESIZE];  /* 32 символа */

 char ut_id[4];              /* идентификатор inittab */

 char ut_user[UT_NAMESIZE];  /* 32 символа */

 char ut_host[UT_HOSTSIZE];  /* 256 символов */

 struct timeval ut_tv;

 struct exit_status ut_exit; /* состояние бездействующего процесса */

 long ut_session;

 int32_t ut_addr_v6[4];

};

Многие одинаковые элементы являются частью struct utmpx под тем же именем. Элементы, от которых не требуется быть элементами struct utmpx, комментируются как "не стандартизованные POSIX" (ни один из них не стандартизован как часть struct utmp, поскольку сама struct utmp не стандартизована).

Элементы массива символов необязательно являются строками, завершающимися NULL. Используйте sizeof() либо другие ограничения размеров благоразумно.

ut_type Одно из следующих значений: EMPTY , INIT_PROCESS , LOGIN_PROCESS , USER_PROCESS , DEAD_PROCESS , BOOT_TIME , NEW_TIME , OLD_TIME , RUN_LVL или ACCOUNTING , каждое из которых описано ниже.
ut_tv Время, ассоциированное с событием. Это единственный элемент, кроме ut_type , определяемый POSIX как всегда подходящий для непустых элементов. В некоторых системах вместо этого есть элемент ut_time , измеряемый только в секундах.
ut_pid Идентификатор ассоциированного процесса для всех типов, заканчивающихся на _PROCESS .
ut_id Идентификатор inittab ассоциированного процесса, для всех типов, заканчивающихся на _PROCESS . Это первое поле в незакомментированных строках файла /etc/inittab , где поля разделены символами : . Сетевые регистрации, не ассоциированные с inittab, могут использовать это по-другому; например, могут включать части информации об устройстве.4
ut_line Строка (базовое имя устройства или номер локального дисплея для X), ассоциированная с процессом. Спецификация POSIX о состоянии ut_line не ясна; она не считает ut_line значащей для LOGIN_PROCESS , но с другой стороны предполагает, что она значащая для LOGIN_PROCESS , и это подтверждается на практике. POSIX утверждает, что ut_line значащая для USER_PROCESS . На практике она также часто значащая для DEAD_PROCESS , в зависимости от происхождения бездействующего процесса.
ut_user Обычно это имя зарегистрированного пользователя; это также может быть имя зарегистрированного процесса (обычно LOGIN ) в зависимости от значения ut_type .
ut_host Имя удаленного хоста, вошедшего в систему или иным образом ассоциированного с этим процессом. Элемент ut_host относится только к USER_PROCESS . Этот элемент не стандартизован POSIX.
ut_exit ut_exit.e_exit дает код завершения, что предоставляется макросом WEXITSTATUS() , a ut_exit.e_termination дает сигнал, вызвавший завершение процесса (если он был завершен сигналом), что предоставляется макросом WTERMSIG() . Этот элемент не стандартизован POSIX.
ut_session Идентификатор сеанса в системе X Window. Этот элемент не стандартизован POSIX.
ut_addr_v6 IP-адрес удаленного хоста в случае активизации USER_PROCESS подключением с удаленного хоста. Используйте функцию inet_ntop() для генерирования печатного содержания. Если первая группа не равна нулю, тогда это адрес IPV4 ( inet_ntop() принимает аргумент AF_INET ); в противном случае это адрес IPV6 ( inet_ntop() принимает аргумент AF_INET6 ). Этот элемент не стандартизован POSIX.

Элемент ut_type устанавливает, каким образом определяются остальные элементы. Некоторые величины ut_type зарезервированы для записи системной информации; они полезны только для специализированных системных программ и документируются не полностью.

EMPTY В данной записи utmp нет достоверных данных (такие записи позже можно повторно использовать), поэтому игнорируйте ее содержимое. Другие элементы структуры являются незначащими.
INIT_PROCESS Приведенный процесс был порожден непосредственно инициализацией. Это значение могут устанавливать системные программы (обычно только сам процесс инициализации); приложения должны прочитывать и распознавать это значение, но не должны ее устанавливать. Значащими являются элементы ut_pid , ut_id и ut_tv .
LOGIN_PROCESS Экземпляры регистрационной программы, ожидающие регистрации пользователя. Элементы ut_id , ut_pid и ut_tv полезны; элемент ut_user полезен номинально (в Linux он сообщает LOGIN , но это имя процесса регистрации определяется реализацией в соответствии с POSIX).
USER_PROCESS Этот элемент определяет лидера сеанса для зарегистрированного пользователя. Это может быть регистрационная программа после регистрации пользователя, управляющая программа монитора либо сеанса для входа в X Window System, программа эмуляции терминала, сконфигурированная для пометки сеансов регистрации, или любая интерактивная регистрация пользователя. Значащими являются элементы ut_id , ut_user , ut_line , ut_pid и ut_tv .
DEAD_PROCESS Приведенный процесс был лидером сеанса для зарегистрированного пользователя, но завершился. Значащими являются элементы ut_id , ut_pid и ut_tv в соответствии POSIX. Элемент ut_exit (не установленный POSIX) значащий только в данном контексте.
BOOT_TIME Время начальной загрузки системы. В utmp это самая поздняя загрузка; в wtmp это элемент для каждой загрузки системы со времени очистки wtmp. Значащим является только ut_tv .
OLD_TIME и NEW_TIME Используются только для записи "скачков" времени. Записываются парами. Не рекомендуется зависеть от записи этих элементов в систему, даже если время на часах по какой-либо причине изменилось.
RUN_LVL и ACCOUNTING Внутренние системные величины; в приложениях использовать не следует.

Ниже приведены интерфейсы, определяемые XPG2, SVID 2 и FSSTND 1.2.

#include

int utmpname(char * file);

struct utmp *getutent(void);

struct utmp *getutid(const struct utmp * id);

struct utmp *getutline(const struct utmp * line);

struct utmp *pututline(const struct utmp * ut);

void setutent(void);

void endutent(void);

void updwtmp(const char * file, const struct utmp * ut);

void logwtmp(const char * line, const char * name, const char * host);

Каждая запись в базе данных utmp или wtmp называется строкой. Все функции, возвращающие указатель на struct utmp, возвращают его на статические данные в случае успеха и NULL — в случае ошибки. Обратите внимание, что статические данные переписаны каждым новым вызовом на каждую функцию, возвращающую struct utmp. Также стандарт POSIX (для utmpx) требует очистки статических данных приложением перед началом какого-либо поиска.

Версии utmpx этих функций принимают struct utmpx вместо struct utmp, требуют включения utmpx.h и называются getutxent, getutxid, getutxline, pututxline, setutxent и endutxent, но в другом случае они идентичны версиям utmp этих функций в Linux. Функции utmpxname(), определенной POSIX, не существует, хотя некоторые платформы могут определять ее в любом случае (например, glibc).

Функция utmpname() используется для определения просматриваемой базы данных. Базой данных по умолчанию является utmp, но вместо этого функцию можно применять для указания на wtmp. Два предопределенных имени — это _PATH_UTMP для файла utmp и _PATH_WTMP для файла wtmp; для целей тестирования можно выбрать указатель на локальную копию. Функция utmpname() возвращает ноль в случае успеха и ненулевое значение в случае ошибки. Но успех может означать просто то, что имя файла удалось скопировать в библиотеку; это не означает, что база данных действительно существует в пути, предоставленном для нее.

Функция getutent() просто возвращает следующую строку из базы данных. Если база данных еще не открыта, она возвращает содержимое первой строки. Если строки больше не доступны, она возвращает NULL.

Функция getutid() принимает struct utmp и рассматривает лишь один или два элемента. Если ut_type является BOOT_TIME, OLD_TIME или NEW_TIME, она возвращает следующую строку этого типа. Если ut_type является INIT_PROCESS, LOGIN_PROCESS, USER_PROCESS или DEAD_PROCESS, тогда getutid() возвращает следующую строку, которая соответствует любому из типов, также имеющему значение ut_id, которое соответствует значению ut_id в struct utmp, передаваемой getutid(). Перед повторным вызовом потребуется удалить из struct utmp данные, возвращаемые getutid(); POSIX разрешает возвращать ту же строку, что и предыдущий вызов. Если соответствующих строк нет, возвращается NULL.

Функция getutline() возвращает следующую строку с ut_id, установленным в LOGIN_PROCESS или USER_PROCESS. Эти процессы тоже имеют значение ut_line, соответствующее значению ut_line в struct utmp, которая передается в getutline(). Как и в случае getutid(), необходимо удалить данные, возвращаемые getutline(), из struct utmp перед его повторным вызовом; в противном случае POSIX разрешает возвращать ту же строку, что и предыдущий вызов. Если соответствующих строк нет, возвращается NULL.

Функция pututline() модифицирует (или по необходимости добавляет) запись базы данных, соответствующую элементу ut_line аргумента struct utmp. Она делает это только в том случае, если у процесса есть полномочия на модификацию базы данных. Если модификация базы данных прошла успешно, она возвращает struct utmp, который соответствует данным, записанным в базу. В ином случае возвращается NULL. Функция pututline() не применима к базе данных wtmp. Для модификации базы данных wtmp используйте updwtmp() или logwtmp().

Функция setutent() перемещает внутренний указатель базы данных в начало.

Функция endutent() закрывает базу данных. Это закрывает файловый дескриптор и освобождает ассоциированные данные. Вызывайте endutent() как перед использованием utmpname() для доступа к другому файлу utmp, так и после завершения доступа к данным utmp.

Наиболее надежным способом модификации базы данных wtmp являются две функции, определенные BSD и доступные как часть glibc.

Функция updwtmp() принимает файловое имя базы данных wtmp (обычно _PATH_WTMP) и заполненную структуру struct utmp, пытаясь добавить элемент к файлу wtmp. Эта функция не сообщает об ошибках.

Функция logwtmp() является удобной функцией, заполняющей struct utmp и вызывающей updwtmp() для нее. Аргумент line копируется в ut_line, name — в ut_user, host — в ut_host, ut_tv заполняется текущим показанием времени, a ut_pid — текущим идентификатором процесса. Как и updwtmp(), эта функция не сообщает об ошибках.

В программе utmp демонстрируются некоторые методы чтения баз данных utmp и wtmp.

  1: /* utmp.с */

  2:

  3: #include

  4: #include

  5: #include

  6: #include

  7: #include

  8: #include

  9: #include

 10: #include

 11: #include

 12: #include

 13: #include

 14:

 15: void print_utmp_entry(struct utmp * u) {

 16:  struct tm *tp;

 17:  char * type;

 18:  char addrtext[INET6_ADDRSTRLEN];

 19:

 20:  switch (u->ut_type) {

 21:  case EMPTY: type = "EMPTY"; break;

 22:  case RUN_LVL: type = "RUN_LVL"; break;

 23:  case BOOT_TIME: type = "BOOT_TIME"; break;

 24:  case NEW_TIME: type = "NEW_TIME"; break;

 25:  case OLD_TIME: type = "OLD_TIME"; break;

 26:  case INIT_PROCESS: type = "INIT_PROCESS"; break;

 27:  case LOGIN_PROCESS: type = "LOGIN_PROCESS"; break;

 28:  case USER_PROCESS: type = "USER_PROCESS"; break;

 29:  case DEAD_PROCESS: type = "DEAD_PROCESS"; break;

 30:  case ACCOUNTING: type = "ACCOUNTING "; break;

 31:  }

 32:  printf("%-13s:", type);

 33:  switch (u->ut_type) {

 34:  case LOGIN_PROCESS:

 35:  case USER_PROCESS:

 36:  case DEAD_PROCESS:

 37:   printf(" line: %s", u->ut_line);

 38:   /* fall through */

 39:  case INIT_PROCESS:

 40:   printf("\n pid: %6d id: %4.4s", u->ut_pid, u->ut_id);

 41:  }

 42:  printf ("\n");

 43:  tp = gmtime(&u->ut_tv.tv_sec);

 44:  printf("time: %24.24s.%lu\n", asctime(tp), u->ut_tv.tv_usec);

 45:  switch (u->ut_type) {

 46:  case USER_PROCESS:

 47:  case LOGIN_PROCESS:

 48:  case RUN_LVL:

 49:  case BOOT_TIME:

 50:   printf("пользователь: %s\n", u->ut_user);

 51:  }

 52:  if (u->ut_type == USER_PROCESS) {

 53:   if (u->ut_session)

 54:    printf(" сеанс: %lu\n", u->ut_session);

 55:   if (u->ut_host)

 56:    printf (" хост: %s\n", u->ut_host);

 57:   if (u->ut_addr_v6[0]) {

 58:    if (!(u->ut_addr_v6[1] |

 59:     u->ut_addr_v6[2] |

 60:     u->ut_addr_v6[3])) {

 61:     /* заполнение только первой группы означает адрес IPV4 */

 62:     inet_ntop(AF_INET, u->ut_addr_v6,

 63:     addrtext, sizeof(addrtext));

 64:     printf(" IPV4: %s\n", addrtext);

 65:    } else {

 66:     inet_ntop(AF_INET_6, u->ut_addr_v6,

 67:      addrtext, sizeof(addrtext));

 68:     printf (" IPV6: %s\n", addrtext);

 69:    }

 70:   }

 71:  }

 72:  if (u->ut_type == DEAD_PROCESS) {

 73:   printf(" завершение : %u: %u\n",

 74:    u->ut_exit.e_termination,

 75:    u->ut_exit.e_exit);

 76:  }

 77:  printf("\n");

 78: }

 79:

 80: struct utmp * get_next_line (char * id, char * line) {

 81:  struct utmp request;

 82:

 83:  if (!id && !line)

 84:   return getutent();

 85:

 86:  memset(&request, 0, sizeof(request));

 87:

 88:  if (line) {

 89:   strncpy(&request.ut_line[0], line, UT_LINESIZE);

 90:   return getutline(&request);

 91:  }

 92:

 93:  request.ut_type = INIT_PROCESS;

 94:  strncpy(&request.ut_id[0], id, 4);

 95:  return getutid(&request);

 96: }

 97:

 98: void print_file(char * name, char * id, char * line) {

 99:  struct utmp * u;

100:

101:  if (utmpname(name)) {

102:   fprintf (stderr, "сбой при открытии базы данных utmp %s\n", name);

103:   return;

104:  }

105:  setutent();

106:  printf("%s:\n====================\n", name);

107:  while ((u = get_next_line(id, line))) {

108:   print_utmp_entry(u);

109:   /* POSIX требует очистки статических данных перед

110:    * повторным вызовом getutline или getutid

111:    */

112:   memset(u, 0, sizeof(struct utmp));

113:  }

114:  endutent();

115: }

116:

117: int main(int argc, const char **argv) {

118:  char * id = NULL, *line = NULL;

119:  int show_utmp = 1, show_wtmp = 0;

120:  int c;

121:  poptContext optCon;

122:  struct poptOption optionsTable[] = {

123:   {"utmp", 'u', POPT_ARG_NONE|POPT_ARGFLAG_XOR,

124:    &show_utmp, 0,

125:    "переключить просмотр содержимого файла utmp", NULL},

126:   { "wtmp", 'w', POPT_ARG_NONE | POPT_ARGFLAG_XOR,

127:     &show_wtmp, 0,

128:     "переключить просмотр содержимого файла wtmp", NULL},

129:   {"id", 'i', POPT_ARG_STRING, &id, 0,

130:    "показать записи процесса для заданного идентификатора inittab",

131:    "" },

132:   {"line", 'l', POPT_ARG_STRING, &line, 0,

133:    "показать записи процесса для заданной строки устройства",

134:    "" },

135:   POPT_AUTOHELP

136:   POPT_TABLEEND

137:  };

138:

139:  optCon = poptGetContext("utmp", argc, argv, optionsTable, 0);

140:  if ((c = poptGetNextOpt(optCon)) < -1) {

141:   fprintf(stderr, "%s:%s\n",

142:    poptBadOption(optCon, POPT_BADOPTION_NOALIAS),

143:    poptStrerror(c));

144:   return 1;

145:  }

146:  poptFreeContext(optCon);

147:

148:  if (id && line)

149:   fprintf(stderr, "Невозможно выбирать сразу по идентификатору и строке,"

150:    "выбор по строке\n");

151:

152:  if (show_utmp)

153:   print_file(_PATH_UTMP, id, line);

154:  if (show_utmp && show_wtmp)

155:   printf("\n\n\n");

156:  if (show_wtmp)

157:   print_file(_PATH_WTMP, id, line);

158:

159:  return 0;

160: }

 

16.2. Обзор

termios

Все манипуляции tty осуществляются с помощью одной структуры, struct termios, а также нескольких функций, определенных в заголовочном файле . Из этих функций широко применяются только шесть. Когда не нужно устанавливать скорость передачи данных по линии, используются только две наиболее важных функции — tcgetattr() и tcsetattr().

#include

struct termios {

 tcflag_t c_iflag; /* флаги режима ввода */

 tcflag_t c_oflag; /* флаги режима вывода */

 tcflag_t c_cflag; /* флаги управляющего режима */

 tcflag_t c_lflag; /* флаги локального режима */

 cc_t c_line;      /* дисциплина линии связи */

 cc_t c_cc[NCCS];  /* управляющие символы */

};

int tcgetattr(int fd, struct termios * tp);

int tcsetattr(int fd, int oact, struct termios * tp);

Почти в каждом случае программы должны использовать tcgetattr() для получения текущих установок устройства, модифицировать эти установки, а затем применять tcsetattr() для активизации модифицированных установок. Многие программы также сохраняют копии оригинальных установок и восстанавливают их перед завершением. В общем случае, следует модифицировать только интересующие вас установки; изменение других установок может усложнить работу пользователей с необычными системными конфигурациями (или сбоями в вашем коде).

Вызов tcsetattr() может не принять на обработку выбранные вами установки; разрешено игнорировать произвольные установки. Если оборудование просто не поддерживает установку, tcsetattr() игнорирует ее, а не возвращает ошибку. Если вам небезразлично воздействие, оказываемое установкой, следует использовать tcgetattr() после tcsetattr() и проверить, оказало ли воздействие внесенное вами изменение.

Для получения установок устройства tty необходимо открыть устройство и передать файловый дескриптор tcgetattr(). Это вызывает проблемы с некоторыми устройствами tty; некоторые обычно можно открыть лишь один раз с целью предотвращения конфликта устройств. К счастью, передача флага O_NONBLOCK в open() вызывает его немедленное открытие и предотвращает блокирование любых операций. Однако все равно можно предпочесть блокирование read(); в таком случае используйте fcntl() для отключения режима O_NONBLOCK перед тем, как появится возможность читать или записывать в него.

fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK);

Четыре флага termios контролируют четыре отдельных части управления вводом и выводом. Флаг входных данных, с_iflag, определяет, каким образом интерпретируются и обрабатываются принятые символы. Флаг выходных данных, c_oflag, определяет, каким образом интерпретируются и обрабатываются символы, записываемые вашим процессом в tty. Управляющий флаг, c_cflag, определяет характеристики последовательного протокола устройства и полезен лишь для физических устройств. Локальный флаг, c_lflag, определяет, каким образом символы собираются и обрабатываются перед отправкой на обработку выходных данных. На рис. 16.1 показана упрощенная схема того, какое место занимает каждый флаг в общей схеме обработки символов.

Рис. 16.1. Упрощенная схема обработки tty

Сначала мы продемонстрируем способы применения termios, а затем представим короткую справку о нем.

 

16.3. Примеры использования

termios

 

16.3.1. Пароли

Самой распространенной причиной модификации установок termios является чтение пароля без эхо-контроля символов. Для этого следует отключить локальное эхо во время чтения пароля. Ваш код должен выглядеть следующим образом:

struct termios ts, ots;

Первая структура хранит оригинальные установки для восстановления, а вторая является копией для модификации.

tcgetattr(STDIN_FILENO, &ts);

Обычно пароли читаются со стандартного устройства ввода.

ots = ts;

Сохраните копию оригинальных установок termios, чтобы позже восстановить их.

ts.c_lflag &= ~ECHO;

ts.c_lflag |= ECHONL;

tcsetattr(STDIN_FILENO, TCSAFLUSH, fits);

Отключите эхо-контроль символов (кроме символов новой строки) после завершения обработки всех выходных данных. (Первая l в c_lflag означает локальную (local) обработку.)

read_password();

Теперь вы читаете пароль. Это может быть простой вызов fgets() или read(), либо же более сложная обработка, в зависимости от режима tty (неформатируемый режим или режим обработки) и от требований программы.

tcsetattr(STDIN_FILENO, TCSANOW, &ots);

Это немедленно восстанавливает исходные установки termios. (Остальные опции объясняются позже, в справочном разделе далее в главе.)

Полный код программы-примера, readpass, показан ниже.

 1: /* readpass.с */

 2:

 3: #include

 4: #include

 5: #include

 6: #include

 7:

 8: int main (void) {

 9:  struct termios ts, ots;

10:  char passbuf[1024];

11:

12:  /* получить и сохранить текущие настройки termios */

13:  tcgetattr(STDIN_FILENO, &ts);

14:  ots = ts;

15:

16:  /* изменить и установить новые настройки termios */

17:  ts.c_lflag & = ~ECHO;

18:  ts.c_lflag |= ECHONL;

19:  tcsetattr(STDIN_FILENO, TCSAFLUSH, &ts);

20:

21:  /*хоть это и параноидально, но проверить, возымели ли эффект новые настройки*/

22:  tcgetattr(STDIN_FILENO, &ts);

23:  if (ts.c_lflag & ECHO) {

24:   fprintf(stderr, "Сбой при отключении эхо-контроля\n");

25:   tcsetattr(STDIN_FILENO, TCSANOW, &ots);

26:   exit(1);

27:  }

28:

29:  /* получить и вывести пароль */

30:  printf("введите пароль:");

31:  fflush(stdout);

32:  fgets(passbuf, 1024, stdin);

33:  printf("прочитан пароль: %s", passbuf);

34:  /* в passbuf был завершающий символ \n */

35:

36:  /* восстановить старые настройки termios */

37:  tcsetattr(STDIN_FILENO, TCSANOW, &ots);

38:

39:  exit(0);

40: }

 

16.3.2. Последовательные коммуникации

В качестве примера программирования обоих концов tty рассмотрим программу, подключающую текущий терминал к последовательному порту. На одном tty программа под названием robin сообщается с вами во время набора. На другом tty она взаимодействует с последовательным портом. С целью мультиплексирования вводных и выходных данных на локальном tty и последовательном порте программа использует системный вызов poll(), описанный в главе 13.

Ниже приведен полный код программы robin.с, за которым даны объяснения.

  1: /* robin.с */

  2:

  3: #include

  4: #include

  5: #include

  6: #include

  7: #include

  8: #include

  9: #include

 10: #include /* для strerror() */

 11: #include

 12: #include

 13:

 14: void die(int exitcode, const char *error, const char *addl) {

 15:  if (error) fprintf(stderr, "%s: %s\n", error, addl);

 16:  exit(exitcode);

 17: }

 18:

 19: speed_t symbolic_speed(int speednum) {

 20:  if (speednum >= 460800) return B460800;

 21:  if (speednum >= 230400) return B230400;

 22:  if (speednum >= 115200) return B115200;

 23:  if (speednum >= 57600) return B57600;

 24:  if (speednum >= 38400) return B38400;

 25:  if (speednum >= 19200) return B19200;

 26:  if (speednum >= 9600) return B9600;

 27:  if (speednum >= 4800) return B4800;

 28:  if (speednum >= 2400) return B2400;

 29:  if (speednum >= 1800) return B1800;

 30:  if (speednum >= 1200) return B1200;

 31:  if (speednum >= 600) return B600;

 32:  if (speednum >= 300) return B300;

 33:  if (speednum >= 200) return B200;

 34:  if (speednum >= 150) return B150;

 35:  if (speednum >= 134) return B134;

 36:  if (speednum >= 110) return B110;

 37:  if (speednum >= 75) return B75;

 38:  return B50;

 39: }

 40:

 41: /* Это нужно для области видимости в пределах файла, так что

 42:  * их можно будет использовать в обработчиках сигналов */

 43: /* старые настройки порта termios для восстановления */

 44: static struct termios pots;

 45: /* старые настройки stdout/stdin termios для восстановления */

 46: static struct termios sots;

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

 48: int pf;

 49:

 50: /* восстановить первоначальные настройки терминала при выходе */

 51: void cleanup_termios(int signal) {

 52:  tcsetattr(pf, TCSANOW, &pots);

 53:  tcsetattr(STDIN_FILENO, TCSANOW, &sots);

 54:  exit(0);

 55: }

 56:

 57: /* обработать одиночный управляющий символ */

 58: void send_escape(int fd, char c) {

 59:  switch (c) {

 60:  case 'q':

 61:   /* восстановить настройки termios и выйти */

 62:   cleanup_termios(0);

 63:   break;

 64:  case 'b':

 65:   /* послать символ разрыва*/

 66:   tcsendbreak(fd, 0);

 67:   break;

 68:  default:

 69:   /* пропустить символ */

 70:   /* "C-\ C-\" sends "C-\" */

 71:   write(fd, &c, 1);

 72:   break;

 73:  }

 74:  return;

 75: }

 76:

 77: /* обработать управляющие символы, записывая их в вывод */

 78: void cook_buf(int fd, char * buf, int num) {

 79:  int current = 0;

 80:  static int in_escape = 0;

 81:

 82:  if (in_escape) {

 83:   /* cook_buf последний раз вызывался с незавершенной

 84:      управляющей последовательностью */

 85:   send_escape(fd, buf[0]);

 86:   num--;

 87:   buf++;

 88:   in_escape = 0;

 89:  }

 90:  while (current < num) {

 91: # define CTRLCHAR(c) ((c)-0x40)

 92:   while ((current < num) && (buf[current] != CTRLCHAR('W')))

 93:    current++;

 94:   if (current) write (fd, buf, current);

 95:   if (current < num) {

 96:    /* найден управляющий символ */

 97:    current++;

 98:    if (current >= num) {

 99:     /*интерпретировать первый символ следующей последовательности*/

100:     in_escape = 1;

101:     return;

102:    }

103:    send_escape(fd, buf[current]);

104:   }

105:   num -= current;

106:   buf += current;

107:   current = 0;

108:  }

109:  return;

110: }

111:

112: int main(int argc, const char * argv[]) {

113:  char с; /* используется для разбора аргументов */

114:  struct termios pts; /* настройки termios для порта */

115:  struct termios sts; /* настройки termios для stdout/stdin */

116:  const char *portname;

117:  int speed = 0; /* используется при разборе аргументов для скорости */

118:  struct sigaction sact; /* используется для инициализации обработчика сигналов */

119:  struct pollfd ufds[2]; /* взаимодействие с poll() */

120:  int raw = 0; /* неформатированный режим? */

121:  int flow = 0; /* тип управления потоком, если применяется*/

122:  int crnl = 0; /* посылать ли символ возврата каретки с символом новой строки? */

123:  int i = 0; /* используется в цикле мультиплексирования*/

124:  int done = 0; 125: # define BUFSIZE 1024

126:  char buf[BUFSIZE];

127:  poptContext optCon; /* контекст опций командной строки */

128:  struct poptOption optionsTable[] = {

129:   { "bps", 'b', POPT_ARG_INT, &speed, 0,

130:     "скорость передачи сигналов, бит/с",

131:     "" },

132:   { "crnl", 'с', POPT_ARG_VAL, &crnl, 'с',

133:     "посылать символ возврата каретки с символом новой строки", NULL },

134:   { "hwflow", 'h', POPT_ARG_VAL, &flow, 'h',

135:     "использовать аппаратное управление потоком", NULL },

136:   { "swflow", 's', POPT_ARG_VAL, &flow, 's',

137:     "использовать программное управление потоком", NULL },

138:   { "noflow", 'n', POPT_ARG_VAL, &flow, 'n',

139:     "отключить управление потоком", NULL },

140:   { "raw", 'r', POPT_ARG_VAL, &raw, 1,

141:     "включить неформатированный режим", NULL },

142:   POPT_AUTOHELP

143:   { NULL, '\0', 0, NULL, '\0', NULL, NULL }

144:  };

145:

146: #ifdef DSLEEP

147:  /* ожидать 10 минут, что позволить подключить отладчик */

148:  sleep(600);

149: #endif

150:

151:  optCon = poptGetContext("robin", argc, argv, optionsTable, 0);

152:  poptSetOtherOptionHelp(optCon, "");

153:

154:  if (argc < 2) {

155:   poptPrintUsage(optCon, stderr, 0);

156:   die(1, "He достаточно аргументов", "");

157:  }

158:

159:  if ((с = poptGetNextOpt(optCon)) < -1) {

160:   /* ошибка во время обработки опций */

161:   fprintf(stderr, "%s: %s\n",

162:    poptBadOption(optCon, POPT_BADOPTION_NOALIAS),

163:    poptStrerror(c));

164:   return 1;

165:  }

166:  portname = poptGetArg(optCon);

167:  if (!portname) {

168:   poptPrintUsage(optCon, stderr, 0);

169:   die(1, "He указано имя порта", "");

170:  }

171:

172:  pf = open(portname, O_RDWR);

173:  if (pf < 0) {

174:   poptPrintUsage(optCon, stderr, 0);

175:   die(1, strerror(errno), portname);

176:  }

177:  poptFreeContext(optCon);

178:

179:  /* изменить конфигурацию порта */

180:  tcgetattr(pf, &pts);

181:  pots = pts;

182:  /* некоторые настройки устанавливаются произвольно */

183:  pts.c_lflag &= ~ICANON;

184:  pts.c_lflag &= ~(ECHO | ECHOCTL | ECHONL);

185:  pts.c_cflag |= HUPCL;

186:  pts.c_cc[VMIN] = 1;

187:  pts.c_cc[VTIME] = 0;

188:

189:  /* Стандартная обработка CR/LF: это неинтеллектуальный терминал.

190:   * Не транслируется:

191:   * нет NL -> отображение CR/NL в выводе,

192:   * нет CR -> отображение NL во вводе.

193:   */

194:  pts.c_oflag &= ~ONLCR;

195:  pts.c_iflag &= ~ICRNL;

196:

197:  /* Теперь перейти на сторону локального терминала */

198:  tcgetattr(STDIN_FILENO, &sts);

199:  sots = sts;

200:  /* и снова несколько произвольных настроек */

201:  sts.c_iflag &= ~(BRKINT | ICRNL);

202:  sts.c_iflag |= IGNBRK;

203:  sts.c_lflag &= ~ISIG;

204:  sts.c_cc[VMIN] = 1;

205:  sts.c_cc[VTIME] = 0;

206:  sts.c_lflag &= ~ICANON;

207:  /* нет локального эхо: разрешить эхо-контроль на другом конце */

208:  sts.c_lflag &= ~(ECHO | ECHOCTL | ECHONL);

209:

210:  /* обработка опций сейчас будет модифицировать pts и sts */

211:  switch (flow) {

212:  case 'h' :

213:   /* аппаратное управление потоком */

214:   pts.c_cflag |= CRTSCTS;

215:   pts.c_iflag &= ~(IXON | IXOFF | IXANY);

216:   break;

217:  case 's':

218:   /* программное управление потоком */

219:   pts.c_cflag &= ~CRTSCTS;

220:   pts.c_iflag |= IXON | IXOFF | IXANY;

221:   break;

222:  case 'n':

223:   /* отключение управления потоком */

224:   pts.c_cflag &= ~CRTSCTS;

225:   pts.c_iflag &= ~(IXON | IXOFF | IXANY);

226:   break;

227:  }

228:  if (crnl) {

229:   /* послать CR с NL */

230:   pts.c_oflag |= ONLCR;

231:  }

232:

233:  /* скорость не изменяется, пока не будет указано -b */

234:  if (speed) {

235:   cfsetospeed(&pts, symbolic_speed(speed));

236:   cfsetispeed(&pts, symbolic_speed(speed));

237:  }

238:

239:  /* установить обработчик сигналов для восстановления

240:   * старого обработчика termios */

241:  sact.sa_handler = cleanup_termios;

242:  sigaction(SIGHUP, &sact, NULL);

243:  sigaction(SIGINT, &sact, NULL);

244:  sigaction(SIGPIPE, &sact, NULL);

245:  sigaction(SIGTERM, &sact, NULL);

246:

247:  /* установить измененные настройки termios */

248:  tcsetattr(pf, TCSANOW, &pts);

249:  tcsetattr(STDIN_FILENO, TCSANOW, &sts);

250:

251:  ufds[0].fd = STDIN_FILENO;

252:  ufds[0].events = POLLIN;

253:  ufds[1].fd = pf;

254:  ufds[1].events = POLLIN;

255:

256:  do {

257:   int r;

258:

259:   r = poll(ufds, 2, -1);

260:   if ((r < 0) && (errno != EINTR))

261:    die(1, "неожиданный сбой poll", "");

262:

263:   /* сначала проверить возможность завершения */

264:   if ((ufds[0].revents | ufds[1].revents) &

265:    (POLLERR | POLLHUP | POLLNVAL)) {

266:    done = 1;

267:    break;

268:   }

269:

270:   if (ufds[1].revents & POLLIN) {

271:    /* pf содержит символы */

272:    i = read (pf, buf, BUFSIZE);

273:    if (i >= 1) {

274:     write(STDOUT_FILENO, buf, i);

275:    } else {

276:     done = 1;

277:    }

278:   }

279:   if (ufds[0].revents & POLLIN) {

280:    /* стандартный ввод содержит символы */

281:    i = read(STDIN_FILENO, buf, BUFSIZE);

282:    if (i >= 1) {

283:     if (raw) {

284:      write(pf, buf, i);

285:     } else {

286:      cook_buf(pf, buf, i);

287:     }

288:    } else {

289:     done = 1;

290:    }

291:   }

292:  } while (!done);

293:

294:  /* восстановить первоначальные настройки терминала и завершиться*/

295:  tcsetattr(pf, TCSANOW, &pots);

296:  tcsetattr(STDIN_FILENO, TCSANOW, &sots);

297:  exit(0);

298: }

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

Функция symbolic_speed() в строке 19 преобразует целочисленную скорость в символическую, которую поддерживает termios. К сожалению, termios не предназначен для работы с произвольными скоростями, так что каждая скорость, которую вы хотите использовать, должна быть частью интерфейса пользователь — ядро.

Обратите внимание, что предусмотрены и довольно высокие скорости. Не все последовательные порты поддерживают скорости 230 400 или 460 800 бит/с; в стандарте POSIX определены скорости лишь до 38 400 бит/с. Чтобы сделать эту программу переносимой, каждую строку над строкой, в которой устанавливается скорость 38 400 бит/с, потребуется расширить до трех строк, как показано ниже:

#ifdef В460800

if (speednum >= 460800) return B46000;

#end if

Это позволит пользователям устанавливать скорости выше тех, которые последовательные порты способны поддерживать, а исходный код теперь будет компилироваться в любой системе с POSIX termios. (Как упоминалось ранее в этой главе, любой последовательный порт может отказаться принять на обработку любую установку termios, которую он не способен поддержать, включая также установки скорости. Поэтому установка B460800 не означает, что можно установить скорость порта равной 460 800 бит/с.)

В строках 44—55 определяются глобальные переменные для передачи некоторых переменных обработчику сигналов и сам обработчик сигналов. Обработчик сигналов предназначен для восстановления настроек termios на обоих интерфейсах tty при доставке сигнала, поэтому ему необходимо получить доступ к структурам, содержащим старые настройки termios. Ему также необходимо знать файловый дескриптор или последовательный порт (файловый дескриптор для стандартных входных данных не меняется, поэтому компилируется в бинарный). Этот код идентичен коду в случае нормального пути завершения, который рассматривается позже. Обработчик сигналов затем прикрепляется к сигналам, которые завершат процесс, если они были проигнорированы.

Функции send_escape() и cook_buf() будут рассматриваться позже. Они используются как часть обработки входных данных в цикле ввода-вывода в конце функции main().

Условно скомпилированный sleep(600) в начале функции main() предназначен для отладки. С целью отладки программ, модифицирующих настройки termios для стандартных входных или выходных данных, лучше всего присоединить процесс в другой окне или терминальном сеансе. Однако это означает, что нельзя установить точку прерывания на основную функцию и проходить по одной инструкции за раз. Необходимо запустить программу, найти идентификатор ее процесса и присоединиться к ней из отладчика. Более подробно этот процесс описан далее в настоящей главе.

Поэтому если необходимо отладить код, запускаемый до того, как программа дождется входных данных, нужно перевести программу в режим ожидания, чтобы оставить время для присоединения. После прикрепления режим ожидания прерывается, поэтому длительный режим ожидания безопасен. Чтобы активизировать это свойство, скомпилируйте robin.с с опцией -DDSLEEP.

Игнорируя отладку, мы в первую очередь анализируем опции, используя библиотеку popt, описанную в главе 26, а затем открываем последовательный порт, с которым будем взаимодействовать.

Затем мы вызываем функцию tcgetattr(), чтобы получить существующую конфигурацию termios последовательного порта, а затем сохраняем копию в pots, чтобы восстановить ее по окончании.

Начиная со строки 183, мы модифицируем установки последовательного порта:

183: pts.c_lflag &= ~ICANON;

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

184: pts.c_lflag &= ~(ECHO | ECHOCTL | ECHONL);

Это отключает локальный эхо-контроль в последовательном порте:

185: pts.c_cflag |= HUPCL;

Если подключен модем, HUPCL сообщает ему об отбое при закрытии устройства конечной программой:

186: pts.с_сс[VMIN] = 1;

187: pts.с_сс[VTIME] = 0;

Когда tty находится в неформатируемом режиме, эти две установки определяют поведение системного вызова read(). Эта особая установка сообщает, что при вызове read() мы хотим, чтобы он подождал с возвратом, пока не считаются один или несколько байтов. Мы никогда не вызовем read(), пока не будем знать, что остался хотя бы один байт для чтения, потому это функциональный эквивалент неблокирующего read(). Определение VMIN и VTIME сложное, как показано далее в настоящей главе.

Настройки termios по умолчанию включают преобразование некоторых символов в конце строки. Это подходит для модемных линий и терминальных сеансов, но при соединении двух tty повтор преобразования нежелателен. Нежелательно отображать символы новой строки на пару "возврат каретки/перевод строки" при выводе, а также принимаемый возврат каретки — на перевод строки при вводе, поскольку мы уже получаем пары "возврат каретки/перевод строки" с удаленной системы.

194: pts.c_oflag &= ~ONLCR;

195: pts.c_iflag &= ~ICRNL;

Без этих двух строк использование программы robin для соединения с другим компьютером Linux или Unix приведет к тому, что удаленная система увидит, что вы нажимаете клавишу дважды всякий раз, когда вы нажимаете ее один раз. И всякий раз, когда она будет пытаться отобразить новую строку на вашем экране, вы будете видеть две строки. Поэтому каждый раз при нажатии (предполагая, что у вас получится зарегистрироваться в этих установках терминала) вы увидите отраженные подсказки. Если же вы запустите vi, то увидите символы ~, расположенные через строку, а не в каждой строке.

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

Затем потребуется модифицировать несколько флагов:

201: sts.c_iflag &= ~(BRKINT | ICRNL);

202: sts.c_iflag |= IGNBRK;

203: sts.c_lflag &= ~ISIG;

Отключение BRKINT играет роль только в том случае, если robin вызывается из регистрационного сеанса, присоединенного к другому последовательному порту, на котором можно получить разрыв. Его отключение означает, что драйвер tty не посылает SIGINT в robin, когда в стандартном устройстве ввода robin возникает разрыв, поскольку robin не может сделать ничего полезного при получении разрыва. Отключение ICRNL предотвращает сообщение в robin о любых символах возврата каретки ('\r') как о символах новой строки ('\n'). Как и при отключении BRKINT это действует лишь тогда, когда сеанс регистрации присоединяется к другому последовательному порту. Также это действует тогда, когда символы возврата каретки не игнорируются (то есть если не установлен флаг IGNCR).

Функция IGNBRK заставляет драйвер tty игнорировать разрывы. Включение IGNBRK будет здесь излишним. При установке IGNBRK игнорируется BRKINT. Но не беда, если вы установите обе функции.

Это флаги обработки входных данных. Также модифицируется локальный флаг обработки: отключается ISIG. Это предотвращает драйвер tty от передачи SIGINT, SIGQUIT и SIGTSTP при получении соответствующего символа (INTR, QUIT или SUSP). Мы делаем это потому, что хотим переслать эти символы на удаленную систему (или на другое устройство, подключенное к последовательному порту) для последующей обработки там.

Затем следует обработка опций. В некоторых случаях модификаций настроек termios по умолчанию может оказаться недостаточно, или же, наоборот, слишком много. В таких случаях мы предлагаем некоторые опции командной строки для модификации опций termios.

По умолчанию мы оставляем последовательный порт в том состоянии управления потоком, в котором мы его находим. Однако в строке 212 есть опции аппаратного управления потоком (использующего управляющие провода CTS и RTS), программного управления потоком (резервирование ^S и ^Q для STOP и START соответственно) и полного отключения управления потоком.

212: case 'h':

213:  /* аппаратное управление потоком */

214:  pts.c_cflag |= CRTSCTS;

215:  pts.c_iflag &= ~(IXON | IXOFF | IXANY);

216:  break;

217: case 's' :

218:  /* программное управление потоком */

219:  pts.c_cflag &= ~CRTSCTS;

220:  pts.c_iflag |= IXON | IXOFF | IXANY;

221:  break;

222: case 'n':

223:  /* отключение управления потоком */

224:  pts.q._cflag &= ~CRTSCTS;

225:  pts.c_iflag &= ~(IXON | IXOFF | IXANY);

226:  break;

Обратите внимание, что программное управление потоком включает три флага.

IXON Прекратить пересылать выходные данные при получении символа STOP (обычно ^S) и начать заново при получении символа START (обычно ^Q).
IXOFF Передать символ STOP, когда во входящем буфере накопится слишком много данных. В случае прочтения достаточного количества данных отправите символ START.
IXANY Позволить любому принятому символу, не только START, перезапустить вывод. (Этот флаг обычно реализован в системах Unix, но не определен в POSIX.)

Когда другая программа использует robin как вспомогательную, может помешать обработка спецсимволов (robin обычно интерпретирует последовательность ^\), поэтому во избежание такой обработки устанавливается неформатируемый режим. В строке 120 мы предоставляем переменную, определяющую, включен ли неформатируемый режим; по умолчанию этот режим не включается. В строке 140 мы сообщаем popt о том, как информировать, когда в командной строке была указана опция -r или -raw, включающая неформатируемый режим.

Некоторые системы требуют передачи им символа возврата каретки для представления новой строки. Слово "системы" здесь следует понимать буквально; например, это применимо ко многим интеллектуальным периферийным устройствам, например, источникам бесперебойного питания (UPS) с последовательными портами, поскольку они предназначены для функционирования в DOS, где пара "возврат каретки/перевод строки" всегда используется для обозначения новой строки. В строке 228 определяется это DOS-ориентированное поведение.

228: if (crnl) {

229:  /* послать CR с NL */

230:  pts.c_oflag |= ONLCR;

231: }

Последняя часть обработки опций управляет скоростью передачи в битах в секунду. Вместо включения огромного вложенного оператора выбора мы вызываем уже описанную ранее функцию symbolic_speed(), чтобы получить speed_t, понимаемый termios, как показано в строке 233.

233: /* скорость не изменяется, пока не будет указано -b */

234: if (speed) {

235:  cfsetospeed(&pts, symbolic_speed(speed));

236:  cfsetispeed(&pts, symbolic_speed(speed));

237: }

Перед тем, как зафиксировать изменения наших копий структур termios, в строке 241 регистрируются обработчики для важных сигналов, которые могут в противном случае вызвать уничтожение процесса и оставить tty в неформатируемом состоянии. Более подробно обработчики сигналов рассматриваются в главе 12.

241: sact.sa_handler = cleanup_termios;

242: sigaction(SIGHUP, &sact, NULL);

243: sigaction(SIGINT, &sact, NULL);

244: sigaction(SIGPIPE, &sact, NULL);

245: sigaction(SIGTERM, &sact, NULL);

Как только обработчик сигналов окажется на месте для восстановления старых настроек termios в случае уничтожения robin, мы можем благополучно обновить установки termios.

248: tcsetattr(pf, TCSANOW, &pts);

249: tcsetattr(STDIN_FILENO, TCSANOW, &sts);

На этом этапе программа robin готова читать и записывать символы. У robin есть два файловых дескриптора для чтения: данные с последовательного порта и данные с клавиатуры. Для мультиплексирования ввода-вывода между четырьмя файловыми дескрипторами используется poll() (см. главу 13).

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

Для данных, поступающих с клавиатуры, может понадобиться обработка управляющих последовательностей перед записью, если пользователь не выбрал неформатируемый режим при запуске robin. Вместо включения этого кода в цикл мы вызываем функцию cook_buf() (строка 78), которая при необходимости обращается к send_escape() (строка 58). Обе эти функции просты. Единственный трюк состоит в том, что cook_buf() может быть вызвана один раз с управляющим символом, а затем второй раз с интерпретируемым символом, а также в оптимизации количества вызовов функции write().

Функция cook_buf() вызывает функцию send_escape() один раз для каждого символа, которому предшествует неотменяемый управляющий символ ^\. Символ q восстанавливает исходные установки termios и завершается вызовом обработчика сигнала (с фальшивым номером сигнала 0), что восстанавливает настройки termios перед выходом. Символ b генерирует состояние разрыва, которое является длинной строкой, состоящей из нулей. Любой другой символ, включая второй управляющий символ ^\, передается в последовательный порт без изменений.

Если какой-то из входных файловых дескрипторов вернет признак конца файла, robin выходит из цикла poll() и передает управление обработке завершения, что соответствует обработчику сигнала: восстановление старых настроек termios на обоих входных файловых дескрипторов и завершение. В неформатируемом режиме существует только два способа завершения robin: закрыть один из файловых дескрипторов или передать ей сигнал.

 

16.4. Отладка

termios

Отладка кода tty далеко не всегда проста. Варианты пересекаются и влияют друг на друга разными способами, часто незапланированными. Но с помощью лишь отладчика невозможно увидеть то, что происходит, поскольку обработка, которой вы пытаетесь управлять, происходит в ядре.

Эффективным способом отладки кода, передающего информацию через последовательный порт, является использование программы-сценария. Во время разработки robin мы соединили два компьютера последовательным кабелем и убедились, что соединение работает, запустив известную программу kermit. В то время как программа kermit уже работала на локальном компьютере, мы запустили программу-сценарий на удаленном компьютере, которая начала регистрировать все символы в файле. Затем мы вышли из kermit и запустили сценарий на локальном компьютере, поместив полученный файл в текущий каталог на местном компьютере. Затем мы попытались запустить robin по сценарию и сравнили два файла в начале и после каждого запуска, чтобы проследить разницу в символах. Таким образом мы разобрались с эффектами выбранных опций обработки.

Еще один метод отладки использует преимущества программы stty. Если во время проверки программы вы распознаете ошибку в настройках termios, можете воспользоваться программой stty для немедленного внесения изменений вместо повторной компиляции своей программы. Если вы работаете на /dev/ttyS0 и хотите установить флаг ECHOCTL, просто во время работы своей программы запустите следующую команду:

stty echoctl < /dev/ttyS0

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

stty -а < /dev/ttyS0

Как объяснялось ранее, трудно использовать один tty для запуска отладчика и программы искажения tty, которая отлаживается. Вместо этого следует присоединиться к процессу. Это не сложно. В одном сеансе X-терминала (делайте это под управлением X Window, чтобы одновременно видеть оба tty) запустите программу, которую собираетесь отладить. В случае надобности поместите ее в долгий режим ожидания в точке, где вы собираетесь присоединиться к процессу:

$ ./robin -b 38400 /dev/ttyS1

Теперь с помощью другого сеанса X-терминала найдите идентификатор процесса программы, которую вы пытаетесь отладить, одним из двух способов:

$ ps | grep robin

30483 ? S 0:00 ./robin - b 38400 /dev/ttyS1

30485 ? S 0:00 grep robin

$ pidof robin

30483

Более удобным является pidof, но он может быть недоступен в системе. Запомните найденный номер (в данном случае 30483) и начните обычный сеанс отладки.

$ gdb robin 30483

GDB is free software...

...

Attaching to program '... /robin', process 30483

Reading symbols from...

0x40075d88 in sigsuspend()

Далее можно устанавливать точки прерывания и слежения, пошагово выполнять программу и так далее.

 

16.5. Справочник по

termios

 

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

#include

struct termios {

 tcflag_t c_iflag; /* флаги режима ввода */

 tcflag_t c_oflag; /* флаги режима вывода */

 tcflag_t c_cflag; /* флаги управляющего режима */ tcflag_t c_lflag; /* флаги локального режима */

 cc_t c_line; /* дисциплина линии связи */

 cc_t c_cc[NCCS]; /* управляющие символы */

};

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

 

16.5.1. Функции

Интерфейс termios определяет несколько функций. Все они объявлены в . Четыре из них являются обслуживающими функциями для переносимого манипулирования структурой struct termios; остальные представляют собой системные вызовы. Функции, начинающиеся с cf, являются обслуживающими, а функции, начинающиеся с tc — системными вызовами управления терминалом. Все системные вызовы управления терминалом генерируют SIGTTOU, если процесс в данный момент работает в фоне и пытается манипулировать своим управляющим терминалом (см. главу 15).

Кроме того, что уже было отмечено, эти функции возвращают 0 в случае успеха и -1 при ошибке. Вызовы функций, которые можно использовать для управления терминалом, описаны ниже.

int tcgetattr(int fd, struct termios * t);

Восстанавливает текущие настройки файлового дескриптора fd и помещает их в структуру, на которую указывает t.

int tcsetattr(int fd, int options, struct termios * t);

Устанавливает текущие настройки терминала для файлового дескриптора fd в настройки, приведенные в t. Всегда используйте tcgetattr() для заполнения t, затем модифицируйте его. Никогда не заполняйте t вручную: некоторые системы требуют установки или снятия флагов, кроме флагов, определенных POSIX, поэтому заполнение вручную является непереносимым.

Аргумент options определяет, когда изменения вступают в силу.

TCSANOW Изменение немедленно вступает в силу.
TCSADRAIN Изменение вступает в силу после того, как передаются все входные данные, уже записанные в fd ; перед вступлением в силу оно очищает очередь. Необходимо использовать это при смене выходных параметров.
TCSAFLUSH Изменение вступает в силу после того, как выходная очередь была очищена; входная же очередь отбрасывается перед вступлением изменений в силу.

Если система не может обработать некоторые настройки, например, скорость передачи данных, ей разрешается игнорировать их без выдачи сообщения об ошибке. Единственный способ, с помощью которого можно узнать, были ли приняты настройки — вызвать tcgetattr() и сравнить содержимое возвращаемой им структуры с содержимым структуры, переданной tcsetattr().

Поэтому более переносимые приложения используют код вроде показанного ниже.

#include

struct termios save;

struct termios set;

struct termios new;

int fd;

...

tcgetattr(fd, &save);

set = save;

cfsetospeed(&set, B2400);

cfsetispeed(&set, B2400);

tcsetattr(fd, &set);

tcgetattr(fd, &new);

if ((cfgetospeed(&set) != B2400) ||

 (cfgetispeed(&set) != B2400)) {

 /* объяснение */

}

Обратите внимание, что если не имеет значения, "зависнет" ли настройка termios, лучше проигнорировать это условие, как делается в robin.

speed_t cfgetospeed(struct termios * t);

speed_t cfgetispeed(struct termios * t);

Извлекает скорость, соответственно, вывода или ввода из t. Эти функции возвращают символическую скорость, такую же, которая дается cfsetospeed() и cfsetispeed().

int cfsetospeed(struct termios * t, speed_t speed);

int cfsetispeed(struct termios * t, speed_t speed);

Устанавливает, соответственно, вывода или ввода в t на speed. Обратите внимание, что эта функция не меняет скорость соединения на любом файловом дескрипторе; она просто устанавливает скорость в структуре termios. Скорость, как и другие характеристики, применяется к файловому дескриптору с помощью tcsetattr().

Эти функции принимают символическую скорость — то есть число, соответствующее определению одного из следующих макросов, имена которых определяют скорость в битах в секунду: B0 (0 бит в секунду, определяет отключенное состояние) B50, B75, B110, B134, B150, B200, B300, B600, B1200, B1800, B2400, B4800, B9600, B19200, B38400, B57600, B115200, B230400, B460800, B500000, B576000, B921600, B1000000, B1152000, B1500000, B2000000, B2500000, B3000000, B3500000 или B4000000. B57600 и выше в POSIX не описаны; переносимые исходные коды использует их только в том случае, если они защищены операторами #ifdef.

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

В настоящий момент скорость ввода игнорируется. Интерфейс termios устанавливает отдельные скорости ввода и вывода для асинхронного оборудования, допускающего раздельные скорости, но такое оборудование встречается довольно редко. Просто вызовите cfsetospeed() и cfsetispeed() попарно, чтобы ваш код продолжил работать в системах, поддерживающих раздельные скорости.

Не все tty поддерживают все скорости — последовательные порты на стандартных ПК не поддерживают более 115 200 бит/с. Как уже упоминалось выше, если для вас имеет значение, вступит ли в силу определенная настройка, необходимо использовать tcgetattr() для проверки после того, как вы попытаетесь установить ее с помощью tcsetattr(). Также обратите внимание, что установленная вами скорость является необязательной. Некоторые tty, например, локальные консоли, благополучно принимают и игнорируют любую установленную вами скорость.

int tcsendbreak(int fd, int duration)

Посылает поток нулей в fd, чтобы узнать определенную длительность (duration), которая также известна как разрыв. Если duration равняется 0, разрыв длится не менее 250 и не более 500 миллисекунд. К сожалению, POSIX не определяет элемент, длительность которого измеряется, поэтому единственной переносимой величиной для duration является 0. В Linux длительность увеличивает разрыв; 0 или 1 задают длительность между четвертью секунды и полсекунды; 2 — между полсекунды и секундой и так далее.

int tcdrain(int fd)

Ожидает, пока не передадутся все входные данные, ожидающие в данный момент на файловом дескрипторе fd.

int tcflush(int fd, int queue_selector)

Отбрасывает некоторые данные в файловом дескрипторе fd в зависимости от величины queue_selector.

TCIFLUSH Сбрасывает на диск все полученные, но еще не прочитанные интерфейсом данные.
TCOFLUSH Сбрасывает на диск все данные, записанные в интерфейс, но еще не отправленные.
TCIOFLUSH Сбрасывает на диск все ожидающие входные и выходные данные.

int tcflow(int fd, int action)

Приостановить или возобновить вывод или ввод в файловом дескрипторе fd. Более точные действия определяются action.

TCOOFF Приостановить вывод.
TCOON Восстановить вывод.
TCIOFF Передать символ STOP, запрашивающий прекращение передачи символов вторым концом соединения.
TCION Передать символ START, запрашивающий восстановление передачи символов вторым концом соединения.

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

 

16.5.2. Размеры окна

Существуют два запроса ioctl(), которые, к сожалению, не были закодированы как часть интерфейса termios, хотя и должны были. Размер tty, измеряемый строками и столбцами, должен управляться tcgetwinsize() и tcsetwinsize(), но, поскольку они не существуют, вместо этого следует использовать ioctl(). Для запроса текущего размера и установки нового размера применяйте структуру struct winsize.

#include

struct winsize {

 unsigned short ws_row;    /* количество строк */

 unsigned short ws_col;    /* количество столбцов */

 unsigned short ws_xpixel; /* не используется */

 unsigned short ws_ypixel; /* не используется */

};

Для запроса текущего размера используйте следующий вызов:

struct winsize ws;

ioctl(fd, TIOCGWINSZ, &ws);

Для установки нового размера заполните struct winsize и предусмотрите такой вызов:

ioctl(fd, TIOCSWINSZ, &ws);

В конце этой главы представлен пример условий, при которых возникает потребность в установке нового размера окна.

При изменении размеров окна лидеру группы процессов переднего плана на данном tty передается сигнал SIGWINCH. Ваш код может перехватить этот сигнал; используйте TIOCGWINSZ для запроса нового размера и внесите в свою программу все необходимые изменения.

 

16.5.3. Флаги

Четыре флаговых переменных — c_iflag, с_oflag, c_cflag и c_lflag — хранят флаги, управляющие определенными характеристиками. Заголовочный файл предоставляет символические константы битовых масок, которые, в свою очередь, предоставляют эти флаги. Устанавливайте их с помощью |= и переустанавливайте с помощью &= и как показано ниже.

t.c_iflag |= BRKINT;

t.c_iflag &= ~IGNBRK;

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

if ((t.c_cflag & CSIZE) == CS7) character_size = 7;

Набор флагов меняется от системы к системе. Наиболее важные флаги определены POSIX, но Linux, как и System V, включает несколько полезных флагов, не описанных в POSIX. Эта документация неполная; Linux поддерживает флаги, которые вряд ли понадобятся. Будут рассмотрены только те флаги, которые будут нужны наверняка.

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

#ifdef IUCLC

t.c_iflag |= IUCLC;

#endif

Также упоминаются области, представляющие особые проблемы переносимости, поэтому мы рассмотрим некоторые подробности функционирования других систем.

 

16.5.4. Флаги режима ввода

Флаги режима ввода влияют на обработку входных данных, хотя иногда они влияют и на выходные данные. Флаги, устанавливаемые в с_iflag, описаны ниже.

BRKINT и IGNBRK При установке IGNBRK состояние разрыва игнорируется (см. описанную ранее tcsendbreak() ). Если IGNBRK не установлен, а BRKINT установлен, состояние разрыва заставляет tty сбросить все очередизованные входные и выходные данные и послать сигнал SIGINT процессам в группе процессов переднего плана для tty. Если IGNBRK и BRKINT не установлены, состояние разрыва читается как нулевой символ ( '\0' ), кроме случая установки PARMRK , в котором обнаруживается ошибка кадрирования и вместо этого в приложение передаются три байта '\377' '\0' '\0' .
PARMRK и IGNPAR При установке IGNPAR полученные байты, содержащие ошибки четности или кадрирования, игнорируются (кроме того, что было ранее определено для состояния разрыва). Если IGNPAR не установлен, а PARMRK установлен, полученный байт с ошибкой четности или кадрирования передается приложению как трехбайтовая последовательность '\377' '\0' '\n' , где n — это байт в виде, в котором он был получен. В этом случае, если ISTRIP не установлен, допустимый символ '\377' передается приложению как последовательность двух символов '\377' '\377' ; при установке ISTRIP старший разряд символа '\377' разделяется и передается как '\177' . Если не установлены ни PARMRK , ни IGNPAR , полученный байт с ошибкой четности или кадрирования (отличной от состояния разрыва) передается приложению как отдельный символ '\0' .
INPCK При установке INPCK включается проверка четности. Если она не включается, PARMRK и IGNPAR не влияют на полученные ошибки четности.
ISTRIP При установке ISTRIP старший разряд отсекается из всех полученных байтов, ограничивая их семью битами.
INLCR При установке INLCR полученные символы новой строки ('\n') преобразуются в символы возврата каретки ( '\r' ).
IGNCR При установке IGNCR полученные символы возврата каретки ( '\r' ) игнорируются (не передаются приложению).
ICRNL Если установлен ICRNL , а IGNCR не установлен, полученные символы возврата каретки ( '\r' ) сообщаются приложению как символы новой строки ( '\n' ).
IUCLC При установке IUCLC и IEXTEN полученные символы верхнего регистра передаются приложению как символы нижнего регистра. Этот флаг в POSIX не определен.
IXOFF При установке IXOFF tty может передать символы Control-S и Control-Q терминалу, чтобы заставить его, соответственно, остановиться и восстановить вывод (то есть передачу данных на компьютер) с целью переполнения входных буферов tty. Это имеет отношение только к последовательным терминалам, поскольку сетевые и локальные терминалы имеют более прямые формы управления потоком. Даже последовательные терминалы часто поддерживают аппаратное управление потоком, контролируемое управляющим флагом ( c_cflag ) и делающее неуместным программное управление потоком (Control-S и Control-Q).
IXON При установке IXON принятый символ Control-S прекращает передачу входных данных в этот tty, а принятый символ Control-Q перезапускает передачу выходных данных в этот tty. Это соответствует любой форме терминального ввода-вывода, поскольку некоторые пользователи вводят буквенные символы Control-S и Control-Q для приостановки и восстановления вывода.
IXANY При установке IXANY любой принятый символ (не просто Control-Q) перезапускает передачу выходных данных. Этот флаг в POSIX не определен.
IMAXBEL При установке IMAXBEL предупреждающий символ ( '\а' ) передается тогда, когда символ принимается, а входной буфер уже полон. Этот флаг в POSIX не определен.

 

16.5.5. Флаги режима вывода

Флаги режима вывода модифицируют обработку выходных данных только в случае установки OPOST. Ни один из этих флагов не переносим, поскольку POSIX определяет только OPOST и называет его "реализация определена". Однако вы обнаружите, что настоящие приложения обработки терминалов часто нуждаются в обработке выходных данных, а флаги режима вывода, доступные в Linux, доступны также в большинстве систем Unix, включая SVR4.

Код терминала отслеживает текущий столбец, что позволяет подавить лишние символы возврата каретки ('\r') и преобразовать, где возможно, табуляцию в пробелы. Столбцы отсчитываются, начиная с нуля. Текущий столбец устанавливается в ноль всякий раз, когда передается или предполагается символ возврата каретки ('\r'), как может быть вызвано символом новой строки ('\n') при установке ONLRET или ONLCR, или когда текущий столбец установлен в единицу и передается символ забоя ('\b').

Флаги, работающие на с_oflag, перечислены ниже.

OPOST Это единственный флаг режима вывода, определенный в POSIX, который сообщает, что он включает обработку выходных данных, "определяемую реализацией". Если OPOST не установлен, к другим флагам режима вывода не обращаются и обработка выходных данных не выполняется.
OLCUC При установке OLCUC символы нижнего регистра передаются терминалу как символы верхнего регистра. Этот флаг в POSIX не определен.
ONLCR При установке ONLCR перед передачей символа новой строки ( '\n' ) передается символ возврата каретки ( '\r' ). Текущий столбец устанавливается в ноль. Этот флаг в POSIX не определен.
ONOCR При установке ONOCR символы возврата каретки ( '\r' ) ни обрабатываются, ни передаются, если текущий столбец равен нулю. Этот флаг в POSIX не определен.
OCRNL При установке OCRNL символы возврата каретки ( '\r' ) преобразуются в символы новой строки ( '\n' ). При установке ONLRET текущий столбец устанавливается в ноль. Этот флаг в POSIX не определен.
ONLRET При установке ONLRET во время передачи символа новой строки ( '\n' ) или возврата каретки ( '\r' ) текущий столбец устанавливается в ноль. Этот флаг в POSIX не определен.
OXTABS При установке OXTABS символы табуляции преобразуются в пробелы. Позиции табуляции установлены после каждого восьмого символа, а количество передаваемых пробелов определяется текущим столбцом. Этот флаг в POSIX не определен.

Кроме того, существуют флаги задержки, которые устанавливать не нужно; они предназначены для компенсации старого, плохо спроектированного и на данный момент, к счастью, редко встречающегося оборудования. За управление флагами задержки ответственны библиотеки termcap и terminfo. Это означает, что в их модификации нет необходимости. В [37] они упомянуты как устаревшие. Ядро Linux не реализует их в данный момент, и, поскольку это свойство не пользуется спросом, они вряд ли будут реализованы в будущем.

 

16.5.6. Управляющие флаги

Флаги режима управления влияют на такие параметры протокола, как четность и управление потоком. Флаги, устанавливаемые в с_cflag, описаны ниже.

CLOCAL При установке CLOCAL линии управления модемом игнорируются. Если он не установлен, open() блокируется до тех пор, пока модем не объявит состояние ответа абонента, утвердив линию обнаружения несущей.
CREAD Символы могут приниматься только в случае установки CREAD . Его сбрасывать не обязательно. ( Примечание . Попробуйте запустить stty -cread .)
CSIZE CSIZE — это маска для кодов, устанавливающих размер передаваемого символа в битах. Размер символа следует установить в перечисленные ниже значения. CS5 для пяти бит на символ; CS6 для шести бит на символ; CS7 для семи бит на символ; CS8 для восьми бит на символ.
CSTOPB При установке CSTOPB на конце каждого кадра символа генерируется по два стоповых бита. Если CSTOPB не установлен, генерируется лишь по одному стоповому биту. Устаревшее оборудование, требующее двух стоповых битов, встречается редко.
HUPCL Если установлен, то при закрытии последнего открытого файлового дескриптора на устройстве уровень на линиях последовательного порта DTR и RTS (если они существуют) будет снижен, чтобы заставить модем разорвать соединение. То есть, например, если пользователь, вошедший в систему через модем, затем выходит из нее, модем разрывает соединение. Если программа передачи данных открывает устройство для исходящих вызовов, а процесс затем закрывает устройство (или завершается), модем разорвет соединение.
PARENB и PARODD При установке PARENB генерируется бит четности. Если PARODD не установлен, генерируется проверка на четность. Если PARODD установлен, генерируется проверка нечетность. Если PARENB не установлен, PARODD игнорируется.
CRTSCTS Использовать аппаратное управление потоком (линии RTS и CTS). При высоких скоростях (19 200 бит/с и более) программное управление потоком с помощью символов XON и XOFF становится неэффективным. Вместо этого следует использовать аппаратное управление потоком. Этот флаг не определен в POSIX и не доступен под этим именем в большинстве других систем Unix. Это особенно непереносимая область управления терминалом, несмотря на распространенную потребность в аппаратном управлении потоком в современных системах. Система SVR4 особенно характерна тем, что она не предоставляет возможности установки управления потоком с помощью termios , а только через другой интерфейс под названием termiox .

 

16.5.7. Управляющие символы

Управляющие символы — это символы со специальными значениями, которые могут отличаться в зависимости от того, находится ли терминал в каноническом или неформатируемом режиме ввода, и в зависимости от установок различных управляющих флагов. Каждое смещение (кроме VMIN и VTIME) в массиве с_сс обозначает действие и содержит код символа, предназначенный для этого действия. Например, установите символ прерывания на Control-C с помощью следующего кода:

ts.с_сс[VINTR] = CTRLCHAR('С');

Макрос CTRLCHAR() определен как

#define CTRLCHAR(ch) ((ch)&0x1F)

Некоторые системы имеют макрос CTRL(), определенный в , но не поддерживаемый во всех системах, поэтому определение нашей собственной версии будет более надежным. Мы используем запись ^C для обозначения Control-C.

Расположения символов, не определенные POSIX, активны только в случае установки локального управляющего флага IEXTEN(c_lflag).

Управляющие символы, которые вы видите как индексы массива с_сс, перечислены ниже.

VINTR Смещение VINTR обычно устанавливается в ^C . Оно обычно сбрасывает на диск очереди ввода-вывода и передает SIGINT элементам группы процесса переднего плана, ассоциированным с tty. Процессы, неявно обрабатывающие SIGINT , немедленно завершаются.
VQUIT Смещение VQUIT обычно устанавливается в ^\ . Оно обычно сбрасывает на диск очереди ввода-вывода и передает SIGQUIT элементам группы процесса переднего плана, ассоциированным с tty. Процессы, неявно обрабатывающие SIGQUIT , завершаются, при возможности сброса дампа ядра (см. главу 10).
VERASE Смещение VERASE обычно устанавливается в ^H или ^? . В каноническом режиме оно обычно стирает предыдущий символ в строке. В неформатируемом режиме это несущественно.
VKILL Смещение VKILL обычно установлено в ^U . В каноническом режиме оно обычно стирает всю строку. В неформатируемом режиме это несущественно.
VEOF Смещение VEOF обычно установлено в ^D . В каноническом режиме оно заставляет read() на файловом дескрипторе возвращать 0, сигнализируя о состоянии конца файла. На некоторых системах оно может делить пространство с символом VMIN , активным лишь в неформатируемом режиме. (Это не проблема, если вы сохраните struct termios с каноническими установками режима для восстановления действий в неформатируемом режиме, что все равно присуще практике программирования с применением termios .)
VSTOP Смещение VSTOP обычно установлено в ^S . Оно заставляет tty приостановить передачу выходных данных до получения символа VSTART , или, в случае установки IXANY , до получения любого символа.
VSTART Смещение VSTART обычно установлено в ^Q . Оно запускает приостановленный вывод tty.
VSUSP Смещение VSUSP обычно установлено в ^Z . Оно вызывает передачу SIGTSTP текущей группе процессов переднего плана; более подробно об этом рассказывается в главе 15.
VEOL и VEOL2 В каноническом режиме эти символы, а также символ новой строки ( '\n' ), сигнализируют о состоянии конца строки. Это вызывает передачу скомпонованного буфера и запуск нового буфера. На некоторых системах VEOL может делить пространство с символом VTIME , активным лишь в неформатируемом режиме, так же, как VEOF может делить пространство с VMIN . Символ VEOL2 в POSIX не определен.
VREPRINT Смещение VREPRINT обычно установлено в ^R . В каноническом режиме в случае установки флага ECHO оно вызывает локальное отражение символа VREPRINT , новой строки (и возврата каретки, если это допустимо), а также перепечатку всего текущего буфера. Этот символ в POSIX не определен.
VWERASE Смещение WERASE обычно установлено в ^W . В каноническом режиме оно стирает все пробелы в конце буфера, затем все остальные символы, что дает эффект стирания предыдущего слова в строке. Этот символ в POSIX не определен.
VLNEXT Смещение VLNEXT обычно установлено в ^V . Само оно не вводится в буфер, но вызывает литеральное помещение в буфер следующего символа, даже если это один из управляющих символов. Для того чтобы ввести один литеральный символ VLNEXT , введите его дважды. Этот символ в POSIX не определен.

Для отключения любой позиции управляющего символа установите его значение в _POSIX_VDISABLE. Это работает только в случае определения _POSIX_VDISABLE как значения, не равного -1. _POSIX_VDISABLE работает в Linux, но переносимая программа, к сожалению, не сможет зависеть от отключения расположений управляющих символов во всех системах.

 

16.5.8. Локальные флаги

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

Некоторые флаги могут вести себя иначе, в зависимости от того, в каком режиме находится терминал: каноническом или неформатируемом. Флаги, ведущие себя иначе в каноническом и неформатируемом режимах, отмечены.

Флаги, работающие на c_cflag, перечислены ниже.

ICANON При установке ICANON включается канонический режим. Если ICANON не установлен, включается неформатируемый режим.
ECHO При установке ECHO включается локальное эхо. Если ECHO не установлен, все остальные флаги, названия которых начинаются с ECHO , эффективно отключаются и функционируют так, как будто они все, кроме ECHONL , не установлены.
ECHOCTL При установке ECHOCTL управляющие символы выводятся как ^C , где С — это символ, формирующийся добавлением восьмеричного 0100 к управляющему символу, по модулю восьмеричного 0200. Поэтому Control-C отображается как ^C , a Control-? (восьмеричный 0177) отображается как ^? ( ? — это восьмеричный 77). Этот флаг в POSIX не определен.
ECHOE В каноническом режиме при установке ECHOE в случае получения символа ERASE предыдущий символ на дисплее по возможности стирается.
ECHOK и ECHOKE В каноническом режиме при получении символа KILL вся текущая строка стирается из буфера. Если не установлены ни ECHOK , ни ECHOKE , ни ECHOE , выводится представление символа KILL с помощью ECHOCTL ( ^U по умолчанию) для обозначения стертой строки. Если установлены ECHOE и ECHOK , но ECHOKE не установлен, выводится представление символа KILL с помощью ECHOCTL , сопровождаемое новой строкой, которая затем обрабатывается OPOST в случае установки OPOST . Если установлены ECHOE , ECHOK и ECHOKE , строка стирается. См. описание ECHOPRT для другой вариации на эту тему. Флаг ECHOKE в POSIX не определен. В системах без флага ECHOKE установка флага ECHOK может быть эквивалентна установке и ECHOK , и ECHOKE в Linux.
ECHONL В каноническом режиме при установке ECHONL символы новой строки ( '\n' ) отражаются даже в том случае, если ECHO не установлен.
ECHOPRT В каноническом режиме при установке ECHOPRT символы выводятся при стирании, когда принимаются символы ERASE или WERASE (или KILL , если установлены ECHOK и ECHOKE ). Когда принимается первый в последовательности символ стирания, выводится \ , а при выводе последнего символа стирания (достигается конец строки или вводится нестертый символ), выводится / . Каждый вводимый вами нормальный символ просто отображается. Поэтому ввод asdf , сопровождаемый двумя символами ERASE , а также df и символом KILL , будет выглядеть следующим образом: asdf\fd/df\fdsa/ . Этот флаг полезен для отладки и использования документирующих терминалов вроде первоначального телетайпа, где символы печатаются на бумаге; в другом случае он не пригодится. Этот флаг в POSIX не определен.
ISIG Если установлен ISIG , управляющие символы INTR , QUIT и SUSP вызывают отправку соответствующего сигнала ( SIGINT , SIGQUIT или SIGTSTP соответственно; см. главу 12) всем процессам в текущей группе процессов переднего плана на данном tty.
NOFLSH Обычно при получении символов INTR и QUIT очереди ввода и вывода сбрасываются. При установке NOFLSH очереди не сбрасываются.
TOSTOP Если установлен TOSTOP , то в том случае, когда процесс, не находящийся в текущей группе процессов переднего плана, пытается выполнить запись в свой управляющий терминал, передается SIGTTOU всей группе процессов, членом которой является данный процесс. По умолчанию этот сигнал останавливает процесс, как при нажатии комбинации клавиш, соответствующей символу SUSP .
IEXTEN Этот флаг описан в POSIX как определяемый реализацией. Он включает обработку символов ввода, определяемую реализацией. Хотя переносимые программы не устанавливают этот бит, IUCLC и определенные возможности стирания символов в Linux зависимы от его установки. К счастью, он чаще всего разрешен по умолчанию в системах Linux, поскольку ядро изначально разрешает его при установке tty, поэтому обычно не нужно устанавливать его по какой-либо причине.

 

16.5.9. Управление

read()

Два элемента в массиве с_сс не являются управляющими символами и имеют отношение только к неформатируемому режиму: VTIME и VMIN. В этом режиме они определяют, когда возвращается read(). В каноническом режиме read() возвращается только в том случае, если строки были собраны или был достигнут конец файла, за исключением случая установки опции O_NONBLOCK.

В неформатируемом режиме считывание по одному байту за раз неэффективно. Также неэффективно опрашивать порт чтением в неблокируемом режиме. Существуют два намного более эффективных дополнительных метода чтения.

Первый заключается в использовании poll(), как описано в главе 13 и демонстрируется в коде robin.с. Если poll() сообщает, что файловый дескриптор готов к чтению, то известно, что вы можете немедленно прочитать некоторое количество байтов. Однако сочетание poll() со вторым методом сделает ваш код более эффективным, предоставляя возможность считывать больше байтов за один раз.

"Управляющие символы" VTIME и VMIN состоят в сложных взаимоотношениях. VTIME определяет промежуток времени для ожидания в десятых долях секунды (он не может быть больше cc_t, обычно это 8-битный unsigned char), который также может равняться нулю. VMIN определяет минимальное количество байт для ожидания (не для считывания — третий аргумент read() определяет максимальное количество байтов для считывания), которое тоже может равняться нулю.

• Если VTIME равен нулю, VMIN определяет количество байт для ожидания. Вызов read() не возвращается, пока не будут считано VMIN байт или пока не будет получен сигнал.

• Если VMIN равен нулю, VTIME определяет количество десятых частей секунд для ожидания read() перед возвращением, даже если данные недоступны. В таком случае read(), возвращающий нуль, необязательно сигнализирует о состоянии конца файла, как он обычно делает.

• Если ни VTIME, ни VMIN не равняются нулю, VTIME определяет количество десятых долей секунды для ожидания read() после того, как будет доступен хотя бы один байт. Если данные доступны при вызове read(), таймер немедленно запускается. Если данные недоступны при вызове read(), таймер запускается при принятии первого байта. Вызов read() возвращается или тогда, когда были приняты хотя бы байты VMIN, или по истечении таймера, независимо от того, что произойдет раньше. Он всегда возвращает хотя бы один байт, поскольку таймер не запускается, пока не будет доступен хотя бы один байт.

• Если и VTIME, и VMIN равны нулю, read() всегда немедленно возвращается, даже если данные недоступны. И снова ноль необязательно указывает на состояние конца файла.

 

16.6. Псевдотерминалы

 

Псевдотерминалы, или pty — это механизм, позволяющий программе на уровне пользователя заменять место (логически говоря) драйвера tty для элемента оборудования. pty имеет два отдельных конца: конец, эмулирующий оборудование, называется ведущим устройством pty, а конец, обеспечивающий программы обычным интерфейсом tty, называется подчиненным компонентом pty. Подчиненный компонент выглядит как обычный tty; ведущее устройство выглядит как стандартное устройство символьного ввода-вывода и не является tty.

Драйвер последовательного порта обычно реализуется как часть кода ядра, управляемая прерываниями. Однако так бывает не всегда. Например, существует хотя бы один терминальный сервер, основанный на SCSI, который использует обобщенный интерфейс SCSI для организации программы на уровне пользователя, сообщающейся с терминальным сервером и предоставляющей доступ к последовательным портам через pty.

Сеансы работы с сетевыми терминалами происходят подобным образом; программы rlogind и telnetd подключают сетевой сокет к ведущему устройству pty и запускают оболочку в подчиненном компоненте pty, чтобы заставить сетевые подключения действовать как tty, позволяя запускать интерактивные программы в сетевом подключении, не имеющем ничего общего с tty. Экранная программа мультиплексирует несколько соединений pty на один tty, который может или не может быть pty, соединенным с пользователем. Ожидаемая программа позволяет программам, настаивающим на запуске в интерактивном режиме в tty, быть запущенными в подчиненном компоненте pty под управлением другой программы, соединенной с ведущим устройством pty.

 

16.6.1. Открытие псевдотерминалов

Существует широкое разнообразие способов открытия псевдотерминалов. Обычно это делается (по крайней мере, в Linux) способом, более или менее соответствующим стандартам, основанным на SysV, а также устаревшим способом, основанным на практике BSD. Наиболее распространенным методом среди системных программистов в Linux является набор расширений BSD, реализованных также как часть glibc. Менее распространенный метод документируется как часть стандарта 1998 года — Unix98, и документируется иначе в версии 2000 года стандарт Unix98.

Исторически существует два различных метода открытия псевдотерминалов в Unix и подобных системах. Linux изначально придерживался модели BSD, хотя она более сложная в использовании, поскольку модель SysV явно написана в рамках STREAMS, а в Linux STREAMS не реализована. Однако модель BSD требует, чтобы каждое приложение искало неиспользуемое ведущее устройство pty, зная о многих специфических именах устройств. Между 64 и 256 устройства pty обычно доступны, а с целью поиска первого открытого устройства программы проводят поиск в устройствах, начиная с наименьшего числа. Они выполняют поиск в специфической манере, которая демонстрируется в программе ptypair, включенной в данный раздел.

С моделью BSD связано несколько проблем.

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

• Время, уходящее на поиск, становится ощутимым при поиске среди тысяч узлов устройств в каталоге /dev. Системное время тратится, и доступ к системе замедляется, что очень плохо масштабируется в больших системах.

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

Поскольку модель SysV явно написана в рамках STREAMS и требует использования вызовов ioctl() для запуска подчиненных компонентов, она не является вариантом выбора Linux. Однако интерфейс Unix98 не определяет функции, присущие STREAMS, поэтому в 1998 году в Linux была добавлена поддержка псевдотерминалов стиля Unix98.

Ядро Linux может быть скомпилировано без поддержки интерфейса Unix98, и можно встретить более старые системы без псевдотерминалов стиля Unix98, поэтому мы представим код, который пытается открыть псевдотерминалы стиля Unix98, но также может вернуться к интерфейсу BSD. (Мы не документируем части модели SysV, присущие STREAMS; в [35] подробно описан интерфейс STREAMS. Вам вряд ли понадобится код, специфичный для STREAMS; спецификация Unix98 не требует его.)

 

16.6.2. Простые способы открытия псевдотерминалов

В библиотеке libutil glibc предлагает две функции — openpty() и forkpty(), — выполняющие почти всю работу по поддержке псевдотерминалов.

#include

int openpty(int * masterfd, int * slavefd, char * name,

 struct termios * term, struct winsize * winp);

int forkpty(int * masterfd, char * name,

 struct termios * term, struct winsize * winp);

Функция openpty() открывает ведущие и подчиненные псевдотерминалы, необязательно используя структуры struct termios и struct winsize, передаваемые как опции настройки псевдотерминала, возвращая 0 в случае успеха и -1 в случае ошибки. Файловые дескрипторы ведущего устройства и подчиненного компонента возвращаются аргументам masterfd и slavefd соответственно. Аргументы term и winp могут быть NULL, в случае чего они игнорируются, и настройка не выполняется.

Функция forkpty() работает так же, как и openpty(), но вместо возврата файлового дескриптора подчиненного компонента она разветвляет псевдотерминал как управляющий терминал stdin, stdout и stderr для дочернего процесса, а затем, подобно fork(), возвращает идентификатор дочернего процесса родительскому и 0 дочернему либо -1 при возникновении ошибки.

Даже с этими удобными интерфейсами связана значительная проблема: аргумент name был изначально предназначен для возврата имени устройства псевдотерминала вызывающему коду, но его использование небезопасно, поскольку openpty() и forkpty() не знают размера буфера. Всегда передавайте NULL в аргументе name. Используйте функцию ttyname(), описанную в начале этой главы, чтобы получить путевое имя файла устройства псевдотерминала.

Предпочтительный способ работы с struct termios заключается в использовании цикла чтение-модификация-запись, но данному случаю это не соответствует по двум причинам. Можно передать NULL и принять значения по умолчанию, что достаточно в большинстве случаев; а когда вы хотите предоставить настройки termios, вы часто заимствуете настройки у другого tty, или знаете точно, какими они должны быть (например, в случае концентратора последовательного порта SCSI, описанного ранее в этой главе).

tcgetattr(STDIN_FILENO, &term);

ioctl(STDIN_FILENO, TIOCGWINSZ, &ws);

pid = forkpty(&masterfd, NULL, &term, &ws);

 

16.6.3. Сложные способы открытия псевдотерминалов

Интерфейс Unix98 для распределения пары псевдотерминала представляет собой следующий набор функций.

#define _XOPEN_SOURCE 600

#include

#include

int posix_openpt(int oflag);

int grantpt(int fildes);

int unlockpt(int fildes);

char * ptsname(int fildes);

Функция posix_openpt() — это то же, что и открытие устройства /dev/ptmx, но теоретически она более переносима (поскольку везде принимается). Рекомендуется в этот раз использовать open("/dev/ptmx", oflag) для максимальной практической переносимости. Если вы хотите установить один или два флага open() или posix_openpt(), используйте O_RDWR, как обычно; если вы вместо этого не открываете управляющий tty для процесса, используйте O_RDWR | O_NOCTTY. open() или posix_openpt() вернет открытый файловый дескриптор управляющему устройству псевдотерминала. Затем вызовите grantpt() с файловым дескриптором управляющего устройства псевдотерминала, возвращенным из posix_openpt(), для изменения режима и владельца подчиненного компонента псевдотерминала, а потом — unlockpt(), чтобы сделать подчиненный компонент псевдотерминала доступным для открытия. Интерфейс Unix98 для открытия подчиненного устройства псевдотерминала должен просто открыть имя, возвращенное ptsname(). Все эти функции возвращают -1 в случае ошибки, кроме ptsname(), возвращающей в такой ситуации NULL.

Функции в ptypair.c распределяют согласованную пару устройств pty. Пример функции get_master_pty() в строке 22 ptypair.с открывает управляющее устройство pty и возвращает файловый дескриптор родительскому процессу, а также предоставляет имя соответствующему подчиненному компоненту pty. Он сначала испытывает интерфейс Unix98 на распределение управляющего устройства pty, а если это не работает (например, если ядро скомпилировано без поддержки pty Unix98, возможно, для встроенных систем), возвращается к старому интерфейсу стиля BSD. Соответствующая функция get_slave_pty() в строке 87 может быть использована после fork() для открытия соответствующего подчиненного компонента pty.

  1: /* ptypair.c */

  2:

  3: #define _XOPEN_SOURCE 600

  4: #include

  5: #include

  6: #include

  7: #include

  8: #include

  9: #include

 10: #include

 11: #include

 12:

 13:

 14: /* get_master_pty() принимает дважды косвенный символьный указатель на

 15:  * место помещения имени подчиненного компонента pty и возвращает целочисленный

 16:  * файловый дескриптор. Если возвращается значение < 0, значит, возникла ошибка.

 17:  * В противном случае возвращается файловый дескриптор ведущего устройства pty

 18:  * и заполняет *name именем соответствующего подчиненного компонента pty. После

 19:  * открытия подчиненного компонента pty, вы отвечаете за освобождение *name.

 20:  */

 21:

 22: int get_master_pty(char **name) {

 23:  int i, j;

 24:  /* значение по умолчанию, соответствующее ошибке */

 25:  int master = -1;

 26:  char *slavename;

 27:

 28:  master = open("/dev/ptmx", O_RDWR);

 29:  /* Это эквивалентно, хотя и более широко реализовано,

 30:   * но теоретически менее переносимо, следующему:

 31:   * master = posix_openpt(O_RDWR);

 32:   */

 33:

 34:  if (master >= 0 && grantpt(master) >= 0 &&

 35:   unlockpt(master) >= 0) {

 36:   slavename = ptsname(master);

 37:   if (!slavename) {

 38:    close(master);

 39:    master = -1;

 40:    /* сквозной проход для нейтрализации ошибки */

 41:   } else {

 42:    *name = strdup(slavename);

 43:    return master;

 44:   }

 45:  }

 46:

 47:  /* Остаток этой функции — нейтрализация ошибки для старых систем */

 48:

 49:  /* создать фиктивное имя для заполнения */

 50:  *name = strdup("/dev/ptyXX");

 51:

 52:  /* искать неиспользуемый pty */

 53:  for (i=0; i<16 && master <= 0; i++) {

 54:   for (j = 0; j<16 && master <= 0; j++) {

 55:    (*name)[8] = "pqrstuvwxyzPQRST"[i];

 56:    (*name)[9] = "0123456789abcdef"[j];

 57:    /* открыть ведущее устройство pty */

 58:    if ((master = open(*name, O_RDWR)) < 0) {

 59:     if (errno == ENOENT) {

 60:      /* устройства pty исчерпаны */

 61:      free(*name);

 62:      return(master);

 63:     }

 64:    }

 65:   }

 66:  }

 67:

 68:  if ((master < 0) && (i == 16) && (j == 16)) {

 69:   /* необходимо для каждого неудачного pty */

 70:   free(*name);

 71:   return(master);

 72:  }

 73:

 74:  /* Подставляя букву, изменить имя ведущего устройства pty

 75:   * в имени подчиненного компонента pty.

 76:   */

 77:  (*name)[5] = 't';

 78:

 79:  return(master);

 80: }

 81:

 82: /* get_slave_pty() возвращает целочисленный файловый дескриптор.

 83:  * Если возвращается значение < 0, значит, возникла ошибка.

 84:  * В противном случае возвращается файловый дескриптор подчиненного

 85:  * компонента. */

 86:

 87: int get_slave_pty(char * name) {

 88:  struct group *gptr;

 89:  gid_t gid;

 90:  int slave = -1;

 91:

 92:  if (strcmp(name, "/dev/pts/")) {

 93:   /* Интерфейс Unix98 не использовался, необходима

 94:    * специальная обработка полномочий или прав владения.

 95:    *

 96:    * Выполнить chown/chmod для соответствующего pty, если возможно.

 97:    * Это будет работать, только если имеет полномочия root.

 98:    * В качестве альтернативы можно написать и запустить небольшую

 99:    * setuid-программу, которая сделает все это.

100:    *

101:    * В противном случае все проигнорировать и пользоваться

102:    * только интерфейсом Unix98.

103:    */

104:   if ((gptr = getgrnam("tty")) != 0) {

105:    gid = gptr->gr_gid;

106:   } else {

107:    /* если группа tty не существует, не изменять группу

108:     * на подчиненном компоненте pty, а только владельца

109:     */

110:    gid = -1;

111:   }

112:

113:   /* Обратите внимание, что здесь не осуществляется проверка на ошибки.

114:    * Однако если выполняемые действия являются критически важными,

115:    * проверка ошибок должна быть. */

116:   chown(name, getuid(), gid);

117:

118:   /* Этот код делает подчиненный компонент доступным для чтения/записи

119:    * только конкретному пользователю. Если код предназначен для

120:    * интерактивной оболочки, которая должна получать сообщения

121:    * "write" и "wall", добавьте ниже "ИЛИ" с S_IWGRP во второй аргумент.

122:    * В таком случае потребуется перенести эту строку за пределы

123:    * оператора if(), чтобы код мог выполняться для интерфейсов как

124:    * BSD-стиля, так и Unix98-стиля.

125:    */

126:   chmod(name, S_IRUSR|S_IWUSR);

127:  }

128:

129:  /* открыть соответствующий подчиненный компонент pty */

130:  slave = open(name, O_RDWR);

131:

132:  return(slave);

133: }

Функция get_slave_pty() не делает ничего нового. Все функции описываются в других местах этой книги, поэтому здесь они не объясняются.

 

16.6.4. Примеры псевдотерминалов

Возможно, одной из самых простых программ, которая может быть написана для использования pty, является программа, открывающая пару pty и запускающая оболочку на подчиненном компоненте pty, соединяя его с управляющим устройством pty. Написав эту программу, вы можете расширять ее любым подходящим способом, forkptytest.с является примером использования функции forkpty(), a ptytest.с — это пример, который использует функции, определенные в ptypair.с, и является несколько более сложным.

  1: /* forkptytest.с */

  2:

  3: #include

  4: #include

  5: #include

  6: #include

  7: #include

  8: #include

  9: #include

 10: #include

 11: #include

 12:

 13:

 14: volatile int propagate_sigwinch = 0;

 15:

 16: /* sigwinch_handler

 17:  * распространяет изменения размеров окна из входного файлового

 18:  * дескриптора на ведущую сторону pty.

 19:  */

 20: void sigwinch_handler(int signal) {

 21:  propagate_sigwinch = 1;

 22: }

 23:

 24:

 25: /* forkptytest пытается открыть пару pty с запуском оболочки

 26:  * на подчиненной стороне pty.

 27:  */

 28: int main(void) {

 29:  int master;

 30:  int pid;

 31:  struct pollfd ufds[2];

 32:  int i;

 33: #define BUFSIZE 1024

 34:  char buf[1024];

 35:  struct termios ot, t;

 36:  struct winsize ws;

 37:  int done = 0;

 38:  struct sigaction act;

 39:

 40:  if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) < 0) {

 41:   perror("ptypair: не удается получить размеры окна");

 42:   exit(1);

 43:  }

 44:

 45:  if ((pid = forkpty(&master, NULL, NULL, &ws)) < 0) {

 46:   perror("ptypair");

 47:   exit(1);

 48:  }

 49:

 50:  if (pid == 0) {

 51:   /* запустить оболочку */

 52:   execl("/bin/sh", "/bin/sh", 0);

 53:

 54:   /* сюда управление никогда не попадет */

 55:   exit(1);

 56: }

 57:

 58:  /* родительский процесс */

 59:  /* установить обработчик SIGWINCH */

 60:  act.sa_handler = sigwinch_handler;

 61:  sigemptyset(&(act.sa_mask));

 62:  act.sa_flags = 0;

 63:  if (sigaction(SIGWINCH, &act, NULL) < 0) {

 64:   perror("ptypair: невозможно обработать SIGWINCH");

 65:   exit(1);

 66:  }

 67:

 68:  /* Обратите внимание, что настройки termios устанавливаются только

 69:   * для стандартного ввода; ведущая сторона pty НЕ является tty.

 70:   */

 71:  tcgetattr(STDIN_FILENO, &ot);

 72:  t = ot;

 73:  t.c_lflag &= ~(ICANON | ISIG | ECHO | ECHOCTL | ECHOE |

 74:   ECHOK | ECHOKE | ECHONL | ECHOPRT);

 75:  t.c_iflag |= IGNBRK;

 76:  t.c_cc[VMIN] = 1;

 77:  t.c_cc[VTIME] = 0;

 78:  tcsetattr(STDIN_FILENO, TCSANOW, &t);

 79:

 80:  /* Этот код взят без изменений из robin.с

 81:   * Если дочерний процесс завершается, читающая ведущая сторона

 82:   * дoлжнa вернуть -1 и завершиться.

 83:   */

 84:  ufds[0].fd = STDIN_FILENO;

 85:  ufds[0].events = POLLIN;

 86:  ufds[1].fd = master;

 87:  ufds[1].events = POLLIN;

 88:

 89:  do {

 90:   int r;

 91:

 92:   r = poll(ufds, 2, -1);

 93:   if ((rs < 0) && (errno != EINTR)) {

 94:    done = 1;

 95:    break;

 96:   }

 97:

 98:   /* сначала проверить возможность завершения */

 99:   if ((ufds[0].revents | ufds[1].revents) &

100:    (POLLERR | POLLHUP | POLLNVAL)) {

101:    done = 1;

102:    break;

103:   }

104:

105:   if (propagate_sigwinch) {

106:    /* обработчик сигналов запросил распространение SIGWINCH */

107:    if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) < 0) {

108:     perror("ptypair: не удается получить размеры окна");

109:    }

110:    if (ioctl(master, TIOCSWINSZ, &ws) < 0) {

111:     perror("не удается восстановить размеры окна");

112:    }

113:

114:    /* не делать этого снова до поступления следующего SIGWINCH */

115:    propagate_sigwinch = 0;

116:

117:    /* опрос мог быть прерван SIGWINCH,

118:     * потому повторить попытку.

119:     */

120:    continue;

121:   }

122:

123:   if (ufds[1].revents & POLLIN) {

124:    i = read (master, buf, BUFSIZE);

125:    if (i >= 1) {

126:     write(STDOUT_FILENO, buf, i);

127:    } else {

128:     done = 1;

129:    }

130:   }

131:

132:   if (ufds[0].revents & POLLIN) {

133:    i = read (STDIN_FILENO, buf, BUFSIZE);

134:    if (i >= 1) {

135:     write(master, buf, i);

136:    } else {

137:     done = 1;

138:    }

139:   }

140:

141:  } while (!done);

142:

143:  tcsetattr(STDIN_FILENO, TCSANOW, &ot);

144:  exit(0);

145: }

Программа forkptytest.с делает очень немногое из того, чего вы раньше не видели. Обработка сигналов рассматривается в главе 12, а цикл poll() почти полностью переписан из кода robin.с, представленного ранее в этой главе (за исключением обработки управляющих символов), равно как и код, модифицирующий настройки termios.

Остается лишь объяснить распространение изменений размеров окна.

В строке 105 после завершения poll() мы проверяем, является ли причиной завершения poll() сигнал SIGWINCH, доставляемый функции sigwinch_handler в строке 20. Если это так, необходимо получить новый размер текущего окна из стандартного ввода и распространить его в pty подчиненного компонента. Установкой размера окна SIGWINCH передается автоматически процессу, работающему на pty; мы не должны явно передавать SIGWINCH этому процессу.

Теперь для сравнения посмотрите, насколько усложняется этот код в случае использования функций, определенных в ptypair.с.

  1: /* ptytest.с */

  2:

  3: #include

  4: #include

  5: #include

  6: #include

  7: #include

  8: #include

  9: #include

 10: #include

 11: #include

 12: #include

 13: #include

 14: #include "ptypair.h"

 15:

 16:

 17: volatile int propagate_sigwinch = 0;

 18:

 19: /* sigwinch_handler

 20: * распространяет изменения размеров окна из входного файлового

 21: * дескриптора на ведущую сторону pty.

 22: */

 23: void sigwinch_handler(int signal) {

 24:  propagate_sigwinch = 1;

 25: }

 26:

 27:

 28: /* ptytest пытается открыть пару pty с запуском оболочки

 29:  * на подчиненной стороне pty.

 30:  */

 31: int main(void) {

 32:  int master;

 33:  int pid;

 34:  char * name;

 35:  struct pollfd ufds[2];

 36:  int i;

 37: #define BUFSIZE 1024

 38:  char buf[1024];

 39:  struct termios ot, t;

 40:  struct winsize ws;

 41:  int done = 0;

 42:  struct sigaction act;

 43:

 44: if ((master = get_master_pty(&name)) < 0) {

 45:  perror("ptypair: не удается открыть ведущее устройство pty");

 46:  exit(1);

 47: }

 48:

 49: /* установить обработчик SIGWINCH */

 50:  act.sa_handler = sigwinch_handler;

 51:  sigemptyset(&(act.sa_mask));

 52:  act.sa_flags = 0;

 53:  if (sigaction (SIGWINCH, &act, NULL) < 0) {

 54:   perror("ptypair: невозможно обработать SIGWINCH");

 55:   exit(1);

 56:  }

 57:

 58:  if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) < 0) {

 59:   perror("ptypair: не удается получить размеры окна");

 60:   exit(1);

 61:  }

 62:

 63:  if ((pid = fork()) < 0) {

 64:   perror("ptypair");

 65:   exit(1);

 66:  }

 67:

 68:  if (pid == 0) {

 69:   int slave; /* файловый дескриптор для подчиненного компонента pty*/

 70:

 71:   /* Мы находимся в дочернем процессе */

 72:   close(master);

 73:

 74:   if ((slave = get_slave_pty(name)) < 0) {

 75:    perror("ptypair: не удается открыть подчиненный компонент pty");

 76:    exit(1);

 77:   }

 78:   free(name);

 79:

 80:   /* Мы должны сделать этот процесс лидером группы сеансов,

 81:    * поскольку он выполняется на новом PTY, а функции вроде

 82:    * управления заданиями просто не будут корректно работать,

 83:    * если нет лидера группы сеансов и лидера группы процессов

 84:    * (который автоматически является лидером группы сеансов).

 85:    * Это также разъединяет со старым управляющим tty.

 86:    */

 87:   if (setsid() < 0) {

 88:    perror("невозможно установить лидер сеанса");

 89:   }

 90:

 91:   /* Соединиться с новым управляющим tty. */

 92:   if (ioctl(slave, TIOCSCTTY, NULL)) {

 93:    perror("невозможно установить новый управляющий tty");

 94:   }

 95:

 96:   /* сделать подчиненный pty стандартным устройством ввода, вывода и ошибок */

 97:   dup2(slave, STDIN_FILENO);

 98:   dup2(slave, STDOUT_FILENO);

 99:   dup2(slave, STDERR_FILENO);

100:

101:   /* в этой точке подчиненный pty должен быть стандартным устройством ввода */

102:   if (slave > 2) {

103:    close(slave);

104:   }

105:

106:   /* Попытаться восстановить размеры окна; сбой не является критичным */

107:   if (ioctl(STDOUT_FILENO, TIOCSWINSZ, &ws) < 0) {

108:    perror("не удается восстановить размеры окна");

109:   }

110:

111:   /* запустить оболочку */

112:   execl("/bin/sh", "/bin/sh", 0);

113:

114:   /* сюда управление никогда не попадет */

115:   exit(1);

116:  }

117:

118:  /* родительский процесс */

119:  free(name);

120:

121:  /* Обратите внимание, что настройки termios устанавливаются только

122:   * для стандартного ввода; ведущая сторона pty НЕ является tty.

123:   */

124:  tcgetattr(STDIN_FILENO, &ot);

125:  t = ot;

126:  t.c_lflag &= ~(ICANON | ISIG | ECHO | ECHOCTL | ECHOE |

127:   ECHOK | ECHOKE | ECHONL | ECHOPRT);

128:  t.c_iflag |= IGNBRK;

129:  t.c_cc[VMIN] = 1;

130:  t.c_cc[VTIME] = 0;

131:  tcsetattr(STDIN_FILENO, TCSANOW, &t);

132:

133:  /* Этот код взят без изменений из robin.с

134:   * Если дочерний процесс завершается, читающая ведущая сторона

135:   * должна вернуть -1 и завершиться.

136:   */

137:  ufds[0].fd = STDIN_FILENO;

138:  ufds[0].events = POLLIN;

139:  ufds[1].fd = master;

140:  ufds[1].events = POLLIN;

141:

142:  do {

143:   int r;

144:

145:   r = poll(ufds, 2, -1);

146:   if ((r < 0) && (errno != EINTR)) {

147:    done = 1;

148:    break;

149:   }

150:

151:   /* сначала проверить возможность завершения */

152:   if ((ufds[0].revents | ufds[1].revents) &

153:    (POLLERR | POLLHUP | POLLNVAL)) {

154:    done = 1;

155:    break;

156:   }

157:

158:   if (propagate_sigwinch) {

159:    /* обработчик сигнала запросил распространение SIGWINCH */

160:    if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) < 0) {

161:     perror("ptypair: не удается получить размеры окна");

162:    }

163:    if (ioctl(master, TIOCSWINSZ, &ws) < 0) {

164:     perror("не удается восстановить размеры окна");

165:    }

166:

167:    /* не делать этого снова до поступления следующего SIGWINCH */

168:    propagate_sigwinch = 0;

169:

170:    /* опрос мог быть прерван SIGWINCH,

171:     * потому повторить попытку. */

172:    continue;

173:   }

174:

175:   if (ufds[1].revents & POLLIN) {

176:    i = read (master, buf, BUFSIZE);

177:    if (i >= 1) {

178:     write(STDOUT_FILENO, buf, i);

179:    } else {

180:     done = 1;

181:    }

182:   }

183:

184:   if (ufds[0].revents & POLLIN) {

185:    i = read (STDIN_FILENO, buf, BUFSIZE);

186:    if (i >= 1) {

187:     write(master, buf, i);

188:    } else {

189:     done = 1;

190:    }

191:   }

192:  } while (!done);

193:

194:  tcsetattr(STDIN_FILENO, TCSANOW, &ot);

195:  exit(0);

196: }

Вся добавленная сложность ptytest.с по сравнению с forkptytest.с связана с обработкой старого интерфейса. Все это было описано в данной главе, кроме запуска дочернего процесса, который рассматривался в главе 10.

 

Глава 17

Работа в сети с помощью сокетов

 

По мере того, как компьютерный мир все шире объединяется в единую сеть, важность сетевых приложений все больше и больше возрастает. Система Linux предлагает программный интерфейс сокетов Беркли (Беркли), который уже стал стандартным сетевым API. Мы рассмотрим основы использования сокетов Беркли и через сетевой протокол TCP/IP, и через простое межпроцессное взаимодействие (interprocess communication — IPC) с помощью сокетов домена Unix.

Мы не планировали превратить данную главу в полное руководство по программированию для сетей. Это отдельная сложная тема, и для тех программистов, которые планируют серьезную работу с сокетами, мы рекомендуем специализированные книги по программированию для сетей, например, [33]. Этой главы, однако, будет достаточно для того, чтобы вы смогли создавать несложные сетевые приложения.

 

17.1. Поддержка протоколов

 

API-интерфейс сокетов Беркли был сконструирован в виде шлюза для нескольких протоколов. Хотя это и приводит к дополнительным сложностям в интерфейсе, это все- таки гораздо легче, чем создавать (или изучать) новый интерфейс для каждого нового протокола, который встречается в работе. В Linux используется интерфейс сокетов для многих протоколов, включая TCP/IP (версии 4 и 6), AppleTalk и IPX.

Мы обсудим применение сокетов для двух протоколов, доступных через реализацию сокетов Linux. Наиболее важным протоколом, поддерживаемым системой Linux, является TCP/IP (Transmission Control Protocol/Internet Protocol — протокол управления передачей/протокол Internet), поскольку именно он управляет всем Internet. Мы также обратим внимание на сокеты домена Unix — механизм IPC, ограниченный одним компьютером. Хотя они и не работают через сеть, сокеты домена Unix широко применяются для приложений, работающих на одном компьютере.

Протоколы, как правило, используются группами, или семействами протоколов. Общераспространенное семейство протоколов TCP/IP среди прочих включает в себя протоколы TCP и UDP (User Datagram Protocol — протокол передачи дейтаграмм пользователя). Для того чтобы хорошо ориентироваться в различных протоколах, потребуется овладеть некоторой терминологией.

 

17.1.1. Идеальная работа в сети

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

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

 

17.1.2. Реальная работа в сети

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

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

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

 

17.1.3. Как заставить реальность играть по точным правилам?

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

• Протоколы на основе логических соединений имеют две конечные точки подобно телефонным разговорам. Соединение должно быть установлено до начала передачи информации (ведь вы отвечаете на звонок словом "Алло!", а не начинаете сразу же разговаривать). Остальные пользователи не могут (и даже не должны иметь возможности) вторгаться в соединение. Протоколы, не имеющие таких характеристик, называются протоколами без установления соединения.

• Говорят, что протоколы обеспечивают упорядочение, если они гарантируют доставку данных в том же порядке, в котором они были отправлены.

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

• Потоковые протоколы распознают только байтовые границы. Последовательности байтов могут разделяться и доставляться адресату по мере появления данных.

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

Хотя каждый из перечисленных атрибутов не зависит от остальных, в приложениях употребляются только два основных типа протоколов. Дейтаграммные протоколы являются механизмами пакетной передачи, не предоставляя при этом ни упорядочения, ни защиты от ошибок. Широко используется дейтаграммный протокол UDP, являющийся представителем семейства протоколов TCP/IP. Потоковые протоколы (такие как TCP из TCP/IP) — это протоколы потоковой передачи, которые обеспечивают и упорядочение, и защиту от ошибок.

Несмотря на то что дейтаграммные протоколы вроде UDP, несомненно, полезны, мы остановимся на применении потоковых протоколов, поскольку их легче использовать для большинства приложений. Подробное описание разработки протоколов и различий между их отдельными видами можно найти во многих книгах, например, [33] и [34].

 

17.1.4. Адреса

Поскольку каждый протокол поддерживает собственное определение сетевого адреса, интерфейс сокетов должен абстрагировать адреса. В качестве базовой формы адреса используется структура struct sockaddr; его содержимое устанавливается по-разному для каждого семейства адресов. Передавая struct sockaddr в системный вызов, процесс также указывает размер передаваемого адреса. Тип socklen_t определяется как число, достаточно большое для хранения размера любого сокета, который используется системой.

Все типы struct sockaddr соответствуют приведенному ниже определению.

#include

struct sockaddr {

 unsigned short sa_family;

 char sa_data[MAXSOCKADDRDATA];

}

Первые два байта (размер short) указывают семейство адресов, к которому относится данный адрес. Перечень стандартных адресных семейств, используемых приложениями Linux, приведен в табл. 17.1.

Таблица 17.1. Семейства протоколов и адресов

Адрес Протокол Описание протокола
AF_UNIX PF_UNIX Домен Unix.
AF_INET PF_INET TCP/IP (версия 4).
AF_INET6 PF_INET6 TCP/IP (версия 6).
AF_AX25 PF_AX25 AX.25, используется радиолюбителями.
AF_IPX PF_IPX Novell IPX.
AF_APPLETALK PF_APPLETALK AppleTalk DDS.
AF_NETROM PF_NETROM NetROM, используется радиолюбителями.

 

17.2. Служебные функции

Во всех примерах этого раздела используются две функции: copyData() и die(). Функция copyData() считывает данные из одного файлового дескриптора и записывает их в какой-то другой дескриптор (до тех пор, пока имеются данные для чтения). Функция die() вызывает perror() и завершает программу. Мы ввели обе указанные функции в файл sockutil.с для того, чтобы сделать обучающие программы немного проще. Для справки ниже показана реализация двух данных функций.

 1: /* sockutil.с */

 2:

 3: #include

 4: #include

 5: #include

 6:

 7: #include "sockutil.h"

 8:

 9: /* выдает сообщение об ошибке через функцию perror() и прекращает работу программы */

10: void die(char * message) {

11:  perror(message);

12:  exit(1);

13: }

14:

15: /* Копирует данные из дескриптора файла 'from' в дескриптор файла

16:  'to' до полного завершения копирования. Выходит из программы, если

17:  происходит ошибка. Предполагается, что для обоих файлов установлено

18:  блокирующее чтение и запись. */

19: void copyData(int from, int to) {

20:  char buf[1024];

21:  int amount;

22:

23;  while ((amount = read(from, buf, sizeof(buf))) > 0) {

24:   if (write(to, buf, amount) != amount) {

25:    die("write");

26:    return;

27:   }

28:  }

29:  if (amount < 0)

30:   die("read");

31: }

 

17.3. Основные действия с сокетами

 

Подобно большинству остальных ресурсов Linux сокеты реализуются через файловую абстракцию. Они создаются при помощи системного вызова socket(), который возвращает файловый дескриптор. После соответствующей инициализации сокета данный дескриптор может использоваться для запросов read() и write(), как и любой другой файловый дескриптор. Когда процесс завершает работу с сокетом, его необходимо закрыть через функцию close() для того, чтобы освободить все ресурсы, ассоциированные с ним.

В настоящем разделе представлены основные системные вызовы для создания и инициализации сокетов для любого протокола. Для того чтобы не зависеть от протоколов, информация в некоторой степени абстрагирована, по этой же причине мы не приводим примеры. Следующие два раздела посвящены применению сокетов в двух различных протоколах (домен Unix и TCP/IP). Здесь вы найдете подробные примеры использования большинства системных вызовов, описанных ниже.

 

17.3.1. Создание сокета

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

#include

int socket(int domain, int type, int protocol);

Подобно open(), функция socket() возвращает значение меньше 0, если имела место ошибка, и файловый дескриптор, больший или равный нулю, если все прошло благополучно. Три параметра устанавливают протокол, который нужно использовать.

Первый параметр указывает семейство протоколов и, как правило, принимает одно из значений, перечисленных в табл. 17.1.

Следующий параметр type может иметь одно из значений: SOCK_STREAM, SOCK_DGRAM или SOCK_RAW. Здесь SOCK_STREAM указывает потоковый протокол из данного семейства, a SOCK_DGRAM специфицирует дейтаграммный протокол из того же семейства. Параметр SOCK_RAW предоставляет возможность передавать пакеты прямо в драйвер сетевого устройства, что позволяет пользовательским приложениям поддерживать сетевые протоколы, которые не воспринимаются ядром.

Последний параметр устанавливает протокол для использования с учетом всех ограничений, введенных первыми двумя параметрами. Как правило, значение этого параметра равно 0, что позволяет ядру использовать стандартный протокол установленного типа из указанного семейства. В табл. 17.2 перечислены некоторые допустимые протоколы для семейства PF_INET. Стандартными протоколами здесь считаются IPPROTO_TCP (потоковый) и IPPROTO_UDP (дейтаграммный).

Таблица 17.2. Протоколы IP

Протокол Описание
IPPROTO_ICMP Internet Control Message Protocol (протокол управляющих сообщений в сети Internet) для IPv4.
IPPROTO_ICMPV6 Internet Control Message Protocol (протокол управляющих сообщений в сети Internet) для IPv6.
IPPROTO_IPIP Тоннели IPIP
IPPROTO_IPV6 Заголовки IPv6.
IPPROTO_RAW Пакеты Raw IP.
IPPROTO_TCP Transmission Control Protocol (TCP) (протокол управления передачей).
IPPROTO_UDP User Datagram Protocol (UDP) (протокол передачи дейтаграмм пользователя).

 

17.3.2. Установка соединений

После создания потокового сокета его необходимо присоединить к чему-то часто используемому. Установка соединений сокетов является в большой степени несимметричной задачей, поскольку каждая сторона проводит соединение по-разному. Одна сторона получает сокет, который готов к соединению, и затем ожидает кого-либо для того, чтобы присоединиться к нему. Эту функцию, как правило, выполняют серверные приложения, которые однажды активизируются и постоянно продолжают работать, ожидая подключения со стороны других процессов.

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

 

17.3.3. Связывание адреса с сокетом

И серверный, и клиентский процессы должны сообщить системе, какой адрес использовать для сокета. Прикрепление адреса к локальной стороне сокета называется связыванием сокета и выполняется через системный вызов bind().

#include

int bind(int sock, struct sockaddr * my_addr, socklen_t addrlen);

Первый параметр — это связываемый сокет, остальные параметры задают адрес для локальной конечной точки.

 

17.3.4. Ожидание соединений

После создания сокета сервер привязывает к нему адрес с помощью функции bind(). Далее процесс сообщает системе путем вызова функции listen(), что он готов разрешить другим процессам соединение с данным сокетом (по указанному адресу). Если сокет привязан к адресу, ядро получает возможность обрабатывать попытки соединения с данным адресом, поступающие от процессов. Однако соединение не устанавливается немедленно. Слушающий процесс сначала должен согласиться с попыткой соединения через системный вызов accept(). До тех пор, пока новая попытка соединения с определенным адресом не принята, она называется ожидающим соединением.

Как правило, функция accept() блокируется до тех пор, пока к ней не пытается присоединиться некоторый клиентский процесс. Если сокет был помечен как неблокируемый через fcntl(), то функция accept() возвращает значение EAGAIN в том случае, если нет ни одного доступного клиентского процесса. Системные вызовы select(), poll() и epoll могут использоваться для указания, ждать ли соединению обработки (эти вызовы помечают сокет как готовый для считывания).

Ниже показаны прототипы listen() и accept().

#include

int listen(int sock, int backlog);

int accept(int sock, struct sockaddr * addr, socklen_t * addrlen);

В обеих функциях предполагается, что первый параметр — это файловый дескриптор. Второй параметр backlog функции listen() задает максимальное количество соединений, которые могут одновременно ожидать обработки на данном сокете. Сетевые соединения не устанавливаются до тех пор, пока сервер не примет соединение через accept(); все входящие соединения считаются приостановленными. Поддерживая небольшое количество ожидающих соединений в очереди, ядро тем самым освобождает серверные процессы от необходимости быть в постоянной готовности принимать соединения. Исторически принято ограничивать в приложениях количество невыполненных заданий пятью, хотя иногда необходимо большее количество. Функция listen() возвращает ноль в случае успеха и какое-то другое число в случае неудачи.

Вызов accept() превращает отложенное соединение в установленное. Установленное соединение получает новый файловый дескриптор, который возвращает функция accept(). Новый дескриптор наследует все атрибуты того сокета, к которому обращалась функция listen(). Необычное свойство accept() состоит в том, что она возвращает сетевые ошибки, ожидающие обработки, как ошибки принятия от accept(). При возврате ошибки серверы не должны прерывать работу, если параметр errno принимает одно из следующих значений: ECONNABORTED, ENETDOWN, EPROTO, ENOPROTOOPT, EHOSTDOWN, ENONET, EHOSTUNREACH, EOPNOTSUPP или ENETUNREACH. Все эти ошибки необходимо игнорировать, просто вызвав функцию accept() на сервере еще раз.

Параметры addr и addrlen указывают данные, в которых ядро размещает адрес удаленного (клиентского) конца соединения. В исходном состоянии addrlen представляет собой целое число, содержащее размер буфера, на который ссылается addr. Функция accept() аналогично open() возвращает файловый дескриптор или некоторое отрицательное значение, если возникла ошибка.

 

17.3.5. Подключение к серверу

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

После этапа связывания (который, впрочем, может быть пропущен) клиент соединяется с сервером через системный вызов connect().

#include

int connect(int sock, struct sockaddr * servaddr, socklen_t addrlen);

Процесс переходит к подключению, придерживаясь адреса, с которым должен соединиться сокет.

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

Рис 17.1. Установка соединений сокетов

 

17.3.6. Поиск адресов соединения

После того как соединение установлено, приложение может найти адреса как удаленного, так и локального концов сокета с помощью функций getpeername() и getsockname().

#include

int getpeername(int s, struct sockaddr * addr, socklen_t * addrlen);

int getsockname(int s, struct sockaddr * addr, socklen_t * addrlen);

Обе функции передают адреса соединений сокета s в те структуры, на которые указывают их параметры addr. Адрес удаленной стороны возвращается функцией getpeername(), тогда как getsockname() сообщает адрес локальной части соединения. Для обеих функций в качестве первоначального целочисленного значения, на которое указывает параметр addrlen, должен быть установлен размер пространства, которое выделяется параметром addr. Это целое число заменяется количеством байт в возвращаемом адресе.

 

17.4. Сокеты домена Unix

 

Сокеты домена Unix — это простейшее семейство протоколов, доступное через API- интерфейс сокетов. Они фактически не являются сетевыми протоколами, поскольку могут соединяться с сокетами только на одном и том же компьютере. Несмотря на то что это значительно ограничивает их полезность, они все же используются многими приложениями благодаря гибкому механизму IPC, который они поддерживают. Их адреса — это путевые имена, которые создаются в файловой системе, когда сокет привязывается к путевому имени. Файлы сокетов, представляющие адреса доменов Unix, могут быть запущены функцией stat(), но не могут быть открыты с помощью open(); вместо этого нужно использовать API сокетов.

Домен Unix предусматривает как дейтаграммные, так и потоковые интерфейсы. Дейтаграммный интерфейс используется редко, и здесь обсуждаться не будет. Мы рассмотрим потоковый интерфейс, работа которого подобна именованным каналам. При этом сокеты домена Unix, однако, не идентичны именованным каналам.

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

Сокеты домена Unix работают на основе соединений; в результате каждого соединения с сокетом возникает новый канал связи. Сервер, который может обрабатывать множество соединений одновременно, сохраняет для каждого из них свой файловый дескриптор. Благодаря этому свойству сокеты домена Unix лучше подходят для выполнения многих задач IPC, чем именованные каналы. Это главная причина, по которой они применяются большинством стандартных служб Linux, включал X Window System и системный регистратор.

 

17.4.1. Адреса домена Unix

Адреса для сокетов домена Unix являются путевыми именами в файловой системе. Если файл еще не существует, то он создается как файл сокетного типа в тот момент, когда сокет привязывается к путевому имени через функцию bind(). Если уже существует файл (или даже сокет) с указанным путевым именем, то функция bind() завершается и возвращает значение EADDRINUSE, bind() устанавливает права доступа для созданного файла сокета равными 0666, как измененные текущей маской umask.

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

Адреса сокетов домена Unix передаются через структуру struct sockaddr_un.

#include

#include

struct sockaddr_un {

 unsigned short sun_family;    /* AF_UNIX */

 char sun_path[UNIX_PATH_MAX]; /* путевое имя */

};

В ядре Linux 2.6.7 значение переменной UNIX_PATH_MAX равно 108, но в последующих версиях ядра Linux оно может измениться.

Первый член sun_family должен содержать AF_UNIX для того, чтобы показать, что структура содержит адрес домена Unix. Параметр sun_path хранит путевое имя, которое нужно использовать для соединения. Если системным вызовам, относящимся к сокету, передается размер адреса, то передаваемая длина равна количеству символов в путевом имени плюс размер элемента sun_family. Параметр sun_path не обязательно должен заканчиваться '\0', хотя обычно делают именно так.

 

17.4.2. Ожидание соединения

Как объяснялось выше, ожидание установки соединения на сокете домена Unix придерживается следующей процедуры: создание сокета, привязка адреса к сокету, перевод системы в режим ожидания соединений и принятие соединения.

Ниже показан пример простого сервера, который многократно принимает соединения с сокетом домена Unix (файл sample-socket в текущем каталоге) и считывает все данные из сокета, посылая их на стандартный вывод.

 1: /* userver.c */

 2:

 3: /* Ожидает соединения на сокете ./sample-socket домена Unix.

 4:    После установки соединения копирует данные

 5:    из сокета в stdout до тех пор, пока вторая сторона не

 6:    закрывает соединение. Далее ожидает следующее соединение

 7:    с сокетом. */

 8:

 9: #include

10: #include

11: #include

12: #include

13:

14: #include "sockutil.h" /* некоторые служебные функции */

15:

16: int main (void) {

17:  struct sockaddr_un address;

18:  int sock, conn;

19:  size_t addrLength;

20:

21:  if ((sock = socket(PF_UNIX, SOCK_STREAM, 0)) < 0)

22:   die("socket");

23:

24:  /* Удалить все сокеты (или файлы), существовавшие ранее */

25:  unlink("./sample-socket");

26:

27:  address.sun_family = AF_UNIX; /* сокет домена Unix */

28:  strcpy(address.sun_path, "./sample-socket");

29:

30:  /* Общая длина адреса, включая элемент

31:     sun_family */

32:  addrLength = sizeof(address.sun_family) +

33:  strlen(address.sun_path);

34:

35:  if (bind(sock, (struct sockaddr *) &address, addrLength))

36:   die("bind");

37:

38:  if (listen(sock, 5))

39:   die("listen");

40:

41:  while ((conn = accept(sock, (struct sockaddr *) &address,

42:   &addrLength)) >=0) {

43:   printf("---- получение данных\n");

44:   copyData(conn, 1);

45:   printf("---- готово\n");

46:   close(conn);

47:  }

48:

49:  if (conn < 0)

50:   die("accept");

51:

52:  close(sock);

53:  return 0;

54: }

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

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

Серверный код приводит тип указателя struct sockaddr_un, передаваемого и в bind(), и в accept(), к (struct sockaddr *). При прототипировании различных системных вызовов, относящихся к сокетам, предполагается, что они принимают указатель на struct sockaddr. Приведение типа предотвращает появление уведомлений от компилятора о несоответствии типов указателей.

 

17.4.3. Соединение с сервером

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

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

 1: /* uclient.c */

 2:

 3: /* Подключиться к сокету ./sample-socket домена Unix, скопировать stdin

 4:    в сокет, после этого выйти из программы. */

 5:

 6: #include

 7: #include

 8: #include

 9:

10: #include "sockutil.h" /* некоторые служебные функции */

11:

12: int main(void) {

13:  struct sockaddr_un address;

14:  int sock;

15:  size_t addrLength;

16:

17:  if ((sock = socket(PF_UNIX, SOCK_STREAM, 0)) < 0)

18:   die("socket");

19:

20:  address.sun_family = AF_UNIX; /* сокет домена Unix */

21:  strcpy(address.sun_path, "./sample-socket");

22:

23:  /* Общая длина адреса, включая элемент

24:     sun_family */

25:  addrLength = sizeof(address.sun_family) +

26:   strlen(address.sun_path);

27:

28:  if (connect(sock, (struct sockaddr *) &address, addrLength))

29:   die("connect");

30:

31:  copyData(0, sock);

32:

33:  close(sock);

34:

35:  return 0;

36: }

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

 

17.4.4. Запуск примеров домена Unix

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

 

17.4.5. Неименованные сокеты домена Unix

Благодаря тому, что сокеты домена Unix обладают некоторыми преимуществами перед каналами (например, они являются полнодуплексными), они часто используются в качестве механизма IPC. Для того чтобы облегчить этот процесс, вводится системный вызов socketpair().

#include

int socketpair(int domain, int type, int protocol, int sockfds[2]);

Первые три параметра совпадают с теми, которые передаются в socket(). Последний параметр sockfds() заполняется функцией socketpair() двумя файловыми дескрипторами (по одному для каждой стороны сокета).

Пример применения socketpair() показан далее в главе.

 

17.4.6. Передача файловых дескрипторов

Сокеты домена Unix обладают уникальным свойством: через них могут передаваться файловые дескрипторы. Ни один из прочих механизмов IPC не поддерживает подобную возможность. Она позволяет процессу открыть файл и передать файловый дескриптор в другой (возможно, несвязанный) процесс. Все проверки доступа выполняются при открытии файла, поэтому получающий процесс приобретает те же самые права доступа к файлу, что и исходный процесс.

Файловые дескрипторы передаются как часть более сложного сообщения, которое отправляется с помощью системного вызова sendmsg() и принимается через recvmsg().

#include

int sendmsg(int fd, const struct msghdr * msg, unsigned int flags);

int recvmsg(int fd, struct msghdr * msg, unsigned int flags);

Параметр fd является файловым дескриптором, через который передается сообщение; второй параметр служит указателем на структуру, описывающую сообщение. Параметр flags обычно не используется и для большинства приложений должен быть равен нулю. В специализированных книгах по программированию для сетей обсуждаются доступные флаги [33].

Сообщение описывается показанной ниже структурой.

#include

#include

struct msghdr {

 void * msg_name;            /* дополнительный адрес */

 unsigned int msg_namelen;   /* размер msg_name */

 struct iovec * msg_iov;     /* массив для чтения вразброс/сборной записи*/

 unsigned int msg_iovlen;    /* количество элементов в msg_iov */

 void * msg_control;         /* вспомогательные данные */

 unsigned int msg_controllen;/* длина буфера вспомогательных данных */

 int msg_flags;              /* флаги на получаемом сообщении */

};

Первые два члена msg_name и msg_namelen не используются в потоковых протоколах. Приложения, посылающие сообщения через потоковые сокеты, должны устанавливать для msg_name значение NULL, для msg_namelen — ноль.

msg_iov и msg_iovlen описывают набор буферов, которые отправляют или принимают. Чтение вразброс и сборная запись, а также struct iovec, обсуждаются в конце главы 13. Последний член структуры msg_flags в настоящее время не используется и должен равняться нулю.

Два элемента, которые мы пропустили, msg_control и msg_controllen предоставляют возможность передачи файлового дескриптора. Член msg_control указывает на массив заголовков управляющих сообщений; msg_controllen устанавливает количество байт, которые содержит массив. Каждое управляющее сообщение состоит из структуры struct cmsghdr, которая сопровождается дополнительными данными.

#include

struct cmsghdr {

 unsigned int cmsg_len; /* длина управляющего сообщения */

 int cmsg_level;        /* SOL_SOCKET */

 int cmsg_type;         /* SCM_RIGHTS */

 int cmsg_data[0];      /* здесь должен быть файловый дескриптор */

};

Размер управляющего сообщения, включая заголовок, хранится в переменной cmsg_len. В текущий момент определен только один тип управляющих сообщений — SCM_RIGHTS, который передает файловые дескрипторы. Для данного типа сообщений параметры cmsg_level и cmsg_type должны быть равны соответственно SOL_SOCKET и SCM_RIGHTS. Последний член cmsg_data является массивом нулевого размера. Это расширение gcc, которое позволяет приложению копировать данные в конец структуры (в следующей программе показан пример).

Получение файлового дескриптора происходит аналогично. Необходимо выделить достаточное пространство буфера для управляющего сообщения, и каждая приходящая структура struct cmsghdr будет сопровождаться новым файловым дескриптором.

Для иллюстрации использования таких вложенных структур мы написали пример программы, которая по нашей прихоти названа просто cat. Она принимает имя файла в качестве единственного аргумента, открывает указанный файл в дочернем процессе и передает результирующий файловый дескриптор в родительский процесс через сокет домена Unix. Родительский процесс затем копирует файл на стандартный вывод. Имя файла посылается вместе с файловым дескриптором с демонстрационной целью.

  1: /* passfd.с */

  2:

  3: /* Программа ведет себя подобно обычной команде /bin/cat, которая обрабатывает

  4:    только один аргумент (имя файла). Мы создаем сокеты домена Unix при помощи

  5:    socketpair(), затем разветвляем через fork(). Дочерний процесс открывает файл,

  6:    имя которого передается в командной строке, пересылает файловый дескриптор и

  7:    имя файла обратно в порождающий процесс, после этого завершается. Родительский

  8:    процесс ожидает файловый дескриптор от дочернего процесса, а потом копирует

  9:    данные из файлового дескриптора в stdout до тех пор, пока данные не

 10:    заканчиваются. Затем родительский процесс завершается. */

 11:

 12: #include

 13: #include

 14: #include

 15: #include

 16: #include

 17: #include

 18: #include

 19: #include

 20: #include

 21:

 22: #include "sockutil.h" /* простые служебные функции */

 23:

 24: /* Дочерний процесс. Он пересылает файловый дескриптор. */

 25: int childProcess(char * filename, int sock) {

 26:  int fd;

 27:  struct iovec vector;   /* некоторые данные для передачи fd в w/ */

 28:  struct msghdr msg;     /* полное сообщение */

 29:  struct cmsghdr * cmsg; /* управляющее сообщение, которое */

 30:                         /* включает в себя fd */

 31:

 32:  /* Открыть файл, дескриптор которого будет передан. */

 33:  if ((fd = open(filename, O_RDONLY)) < 0) {

 34:   perror("open");

 35:   return 1;

 36:  }

 37:

 38:  /* Передать имя файла через сокет, включая завершающий

 39:     символ '\0' */

 40:  vector.iov_base = filename;

 41:  vector.iov_len = strlen(filename) + 1;

 42:

 43:  /* Соединить первую часть сообщения. Включить

 44:     имя файла iovec */

 45:  msg.msg_name = NULL;

 46:  msg.msg_namelen = 0;

 47:  msg.msg_iov = &vector;

 48:  msg.msg_iovlen = 1;

 49:

 50:  /* Теперь управляющее сообщение. Мы должны выделить участок памяти

 51:     для файлового дескриптора. */

 52:  cmsg = alloca(sizeof(struct cmsghdr) + sizeof(fd));

 53:  cmsg->cmsg_len = sizeof(struct cmsghdr) + sizeof(fd);

 54:  cmsg->cmsg_level = SOL_SOCKET;

 55:  cmsg->cmsg_type = SCM_RIGHTS;

 56:

 57:  /* Копировать файловый дескриптор в конец

 58:     управляющего сообщения */

 59:  memcpy(CMSG_DATA(cmsg), &fd, sizeof(fd));

 60:

 61:  msg.msg_control = cmsg;

 62:  msg.msg_controllen = cmsg->cmsg_len;

 63:

 64:  if (sendmsg(sock, &msg, 0) != vector.iov_len)

 65:   die("sendmsg");

 66:

 67:  return 0;

 68: }

 69:

 70: /* Родительский процесс. Он получает файловый дескриптор. */

 71: int parentProcess(int sock) {

 72:  char buf[80];          /* пространство для передачи имени файла */

 73:  struct iovec vector;   /* имя файла от дочернего процесса */

 74:  struct msghdr msg;     /* полное сообщение */

 75:  struct cmsghdr * cmsg; /* управляющее сообщение с fd */

 76:  int fd;

 77:

 78:  /* установка iovec для имени файла */

 79:  vector.iov_base = buf;

 80:  vector.iov_len = 80;

 81:

 82:  /* сообщение, которое мы хотим получить */

 83:

 84:  msg.msg_name = NULL;

 85:  msg.msg_namelen = 0;

 86:  msg.msg_iov = &vector;

 87:  msg.msg_iovlen = 1;

 88:

 89:  /* динамическое распределение (чтобы мы могли выделить участок

 90:     памяти для файлового дескриптора) */

 91:  cmsg = alloca(sizeof(struct cmsghdr) + sizeof(fd));

 92:  cmsg->cmsg_len = sizeof(struct cmsghdr) + sizeof(fd);

 93:  msg.msg_control = cmsg;

 94:  msg.msg_controllen = cmsg->cmsg_len;

 95:

 96:  if (!recvmsg(sock, &msg, 0))

 97:   return 1;

 98:

 99:  printf("получен файловый дескриптор для '%s'\n",

100:   (char *) vector.iov_base);

101:

102:  /* присвоение файлового дескриптора из управляющей структуры */

103:  memcpy(&fd, CMSG_DATA(cmsg), sizeof(fd));

104:

105:  copyData(fd, 1);

106:

107:  return 0;

108: }

109:

110: int main(int argc, char ** argv) {

111:  int socks[2];

112:  int status;

113:

114:  if (argc != 2) {

115:   fprintf(stderr, "поддерживается только один аргумент\n");

116:   return 1;

117:  }

118:

119:  /* Создание сокетов. Один служит для родительского процесса,

120:     второй — для дочернего (мы можем поменять их местами,

121:     если нужно). */

122:  if (socketpair(PF_UNIX, SOCK_STREAM, 0, socks))

123:   die("socketpair");

124:

125:  if (!fork()) {

126:   /* дочерний процесс */

127:   close(socks[0]);

128:   return childProcess(argv[1], socks[1]);

129:  }

130:

131:  /* родительский процесс */

132:  close(socks[1]);

133:  parentProcess(socks[0]);

134:

135:  /* закрытие дочернего процесса */

136:  wait(&status);

137:

138:  if (WEXITSTATUS(status))

139:   fprintf(stderr, "childfailed\n");

140:

141:  return 0;

142: }

 

17.5. Сетевая обработка с помощью TCP/IP

 

Самое важное применение сокетов заключается в том, что они позволяют приложениям, работающим на основе различных механизмов, общаться друг с другом. Семейство протоколов TCP/IP [34] используется в Internet самым большим в мире числом компьютеров, объединенных в сеть. Система Linux предлагает полную устойчивую реализацию TCP/IP, которая позволяет действовать и как сервер, и как клиент TCP/IP.

Наиболее распространенной версией TCP/IP является версия 4 (IPv4). В данный момент для большинства операционных систем и продуктов сетевой инфраструктуры уже доступна версия 6 протокола TCP/IP (IPv6), однако IPv4 доминирует до сих пор. В данном разделе мы сосредоточимся на создании приложений для IPv4, но обратим внимание на отличия для приложений IPv6, а также для тех программ, которые должны поддерживать обе версии.

 

17.5.1. Упорядочение байтов

Сети TCP/IP, как правило, являются неоднородными; они включают в себя широкий ряд механизмов и архитектур. Одно из основных отличий между архитектурами связано со способом хранения чисел.

Машинные числа составляются из последовательности байтов. Например, целые числа в С обычно представляются 4 байтами (32 битами). Существует довольно много способов хранения этих четырех байтов в памяти компьютера. Архитектуры с обратным порядком байтов сохраняют старший (наиболее значимый) байт в наименьшем аппаратном адресе, остальные следуют в порядке от более значимого к менее значимому. Механизмы с прямым порядком байтов хранят многобайтовые значения в абсолютно противоположном порядке: наименее значимый байт отправляется в наименьший адрес памяти. В других механизмах байты сохраняются в различных порядках.

Так как многобайтовые значения являются частью протокола TCP/IP, то разработчики протоколов позаботились о едином стандарте способа передачи многобайтовых значений через сеть. TCP/IP требует использования обратного порядка байтов для передачи протокольной информации и рекомендует также применять его к данным приложений (хотя попытки зафиксировать формат потока данных приложений не предпринимались). Упорядочение, которое применяется для многобайтовых значений, передаваемых через сеть, известно как сетевой порядок байтов.

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

#include

unsigned int htonl(unsigned int hostlong);

unsigned short htons(unsigned short hostshort);

unsigned int ntohl(unsigned int netlong);

unsigned short ntohs(unsigned short netshort);

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

Первые две функции htonl() и htons() преобразуют длинные и короткие числа соответственно из порядка байтов хоста в сетевой порядок байтов. Последние две ntohl() и ntohs() выполняют обратные преобразования длинных и коротких чисел (из сетевого порядка в порядок хоста).

Хотя мы использовали термин длинный в описаниях, на самом деле, это неправильно. Обе функции htonl() и ntohl() принимают 32-битные значения, а не те, которые относятся к типу long. В прототипах обеих функций предполагалось, что они обрабатывают значения int, поскольку все платформы Linux в настоящее время используют 32-битные целые числа.

 

17.5.2. Адресация IPv4

Соединения IPv4 представляют собой кортеж из 4-х элементов (локальный хост, локальный порт, удаленный хост, удаленный порт). До установки соединения необходимо определить каждую его часть. Элементы локальный хост и удаленный хост являются IPv4-адресами. IPv4-адреса — это 32-битные (4-байтовые) числа, уникальные для всей установленной сети. Как правило, они записываются в виде aaa.bbb.ccc.ddd, где каждый элемент адреса является десятичным представлением одного из байтов адреса машины. Первое слева число в адресе соответствует самому значимому байту в адресе. Такой формат для IPv4-адресов известен как десятичное представление с разделителями-точками.

В связи с тем, что большинство компьютеров вынуждено поддерживать работу нескольких параллельных TCP/IP приложений, IP-номер не обеспечивает уникальную идентификацию для соединения на одной машине. Номера портов — это 16-битные числа, которые позволяют однозначно распознавать одну из сторон соединения на данном хосте. Объединение IPv4-адреса и номера порта обеспечивает идентификацию стороны соединения где-либо в пределах одной сети TCP/IP (например, Internet является единой TCP/IP сетью). Две конечные точки соединения образуют полное TCP-соединение, таким образом, две пары, состоящие из IP-номера и номера порта, однозначно определяют TCP/IP соединение в сети.

Распределение номеров портов для различных протоколов производится на основе раздела стандартов Internet, известного как официальные номера портов, который утверждается Агентством по выделению имен и уникальных параметров протоколов Internet (Internet Assigned Numbers Authority, LANA). Общие протоколы Internet, такие как ftp, telnet и http, имеют свои номера портов. Большинство серверов предусматривают данные службы на присвоенных номерах, что позволяет их легко найти. Некоторые сервера запускаются на альтернативных номерах портов, как правило, для поддержки нескольких служб на одной машине. Поскольку официальные номера портов не изменяются, система Linux просто находит соответствие между именами протоколов (обычно называемых службами) и номерами портов с помощью файла /etc/services.

Все номера портов попадают в диапазон от 0 до 65 535; в системе Linux они разделяются на два класса. Зарезервированные порты с номерами от 0 до 1 024 могут использоваться только процессами, работающими как root. Это позволяет клиентским программам иметь гарантию того, что программа, запущенная на сервере, не является троянским конем, активизированным каким-то пользователем.

IPv4-адреса хранятся в структуре struct sockaddr_in, которая определяется следующим образом.

#include

#include

struct sockaddr_in {

 short int sin_family;        /* AF_INET */

 unsigned short int sin_port; /* номер порта */

 struct in_addr sin_addr;     /* IP-адрес */

}

Первым членом должен быть AF_INET, указывающий, что это IP-адрес. Следующий член — это номер порта в сетевом порядке байтов. Последний элемент — это IP-номер машины для данного TCP адреса. IP-номер, хранящийся в sin_addr, должен трактоваться как непрозрачный тип и не иметь возможности прямого доступа.

Если хотя бы одна из переменных sin_port или sin_addr заполнена байтами \0 (обычно функцией memset()), то это указывает на условие "пренебречь". Серверные процессы, как правило, не беспокоятся о том, какой IP-адрес используется для локального соединения. Другими словами, они согласны принимать соединения с любым адресом, имеющимся на данной машине. Если в приложении требуется принимать соединения только на одном интерфейсе, то при этом нужно обязательно указать адрес. Такой адрес иногда называется неустановленным, поскольку он не представляет собой полное определение адреса соединения (для него требуется еще IP-адрес).

 

17.5.3. Адресация IPv6

В IPv6 используется тот же самый кортеж (локальный хост, локальный порт, удаленный хост, удаленный порт), что и в IPv4, и одни и те же номера портов (16-битные значения).

IPv6-адреса локального и удаленного хостов являются 128-битными (16-байтовыми) числами вместо 32-битных чисел, которые использовались в IPv4. Применение таких больших адресов обеспечивает протоколы достаточным количеством адресов для будущего развития (можно без проблем предоставить уникальный адрес каждому атому в Млечном Пути). На первый взгляд, это может показаться избыточной тратой ресурсов. Однако сетевые архитектуры имеют склонность небрежно относиться к адресам и растрачивать огромное их число впустую, поэтому разработчики версии IPv6 предпочли перейти к 128-битным адресам сейчас, чем переживать о возможной необходимости изменять адреса в будущем.

Аналогом десятичного представления с разделителями-точками, которое используется в IPv4, для версии IPv6 является представление с разделителями-двоеточиями. Как подсказывает название, двоеточия отделяют каждую пару байтов в адресе (вместо точки, которая отделяет каждый отдельный байт). Из-за большой длины IPv6-адреса записываются в шестнадцатеричной (а не в десятичной) форме, что помогает уменьшить их длину. Ниже показано несколько примеров того, как выглядит IPv6-адрес в представлении с разделителями-двоеточиями.

1080:0:0:0:8:800:200С:417А

FF01:0:0:0:0:0:0:43

0:0:0:0:0:0:0:1

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

1080::8:800:200C:417A

FF01::43

::1

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

Последний метод записи IPv6-адресов заключается в том, что последние 32 бита представляются с разделительными точками, а первые 96 битов — с разделительными двоеточиями. При этом адрес обратной связи IPv6 ::1 будет записан либо как ::0.0.0.1, либо как 0:0:0:0:0:0:0.0.0.1.

IPv6 определяет любой адрес с 96 начальными нулями (за исключением адреса обратной связи и неустановленного адреса) как совместимый IPv4-адрес, который позволяет сетевым маршрутизаторам передавать через сети IPv6 пакеты, предназначенные для IPv4-хостов. Сокращение двоеточий позволяет легко записать IPv4-адрес как IPv6-адрес путем добавления :: перед стандартным десятичным адресом с точками. Такой тип адресов называется IPv4-совместимым IPv6-адресом. Такая адресация применяется только маршрутизаторами; обычные программы не могут воспользоваться ее преимуществами.

Программы, работающие на машинах IPv6 и требующие обращения к машинам IPv4, могут использовать отображенные IPv4-адреса. Они дополняют IPv4-адрес 80-ю нулевыми старшими разрядами и 16-битным значением 0xffff, которое записывается как ::ffff:, а за ним следует десятичный IPv4-адрес с точками. Подобная адресация позволяет большинству программ в системе, поддерживающей только версию IPv6, явно общаться с узлами IPv4.

IPv6-адреса хранятся в переменных типа struct sockaddr_in6.

#include

#include

struct sockaddr_in6 {

 short int sin6_family;        /* AF_INET6 */

 unsigned short int sin6_port; /* номер порта */

 unsigned int sin6_flowinfo;   /* информация о потоке обмена IPv6 */

 struct in6_addr sin6_addr;    /* IP-адрес */

 unsigned int sin6_scope_id;   /* набор граничных интерфейсов */

}

Данная структура подобна struct sockaddr_in; здесь первый член сохраняет семейство адресов (в этом примере AF_INET6), а следующий — 16-битный номер порта в сетевом порядке байтов.

Четвертый член содержит двоичное представление IPv6-адреса, выполняя те же самые функции, что и последний член структуры struct sockaddr_in. Оставшиеся два элемента sin6_flowinfo и sin6_scope_id используются в более сложных задачах и для большинства приложений должны быть равны нулю.

Стандарты ограничивают struct sockaddr_in в точности тремя членами, тогда как struct sockaddr_in6 позволительно иметь дополнительные элементы. По этой причине программы, которые вручную заполняют struct sockaddr_in6, должны обнулить все данные структуры с помощью функции memset().

 

17.5.4. Манипулирование IP-адресами

В приложениях нередко требуется преобразовывать IP-адреса из удобочитаемых для человека представлений (либо десятичное с разделителями-точками, либо с разделителями-двоеточиями) в двоичное представление struct in_addr и наоборот. Функция inet_ntop() принимает двоичный IP-адрес и возвращает указатель на строку, содержащую десятичную форму с точками или двоеточиями.

#include

const char * inet_ntop(int family, const void * address, char * dest,

 int size);

Здесь family — это адресное семейство того адреса, который передается во втором параметре; поддерживаются только AF_INET и AF_INET6. Следующий параметр указывает на struct in_addr или struct in6_addr6 в зависимости от первого параметра. Значение dest представляет массив символов, состоящий из size элементов, в котором хранится адрес, удобочитаемый для человека. Если форматирование адреса прошло успешно, то функция inet_ntop() возвращает dest, в противном случае возвращается NULL. Существуют только две причины, по которым inet_ntop() может не выполнить свою работу: если буфер назначения недостаточно велик для хранения форматированного адреса (переменной errno присваивается значение ENOSPC) или если параметр family задан неверно (errno содержит EAFNOSUPPORT).

INET_ADDRSTRLEN является константой, определяющей наибольший размер dest, необходимый для хранения любого IPv4-адреса. Соответственно, INET6_ADDRSTRLEN определяет максимальный размер массива для IPv6-адреса.

Программа-пример netlookup.с демонстрирует использование inet_ntop(); полная программа представлена далее в этой главе.

120: if (addr->ai_family == PF_INET) {

121:  struct sockaddr_in * inetaddr = (void*)addr->ai_addr;

122:  char nameBuf[INET_ADDRSTRLEN];

123:

124:  if (serviceName)

125:   printf("\tport %d", ntohs(inetaddr->sin_port));

126:

127:  if (hostName)

128:   printf("\thost %s",

129:    inet_ntop(AF_INET, &inetaddr->sin_addr,

130:    nameBuf, sizeof(nameBuf)));

131: } else if (addr->ai_family == PF_INET6) {

132:  struct sockaddr_in6 *inetaddr =

133:   (void *) addr->ai_addr;

134:  char nameBuf[INET6_ADDRSTRLEN];

135:

136:  if (serviceName)

137:   printf("\tport %d", ntohs(inetaddr->sin6_port));

138:

139:  if (hostName)

140:   printf("\thost %s",

141:    inet_ntop(AF_INET6, &inetaddr->sin6_addr,

142:     nameBuf, sizeof(nameBuf)));

143: }

Обратное преобразование строки, содержащей адрес с точками или двоеточиями, в двоичный IP-адрес выполняет функция inet_pton().

#include

int inet_pton(int family, const char * address, void * dest);

Параметр family определяет тип преобразуемого адреса (либо AF_INET, либо AF_INET6), a address указывает на строку, в которой содержится символьное представление адреса. Если используется AF_INET, то десятичная строка с точками преобразуется в двоичный адрес, хранящийся в переменной, на которую указывает параметр dest структуры struct in_addr. Для AF_INET6 строка с двоеточиями преобразуется и сохраняется в переменной, на которую указывает dest структуры struct in6_addr. В отличие от большинства библиотечных функций, inet_pton() возвращает 1, если преобразование прошло успешно, 0, если dest не содержит соответствующий адрес, и -1, если параметр family не совпадает с AF_INET или AF_INET6.

Программа-пример reverselookup, код которой представлен далее в главе, использует функцию inet_pton() для преобразования IPv4- и IPv6-адресов, передаваемых пользователем, в структуры struct sockaddr. Ниже приводится раздел кода, выполняющий преобразования IP-адреса, на который указывает hostAddress. В конце данного кода struct sockaddr * addr указывает на структуру, содержащую преобразованный адрес.

 79: if (!hostAddress) {

 80:  addr4.sin_family = AF_INET;

 81:  addr4.sin_port = portNum;

 82: } else if (! strchr(hostAddress, ':')) {

 83:  /* Если в hostAddress появляется двоеточие, то принимаем версию IPv6.

 84:     В противном случае это IPv4-адрес */

 85:

 86:  if (inet_pton(AF_INET, hostAddress,

 87:      &addr4.sin_addr) <= 0) {

 88:   fprintf(stderr, "ошибка преобразования IPv4-адреса %s\n",

 89:    hostAddress);

 90:   return 1;

 91:  }

 92:

 93:  addr4.sin_family = AF_INET;

 94:  addr4.sin_port = portNum;

 95: } else {

 96:

 97:  memset(&addr6, 0, sizeof(addr6));

 98:

 99:  if (inet_pton(AF_INET6, hostAddress,

100:      &addr6.sin6_addr) <= 0) {

101:   fprintf(stderr, "ошибка преобразования IPv6-адреса %s\n",

102:   hostAddress);

103:   return 1;

104:  }

105:

106:  addr6.sin6_family = AF_INET6;

107:  addr6.sin6_port = portNum;

108:  addr = (struct sockaddr *) &addr6;

109:  addrLen = sizeof(addr6);

110: }

 

17.5.5. Преобразование имен в адреса

Длинные последовательности чисел являются отлично подходящим методом идентификации для компьютеров, позволяющим им однозначно узнавать друг друга. Однако большинство людей охватывает ужас при мысли о том, что придется иметь дело с большим количеством цифр. Для того чтобы разрешить людям применять текстовые названия для компьютеров вместо числовых, в состав протоколов TCP/IP входит распределенная база данных для взаимных преобразований имен хостов и IP-адресов. Эта база данных называется DNS (Domain Name System — служба имен доменов), она подробно рассматривается в [34] и [1].

Служба DNS предлагает много функций, но сейчас нас интересует одна — возможность преобразования IP-адресов в имена хостов и наоборот. Несмотря на то что это преобразование должно выполняться как однозначное соответствие, на самом деле оно представляет собой отношение типа "многие ко многим". Другими словами, каждый IP-адрес может соответствовать нулю или более именам хостов, а каждое имя хоста соответствует нулю или более IP-адресам.

Использование неоднозначного соответствия между именами хостов и IP-адресами может показаться странным. Однако многие Internet-сайты применяют одну и ту же машину для ftp-сайта и Web-сайта. При этом адреса www.some.org и ftp.some.org должны ссылаться на одну и ту же машину, а для одной машины не нужны два IP-адреса. Таким образом, два имени хостов сводятся к одному IP-адресу. Каждый IP-адрес имеет одно первичное, или каноническое имя хоста, которое используется, если IP-адрес требуется преобразовать в единственное имя хоста во время обратного поиска имен.

Наиболее распространенной причиной, по которой одному имени хоста ставится в соответствие несколько IP-адресов, является балансировка нагрузки. Серверы имен (программы, предлагающие преобразование имен хостов в IP-адреса) часто конфигурируются так, что возвращают в разное время разные адреса для одного и того же имени. Это позволяет нескольким физическим машинам поддерживать единую службу.

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

Библиотечная функция getaddrinfo() предлагает программам простой доступ к преобразованиям имен хостов DNS.

#include

#include

#include

int getaddrinfo(const char * hostname, const char * servicename,

 const struct addrinfo * hints, struct addrinfo ** res);

Концепция этой функции достаточно простая, однако весьма мощная, в связи с этим ее описание может показаться несколько запутанным. Идея заключается в том, что функция принимает имя хоста, имя службы (или оба из них) и превращает их в список IP-адресов. Затем с использованием hints список фильтруется и те адреса, которые не нужны приложению, отбрасываются. Окончательный список возвращается в виде связного списка в переменной res.

Искомое имя хоста содержится в первом параметре и может равняться NULL, если производится поиск только службы. Параметр hostname может быть именем (например, www.ladweb.net) или IP-адресом (с точками или двоеточиями в качестве разделителей), который функция getaddrinfo() преобразует в двоичный адрес.

Второй параметр servicename указывает имя той службы, для которой нужно извлечь официальный порт. Если он равен NULL, то поиск службы не выполняется.

Структура struct addrinfo используется как для hints (при фильтрации полного списка адресов), так и для передачи окончательного списка в приложение.

#include

struct addrinfo {

 int ai_flags;

 int ai_family;

 int ai_socktype;

 int ai_protocol;

 socklen_t ai_addrlen;

 struct sockaddr_t * ai_addr;

 char * ai_canonname;

 struct addrinfo * next;

}

Если struct addrinfo используется для параметра hints, то участвуют только первые четыре члена, остальные должны равняться нулю или NULL. Если задано значение ai_family, то getaddrinfo() возвращает адреса только для указанного семейства протоколов (например, PF_INET). Аналогично, если устанавливается ai_socktype, то возвращаются только адреса данного типа сокета.

Член ai_protocol позволяет ограничивать результаты определенным протоколом. Этот параметр нельзя применять, если не установлен параметр ai_family, а также, если числовое значение протокола (такое как IPPROTO_TCP) не является уникальным среди всех протоколов; он хорошо подходит только для PF_INET и PF_INET6.

Последний член, используемый для hints — это aflags, который принимает одно или несколько (объединенных логическим "ИЛИ") из перечисленных ниже значений.

AI_ADDRCONFIG

По умолчанию функция getaddrinfo() возвращает все адреса, соответствующие запросу. Данный флаг указывает на возврат адресов только тех протоколов, чьи адреса сконфигурированы в локальной системе. Другими словами, она возвращает только IPv4-адреса в системах с IPv4-интерфейсами и только IPv6-адреса в системах с интерфейсами IPv6.

AI_CANONNAME

При возврате поле ai_canonname содержит каноническое имя хоста для адреса, указанного в struct addrinfo. Поиск этого адреса сопровождается дополнительными поисками в службе DNS и, как правило, не является необходимым.

AI_NUMERICHOST

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

AI_PASSIVE

Если hostname равен NULL и присутствует этот флаг, то возвращается неустановленный адрес, который позволяет ожидать соединений на всех интерфейсах. Если данный флаг не указан (а значение hostname равно NULL), возвращается адрес обратной связи.

Последний параметр res в getaddrinfo() должен быть адресом указателя на struct addrinfo. Для успешного завершения переменная, на которую указывает res, устанавливается на первую запись в односвязном списке адресов, который соответствует запросу. Член ai_next структуры struct addrinfo указывает на следующий член связного списка, и для последнего узла в списке параметр ai_next равен NULL.

Когда приложение завершает работу с возвращенным связным списком, функция freeaddrinfo() освобождает память, занимаемую списком.

#include

#include

#include

void freeaddrinfo(struct addrinfo * res);

Единственным параметром для freeaddrinfo является указатель на первый узел в списке.

Каждый узел в возвращаемом списке имеет тип struct addrinfo и специфицирует один адрес, соответствующий запросу. Каждый адрес содержит не только IPv4- или IPv6-адрес, он также определяет тип соединения (например, дейтаграмма) и протокол (такой как UDP). Если для одного IP-адреса в запросе подходит несколько типов соединений, то данный адрес включается в несколько узлов.

Каждый узел содержит описанную ниже информацию.

• ai_family — семейство протоколов (PF_INET или PF_INET6), к которому принадлежит адрес.

• ai_socktype — тип соединения для адреса (как правило, принимает одно из значений SOCK_STREAM, SOCK_DGRAM или SOCK_RAW).

• ai_protocol — протокол для адреса (обычно IPPROTO_TCP или IPPROTO_UDP).

• Если в параметре hints был указан флаг AI_CANONNAME, то ai_canonname содержит каноническое имя для адреса.

• ai_addr указывает на struct sockaddr для соответствующего протокола. Например, если ai_family принимает значение PF_INET, то ai_addr указывает на struct sockaddr_in. Член ai_addrlen определяет длину структуры, на которую указывает ai_addr.

• Если предусмотрен параметр servicename, то в качестве номера порта в каждом адресе устанавливается официальный порт данной службы. В противном случае номер порта для каждого адреса равен нулю.

• Если не был передан параметр hostname, то номера портов устанавливаются для каждого адреса, однако в качестве IP-адреса определяется или адрес обратной связи, или неустановленный адрес (как указывалось ранее в описании флага AI_PASSIVE).

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

 1: /* clientlookup.c */

 2:

 3: #include

 4: #include

 5: #include

 6:

 7: int main(int argc, const char ** argv) {

 8:  struct addrinfo hints, * addr;

 9:  const char * host = argv[1], * service = argv[2];

10:  int rc;

11:

12:  if (argc != 3) {

13:   fprintf(stderr, "требуется в точности два аргумента\n");

14:   return 1;

15:  }

16:

17:  memset(&hints, 0, sizeof(hints));

18:

19:  hints.ai_socktype = SOCK_STREAM;

20:  hints.ai_flags = AI_ADDRCONFIG;

21:  if ((rc = getaddrinfo(host, service, &hints, &addr)))

22:   fprintf(stderr, "сбой поиска\n");

23:  else

24:   freeaddrinfo(addr);

25:

26:  return 0;

27: }

Давайте обратим внимание на строки 17–24 этой программы. После очистки структуры hints приложение запрашивает адреса SOCK_STREAM, которые используют протокол, сконфигурированный на локальной системе (путем установки флага AI_ADDRCONFIG). Затем активизируется функция getaddrinfo() с именем хоста, именем службы, подсказками и в случае невозможности найти соответствие отображается сообщение об ошибке. Если все проходит нормально, то первый элемент в связном списке, на который указывает addr, представляет собой соответствующий адрес, который программа может использовать для установки соединения с указанной службой и хостом. Программа не решает, через какой протокол (IPv4 или IPv6) соединение будет лучшим.

Серверные приложения немного проще. В них, как правило, требуется согласиться на соединение с определенным портом, при этом на всех адресах. Если установлены флаги AI_PASSIVE, функция getaddrinfo() возвращает адрес, вынуждающий ядро разрешать все соединения (со всеми адресами, которые оно знает) при условии, что в качестве первого параметра передается NULL. Как и в клиентском примере, используется AI_ADDRCONFIG, дабы убедиться, что возвращаемый адрес соответствует протоколу, который поддерживает данная машина.

 1: /* serverlookup.с */

 2:

 3: #include

 4: #include

 5: #include

 6:

 7: int main(int argc, const char ** argv) {

 8:  struct addrinfo hints, * addr;

 9:  const char * service = argv[1];

10:  int rc;

11:

12:  if (argc != 3) {

13:   fprintf(stderr, "требуется в точности один аргумент\n");

14:   return 1;

15:  }

16:

17:  memset(&hints, 0, sizeof(hints));

18:

19:  hints.ai_socktype = SOCK_STREAM;

20:  hints.ai_flags = AI_ADDRCONFIG | AI_PASSIVE;

21:  if ((rc = getaddrinfo(NULL, service, &hints, &addr)))

22:   fprintf(stderr, "сбой поиска\n");

23:  else

24:   freeaddrinfo(addr);

25:

26:  return 0;

27: }

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

Следующий пример демонстрирует куда более полезную программу. Она предоставляет интерфейс командной строки для большинства возможностей getaddrinfo(). Она дает возможность пользователю указывать имя хоста или имя службы (или оба имени), тип сокета (потоковый или дейтаграммный), семейство адресов, протокол (TCP или UDP). Пользователь может также запрашивать программу отображать каноническое имя или только те адреса для протоколов, для которых сконфигурирована машина (через флаг AI_ADDRCONFIG). Ниже показано, как можно применить программу для извлечения адреса для telnet-соединения с локальной машиной (данная машина сконфигурирована и под IPv4, и под IPv6).

$ ./netlookup --hdst localhost --service telnet

IPv6 stream tcp port 23 host ::1

IPv6 dgram  udp port 23 host ::l

IPv4 stream tcp port 23 host 127.0.0.1

IPv4 dgram  udp port 23 host 127.0.0.1

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

[ewt@patton code]$ ./netlookup --host localhost -service telnet --stream

IPv6 stream tcp port 23 host ::1

IPv4 stream tcp port 23 host 127.0.0.1

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

[ewt@patton code]$ ./netlookup --host localhost --service telnet —stream

IPv4 stream tcp port 23 host 127.0.0.1

Вот так выглядит поиск соответствия для хоста Internet, который имеет и IPv4, и IPv6 конфигурации.

$ ./netlookup --host www.6bone.net —stream

IPv6 stream tcp host 3ffe:b00:c18:1::10

IPv4 stream tcp host 206.123.31.124

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

  1: /* netlookup.с */

  2:

  3: #include

  4: #include

  5: #include

  6: #include

  7: #include

  8: #include

  9:

 10: /* Вызывается, если во время обработки командной строки происходит ошибка;

 11:    отображает короткое сообщение для пользователя и завершается. */

 12: void usage(void) {

 13:  fprintf(stderr, "использование: netlookup [--stream] [--dgram] "

 14:   "[--ipv4] [--ipv6] [--name] [--udp]\n");

 15:  fprintf (stderr, " [--tcp] [--cfg] "

 16:   "[--service <служба>] [--host <имя_хоста>]\n");

 17:  exit(1);

 18: }

 19:

 20: int main(int argc, const char ** argv) {

 21:  struct addrinfo * addr, * result;

 22:  const char ** ptr;

 23:  int rc;

 24:  struct addrinfo hints;

 25:  const char * serviceName = NULL;

 26:  const char * hostName = NULL;

 27:

 28:  /* очищает структуру подсказок */

 29:  memset(&hints, 0, sizeof(hints));

 30:

 31:  /* анализирует аргументы командной строки, игнорируя argv[0]

 32:

 33:     Структура hints, параметры serviceName и hostName будут

 34:     заполнены на основе переданных аргументов. */

 35:  ptr = argv + 1;

 36:  while (*ptr && *ptr[0] == '-') {

 37:   if (!strcmp(*ptr, "--ipv4"))

 38:    hints.ai_family = PF_INET;

 39:   else if (!strcmp(*ptr, "--ipv6"))

 40:    hints.ai_family = PF_INET6;

 41:   else if (!strcmp(*ptr, "--stream"))

 42:    hints.ai_socktype = SOCK_STREAM;

 43:   else if (!strcmp(*ptr, "--dgram"))

 44:    hints.ai_socktype = SOCK_DGRAM;

 45:   else if (!strcmp(*ptr, "--name"))

 46:    hints.ai_flags |= AI_CANONNAME;

 47:   else if (!strcmp(*ptr, "--cfg"))

 48:    hints.ai_flags |= AI_ADDRCONFIG;

 49:   else if (!strcmp(*ptr, "--tcp")) {

 50:    hints.ai_protocol = IPPROTO_TCP;

 51:   } else if (!strcmp(*ptr, "--udp")) {

 52:    hints.ai_protocol = IPPROTO_UDP;

 53:   } else if (!strcmp(*ptr, "--host")) {

 54:    ptr++;

 55:    if (!*ptr) usage();

 56:    hostName = *ptr;

 57:   } else if (!strcmp(*ptr, "--service")) {

 58:    ptr++;

 59:    if (!*ptr) usage();

 60:    serviceName = *ptr;

 61:   } else

 62:    usage();

 63:

 64:   ptr++;

 65:  }

 66:

 67:  /* необходимы имена hostName, serviceName или оба */

 68:  if (!hostName && !serviceName)

 69:   usage();

 70:

 71:  if ((rc = getaddrinfo(hostName, serviceName, &hints,

 72:   &cresult))) {

 73:   fprintf(stderr, "сбой поиска службы: %s\n",

 74:    gai_strerror(rc));

 75:   return 1;

 76:  }

 77:

 78:  /* проходит по связному списку, отображая все результаты */

 79:  addr = result;

 80:  while (addr) {

 81:   switch (addr->ai_family) {

 82:   case PF_INETs: printf("IPv4");

 83:    break;

 84:   case PF_INET6: printf("IPv6");

 85:    break;

 86:   default: printf("(%d) addr->ai_family);

 87:    break;

 88:   }

 89:

 90:   switch (addr->ai_socktype) {

 91:   case SOCK_STREAM: printf("\tstream");

 92:    break;

 93:   case SOCK_DGRAM: printf("\tdgram");

 94:    break;

 95:   case SOCK_RAW: printf("\traw");

 96:    break;

 97:   default: printf("\t(%d)

 98:    addr->ai_socktype);

 99:    break;

100:   }

101:

102:   if (addr->ai_family == PF_INET ||

103:    addr->ai_family == PF_INET6)

104:    switch (addr->ai_protocol) {

105:    case IPPROTO_TCP: printf("\ttcp");

106:     break;

107:    case IPPROTO_UDP: printf("\tudp");

108:     break;

109:    case IPPROTO_RAW: printf("\traw");

110:     break;

111:    default: printf("\t(%d)

112:     addr->ai_protocol);

113:     break;

114:    }

115:   else

116:    printf("\t");

117:

118:   /* отобразить информацию и для IPv4-, и для IPv6-адресов */

119:

120:   if (addr->ai_family == PF_INET) {

121:    struct sockaddr_in * inetaddr = (void*)addr->ai_addr;

122:    char nameBuf[INET_ADDRSTRLEN];

123:

124:    if (serviceName)

125:     printf("\tпорт%d", ntohs(inetaddr->sin_port));

126:

127:    if (hostName)

128:     printf("\tхост%s",

129:      inet_ntop(AF_INET, &inetaddr->sin_addr,

130:       nameBuf, sizeof(nameBuf)));

131:   } else if (addr->ai_family == PF_INET6) {

132:    struct sockaddr_in6 * inetaddr =

133:     (void*)addr->ai_addr;

134:    char nameBuf[INET6_ADDRSTRLEN];

135:

136:    if (serviceName)

137:     printf("\tпорт%d", ntohs(inetaddr->sin6_port));

138:

139:    if (hostName)

140:     printf("\tхост%s",

141:      inet_ntop(AF_INET6, &inetaddr->sin6_addr,

142:       nameBuf, sizeof(nameBuf)));

143:   }

144:

145:   if (addr->ai_canonname)

146:    printf("\tname%s", addr->ai_canonname);

147:

148:   printf("\n");

149:

150:   addr = addr->ai_next;

151:  }

152:

153:  /* очистить результаты getaddrinfo() */

154:  freeaddrinfo(result);

155:

156:  return 0;

157: }

В отличие от большинства библиотечных функций, getaddrinfo() возвращает целое число, которое равно нулю в случае успеха, и описывает ошибку в случае неудачи. Такие функции, как правило, не используют errno. В табл. 17.3 описаны различные коды ошибок, которые могут возвращать подобные функции.

Таблица 17.3. Ошибки поиска соответствия адреса и имени

Ошибка Описание
EAI_AGAIN Имя не может быть найдено. Повторный поиск может оказаться успешным.
EAI_BADFLAGS В функцию переданы недействительные флаги.
EAI_FAIL В процессе поиска соответствия возникла постоянная ошибка.
EAI_FAMILY Семейство адресов не распознано.
EAI_MEMORY Запрос на выделение памяти не выполнен.
EAI_NONAME Имя или адрес невозможно преобразовать.
EAI_OVERFLOW Переданный буфер слишком мал.
EAI_SERVICE Для данного типа сокета служба не существует.
EAI_SOCKTYPE Был передан недействительный тип сокета.
EAI_SYSTEM Произошла системная ошибка; сама ошибка содержится в переменной errno .

Коды ошибок можно преобразовать в строки, описывающие проблему, с помощью функции gai_strerror().

#include

const char * gai_strerror(int error);

Здесь параметр error должен быть ненулевым значением, возвращенным функцией getaddrinfo(). Если произошла ошибка EAI_SYSTEM, то для получения более точного описания программа должна использовать strerror(errno).

 

17.5.6. Преобразование адресов в имена

К счастью, переводить IP-адреса и номера портов в имена хостов и служб гораздо проще, чем наоборот.

#include

#include

int getnameinfo(struct sockaddr * addr, socklen_t addrlen,

 char * hostname, size_t hostlen,

 char * servicename, size_tservicelen, intflags);

Здесь параметр addr указывает либо на struct sockaddr_in, либо на struct sockaddr_in6, член addrlen содержит размер структуры, на которую указывает addr. IP-адрес и номер порта, определенные addr, преобразуются в имя хоста, сохраняющееся в ячейке, на которую указывает hostname, и в имя службы, сохраняющееся в servicename.

Один из параметров может равняться NULL, при этом функция getnameinfo() не ищет соответствие имени для данного параметра.

Параметры hostlen и servicelen определяют, сколько байт доступно в буферах, на которые указывают hostname и servicename соответственно. Если ни одно имя не умещается в доступном пространстве, буфера переполняются и возвращается ошибка (EAI_OVERFLOW).

Последний аргумент flags изменяет способ, которым функция getnameinfo() производит поиск имен. Параметр должен быть равен нулю или принимать одно или несколько (объединенных логическим "ИЛИ") из описанных ниже значений.

NI_DGRAM Отыскивается имя службы UDP для указанного порта (вместо имени службы TCP). Примечание . Эти два имени почти всегда идентичны, однако существует несколько портов, определенных только для UDP-портов (протокол прерывания SNMP — один из них), и несколько случаев, когда один и тот же номер порта используется для различных TCP и UDP служб (например, порт 512 применяется и для TCP-службы exec , и для UDP-службы biff ).
NI_NAMEREQD Если преобразование IP-адреса в имя хоста завершается неудачей и установлен данный флаг, то функция getnameinfo() возвращает ошибку. В противном случае она возвращает IP-адрес в формате с разделительными точками или двоеточиями.
NI_NOFQDN Имена хостов обычно возвращаются как полностью уточненные имена доменов. Это означает, что возвращается полное имя хоста, а не локальное сокращение. Если, к примеру, установлен данный флаг, вашим хостом является digit.iana.org , и вы ищете IP-адрес, соответствующий www.iana.org , тогда будет возвращено имя хоста www . Поиск имен хостов для остальных машин при этом не затрагивается (в предыдущем примере поиск адреса для www.ietf.org предоставит полное имя хоста www.ietf.org .
NI_NUMERICHOST Вместо выполнения поиска имен хостов функция getnameinfo() преобразует IP-адрес в IP-адрес по аналогии с inet_ntop() .
NI_NUMERICSERV Номер порта размещается в servicename в виде форматированной числовой строки (а не преобразуется в имя службы).

Возвращаемые коды для getnameinfo() — те же самые, что и для gethostinfо(); в случае успеха возвращается нуль, в случае неудачи — код ошибки. Полный перечень возможных ошибок приведен в табл. 17.3. Для преобразования этих ошибок в описательные строки служит функция gai_strerror().

Ниже приведен пример, показывающий использование getnameinfo() для выполнения обратного поиска имени для адресов IPv4 и IPv6.

$ ./reverselookup --host ::1

hostname: localhost

$ ./reverselookup --host 127.0.0.1

hostname: localhost

$ ./reverselookup --host 3ffe:b00:c18:1::10

hostname: www.6bone.net

$ ./reverselookup --host 206.123.31.124 --service 80

hostname: www.6bone.net service name: http

  1: /* reverselookup.с */

  2:

  3: #include

  4: #include

  5: #include

  6: #include

  7: #include

  8: #include

  9:

 10: /* Вызывается, если во время обработки командной строки происходит ошибка;

 11:    отображает короткое сообщение для пользователя и завершается. */

 12: void usage(void) {

 13:  fprintf(stderr, "использование: reverselookup [--numerichost] "

 14:   "[--numericserv] [--namereqd] [--udp]\n");

 15:  fprintf(stderr, " [--nofqdn] "

 16:   "[--service<служба>] [--host<имя_хоста>]\n");

 17:  exit(1);

 18: }

 19:

 20: int main(int argc, const char ** argv) {

 21:  int flags;

 22:  const char * hostAddress = NULL;

 23:  const char * serviceAddress = NULL;

 24:  struct sockaddr_in addr4;

 25:  struct sockaddr_in6 addr6;

 26:  struct sockaddr *addr = (struct sockaddr *) &addr4;

 27:  int addrLen = sizeof(addr4);

 28:  int rc;

 29:  int portNum = 0;

 30:  const char ** ptr;

 31:  char hostName[1024];

 32:  char serviceName[256];

 33:

 34:  /* очистить флаги */

 35:  flags = 0;

 36:

 37:  /* разобрать аргументы командной строки, игнорируя argv[0] */

 38:  ptr = argv + 1;

 39:  while (*ptr && *ptr[0] == '-') {

 40:   if (!strcmp(*ptr, "—numerichost")) {

 41:    flags |= NI_NUMERICHOST;

 42:   } else if (!strcmp (*ptr, "--numericserv")) {

 43:    flags |= NI_NUMERICSERV;

 44:   } else if (!strcmp (*ptr, "--namereqd")) {

 45:    flags |= NI_NAMEREQD;

 46:   } else if (!strcmp(*ptr, "--nofqdn")) {

 47:    flags |= NI_NOFQDN;

 48:   } else if (!strcmp (*ptr, "--udp")) {

 49:    flags |= NI_DGRAM;

 50:   } else if (!strcmp(*ptr, "--host")) {

 51:    ptr++;

 52:    if (!*ptr) usage();

 53:    hostAddress = *ptr;

 54:   } else if (!strcmp(*ptr, "--service")) {

 55:    ptr++;

 56:    if (!*ptr) usage();

 57:    serviceAddress = *ptr;

 58:   } else

 59:    usage();

 60:

 61:   ptr++;

 62:  }

 63:

 64:  /* необходимы адреса hostAddress, serviceAddress или оба */

 65:  if (!hostAddress && !serviceAddress)

 66:   usage();

 67:

 68:  if (serviceAddress) {

 69:   char * end;

 70:

 71:   portNum = htons(strtol(serviceAddress, &end, 0));

 72:   if (*end) {

 73:    fprintf(stderr, "сбой при преобразовании %s в число\n",

 74:     serviceAddress);

 75:    return 1;

 76:   }

 77:  }

 78:

 79:  if (!hostAddress) {

 80:   addr4.sin_family = AF_INET;

 81:   addr4.sin_port = portNum;

 82:  } else if (!strchr(hostAddress, ':')) {

 83:   /* Если hostAddress содержит двоеточие, то предполагаем версию IPv6.

 84:      В противном случае это IPv4 */

 85:

 86:   if (inet_pton(AF_INET, hostAddress,

 87:    &addr4.sin_addr) <= 0) {

 88:    fprintf(stderr, "ошибка преобразования IPv4-адреса %s\n",

 89:     hostAddress);

 90:    return 1;

 91:   }

 92:

 93:   addr4.sin_family = AF_INET;

 94:   addr4.sin_port = portNum;

 95:  } else {

 96:

 97:   memset(&addr6, 0, sizeof(addr6));

 98:

 99:   if (inet_pton(AF_INET6, hostAddress,

100:    &addr6.sin6_addr) <= 0) {

101:    fprintf(stderr, "ошибка преобразования IPv6-адреса %s\n",

102:     hostAddress);

103:    return 1;

104:   }

105:

106:   addr6.sin6_family = AF_INET6;

107:   addr6.sin6_port = portNum;

108:   addr = (struct sockaddr *) &addr6;

109:   addrLen = sizeof(addr6);

110:  }

111:

112:  if (!serviceAddress) {

113:   rc = getnameinfo(addr, addrLen, hostName, sizeof(hostName),

114:    NULL, 0, flags);

115:  } else if (!hostAddress) {

116:   rc = getnameinfo(addr, addrLen, NULL, 0,

117:    serviceName, sizeof(serviceName), flags);

118:  } else {

119:   rc = getnameinfo(addr, addrLen, hostName, sizeof(hostName),

120:    serviceName, sizeof(serviceName), flags);

121:  }

122:

123:  if (rc) {

124:   fprintf(stderr, "сбой обратного поиска: %s\n",

125:    gai_strerror(rc));

126:   return 1;

127:  }

128:

129:  if (hostAddress)

130:   printf("имя хоста: %s\n", hostName);

131:  if (serviceAddress)

132:   printf("имя службы: %s\n", serviceName);

133:

134:  return 0;

135: }

 

17.5.7. Ожидание TCP-соединений

Ожидание соединений TCP происходит почти идентично ожиданию соединений домена Unix. Единственные различия заключаются в семействах протоколов и адресов. Ниже показан вариант примера сервера домена Unix, который работает через сокеты TCP.

 1: /* tserver.с */

 2:

 3: /* Ожидает соединение на порте 4321. Как только соединение установлено,

 4:    из сокета в stdout копируются данные до тех пор, пока вторая

 5:    сторона не закроет соединение. Затем ожидает следующее соединение

 6:    с сокетом. */

 7:

 8: #include

 9: #include

10: #include

11: #include

12: #include

13: #include

14: #include

15:

16: #include "sockutil.h" /* некоторые служебные функции */

17:

18: int main(void) {

19:  int sock, conn, i, rc;

20:  struct sockaddr address;

21:  size_t addrLength = sizeof(address);

22:  struct addrinfo hints, * addr;

23:

24:  memset(&hints, 0, sizeof(hints));

25:

26:  hints.ai_socktype = SOCK_STREAM;

27:  hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG;

28:  if ((rc = getaddrinfo(NULL, "4321", &hints, &addr))) {

29:   fprintf(stderr, "сбой поиска имени хоста: %s\n",

30:   gai_strerror(rc));

31:   return 1;

32:  }

33:

34:  if ((sock = socket(addr->ai_family, addr->ai_socktype,

35:   addr->ai_protocol)) < 0)

36:   die("socket");

37:

38:  /* Позволяет ядру повторно использовать адрес сокета. Это разрешает

39:     нам запускать программу два раза подряд, не ожидая пока истечет

40:     время для кортежа (ip-адрес, порт). */

41:  i = 1;

42:  setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i));

43:

44:  if (bind(sock, addr->ai_addr, addr->ai_addrlen))

45:   die("bind");

46:

47:  freeaddrinfo(addr);

48:

49:  if (listen(sock, 5))

50:   die("listen");

51:

52:  while ((conn = accept(sock, (struct sockaddr *) &address,

53:   &addrLength)) >=0) {

54:   printf("----получение данных\n");

55:   copyData(conn, 1);

56:   printf("----готово\n");

57:   close(conn);

58:  }

59:

60:  if (conn < 0)

61:   die("accept");

62:

63:  close(sock);

64:  return 0;

65: }

Обратите внимание на то, что IP-адрес, привязанный к сокету, указывает номер порта 4321, но не IP-адрес. Это предоставляет ядру возможность при необходимости воспользоваться локальным IP-адресом.

Код в строках 41–42 требует дополнительного объяснения.

41: i = 1;

42: setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i));

Linux-реализация TCP, как и в остальных системах Unix, вводит ограничения на то, насколько скоро можно повторно использовать кортеж (локальный хост, локальный порт). Этот код устанавливает опцию на сокет, которая обходит это ограничение и позволяет дважды запускать сервер за короткий период времени. По сходной причине сервер-пример сокета домена Unix удаляет любой существующий файл сокета, прежде чем вызывать bind().

Функция setsockopt() позволяет устанавливать множество специальных опций для сокета и протокола:

#include

int setsockopt(int sock, int level, int option,

 const void * valptr, int vallength);

Первый аргумент — это сокет, для которого определяется опция. Второй аргумент, level, указывает тип устанавливаемой опции. В нашем сервере используется SOL_SOCKET, что указывает на установку опции обобщенного сокета. Параметр option определяет опцию, которая подлежит изменению. Указатель на новое значение опции передается через valptr, а размер значения, на которое указывает valptr, передается как vallength. Для нашего сервера применяется указатель на ненулевое целое число, которое вводит в действие опцию SO_REUSEADDR.

 

17.5.8. Клиентские приложения TCP

Клиенты TCP подобны клиентам домена Unix. Как правило, сразу же после создания сокета, клиент подключается к серверу с помощью функции connect(). Единственное различие состоит в способе передачи адреса в connect(). Вместо того, чтобы использовать имя файла, большинство клиентов TCP отыскивают имя хоста через функцию getaddrinfo(), которая предоставляет информацию для connect().

Ниже приводится несложный TCP-клиент, который взаимодействует с сервером, представленным в предыдущем разделе. Он принимает один аргумент: имя хоста, на котором работает сервер, или его IP-номер (в десятичном представлении с разделительными точками). Во всем остальном программа ведет себя также как клиент сокета домена Unix, показанный ранее в этой главе.

 1: /* tclient.с */

 2:

 3: /* Подключиться к серверу, чье имя хоста или IP-адрес переданы в качестве

 4: аргумента, на порте 4321. После соединения скопировать все содержимое

 5: stdin в сокет, затем завершить работу. */

 6:

 7: #include

 8: #include

 9: #include

10: #include

11: #include

12: #include

13: #include

14: #include

15:

16: #include "sockutil.h" /* некоторые служебные функции */

17:

18: int main(int argc, const char ** argv) {

19:  struct addrinfo hints, *addr;

20:  struct sockaddr_in * addrinfo;

21:  int rc;

22:  int sock;

23:

24:  if (argc !=2) {

25:   fprintf(stderr, "поддерживается только одиночный аргумент\n");

26:   return 1;

27:  }

28:

29:  memset(&hints, 0, sizeof(hints));

30:

31:  hints.ai_socktype = SOCK_STREAM;

32:  hints.ai_flags = AI_ADDRCONFIG;

33:  if ((rc = getaddrinfo(argv[1], NULL, &hints, &addr))) {

34:   fprintf(stderr, "сбой поиска имени хоста: %s\n",

35:   gai_strerror(rc));

36:   return 1;

37:  }

38:

39:  /* это позволяет получить доступ к sin_family и sin_port

40:     (которые расположены там же, где и sin6_family и sin6_port) */

41:  addrinfo = (struct sockaddr_in *) addr->ai_addr;

42:

43:  if ((sock = socket(addrInfo->sin_family, addr->ai_socktype,

44:   addr->ai_protocol)) < 0)

45:   die("socket");

46:

47:  addrInfo->sin_port = htons(4321);

48:

49:  if (connect(sock, (struct sockaddr *) addrinfo,

50:   addr->ai_addrlen))

51:   die("connect");

52:

53:  freeaddrinfo(addr);

54:

55:  copyData(0, sock);

56:

57:  close(sock);

58:

59:  return 0;

60: }

 

17.6. Использование дейтаграмм UDP

 

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

• Протоколы без соединений обрабатывают перезапуски машин более плавно, поскольку нет необходимости в переустановке соединений. Это очень заманчивое свойство для сетевых файловых систем (таких как NFS, действующей на основе UDP), поскольку оно позволяет перезапускать файловый сервер без уведомления клиента.

• Простейшие протоколы могут работать гораздо быстрее через дейтаграммный протокол. Служба имен доменов DNS использует UDP только по этой причине (несмотря на то что наряду с этим дополнительно поддерживается TCP). При установке соединения TCP клиентская машина отправляет сообщение на сервер, получает от сервера подтверждение, указывающее на активность соединения, затем сообщает серверу о том, что установлена клиентская сторона соединения23. После этого клиент может отправить свой запрос имени хоста на взаимодействующий сервер. Все это в итоге составляет процесс из пяти сообщений, не считая проверки ошибок и ожидания фактического отправления запроса и ответа на него. Используя UDP, запросы имени хоста пересылаются как первый пакет на сервер, который отвечает одним или более UDP-пакетами, тем самым уменьшая общий счетчик пакетов до пяти. Если клиент не получает ответ, то он просто перепосылает запрос.

• При первичной установке компьютеров часто требуется установить для них IP-адрес, а затем загрузить первую часть операционной системы через сеть. Применение UDP для подобных операций создает набор протоколов, который внедряется в такие машины гораздо проще, чем, если бы требовалась полная TCP-реализация.

 

17.6.1. Создание UDP-сокета

Как и любой другой сокет, UDP-сокет создается с помощью функции socket(), однако второй аргумент должен быть SOCK_DGRAM, а последний — либо IPPROTO_UDP, либо просто ноль (так как UDP является стандартным IP-дейтаграммным протоколом).

После создания сокета ему необходимо присвоить номер локального порта. Это происходит тогда, когда программа удовлетворяет одному из следующих трех условий.

• Номер порта задается явно через вызов функции bind(). Этот шаг является обязательным для тех серверов, для которых необходимо получение дейтаграмм на номер официального порта. Системный вызов в точности совпадает с системным вызовом для TCP-серверов.

• Дейтаграмма посылается через сокет. Ядро присваивает данному сокету номер порта UDP при первой передаче данных через него. В большинстве клиентских программ применяется именно этот прием, поскольку номер используемого порта для них не имеет значения.

• Для сокета устанавливается удаленный адрес через функцию connect() (которая является дополнительной для UDP-сокетов).

Также существует два различных способа присвоения номера удаленного порта. Вспомните о том, что TCP-сокеты имеют удаленный адрес, который присваивается через connect(). Этот адрес может использоваться и для UDP-сокетов. При этом функция connect() для TCP вызывает обмен пакетами для инициализации соединения (что делает connect() медленным системным вызовом), в то время как вызов connect() для UDP-сокетов просто присваивает удаленный IP-адрес и номер порта для исходящих дейтаграмм (и является быстрым системным вызовом). Еще одно различие состоит в том, что приложения могут подключаться к TCP-сокету только один раз; UDP-сокеты могут повторно использовать свои адреса назначения.

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

 

17.6.2. Отправка и получение дейтаграмм

Для отправки и получения UDP-пакетов обычно используются четыре системных вызова: send(), sendto(), recv(), recvfrom().

#include

#include

int send(int s, const void * data, size_t len, int flags);

int sendto(int s, const void * data, size_t len, int flags,

 const struct sockaddr * to, socklen_t toLen);

int recv(int s, void * data, size_t maxlen, int flags);

int recvfrom(int s, void * data, size_t maxlen, int flags,

 struct sockaddr * from, socklen_t * fromLen);

Здесь во всех случаях параметр flags всегда равен нулю. В других ситуациях он может принимать множество значений, они подробно рассматриваются в [33].

Первый из названных вызовов send() может применяться только для тех сокетов, для которых IP-адрес назначения и порт устанавливались через вызов connect(). Он посылает первые len байтов, на которые указывает data, на другой конец сокета s. Данные передаются как единая дейтаграмма. Если параметр len задает слишком большое количество данных для передачи в одной дейтаграмме, то в переменной errno возвращается значение EMSGSIZE.

Следующий системный вызов sendto() работает аналогично send(), но позволяет указывать IP-адрес и номер порта назначения для неподключенных сокетов. Последние два параметра являются указателями на адрес сокета и длину адреса сокета. Применение этой функции не устанавливает адрес назначения для сокета; он остается неподключенным. Последующие вызовы sendto() могут передавать дейтаграммы в другие пункты назначения. Если аргумент to равен NULL, то функция sendto() ведет себя точно также как и send().

Системные вызовы recv() и recvfrom() подобны send() и sendto(), но они получают дейтаграммы, а не отправляют их. Оба вызова записывают одну дейтаграмму в data (не более чем *maxlen байт) и отбрасывают некоторую часть дейтаграммы, которая не помещается в буфер. Удаленный адрес, отправивший дейтаграмму, сохраняется в параметре from функции recvmsg(), если только его длина не превышает fromLen байт.

 

17.6.3. Простой tftp-сервер

Данный простой tftp-сервер иллюстрирует отправку и получение UDP-дейтаграмм как для подключенных, так и для неподключенных сокетов. Протокол tftp представляет собой несложный протокол передачи файлов, построенный на основе UDP. Он часто используется встроенными компьютерными программами для пересылки первоначального загрузочного образа при сетевой загрузке. Сервер, который мы предлагаем рассмотреть, обладает рядом ограничений, поэтому он непригоден для какой-либо практической работы.

• С сервером одновременно может взаимодействовать только один клиент (этот недостаток легко устранить).

• Сервер может только отправлять файлы, но не может получать.

• Отсутствуют условия для ввода ограничений на передачу файлов анонимному удаленному пользователю.

• Выполняется очень поверхностная проверка ошибок, что, скорее всего, приведет к проблемам во время эксплуатации.

Клиент tftp начинает tftp-сеанс передачей "пакета запроса на чтение", содержащего имя файла, который нужно получить, и режим. Существует два исходных режима: netascii (выполняет некоторые простые преобразования файла) и octet (передает файл точно в таком же состоянии, в каком он находится на диске). Рассматриваемый сервер поддерживает только режим octet, поскольку он проще.

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

Основной формат дейтаграммы определен в строках 17-46. Некоторые константы указывают тип посылаемой дейтаграммы, а также код ошибки, отправляемой в том случае, если запрашиваемый файл не существует (все остальные ошибки обрабатываются непосредственно сервером). Структура struct tftpPacket описывает внешний вид дейтаграммы и код операции, следующей за данными, которая зависит от типа дейтаграммы. Затем логическое объединение, вложенное в структуру, определяет остальные форматы дейтаграмм для ошибок, данных и пакетов подтверждения.

Первая часть main() (строки 156—169) создает UDP-сокет и устанавливает номер локального порта с помощью вызова bind(). Последний является либо официальной tftp-службой, либо портом, указанным в качестве единственного аргумента командной строки программы. В отличие от нашего примера TCP-сервера здесь нет необходимости вызывать ни listen(), ни accept(), поскольку UDP работает без установки соединений.

После создания сокета сервер ожидает получение дейтаграммы путем вызова recvfrom(). Функция handleRequest(), которая активизируется в строке 181, преобразует запрашиваемый файл и возвращает его. После этого вызова сервер еще раз активизирует recvfrom() и ожидает запрос от следующего клиента.

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

В то время как главная часть программы использует неподключенный UDP-сокет (позволяющий любому клиенту соединение с ним), handleSocket() применяет для преобразования файла подключенный UDP-сокет. После анализа имени файла, который нужно передать, и проверки правильности режима преобразования в строке 93 создается сокет с тем же самым семейством, типом и протоколом, с которыми контактировал сервер. Затем используется connect() для установки удаленного конца сокета на том адресе, от которого поступил запрос на файл, и начинается передача файла. После отправки каждого блока сервер ожидает пакет подтверждения, прежде чем продолжить передачу. Когда приходит последний пакет подтверждения, сервер закрывает сокет и возвращается к главному циклу.

Данный сервер, как правило, запускается с единственным аргументом — номером порта. Для проверки можно применить стандартную клиентскую программу tftp, где первый аргумент является именем хоста для соединения (неплохим выбором будет localhost), а второй — номером порта, на котором работает сервер. После запуска клиента нужно активизировать команду bin, при этом файлы будут запрашиваться в режиме octet, а не в стандартном режиме netascii. Как только это сделано, команда get позволит передать любой файл от сервера клиенту.

  1: /* tftpserver.c */

  2:

  3: /* Это частичная реализация tftp. Она не поддерживает даже тайм-ауты

  4:    или повторную передачу пакетов, и она не очень хорошо

  5:    обрабатывает непредвиденные события.*/

  6:

  7: #include

  8: #include

  9: #include

 10: #include

 11: #include

 12: #include

 13: #include

 14:

 15: #include "sockutil.h" /* некоторые служебные функции */

 16:

 17: #define RRQ   1 /* запрос на чтение */

 18: #define DATA  3 /* блок данных */

 19: #define ACK   4 /* подтверждение */

 20: #define ERROR 5 /* возникла ошибка */

 21:

 22: /* коды ошибок tftp */

 23: #define FILE_NOT_FOUND 1

 24:

 25: struct tftpPacket {

 26:  short opcode;

 27:

 28:  union {

 29:   char bytes[514]; /* самый большой блок, который мы

 30:                       можем обработать, содержит 2 байта

 31:                       для номера блока и 512 для данных */

 32:   struct {

 33:    short code;

 34:    char message[200];

 35:   } error;

 36:

 37:   struct {

 38:    short block;

 39:    char bytes[512];

 40:   } data;

 41:

 42:   struct {

 43:    short block;

 44:   } ack;

 45:  } u;

 46: };

 47:

 48: void sendError(int s, int errorCode) {

 49:  struct tftpPacket err;

 50:  int size;

 51:

 52:  err.opcode = htons(ERROR);

 53:

 54:  err.u.error.code = htons(errorCode); /* файл не найден */

 55:  switch (errorCode) {

 56:  case FILE_NOT_FOUND:

 57:   strcpy(err.u.error.message, "файл не найден");

 58:   break;

 59:  }

 60:

 61:  /* 2 байта кода операции, 2 байта кода ошибки, сообщение и '\0' */

 62:  size = 2 + 2 + strlen(err.u.error.message) + 1;

 63:  if (send(s, &err, size, 0) != size)

 64:   die("erarorsend");

 65: }

 66:

 67: void handleRequest(struct addrinfo tftpAddr,

 68:  struct sockaddr remote, int remoteLen,

 69:  struct tftpPacket request) {

 70:  char * fileName;

 71:  char * mode;

 72:  int fd;

 73:  int s;

 74:  int size;

 75:  int sizeRead;

 76:  struct tftpPacket data, response;

 77:  int blockNum = 0;

 78:

 79:  request.opcode = ntohs(request.opcode);

 80:  if (request.opcode != RRQ) die("неверный код операции");

 81:

 82:  fileName = request.u.bytes;

 83:  mode = fileName + strlen(fileName) + 1;

 84:

 85:  /* здесь поддерживается только режим bin */

 86:  if (strcmp(mode, "octet")) {

 87:   fprintf(stderr, "неверный режим %s\n", mode);

 88:   exit(1);

 89:  }

 90:

 91:  /* требуется передача при помощи сокета того же семейства и типа,

 92:     с которым мы начинали */

 93:  if ((s = socket(tftpAddr.ai_family, tftpAddr.ai_socktype,

 94:   tftpAddr.ai_protocol)) < 0)

 95:   die("send socket");

 96:

 97:  /* установить удаленный конец сокета на адрес, который

 98:     инициирует данное соединение */

 99:  if (connect(s, &remote, remoteLen))

100:   die("connect");

101:

102:  if ((fd = open(fileName, O_RDONLY)) < 0) {

103:   sendError(s, FILE_NOT_FOUND);

104:   close(s);

105:   return;

106:  }

107:

108:  data.opcode = htons(DATA);

109:  while ((size = read(fd, data.u.data.bytes, 512)) > 0) {

110:   data.u.data.block = htons(++blockNum);

111:

112:   /* размер составляют 2 байта (код операции), 2 байта (номер блока) и данные*/

113:   size += 4;

114:   if (send(s, &data, size, 0) != size)

115:    die("data send");

116:

117:   sizeRead = recv(s, &response, sizeof(response), 0);

118:   if (sizeRead < 0) die("recv ack");

119:

120:   response.opcode = ntohs(response.opcode);

121:   if (response.opcode != ACK) {

122:    fprintf(stderr, "непредвиденный код операции в отклике\n");

123:    exit(1);

124:   }

125:

126:   response.u.ack.block = ntohs(response.u.ack.block);

127:   if (response.u.ack.block != blockNum) {

128:    fprintf(stderr, "получено подтверждение неверного блока\n");

129:    exit(1);

130:   }

131:

132:   /* если блок, который мы только что отправили, содержит

133:      меньше 512 байт, то задача выполнена */

134:   if (size < 516) break;

135:  }

136:

137:  close(s);

138: }

139:

140: int main(int argc, char ** argv) {

141:  struct addrinfo hints, * addr;

142:  char * portAddress = "tftp";

143:  int s;

144:  int rc;

145:  int bytes, fromLen;

146:  struct sockaddr from;

147:  struct tftpPacket packet;

148:

149:  if (argc > 2) {

150:   fprintf(stderr, "использование: tftpserver [порт]\n");

151:   exit(1);

152:  }

153:

154:  if (argv[1]) portAddress = argv[1];

155:

156:  memset(&hints, 0, sizeof (hints));

157:

158:  hints.ai_socktype = SOCK_DGRAM;

159:  hints.ai_flags = AI_ADDRCONFIG | AI_PASSIVE;

160:  if ((rc = getaddrinfo(NULL, portAddress, &hints, &addr)))

161:   fprintf(stderr, "сбой поиска порта %s\n",

162:    portAddress);

163:

164:  if ((s = socket(addr->ai_family, addr->ai_socktype,

165:   addr->ai_protocol)) < 0)

166:   die("socket");

167:

168:  if (bind(s, addr->ai_addr, addr->ai_addrlen))

169:   die("bind");

170:

171:  /* Основной цикл состоит из ожидания tftp-запроса, его обработки

172:     и затем ожидания следующего запроса. */

173:  while (1) {

174:   bytes = recvfrom(s, &packet, sizeof(packet), 0, &from,

175:    &fromLen);

176:   if (bytes < 0) die("recvfrom");

177:

178:   /* Если выполнить разветвление перед вызовом handleRequest() и

179:      завершить дочерний процесс после возврата функции, то данный

180:      сервер будет работать точно как параллельный tftp-сервер */

181:   handleRequest(*addr, from, fromLen, packet);

182:  }

183: }

 

17.7. Ошибки сокетов

Некоторые значения errno встречаются только при работе с сокетами. Ниже приведен список специфических ошибок сокетов вместе с краткими их описаниями.

EADDRINUSE Запрашиваемый адрес уже используется и не может быть переприсвоен.
EADDRNOTAVAIL Запрашивается несуществующий адрес.
EAFNOSUPPORT Указано неподдерживаемое семейство адресов.
ECONNABORTED Соединение прервано программным обеспечением.
ECONNREFUSED Удаленная машина отклонила попытку соединения.
ECONNRESET Соединение переустановлено удаленным концом. Это, как правило, указывает на то, что удаленная машина была перезагружена.
EDESTADDRREQ Выполнена попытка передачи данных через сокет без предоставления адреса назначения. Это может происходить только в дейтаграммных сокетах.
EHOSTDOWN Удаленный хост не находится в сети.
EHOSTUNREACH Удаленный хост недоступен.
EISCONN Для сокета уже установлено соединение.
EMSGSIZE Данные, передаваемые через сокет, слишком велики для отправления в одном элементарном сообщении.
ENETDOWN Сетевое соединение прекратилось.
ENETRESET Сеть была сброшена, что вызвало потерю соединения.
ENETUNREACH Указанная сеть недоступна.
ENOBUFS Для обработки запроса доступного пространства буфера недостаточно.
ENOPROTOOPT Выполнена попытка установить неправильную опцию.
ENOTCONN До выполнения операции необходимо установить соединение.
ENOTSOCK Специфическая сокетная операция была направлена на файловый дескриптор, который ссылается не на сокет.
EPFNOSUPPORT Указано неподдерживаемое семейство протоколов.
EPROTONOSUPPORT Запрос был сделан для неподдерживаемого протокола.
EPROTOTYPE Для сокета был указан несоответствующий тип протокола.
ESOCKTNOSUPPORT Выполнена попытка создания неподдерживаемого типа сокета.
ETIMEDOUT Время соединения истекло.

 

17.8. Унаследованные сетевые функции

 

В данный момент действует множество библиотечных функций, относящихся к работе сетей TCP/IP, которые нельзя применять в новых приложениях. Однако они широко используются в существующих IPv4-программах. В связи с этим они рассматриваются ниже для того, чтобы помочь вам понять и обновить старые коды.

 

17.8.1. Манипулирование IPv4-адресами

Функции inet_ntop() и inet_pton() являются относительно новыми и были введены для того, чтобы один набор функций мог обрабатывать и IPv4-, и IPv6-адреса. До их появления в программах использовались функции inet_addr(), inet_aton() и inet_ntoa(), которые предназначены только для IPv4.

Вспомните, что struct sockaddr_in определяется следующим образом

struct sockaddr__in {

 short int sin_family;        /* AF_INET */

 unsigned short int sin_port; /* номер порта */

 struct in_addr sin_addr;     /* IP-адрес */

}

Член sin_addr представляет собой структуру struct in_addr; унаследованные функции используют его в качестве параметра. Подразумевается, что данная структура является непрозрачной; программы приложений могут обрабатывать struct in_addr исключительно через библиотечные функции. Старой функцией для преобразования IPv4-адреса в десятичную форму с разделительными точками служит inet_ntoa().

#include

#include

char * inet_ntoa(struct in_addr address);

Передаваемый адрес преобразуется в строку в десятичном формате с разделительными точками, возвращается указатель на данную строку. Строка сохраняется в статическом буфере библиотеки С и уничтожается при следующем вызове inet_ntoa().

Существуют две функции, которые предлагают обратное преобразование десятичной строки в двоичный IP-адрес. Более старая из них функция inet_addr() имеет две проблемы, обе вызванные тем, что она возвращает результат типа long. Она не возвращает struct in_addr, как предполагается остальными стандартными функциями, поэтому программисты были вынуждены выполнять неуклюжие приведения. К тому же, если переменная типа long имела 32 бита, то программы не могли различить возврат числа -1 (что указывает на ошибку, например, неправильный адрес) и двоичного представления адреса 255.255.255.255.

#include

#include

unsigned long int inet_addr(const char * ddaddress);

Функция принимает передаваемую строку, которая должна содержать десятичный IP-адрес с разделительными точками, и преобразует ее в двоичный IP-адрес.

Для исправления недостатков inet_addr() была введена функция inet_aton().

#include

#include

int inet_aton(const char * ddaddress, struct in_addr * address);

Данная функция ожидает строку, содержащую десятичный IP-адрес, и размещает двоичное представление этого адреса в структуре struct in_addr, на которую указывает параметр address. В отличие от большинства библиотечных функций inet_aton() возвращает нуль в случае ошибки и ненулевое значение, если преобразование прошло успешно.

 

17.8.2. Преобразование имен хостов

Функции getaddrinfo(), getnameinfo(), позволяющие легко создавать программы, которые поддерживают и IPv4, и IPv6, были введены именно с этой целью. Исходные функции имен хостов было сложно расширить на IPv6, их интерфейсы требовали, чтобы приложения учитывали множество особенностей версии в структурах, сохраняющих IP-адрес. Новые интерфейсы абстрактны, поэтому поддерживают IPv4 и IPv6 одинаково.

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

#include

struct hostent {

 char* h_name;       /* каноническое имя хоста */

 char** h_aliases;   /* псевдонимы (завершающиеся NULL) */

 int h_addrtype;     /* тип адреса хоста */

 int h_length;       /* длина адреса */

 char** h_addr_list; /* список адресов (завершающийся NULL) */

};

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

Параметр h_addrtype сообщает тип адреса хоста. В данной главе будет применяться только AF_INET. Приложения, которые создавались для поддержки IPv6, получат и другие типы адресов. Следующий член h_length указывает длину двоичных адресов для данного хоста. Для адресов AF_INET эта длина равна sizeof(struct in_addr). Последний элемент h_addr_list представляет собой массив указателей на адреса данного хоста, последний из которых равен NULL для обозначения конца списка. Если h_addrtype равен AF_INET, то каждый указатель в этом списке указывает на структуру struct in_addr.

Две библиотечные функции выполняют преобразования между IP-номерами и именами хостов. Первая из них gethostbyname() возвращает struct hostent для имени хоста. Вторая — gethostbyaddr() — возвращает информацию о машине с данным IP-адресом.

#include

struct hostent * gethostbyname(const char * name);

struct hostent * gethostbyaddr(const char * addr, int len, int type);

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

Функция gethostbyname() принимает один параметр — строку, содержащую имя хоста. Функция gethostbyaddr() принимает три параметра, которые вместе определяют адрес. Первый из них addr указывает на struct in_addr. Следующий len устанавливает длину информации, на которую указывает параметр addr. Последний type излагает тип адреса, который нужно преобразовать в имя хоста (AF_INET для IPv4-адресов).

Если во время поиска имени хоста происходят ошибки, то код ошибки передается в h_errno. Вызов функции herror() распечатывает описание ошибки (данная функция почти идентична стандартной функции perror()).

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

 

17.8.3. Пример поиска информации хоста с использованием унаследованных функций

Ниже приводится пример программы, использующей inet_aton(), inet_ntoa(), gethostbyname(), gethostbyaddr(). Она принимает единственный аргумент, который может быть либо именем хоста, либо IP-адресом в десятичном представлении с точками. Она отыскивает хост и распечатывает все имена хоста и IP-адреса, ассоциированные с ним.

Любой аргумент, который является действительным десятичным адресом, считается IP-номером, а не именем хоста.

 1: /* lookup.с */

 2:

 3: /* Получает либо имя хоста, либо IP-адрес в командной строке, выводит

 4:    каноническое имя хоста для данного хоста и все IP-номера и имена

 5:    хостов, ассоциированные с ним. */

 6:

 7: #include /* для gethostby* */

 8: #include

 9: #include /* для адресных структур */

10: #include /* для inet_ntoa() */

11: #include

12:

13: int main(int argc, const char ** argv) {

14:  struct hostent * answer;

15:  struct in_addr address, ** addrptr;

16:  char ** next;

17:

18:  if (argc != 2) {

19:   fprintf(stderr, "поддерживается только одиночный аргумент\n");

20:   return 1;

21:  }

22:

23:  /* Если аргумент выглядит как IP, то принимаем его как таковой

24:     и выполняет обратный поиск имени */

25:  if (inet_aton(argv[1], &address))

26:   answer = gethostbyaddr((char *)&address, sizeof(address),

27:                           AF_INET);

28:  else

29:   answer = gethostbyname(argv[1])

30:

31:  /* поиск имени хоста не удался */

32:  if (!answer) {

33:   herror("ошибка поиска хоста");

34:   return 1;

35:  }

36:

37:  printf("Каноническое имя хоста: %s\n", answer->h_name);

38:

39:  /* если есть псевдонимы, все они выводятся на печать */

40:  if (answer->h_aliases[0]) {

41:   printf("Псевдонимы:");

42:   for(next = answer->h_aliases; *next; next++)

43:    printf(" %s", *next);

44:   printf("\n");

45:  }

46:

47:  /* отобразить все IP-адреса для данной машины */

48:  printf("Адреса:");

49:  for (addrptr = (structin_addr **) answer->h_addr_list;

50:   *addrptr; addrptr++)

51:   printf (" %s", inet_ntoa(**addrptr));

52:  printf("\n");

53:

54:  return 0;

55: }

Ниже показан пример вывода этой программы.

$ ./lookup ftp.netscape.com

Каноническое имя хоста: ftp25.netscape.com

Псевдонимы: ftp.netscape.com anonftp10.netscape.com

Адреса: 207.200.74.21

 

17.8.4. Поиск номеров портов

Новые функции getaddrinfo() и getnameinfo() предлагают простое выполнение преобразований имен служб в номера портов с одновременным определением имени хоста. В старых реализациях поиск имен служб проводился абсолютно независимо от поиска имен хостов. Доступ к именам служб можно получить через функцию getservbyname().

#include

struct servent * getservbyname(const char * name,

 const char * protocol);

Первый параметр name представляет собой имя службы, о которой в приложении требуется информация. Параметр protocol указывает протокол для использования. База данных служб содержит информацию о других протоколах (особенно UDP); конкретное определение протокола позволяет функции игнорировать информацию по другим протоколам. Параметр protocol обычно является строкой "tcp", хотя могут использоваться и другие имена протоколов, например, "udp".

Функция getservbyname() возвращает указатель на структуру, которая содержит информацию о запрашиваемой службе. Информация может перезаписываться при последующем вызове getservbyname(), поэтому важные данные нужно сохранять в приложении. Функция getservbyname() возвращает следующую информацию:

#include

struct servent {

 char * s_name;     /* имя службы */

 char ** s_aliases; /* псевдонимы службы */

 int s_port;        /* номер порта */

 char * s_proto;    /* протокол для использования */

}

Каждая служба может иметь несколько имен, ассоциированных с ней, но только один номер порта. Переменная s_name регистрирует каноническое имя службы, s_port содержит официальный номер порта данной службы (представленный в сетевом порядке байтов), s_proto представляет протокол для использования (например, "tcp"). Член s_aliases является массивом указателей псевдонимов службы (указатель NULL обозначает конец списка).

Если функция не выполняет свою работу, то она возвращает NULL и устанавливает h_errno. Ниже приведен пример программы, которая извлекает TCP-службу, указанную в командной строке, и выводит на экран каноническое имя, номер порта и все псевдонимы данной службы.

 1: /* services.с */

 2:

 3: #include

 4: #include

 5: #include

 6:

 7: /* Отображает номер порта TCP и все псевдонимы службы,

 8:    указанной в командной строке */

 9:

10: /* services.с отыскивает номер порта для службы */

11: int main(int argc, const char ** argv) {

12:  struct servent * service;

13:  char ** ptr;

14:

15:  if (argc != 2) {

16:   fprintf(stderr, "поддерживается только одиночный аргумент\n");

17:   return 1;

18:  }

19:

20:  /* поиск службы в /etc/services, в случае неудачи

21:     передается ошибка */

22:  service = getservbyname(argv[1] , "tcp");

23:  if (!service) {

24:   herror("getservbyname failed");

25:   return 1;

26:  }

27:

28:  printf("служба: %s\n", service->s_name);

29:  printf("tcp-порт: %d\n", ntohs(service->s_port));

31:  /* отобразить все псевдонимы, которые имеет данная служба */

32:  if (*service->s_aliases) {

33:   printf("псевдонимы:");

34:   for (ptr = service->s_aliases; *ptr; ptr++)

35:    printf(" %s", *ptr);

36:   printf("\n");

37:  }

38:

39:  return 0;

40: }

Ниже показан пример запуска программы. Обратите внимание на то, что она извлекает службы либо по каноническому имени, либо по псевдониму.

$ ./services http

служба: http

tcp-порт: 80

$ ./services source

служба: chargen

tcp-порт: 19

псевдонимы: ttytst source

 

Глава 18

Время

 

18.1. Вывод времени и даты

 

18.1.1. Представление времени

В системах Unix и Linux время отслеживается в секундах до или после начала эпохи, которое определяется как полночь 1 января 1970 года по UTC. Положительные значения времени относятся к периоду после начала эпохи; отрицательные — до начала эпохи. Для того чтобы обеспечить работу процессов в режиме текущего времени, в Linux, как и во всех остальных версиях Unix, предусмотрен системный вызов time().

#include

time_t time (time_t *t);

Функция time() возвращает количество секунд, прошедших с момента начала эпохи. Если значение t не является нулевым, то данная функция передает в эту переменную количество секунд, прошедших с начала эпохи.

Для решения некоторых проблем требуется более высокая разрешающая способность. В Linux предусмотрен еще один системный вызов — gettimeofday(), который предоставляет более подробную информацию.

#include

#include

int gettimeofday(struct timeval *tv, struct timezone *tz);

struct timeval {

 int tv_sec;  /* секунды */

 int tv_usec; /* микросекунды */

};

struct timezone {

 int tz_minuteswest; /* минуты на запад от Гринвича */

 int tz_dsttime;     /* тип корректировки dst */

};

На большинстве платформ, включая i386, система Linux поддерживает возможность очень точного измерения времени. Стандартные персональные компьютеры содержат встроенные часы, которые обеспечивают информацию о текущем времени с точностью до микросекунд. Оборудование Alpha и SPARC также предлагает высокоточный таймер. На некоторых других платформах система Linux может отслеживать время только в пределах разрешающей способности системного таймера, который в общем случае устанавливается на значение 100 Гц. В связи с этим член tv_usec структуры timeval в подобных системах может иметь меньшую точность.

В sys/time.h определены пять макросов для обработки структур timeval.

timerclear(struct timeval *)

Данный макрос очищает структуру timeval.

timerisset(struct timeval *)

Данный макрос проверяет структуру timeval на заполнение (другими словами, отличен ли хотя бы один элемент от нуля).

timercmp(struct timeval *t0, struct timeval *t1, operator)

Данный макрос позволяет сравнивать две структуры timeval в одном временном интервале. Он вычисляется в логический эквивалент t0 операция t1, если t0 и t1 относятся к арифметическим типам. Обратите внимание на то, что макрос timercmp() не работает для операций <= и >=. Вместо этого нужно применять формы !timercmp(t1, t2, >) и !timercmp(t1, t2, <).

timeradd(struct timeval *t0, struct timeval *t1, struct timeval *result)

Добавляет t0 к t1 и размещает сумму в переменной result.

timersub(struct timeval *t0, struct timeval *t1, struct timeval *result)

Вычитает t1 из t0 и передает разность в переменную result.

Третье представление времени struct tm дает время в исчислении, более привычном для человека.

struct tm {

 int tm_sec;

 int tm_min;

 int tm_hour;

 int tm_mday;

 int tm_mon;

 int tm_year;

 int tm_wday;

 int tm_yday;

 int tm_isdst;

 long int tm_gmtoff;

 const char *tm_zone;

};

Первые девять элементов являются стандартными, последние два — нестандартные, однако очень полезные (они существуют в системах Linux).

tm_sec Количество прошедших секунд в минуте. Принимает значения от 0 до 61 (две дополнительные секунды выделяются для учета лишних секунд, относящихся к високосному году).
tm_min Количество прошедших минут в часе. Принимает значения от 0 до 59.
tm_hour Количество прошедших часов в сутках. Принимает значения от 0 до 23.
tm_mday Номер дня месяца. Принимает значения от 1 до 31. Это единственный элемент, который не может равняться нулю.
tm_mon Количество прошедших месяцев в году. Принимает значения от 0 до 11.
tm_year Количество прошедших лет (считая с 1900 года).
tm_wday Количество прошедших дней в неделе (считая от воскресенья). Принимает значения от 0 до 6.
tm_yday Количество прошедших дней в году. Принимает значения от 0 до 365.
tm_isdst Определяет, поддерживается ли летнее время в текущем часовом поясе, tm_isdst принимает положительное значение, если время переведено на летнее, 0 — если не переведено, 1 — если система не может это определить.
tm_gmtoff Параметр не является переносимым, поскольку он используется не во всех системах. Если он существует, то он может также называться __tm_gmtoff . Данная переменная указывает число секунд к востоку от UTC или отрицательное число секунд к западу от UTC для часовых поясов к востоку от линии перемены дат.
tm_zone Параметр не является переносимым, поскольку он используется не во всех системах. Если он существует, то он может также называться __tm_zone . Он содержит название текущего часового пояса (некоторые часовые пояса могут иметь несколько имен).

В завершение, стандарт POSIX.1b обработки данных в режиме реального времени поддерживает даже большую разрешающую способность, чем доступные в стандарте struct timeval микросекунды. В структуре struct timespec используются наносекунды, а также выделено больше пространства для размещения чисел.

struct timespec {

 long int tv_sec;  /* секунды */

 long int tv_nsec; /* наносекунды */

};

 

18.1.2. Преобразование, форматирование и разбор значений времени

Для взаимно-обратных преобразований времени, выраженного в показателях time_t, и времени, выраженного в показателях struct tm, используются четыре функции. Три из них являются стандартными и доступны во всех системах Linux и Unix. Четвертая, не менее полезная, может применяться не всегда, поскольку она работает только в современных системах Linux. Пятая функция (стандартная) вычисляет разность в секундах между значениями времени time_t. (Обратите внимание на то, что даже аргументы time_t передаются как указатели, а не как только аргументы struct tm.)

struct tm * gmtime(const time_t *t)

Сокращенная форма времени по Гринвичу; функция gmtime() преобразует значение time_t в struct tm, которое выражает данное время в UTC.

struct tm * localtime(const time_t *t)

localtime() ведет себя подобно gmtime() за исключением того, что создается объект struct tm, выраженный в показателях местного времени. Местное время определяется для всей системы путем установки файлов часовых поясов. Его можно переопределить с помощью переменной окружения TZ для пользователей, работающих в часовом поясе, отличном от того, в котором находится компьютер.

time_t mktime(struct tm *tp);

mktime() преобразует struct tm в time_t, предполагая, что struct tm выражается в показателях местного времени.

time_t timegm(struct tm *tp);

timegm() ведет себя подобно mktime() за исключением предположения о том, что struct tm выражается в показателях UTC. Данная функция не является стандартной.

double difftime(time_t time1, time_t time0);

difftime() возвращает число с плавающей запятой, представляющее разность во времени в секундах между двумя значениями time_t. Хотя time_t гарантированно принадлежит к арифметическому типу, единица измерения не определяется в ANSI/ISO С; difftime() возвращает разность в секундах в зависимости от единиц измерения time_t.

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

char *asctime(struct tm *tp);

char *ctime(time_t *t);

asctime() и ctime() служат для преобразования временных значений в стандартную строку даты Unix, которая выглядит примерно так:

Tue Jun 17 23:17:29 1997

В обоих случаях длина сроки равна 26 символам и включает в себя завершающие символы новой строки и '\0'.

Не во всех локалях длина строки обязательно равна 26 символам, как в стандартной локали С.

ctime() выражает указанную дату в местном времени; asctime() — в том часовом поясе, который указан в struct tm. Если последний объект был создан с помощью gmtime(), то в показателях UTC, если при помощи localtime(), то по местному времени.

size_t strftime (char *s, size_t max, char *fmt, struct tm *tp);

strftime() работает также как sprintf() для времени. Она форматирует struct tm в соответствии с форматом fmt и размещает результат в не более чем max байтах (включая завершающий символ '\0') строки s.

Подобно sprintf(), функция strftime() использует символ % для ввода управляющих последовательностей, в которые подставляются данные. Все подстановочные строки выражаются в показателях текущей локали. Однако сами управляющие последовательности являются совершенно разными. В некоторых случаях строчные буквы применяются для аббревиатур, а заглавные буквы — для полных имен. В отличие от sprintf(), здесь отсутствует опция употребления чисел в середине управляющей последовательности для ограничения длины подстановочной строки; выражение %.6А недопустимо. По аналогии с функцией sprintf(), strftime() возвращает количество символов, выведенных в буфер s. Равенство данной величины значению max означает, что объем буфера недостаточен для текущей локали; необходимо выделить больший буфер и попытаться снова.

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

Трехбуквенная аббревиатура для названия дня недели.
Полное название дня недели.
%b Трехбуквенная аббревиатура для названия месяца.
Полное название месяца.
Предпочтительное локальное выражение даты и времени (такое как возвращают функции ctime() и asctime() ).
%d День месяца в числовом виде (отсчет ведется от нуля).
Час дня по 24-часовому времени (отсчет ведется от нуля).
%I Час дня по 12-часовому времени (отсчет ведется от нуля).
%j День года (отсчет ведется от единицы).
%m Месяц года (отсчет ведется от единицы).
Минута в часе (отсчет ведется от нуля).
%p Соответствующая строка для локального эквивалента выражений AM или PM.
%S Секунда в минуте (отсчет ведется от нуля).
%U Неделя года в числовом виде (первая неделя начинается с первого воскресенья года).
%W Неделя года в числовом виде (первая неделя начинается с первого понедельника года).
%w День недели в числовом виде (отсчет ведется с нуля).
%x Предпочтительное локальное выражение только для даты, без времени.
%X Предпочтительное локальное выражение только для времени, без даты.
%y Двухзначное представление года (без столетия). Не рекомендуется использовать такой формат — это потенциальный источник "проблемы 2000-го года".
%Y Полное четырехзначное числовое представление года.
%Z Название стандартной аббревиатуры часовой зоны.
%% Буквенный символ % .

char *strptime(char *s, char *fmt, struct tm *tp);

Как и scanf(), функция strptime() преобразует строку в разобранный формат. Она пытается быть либеральной при интерпретации введенной строки s в соответствии с форматирующей строкой fmt. Она принимает те же самые управляющие последовательности, что и strftime(), при этом для каждого типа ввода она допускает как аббревиатуры, так и полные имена. Она не различает символы верхнего и нижнего и регистра, а также не распознает %U и %W.

Функция strptime() предусматривает несколько дополнительных управляющих последовательностей; также она интерпретирует несколько последовательностей иначе (не так, как strftime()). В последующем списке перечислены только те управляющие последовательности, которые отличаются значительно (другими словами, выходят за рамки тех изменений, на которые мы уже указывали). Числа могут предваряться нулями, но это не обязательно.

%h Эквивалент %b и %B .
Считывает дату и время так, как печатает функция strftime() с форматирующей строкой %x %X .
Считывает дату и время так, как печатает функция strftime() с форматирующей строкой %с .
%e Эквивалент %d .
%D Считывает дату так, как печатает функция strftime() с форматирующей строкой %m/%d/%y .
%k Эквивалент %Н .
%l Эквивалент %I .
%r Считывает время так, как печатает функция strftime( ) с форматирующей строкой %I:%М:%S %p .
%R Считывает время так, как печатает функция strftime() с форматирующей строкой %Н:%М .
%T Считывает время так, как печатает функция strftime() с форматирующей строкой %Н:%М:%S .
%y Считывает год в пределах двадцатого столетия. Допустимы значения только от 0 до 99, поскольку к ним добавляется число 1900.
%Y Считывает полный год. Применяйте, по возможности, этот формат вместо %у для того, чтобы избежать "проблемы 2000-го года".

Функция strptime() возвращает указатель на символ в s — символ, находящийся за последним прочитанным во время разбора.

Функция strptime(), к сожалению, не определена ни в ANSI/ISO, ни в POSIX, что ограничивает ее переносимость.

 

18.1.3. Ограничения, связанные со временем

В 32-разрядных системах Linux, как и в большинстве систем Unix, переменная time_t является целым числом со знаком длиной 32 бита. Это означает, что в 10:14:07 вечера 18 января (четверг) 2038 года она переполнится. Поэтому время 10:14:08 вечера 18 января (четверг) 2038 года будет представлено как 3:45:52 вечера 13 декабря (пятница) 1901 года. Как видите, система Linux не проявляет "проблему 2000-го года" (поскольку используются собственные библиотеки времени), однако с ней связана "проблема 2038-го года".

На 64-разрядных платформах переменная time_t является соответственно 64-битовым числом со знаком. Это действительно эффективное решение, поскольку 64-битовое время со знаком можно назвать астрономическим.

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

 1: /* daytime.с */

 2:

 3: #include

 4: #include

 5: #include

 6:

 7: int main () {

 8:  struct timeval tv;

 9:  struct timezone tz;

10:  time_t now;

11:  /* beginning_of_time — это наименьшее значении, измеряемое time_t*/

12:  time_t beginning_of_time = 1L<<(sizeof(time_t)*8 - 1);

13:  /* end_of_time - это наибольшее значение, измеряемое time_t */

14:  time_t end_of_time = ~beginning_of_time;

15:

16:  printf("time_t имеет %d бит в длину\n\n", sizeof(time_t) *8);

17:

18:  gettimeofday(&tv, &tz);

19:  now = tv.tv_sec;

20:  printf("Текущее время дня, представленное в виде структуры timeval:\n"

21:   "tv.tv_sec = 0x%08x, tv.tv_usec = 0x%08х\n"

22:   "tz.tz_minuteswest = 0x%08х, tz.tz_dsttime = 0x%08x\n\n",

23:   tv.tv_sec, tv.tv_usec, tz.tz_minuteswest, tz.tz_dsttime);

24:

25:  printf("Демонстрация ctime()%s:\n",

26:   sizeof(time_t)*8 <= 32 ? "" :

27:   " (может зависнуть после печати первой строки; нажмите "

28:   "Control-C)") ;

29:  printf("текущее время: %s", ctime(&now));

30:  printf("начало времени: %s", ctime(&beginning_of_time));

31:  printf("конец времени: %s", ctime(&end_of_time));

32:

33:  exit(0);

34: }

К сожалению, функция ctime() является итеративной по своей природе. Это означает, что она (при любых практических целях) никогда не прерывает свою работу в 64-разрядных системах даже для астрономических дат (вроде 64-битового времени начала и завершения). Если вы устали ждать, когда же программа завершит свою работу, нажмите Control-C для ее завершения.

 

18.2. Использование таймеров

 

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

Существуют два способа применения таймеров: синхронный и асинхронный. Синхронное использование таймера возможно в единственном режиме — режиме ожидания (дожидаться истечения времени таймера). Асинхронная работа таймера, как и любого другого асинхронного устройства, сопровождается сигналами. Сюрпризом может оказаться то, что синхронный таймер может также вызывать сигналы.

 

18.2.1. Режим ожидания

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

unsigned int sleep(unsigned int seconds);

Функция sleep() вынуждает текущий процесс засыпать на время (в секундах), указанное параметром seconds, или до тех пор, пока процесс не получит сигнал, который он не может проигнорировать. На большинстве платформ функция sleep() реализуется в терминах сигнала SIGALRM, поэтому она не очень хорошо совмещается с системным вызовом alarm(), созданием обработчика SIGALRM, игнорированием сигнала SIGALRM, или применением интервальных таймеров (рассматриваются далее), которые разделяют один и тот же таймер и сигнал.

Если работа sleep() завершается раньше истечения полного выделенного времени, она возвращает количество оставшихся секунд. Если режим ожидания длился ровно столько, сколько запрашивалось, она возвращает ноль.

void usleep(unsigned long usec);

Функция usleep() вынуждает текущий процесс засыпать на время (в микросекундах), указанное параметром usec. Никакие сигналы не используются. На большинстве платформ usleep() реализуется с помощью select().

int select(0, NULL, NULL, NULL, struct timeval tv);

Функция select(), описанная в главе 13, предлагает мобильный способ откладывания процессов на точное количество времени. Просто введите в объект struct timeval минимальное время, которое нужно ожидать, и можете быть уверены — ни одно событие не произойдет.

int nanosleep(struct timespec *req, struct timespec *rem);

Функция nanosleep() вынуждает текущий процесс засыпать на время, указанное параметром req (описание объекта timespec можно найти в начале этой главы), пока процесс не получит сигнал. Если работа nanosleep() прекращается раньше из-за полученного сигнала, то она возвращает -1 и устанавливает для errno значение EINTR, а также, если rem не является NULL, то передает в переменную rem количество времени, оставшегося в периоде ожидания.

Функция nanosleep() наименее переносима из всех рассмотренных, поскольку она была определена как часть спецификации POSIX.1b реального времени (ранее она называлась POSIX.4), которая выполняется не во всех версиях Unix. Однако все новые реализации Unix поддерживают ее, так как функции POSIX.1b в настоящее время являются стандартной частью Single Unix Specification (Единая спецификация Unix).

Не все платформы, предусматривающие функцию nanosleep(), обеспечивают высокую точность, однако Linux, как и остальные операционные системы реального времени, стремится принимать короткие запросы на обработку с предельной точностью. Более подробную информацию о программировании в режиме реального времени можно найти в [12].

 

18.2.2. Интервальные таймеры

Интервальные таймеры, будучи активизированными, непрерывно передают сигналы в процесс на систематической основе. Точное значение термина систематический зависит от используемого интервального таймера. С каждым процессом ассоциированы три таймера.

ITIMER_REAL Отслеживает время в терминах настенных часов — в реальном времени (в зависимости от выполнения процесса) — и генерирует сигнал SIGALRM. Несовместим с системным вызовом alarm() , который используется функцией sleep() . Не применяйте ни alarm() , ни sleep() , если имеется реальный интервальный таймер.
ITIMER_VIRTUAL Подсчитывает время только при исполнении процесса — не учитывая системные вызовы, которые производит процесс — и генерирует сигнал SIGVTALRM .
ITIMER_PROF Подсчитывает время только при выполнении процесса — включая время, за которое ядро посылает исполнительные системные вызовы от имени процесса, и не включая время, потраченное на прерывание процесса по инициативе самого процесса — и генерирует сигнал SIGPROF . Учет времени, затраченного на обработку прерываний, оказывается настолько трудоемким, что даже может изменить настройки таймера.

Комбинация таймеров ITIMER_VIRTUAL и ITIMER_PROF часто используется в профилирующих кодах.

Каждый из этих таймеров генерирует ассоциированный сигнал об истечении таймера в пределах одного хода системных часов (как правило, 1-10 миллисекунд). Если процесс работает в данное время, то сигнал генерируется сразу же; в противном случае сигнал генерируется немного позже (в зависимости от загрузки системы). Поскольку таймер ITIMER_VIRTUAL следит за временем только во время работы процесса, то сигнал всегда доставляется незамедлительно.

Используйте структуру struct itimerval для передачи запроса и установки интервальных таймеров.

struct itimerval {

 struct timeval it_interval;

 struct timeval it_value;

};

Член it_value показывает количество времени, оставшееся до отправления следующего сигнала. Член it_interval определяет время между сигналами; каждый раз при истечении таймера это значение присваивается переменной it_value.

Для взаимодействия с интервальными таймерами предусмотрены два системных вызова. Оба принимают аргумент which, указывающий обрабатываемый таймер.

int getitimer(int which, struct itimerval *val);

Переменной val присваивается текущее состояние таймера which.

int setitimer(int which, struct itimerval *new, struct itimerval *old);

Устанавливает таймер which на значение new и заменяет old предыдущей установкой, если она не равна NULL.

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

В следующем примере родительский процесс активизирует дочерний процесс, запускает односекундный таймер ITIMER_REAL, засыпает на 10 секунд, а затем уничтожает дочерний процесс.

 1: /* itimer.c */

 2:

 3: #include

 4: #include

 5: #include

 6: #include

 7: #include

 8: #include

 9: #include

10:

11:

12: void catch_signal(int ignored) {

13:  static int iteration=0;

14:

15:  printf("получен сигнал интервального таймера, итерация %d\n",

16:   iteration++);

17: }

18:

19: pid_t start_timer(int interval) {

20:  pid_t child;

21:  struct itimerval it;

22:  struct sigaction sa;

23:

24:  if (!(child = fork())) {

25:   memset(&sa, 0, sizeof(sa));

26:   sa.sa_handler = catch_signal;

27:   sigemptyset(&sa.sa_mask);

28:   sa.sa_flags = SA_RESTART;

29:

30:   sigaction(SIGALRM, &sa, NULL);

31:

32:   memset(&it, 0, sizeof(it));

33:   it.it_interval.tv_sec = interval;

34:   it.it_value.tv_sec = interval;

35:   setitimer(ITIMER_REAL, &it, NULL);

36:

37:   while (1) pause();

38:  }

39:

40:  return child;

41: }

42:

43: void stop_timer(pid_t child) {

44:  kill(child, SIGTERM);

45: }

46:

47: int main (int argc, const char **argv) {

48:  pid_t timer = 0;

49:

50:  printf("Демонстрация интервальных таймеров для 10 секунд, "

51:   "ожидайте...\n");

52:  timer = start_timer(1);

53:  sleep(10);

54:  stop_timer(timer);

55:  printf("Готово.\n");

56:

57:  return 0;

58: }

 

Глава 19

Случайные числа

 

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

Однако некоторые приложения (включая криптографические) для достижения наилучших результатов требуют использования действительно случайных чисел. Ядро Linux для предоставления криптографически устойчивых случайных чисел производит выборку событий из непредсказуемого внешнего мира.

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

 

19.1. Псевдослучайные числа

В некоторых ситуациях все же требуется обеспечить невозможность прогнозирования. Библиотека С содержит функции для генерирования ожидаемых последовательностей псевдослучайных чисел. Эти функции легки в применении и являются одинаковыми на всех платформах Unix. Рассмотрим пример типичного использования данных функций.

#include

#include ...

srand(time(NULL) +getpid());

for (...;...;...) {

 do_something(rand());

}

Общепринято в качестве начального значения для генератора псевдослучайных чисел задавать текущую дату в формате, возвращаемом функцией time(). Последняя возвращает количество секунд, прошедших с 1 января 1970 года, поэтому начальное значение изменяется каждую секунду. Таким образом, оно может считаться уникальным в течение достаточно длинного интервала времени (приблизительно 49 710 дней на 32-разрядном компьютере). Если необходимо предотвратить возможность одинаковой активизации программы для двух пользователей, которые запускают ее в одну и ту же секунду, добавьте в начальном значении ко времени идентификатор текущего процесса.

Числа, возвращаемые функцией rand(), удовлетворяют математическим свойствам случайного распределения, но не обладают высокой энтропией (уровнем неупорядоченности). Для достаточно больших выборок они хорошо распределены в пределах всевозможных 32-битовых чисел, однако для одного и того же начального значения можно получить различные наборы чисел. Это означает, что такие псевдослучайные числа пригодны почти для всех приложений, требующих случайного распределения чисел. К таким приложениям относятся игры, методы Монте-Карло (здесь важно сохранить начальное значение, чтобы любой желающий мог проверить ваши результаты), а также протоколы, которые обрабатывают коллизии путем ввода случайной задержки.

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

 

19.2. Криптография и случайные числа

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

• Убедить тех программистов, которые не являются специалистами в шифровании, оставить работу в этой области экспертам.

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

Если вы недостаточно хорошо знакомы с криптографией, однако вынуждены ее применять, мы рекомендуем [30] в качестве превосходного вводного руководства по этой теме.

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

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

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

Поскольку компьютеры не оснащены подобными устройствами, в системе Linux предлагается выход из ситуации. Тед Тсо (Ted Ts'o) написал код, который измеряет временные диаграммы внешних событий (щелчок мыши, нажатие кнопки клавиатуры и так далее), извлекает из них информацию и сохраняет ее в пуле энтропии. Некоторые компоненты человеческого (и другого внешнего) взаимодействия с компьютером являются в высшей степени случайными. Код, заполняющий пул энтропии, старается некоторым образом охарактеризовать величину добавленной энтропии, что позволяет программисту оценить степень неупорядоченности при генерировании случайной информации. В последнее время во многих компьютерах предусматриваются аппаратные источники криптографически случайных данных. В Linux такие случайные данные подаются в системном пуле энтропии, поэтому все программы Linux могут применять один и тот же интерфейс в зависимости от оборудования, которое они используют.

Если программисту требуются случайные числа, основанные на непредсказуемых событиях, он может воспользоваться пулом энтропии с помощью одного из двух похожих устройств: /dev/random и /dev/urandom. Устройство /dev/random возвращает только то количество байт случайных данных, которое находится в пуле по текущей оценке самого устройства. Устройство /dev/urandom не предоставляет никаких гарантий касательно уровня неупорядоченности возвращаемой информации; оно генерирует на основе пула столько случайных данных, сколько вам нужно. Какое бы устройство не использовалось, оно уменьшает счетчик энтропии на количество прочитанных байтов.

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

• Не используйте ни /dev/random, ни /dev/urandom для тех данных, которые потребуется продублировать. Они также являются крайне неподходящими источниками данных для методов Монте-Карло. Даже последовательность 1, 2, …, n–1, n для них более приемлема; ее, по крайней мере, можно воспроизвести.

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

Исходный код драйвера случайных чисел, drivers/char/random.с, включает в себя важную информацию о технических деталях. Если вы планируете создание криптографической программы на основе данных, предоставляемых одним из описанных интерфейсов, настоятельно рекомендуем сначала изучить всю документацию.

 

Глава 20

Программирование виртуальных консолей

 

Интерфейс программирования виртуальных консолей Linux основан на интерфейсе, который предоставляют некоторые версии UNIX. Это не полная реализация (хотя ее достаточно для совместимости исходного кода почти со всеми программами), но в ней также предусмотрено несколько важных дополнений.

Система Linux может переключать несколько сеансов работы с терминалом на одном экране и одной клавиатуре. Специальные клавиатурные последовательности позволяют пользователю определять, какой сеанс отображается в данный момент. Каждый из этих сеансов входа в систему имеет собственные установки клавиатуры (подобные тем, которые работают при нажатии или отключении клавиши ), установки терминала (размеры, графический режим экрана, шрифты и так далее), компоненты устройств (такие как /dev/tty1, /dev/vcs1 и /dev/vcsa1).

Клавиатурные и терминальные установки вместе называются виртуальными консолями (virtual console — VC). Это название объясняется их схожестью с виртуальной памятью, в которой система использует дисковое пространство для предоставления большего объема доступной памяти, чем физически установлено в компьютере.

Если вы не собираетесь управлять или обрабатывать VC, то можете пропустить эту главу. Работу с VC могут выполнять вместо вас несколько программных библиотек. При этом вам, возможно, захочется узнать, что они делают "за кулисами", чтобы вы могли работать с ними, а не против них.

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

Виртуальные консоли предлагают пользователям множество опций, однако многие пренебрегают ними и просто используют X Window System. Те пользователи, которые предпочитают применять VC, имеют перечисленные ниже возможности.

• Выбор отдельного шрифта для каждой VC.

• Выбор индивидуального размера терминала в каждой VC.

• Выбор соответствий ключа (подробнее об этом далее) для всех VC.

• Выбор различных клавиатурных кодировок для всех VC.

• Переключение виртуальных консолей при помощи особых нажатий клавиш, установленных пользователем.

Проект документации Linux (Linux Documentation Project — LDP) предлагает документацию, объясняющую, как использовать существующие программы, извлекая все преимущества описанных возможностей. Перед вами стоит другая цель — вы хотите программировать для VC, а не просто их использовать. В утилитах хорошо инкапсулированы установки шрифтов и клавиатуры, поэтому вы можете просто вызывать их из своих программ. Однако встречаются ситуации, в которых такие внешние программы бесполезны.

 

20.1. Начало работы

Ниже приведен список тех действий, которые вы можете производить над VC. Некоторые из них относятся только к отдельной виртуальной консоли (как правило, к той, которая активна в данный момент); некоторые используются для всех работающих VC.

• Найти текущую VC.

• Инициировать переключение VC.

• Отклонить или принять переключение VC.

• Полностью запретить переключение VC.

• Найти неиспользуемую VC.

• Динамически назначить или освободить память VC в ядре.

• Генерировать простые звуки.

Во всех случаях необходима одна и та же подготовительная работа. Вы будете применять команды ioctl() на /dev/tty — поэтому нужно начать с включения заголовочных файлов, которые определяют аргументы ioctl().

#include

#include

#include

#include

#include

После этого нужно открыть /dev/tty.

if ((fd = open("/dev/tty", O_RDWR)) < 0) {

 perror("myapp: не удается открыть /dev/tty");

 exit(1);

}

Если вы обнаруживаете, что не можете открыть /dev/tty, то, возможно, у вас проблемы с полномочиями: устройство /dev/tty должно быть доступно для чтения и записи всем без исключения.

Обратите внимание на то, что в качестве дополнения к ioctl.h существуют два главных заголовочных файла, в которых определены вызовы ioctl(), обрабатывающие VC. В файле vt.h определяются вызовы, начинающиеся с букв VT, и реализуется управление виртуальным терминалом (экраном), как частью виртуальных консолей. В файле kd.h определены вызовы, которые начинаются с KD и обрабатывают клавиатуру и шрифты. Почти все содержимое kd.h можно проигнорировать, поскольку эти функциональные возможности прекрасно инкапсулируются в утилитах. Однако оно окажется весьма полезным при выдаче звуковых сигналов консолью на управляемых частотах.

Данные основные заголовочные файлы также определяют структуры, которые используются с ioctl().

Структура vt_mode применяется для поиска и изменения текущей VC:

struct vt_mode {

 char mode;

 char waitv;

 short relsig;

 short acqsig;

 short frsig;

};

• Переменная mode принимает одно из двух значений: VT_AUTO (вынуждает ядро автоматически переключать консоли во время нажатия клавиш или при получении запроса от программы на переключение VC) или VT_PROCESS (предписывает ядру запрашивать подтверждение прежде чем переключать консоли).

• Переменная waitv не используется, однако для совместимости с SVR4 ей нужно присвоить значение 1.

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

• Переменная acqsig именует сигнал, который должно сгенерировать ядро для извещения процесса о том, что он получает VC.

• Переменная frsig не используется, однако для совместимости с SVR4 ей нужно присвоить значение 0.

struct vt_stat {

 unsigned short v_active;

 unsigned short v_signal;

 unsigned short v_state;

};

• Переменная v_active хранит количество VC, активных в данный момент.

• Переменная v_signal не реализована.

• Переменная v_state хранит битовую маску, сообщающую, какие из первых 16 VC открыты в данный момент (в системе Linux поддерживается до 63 VC). В системе Linux редко появляется смысл консультироваться с данной маской, поскольку она недостаточно велика, чтобы содержать полную информацию. В большинстве случаев вам понадобится знать только номера ряда открытых консолей, которые вы можете извлечь с помощью функции VT_OPENQRY (рассматривается далее в этой главе).

 

20.2. Выдача звукового сигнала

Заставить консоль генерировать звуковой сигнал в течение определенного периода времени на указанной частоте совсем не сложное дело. Для этого существуют два способа. Первый состоит во включении или отключении постоянной тональной посылки. Команда KIOCSOUND отключает звук, если ее аргумент равен нулю. В противном случае она устанавливает частоту звука (причем довольно необычным способом, как показано в следующем коде).

void turn_tone_on(int fd, int hertz) {

 ioctl(fd, KIOCSOUND, 1193180/hertz)

}

void turn_tone_off(int fd) {

 ioctl(fd, KIOCSOUND, 0)

}

Вторым вариантом для выдачи консолью звукового сигнала является применение команды управления вводом-выводом KDMKTONE. Она включает тональную посылку на время, указанное в тиках системных часов (jiffy). К сожалению, время одного тика в различных архитектурах разное. Макрос HZ, определенный sys/param.h, позволяет получить количество тиков в секунду. Функция tone(), показанная ниже, демонстрирует, как извлекать количество тиков в сотых долях секунды и значение макроса HZ.

#include

void tone(int fd, int hertz, int hundredths) {

 unsigned int ticks = hundredths * HZ / 100;

 /* ticks & 0xffff не будет работать, если ticks — 0xf0000;

  * вместо этого нужно округлить до наибольшего допустимого значения */

 if (ticks > 0xffff) ticks = 0xffff;

 /* еще одна ошибка округления */

 if (hundredths && ticks == 0) ticks = 1;

 ioctl(fd, KDMKTONE, (ticks << 16 | (1193180/hertz)));

}

 

20.3. Определение, является ли терминал виртуальной консолью

Для того чтобы определить, является ли текущий терминал виртуальной консолью, можно открыть /dev/tty и применить VT_GETMODE для запроса режима:

struct vt_mode vtmode;

fd = open("/dev/tty", O_RDWR);

retval = ioctl (fd, VT_GETMODE, &vtmode);

if (retval < 0) {

 /* Данный терминал не является VC; выполните соответствующие действия */

}

 

20.4. Поиск текущей виртуальной консоли

Для извлечения номера текущей VC применяется команда управления вводом-выводом VT_GETSTATE, которая принимает указатель на структуру struct vt_stat и возвращает номер текущей консоли в ее элементе v_active.

unsigned short get_current_vc(int fd) {

 struct vt_stat vs;

 ioctl(fd, VT_GETSTATE, &vs);

 return(vs.v_active);

}

Для локализации соответствующего элемента устройства для текущей VC служит следующая функция:

sprintf(ttyname, "/dev/tty%d", get_current_vc(fd));

 

20.5. Управление переключением виртуальных консолей

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

retcode = ioctl(fd, VT_OPENQRY, &vtnum);

if ((retcode < 0) || (vtnum == -1)) {

 perror("myapp: нет доступных виртуальных терминалов");

 /* выполнить соответствующее действие */

}

Если в настоящее время используется менее 63 VC, и все из них заняты, то ядро автоматически выделяет память для новой VC.

Для запуска переключения на другую VC (например, на ту свободную консоль, которую вы только что обнаружили) используется команда управления вводом-выводом VT_ACTIVATE. Если нужно подождать до тех пор, пока консоль не станет активной, применяется команда VT_WAITACTIVE. Смена консоли может занять некоторое время, возможно, несколько секунд. Это объясняется тем, что активизируемая консоль может находиться в графическом режиме, при этом содержимое экрана нужно реконструировать из памяти, выбрать из буфера обмена или восстановить каким-то другим способом, отнимающим немало времени.

ioctl(fd, VT_ACТIVATE, vtnum);

ioctl(fd, VT_WAITACTIVE, vtnum);

Для осуществления контроля над переключениями VC или для получения уведомлений о подобных переключениях необходимо предусмотреть надежные обработчики сигналов с sigaction, как обсуждалось в главе 12. Здесь мы применяем SIGUSR1 и SIGUSR2; если нужно, вы можете использовать любые другие два сигнала, которые не предназначены для других целей (например, SIGPROF и SIGURG). Просто убедитесь, что выбранные вами сигналы удовлетворяют перечисленным ниже критериям.

• Они не требуются остальным системным функциям, особенно это касается тех сигналов, которые не могут быть перехвачены или проигнорированы.

• Они нигде не используются в вашем приложении для других целей.

• Они не представляют один и тот же сигнальный номер с двумя различными именами, как SIGPOLL и SIGIO (определения смотрите в /usr/include/asm/signal.h, либо ограничьте себя использованием сигналов из табл. 12.1).

void relsig(int signo) {

 /* выполнить соответствующее действие для освобождения VC */

}

void acqsig(int signo) {

 /* выполнить соответствующее действие для запроса VC */

}

void setup_signals(void) { struct sigaction sact;

 /* He маскировать никаких сигналов в то время,

  * когда активизированы данные обработчики. */

 sigemptyset(&sact.sa_mask);

 /* Здесь может понадобиться добавление вызовов sigaddset(),

  * если существуют сигналы, которые нужно маскировать

  * при переключении VC. */

 sact.flags = 0;

 sact.sa_handler = relsig;

 sigaction(SIGUSR1, &sact, NULL);

 sact.sa_handler = acqsig;

 sigaction(SIGUSR2, &sact, NULL);

}

После этого потребуется изменить стандартный режим VC (mode) с VT_AUTO на VT_PROGRESS, пока консоль уведомляется об обработчиках сигналов путем установки relsig и acqsig.

void control_vc_switching(int fd) {

 struct vt_mode vtmode;

 vtmode.mode = VT_PROCESS;

 vtmode.waitv = 1;

 vtmode.relsig = SIGUSR1;

 vtmode.acqsig = SIGUSR2;

 vtmode.frsig = 0;

 ioctl(fd, VT_SETMODE, &vtmode);

}

Обработчики сигналов, которые вызываются тогда, когда консоль находится в режиме VT_PROCESS, не должны соглашаться на переключение. Говоря более точно, обработчик relsig может отклонить разрешение на переключение VC. Обработчик acqsig, как правило, управляет процессом передачи консоли, но существует вероятность того, что он инициирует переключение на другую консоль. Будьте внимательны при кодировании обработчиков сигналов, чтобы избежать вызова любых нереентерабельных библиотечных функций. POSIX.1 устанавливает, что функции, перечисленные в табл. 12.2, являются реентерабельными. Значит, вы должны считать все остальные функции нереентерабельными, особенно, если хотите написать переносимую программу. Обратите внимание, в частности, на то, что malloc() и printf() являются нереентерабельными.

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

void relsig (int signo) {

 if (change_vc_ok()) {

  /* Разрешено переключение VC */

  save_state();

  ioctl(fd, VT_RELDISP, 1);

 } else {

  /* Запрещено переключение VC */

  ioctl(fd, VT_RELDISP, 0);

 }

}

void acqsig (int signo) {

 restore_state();

 ioctl(fd, VT_RELDISP, VT_ACKACQ);

}

Теперь вы в состоянии реализовать код функций change_vc_ok(), save_state() и restore_state().

Динамическое выделение памяти под виртуальные консоли происходит при их открытии, однако, они не удаляются автоматически, когда закрываются. Для того чтобы освободить память ядра, в которой сохранялось состояние VC, нужно вызвать ioctl().

ioctl(fd, VT_DISALLOCATE, vtnum);

Вы можете заблокировать и повторно открыть переключение VC с помощью нескольких простых команд управления вводом-выводом:

void disallow_vc_switch(int fd) {

 ioctl(fd, VT_LOCKSWITCH, 0);

}

void allow_vc_switch(int fd) {

 ioctl(fd, VT_UNLOCKSWITCH, 0);

}

 

20.6. Пример команды

open

Ниже показан пример кода, выполняющий следующие задачи: поиск неиспользуемой VC, запуск на ней оболочки, ожидание завершения оболочки, переключение обратно, а также освобождение памяти, выделенной под VC, по завершении программы. Программа open, входящая в состав дистрибутива Linux, выполняет те же самые действия, но при этом она содержит больше опций и лучший контроль ошибок, таким образом, является более надежной. Хотя приведенная ниже версия тоже будет работать, ее трудно назвать удобной, надежной или безопасной.

 1: /* minopen.c */

 2:

 3: #include

 4: #include

 5: #include

 6: #include

 7: #include

 8: #include

 9: #include

10: #include

11: #include

12: #include

13:

14: int main (int argc, const char ** argv) {

15:  int vtnum;

16:  int vtfd;

17:  struct vt_stat vtstat;

18:  char device[32];

19:  int child;

20:

21:  vtfd = open("/dev/tty", O_RDWR, 0);

22:  if (vtfd < 0) {

23:   perror("minopen: не удается открыть /dev/tty");

24:   exit(1);

25:  }

26:  if (ioctl(vtfd, VT_GETSTATE, &vtstat) < 0) {

27:   perror("minopen: tty не является виртуальной консолью");

28:   exit(1);

29:  }

30:  if (ioctl(vtfd, VT_OPENQRY, &vtnum) < 0) {

31:   perror("minopen: нет свободных виртуальных консолей");

32:   exit(1);

33:  }

34:  sprintf(device, "/dev/tty%d", vtnum);

35:  if (access(device, (W_OK|R_OK)) < 0) {

36:   perror("minopen: недостаточные полномочия на tty");

37:   exit(1);

38:  }

39:  child = fork();

40:  if (child == 0) {

41:   ioctl(vtfd, VT_ACTIVATE, vtnum);

42:   ioctl(vtfd, VT_WAITACTIVE, vtnum);

43:   setsid();

44:   close(0); close(1); close(2);

45:   close(vtfd);

46:   vtfd = open(device, O_RDWR, 0); dup(vtfd); dup(vtfd);

47:   execlp("/bin/bash", "bash", NULL);

48:  }

49:  wait(&child);

50:  ioctl(vtfd, VT_ACTIVATE, vtstat.v_active);

51:  ioctl(vtfd, VT_WAITACTIVE, vtstat.v_active);

52:  ioctl(vtfd, VT_DISALLOCATE, vtnum);

53:  exit(0);

54: }

 

Глава 21

Консоль Linux

 

Консоль Linux, как правило, имитирует последовательный терминал. Выводя специальные последовательности символов в компонент консоли, можно управлять всеми аспектами воспроизведения на экране. Для вывода информации на экран обычно применяются S-Lang, curses или ряд других библиотек рисования на экране; они используют упомянутые управляющие последовательности. Консоль можно также читать и модифицировать через альтернативный полноэкранный интерфейс, который особенно полезен для некоторых специализированных программ.

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

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

Кроме того, вы заметите, что управляющие коды являются рациональными низкоуровневыми интерфейсами для экрана. Они также служат хорошей основой для построения примитивов более высокого уровня вроде модуля curses, описанного в [36]. И это не просто случайность; последовательные терминалы являются весьма уважаемой технологией, которая годами превосходно отвечает фактическим потребностям программистов. Консоль Linux основана на наиболее популярном семействе последовательных терминалов, наследников DEC VT100.

Большинство управляющих последовательностей используют символ перехода (escape) ANSI, который не имеет печатного представления. Мы будем следовать библиотекам termcap и terminfo, в которых для обозначения символа перехода ANSI применяется ^[. Имейте в виду при чтении, что они иногда ссылаются на тот же самый символ, как на \Е. Всюду в данной книге, как и в termcap и terminfo, выражение ^C указывает на символ Control-C.

 

21.1. Базы данных возможностей

Действия, контролируемые заданными управляющими последовательностями, часто называются возможностями (capabilities). Некоторые управляющие последовательности совместно используются большим числом терминалов. Многие из таких последовательностей определены в стандарте ANSI X3.64-1979. Почти все терминалы с возможностями поддержки цвета используют одни и те же последовательности для выбора цветов изображения. При этом многие терминалы поддерживают совершенно различные управляющие последовательности. Например, на терминале Wyse 30 нажатие передает последовательность ^A@\r, тогда как на консоли Linux — последовательность ^[[[А. Аналогично, для того, чтобы переместить курсор вверх на Wyse 30, нужно послать символ ^K, а на консоли Linux — последовательность ^[[А. Для создания программы, которая может работать на любом терминале, вам крайне необходим метод абстрагирования подобных различий, который позволит программировать возможности, а не необработанные последовательности символов.

Наиболее известная библиотека программирования, которая предоставляет такую абстракцию, называется curses (предписания); она описана в [36]. В нее входит два важных концептуальных уровня. Первый уровень предлагает функции, которые (говоря в общем) посылают одну управляющую последовательность для выполнения одного действия, такого как перемещение курсора вверх или вниз, прокрутка экрана и так далее. Второй уровень реализует концепцию "окон": независимые области окна, которые могут обрабатываться отдельно, при этом curses обеспечивает кратчайшие последовательности символов для достижения требуемого эффекта.

Библиотека curses определяет, какие последовательности передавать в терминал, консультируясь с базой данных, которая для каждого терминала ставит в соответствие названия возможностей строкам, которые должны пересылаться.

Linux, как все современные системы Unix, предлагает две базы данных, которые описывают терминалы в смысле их возможностей и соответствующих им управляющих последовательностей. Более старая база данных называется termcap (сокращение от terminal capabilities — терминальные возможности) и хранится в одном большом двумерном ASCII-файле по имени /etc/termcap. Этот файл постепенно стал очень громоздким; его размер вырос приблизительно до половины мегабайта. Более новая база данных называется terminfo (сокращение от terminal information — терминальная информация) и хранится во множестве бинарных файлов (по одному на терминал), как правило, в подкаталогах каталога /usr/lib/terminfo.

Внутри каждой базы данных информация о возможностях для каждого терминала индексируется одним (или более) уникальным именем. Обе базы данных используют одно и то же имя для одного и того же сериала. Например, консоль Linux называется linux и в termcap, и в terminfo. Вы сообщаете программам, какой терминальный вход использовать, путем установки переменной окружения TERM. При создании программ, использующих termcap и terminfo, вам редко придется обращаться к переменной TERM; обычно это берут на себя низкоуровневые библиотеки для получения доступа к базам данных termcap или terminfo.

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

if (!strcmp("linux", getenv("TERM"))) {

 /* должна быть консоль Linux */

} else {

 /* обрабатывать как обычный последовательный терминал */

}

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

Разумеется, то, что ваш терминал имеет тип linux, не является гарантией того, что программа работает на локальном терминале. Это означает только то, что у вас есть доступ к управляющим последовательностям, описанным в данной главе, при этом вы не знаете, можете ли вы использовать устройства vcs (о них позже в этой главе) или ioctl(). Стандарт POSIX определяет функцию ttyname(), которую вы можете применить для извлечения имени файла устройства для управляющего терминала. В системе Linux виртуальные консоли называются /dev/tty n , где n принимает значения от 1 до 63 (/dev/tty0 — всегда текущая консоль).

Полное описание систем termcap и terminfo можно найти в [37]. В настоящий момент базы данных termcap и terminfo поддерживает Эрик Раймонд (Eric Raymond), и они доступны по адресу http://www.ccil.org/~esr/terminfo/.

Исходный код ncurses (новая библиотека curses — реализация curses, используемая в Linux) включает в себя введение в программирование с применением curses (файл misc/ncurses-intro.html).

 

21.2. Глифы, символы и отображения

Когда вы выводите символ на любой терминал, могут произойти несколько шагов преобразования. Значение, выводимое на терминал, представляет собой номер символа, или его код. Однако такого кода символа недостаточно для определения того, что нужно изобразить на экране. Форма зависит от используемого шрифта. Символьный код 97 может быть распечатан как а (в шрифте, предназначенном для визуализации латиницы) или как ∂ (в шрифте для визуализации греческого алфавита или математических знаков). Изображаемая на экране форма называется глифом (glyph). Преобразование символьных кодов в глифы называется отображением.

 

21.3. Возможности консоли Linux

 

Консоль Linux, как и большинство терминалов, является модальной: ее взаимодействие с данными зависит от того, в каком режиме она находится. По умолчанию она выводит на экран передаваемые символы, пока не получает управляющий символ или символ перехода. Управляющий символ просто вызывает некоторое регулирующее действие, а следующий символ уже читается обычным образом. Изменений в режиме обработки не происходит. Символ перехода сигнализирует о начале управляющей последовательности и изменяет режим обработки на режим управления.

Например, рассмотрим следующую строку С:

"this is a line\na \033[1mbold\033[0m word\n"

Консоль обрабатывает эту строку в описанной ниже последовательности.

1. Начиная с текущей позиции курсора, консоль печатает слова this is a line.

2. Обнаруживается символ новой строки (\n), поэтому (так как Linux и Unix обычно работают в режиме, когда новая строка сигнализирует также о возврате каретки) консоль перемещает курсор в начало следующей строки. При этом если курсор уже был на самой нижней строке, весь экран прокручивается вверх на одну строку.

3. В начале данной линии отображается строка а.

4. Консоль сталкивается с символом перехода \033 и переводится в управляющий режим.

5. Считывается символ [, консоль переходит в режим ввода командной последовательности (Command Sequence Introduction — CSI).

6. В режиме CSI считывается последовательность десятичных чисел, закодированных в ASCII и разделенных знаками ;, которые называются параметрами. Это продолжается до тех пор, пока не встретится первая буква. Буква определяет действие, которое нужно предпринять, с учетом данных в параметрах. В данном случае имеется один параметр 1, а буква m означает, что параметр используется для определения изображения символа. Например, параметр 1 устанавливает атрибут полужирного шрифта.

7. Распечатывается строка bold в полужирном представлении.

8. Далее идет другая последовательность изображения символов, которая восстанавливает все стандартные атрибуты, поэтому строка word выводится в обычном изображении.

9. В завершение встречается и обрабатывается еще один символ новой строки.

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

this is a line

a bold word

 

21.3.1. Управляющие символы

Консоль считывает управляющие символы незамедлительно, действует в соответствии с ними и затем продолжает считывать символы в нормальном режиме.

В файлах и документации termcap и terminfo управляющие символы изображаются символом ^c. Мы будем часто в данной книге использовать это условное обозначение, поскольку оно более универсально и удобно для вас, чем восьмеричные управляющие последовательности С. Для отыскания числового значения управляющего символа в некоторых системах предусмотрен макрос CTRL() в , но он не является стандартным для всех систем.

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

#define CTRLCHAR(ch) ((ch)&0x1F)

Она используется так:

if (с == CTRLCHAR('С')) {

 /* был нажат символ Control-C */

}

Управляющие символы, воспринимаемые консолью Linux, описаны в табл. 21.1. Символ ^? фактически представляет собой '?'+0100, а не '?'-0100, поэтому это не настоящий управляющий знак вопроса, но в любом случае стандартное обозначение для него ^?. Его значение есть 0177 (восьмеричное), 127 (десятичное), 7F (шестнадцатеричное). Вы не сможете использовать макрос CTRL, описанный только что, для проверки. Вместо этого придется применять числовое значение 127.

Таблица 21.1. Символы управления консолью

Управляющий символ Имя ASCII Описание
^G BEL Выдает тональный сигнал.
BS Курсор перемещается к предыдущему символу, не перезаписывая его (если только курсор не находится в первой колонке).
^I НТ Горизонтальная табуляция; курсор перемещается к следующей точке табуляции.
^J LF Новая строка; курсор перемещается на следующую строку; если курсор уже находился в самой нижней точке области прокручивания, то она продвигается вверх.
^K VT Вертикальная табуляция; интерпретируется так же, как новая строка.
^L FF Подача страницы; интерпретируется так же, как новая строка.
CR Возврат каретки; курсор перемещается в начало текущей строки.
^N SO Сдвиг; используется альтернативный ( G1 ) символ, установленный для отображения глифов; изображаются глифы для управляющих символов.
^O SI Сдвиг; используется стандартный ( G0 ) символ, установленный для отображения глифов; не изображаются глифы для управляющих символов.
^X CAN Отменяется любая действующая управляющая последовательность.
^Z SUB Отменяется любая действующая управляющая последовательность.
^[ ESC ESCape; начало управляющей последовательности.
^? DEL Игнорируется.
ALT-^[ - Вводится последовательность команд, которая будет описана далее.

Обратите внимание на то, что результат некоторых из данных кодов зависит от настроек tty. Хотя сама консоль описана здесь абсолютно точно, настройки tty могут изменять передаваемые символы. Например, передача ^J (LF) обычно вынуждает уровень tty также передавать ^M (CR), а символ ^? (DEL) может быть настроен на передачу ^Н (BS).

Символ ALT-^[ вообще не является символом ASCII. Это восьмибитовый символ ESC, тогда как ASCII определяет только семибитовые символы. Вы можете применять этот символ в качестве комбинации быстрого вызова для ввода последовательности CSI. Однако мы рекомендуем избегать этого, так как при этом понадобится чистый восьмибитовый канал связи, который может помешать удаленной работе вашей программы на другой подключенной системе Linux, возможно, из-за последовательного канала, передающего только семь битов из каждого байта.

Для получения более подробной информации о символах ASCII обратитесь к man-странице ascii(7). Кроме того, на man-странице iso_8859_1(7) рассматривается набор восьмибитовых знаков ISO Latin 1 (точнее говоря, ISO 8859 Latin Alphabet number 1); этот более новый стандарт стал фактической заменой ASCII и сейчас официально называется ISO 646-IRV.

 

21.3.2. Управляющие последовательности

Существуют несколько отдельных типов управляющих последовательностей. Самый простой тип представляет собой символ перехода (^[), за которым следует один командный символ. (Несмотря на то что символ перехода отображается в строках С как \033, в файлах и документации по termcap и terminfo принято обозначение ^[.) Пять из таких односимвольных команд предваряют более сложные управляющие последовательности, которые называются командными последовательностями. Остальные побуждают консоль предпринимать простые действия и немедленно покидать режим перехода. Простейшие управляющие последовательности описаны в табл. 21.2.

Таблица 21.2. Последовательности управления консолью

Управляющие последовательности Описание
^[М Курсор перемещается вверх на одну строку в текущей колонке; если необходимо, то экран прокручивается вниз (обратный перевод строки).
^[D Курсор перемещается вниз на одну строку в текущей колонке; если необходимо, то экран прокручивается вверх (перевод строки)
^[E Возврат каретки и перевод строки.
^[Н Точка табуляции устанавливается в текущей колонке.
^[7 Сохраняются позиция и атрибуты курсора.
^[8 Восстанавливаются позиция и атрибуты курсора.
^[> Переводит малую клавиатуру в числовой режим (стандартный).
^[= Переводит малую клавиатуру в режим приложения (она ведет себя как функциональные клавиши DEC VT102).
^[с Сбрасывает все терминальные установки, которые могут быть получены через управляющие символы и последовательности.
^[Z Запрашивается идентификатор терминала. Ответом будет ^[[?6с ; это говорит о том, что консоль точно эмулирует DEC VT102 (она включает в себя расширенный набор возможностей DEC VT102).

Сохранение и восстановление позиции курсора (^[7 и ^[8) не осуществляется в стеке. Если вы делаете два сохранения в одной строке, то вторая сохраняемая позиция перезаписывает первую. Наоборот, один раз сохранив позицию курсора, вы можете восстанавливать ее столько раз, сколько нужно. Всякий раз курсор будет возвращаться в одно и то же расположение. При восстановлении положения курсора также восстанавливаются атрибуты изображения курсора, текущий набор символов, описания набора символов (все это будет описываться далее в данной главе).

Позиция курсора задается в показателях адреса знакоместа, парой чисел x,y, которая обозначает одну позицию на экране. Нумерация адресов знакомест на большинстве терминалов, включая консоль Linux, не начинается с нуля, как это принято в обычной компьютерной практике. Верхний левый символ на экране является началом отсчета и получает адрес как знакоместо 1,1.

Обратите внимание на то, что управляющие символы могут включаться внутри управляющей последовательности. Например, ^[^G8 сначала выдает тональный сигнал, а затем восстанавливает позицию и атрибуты курсора. Последовательность ^[^X8 просто печатает число 8.

 

21.3.3. Тестирование последовательностей

Для проверки большинства последовательностей вам нужно просто войти в виртуальную консоль и запустить cat. Введите последовательности, которые вы хотите протестировать, и увидите результаты. Для ^[ нажмите клавишу .

Терминалы отвечают на команды вроде идентификации терминала ^[Z или команды CSIn, рассматриваемые позже, управляющим последовательностями, которые на терминалах не отображаются. В тех случаях, когда вы хотите увидеть подобное взаимодействие, просто запустите

cat > /tmp/somefile

Затем введите команды, после которых укажите возврат каретки и ^D. Используйте less, vi, Emacs или какую-то другую программу, которая может обрабатывать произвольные символы для того, чтобы прочитать /tmp/somefile, где непосредственно после напечатанных вами последовательностей вы найдете ответы на них.

 

21.3.4. Составные управляющие последовательности

Пять двухсимвольных управляющих последовательностей (которые показаны в табл. 21.3) фактически являются префиксами более длинных и сложных последовательностей. Рассмотрим каждую из них по очереди.

Таблица 21.3. Составные последовательность управления консолью

Управляющие последовательности Описание
^[[ Начинается последовательность CSI ( ALT-^[ является синонимом).
^[] Начинается последовательность управления палитрой.
^[% Начинается последовательность UTF (UTF-8 wide-character Unicode).
^[( Выбирается шрифт, соответствующий набору символов G0 .
^[) Выбирается шрифт, соответствующий набору символов G1 .
^[#8 Внутренняя тестовая последовательность DEC; заполняет экран символами Е.

Последовательности CSI имеют три или четыре части.

1. ^[[ запускает последовательность CSI, переводя терминал в режим CSI.

2. Только для последовательностей h и l вы можете добавлять символ ?, что позволит устанавливать или очищать собственные режимы DEC (см. табл. 21.9).

3. Предусматривается не более чем 16 параметров. Параметры — это десятичные числа, разделенные символами ;. Например, 1;23;45 представляет собой список из трех параметров: 1, 23 и 45. (Если после прочтения 16 параметров обнаруживается разделитель ;, то последовательность CSI немедленно прерывает работу и терминал переходит в нормальный режим, распечатывая оставшуюся часть последовательности).

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

На параметры обычно ссылаются как на некоторые переменные от par1 до par16. Если вы не установили параметр явно, то его значение автоматически приравнивается к нулю или единице, в зависимости от смысла операции. Командные символы CSI перечислены в табл. 21.4.

Таблица 21.4. Последовательности CSI

Символ Описание
h Устанавливает режим; см. табл. 21.8.
l Очищает режим; см. табл. 21.8.
n par1 =5 Отчет о состоянии: терминал отвечает ^[[0n , что означает "OK" par1 =6 Отчет о положении курсора: терминал отвечает ^[[x;yR , где у указывается относительно начала отсчета, а не области (если выбран режим начала отсчета, смотрите табл. 21.9)
G или ` Устанавливает горизонтальное положение курсора в колонке par1 .
A Передвигает вертикальную позицию курсора вверх на par1 строк.
В или e Передвигает вертикальную позицию курсора вниз на par1 строк.
С или a Передвигает горизонтальную позицию курсора вправо на par1 колонок.
D Передвигает горизонтальную позицию курсора влево на par1 колонок.
E Передвигает курсор в начало линии и ниже на par1 строк (1 по умолчанию).
F Передвигает курсор в начало линии и выше на par1 строк (1 по умолчанию).
d Устанавливает вертикальное положение курсора в строке par1 .
H или f Устанавливает вертикальное положение курсора в строке par1 и горизонтальное положение курсора в колонке par2 (по умолчанию оба параметра равны нулю, перемещая курсор в начало отсчета).
J par1 =0 Очищает экран от курсора до конца дисплея par1 =1 Очищает экран от начала отсчета до курсора par1 =2 Очищает экран полностью
K par1 =0 Очищает экран от курсора до конца строки par1 =1 Очищает экран от начала строки до курсора par1 =2 Очищает строку полностью
L Вставляет par1 строк ниже текущей строки.
М Удаляет par1 строк, начиная с текущей строки.
P Удаляет par1 символов, начиная с текущей позиции, передвигая остальную часть строки влево.
с Отвечает ^[[?6c (синоним ^[Z ).
g par1 =0 Удаляет точку табуляции в текущем столбце (по умолчанию) par1 =3 Удаляет все точки табуляции
m Последовательность изображения символов; смотрите табл. 21.7.
q Включает клавиатурный LED par1 и отключает остальные (0 выключает все).
r Устанавливает область прокручивания (применяется только в режиме начала отсчета DEC; см. табл. 21.9): par1 Первая строка области, должна находиться в пределах от 1 (по умолчанию) до par2 –1 par2 Последняя строка области, должна находиться в пределах от par1 +1 и нижней строкой (по умолчанию)
s Сохраняет позицию и атрибуты курсора (синоним ^[7 ).
u Восстанавливает позицию и атрибуты курсора (синоним ^[8 ).
X Стирает par1 символов (до конца текущей строки).
@ Стирает par1 символов (до конца текущей строки).
] Последовательности setterm; смотрите табл. 21.10.

Несколько последовательностей принимают аргументы, описывающие цвета. Во всех таких последовательностях используется одно и то же соответствие между числами и цветами, приведенное в табл. 21.5. Последовательности, которые указывают цвета фона, допускают номера цветов только от 0 до 7. Те последовательности, которые задают цвет переднего плана, принимают числа от 8 до 15 (они описывают насыщенные или яркие цвета).

Таблица 21.5. Коды цветов

Число Цвет Число Яркий цвет
0 Черный 8 Темно-серый
1 Красный 9 Светло-красный
2 Зеленый 10 Светло-зеленый
3 Коричневый 11 Желтый
4 Голубой 12 Светло-голубой
5 Пурпурный 13 Ярко-красный
6 Синий 14 Светло-синий
7 Серый 15 Белый

Указанные цвета фактически представляют собой смещения — названия цветов в таблице описывают стандартные цвета, которые хранятся по данным смещениям. Однако вы можете изменять эти цвета при помощи последовательности установки палитры. Например, последовательность ^[]P определяет отдельный компонент палитры; последовательность ^[]R восстанавливает стандартную системную палитру. Компоненты палитры определяются семью шестнадцатеричными цифрами, введенными после ^[]P, как описано в табл. 21.6. Таким образом, для каждого элемента палитры вы можете предоставить 24-битовое определение цвета с восемью битами для каждого цвета.

Таблица 21.6. Компоненты цветовой палитры

Число Что определяет
1 Элемент палитры, который нужно переопределить.
2*16+3 Значение красного компонента элемента палитры.
4*16+5 Значение зеленого компонента элемента палитры.
6*16+7 Значение синего компонента элемента палитры.

Последовательности изображения символов, указанные командами CSIm, могут принимать в произвольном порядке до 16 параметров, перечисленных в табл. 21.7. Параметры применяются к терминалу в том порядке, в котором они передаются. Таким образом, если 0 (установка стандартного изображения) сопровождается 1 (для установки полужирного шрифта), то результатом будет полужирный символ (а не мерцающее негативное видеоизображение) или подчеркнутый символ в зависимости от предыдущих установок изображения.

Таблица 21.7. Параметры изображения символов

par Описание
0 Стандартное изображение: средняя интенсивность, без подчеркивания, без негативного изображения, без мерцания, с обычной цветовой схемой (белое на черном, если не установлен другой способ при помощи последовательности сохранения setterm ^[[]8 ).
1 Интенсивность — насыщенная.
2 Интенсивность — матовая.
4 Включается подчеркивание.
5 Включается мерцание.
7 Включается негативное видеоизображение.
10 Выбирается исходный шрифт (ISO latin 1), при этом не отображаются управляющие символы, сбрасывает бит 8 в выводе.
11 Выбирается альтернативный шрифт (IBM Codepage 437), при этом управляющие символы отображаются как графические данные, сбрасывает бит 8 в выводе.
12 Выбирается альтернативный шрифт (IBM Codepage 437), при этом управляющие символы отображаются как графические данные, оставляет бит 8 в выводе.
21 22 Интенсивность — стандартная.
24 Отключается подчеркивание.
25 Отключается мерцание.
27 Отключается негативное видеоизображение.
30-37 Устанавливается цвет переднего плана par ||30; см. табл. 21.5.
38 Включается подчеркивание и используется стандартный цвет текста.
39 Отключается подчеркивание и используется стандартный цвет текста.
40-47 Устанавливается цвет фона par ||40; см. табл. 21.5.
49 Используется стандартный цвет фона.

Некоторое отношение к последовательностям изображения символов имеют последовательности режимов. Существует два типа режимов: режимы ANSI и внутренние режимы DEC. Последовательность СSIh устанавливает режимы ANSI, описанные в табл. 21.8; последовательность CSIl сбрасывает их. В последовательность может входить более одного параметра. Последовательность CSI?h определяет внутренние режимы DEC, перечисленные в табл. 21.9; последовательность CSI?l сбрасывает их. Также может приниматься более одного параметра.

Таблица 21.8. Режимы ANSI

par Описание
3 Отображаются управляющие символы.
4 Режим вставки.
20 Режим CRLF (при получении символа новой строки выполняется возврат каретки).

Таблица 21.9. Внутренние режимы DEC

par Описание
1 Клавиши управления курсором работают как клавиши приложения ; в режиме приложения к ним добавляется префикс ^[O вместо обычного ^[[ .
3 На данный момент не реализован; в будущем предназначен для переключения между режимами 80 и 132 колонки.
5 Весь экран переводится в режим негативного изображения.
6 Устанавливается режим начала отсчета DEC, при котором принимаются области прокрутки; перемещается в начало отсчета (текущей области прокрутки, если она задана).
7 Устанавливается режим автоматического перехода на новую строку (по умолчанию), при котором продолжается ввод текста с новой строки, когда курсор достигает конца текущей строки. Если данный режим выключен, то лишние символы печатаются поверх самого правого символа текущей строки.
8 Клавиатура переводится в режим повторения символов (включен по умолчанию).
9 Режим отчета мыши 1 (поддержка может предоставляться внешней программой).
25 Курсор становится видимым (включен по умолчанию).
1000 Режим отчета мыши 2 (поддержка может предоставляться внешней программой).

Последовательности setterm представляют собой набор последовательностей CSI с управляющим символом ]. Они перечислены в табл. 21.10.

Таблица 21.10. Консольные последовательности setterm

par Описание
1 Устанавливает цвет для представления атрибута подчеркивания параметра par2 .
2 Устанавливает цвет для представления атрибута тусклости параметра par2 .
8 Текущие атрибуты setterm сохраняются как значения по умолчанию, тем самым они становятся стандартными атрибутами изображения символов.
9 Устанавливает интервал гашения экрана на par2 минуты, но не более чем на 60 минут. Если параметр par2 равен нулю, то гашение экрана блокируется.
10 Частота звонковой сигнализации консоли приравнивается к par2 Гц или к стандартному шагу, если параметр par2 не определен.
11 Длительность звукового сигнала консоли приравнивается к par2 миллисекундам, если параметр par2 указан, но не более чем 2000. Если par2 не задан, то восстанавливается стандартная длительность.
12 Если для консоли par2 выделена память, то консоль par2 становится активной (см. главу 20).
13 Восстанавливает экран после гашения.
14 Интервал выключения питания VESA приравнивается к par2 минутам, но не более чем 60 минут. Если параметр par2 равен нулю, то отключение питания VESA блокируется.

Сообщение консоли того, что она должна отображать — далеко не все; вы также обязаны распознавать последовательности нажатия клавиш и знать, к каким клавишам они привязаны. Некоторые из этих последовательностей определены в базе данных terminfo, некоторые — нет. Кроме этого, клавиатура является модальной для увеличения разнообразия возможностей. В режиме приложения клавиши курсора порождают другие коды. Как показано в табл. 21.9, к ним добавляется префикс ^[О вместо ^[[. Это необходимо для поддержки унаследованных приложений, в которых предполагается, что они обращаются к терминалам DEC.

Последовательности нажатия клавиш описаны в табл. 21.11. Обратите внимание на то, что в нумерации функциональных клавиш имеются пропуски. Это спланировано для того, чтобы пользователи клавиатур, у которых нет клавиш и , не были ущемлены.

Таблица 21.11. Кодирование функциональных клавиш

Последовательности нажатия клавиш Клавиша (клавиши)
^[[[А <F1>
^[[[В <F2>
^[[[С <F3>
^[[[D <F4>
^[[[Е <F5>
^[[17~ <F6>
^[[18~ <F7>
^[[19~ <F8>
^[[20~ <F9>
^[[21~ <F10>
^[[23~ <F11>, <Shift+F1>, <Shift+F11>
^[[24~ <F12>, <Shift+F2>, <Shift+F11>
^[[25~ <Shift+F3>
^[[26~ <Shift+F4>
^[[28~ <Shift+F5>
^[[29~ <Shift+F6>
^[[31~ <Shift+F7>
^[[32~ <Shift+F8>
^[[33~ <Shift+F9>
^[[34~ <Shift+F10>
^[[А <Стрелка вверх>
^[[D <Стрелка влево>
^[[В <Стрелка вниз>
^[[С <Стрелка вправо>
^[[1~ <Home>
^[[2~ <Insert>
^[[3~ <Delete>
^[[4~ <End>
^[[5~ <Page Up>
^[[6~ <Page Down>

 

21.4. Прямой вывод на экран

В некоторых случаях наличие одной только способности выводить символы на экран не является достаточным. Частично это связано с невозможностью определить текущее состояние экрана. В системе Unix принята стандартная практика — состояние экрана игнорируется. Если нужно, вы можете задать настройки экрана, при появлении необходимости внести в них изменения, после чего полностью перерисовывать экран всякий раз, когда этого требует пользователь (как правило, нажатием комбинации ^L). Можно планировать и другие применения.

В частности, для работы программ и функций, предназначенных для фиксирования и восстановления экрана, требуется доступ к текущему содержимому экрана. Система Linux предоставляет такой доступ через два интерфейса. Один из них предлагает только текстовое содержимое экрана, второй содержит атрибуты (цвет и так далее).

Простейший текстовый механизм носит название vcs, что, вероятно, означает virtual console screen (экран виртуальной консоли). Чтение устройства /dev/vcs0 дает содержимое текущей виртуальной консоли, как оно выглядит на момент чтения. Если экран в настоящий момент прокручен (по умолчанию для прокрутки экрана назначаются клавиатурные последовательности и ), устройство /dev/vcs0 содержит прокрученное содержимое, видимое на экране. Остальные устройства vcs, /dev/vcs n , представляют текущее состояние виртуальной консоли n и обычно доступны через /dev/tty n .

При чтении файла /dev/vcs* не дается никаких указаний относительно новых строк или размера консоли, кроме метки EOF в конце экрана. Если вы прочитали 2000 байт и затем получили EOF, вы не сможете определить размеры экрана: может быть, он содержит 80 столбцов и 25 строк, а, может быть, 40 столбцов и 50 строк. Для отметки конца строк не выводятся символы новой строки, к тому же каждая пустая символьная ячейка (независимо от того, записывалась ли в нее когда-либо информация) обозначается символом пробела. Существует несколько популярных конфигураций экрана, и нет никакой гарантии, что каждая из них имеет однозначное количество строк и столбцов. Механизм vcs предлагает легкий способ для находчивых системных администраторов или разработчиков увидеть содержимое любой виртуальной консоли. Однако он не особенно полезен с точки зрения программистов, по крайней мере, без посторонней помощи.

Один из удобных способов — это использование vcs из X. По умолчанию XFree86 запускает X-сервер на первой свободной виртуальной консоли, но не на той консоли, из которой была активизирована программа. Если вы запускаете XFree86 из виртуальной консоли 1, то вам не нужно возвращаться в консоль 1 для того, чтобы увидеть регистрационные сообщения, которые XFree86 выводит на экран. Просто перенесите наверх окно терминала того же самого размера, что и консоль (обычно 80 столбцов на 25 строк), войдите в систему как привилегированный пользователь (для того чтобы получить доступ к механизму vcs) и запустите cat /dev/vcs1. Содержимое первой виртуальной консоли заполнит ваше терминальное окно.

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

• Цвета.

• Другие атрибуты (например, мерцание).

• Текущая позиция курсора.

• Конфигурация экрана (количество строк и столбцов).

Механизм vcsа (что означает virtual console screen with attributes — экран виртуальной консоли с атрибутами) предоставляет всю необходимую информацию. Первые четыре байта /dev/vcsa n (для того же самого значения n , что и в vcs) содержат заголовок, показывающий текущую позицию курсора и конфигурацию экрана. Первый байт указывает количество строк, второй — количество колонок, третий — текущий столбец курсора, четвертый — текущую строку курсора. Остальная часть файла содержит переменные байты, которые отображают текст, и байты атрибутов текста данной консоли.

Таким образом, если вам требуется знать только размеры консоли и ее текстовое содержимое, то вы можете прочитать первые два байта из соответствующего механизма vcsa, а после этого работать только с механизмом vcs. Если вы хотите задать текущую позицию курсора, то нужно заполнить третий и четвертый байты механизма vcsa. Обратите внимание на то, что первые два байта предназначены только для чтения (то есть первые два символа являются просто заполнителями). Мы предпочитаем применять пробелы или другие подобные символы для того, чтобы подчеркнуть это. В качестве примера показано перемещение курсора на четвертой виртуальной консоли в восьмую строку и двенадцатый столбец (отсчитывая от нуля):

echo -n -е '..\023\007' > /dev/vcsa4

Параметр -n предотвращает добавление символа новой строки в конце, -е выполняет интерпретацию кодов смены алфавита, поэтому выражение \nnn трактуется как восьмеричный символ nnn.

Атрибуты и символьное содержимое отображаются как переменные байты, первый из которых содержит символ, а второй — атрибуты для применения к этому символу. Байт атрибута, как правило, определяется по аналогии с байтом атрибута, используемым на оборудовании VGA. Остальные виды технических средств, включая карты TGA, применяемые во многих машинах Linux/Alpha, и консольный драйвер SPARC, эмулируют обработку атрибутов VGA. На видеоаппаратуре без поддержки цвета, но с поддержкой подчеркивания, атрибуты могут считываться несколько по-другому. Однако способ разработки позволяет делать вид, что все оборудование ведет себя как VGA.

В каждом атрибутном байте отдельные биты интерпретируются так, как описано в табл. 21.12. Это VGA представление; некоторые цветовые устройства заменяют мерцание ярким задним фоном. Монохромное изображение использует нулевой бит цвета переднего плана для указания подчеркивания.

Таблица 21.12. Атрибуты

Бит (биты) Результат
7 Мерцание
6-4 Фон
3 Полужирный шрифт
2-0 Передний план

 

Глава 22

Написание защищенных программ

 

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

В данной главе предлагается краткий обзор тех важных моментов, которые необходимо учитывать при создании защищенных программ на языке С. Мы выясним, какие типы программ нуждаются в особом уровне надежности и защищенности, а также как минимизировать риски. Мы обратим внимание на самые общие ошибки в вопросе обеспечения безопасности. Все это может послужить введением в написание безопасных программ. Если вам необходима более глубокая информация, обратитесь к книге Давида Вилера (David A. Wheeler) Secure Programming for Linux and UNIX HOW TO. В нее входит также замечательная биография автора, а найти эту книгу можно по адресу http://www.dwheeler.com/secure-programs.

 

22.1. Когда безопасность имеет значение?

 

Компьютерные программы — это очень сложные вещи. Даже самая простая программа "Hello World" является на удивление запутанной. Игнорируя все, что происходит в ядре, библиотека С должна отыскать соответствующие совместно используемые библиотеки, загрузить их в систему, инициализировать стандартные методы ввода-вывода. На компьютере, с помощью которого готовилась данная глава, полная программа состояла из 25 системных вызовов. Только один из них оказался вызовом функции write(), который использовался непосредственно для вывода слов "Hello World".

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

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

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

• Программы, обрабатывающие данные, которые могут поступать из ненадежных источников, очень уязвимы. Такие данные могут содержать скрытые атаки, использующие программные ошибки и вызывающие непредсказуемое поведение программ. Нередко целью таких атак является получение полного доступа к компьютеру для анонимных пользователей. Любая программа, имеющая доступ к данным через сеть (как клиентская, так и серверная) становится потенциальным мишенью для атак. Но даже безобидные программы вроде текстовых процессоров, могут быть поражены через поврежденные файлы данных.

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

• Любая программа, работающая как системный демон, может вызвать проблему безопасности. Подобные программы работают в системе как привилегированные пользователи (довольно часто они выступают в качестве root), поэтому любое взаимодействие с обычными пользователями подвергает систему опасности.

 

22.1.1. Когда выходит из строя система безопасности?

Дефекты в системе безопасности программ являются причиной четырех обширных категорий атак: удаленная эксплуатация, локальная эксплуатация, удаленные атаки отказа в обслуживании и локальные атаки отказа в обслуживании. Удаленные эксплуатации позволяют пользователям, имеющим доступ к сетевым службам компьютера, запустить на этом компьютере произвольный код. Для того чтобы удаленная атака могла иметь место, в программе, обращающейся к сети, должна быть некоторая неисправность. Обычно такие ошибки встречаются в сетевых серверах, что позволяет удаленному взломщику ввести сервер в заблуждение и получить доступ в систему. С недавних пор начали эксплуатироваться и ошибки в сетевых клиентах. Например, если Web-браузер имеет изъян в способе анализа HTML-данных, то Web-страница, загружаемая этим браузером, может побудить браузер активизировать произвольную последовательность кодов.

Локальная эксплуатация дает пользователям возможность выполнять такие действия, на которые у них обычно нет полномочий (на них часто ссылаются как на разрешение превышения локальных полномочий), например, нелегально проникать в систему под видом другого пользователя. Такой тип эксплуатации, как правило, направлен на локальных демонов (таких как серверы cron или sendmail) и setuid-программы (вроде mount или passwd).

Атаки отказа в обслуживании не позволяют взломщику получить контроль над системой, однако они могут помешать законному использованию данной системы. Это наиболее хитрые помехи, многие из них очень трудно ликвидировать. Например, многие программы, использующие файлы блокировок, подвергаются опасности таких атак. Хакер может просто вручную создать файл блокировки и ни одна программа никогда не удалит его. Одна из простейших атак отказа в обслуживании для пользователей заключается в заполнении их домашних каталогов ненужными файлами, тем самым исключая возможность создания новых файлов другими пользователями в той же файловой системе. В общем случае для локальных атак рассматриваемого типа существует гораздо больше удобных случаев, чем для удаленных атак. Мы не будем здесь изучать атаки отказа в обслуживании подробно, поскольку они часто являются следствием всей программной архитектуры, а не одного дефекта.

 

22.2. Минимизация возможности появления атак

 

Одной из наилучших стратегий по обеспечению безопасности программ перед попытками несанкционированного использования прав доступа является создание отдельных частей программы, которые чрезвычайно легко атаковать. Подобную стратегию иногда трудно воплотить в сетевых программах и системных демонах, однако в тех программах, которые должны запускаться с особыми правами доступа (через биты setuid и setgid либо при активизации привилегированным пользователем), как правило, можно применить несколько алгоритмов для уменьшения их областей уязвимости.

 

22.2.1. Передача полномочий

Многие программы, которые требуют определенных прав доступа, используют эти права только во время запуска. Например, некоторые сетевые демоны могут активизироваться только привилегированным пользователем для того, чтобы они имели возможность подключиться к резервному порту с помощью функции listen(), но после этого никакие особые полномочия не понадобятся. Большинство Web-серверов используют этот прием для усиления защиты от атак путем переключения на другого пользователя (обычно пользователь называется nobody или apache) сразу после открытия TCP/IP-порта 80. В это время сервер все еще остается объектом для удаленного использования, но, по крайней мере, такая эксплуатация больше не сможет предоставить взломщику доступ к процессу, активизированному как root. Сетевые клиенты, нуждающиеся в резервных портах (таких как rsh), могут применять подобную методику. Они запускаются как setuid на root, что позволяет им открывать подходящий порт. Как только порт открыт, необходимость в привилегиях root отпадает, и особые возможности можно отключить.

Для восстановления полномочий процесса необходимо использовать один или более из следующих методов: setuid(), setgid(), setgroups(). Этот прием эффективен только в том случае, если используется настоящая действующая файловая система и для всех сохраненных идентификаторах uid (или gid) установлены соответствующие значения. Если программа является setuid (или setgid), то процесс, вероятно, пожелает присвоить данным идентификаторам uid их сохраненное значение uid. Системные демоны, передающие управление другому пользователю после запуска от имени root, должны изменять пользовательские и групповые идентификаторы, а также очищать свой дополнительный групповой список. Более подробное описание того, как процесс может изменять свои сертификаты, можно найти в главе 10.

 

22.2.2. Получение вспомогательной программы

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

Применение маленьких вспомогательных программ приобрело широкую популярность в сообществе Linux. Библиотека utempter (обсуждаемая в главе 16) использует вспомогательную setgid-программу для обновления базы данных utmp. Эта программа очень внимательно проверяет правильность аргументов командной строки, а также контролирует, имеет ли вызывающее приложение разрешение на обновление базы данных utmp. Тем программам, которые предусматривают данную службу через вспомогательное приложение utempter, вообще не требуется никаких особых полномочий. До создания этой библиотеки каждая программа, использующая псевдотерминалы, была обязана быть setgid для той группы, которой принадлежит база данных utmp.

Еще одним примером вспомогательной программы может послужить программа unix_chkpwd, которая используется РАМ (Pluggable Authentication Modules — подключаемые модули аутентификации, подробнее рассматривается в главе 28). Пароли в большинстве систем Linux хранятся в файле, доступном для чтения только пользователю root. Это предотвращает словарные атаки на зашифрованные пароли пользователей. Некоторые программы проверяют, действительно ли возле компьютера находится тот пользователь, который вошел в систему (программа xscreensaver может применяться для блокировки экрана до возвращения пользователя), но они обычно работают не как программы root. Вместо того чтобы делать такие программы setuid на root, дабы они могли проверить правильность пользовательского пароля, стандартная аутентификация РАМ Unix вызывает unix_chkpwd для подтверждения пароля. Таким образом, необходимость быть setuid на root существует только для программы unix_chkpwd. Это означает, что потребность создавать xscreensaver как привилегированную программу отпадает, а также что все слабые места в системе безопасности библиотек X11 не допускают локальной эксплуатации.

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

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

Вспомогательные программы также должны тщательно проверять тот факт, что вызывающая их программа имеет разрешение на выполнение запрашиваемого действия. Вспомогательная программа unx_chkpwd не позволяет программе проверять пароли ни одного пользователя, кроме того, который ее запустил. Здесь применяется свой собственный uid для подтверждения того, что активизирующая программа может проверять пароль пользователя. Вспомогательная программа utempter выполняет аналогичные проверки для того, чтобы убедиться, что программы не могут удалять терминалы из базы данных utmp, если только они не предназначены делать это.

 

22.2.3. Ограничение доступа к файловой системе

Еще одним способом устранения ошибок в кодах, предоставляющих возможность для атак, является ограничение набора файлов, к которым программа имеет доступ, с помощью системного вызова chroot(). Как обсуждалось в главе 14, метод chroot(), сопровождающийся вызовом chdir(), изменяет корневой каталог процесса, ограничивая совокупность файлов, доступных процессу. Это не предотвращает возможность эксплуатации, но иногда может нарушать работу приложения. Если сетевой сервер, работающий как не root, эксплуатируется удаленно, то данному удаленному пользователю значительно труднее применить этот сервер в качестве базы локальной эксплуатации, поскольку он не сможет получить доступ ни к каким setuid-файлам (этим могут воспользоваться наиболее простые локальные эксплуатации программ).

Анонимные ftp-серверы являются наиболее распространенными программами, использующими преимущества механизма chroot(). В последнее время он становится более популярным и в других программах, многие системные администраторы применяют команду chroot для того, чтобы перевести работу системных демонов в ограниченное окружение и предусмотреть проникновение "незваных гостей".

 

22.3. Общие бреши системы безопасности

 

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

 

22.3.1. Переполнение буфера

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

 1: /* bufferoverflow.с */

 2:

 3: #include

 4: #include

 5: #include

 6:

 7: int main(int argc, char ** argv) {

 8:  char path[_POSIX_PATH_MAX];

 9:

10:  printf("копирование строки длиной %d\n", strlen(argv[1]));

11:

12:  strcpy(path, argv[1]);

13:

14:  return 0;

15: }

16:

На первый взгляд код кажется довольно безопасным; хотя в итоге эта программа фактически даже ничего не делает. Она копирует строку, передаваемую пользователем, в фиксированное пространство стека, даже не проверяя, есть ли в стеке для этого свободное место. Попробуйте запустить эту программу с одним длинным аргументом командной строки (скажем, 300 символов). Это вызовет ошибку сегментации, когда strcpy() попытается записать информацию, превышающую пространство, выделенное для массива path.

Чтобы лучше понять, как происходит распределение памяти для программного стека, взгляните на рис. 22.1. В большинстве систем стек процессора растет вниз; то есть, чем раньше какой-либо объект размещается в стеке, тем больший адрес логической памяти он получает. К тому же первый элемент стека является защищенной областью памяти; любая попытка получить к нему доступ приводит к ошибке сегментации.

Рис. 22.1. Карта памяти стека приложения

Следующий участок стека содержит локальные переменные, используемые кодом, который запускает остальную часть программы. Здесь мы вызываем функцию _main(), несмотря на то что это может принести дополнительные сложности, поскольку касается таких моментов, как динамическая загрузка. Когда запускающий код вызывает для программы метод main(), он сохраняет адрес, который метод main() возвращает при завершении работы со стеком. Когда активизируется main(), может понадобиться сохранение некоторых регистров микропроцессора в стеке с возможностью повторного использования этих регистров. Затем метод выделяет пространство для своих локальных переменных.

Возвращаясь к нашему примеру переполнения буфера, следует отметить, что для переменной path выделяется память на верхушке стека. Байт path[0] находится на самом верху, затем следующий байт — path[1] и так далее. Если наша программа-пример записывает в path более _POSIX_PATH_MAX байтов, то начинается перезапись остальных элементов стека. Если этот процесс продолжается, то происходит попытка записи за пределами верхушки стека, что вызывает ошибку сегментации.

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

Реализации, использующие переполнение буфера, как правило, включают некоторый код в массив, записываемый в стек, и возвращаемый адрес устанавливается на этот код. Этот прием позволяет взломщику запускать любой произвольно выбранный код с теми правами доступа, которыми обладает атакуемая программа. Если эта программа является сетевым демоном, работающим как root, то любой удаленный пользователь получает доступ root к локальной системе!

Обработка строк — не единственное место, в котором встречается переполнение буфера (хотя, пожалуй, наиболее распространенное). Еще одним уязвимым моментом является чтение файлов. Файловые форматы нередко сохраняют размер элемента данных, за которым следуют сами данные. Если размер сохранения используется для выделения буфера, а конец поля данных определяется каким-то другим способом, может произойти ошибка переполнения буфера. Этот тип ошибки сделал возможным для Web-сайтов обращение к файлам, которые искажены так, чтобы предоставить удаленное пользование.

Чтение данных через сетевое соединение предоставляет еще одну возможность для переполнения буфера. Многие сетевые протоколы указывают максимальный размер для полей данных. Например, протокол ВООТР фиксирует для всех пакетов размер 300 байтов. Это, однако, не мешает другой машине передать через сеть 350-байтовый пакет ВООТР. Если в сети работают программы с дефектами, то они попытаются скопировать этот нестандартный 350-байтовый пакет в пространство, выделенное для корректного 300-байтового пакета ВООТР, тем самым вызовут переполнение буфера.

Локализация и трансляция служат еще двумя побудителями переполнения буфера. Если программа написана для английского языка, то без сомнения для хранения названия месяца, загружаемого из таблицы, будет достаточно десятисимвольной строки. Когда эта программа переводится на испанский, "September" превращается в "Septiembre" и может произойти переполнение буфера. Всякий раз, когда программа поддерживает различные языки и локали, большинство первоначально статических строк становятся динамическими, и внутренние строковые буферы должны это учитывать.

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

Существует несколько приемов для устранения из кода возможности переполнения буфера. Хорошо продуманные программы используют множество способов для внимательного выделения буферов соответствующих размеров.

Лучшим способом распределения памяти для объектов является метод malloc(), который устраняет проблемы, возникающие из-за перезаписывания возвращаемого адреса, поскольку malloc() не выделяет память из стека. Аккуратное применение функции strlen() для вычисления необходимого размера и динамическое выделение буфера в программной куче обеспечивает хорошую защиту от переполнения. К сожалению, при этом также расходуется память, поскольку каждый вызов метод malloc() требует вызова метода free(). В главе 7 обсуждалось несколько способов отслеживания ненужных расходов памяти, однако даже с описанными инструментами трудно точно знать, когда можно освободить память, занимаемую объектом. Особенно в том случае, если динамическое распределение памяти для объекта подстроено под уже существующий код. Функция alloca() предлагает альтернативу malloc().

#include

void * alloca(size_t size);

Подобно malloc(), alloca() выделяет область памяти длиной size байтов и возвращает указатель на начало этой области. Вместо использования памяти из программной кучи этот метод распределяет память из вершины стека, из того же места, где хранятся локальные переменные. Первое преимущество данной функции перед локальными переменными состоит в том, что необходимое количество байтов точно вычисляется в программе, а не определяется приблизительно. Превосходство над malloc() заключается в том, что при завершении работы функции память освобождается автоматически. Все это позволяет охарактеризовать alloca() как легкий способ распределения памяти, которая требуется только временно. До тех пор, пока размер буфера вычисляется должным образом (не забудьте учесть '\0' в конце каждой строки С!), можно не бояться переполнения буфера.

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

#include

char * strncpy (char * dest, const char * src, size_t max);

char * strncat (char * dest, const char * src, size_t max);

Обе функции ведут себя как их родственники, называемые аналогично, strcpy() и strcat(), но они возвращают за один раз только max байт, копируемые в строку назначения. Если достигнут предел, то результирующая строка не завершается '\0', поэтому обычные строковые функции не смогут с ней работать. По этой причине необходимо явно завершить строку после вызова одной из подобных функций.

strncpy(dest, src, sizeof(dest));

dest[sizeof(dest) - 1] = '\0';

Частой ошибкой при использовании strncat() является передача общего размера dest в качестве параметра max. Это приводит к потенциальному переполнению буфера, так как strncat() добавляет до max байт в dest; она не прекращает копировать байты, когда общая длина dest достигает max байтов.

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

Функция strncpy() решает проблему копирования строки в статический буфер без переполнения его. А функции strdup() автоматически выделяют буфер, достаточный для хранения строки, до начала копирования в него исходной строки.

#include

char * strdup(const char * src);

char * strdupa(const char * src);

char * strndup(const char * src, int max);

char * strndupa(const char * src, int max);

Первая из приведенных функций, strdup(), копирует строку src в буфер, выделенный методом malloc(), и возвращает буфер вызывающему оператору. Вторая функция, strdupa(), выделяет буфер с помощью alloca(). При этом обе функции выделяют буфер, в точности достаточный для хранения строки и замыкающего символа '\0'.

Остальные две функции, strndup() и strndupa(), копируют не более чем max байтов из str в буфер вместе с замыкающим '\0' (и выделяют не более чем max+1 байтов). При этом выделение буфера происходит при помощи метода malloc() (для strndup()) или alloca() (для strndupa()).

Функция sprintf() также входит в число тех, которые часто вызывают переполнение буфера. Так же как strcat() и strcpy(), функция sprintf() имеет разновидность, позволяющую облегчить защиту от перегрузок.

#include

int snprintf(char * str, size_t max, char * format, ...);

Попытки определить размер буфера, необходимый для sprintf(), могут оказаться слишком сложными. Он зависит от таких элементов, как значения всех форматируемых чисел (для которых могут быть нужны или не нужны знаки чисел), используемые аргументы форматирования и длины всех строк, которые были затронуты форматированием. Для того чтобы избежать переполнения буфера, функция snprintf() помещает в str не более чем max символов, включая замыкающий '\0'. В отличие от strcat() и strncat(), функция snprintf() корректно завершает строку, при необходимости пренебрегая символом из форматируемой строки. Она возвращает количество символов, которые будет занимать конечная строка при наличии доступного пространства. Также сообщается, нужно ли усекать строку до max символов (не считая последний '\0'). Если возвращаемое значение меньше чем max, значит, функция успешно завершила свою работу. Если же равно или больше, значит, предел max превышен.

Функция vsprintf() несет те же проблемы, a vsnprintf() предлагает способ их преодоления.

 

22.3.2. Разбор имен файлов

Абсолютно обычным действием для привилегированных приложений является предоставление доступа к файлам ненадежным пользователям и разрешение этим пользователям передавать имена файлов, к которым необходим доступ. Хорошим примером служит Web-сервер. URL-адрес HTTP содержит имя файла, полученное сервером как запрос на передачу удаленному (ненадежному) пользователю. На Web-сервере необходимо убедиться, что возвращаемый файл — это именно тот, который был сконфигурирован на отправку, а также внимательно проверить правильность имен файлов.

Представьте Web-сервер, обслуживающий файлы из home/httpd/html, выполняющий это посредством простого добавления имени файла из URL, который требуется предоставить, концу /home/httpd/html. Такой процесс дает правильный файл, однако это также позволяет удаленным пользователям увидеть любой файл системы, к которой Web-сервер имеет доступ, просто запросив, к примеру, файл ../../.. /etc/passwd. Подобные каталоги .. необходимо явно проверять и отклонять. Системный вызов chroot() предоставляет хороший способ, позволяющий сделать обработку имен файлов в программах более простой.

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

 

22.3.3. Переменные окружения

В программах, работающих с возможностями setuid или setgid, нужно проявлять особую осторожность с установками окружения. Эти переменные определяются пользователем, активизировавшим программу, тем самым открывается путь для атак. Самая явная атака может пройти через переменную окружения PATH, изменяющую те каталоги, в которых функции execlp() и execvp() отыскивают программы. Если привилегированная программа запускает другие программы, то она должна убедиться, что это именно те программы, которые нужны! Пользователь, который имеет возможность подменить программный путь поиска, легко может подвергнуть программу опасности.

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

Если программа локализована, то переменная NLSPATH также становится проблемной. Она позволяет пользователю переключать используемый программой языковой каталог, который определяет способ перевода строк. Это означает, что в переводных программах пользователь имеет возможность указать значение для любой переводимой строки. Строку можно сделать сколько угодно длинной, вынуждая программу быть крайне осторожной при выделении буфера. Еще более опасным является то, что при переводе форматирующих строк для таких функций, как printf(), можно изменить формат. Например, строка Hello World, today is %s может превратиться в Hello World, today is %c%d%s. Трудно предсказать, какое воздействие могут оказать подобные изменения на функционирование программы!

Все это сводится к тому, что наилучшим решением для переменных окружения setuid- или setgid-программ является исключение этих переменных. Функция clearenv() стирает все значения из окружения, оставляя его пустым. После этого программа может заполнить любые необходимые ей переменные окружения известными значениями.

 

22.3.4. Запуск командной оболочки

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

Каждую строку, передаваемую в оболочку, необходимо очень тщательно проверять на достоверность. К примеру, символ '\n' или ;, вставленный в строку, может привести к тому, что оболочка примет две команды вместо одной. Если строка содержит символы ` или последовательность $(), оболочка запускает другую программу для построения аргумента командной строки. Может также иметь место обычное расширение оболочки, при этом переменные окружения и универсализация файловых имен становятся доступными для взломщиков. Переменная IFS позволяет указать символы (отличные от пробела и табуляции) для разделения полей при анализе командных строк при помощи символов, тем самым, открывая новые бреши для атак. Другие специальные символы, такие как <, > и |, предоставляют еще больший простор для построения командных строк, которые ведут себя не так, как подразумевает программа.

Очень трудно выполнить полную проверку всех этих возможностей. Наилучшим способом предотвращения всех возможных атак против командной оболочки служит в первую очередь уклонение от ее запуска. Функции вроде pipe(), fork(), exec() и glob() позволяют достаточно легко выполнять большинство тех задач, для которых обычно используется оболочка. При этом проблемы расширения командной строки оболочки не возникают.

 

22.3.5. Создание временных файлов

Довольно часто в программах применяются временные файлы. Система Linux даже предусматривает для этой цели особые каталоги (/tmp и /var/tmp). К сожалению, использование временных файлов в безопасном режиме — дело очень ненадежное. Лучшим решением будет создание временных файлов в каталоге, который доступен только через эффективный uid программы. Неплохим выбором, например, может стать домашний каталог данного пользователя. При таком подходе употребление временных файлов становится простым и безопасным. Однако большинство программистов не любят этот способ, так как он загромождает каталоги, причем вполне возможно, что эти файлы никогда не будут удалены, если программа неожиданно выйдет из строя.

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

char fn[200];

int fd;

sprintf(fn, "/tmp/myprogram.%d", getpid());

fd = open(fn, O_CREAT | O_RDWR | O_TRUNC, 0600);

Программа создает уникальное имя файла и усекает любой существующий файл с таким именем перед записью в него. Хотя на первый взгляд этот способ может показаться рациональным, фактически им легко воспользоваться для атак. Если файл, который программа пытается создать, уже существует как символическая ссылка, то открытый запрос следует по такой ссылке и открывает произвольный указываемый файл. Первым примером эксплуатации в такой ситуации является создание символических ссылок в /tmp с использованием многих (или всех) возможных программных идентификаторов, указывающих на файл типа /etc/passwd. При запуске данной программы это приводит к перезаписыванию системного файла паролей, результатом чего становится атака отказа в обслуживании.

Еще более опасной является атака, при которой символические ссылки указывают на собственный файл взломщика (или когда в /tmp создаются нормальные файлы со всеми возможными именами). При открытии файла целевой файл искажается, но во временной промежуток между открытием файла и выполнением программы атакующий (который все еще владеет файлом) может записать в него все, что угодно (добавление строки типа chmod u+s /bin/sh определенно будет полезным в основном сценарии, работающим как root!). Может показаться трудным точно угадать время, однако, режимы состязаний такого типа часто эксплуатируются, подвергая риску безопасность программы. Если программа была setuid, а не запущенная как root, то эксплуатация фактически становится еще легче, так как пользователь может передать SIGSTOP в программу сразу после открытия файла, а затем после эксплуатации этого режима состязаний послать SIGCONT.

Добавление O_EXCL в вызов open() мешает open() открывать файл, который является символической ссылкой, а также уже существующий файл. В данном случае также есть возможность простой атаки отказа в обслуживании, поскольку код выйдет из строя, если первое испробованное имя файла уже существует. Это легко исправить, разместив open() в цикле, испытывающем различные имена файлов до тех пор, пока одно из них не подойдет.

Самым лучшим способом создания временных файлов является применение библиотечной функции mkstemp() интерфейса POSIX, которая гарантирует, что файл создается соответствующим образом.

int mkstemp(char * template);

Параметр template — это имя файла, в котором последние шесть символов должны выглядеть как "XXXXXX". Последняя часть заменяется номером, который позволяет имени файла стать уникальным в данной файловой системе. Такой подход предоставляет функции mkstemp() возможность испытывать различные имена файлов до тех пор, пока одно из них не подойдет. Параметр template обновляется тем именем файла, которое использовалось (позволяя программе удалить файл), также возвращается файловый дескриптор, ссылающийся на временный файл. Если функция прерывает свою работу, возвращается значение -1.

В более старых версиях библиотеки С системы Linux создавался файл с режимом 0666 (общедоступное чтение/запись) и в зависимости от umask программы приобретались соответствующие права на файл. В более новых версиях читать и записывать в файл разрешено только текущему пользователю, но поскольку POSIX не определяет такое поведение, неплохо явно установить umask процесса (077 — хороший выбор!) до вызова mkstemp().

Система Linux и некоторые другие операционные системы предлагают функцию mkdtemp() для создания временных каталогов.

char * mkdtemp(char * template);

Параметр template работает так же, как и для mkstemp(), за исключением того, что функция возвращает указатель на template при успешном завершении работы и NULL в случае неудачи.

Многие операционные системы, поддерживающие mkdtemp(), также предоставляют программу mktemp, которая позволяет основным сценариям создавать временные файлы и каталоги в безопасном режиме.

С временными файлами связана еще одна проблема, которая не рассматривалась до сих пор. Они содержат режимы состязаний, добавляемые временными каталогами, которые постоянно хранятся в сетевых (особенно NFS) файловых системах, а также программами, которые регулярно удаляют старые файлы из этих каталогов. При повторном открытии временных файлов после их создания следует проявлять крайнюю осторожность. Более подробное описание этих и других проблем, связанных с временными файлами, можно найти в книге Давида Вилера Secure Programming for Linux and UNIX HOW TO (http://www.dwheeler.com/secure-programs/). Если вам необходимо реализовать один из таких моментов, возможно, лучше будет создавать временные файлы в домашнем каталоге текущего пользователя.

 

22.3.6. Режимы состязаний и обработчики сигналов

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

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

• Выполнение динамического распределения памяти. Функции распределения памяти не являются реентерабельными и не должны применяться в обработчиках сигналов.

• Применение каких-либо функций, кроме перечисленных в табл. 12.2, всегда приводит к ошибке. Программы, вызывающие такие функции, как printf(), из обработчика сигналов, содержат режимы состязаний, поскольку printf() имеет внутренние буферы и не является реентерабельной.

• Неправильное блокирование остальных сигналов. Большинство обработчиков сигналов не являются реентерабельными, однако, довольно часто в обработчиках, управляющих несколькими сигналами, не блокируются автоматически остальные сигналы. Применение sigaction() помогает исправить ситуацию (если программист старательный).

• Неблокирование сигналов в тех областях кода, модифицирующих переменные, к которым обработчик сигналов также имеет доступ. (Такие зоны часто называются критическими областями.)

В то время как режимы состязаний, порожденные сигналами, могут показаться не опасными, сетевые коды, setuid- и setgid-программы могут использовать сигналы, посылаемые ненадежными пользователями. Передача экстренных данных в программу может стать причиной отправки SIGURG, в то время как setuid- и setgid-программы могут принимать сигналы от пользователя, который запускает их, при этом фактические универсальные имена данных процессов не изменяются. Даже если эти программы изменяют свои действительные имена для предупреждения передачи сигналов, при закрытии пользователем терминала все программы, использующие этот терминал, посылают SIGHUP.

 

22.3.7. Закрытие файловых дескрипторов

В системах Linux и Unix файловые дескрипторы, как правило, наследуются через системные вызовы exec() (и всегда наследуются через fork() и vfork()). В большинстве случаев такое поведение нежелательно, поскольку только разделяться должны только stdin, stdout и stderr. Программы, запускаемые привилегированным процессом, не должны иметь доступа к файлам через унаследованный файловый дескриптор. Поэтому очень важно, чтобы программы внимательно закрывали все файловые дескрипторы, к которым не должна получить доступ новая программа. Это может стать проблемой, если ваша программа вызывает библиотечные функции, которые открывают файлы и не закрывают их. Одним из методов закрытия файловых дескрипторов является закрытие всех файловых дескрипторов вслепую из дескриптора номер 3 (тот, который следует сразу за stderr) произвольным большим значением (скажем, 100 или 1024). В большинстве программ это обеспечивает закрытие всех надлежащих файловых дескрипторов.

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

 

22.4. Запуск в качестве демона

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

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

2. Текущий каталог должен быть изменен на какой-либо подходящий. Это может быть корневой каталог, но никогда не может быть тот каталог, из которого была запущена программа. Если демон этого не сделает, то, возможно, он будет работать соответствующим образом, но это не позволяет удалить тот каталог, из которого он был активизирован, поскольку он остается текущим каталогом программы. Если это возможно, то неплохо применить chroot() на какой-то каталог. Причины обсуждались ранее в этой главе.

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

4. Затем программа должна вызвать fork(), а родительский процесс должен вызвать exit(), позволяя программе, запустившей демон (чаще всего командному процессору), продолжить работу.

5. Дочерний процесс, продолжающий работу, должен закрыть stdin, stdout и stderr, поскольку он не будет больше использовать терминал. Вместо повторного применения файловых дескрипторов 0, 1 и 2 лучше открывать эти файлы как /dev/null. Это гарантирует, что ни одна библиотечная функция, передающая отчеты о состоянии ошибок в stdout или stderr, не запишет эти ошибки в другие файлы, открытые демоном. При этом демон сможет запускать внешние программы, не беспокоясь об их выходных данных.

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

Библиотека С предлагает функцию daemon(), которая обрабатывает некоторые из перечисленных задач.

int daemon(int nochdir, in tnoclose);

Данная функция сразу осуществляет ветвление, и если оно прошло успешно, родительский процесс вызывает _exit() с кодом завершения 0. Затем дочерний процесс переходит в корневой каталог, если nochdir не является нулем, и перенаправляет stdin, stdout и stderr в /dev/null, если noclose не равен нулю. Перед возвратом в дочерний процесс она также вызывает setsid(). При этом унаследованные файловые дескрипторы все равно могут оставаться открытыми, поэтому в программах, использующих daemon(), необходимо следить за ними. Если возможно, в программе также нужно использовать chroot().