Программирование в стандарте POSIX

         

Аналоги реляционных операций для текстовых файлов


Служебная программа cut

cut -b список [-n] [файл ...] cut -c список [файл ...] cut -f список [-d разделитель] [-s] [файл ...]

используется для выборки байт (опция -b), символов (-c) либо ограниченных разделителями полей (опция -f) с заданными списком номерами из строк исходных файлов, их конкатенации и выдачи на стандартный вывод. Если применить терминологию реляционных баз данных, cut выполняет операцию проекции отношения.

Список, являющийся аргументом опций -b, -c и -f, задается как последовательность разделенных запятыми или пробелами положительных чисел или диапазонов (с естественными умолчаниями, когда границы опущены). Опция -d определяет разделитель полей (по умолчанию это символ табуляции), -n предписывает не разрывать (многобайтные) символы, опция -s в сочетании с -f подавляет вывод строк, в которых не оказалось символов-разделителей (по умолчанию такие строки копируются на стандартный вывод без изменений).

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

cut -d : -f 1,3 /etc/passwd

Листинг 6.40. Пример использования служебной программы cut. (html, txt)

root:0 bin:1 daemon:2 adm:3 lp:4 sync:5 . . .

Листинг 6.41. Начальный фрагмент возможного результата работы служебной программы cut. (html, txt)

Служебная программа paste

paste [-s] [-d список] файл ...

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

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

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

ls | paste - - - -

Листинг 6.42. Пример использования служебной программы paste. (html, txt)

Для попарного слияния последовательных строк файла целесообразно "сыграть" на том, что список разделителей - кольцевой (см. пример 6.43).

paste -s -d "\0\n" f.txt

Листинг 6.43. Пример использования служебной программы paste с кольцевым списком разделителей. (html, txt)

Если файл не подпадает под определение текстового из-за чрезмерно длинных строк, утилитой cut можно выделить начальные байты, поместив "хвосты" строк в другой файл. В дальнейшем из двух полученных файлов с помощью служебной программы paste можно воссоздать исходный (см. пример 6.44).

cut -b 1-80 -n f > f1.txt cut -b 81- -n f > f2 . . . paste -d '\0' f1.txt f2 > f3

Листинг 6.44. Пример использования служебных программ cut и paste. (html, txt)





Подразумеваемым разделителем при выводе является пробел.

Допустимы следующие опции.

-a номер_файла

В дополнение к обычному выводу выдать строку для каждой непарной строки из файла с указанным номером (1 или 2).

-e цепочка

Заменить пустые поля вывода из списка опции -o заданной цепочкой символов.

-o список

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

-t символ

Использовать символ в качестве разделителя. Каждое вхождение символа в строку значимо. Указанный символ используется как разделитель и при вводе, и при выводе.

-v номер_файла

Вместо подразумеваемого вывода выдавать только непарные строки из файла с указанным номером (1 или 2).

-1 номер_поля

Производить соединение по полю   файла1 с заданным номером.

-2 номер_поля

Производить соединение по полю   файла2 с заданным номером.

Рассмотрим примеры. Командная строка (см. пример 6.45) выполняет соединение баз данных пользователей и групп, отсортированных в порядке возрастания идентификаторов групп. На стандартный вывод поступают входные имена, имена групп и основные каталоги (см. пример 6.46).

join -1 4 -2 3 -o 1.1,2.1,1.6 -t : passwd.sorted group.sorted

Листинг 6.45. Пример использования служебной программы join.

halt:root:/sbin operator:root:/root root:root:/root shutdown:root:/sbin sync:root:/sbin bin:bin:/bin daemon:daemon:/sbin . . .

Листинг 6.46. Начальный фрагмент возможного результата работы служебной программы join.

Пусть имеется два упорядоченных по алфавиту справочника: номера телефонов и адреса электронной почты (см. пример 6.47). Предполагается, что в качестве разделителя полей применяется символ табуляции. Тогда командная строка, показанная в пример 6.48, позволит получить объединенный справочник (см. пример 6.49).

Имя Номер телефона Иван 123-4567 Петр 123-5678 Яков 123-6789

Имя Адрес электронной почты Иван ivan123@mail.ru Олег oleg@yahoo.com Яков yak@yandex.ru

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

join -t '<tab>' -a 1 -a 2 -e '---------' -o 0,1.2,2.2 phone.txt email.txt

Листинг 6.48. Еще один пример использования служебной программы join.

Имя Номер телефона Адрес электронной почты Иван 123-45-67 ivan123@mail.ru Олег --------- oleg@yahoo.com Петр 123-56-78 --------- Яков 123-67-89 yak@yandex.ru

Листинг 6.49. Возможный результат работы служебной программы join.

Читателю предлагается самостоятельно выбрать правильный способ задания символа табуляции в качестве аргумента опции -t служебной программы join, а также объяснить, зачем нужен пробел в начале заголовка каждого из справочников.


Использование регулярных выражений


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

grep [-E | -F] [-c | -l | -q] [-insvx] -e список_шаблонов ... [-f файл_шаблонов] ... [файл ...]

grep [-E | -F] [-c | -l | -q] [-insvx] [-e список_шаблонов ...] -f файл_шаблонов ... [файл ...]

grep [-E | -F] [-c | -l | -q] [-insvx] список_шаблонов [файл ...]

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

Три приведенные выше варианта вызова служебной программы grep отличаются способом задания списка шаблонов. В первом случае он является аргументом опции -e, во втором извлекается из файла шаблонов - аргумента опции -f, в третьем указывается как самостоятельный аргумент командной строки, но отдельные элементы этого списка всегда разделяются символами перевода строки. Опция -e полезна в ситуациях, когда нужно задать шаблон, начинающийся со знака минус.

По умолчанию шаблоны трактуются как базовые регулярные выражения. Опция -E предписывает переход к расширенным РВ, а опция -F означает, что в качестве шаблонов выступают цепочки символов (и, следовательно, процесс сопоставления существенно упрощается).

Опции -c, -l, -q и -n влияют на выдачу результатов. По умолчанию на стандартный вывод выдаются строки исходных файлов, в которых присутствуют успешно сопоставленные   цепочки символов. Опция -c предписывает выдавать только общее число подобных строк, -l - только имена файлов, где имеются успешно сопоставленные строки, -q - только код завершения (0 - есть успешно сопоставленные строки), -n требует ставить ее номер в исходном файле перед каждой выводимой строкой (нумерация строк начинается с 1).

Опции -i, -v и -x воздействуют на процесс сопоставления: -i предписывает не различать при сопоставлении большие и малые буквы, -v - выбирать строки, не удовлетворяющие ни одному из заданных шаблонов, -x - рассматривать только строки, все символы которых участвуют в успешном сопоставлении с одним из шаблонов.


Опция - s подавляет выдачу диагностических сообщений о том, что исходный файл не существует или не доступен на чтение.

Отметим, что опции -F и -q, каждая по-своему, ускоряют работу служебной программы grep: -F упрощает сопоставление, -q позволяет завершить действие после первого успешного сопоставления (и не обрабатывать оставшиеся строки и/или файлы). Сочетание опций -q и -s позволяет также более свободно задавать исходные файлы, не заботясь об их существовании и доступности.

Рассмотрим примеры использования утилиты grep. Для выборки пустых строк из файла стандартного ввода пригодны два шаблона:

grep ^$ grep -v .

Если нужно выбрать строки, имеющие вид abc или def, можно воспользоваться одной из трех команд:

grep -E '^abc$|^def$' grep -F -x 'abc def'

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

grep -i '^[^C].* CALL ' *.for

В командных файлах для обработки текстов часто используется потоковый редактор   sed:

sed [-n] сценарий [файл ...] sed [-n] [-e сценарий] ... [-f файл_сценария] ... [файл ...]

Редактор sed читает указанные текстовые файлы (по умолчанию - стандартный ввод), выполняет редактирование в соответствии с командами сценария и записывает результат на стандартный вывод. Смысл опций -e и -f аналогичен утилите grep. Опция -n подавляет подразумеваемый вывод и предписывает выдавать только явно отобранные строки.

Сценарий для sed состоит из редактирующих команд (каждая на отдельной строке), имеющих следующий формат:

[адрес [, адрес]] функция [аргумент ...]

Функция имеет здесь однобуквенное обозначение.

В нормальном режиме sed циклически выполняет следующие действия:

Добавляет входную строку в буфер. Обычно буфер пуст, если только предыдущий цикл не завершился командой D.Применяет к буферу последовательно все команды сценария, адреса в которых позволяют их применить.Если не указана опция -n, копирует буфер на стандартный вывод, добавив в конце перевод строки.Очищает буфер.

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

Адрес в редактирующей команде sed - это либо десятичное число, означающее номер входной строки в совокупности входных файлов, либо символ $, который обозначает последнюю входную строку, либо контекстный адрес, имеющий вид /базовое_регулярное_выражение/. Контекстный адрес задает первую (начиная с текущей) из строк, успешно сопоставленных с БРВ при движении вперед.

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



Очищает буфер.

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

Адрес в редактирующей команде sed - это либо десятичное число, означающее номер входной строки в совокупности входных файлов, либо символ $, который обозначает последнюю входную строку, либо контекстный адрес, имеющий вид /базовое_регулярное_выражение/. Контекстный адрес задает первую (начиная с текущей) из строк, успешно сопоставленных с БРВ при движении вперед.

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

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

Аргумент текст состоит из одной или более строк. Все строки, кроме последней, заканчиваются на \, чтобы экранировать символ перевода строки.

(2){ функция функция ... }

Выполнить заданную последовательность функций.

(1)a\ текст

Добавить. Вывести текст перед чтением следующей входной строки.

(2)b [метка]

Перейти к команде :, содержащей метку. Если метка пуста, перейти на конец сценария.

(2)c\ текст

Заменить. Удалить содержимое буфера. При 0 или 1 адресе или в конце двухадресного диапазона вывести текст. Начать новый цикл.

(2)d

Удалить содержимое буфера. Начать новый цикл.

(2)D

Удалить начало буфера до первого перевода строки. Начать новый цикл.

(2)g

Заменить содержимое буфера содержимым хранилища.

(2)G

Добавить к содержимому буфера содержимое хранилища.

(2)h

Заменить содержимое хранилища содержимым буфера.

(2)H

Добавить к содержимому хранилища содержимое буфера.

(1)i\ текст

Вставить. Вывести текст.

(2)l

Вывести буфер, заменяя непечатные символы на пары символов ASCII и разбивая длинные строки.

(2)n

Скопировать буфер на стандартный вывод, если подразумеваемый вывод не подавлен.


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

(2)N

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

(2)p

Скопировать буфер на стандартный вывод.

(2)P

Скопировать начальный сегмент буфера (до первого перевода строки) на стандартный вывод.

(1)q

Выйти. Перейти на конец сценария. Нового цикла не начинать.

(2)r ч_файл

Прочитать содержимое ч_файла. Поместить его на стандартный вывод перед чтением следующей входной строки.

(2)s/БРВ/замена/флаги

Подставить замену вместо фрагментов буфера, отождествленных с БРВ. Флаги могут быть опущены или иметь следующие значения:

число n - заменить n-е вхождение БРВ;

g - заменить все вхождения БРВ, а не только первое;

p - если замена произошла, вывести содержимое буфера;

w з_файл - если замена произошла, добавить содержимое буфера к з_файлу.

Вместо символа & в замене подставляется цепочка, отождествленная с БРВ.

(2)t [метка]

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

(2)w з_файл

Записать. Добавить содержимое буфера к з_файлу.

(2)x

Обменять содержимое буфера и хранилища.

(2)y/цепочка1/цепочка2/

Заменить все символы буфера, содержащиеся в цепочке1, на соответствующие символы цепочки2. Длины цепочек должны совпадать.

(2)! функция

Отрицание. Применить функцию (или группу, если функция начинается с {) только к строкам, не соответствующим адресам.

(0): метка

Не делает ничего. Содержит лишь метку, на которую может быть осуществлен переход командами t или b.

(1)=

Вывести в качестве отдельной строки номер текущей строки.

(0)

Пустая команда.

(0)#

Управляющий комментарий. Если сценарий начинается с символов #n, подразумеваемый вывод подавляется (что эквивалентно опции -n в командной строке).


В остальных случаях игнорировать # и остаток строки.

Последовательность символов \n успешно сопоставляется с переводом строки. Явный символ перевода строки не должен использоваться в БРВ контекстных адресов и функции замены.

Приведем примеры использования потокового редактора   sed. В процессе загрузки ОС Linux выполняются командные строки, аналогичные показанным в пример 6.23.

map=`basename $map | sed -e s/^auto_home/auto.home/ -e s/^auto_mnt/auto.mnt/` cat /etc/auto.master | grep -v '^+' | sed -e '/^#/d' -e '/^$/d'

Листинг 6.23. Пример использования редактора sed.

Первая из них заменяет подчеркивание на точку в именах файлов, обслуживающих автомонтирование файловых систем, вторая отсеивает строки файла auto.master, начинающиеся с символа + (это делает grep -v), комментарии (строки, начинающиеся символом #) и пустые строки.

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

sed -n ' p /^$/ { # Текущая строка - пустая. # Добавляем следующие строки к буферу, # пока он остается пустым. # Тем самым игнорируются "лишние" пустые # строки. :Empty n /^$/ b Empty # Добавленная строка оказалась непустой. # Выведем ее. p } '

Листинг 6.24. Сжатие пустых строк средствами редактора sed.

Любопытно сопоставить приведенный нами сценарий с примером, включенным в текст стандарта POSIX-2001 (см. пример 6.25). Наш вариант явно проще и короче.

sed -n ' # Выведем непустые строки /./ { p d } # Выведем одну пустую строку, затем # проанализируем следующие. /^$/ p # Прочитаем следующую строку, отбросим # оставшийся перевод строки (пустую строку) # и вернемся к проверке пустой строки. :Empty /^$/ { N s /.// b Empty } # Выведем непустую строку, затем вернемся к # поиску первой пустой. p '

Листинг 6.25. «Стандартный» вариант сжатия пустых строк средствами редактора sed.

Еще одно популярное средство обработки текстовых файлов - служебная программа awk:

awk [-F РРВ] [-v присваивание] ... программа [аргумент ...] awk [-F РРВ] -f программный_файл ... [-v присваивание] ... [аргумент ...]



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

шаблон { действие }

Ввод для awk делится на записи, разделяемые специальным символом. По умолчанию это перевод строки; в таком случае awk обрабатывает ввод построчно. Разделитель записей можно изменить, переопределив переменную   RS. Каждая запись делится на поля, ограниченные разделителями полей (по умолчанию - пробелами или табуляциями). Любой из них можно изменить, переопределив переменную   FS или указав опцию -F с аргументом - расширенным регулярным выражением (РРВ). Поля исходных строк доступны по именам $1, $2,...; $0 - вся входная строка.

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

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

if ( условие ) оператор [ else оператор ];while ( условие ) оператор;for ( выражение; условие; выражение ) оператор;break;continue;{ [ оператор ] ... };переменная = выражение # оператор присваивания;print [ список_выражений ] [> выражение ];printf формат [, список_выражений ] [> выражение ];next # пропустить оставшиеся шаблоны и перейти к следующей строке;exit # пропустить оставшиеся строки.



Операторы завершаются точкой с запятой, переводом строки или правой скобкой. Пустой список_выражений означает всю строку. Выражения строятся из цепочек символов и чисел с помощью операций +, -, *, /, %, ^ (возведение в степень) и конкатенации (обозначается пробелом). В них также можно использовать операции из языка C: ++, --, +=, -=, *=, /=, %=, ^=, ? : (условное выражение). Переменные инициализируются пустыми цепочками, могут быть скалярами, элементами массива (обозначается x[i]) или полями. Индексами массива служат любые (не обязательно числовые) цепочки символов, что позволяет реализовать разновидность ассоциативной памяти. Цепочки символов заключаются в двойные кавычки (").

Оператор   print выдает свои аргументы на стандартный вывод (или в файл, если присутствует часть >выражение), разделяя их текущим разделителем полей и завершая каждую запись выходным разделителем записей. Оператор   printf делает то же, но под управлением формата.

Язык awk содержит большое число встроенных функций. Кратко опишем их.

Математические функции atan2 (y, x), cos (x), sin (x), exp (x), log (x), sqrt (x) не нуждаются в пояснениях. Функция int (x) отбрасывает дробную часть своего аргумента, rand () возвращает псевдослучайное число в диапазоне от

В число функций, оперирующих цепочками символов, входят gsub (РРВ, замена[, цепочка]) и sub (РРВ, замена[, цепочка]) - соответственно, глобальная и однократная замена вхождений РРВ в $0 или цепочку, по аналогии с командой s редактора sed и ее флагом g; index (цепочка, подцепочка) - поиск подцепочки в цепочке; length [([цепочка])] - вычисление длины цепочки-аргумента или $); match> (цепочка, РРВ) - поиск вхождения РРВ в цепочку с установкой значений переменных   RSTART и RLENGTH (см. далее); split (цепочка, массив[, РРВ-разделитель]) - расщепление цепочки по полям в элементы массива); sprintf (формат, выражение, выражение, ...) - формирование цепочки символов средствами форматного вывода; substr (цепочка, m[, n]) - выделение n-символьной подцепочки, начинающейся с позиции m; tolower (цепочка) - приведение к строчным буквам; toupper (цепочка) - приведение к прописным буквам.



В языке awk имеются также группа функций ввода/вывода и функции общего назначения. Функция close (выражение) закрывает файл или канал, поименованный заданным выражением, getline [переменная] обеспечивает чтение записи из текущего входного файла (возможно использование конвейера вида выражение   | getline [переменная] и перенаправление ввода getline [переменная] < выражение), system (выражение) - выполнение команды, заданной выражением.)

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

function имя_функции ([аргумент, ...]) { операторы }

Шаблон в языке awk - это произвольная логическая комбинация, составленная с помощью операций !, ||, && и скобок из расширенных регулярных выражений и выражений сравнения. РРВ обрамляются символами /. Отдельное РРВ в шаблоне сопоставляется со всей строкой. РРВ допускаются и в выражениях сравнения. Шаблон может состоять из двух шаблонов, разделенных запятой; указанные действия выполняются для всех строк между строкой, удовлетворяющей первому шаблону, и строкой, удовлетворяющей второму.

Выражение сравнения - одна из следующих конструкций:

выражение опер_сопост РРВ выражение опер_сравн выражение

Здесь опер_сравн - любая из шести операций сравнения языка C, опер_сопост - это ~ (успешно сопоставляется) или !~ (не сопоставляется).

Условие - арифметическое выражение, выражение сравнения или их логическая комбинация.

Для выполнения каких-либо действий перед чтением первой или после чтения последней исходной строки определены специальные шаблоны BEGIN и END. Шаблон BEGIN следует указывать первым, END - последним.

Присваивания, заданные в командной строке с помощью опции -v, выполняются до начала интерпретации awk-программы (в частности, до действий, ассоциированных с шаблоном BEGIN). Например, для использования символа c в качестве разделителя полей можно указать в командной строке -v 'FS = c'.

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


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

Перечислим специальные переменные awk.

ARGC

Число элементов в массиве ARGV.

ARGV

Массив аргументов командной строки awk, исключая опции и программы.

CONVFMT

Формат для преобразования чисел в цепочки символов (кроме операторов вывода, где используется переменная   OFMT, см. далее). По умолчанию - %.6g.

ENVIRON

Массив, представляющий окружение. Индексами служат цепочки символов, совпадающих с именами переменных окружения.

FILENAME

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

FNR

Порядковый номер текущей записи в текущем исходном файле.

FS

РРВ - разделитель полей во входных данных, по умолчанию - пробел.

NF

Количество полей в текущей записи.

NR

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

OFMT

Формат вывода чисел, по умолчанию %.6g.

OFS

Разделитель полей при выводе, по умолчанию - пробел.

ORS

Разделитель записей при выводе, по умолчанию - перевод строки.

RLENGTH

Длина успешно сопоставленной функцией match() цепочки символов.

RS

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

RSTART

Начальная позиция успешно сопоставленной функцией match()   цепочки символов (считая от 1).

SUBSEP

Цепочка символов - разделитель индексов многомерных массивов; подразумеваемое значение зависит от реализации.

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



{ s += $1 } END { print "Сумма:", s, " Среднее арифметическое:", s/NR }

Листинг 6.26. Пример awk-программы, оперирующей с числами.

Командная строка из пример 6.27, служит для вывода тех строк файла f1.txt, у которых первое поле не совпадает с первым полем предыдущей строки.

awk '$1 != prev { print; prev = $1 }' f1.txt

Листинг 6.27. Пример awk-программы, заданной в командной строке.

Чтобы распечатать файл f2.txt, вставляя после слова "Page" номера страниц (начиная с первой), можно воспользоваться awk-программой (предполагается, что она помещена в файл prog.awk) и командной строкой, представленными, соответственно, в листингах пример 6.28 и пример 6.29.

/Page/ { $2 = n++ } { print }

Листинг 6.28. Пример awk-программы, использующей шаблоны.

awk -f prog.awk -v 'n=1' f2.txt

Листинг 6.29. Пример вызова awk-программы, использующей шаблоны.

Программа, показанная в пример 6.30, выводит поля входных записей, по одному на строке.

{ for (i = NF; i > 0; --i) print $i }

Листинг 6.30. Пример awk-программы, использующей оператор цикла.

Промоделировать работу утилиты echo можно с помощью awk-программы (см. пример 6.31).

BEGIN { for (i = 1; i < ARGC; ++i) printf ("%s%s", ARGV [i], i == ARGC - 1 ? "\n" : " ") }

Листинг 6.31. Пример awk-программы, использующей оператор цикла и специальные переменные awk.

Следующая awk-программа (см. пример 6.32) позволяет разложить список поиска, хранящийся в переменной окружения PATH, по элементам массива.

BEGIN { n = split (ENVIRON ["PATH"], path, ":") for (i = 1; i <= n; ++i) print path [i] }

Листинг 6.32. Пример awk-программы, использующей встроенную функцию split().

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

# Перемонтируем на чтение все, что еще остается смонтированным. mount | awk '/( \/ |^\/dev\/root)/ { print $3 }' | while read line; do mount -n -o ro,remount $line done



Листинг 6.33. Пример использования утилиты awk в системном командном файле.

Отметим, что в POSIX- 2001 стандартизована весьма развитая версия awk, входной язык которой приближен к языку C.

На уровне функций работа с регулярными выражениями поддержана семейством regex (см. пример 6.34).

#include <regex.h> int regcomp (regex_t *restrict preg, const char *restrict pattern, int cflags); int regexec (const regex_t *restrict preg, const char *restrict string, size_t nmatch, regmatch_t pmatch [restrict], int eflags); void regfree (regex_t *preg); size_t regerror (int errcode, const regex_t *restrict preg, char *restrict errbuf, size_t errbuf_size);

Листинг 6.34. Описание функций семейства regex().

Первый член этого семейства, функция regcomp(), компилирует регулярное выражение, заданное аргументом pattern, и помещает результат компиляции в структуру типа regex_t, на которую указывает аргумент preg. Эта структура, описанная в заголовочном файле <regex.h>, должна содержать про крайней мере поле

size_t re_nsub; /* Число заключенных в скобки подвыражений */

Третий аргумент функции regcomp(), cflags, задается как побитное ИЛИ следующих флагов:

REG_EXTENDED

Использовать расширенные регулярные выражения (подразумеваемый тип регулярных выражений - базовые).

REG_ICASE

При сопоставлении не различать большие и малые буквы.

REG_NOSUB

В regexec() сообщать только об успехе/неудаче сопоставления (и не устанавливать значения поля   re_nsub структуры regex_t).

REG_NEWLINE

Изменить трактовку переводов строк (мы не будем на этом останавливаться).

Функция regexec() сопоставляет цепочку символов   string со скомпилированным шаблоном, заданным аргументом preg. При успешном выполнении результат равен нулю; в противном случае возвращается ненулевое значение, свидетельствующее о неудаче сопоставления или ошибке. Аргумент eflags - побитное ИЛИ флагов REG_NOTBOL и REG_NOTEOL - определяет, являются ли границы цепочки границами строки, что важно для обработки фиксаторов ^ и $.


Если значение аргумента nmatch равно нулю или при вызове regcomp() был задан флаг REG_NOSUB, аргумент pmatch функции regexec() игнорируется. В противном случае он должен указывать на массив не менее чем из nmatch элементов, который будет заполнен смещениями подцепочек, сопоставленных с заключенными в скобки подвыражениями шаблона (pmatch [0] соответствует всему регулярному выражению, в неиспользуемые элементы помещается -1).

Структурный тип regmatch_t должен включать по крайней мере следующие поля:

regoff_t rm_so; /* Смещение в байтах начала подцепочки от начала цепочки */

regoff_t rm_eo; /* Смещение в байтах первого символа за концом подцепочки от начала цепочки */

Тип regoff_t определяется как целое со знаком, способное вместить любое значение типов off_t и ssize_t.

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

В файле <regex.h> определены константы, возвращаемые функциями семейства regex() в случае ошибки. Например, значение REG_NOMATCH возвращается функцией regexec() при неудаче сопоставления, REG_BADPAT обозначает некорректное регулярное выражение, REG_ESPACE - нехватку памяти и т.д. Функция regerror() отображает эти константы в неспецифицируемые стандартом цепочки печатных символов и помещает их в буфер errbuf. Приложение, вызывая regerror(), должно передать в качестве аргумента errcode последнее ненулевое значение, возвращенное функциями regcomp() или regexec() с заданным значением аргумента preg.

Приведем пример использования функций семейства regex() (см. пример 6.35). Обратим внимание на задание флага REG_NOTBOL при повторных обращениях к regexec().

#include <stdio.h> #include <limits.h> #include <regex.h>

/* Программа ищет все вхождения заданного шаблона во всех входных строках */ /* и выводит успешно сопоставленные подцепочки */

#define PATTERN "[A-Za-z][A-Za-z0-9]{0,31}"



int main (void) { char line [LINE_MAX]; /* Буфер для входных строк */ char *pline; /* Указатель на начало сопоставляемой части строки */ regex_t cere; /* Скомпилированное расширенное регулярное выражение */ regmatch_t pm; /* Структура для запоминания границ сопоставленной подцепочки */ int reerrcode; /* Код ошибки от regcomp или regexec */ char reerrbuf [LINE_MAX]; /* Буфер для строк с сообщениями об ошибках */ int i;

if ((reerrcode = regcomp (&cere, PATTERN, REG_EXTENDED)) != 0) { (void) regerror (reerrcode, &cere, reerrbuf, sizeof (reerrbuf)); fputs (reerrbuf, stderr); fputc ('\n', stderr); regfree (&cere); return (reerrcode); }

fputs ("Вводите строки, сопоставляемые с шаблоном " PATTERN "\n", stdout); while (fgets (line, sizeof (line), stdin) != NULL) { /* Произведем первое сопоставление с прочитанной строкой. */ /* Оно отличается от остальных при наличии в шаблоне фиксатора начала */ reerrcode = regexec (&cere, pline = line, 1, &pm, 0); while (reerrcode == 0) { /* Повторяем, пока сопоставления с остатком строки успешны */ fputs ("Сопоставленная подцепочка: ", stdout); for (pline += pm.rm_so, i = pm.rm_eo - pm.rm_so; i-- > 0; ) { fputc (*pline++, stdout); } fputc ('\n', stdout); reerrcode = regexec (&cere, pline, 1, &pm, REG_NOTBOL); } }

regfree (&cere); return (ferror (stdin) || ferror (stdout)); }

Листинг 6.35. Пример использования функций семейства regex().

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

Идейно простым, но весьма мощным и полезным средством обработки текстовых файлов является служебная программа преобразования символов tr:

tr [-c | -C] [-s] цепочка1 цепочка2 tr -s [-c | -C] цепочка1 tr -d [-c | -C] цепочка1 tr -ds [-c | -C] цепочка1 цепочка2

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

Утилита tr копирует стандартный ввод на стандартный вывод с заменой либо удалением выбранных символов.


При отсутствии опций введенные символы, найденные в цепочке1, заменяются на соответствующие (стоящие на тех же относительных позициях) символы из цепочки2. Опции -c и -C предписывают использовать вместо цепочки1 ее дополнение до множества всех символов; в первом случае дополнение упорядочивается в соответствии с кодировкой, во втором - по алфавиту. По опции -d будут удалены все входные символы, заданные цепочкой1. Опция -s задает сжатие (до одного) последовательностей одинаковых символов, специфицированных последней из цепочек, указанных в командной строке (сжатие производится после каждой замены и/или удаления).

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

c1-c2

Обозначает цепочку символов, лежащих в диапазоне от c1 до c2 включительно.

[:класс_символов:]

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

[=класс_эквивалентности=]

Обозначает цепочку символов, принадлежащих указанному классу эквивалентности при алфавитном сравнении.

[c*n]

Обозначает символ c, повторенный n раз. Может использоваться только в цепочке2. Если первая цифра в n есть 0, n рассматривается как восьмеричное число; иначе - как десятичное. Нулевое или отсутствующее n воспринимается как "очень много"; эта возможность полезна при дополнении цепочки2 до длины цепочки1.

Обратный слэш можно использовать для задания управляющих символов ('\\', '\a', '\b', '\f', '\n', '\r', '\t', '\v'). Кроме того, \ обозначает код символа, если за ним идут одна, две или три восьмеричные цифры.

Следующая команда (см. пример 6.36) помещает список всех слов из файла f1, по одному на строку, в файл f2 (под словом понимается максимальная последовательность букв).

tr -cs '[:alpha:]' '[\n*]' < f1 > f2

Листинг 6.36. Пример использования служебной программы tr.

Команда, показанная в пример 6.37, переводит большие буквы в малые, попутно сжимая последовательности одинаковых (без учета регистра) букв.



tr -s '[:upper:]' '[:lower:]'

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

Служебная программа uniq

uniq [-c | -d | -u] [-f число] [-s число] [входной_файл [выходной_файл]]

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

-c

Перед каждой выходной строкой помещать ее кратность во входном файле.

-d

Подавить вывод неповторяющихся строк.

-f число

При сравнении строк игнорировать заданное число начальных полей. Поле определяется как максимальная цепочка символов, успешно сопоставляемая с базовым регулярным выражением   [[:blank:]]*[^[:blank:]]*.

-s число

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

-u

Подавить вывод строк, повторявшихся во входном файле.

В качестве примера употребления утилиты uniq приведем конвейер, позволяющий найти десять самых употребительных заголовочных файлов среди включаемых в стандартные заголовочные файлы, расположенные в каталоге   /usr/include и его подкаталогах (см. пример 6.38). Результат работы конвейера может выглядеть так, как показано в пример 6.39.

find /usr/include -name '*.h' -exec cat {} \; | tr -d '[:blank:]' | \ grep -E -e '^#include(<.*>|".*")' | sort | uniq -dc | sort -r | head

Листинг 6.38. Пример использования служебной программы uniq.

977 #include"nsISupports.h" 315 #include<glib.h> 201 #include<gdk/gdk.h> 167 #include<glibmm.h> 160 #include<features.h> 154 #include<glib-object.h> 144 #include"nsCOMPtr.h" 139 #include<sys/types.h> 139 #include<glibmm/class.h> 135 #include"nscore.h"

Листинг 6.39. Возможный результат работы конвейера, показанного в листинге 6.38.


txt cat


cat f1.txt - f2.txt - f3. txt cat g1.txt - g2.txt - g3.txt < input.txt cat g1.txt - g2.txt /dev/null g3.txt < input.txt cat f1.txt f2.txt > f1.txt
Листинг 6.1. Пример использования утилиты cat.
Закрыть окно




od -A x -t a ascii.tab
Листинг 6.2. Пример использования утилиты od.
Закрыть окно




000000 nul soh stx etx eot enq ack bel bs ht nl vt ff cr so si 000010 dle dc1 dc2 dc3 dc4 nak syn etb can em sub esc fs gs rs us 000020 sp ! " # $ % & ' ( ) * + , - . / 000030 0 1 2 3 4 5 6 7 8 9 : ; < = > ? 000040 @ A B C D E F G H I J K L M N O 000050 P Q R S T U V W X Y Z [ \ ] ^ _ 000060 ` a b c d e f g h i j k l m n o 000070 p q r s t u v w x y z { | } ~ del 000080
Листинг 6.3. Возможный результат использования утилиты od.
Закрыть окно




echo " Reloading automounter: checking\ for changes ... " TMP=/var/run/autofs.tmp getmounts > $TMP for i in /var/run/autofs.*.pid do pid=`head -n 1 $i 2>/dev/null` [ "$pid" = "" ] && continue command=`tail -n +2 $i` if ! grep -q "^$command" $TMP then echo "Stopping automounter: $command" kill -USR2 $pid fi done rm -f $TMP
Листинг 6.4. Пример использования служебных программ head и tail.
Закрыть окно




2003-10-17 17:24 Список файлов каталога /var/tmp Page 1 1 . 2 .. 3 Blank.ReS 4 Make45.ReS 5 Make46.ReS 6 from_cvs 7 gcc-20032204 8 gcc-3.4-16-jun-2003 9 htdocs 10 rpm-tmp.29785 . . .
Листинг 6.5. Начальный фрагмент возможного результата работы служебной программы pr.
Закрыть окно




wc ascii.od
Листинг 6.6. Пример использования утилиты wc.
Закрыть окно




9 137 575 ascii.od
Листинг 6.7. Возможный результат использования утилиты wc.
Закрыть окно




12.05.2000 17: 30 200 125 120 15.05.2000 17:00 130 80 70 17.05.2000 10:30 150 90 70 17.05.2000 21:45 154 99 74 19.05.2000 10:05 158 83 89 21.05.2000 21:00 161 104 64 22.05.2000 21:00 147 104 69 . . .
Листинг 6.8. Пример исходных данных для служебной программы sort.
Закрыть окно




sort -r -k 3,3 pp.txt
Листинг 6.9. Пример использования служебной программы sort.
Закрыть окно




12.05.2000 17:30 200 125 120 18.11.2000 19:30 172 107 68 04.07.2002 09:00 170 98 85 30.10.2001 13:00 168 94 88 27.12.2000 20:30 166 98 69 23.05.2002 10:00 166 104 56 22.05.2002 10:00 166 103 57 23.10.2001 11:00 165 88 88 . . .
Листинг 6.10. Возможный результат работы служебной программы sort.
Закрыть окно




# Первый вариант - ключ сортировки покрывает # несколько полей sort -r -k 3,5 pp.txt # Второй вариант - используется несколько # ключей сортировки # sort -n -r -k 3,3 -k 4,4 -k 5,5 pp.txt
Листинг 6.11. Два варианта использования служебной программы sort.
Закрыть окно




12.05.2000 17:30 200 125 120 18.11.2000 19:30 172 107 68 04.07.2002 09:00 170 98 85 30.10.2001 13:00 168 94 88 23.05.2002 10:00 166 104 56 22.05.2002 10:00 166 103 57 27.12.2000 20:30 166 98 69 23.10.2001 11:00 165 88 88 . . .
Листинг 6.12. Результат работы служебной программы sort с несколькими ключами сортировки.
Закрыть окно




# Первый вариант слияния файлов # sort -m -o pp.sorted -n -r -k 3,3 -k 4,4 # -k 5,5 pp2*.sorted # Второй вариант слияния файлов > pp.sorted for f in pp2*.sorted do sort -m -o pp.sorted -n -r -k 3,3\ -k 4,4 -k 5,5 $f pp.sorted done
Листинг 6.13. Два варианта слияния файлов с помощью служебной программы sort.
Закрыть окно




name=pp.sorted if sort -c -r -n -k 3,3 -k 4,4 -k 5,5 $name then echo Данные в файле $ name отсортированы\ верно elif echo Данные в файле $name отсортированы\ неверно fi
Листинг 6.14. Проверка правильности упорядоченности строк в файле с помощью служебной программы sort.
Закрыть окно




sort -k 2.2b,2. 2b f1 f2
Листинг 6.15. Пример использования служебной программы sort с модификаторами в определении ключей.
Закрыть окно




sort -t ':' -k 3,3n /etc/passwd
Листинг 6.16. Пример использования служебной программы sort с опцией -t.
Закрыть окно




sort -um -k 3.1,3 f.sorted
Листинг 6.17. Пример использования служебной программы sort с опциями -m и -u.
Закрыть окно




diff -r binutils-2_14 binutils-2_14-branch
Листинг 6.18. Пример использования служебной программы diff.
Закрыть окно




diff -r binutils-2_14/bfd/version.h binutils-2_14-branch/bfd/version.h 1c1 < #define BFD_VERSION_DATE 20030612 --- > # define BFD_VERSION_DATE 20031007 Only in binutils-2_14-branch/binutils: ChangeLog Only in binutils-2_14-branch/binutils: arlex.c Only in binutils-2_14-branch/binutils: deflex.c Only in binutils-2_14-branch/binutils: rclex.c Only in binutils-2_14-branch/binutils: syslex.c Only in binutils-2_14-branch: config.guess Only in binutils-2_14-branch/gas: ChangeLog Only in binutils-2_14-branch/gas/config: tc-ns32k.c Only in binutils-2_14-branch/gas: configure Only in binutils-2_14-branch/gas: configure.in Only in binutils-2_14-branch/gas: itbl-lex.c . . .
Листинг 6.19. Фрагмент возможного результата работы служебной программы diff.
Закрыть окно




cmp -l binutils-2_14/bfd/version.h\ binutils-2_14-branch/bfd/version.h
Листинг 6.20. Пример использования служебной программы cmp.
Закрыть окно




30 60 61 31 66 60 32 61 60 33 62 67
Листинг 6.21. Возможный результат работы служебной программы cmp.
Закрыть окно




comm - 12 xpg3 svid89 | comm -23 - xcu
Листинг 6.22. Пример использования служебной программы comm.
Закрыть окно




map=`basename $map | sed -e s/^auto_home/auto.home/ -e s/^auto_mnt/auto.mnt/` cat /etc/auto.master | grep -v '^+' | sed -e '/^#/d' -e '/^$/d'
Листинг 6.23. Пример использования редактора sed.
Закрыть окно




sed -n ' p /^$/ { # Текущая строка - пустая. # Добавляем следующие строки к буферу, # пока он остается пустым. # Тем самым игнорируются "лишние" пустые # строки. :Empty n /^$/ b Empty # Добавленная строка оказалась непустой. # Выведем ее. p } '
Листинг 6.24. Сжатие пустых строк средствами редактора sed.
Закрыть окно




sed -n ' # Выведем непустые строки /./ { p d } # Выведем одну пустую строку, затем # проанализируем следующие. /^$/ p # Прочитаем следующую строку, отбросим # оставшийся перевод строки (пустую строку) # и вернемся к проверке пустой строки. :Empty /^$/ { N s /.// b Empty } # Выведем непустую строку, затем вернемся к # поиску первой пустой. p '
Листинг 6.25. «Стандартный» вариант сжатия пустых строк средствами редактора sed.
Закрыть окно




{ s += $1 } END { print "Сумма:", s, " Среднее арифметическое:", s/NR }
Листинг 6. 26. Пример awk-программы, оперирующей с числами.
Закрыть окно




awk '$1 != prev { print; prev = $1 }' f1.txt
Листинг 6.27. Пример awk-программы, заданной в командной строке.
Закрыть окно




/Page/ { $2 = n++ } { print }
Листинг 6. 28. Пример awk-программы, использующей шаблоны.
Закрыть окно




awk -f prog.awk -v 'n=1' f2.txt
Листинг 6.29. Пример вызова awk-программы, использующей шаблоны.
Закрыть окно




{ for (i = NF; i > 0; --i) print $i }
Листинг 6.30. Пример awk-программы, использующей оператор цикла.
Закрыть окно




BEGIN { for (i = 1; i < ARGC; ++i) printf ("%s%s", ARGV [i], i == ARGC - 1 ? "\n" : " ") }
Листинг 6.31. Пример awk-программы, использующей оператор цикла и специальные переменные awk.
Закрыть окно




BEGIN { n = split (ENVIRON ["PATH"], path, ":") for (i = 1; i <= n; ++i) print path [i] }
Листинг 6.32. Пример awk-программы, использующей встроенную функцию split().
Закрыть окно




# Перемонтируем на чтение все, что еще остается смонтированным. mount | awk '/( \/ |^\/dev\/root)/ { print $3 }' | while read line; do mount -n -o ro,remount $line done
Листинг 6.33. Пример использования утилиты awk в системном командном файле.
Закрыть окно




#include <regex.h> int regcomp (regex_t *restrict preg, const char *restrict pattern, int cflags); int regexec (const regex_t *restrict preg, const char * restrict string, size_t nmatch, regmatch_t pmatch [restrict], int eflags); void regfree (regex_t *preg); size_t regerror (int errcode, const regex_t *restrict preg, char *restrict errbuf, size_t errbuf_size);
Листинг 6.34. Описание функций семейства regex().
Закрыть окно




#include <stdio.h> #include <limits.h> #include <regex.h>
/* Программа ищет все вхождения заданного шаблона во всех входных строках */ /* и выводит успешно сопоставленные подцепочки */
#define PATTERN "[A-Za-z][A-Za-z0-9]{0,31}"
int main (void) { char line [LINE_MAX]; /* Буфер для входных строк */ char *pline; /* Указатель на начало сопоставляемой части строки */ regex_t cere; /* Скомпилированное расширенное регулярное выражение */ regmatch_t pm; /* Структура для запоминания границ сопоставленной подцепочки */ int reerrcode; /* Код ошибки от regcomp или regexec */ char reerrbuf [LINE_MAX]; /* Буфер для строк с сообщениями об ошибках */ int i;
if ((reerrcode = regcomp (&cere, PATTERN, REG_EXTENDED)) != 0) { (void) regerror (reerrcode, &cere, reerrbuf, sizeof (reerrbuf)); fputs (reerrbuf, stderr); fputc ('\n', stderr); regfree (&cere); return (reerrcode); }
fputs ("Вводите строки, сопоставляемые с шаблоном " PATTERN "\n", stdout); while (fgets (line, sizeof (line), stdin) != NULL) { /* Произведем первое сопоставление с прочитанной строкой. */ /* Оно отличается от остальных при наличии в шаблоне фиксатора начала */ reerrcode = regexec (&cere, pline = line, 1, &pm, 0); while (reerrcode == 0) { /* Повторяем, пока сопоставления с остатком строки успешны */ fputs ("Сопоставленная подцепочка: ", stdout); for (pline += pm.rm_so, i = pm.rm_eo - pm.rm_so; i-- > 0; ) { fputc (*pline++, stdout); } fputc ('\n', stdout); reerrcode = regexec (&cere, pline, 1, &pm, REG_NOTBOL); } }
regfree (&cere); return (ferror (stdin) || ferror (stdout)); }
Листинг 6.35. Пример использования функций семейства regex().
Закрыть окно




tr -cs '[:alpha:]' '[\n*]' < f1 > f2
Листинг 6.36. Пример использования служебной программы tr.
Закрыть окно




tr -s '[:upper:]' '[:lower:]'
Листинг 6.37. Пример трансляции и сжатия последовательностей символов с использованием служебной программы tr.
Закрыть окно




find /usr/include -name '*.h' - exec cat {} \; | tr -d '[:blank:]' | \ grep -E -e '^#include(<.*>|".*")' | sort | uniq -dc | sort -r | head
Листинг 6.38. Пример использования служебной программы uniq.
Закрыть окно




977 #include"nsISupports.h" 315 #include<glib.h> 201 #include<gdk/gdk.h> 167 #include<glibmm.h> 160 #include<features.h> 154 #include<glib-object.h> 144 #include"nsCOMPtr.h" 139 #include<sys/types.h> 139 #include<glibmm/class.h> 135 #include"nscore.h"
Листинг 6.39. Возможный результат работы конвейера, показанного в листинге 6.38.
Закрыть окно




cut -d : -f 1,3 /etc/passwd
Листинг 6.40. Пример использования служебной программы cut.
Закрыть окно




root:0 bin:1 daemon:2 adm:3 lp:4 sync:5 . . .
Листинг 6.41. Начальный фрагмент возможного результата работы служебной программы cut.
Закрыть окно




ls | paste - - - -
Листинг 6.42. Пример использования служебной программы paste.
Закрыть окно




paste -s -d "\0\n" f.txt
Листинг 6.43. Пример использования служебной программы paste с кольцевым списком разделителей.
Закрыть окно




cut -b 1-80 -n f > f1. txt cut -b 81- -n f > f2 . . . paste -d '\0' f1.txt f2 > f3
Листинг 6.44. Пример использования служебных программ cut и paste.
Закрыть окно




join -1 4 -2 3 -o 1.1,2.1,1.6 -t : passwd.sorted group.sorted
Листинг 6.45. Пример использования служебной программы join.
Закрыть окно




halt:root:/sbin operator:root:/root root:root:/root shutdown:root:/sbin sync:root:/sbin bin:bin:/bin daemon:daemon:/sbin . . .
Листинг 6.46. Начальный фрагмент возможного результата работы служебной программы join.
Закрыть окно




Имя Номер телефона Иван 123-4567 Петр 123-5678 Яков 123-6789
Имя Адрес электронной почты Иван ivan123@mail.ru Олег oleg@yahoo.com Яков yak@yandex.ru
Листинг 6.47. Возможное содержимое двух справочников с информацией о телефонных номерах и об адресах электронной почты.
Закрыть окно




join -t '<tab>' -a 1 -a 2 -e '---------' -o 0,1.2,2.2 phone.txt email.txt
Листинг 6.48. Еще один пример использования служебной программы join.
Закрыть окно




Имя Номер телефона Адрес электронной почты Иван 123-45-67 ivan123@mail.ru Олег --------- oleg@yahoo.com Петр 123-56-78 --------- Яков 123-67-89 yak@yandex.ru
Листинг 6.49. Возможный результат работы служебной программы join.
Закрыть окно




#include <dirent.h> DIR *opendir ( const char *dirname);
Листинг 6.50. Описание функции opendir().
Закрыть окно




#include <dirent.h> void rewinddir (DIR *dirp);
Листинг 6.51. Описание функции rewinddir().
Закрыть окно




#include <dirent.h> struct dirent *readdir (DIR *dirp);
Листинг 6.52. Описание функции readdir().
Закрыть окно




#include <dirent.h> int closedir (DIR *dirp);
Листинг 6.53. Описание функции closedir().
Закрыть окно




#include <fnmatch.h> int fnmatch (const char *file_pattern, const char *file_name, int flags);
Листинг 6.54. Описание функции fnmatch().
Закрыть окно




#include <dirent.h> #include <fnmatch.h> #include <errno.h> #include <stdio.h>
/* Программа сопоставляет имена файлов текущего каталога с заданными шаблонами */
#define SEARCH_DIR "."
static void match_names (DIR *dirp, const char *pattern) { struct dirent *dp;
rewinddir (dirp);
while (errno = 0, (dp = readdir (dirp)) != NULL) { if (fnmatch (pattern, dp->d_name, FNM_PERIOD) == 0) { (void) printf (" %s\n", dp->d_name); } }
if (errno != 0) { perror ("Ошибка при чтении каталога " SEARCH_DIR); } }
int main (int argc, char *argv []) { DIR *dirp; int i;
if ((dirp = opendir (SEARCH_DIR)) == NULL) { perror ("Ошибка при открытии каталога " SEARCH_DIR); return (-1); }
for (i = 1; i < argc; i++) { (void) printf ("Файлы каталога " SEARCH_DIR ", удовлетворяющие шаблону %s\n", argv [i]); match_names (dirp, argv [i]); }
return (closedir (dirp)); }
Листинг 6.55. Пример использования функций, обрабатывающих каталоги.
Закрыть окно




#include <glob.h> int glob (const char * restrict file_pattern, int flags, int (*errfunc) (const char *epath, int eerrno), glob_t *restrict pglob); void globfree (glob_t *pglob);
Листинг 6.56. Описание функций glob() и globfree().
Закрыть окно




#include <glob.h> #include <errno.h> #include <stdio.h>
/* Программа выводит маршрутные имена, сгенерированные по заданным шаблонам */
static int errfunc (const char *epath, int eerrno) { fprintf (stderr, "Ошибка при обработке каталога %s: ", epath); errno = eerrno; perror (NULL); return (0); }
int main (int argc, char *argv []) { glob_t gl_buf; int i;
for (i = 1; i < argc; i++) { (void) glob (argv [i], ((i == 1) ? }
(void) printf ("Маршрутные имена, сгенерированные по заданным шаблонам:\n"); for (i = 0; (unsigned) i < gl_buf.gl_pathc; i++) { (void) printf ("%s\n", gl_buf.gl_pathv [i]); }
globfree (&gl_buf); return (0); }
Листинг 6.57. Пример программы, использующей функции glob() и globfree().
Закрыть окно




if [ "`dirname $RAW`" = "/dev/raw" -a -f /dev/raw ]; then echo $" Please correct your /etc/sysconfig/rawdevices:" echo $" rawdevices are now located in the directory /dev/raw/ " echo $" If the command 'raw' still refers to /dev/raw as a file." echo $" you'll have to upgrade your util-linux package" exit fi
Листинг 6.58. Пример использования служебной программы dirname.
Закрыть окно




gcc -Wall -W -pedantic -o $(basename "$1" .c) $(dirname "$1")/$(basename "$1" .c).c
Листинг 6.59. Пример совместного использования служебных программ basename и dirname.
Закрыть окно



Простейшие средства обработки текстовых файлов


Согласно определению, данному в стандарте POSIX-2001, текстовым называется файл, символы которого объединены в строки длиной не более {LINE_MAX}, не содержащие нулевого символа.

Вероятно, простейшей операцией с файлами (не обязательно текстовыми) является их выдача на стандартный вывод, что обеспечивается посредством утилиты

cat [-u] [файл ...]

Несмотря на внешнюю простоту, применение служебной программы cat сопряжено с рядом тонкостей. Во-первых, опция -u предписывает передавать на стандартный вывод байты из входных файлов без задержек, это означает, в частности, отмену буферизации стандартного вывода. Во-вторых, если не указаны исходные файлы или в качестве аргумента задан знак минус, используется стандартный ввод. Минус может фигурировать в командной строке несколько раз, стандартный ввод не закрывается и не переоткрывается. Например, допустимы команды, показанные в пример 6.1, причем первая обеспечит ввод двух интерактивных вставок, а вторая эквивалентна третьей (читателю предлагается самостоятельно объяснить данный факт).

cat f1.txt - f2.txt - f3.txt cat g1.txt - g2.txt - g3.txt < input.txt cat g1.txt - g2.txt /dev/null g3.txt < input.txt cat f1.txt f2.txt > f1.txt

Листинг 6.1. Пример использования утилиты cat. (html, txt)

В-третьих, нужно учитывать особенности перенаправления вывода командным интерпретатором shell. В общем случае утилита cat выдает результат конкатенации содержимого файлов-аргументов, но выполнение четвертой команды, показанной в пример 6.1, приведет к утрате содержимого файла f1.txt.

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

more [опция ...] [файл ...]

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

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


-n число

Задает число строк, выдаваемых на экран.

-p команды more

Команды more выполняются после входа в очередной файл и вывода его первого экрана. Это может быть, к примеру, команда поиска.

-s

Сжимать последовательные пустые строки в одну.

Утилита more часто используется как заключительное звено конвейеров для удобного просмотра результатов.

Для просмотра нетекстовых файлов рекомендуется служебная программа

od [опция ...] [файл ...]

Она выдает на стандартный вывод содержимое исходных файлов в формате, заданном пользователем, а также в виде блоков, каждый из которых в простейшем (и наиболее употребительном) случае состоит из шестнадцати байт и занимает при выводе одну строку. В начале подобной строки располагается смещение блока от начала файла, затем следует содержимое блока. С помощью аргумента опции -A можно задать основание системы счисления для смещений (d - десятичное, o - восьмеричное, x - шестнадцатеричное, n - не выдавать смещение). Формат выдачи содержимого определяется аргументом опции -t. Помимо перечисленных типов могут быть заданы a (именованные символы), c (символы), f (вещественные числа), u (беззнаковые десятичные). За спецификациями d, f, o, u и x может следовать десятичное число - количество байт в одном значении заданного типа; за спецификацией f - символы F, D или L, указывающие тип вещественных чисел (float, double или long double, соответственно), а за спецификациями d, o, u, x - спецификаторы целочисленного типа C (char), S (short), I(int) или L (long).

Рассмотрим пример. Пусть файл ascii.tab содержит байты от

od -A x -t a ascii.tab

Листинг 6.2. Пример использования утилиты od. (html, txt)

Листинг 6.3. Возможный результат использования утилиты od. (html, txt)

Полезная возможность - взглянуть на начало файла, чтобы понять, нуждается ли он в более детальном изучении. Эта возможность реализуется служебной программой head:

head [-n число] [файл ...]

Утилита head копирует на стандартный вывод указанное число (по умолчанию - 10) начальных строк исходных файлов (или весь файл, если он слишком короткий).



-n число

Задает число строк, выдаваемых на экран.

-p команды more

Команды more выполняются после входа в очередной файл и вывода его первого экрана. Это может быть, к примеру, команда поиска.

-s

Сжимать последовательные пустые строки в одну.

Утилита more часто используется как заключительное звено конвейеров для удобного просмотра результатов.

Для просмотра нетекстовых файлов рекомендуется служебная программа

od [опция ...] [файл ...]

Она выдает на стандартный вывод содержимое исходных файлов в формате, заданном пользователем, а также в виде блоков, каждый из которых в простейшем (и наиболее употребительном) случае состоит из шестнадцати байт и занимает при выводе одну строку. В начале подобной строки располагается смещение блока от начала файла, затем следует содержимое блока. С помощью аргумента опции -A можно задать основание системы счисления для смещений (d - десятичное, o - восьмеричное, x - шестнадцатеричное, n - не выдавать смещение). Формат выдачи содержимого определяется аргументом опции -t. Помимо перечисленных типов могут быть заданы a (именованные символы), c (символы), f (вещественные числа), u (беззнаковые десятичные). За спецификациями d, f, o, u и x может следовать десятичное число - количество байт в одном значении заданного типа; за спецификацией f - символы F, D или L, указывающие тип вещественных чисел (float, double или long double, соответственно), а за спецификациями d, o, u, x - спецификаторы целочисленного типа C (char), S (short), I(int) или L (long).

Рассмотрим пример. Пусть файл ascii.tab содержит байты от

od -A x -t a ascii.tab

Листинг 6.2. Пример использования утилиты od.

000000 nul soh stx etx eot enq ack bel bs ht nl vt ff cr so si 000010 dle dc1 dc2 dc3 dc4 nak syn etb can em sub esc fs gs rs us 000020 sp ! " # $ % & ' ( ) * + , - . / 000030 0 1 2 3 4 5 6 7 8 9 : ; < = > ? 000040 @ A B C D E F G H I J K L M N O 000050 P Q R S T U V W X Y Z [ \ ] ^ _ 000060 ` a b c d e f g h i j k l m n o 000070 p q r s t u v w x y z { | } ~ del 000080



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

Утилита pr обрабатывает следующие аргументы командной строки.

+номер_страницы

Начать вывод со страницы с заданным номером (по умолчанию с первой).

-число_столбцов

Вывод в заданное число столбцов (по умолчанию в один). При выводе в несколько столбцов автоматически действуют опции -e и -i. Этот аргумент несовместим с опцией -m.

-a

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

-d

Выдача через строку.

-e[символ][число]

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

-F

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

-h заголовок

Использовать заданный заголовок (по умолчанию в качестве заголовка выступает имя исходного файла).

-i[символ][число]

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

-l число_строк

Установка длины страницы (по умолчанию 66 строк).

-m

Слияние и печать всех файлов параллельно, по одному в столбце. Реализация должна поддерживать слияние по крайней мере девяти файлов.

-n[символ][ширина]

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



Служебная программа sort

sort [-m] [-o выходной_файл] [-bdfinru] [-t символ] [-k определение_ключа] ... [файл ...]

sort -c [-bdfinru] [-t символ] [-k определение_ключа] [файл]

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



сортировку строк всех исходных файлов с записью результата в выходной файл;

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

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

Следующие опции управляют порядком работы утилиты sort.

-c

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

-m

Слияние исходных файлов, которые предполагаются отсортированными.

-o выходной_файл

Результат направляется не на стандартный вывод, а в выходной_файл, который может совпадать с одним из исходных.

-u

Опция уникальности: из всех совпадающих строк выводить только одну, при наличии опции -c контролировать отсутствие строк с совпадающими ключами сортировки.

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

-d

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

-f

При сравнении преобразовывать малые буквы в большие.

-i

При сравнении игнорировать непечатные символы.

-n

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



Возможный результат приведен в пример 6.10.

12.05.2000 17:30 200 125 120 15.05.2000 17:00 130 80 70 17.05.2000 10:30 150 90 70 17.05.2000 21:45 154 99 74 19.05.2000 10:05 158 83 89 21.05.2000 21:00 161 104 64 22.05.2000 21:00 147 104 69 . . .

Листинг 6.8. Пример исходных данных для служебной программы sort.

sort -r -k 3,3 pp.txt

Листинг 6.9. Пример использования служебной программы sort.

12.05.2000 17:30 200 125 120 18.11.2000 19:30 172 107 68 04.07.2002 09:00 170 98 85 30.10.2001 13:00 168 94 88 27.12.2000 20:30 166 98 69 23.05.2002 10:00 166 104 56 22.05.2002 10:00 166 103 57 23.10.2001 11:00 165 88 88 . . .

Листинг 6.10. Возможный результат работы служебной программы sort.

Если ключи сортировки равны, строки упорядочиваются с учетом всех байт. В рассматриваемом примере это не очень удобно, поскольку они начинаются с номера дня в месяце. Более естественно либо расширить ключ сортировки путем охвата полей нижнего давления и пульса, либо сделать эти поля дополнительными ключами. Оба варианта показаны в пример 6.11, а результат сортировки - в пример 6.12. Отметим, что второй вариант предпочтительнее, поскольку он устойчив к ошибкам выравнивания столбцов.

# Первый вариант - ключ сортировки покрывает # несколько полей sort -r -k 3,5 pp.txt # Второй вариант - используется несколько # ключей сортировки # sort -n -r -k 3,3 -k 4,4 -k 5,5 pp.txt

Листинг 6.11. Два варианта использования служебной программы sort.

12.05.2000 17:30 200 125 120 18.11.2000 19:30 172 107 68 04.07.2002 09:00 170 98 85 30.10.2001 13:00 168 94 88 23.05.2002 10:00 166 104 56 22.05.2002 10:00 166 103 57 27.12.2000 20:30 166 98 69 23.10.2001 11:00 165 88 88 . . .

Листинг 6.12. Результат работы служебной программы sort с несколькими ключами сортировки.

Предположим теперь, что данные о давлении и пульсе разбиты по годам и в уже отсортированном виде хранятся в файлах pp2000.sorted, pp2001.sorted и т.д. Для их слияния можно воспользоваться командной строкой или циклом из пример 6.13. Опять-таки второй вариант предпочтительнее по причине его устойчивости к количеству исходных файлов.



Игнорировать пробельные символы в конце строк; остальные цепочки пробельных символов считать равными.

-c

Производить вывод в формате, обеспечивающем три строки контекста.

-C число

Производить вывод в формате, обеспечивающем заданное число строк контекста.

-e

Производить вывод в формате, пригодном для подачи на вход редактора ed и преобразования файла1 в файл2.

-f

Производить вывод в альтернативном формате, напоминающем -e, но в обратном порядке и не предназначенном для подачи на вход редактора ed.

-r

Если файл1 и файл2 представляют собой каталоги, применять утилиту diff рекурсивно к одноименным и однотипным обычным файлам и каталогам. Если только один из файлов является каталогом, в нем сравнивается "тезка" другого исходного файла.

В качестве примера использования служебной программы diff сравним две версии бинарных утилит (см. пример 6.18). Начальный фрагмент результата показан в пример 6.19. Видно, что различия, по сути, сводятся к добавлению новых файлов.

diff -r binutils-2_14 binutils-2_14-branch

Листинг 6.18. Пример использования служебной программы diff.

diff -r binutils-2_14/bfd/version.h binutils-2_14-branch/bfd/version.h 1c1 < #define BFD_VERSION_DATE 20030612 --- > #define BFD_VERSION_DATE 20031007 Only in binutils-2_14-branch/binutils: ChangeLog Only in binutils-2_14-branch/binutils: arlex.c Only in binutils-2_14-branch/binutils: deflex.c Only in binutils-2_14-branch/binutils: rclex.c Only in binutils-2_14-branch/binutils: syslex.c Only in binutils-2_14-branch: config.guess Only in binutils-2_14-branch/gas: ChangeLog Only in binutils-2_14-branch/gas/config: tc-ns32k.c Only in binutils-2_14-branch/gas: configure Only in binutils-2_14-branch/gas: configure.in Only in binutils-2_14-branch/gas: itbl-lex.c . . .

Листинг 6.19. Фрагмент возможного результата работы служебной программы diff.

Если нужно проверить два файла на совпадение, предпочтительнее воспользоваться не утилитой diff, а более простой и быстрой служебной программой cmp:


Регулярные выражения


Понятие регулярного выражения (РВ) - одно из важнейших для программ обработки текстовых файлов. Согласно стандарту POSIX-2001, регулярное выражение - это шаблон, служащий для выборки определенных цепочек символов из множества подобных цепочек. Говорят, что выбранные цепочки удовлетворяют РВ (успешно с ним сопоставляются).

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

Стандарт накладывает на сложность (полных) РВ единственное ограничение: реализация должна поддерживать любое РВ, длина которого не превышает 256 байт.

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

Различают базовые (БРВ) и расширенные регулярные выражения (РРВ). В большинстве случаев используются БРВ, они и будут описаны в первую очередь.

Минимальным элементом БРВ являются односимвольные БРВ, т. е. БРВ, которым удовлетворяют цепочки из одного символа. Односимвольные БРВ строятся по следующим правилам.

Обычный символ (не входящий в перечень из следующего пункта) - это односимвольное   БРВ, которое успешно сопоставляется с указанным символом.Если за символом \ следует любой специальный символ, то последний теряет свой специальный смысл. Данная комбинация определяет односимвольное   БРВ, успешно сопоставляющееся со специальным символом. Специальными символами являются:

. [ \

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

*

Символ имеет специальный смысл, если он не заключен в квадратные скобки или не является первым символом БРВ.

^

Символ имеет специальный смысл в начале полного БРВ или в квадратных скобках сразу после открывающей скобки.

$

Символ имеет специальный смысл в конце полного БРВ.

Точка (.) - односимвольное   БРВ, успешно сопоставляющееся с любым символом.Непустая цепочка символов, заключенная в квадратные скобки [ ] представляет собой односимвольное   БРВ, которое успешно сопоставляется с любым символом из этой цепочки.
Если цепочка начинается с символа ^, то БРВ успешно сопоставляется с любым не входящим в данный набор символом.

Внутри квадратных скобок можно использовать также конструкции, описываемые ниже.

Диапазон символов можно задать с помощью знака -. Например, БРВ   [0-9] эквивалентно [0123456789]. Минус теряет свой специальный смысл, если он стоит в начале (допустимо после ^) или в конце цепочки символов в квадратных скобках либо использован в качестве границы диапазона. Так, БРВ   [%--] успешно сопоставляется со всеми символами от процента до минуса включительно.

Закрывающая квадратная скобка не рассматривается как окончание цепочки символов, если она стоит вначале (быть может, после ^). Таким образом, БРВ   []a-f] определяет либо закрывающую квадратную скобку, либо любой символ от a до f включительно.

Стандарт POSIX-2001 предусматривает поддержку классов символов (таких, например, как буквы, цифры, знаки пунктуации, пробельные символы, управляющие символы и т.д.). Класс символов задается своим именем, заключенным в квадратные скобки с двоеточиями. Должны поддерживаться по крайней мере следующие классы:

[:alnum:] [:cntrl:] [:lower:] [:space:] [:alpha:] [:digit:] [:print:] [:upper:] [:blank:] [:graph:] [:punct:] [:xdigit:]

Например, БРВ [^[:alnum:]] удовлетворяют символы, отличные от букв и цифр.

В некоторых языках элемент алфавитного сравнения может задаваться несколькими символами. Такие элементы следует заключать в квадратные скобки с точками: [[.ch.]].

Если определены классы элементов, эквивалентных при алфавитном сравнении, то подобный класс можно задать, заключив любой его элемент в квадратные скобки со знаками равенства: ([= и =]).

Таковы правила построения заключенных в квадратные скобки односимвольных БРВ.

Построение многосимвольных   БРВ из односимвольных регламентируется следующим образом.

В состав БРВ могут входить подвыражения ( в том числе вложенные), заключенные в экранированные круглые скобки: \( и \). Подвыражение в скобках успешно сопоставляется с теми же цепочками, что и без скобок.На подвыражения допускаются обратные ссылки вида \n (где n - цифра от 1 до 9).


Обратной ссылке удовлетворяет такая же цепочка символов, что была успешно сопоставлена подвыражением, открывающимся экранированной скобкой номер n, считая от начала полного БРВ. Например, БРВ \(.\)\1 удовлетворяют пары одинаковых символов.За односимвольными БРВ, подвыражениями и обратными ссылками может следовать звездочка. Такая конструкция успешно сопоставляется с любым (в частности, с нулевым) числом последовательных вхождений упомянутых БРВ. Например, шаблону \(.*\)\1 удовлетворяют пары стоящих рядом одинаковых цепочек символов.За односимвольными   БРВ, подвыражениями и обратными ссылками может следовать запись вида \{m\}, \{m,\} или \{m,n\}, называемая интервальным выражением. Такая конструкция успешно сопоставляется с определенным числом вхождений упомянутых БРВ. Значения m и n должны удовлетворять неравенствамКонкатенация компонентных БРВ есть БРВ, которое успешно сопоставляется с конкатенацией цепочек, удовлетворяющих каждому из компонентных БРВ.На БРВ можно наложить ограничение, чтобы успешно сопоставленная   подцепочка символов примыкала к одной или обоим границам анализируемой цепочки (произвести фиксацию границ). Если в начале БРВ стоит символ ^ (фиксатор начала), то сопоставление должно выполняться с начала цепочки.   Если в конце БРВ стоит символ $ (фиксатор конца), то сопоставление должно выполняться до конца цепочки. Например, БРВ ^\(.*\)\1$ успешно сопоставляется с цепочками символов, состоящими из двух одинаковых подцепочек.

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

Опишем отличия расширенных регулярных выражений (РРВ) от базовых.

В число специальных символов дополнительно входят круглые скобки, открывающая фигурная скобка, а также символы +, ?, |.Для группирования (выделения подвыражений) используются неэкранированные круглые скобки.Отсутствуют обратные ссылки.Добавлены два повторителя: плюс обозначает любое ненулевое число последовательных вхождений; знак вопроса - нуль или одно вхождение.Для выделения интервальных выражений используются неэкранированные фигурные скобки.Введена операция логического ИЛИ, которая обозначается символом | и имеет низший приоритет.Результат сопоставляется с цепочками, удовлетворяющими хотя бы одному из операндов. Например, РРВ   a((bc)|d) успешно сопоставляется и с цепочкой символов   "abc", и с "ad". Односимвольные РРВ, разделенные символом | и заключенные в круглые скобки, трактуются как односимвольные.

Таковы правила построения и обработки регулярных выражений, зафиксированные в стандарте POSIX-2001. Отметим, что базовые регулярные выражения не являются подмножеством расширенных, хотя число специфических особенностей БРВ невелико.


Средства обработки каталогов


Обработка каталогов, как и обычных файлов, начинается с их открытия. Для этого предназначена функция opendir() (см. пример 6.50).

#include <dirent.h> DIR *opendir (const char *dirname);

Листинг 6.50. Описание функции opendir(). (html, txt)

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

#include <dirent.h> void rewinddir (DIR *dirp);

Листинг 6.51. Описание функции rewinddir(). (html, txt)

Чтение элементов каталога выполняет функция readdir() (см. пример 6.52), которая возвращает указатель на структуру, представляющую текущий элемент каталога; после ее завершения текущим становится следующий элемент достижении конца каталога и в случае ошибки возвращается пустой указатель. Следовательно, если приложению необходимо различать обе ситуации, оно должно обнулить значение переменной   errno перед вызовом readdir(), а затем, если результат равен NULL, проанализировать это значение.

#include <dirent.h> struct dirent *readdir (DIR *dirp);

Листинг 6.52. Описание функции readdir(). (html, txt)

Согласно стандарту POSIX-2001, структура dirent содержит по крайней мере одно поле:

char d_name []; /* Имя файла */

В качестве необязательного описано еще одно поле:

ino_t d_ino; /* Порядковый номер файла */

Если элемент каталога представляет символьную ссылку, значение этого поля не определено.

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

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

#include <dirent.h> int closedir (DIR *dirp);

Листинг 6.53. Описание функции closedir(). (html, txt)


Нередко чтение элементов каталога и сопоставление с шаблоном имен файлов сочетаются (см. выше раздел "Генерация маршрутных имен файлов"). Подобное сопоставление реализует функция fnmatch() (см. пример 6.54).

#include <fnmatch.h> int fnmatch (const char *file_pattern, const char *file_name, int flags);

Листинг 6.54. Описание функции fnmatch(). (html, txt)

На процесс сопоставления имени file_name с шаблоном   file_pattern влияют следующие флаги:

FNM_PATHNAME

Трактовать имя file_name как маршрутное. Символу / в имени должен явным образом сопоставляться этот же символ в шаблоне (а не *, ? или выражение в квадратных скобках). Если флаг не установлен, символ / трактуется наравне с другими.

FNM_NOESCAPE

При наличии этого флага символ \ трактуется наравне с другими. В противном случае он играет экранирующую роль.

FNM_PERIOD

Если этот флаг установлен, точка в начале имени файла должна сопоставляться с точкой в шаблоне. Иначе точка в начале имени сопоставляется на общих основаниях.

В случае успешного сопоставления функция fnmatch() возвращает ноль, при неудаче результат равен FNM_NOMATCH, в случае ошибки возвращается другое ненулевое значение.

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

Листинг 6.55. Пример использования функций, обрабатывающих каталоги. (html, txt)


Опрос и изменение атрибутов процессов


Для выдачи информации о процессах служит утилита ps:

ps [-aA] [-defl] [-G список_групп] [-o формат] ... [-p список_процессов] [-t список_терминалов] [-U список_пользователей] -g список_групп] [-n список_имен] [-u список_пользователей]

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

PID TTY TIME CMD 1594 ttyS4 00:00:02 sh 1645 ttyS4 00:00:00 sh 1654 ttyS4 00:02:45 rk.20.01 18356 ttyS4 00:00:00 prconecm 18357 ttyS4 00:00:00 sh 18358 ttyS4 00:00:00 ps

Листинг 7.1. Возможный результат использования утилиты ps. (html, txt)

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

-a

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

-A

Выдать информацию обо всех процессах.

-G список_групп

Выдать информацию о процессах с заданными реальными идентификаторами групп.

-o формат

Выдать информацию о процессах в заданном формате.

-p список_процессов

Выдать информацию о процессах с заданными идентификаторами.

-t список_терминалов

Выдать информацию о процессах, ассоциированных с заданными терминалами. Способ задания терминалов зависит от реализации. Обычно указывается имя специального файла, например, ttyS4, или, если имя начинается с tty, просто S4.

-U список_пользователей

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


Все перечисленные опции, кроме -o, ведают отбором процессов. Если задан целый ряд подобных опций, выводится информация обо всех специфицированных ими процессах.

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

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

ruser (RUSER)

Выдавать реальный идентификатор пользователя процесса (в символьной или числовой форме).

user (USER)

Действующий идентификатор пользователя процесса.

rgroup (RGROUP)

Реальный идентификатор группы процесса.

group (GROUP)

Действующий идентификатор группы процесса.

pid (PID)

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

ppid (PPID)

Идентификатор родительского процесса.

pgid (PGID)

Идентификатор группы процессов.

pcpu (%CPU)

Процент процессорного времени, потребляемый процессом.

vsz (VSZ)

Размер процесса в (виртуальной) памяти (в килобайтных блоках).

nice (NI)

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

etime (ELAPSED)

Астрономическое время, прошедшее с момента запуска процесса.

time (TIME)

Процессорное время, потребленное процессом.

tty (TT)

Имя управляющего терминала.

comm (COMMAND)

Имя выполняемой команды (argv [0]).

args (COMMAND)

Выполняемая командная строка.



Листинг 7.3. Фрагмент возможного результата использования утилиты ps.

Для опроса идентификаторов процесса, родительского процесса и группы процессов предусмотрены функции getpid() и getppid()   getpgrp() (см. листинг 7.4).

#include <unistd.h> pid_t getpid (void);

#include <unistd.h> pid_t getppid (void);

#include <unistd.h> pid_t getpgrp (void);

Листинг 7.4. getpid(), getppid() и getpgrp().

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

Для установки идентификатора группы процессов в целях управления заданиями предназначена функция setpgid() (см. листинг 7.5).

#include <unistd.h> int setpgid (pid_t pid, pid_t pgid);

Листинг 7.5. Описание функции setpgid().

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

В случае успешного завершения функции setpgid() (результат при этом равен нулю) идентификатор группы процессов устанавливается равным pgid для заданного аргументом pid процесса. Если значение pid равно нулю, установка производится для вызывающего процесса. А если значение pgid равно нулю, то в качестве идентификатора группы процессов используется идентификатор процесса, заданного аргументом pid.

Для создания сеанса и установки идентификатора группы процессов служит функция setsid() (см. листинг 7.6).

#include <unistd.h> pid_t setsid (void);

Листинг 7.6. Описание функции setsid().

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

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


листинг 7.9). Как и getpid(), они всегда завершаются успешно.

#include <unistd.h> uid_t getuid (void);

#include <unistd.h> uid_t geteuid (void);

#include <unistd.h> gid_t getgid (void);

#include <unistd.h> gid_t getegid (void);

Листинг 7.9. Описание функций getuid(), geteuid(), getgid(), getegid().

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

#include <unistd.h> int getgroups (int gidsetsize, gid_t grouplist []);

Листинг 7.10. Описание функции getgroups().

Аргумент gidsetsize задает число элементов в массиве grouplist, а реальное количество записанных идентификаторов групп возвращается в виде результата функции. Если в качестве значения gidsetsize задать нуль, getgroups() выдаст количество дополнительных групп, не модифицируя массив grouplist.

Переустановить действующий идентификатор пользователя вызывающего процесса позволяют функции setuid() и seteuid() (см. листинг 7.11). Операция разрешена, если реальный или сохраненный ПДП-идентификатор пользователя совпадает со значением аргумента uid. Помимо этого, обладающие соответствующими привилегиями процессы с помощью функции setuid() могут установить по значению uid все три идентификатора пользователя процесса – реальный, действующий и сохраненный.

#include <unistd.h> int setuid (uid_t uid);

#include <unistd.h> int seteuid (uid_t uid);

Листинг 7.11. Описание функций setuid() и seteuid().

Для непривилегированных процессов по соображениям мобильности рекомендуется использование функции seteuid().

Аналогичные функции для переустановки идентификаторов группы процесса показаны в листинге 7.12.

#include <unistd.h> int setgid (gid_t gid);

#include <unistd.h> int setegid (gid_t gid);

Листинг 7.12. Описание функций setgid() и setegid().



Листинг 7.13. Пример использования функций опроса и изменения идентификаторов пользователя процесса.

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

Идентификаторы пользователя текущего процесса: реальный: 108, действующий: 108 Идентификаторы группы текущего процесса: реальный: 3, действующий: 3 Количество дополнительных групп текущего процесса: 1 Идентификаторы дополнительных групп текущего процесса: 3 setuid (1): Operation not permitted Идентификаторы пользователя текущего процесса после первой смены: реальный: 108, действующий: 108 Идентификаторы пользователя текущего процесса после второй смены: реальный: 108, действующий: 108 seteuid (1): Operation not permitted Идентификаторы пользователя текущего процесса после третьей смены: реальный: 108, действующий: 108

Листинг 7.14. Возможный результат работы программы, показанной в листинге 7.13 и запущенной от имени обычного пользователя.

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

Идентификаторы пользователя текущего процесса: реальный: 0, действующий: 0 Идентификаторы группы текущего процесса: реальный: 0, действующий: 0 Количество дополнительных групп текущего процесса: 7 Идентификаторы дополнительных групп текущего процесса: 0 1 2 3 4 6 10 Идентификаторы пользователя текущего процесса после первой смены: реальный: 1, действующий: 1 setuid (uid): Operation not permitted Идентификаторы пользователя текущего процесса после второй смены: реальный: 1, действующий: 1 Идентификаторы пользователя текущего процесса после третьей смены: реальный: 1, действующий: 1

Листинг 7.15. Возможный результат работы программы, показанной в листинге 7.13 и запущенной от имени суперпользователя.

Утерять статус суперпользователя легко, а вернуть трудно...

Наконец, сделаем владельцем выполнимого файла рассматриваемой программы пользователя с идентификатором 1, то же проделаем с владеющей группой, взведем в режиме этого файла биты ПДИП и ПДИГ(на ОС Linux можно воспользоваться командой chmod ug+s) и вновь запустим его от имени обычного пользователя (см.

Основные понятия


Напомним данное в стандарте POSIX-2001 определение процесса. Процесс – это адресное пространство вместе с выполняемыми в нем потоками управления, а также системными ресурсами, которые этим потокам требуются.

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

Процессы могут создаваться и завершаться. Время жизни процесса – это период от его создания до возврата идентификатора операционной системе.

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

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

Завершение процесса может быть нормальным или аварийным. Нормальное завершение происходит, в частности, при возврате из функции main().

Зомби-процесс – завершившийся процесс, подлежащий ликвидации после того, как код его завершения будет передан ожидающему этого другому процессу.

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

Группа – совокупность процессов, допускающая согласованную доставку сигналов. У каждой группы имеется уникальный положительный целочисленный идентификатор, представляющий ее в течение времени ее жизни. В такой роли выступает идентификатор процесса, именуемого лидером группы.

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


Задание – это набор процессов, составляющих конвейер, а также порожденных ими процессов, входящих в одну группу.

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

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

Время жизни сеанса представляет собой период от создания сеанса до истечения времени жизни всех групп процессов, принадлежавших сеансу.

Лидер сеанса – процесс, создавший данный сеанс.

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

Управляющий процесс – это лидер сеанса, установивший соединение с управляющим терминалом. Если в дальнейшем терминал перестанет быть управляющим для сеанса, лидер сеанса утратит статус управляющего процесса.

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

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

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



Задание – это набор процессов, составляющих конвейер, а также порожденных ими процессов, входящих в одну группу.

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

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

Время жизни сеанса представляет собой период от создания сеанса до истечения времени жизни всех групп процессов, принадлежавших сеансу.

Лидер сеанса – процесс, создавший данный сеанс.

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

Управляющий процесс – это лидер сеанса, установивший соединение с управляющим терминалом. Если в дальнейшем терминал перестанет быть управляющим для сеанса, лидер сеанса утратит статус управляющего процесса.

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

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

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


PID TTY TIME CMD 1594


PID TTY TIME CMD 1594 ttyS4 00:00:02 sh 1645 ttyS4 00:00:00 sh 1654 ttyS4 00:02:45 rk.20.01 18356 ttyS4 00:00:00 prconecm 18357 ttyS4 00:00:00 sh 18358 ttyS4 00:00:00 ps
Листинг 7.1. Возможный результат использования утилиты ps.
Закрыть окно




ps -A -o ruser,user,pid,ppid,tty=TTY -o nice,vsz,args
Листинг 7.2. Пример использования утилиты ps.
Закрыть окно




RUSER USER PID PPID TTY NI VSZ COMMAND root root 1 0 ? 0 1372 init [5] root root 4 1 ? 19 0 [ksoftirqd_CPU0] root root 555 1 ? 0 1428 syslogd -m 0 root root 560 1 ? 0 1364 klogd -x rpc rpc 580 1 ? 0 1508 portmap rpcuser rpcuser 608 1 ? 0 1560 rpc.statd root root 743 1 ? 0 2620 /usr/sbin/sshd root root 776 1 ? 0 2200 xinetd -stayalive -reuse -pidfi root root 805 1 ? 0 1500 rpc.rquotad root root 810 1 ? 0 1504 rpc.mountd root root 897 1 ? 0 3236 /usr/libexec/postfix/master postfix postfix 906 897 ? 0 3384 nqmgr -l -n qmgr -t fifo -u -c root root 918 1 ? 0 1400 gpm -t ps/2 -m /dev/mouse root root 936 1 ? 0 1548 crond xfs xfs 968 1 ? 0 4432 xfs -droppriv -daemon nobody nobody 987 1 ? 0 36488 dictd 1.9.7: 0/0 root daemon 1022 1 ? 0 1404 /usr/sbin/atd root root 1057 1 ? 0 5768 cupsd root root 1064 1 tty1 0 1344 /sbin/mingetty tty1 root root 1070 1 ttyS2 0 1352 /sbin/agetty -i -L ttyS2 38400 root root 1072 1 ? 0 2300 login -- galat galat galat 1086 1072 ttyS4 0 2260 -sh root root 1124 1085 ? 0 16900 /usr/bin/kdm_greet postfix postfix 1826 897 ? 0 3304 pickup -l -t fifo -u -c galat galat 2013 1171 ttyS4 0 1940 /bin/sh -c ps -A -o user,pid,pp galat galat 2014 2013 ttyS4 0 2584 ps -A -o user,pid,ppid,tty=TTY
Листинг 7.3. Фрагмент возможного результата использования утилиты ps.
Закрыть окно




#include <unistd.h> pid_t getpid (void);
#include <unistd.h> pid_t getppid (void);
#include <unistd.h> pid_t getpgrp (void);
Листинг 7.4. getpid(), getppid() и getpgrp().
Закрыть окно




#include <unistd.h> int setpgid ( pid_t pid, pid_t pgid);
Листинг 7.5. Описание функции setpgid().
Закрыть окно




#include <unistd.h> pid_t setsid (void);
Листинг 7.6. Описание функции setsid().
Закрыть окно




#include <unistd.h> #include <sys/types.h> #include <stdio.h> int main (void) { pid_t ppid; pid_t pgid; /* Отменим буферизацию стандартного вывода */ setbuf (stdout, NULL); printf ("Атрибуты текущего процесса: pid: %d, ppid: %d, pgid: %d\n", getpid (), ppid = getppid (), pgid = getpgrp ()); /* Выделимся в новую группу */ if (setpgid (0, 0) != 0) { perror ("setpgid (0, 0)"); } printf ("Новая группа текущего процесса: %d\n", getpgrp ()); /* Попробуем создать новый сеанс */ if (setsid () == (pid_t) (-1)) { perror ("setsid от имени лидера группы"); } /* Вернемся в прежнюю группу */ if (setpgid (0, pgid) != 0) { perror ("setpgid (0, pgid)"); } printf (" Группа текущего процесса после повторной смены: %d\n", getpgrp ()); /* Повторим попытку создать новый сеанс */ if (setsid () == (pid_t) (-1)) { perror ("setsid от имени не-лидера группы"); } printf ("Группа текущего процесса после создания нового сеанса: %d\n", getpgrp ()); /* Попробуем сменить группу родительского процесса */ if (setpgid (ppid, 0) != 0) { perror ("setpgid (ppid, 0)"); } /* Попробуем установить несуществующий */ /* идентификатор группы процессов */ if (setpgid (0, ppid) != 0) { perror ("setpgid (0, ppid)"); } return (0); }
Листинг 7.7. Пример программы, использующей функции getpid(), getppid(), getpgrp(), setpgid(), setsid().
Закрыть окно




Атрибуты текущего процесса: pid: 11726, ppid: 11725, pgid: 1153 Новая группа текущего процесса: 11726 setsid от имени лидера группы: Operation not permitted Группа текущего процесса после повторной смены: 1153 Группа текущего процесса после создания нового сеанса: 11726 setpgid (ppid, 0): No such process setpgid (0, ppid): Operation not permitted
Листинг 7.8. Возможный результат работы программы, показанной в листинге 7.7.
Закрыть окно




#include <unistd.h> uid_t getuid (void);
#include <unistd.h> uid_t geteuid (void);
#include <unistd.h> gid_t getgid (void);
#include <unistd.h> gid_t getegid (void);
Листинг 7.9. Описание функций getuid(), geteuid(), getgid(), getegid().
Закрыть окно




#include <unistd.h> int getgroups ( int gidsetsize, gid_t grouplist []);
Листинг 7.10. Описание функции getgroups().
Закрыть окно




#include <unistd.h> int setuid (uid_t uid);
#include <unistd.h> int seteuid (uid_t uid);
Листинг 7.11. Описание функций setuid() и seteuid().
Закрыть окно




#include <unistd.h> int setgid (gid_t gid);
#include <unistd.h> int setegid (gid_t gid);
Листинг 7.12. Описание функций setgid() и setegid().
Закрыть окно




#include <unistd.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h>
int main (void) { uid_t uid; int nsupgroups; gid_t *supgroupslist; int i; /* Отменим буферизацию стандартного вывода */ setbuf (stdout, NULL); printf ("Идентификаторы пользователя текущего процесса:\n" " реальный: %d, действующий: %d\n", uid = getuid (), geteuid ()); printf ("Идентификаторы группы текущего процесса:\n" " реальный: %d, действующий: %d\n", getgid (), getegid ()); printf ("Количество дополнительных групп текущего процесса: %d\n", nsupgroups = getgroups (0, supgroupslist)); if (nsupgroups > 0) { if ((supgroupslist = (gid_t *) malloc (nsupgroups * sizeof (gid_t))) == NULL) { perror ("MALLOC"); } else if (getgroups (nsupgroups, supgroupslist) == (-1)) { perror ("GETGROUPS"); } else { /* Выдадим идентификаторы дополнительных */ /* групп процесса */ printf ("Идентификаторы дополнительных групп текущего процесса:\n"); for (i = 0; i < nsupgroups; i++) { printf (" %d", supgroupslist [i]); } printf ("\n"); } } /* Попробуем переустановить идентификатор */ /* пользователя процесса */ if (setuid ((uid_t) 1) != 0) { perror ("setuid (1)"); } printf ("Идентификаторы пользователя текущего процесса после первой смены:\n" " реальный: %d, действующий: %d\n", getuid (), geteuid ()); /* Попробуем вернуть прежний идентификатор */ /* пользователя процесса */ if (setuid (uid) != 0) { perror ("setuid (uid)"); } printf ("Идентификаторы пользователя текущего процесса после второй смены:\n" " реальный: %d, действующий: %d\n", getuid (), geteuid ()); /* Попробуем сменить действующий идентификатор */ /* с помощью функции seteuid() */ if (seteuid ((uid_t) 1) != 0) { perror ("seteuid (1)"); } printf ("Идентификаторы пользователя текущего процесса после третьей смены:\n" " реальный: %d, действующий: %d\n", getuid (), geteuid ()); return (0); }
Листинг 7.13. Пример использования функций опроса и изменения идентификаторов пользователя процесса.
Закрыть окно




Идентификаторы пользователя текущего процесса: реальный: 108, действующий: 108 Идентификаторы группы текущего процесса: реальный: 3, действующий: 3 Количество дополнительных групп текущего процесса: 1 Идентификаторы дополнительных групп текущего процесса: 3 setuid (1): Operation not permitted Идентификаторы пользователя текущего процесса после первой смены: реальный: 108, действующий: 108 Идентификаторы пользователя текущего процесса после второй смены: реальный: 108, действующий: 108 seteuid (1): Operation not permitted Идентификаторы пользователя текущего процесса после третьей смены: реальный: 108, действующий: 108
Листинг 7.14. Возможный результат работы программы, показанной в листинге 7.13 и запущенной от имени обычного пользователя.
Закрыть окно




Идентификаторы пользователя текущего процесса: реальный: 0, действующий: 0 Идентификаторы группы текущего процесса: реальный: 0, действующий: 0 Количество дополнительных групп текущего процесса: 7 Идентификаторы дополнительных групп текущего процесса: 0 1 2 3 4 6 10 Идентификаторы пользователя текущего процесса после первой смены: реальный: 1, действующий: 1 setuid (uid): Operation not permitted Идентификаторы пользователя текущего процесса после второй смены: реальный: 1, действующий: 1 Идентификаторы пользователя текущего процесса после третьей смены: реальный: 1, действующий: 1
Листинг 7.15. Возможный результат работы программы, показанной в листинге 7.13 и запущенной от имени суперпользователя.
Закрыть окно




Идентификаторы пользователя текущего процесса: реальный: 108, действующий: 1 Идентификаторы группы текущего процесса: реальный: 3, действующий: 1 Количество дополнительных групп текущего процесса: 1 Идентификаторы дополнительных групп текущего процесса: 3 Идентификаторы пользователя текущего процесса после первой смены: реальный: 108, действующий: 1 Идентификаторы пользователя текущего процесса после второй смены: реальный: 108, действующий: 108 Идентификаторы пользователя текущего процесса после третьей смены: реальный: 108, действующий: 1
Листинг 7.16. Возможный результат работы программы, показанной в листинге 7.13 и запущенной от имени обычного пользователя после взведения в режиме выполнимого файла бита ПДИП.
Закрыть окно




#include <sys/stat.h>> mode_t umask (mode_t cmask);
Листинг 7.17. Описание функции umask().
Закрыть окно




umask 0 umask -S umask -- -x umask
Листинг 7.18. Пример использования служебной программы umask.
Закрыть окно




u=rwx,g=rwx,o=rwx 0111
Пример 7.19. Листинг 7.19. Возможный результат использования служебной программы umask.
Закрыть окно




#include <unistd.h> pid_t fork (void);
Листинг 7.20. Описание функции fork().
Закрыть окно




int main ( int argc, char *argv []);
Пример 7.21. Заголовок функции main() C-программы.
Закрыть окно




#include <unistd.h> extern char **environ; int execl (const char *path, const char *arg0, ... /*, (char *) 0 */); int execv (const char *path, char *const argv []); int execle (const char *path, const char *arg0, ... /*, (char *) 0, char *const envp [] */); int execve (const char *path, char *const argv [], char *const envp []); int execlp (const char *file, const char *arg0, ... /*, (char *) 0 */); int execvp (const char *file, char *const argv []);
Пример 7.22. Описание функций семейства exec().
Закрыть окно




#include <sys/wait.h> pid_t wait (int *stat_loc); pid_t waitpid ( pid_t pid, int *stat_loc, int options);
Пример 7.23. Описание функций семейства wait().
Закрыть окно




#include <stdlib.h> void exit (int status); void _Exit (int status); #include <unistd.h> void _exit (int status);
Пример 7.24. Описание функций семейства exit().
Закрыть окно




#include <stdlib.h> int atexit (void (*func) (void));
Пример 7.25. Описание функции atexit().
Закрыть окно




#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <sys/wait.h>
static void atefunc (void) { /* Перед завершением выдадим информацию о */ /* процессах */ printf ("Ситуация перед завершением родительского процесса\n"); (void) system ("ps -o pid,ppid,vsz,args"); }
int main (void) { int pid; int stat; /* Отменим буферизацию стандартного вывода */ setbuf (stdout, NULL); /* Зарегистрируем обработчик завершения процесса */ if (atexit (atefunc) != 0) { perror ("ATEXIT"); exit (1); } /* Создадим новый процесс */ if ((pid = fork ()) < 0) { perror ("FORK"); exit (2); } else if (pid == 0) { /* Порожденный процесс */ /* Выполним служебную программу ps */ printf ("Ситуация с точки зрения порожденного процесса\n"); (void) execl ("/bin/ps", "ps", "-o", "pid,ppid,args", (char *) 0); perror ("EXEC"); exit (3); /* execl() завершился неудачей */ } else { /* Родительский процесс */ sleep (1); /* Вероятно, порожденный процесс уже */ /* завершился */ /* Посмотрим на текущую ситуацию */ printf ("Ситуация перед вызовом waitpid() в родительском процессе\n"); (void) system ("ps -o pid,ppid,vsz,args"); (void) waitpid (pid, &stat, 0); printf ("Статус завершения порожденного процесса с идентификатором %d: %d\n", pid, stat); } return 0; }
Пример 7.26. Пример использования функций порождения и завершения процессов.
Закрыть окно




Ситуация с точки зрения порожденного процесса PID PPID COMMAND 6123 1072 -sh 29568 6123 prog30 29569 29568 ps -o pid,ppid,args Ситуация перед вызовом waitpid() в родительском процессе PID PPID VSZ COMMAND 6123 1072 2260 -sh 29568 6123 1340 prog30 29569 29568 0 [ps <defunct>] 29570 29568 2584 ps -o pid,ppid,vsz,args Статус завершения порожденного процесса с идентификатором 29569: 0 Ситуация перед завершением родительского процесса PID PPID VSZ COMMAND 6123 1072 2260 -sh 29568 6123 1340 prog30 29571 29568 2584 ps -o pid,ppid,vsz,args
Пример 7.27. Возможный результат работы программы, использующей функции порождения и завершения процессов.
Закрыть окно



Создание и завершение процессов


Новые процессы создаются при помощи функции fork() (см. листинг 7.20).

#include <unistd.h> pid_t fork (void);

Листинг 7.20. Описание функции fork(). (html, txt)

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

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

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

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

Напомним, что заголовок функции main() C-программы выглядит в общем случае так, как показано в листинге 7.21.

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

Пример 7.21. Заголовок функции main() C-программы. (html, txt)

Значение argc равно количеству аргументов; argv – это массив указателей собственно на аргументы, которые определяются исходя из командной строки, запускающей C-программу.
В соответствии с принятым соглашением, значение argc не меньше единицы, а первый элемент массива argv указывает на цепочку символов, содержащую имя выполняемого файла.

Аналогичный смысл имеют аргументы функций семейства exec() (см. листинг 7.22).

Пример 7.22. Описание функций семейства exec(). (html, txt)

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

Переменная environ инициализируется как указатель на массив указателей на составляющие окружение цепочки символов. Массивы argv и environ завершаются пустым указателем.

Аргумент path указывает на маршрутное имя файла с новым образом процесса.

Аргумент file имеет аналогичный смысл, однако, если он задан как простое имя, то производится поиск в каталогах, заданных переменной окружения PATH.

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

Аргумент envp имеет тот же смысл и назначение, что и переменная environ.

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

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

Следующие атрибуты процесса остаются неизменными:

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

Родительский процесс реализует ожидание завершения процессов-потомков и получает информацию о его (завершения) статусе с помощью функций семейства wait() (см.


листинг 7.23). Если информация о статусе завершения была доступна до вызова wait(), родительский процесс не приостанавливается, возврат из wait() происходит немедленно.

#include <sys/wait.h> pid_t wait (int *stat_loc); pid_t waitpid (pid_t pid, int *stat_loc, int options);

Пример 7.23. Описание функций семейства wait(). (html, txt)

Функция waitpid() эквивалентна wait(), если аргумент pid равен (pid_t) (-1), а аргумент options имеет нулевое значение. Аргумент pid задает набор порожденных процессов, статус завершения которых запрашивается. Значение (pid_t) (-1) представляет произвольный элемент множества порожденных процессов. Если pid > 0>, имеется в виду один процесс с данным идентификатором. Нулевое значение специфицирует любого потомка из той же группы процессов, что и вызывающий. Наконец, при pid < (pid_t) (-1) запрашивается статус завершения любого порожденного процесса из группы, идентификатор которой равен абсолютной величине pid.


Каналы


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

К числу наиболее простых и в то же время самых употребительных средств межпроцессного взаимодействия принадлежат каналы, представляемые файлами соответствующего типа. Стандарт POSIX-2001 различает именованные и безымянные каналы. Напомним, что первые создаются функцией mkfifo() и одноименной служебной программой, а вторые - функцией pipe(). Именованным каналам соответствуют элементы файловой системы, ко вторым можно обращаться только посредством файловых дескрипторов. В остальном эти разновидности каналов эквивалентны.

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

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

Решение той же задачи, но с использованием функций буферизованного ввода/вывода, показано в листинге 8.2.

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

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

Подчеркнем, что при попытке чтения из пустого канала результат равен 0 (как признак конца файла), только если канал не открыт кем-либо на запись.
Под "кем-либо" понимается и сам читающий процесс; по этой причине в приведенной выше программе потребовалось закрыть все экземпляры файлового дескриптора   fd [1], возвращенного функцией pipe() как дескриптор для записи в канал.

Функция popen(), описанная выше, при рассмотрении командного интерпретатора, является более высокоуровневой по сравнению с pipe(). Она делает сразу несколько вещей: порождает процесс, обеспечивает выполнение в его рамках заданной команды, организует канал между вызывающим и порожденным процессами и формирует необходимые потоки для этого канала. Если при обращении к popen() задан режим "w", то стандартный ввод команды, выполняющейся в рамках порожденного процесса, перенаправляется на конец канала, предназначенный для чтения; если задан режим "r", то в канал перенаправляется стандартный вывод.

После вызова popen() процесс может писать в канал или читать из него посредством функций буферизованного ввода/вывода, используя сформированный поток. Канал остается открытым до момента вызова функции pclose() (см. листинг 8.3).

#include <stdio.h> int pclose (FILE *stream);

Листинг 8.3. Описание функции pclose(). (html, txt)

Функция pclose() не только закрывает поток, сформированный popen(), но и дожидается завершения порожденного процесса, возвращая его статус.

Типичное применение popen() - организация канала для выдачи динамически порождаемых данных на устройство печати командой lp (см. листинг 8.4).

Листинг 8.4. Пример создания и использования канала для вывода данных. (html, txt)

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

Листинг 8.5. Пример создания и использования канала для ввода данных. (html, txt)



exit (0); }

/* Чтение со стандартного ввода и запись в канал */ /* возложим на родительский процесс. */ /* Из соображений симметрии закроем поток, */ /* предназначенный для чтения из канала */ fclose (fp [0]); fputs ("Вводите строки\n", fp [1]); while (fgets (line, sizeof (line), stdin) != NULL) { if ((fputs ("Вы ввели: ", fp [1]) == EOF) || (fputs (line, fp [1]) == EOF)) { break; } } fclose (fp [1]);

(void) wait (NULL); return (0); }

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

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

Подчеркнем, что при попытке чтения из пустого канала результат равен 0 (как признак конца файла), только если канал не открыт кем-либо на запись. Под "кем-либо" понимается и сам читающий процесс; по этой причине в приведенной выше программе потребовалось закрыть все экземпляры файлового дескриптора   fd [1], возвращенного функцией pipe() как дескриптор для записи в канал.

Функция popen(), описанная выше, при рассмотрении командного интерпретатора, является более высокоуровневой по сравнению с pipe(). Она делает сразу несколько вещей: порождает процесс, обеспечивает выполнение в его рамках заданной команды, организует канал между вызывающим и порожденным процессами и формирует необходимые потоки для этого канала. Если при обращении к popen() задан режим "w", то стандартный ввод команды, выполняющейся в рамках порожденного процесса, перенаправляется на конец канала, предназначенный для чтения; если задан режим "r", то в канал перенаправляется стандартный вывод.



После вызова popen() процесс может писать в канал или читать из него посредством функций буферизованного ввода/вывода, используя сформированный поток. Канал остается открытым до момента вызова функции pclose() (см. листинг 8.3).

#include <stdio.h> int pclose (FILE *stream);

Листинг 8.3. Описание функции pclose().

Функция pclose() не только закрывает поток, сформированный popen(), но и дожидается завершения порожденного процесса, возвращая его статус.

Типичное применение popen() - организация канала для выдачи динамически порождаемых данных на устройство печати командой lp (см. листинг 8.4).

#include <stdio.h> /* Программа печатает несколько первых строк треугольника Паскаля */ #define T_SIZE 16 int main (void) { FILE *outptr; long tp [T_SIZE]; /* Массив для хранения текущей строки треугольника */ int i, j;

/* Инициализируем массив, чтобы далее все элементы */ /* можно было считать и выводить единообразно */ tp [0] = 1; for (i = 1; i < T_SIZE; i++) { tp [i] = 0; }

/* Создадим канал с командой */ if ((outptr = popen ("lp", "w")) == NULL) { perror ("POPEN"); return (-1); }

(void) fprintf (outptr, "\nТреугольник Паскаля:\n");

for (i = 0; i < T_SIZE; i++) { /* Элементы очередной строки нужно считать от конца к началу */ /* Элемент tp [0] пересчитывать не нужно */ for (j = i; j > 0; j--) { tp [j] += tp [j - 1]; } /* Вывод строки треугольника в канал */ for (j = 0; j <= i; j++) { (void) fprintf (outptr, " %ld", tp [j]); } (void) fprintf (outptr, "\n"); }

return (pclose (outptr)); }

Листинг 8.4. Пример создания и использования канала для вывода данных.

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

#include <stdio.h> #include <limits.h> #include <assert.h>

#define MY_CMD "ls -l *.c"

int main (void) { FILE *inptr; char line [LINE_MAX];

assert ((inptr = popen (MY_CMD, "r")) != NULL);

while (fgets (line, sizeof (line), inptr) != NULL) { fputs (line, stdout); }

return (pclose (inptr)); }

Листинг 8.5. Пример создания и использования канала для ввода данных.


Очереди сообщений


Мы переходим к рассмотрению средств локального межпроцессного взаимодействия, относящихся к необязательной части стандарта POSIX-2001, именуемой "X/Open-расширение системного интерфейса" (XSI). Будут описаны очереди сообщений, семафоры и разделяемые сегменты памяти.

Остановимся сначала на понятиях и структурах, общих для всех трех упомянутых средств.

Каждая очередь сообщений, набор семафоров и разделяемый сегмент однозначно идентифицируются положительным целым числом, которое обычно обозначается, соответственно, как msqid, semid и shmid и возвращается в качестве результатов функций msgget(), semget() и shmget().

При получении идентификаторов средств межпроцессного взаимодействия используется еще одна сущность - ключ, а для его генерации предназначена функция ftok() (см. листинг 8.22). Аргумент path должен задавать маршрутное имя существующего файла, к которому вызывающий процесс может применить функцию stat(). В качестве значения аргумента id, по соображениям мобильности, рекомендуется использовать однобайтный символ. Гарантируется, что функция ftok() сгенерирует один и тот же ключ для заданной пары (файл, символ) и разные ключи для разных пар.

#include <sys/ipc.h> key_t ftok (const char *path, int id);

Листинг 8.22. Описание функции ftok(). (html, txt)

С идентификатором средства межпроцессного взаимодействия ассоциирована структура данных, содержащая информацию о допустимых и выполненных операциях. Соответствующие декларации сосредоточены в заголовочных файлах <sys/msg.h>, <sys/sem.h> и <sys/shm.h>.

В упомянутую структуру входит подструктура ipc_perm с данными о владельцах и режимом доступа, описанная в файле <sys/ipc.h> и содержащая по крайней мере следующие поля.

uid_t uid; /* Идентификатор владельца */ gid_t gid; /* Идентификатор владеющей группы */ uid_t cuid; /* Идентификатор пользователя, создавшего данное средство межпроцессного взаимодействия */ gid_t cgid; /* Идентификатор создавшей группы */ mode_t mode; /* Режим доступа на чтение/запись */


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

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

ipcs [-qms] [-a | -bcopt]

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

Следующие опции управляют форматом выдачи. Задание опции -a равносильно указанию всех опций формата. Опция -b предписывает выдавать лимиты на размер (максимальное количество байт в сообщениях очереди и т.п.), -c - имена пользователя и группы создателя средства, -o - информацию об использовании (количество сообщений в очереди, их суммарный размер и т.п.), -p - информацию о процессах (идентификаторы последнего отправителя, получателя и т.п.), -t - информацию о времени (последняя управляющая операция, последняя отправка сообщения и т.п.).

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

ipcrm [-q msgid | -Q msgkey | -s semid | -S semkey | -m shmid | -M shmkey ] ...

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

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

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



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

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

Для работы с очередями сообщений стандарт POSIX-2001 предусматривает следующие функции (см. листинг 8.23): msgget() (получение идентификатора очереди сообщений), msgctl() (управление очередью сообщений), msgsnd() (отправка сообщения) и msgrcv() (прием сообщения).

#include <sys/msg.h> int msgget (key_t key, int msgflg); int msgsnd (int msqid, const void *msgp, size_t msgsz, int msgflg); ssize_t msgrcv (int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); int msgctl (int msqid, int cmd, struct msqid_ds *buf);

Листинг 8.23. Описание функций для работы с очередями сообщений.

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

struct ipc_perm msg_perm; /* Данные о правах доступа к очереди сообщений */ msgqnum_t msg_qnum; /* Текущее количество сообщений в очереди */ msglen_t msg_qbytes; /* Максимально допустимый суммарный размер сообщений в очереди */ pid_t msg_lspid; /* Идентификатор процесса, отправившего последнее сообщение */ pid_t msg_lrpid; /* Идентификатор процесса, принявшего последнее сообщение */ time_t msg_stime; /* Время последней отправки */ time_t msg_rtime; /* Время последнего приема */ time_t msg_ctime; /* Время последнего изменения посредством msgctl() */

Перейдем к детальному рассмотрению функций для работы с очередями сообщений.

Функция msgget() возвращает идентификатор очереди сообщений, ассоциированный с ключом key.


Новая очередь, ее идентификатор и соответствующая структура msqid_ds создаются для заданного ключа, если значение аргумента key равно IPC_PRIVATE или очередь еще не ассоциирована с ключом, а в числе флагов msgflg задан IPC_CREAT.

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

Структура msqid_ds для новой очереди инициализируется следующим образом.

Значения полей msg_perm.cuid, msg_perm.uid, msg_perm.cgid и msg_perm.gid устанавливаются равными действующим идентификаторам пользователя и группы вызывающего процесса.Младшие девять бит поля msg_perm.mode устанавливаются равными младшим девяти битам значения msgflg.Поля msg_qnum, msg_lspid, msg_lrpid, msg_stime и msg_rtime обнуляются.В поле msg_ctime помещается текущее время, а в поле msg_qbytes - определенный в системе лимит.

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

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

#include <stdio.h> #include <sys/ipc.h> #include <sys/msg.h>

/* Программа создает очередь сообщений. */ /* В командной строке задаются имя файла для ftok() */ /* и режим доступа к очереди сообщений */

#define FTOK_CHAR 'G'

int main (int argc, char *argv []) { key_t key; int msqid; int mode = 0;

if (argc != 3) { fprintf (stderr, "Использование: %s маршрутное_имя режим_доступа\n", argv [0]); return (1); }



if ((key = ftok (argv [1], FTOK_CHAR)) == (key_t) (-1)) { perror ("FTOK"); return (2); } (void) sscanf (argv [2], "%o", (unsigned int *) &mode);

if ((msqid = msgget (key, IPC_CREAT | mode)) < 0) { perror ("MSGGET"); return (3); }

return 0; }

Листинг 8.24. Пример программы, создающей очередь сообщений.

Если после выполнения этой программы воспользоваться командой ipcs -q, то результат может выглядеть так, как показано в листинге 8.25.

------ Message Queues -------- key msqid owner perms used-bytes messages 0x47034bac 163840 galat 644 0 0

Листинг 8.25. Возможный результат опроса статуса очередей сообщений.

Удалить созданную очередь из системы, соответствующей стандарту POSIX-2001, можно командой ipcrm -q 163840.

Операции отправки/приема сообщений выполняют функции msgsnd() и msgrcv(); msgsnd() помещает сообщения в очередь, а msgrcv() читает и "достает" их оттуда.

В обоих случаях первый аргумент задает идентификатор очереди; второй является указателем на содержащую сообщение структуру. Сообщение состоит из двух частей: текста (последовательности байт) и так называемого типа (положительного целого числа). Тип, указанный во время отправки, используется впоследствии при выборе сообщения из очереди. Аргумент msgsz определяет длину сообщения; аргумент msgflg задает флаги.

В зависимости от значения, указанного в качестве аргумента msgtyp функции msgrcv(), из очереди выбирается то или иное сообщение. Если значение аргумента равно нулю, запрашивается первое сообщение в очереди, если больше нуля - первое сообщение типа msgtyp, а если меньше нуля - первое сообщение наименьшего из типов, не превышающих абсолютную величину аргумента msgtyp. Пусть, например, в очередь последовательно помещены сообщения с типами 5, 3 и 2. Тогда вызов msgrcv (msqid, msgp, size, 0, flags) выберет из очереди сообщение с типом 5, поскольку оно отправлено первым; вызов msgrcv (msqid, msgp, size, -4, flags) - последнее сообщение, так как 2 - это наименьший из возможных типов в указанном диапазоне; наконец, вызов msgrcv (msqid, msgp, size, 3, flags) - сообщение с типом 3.



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

Если не указано противное, функции msgsnd() и msgrcv() выполняют операции с блокировкой, например: msgsnd (msqid, msgp, size, 0); msgrcv (msqid, msgp, size, type, 0). Чтобы выполнить операцию без блокировки, необходимо установить флаг IPC_NOWAIT: msgsnd (msqid, msgp, size, IPC_NOWAIT); msgrcv (msqid, msgp, size, type, IPC_NOWAIT).

Аргумент msgp указывает на значение структурного типа, в котором представлены тип и тело сообщения (см. листинг 8.26).

struct msgbuf { long mtype; /* Тип сообщения */ char mtext [1]; /* Текст сообщения */ };

Листинг 8.26. Описание структурного типа для представления сообщений.

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

#define MAXSZTMSG 8192

struct mymsgbuf { long mtype; /* Тип сообщения */ char mtext [MAXSZTMSG]; /* Текст сообщения */ }; struct mymsgbuf msgbuf;

Листинг 8.27. Описание структуры для хранения сообщений.

В качестве аргумента msgsz обычно указывается размер текстового буфера, например: sizeof (msgbuf.text).

Если не указано противное, в случае, когда длина выбранного сообщения больше, чем msgsz, вызов msgrcv() завершается неудачей.


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

При успешном завершении msgsnd() возвращает 0, а msgrcv() - значение, равное числу реально полученных байт; при неудаче возвращается -1.

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

Управляющее действие определяется значением аргумента cmd. Допустимых значений три: IPC_STAT - получить информацию о состоянии очереди, IPC_SET - переустановить характеристики очереди, IPC_RMID - удалить очередь.

Команды IPC_STAT и IPC_SET для хранения информации об очереди используют имеющуюся в прикладной программе структуру типа msqid_ds, указатель на которую содержит аргумент buf: IPC_STAT копирует в нее ассоциированную с очередью структуру данных, а IPC_SET, наоборот, в соответствии с ней обновляет ассоциированную структуру. Команда IPC_SET позволяет переустановить значения идентификаторов владельца (msg_perm.uid) и владеющей группы (msg_perm.gid), режима доступа (msg_perm.mode), максимально допустимый суммарный размер сообщений в очереди (msg_qbytes). Увеличить значение msg_qbytes может только процесс, обладающий соответствующими привилегиями.

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

#include <stdio.h> #include <sys/msg.h>

int main (int argc, char *argv []) { int msqid; struct msqid_ds msqid_ds;

if (argc != 3) { fprintf (stderr, "Использование: %s идентификатор_очереди максимальный_размер\n", argv [0]); return (1); }



(void) sscanf (argv [1], "%d", &msqid);

/* Получим исходное значение структуры данных */ if (msgctl (msqid, IPC_STAT, &msqid_ds) == -1) { perror ("IPC_STAT-1"); return (2); } printf ("Максимальный размер очереди до изменения: %ld\n", msqid_ds.msg_qbytes);

(void) sscanf (argv [2], "%d", (int *) &msqid_ds.msg_qbytes);

/* Попробуем внести изменения */ if (msgctl (msqid, IPC_SET, &msqid_ds) == -1) { perror ("IPC_SET"); }

/* Получим новое значение структуры данных */ if (msgctl (msqid, IPC_STAT, &msqid_ds) == -1) { perror ("IPC_STAT-2"); return (3); } printf ("Максимальный размер очереди после изменения: %ld\n", msqid_ds.msg_qbytes);

return 0; }

Листинг 8.28. Пример программы управления очередями сообщений.

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

#include <unistd.h> #include <stdio.h> #include <limits.h> #include <string.h> #include <sys/wait.h> #include <sys/msg.h>

/* Программа копирует строки со стандартного ввода на стандартный вывод, */ /* "прокачивая" их через очередь сообщений */

#define FTOK_FILE "/home/galat" #define FTOK_CHAR "G"

#define MSGQ_MODE 0644

#define MY_PROMPT "Вводите строки\n" #define MY_MSG "Вы ввели: "

int main (void) { key_t key; int msqid; struct mymsgbuf { long mtype; char mtext [LINE_MAX]; } line_buf, msgbuf;

switch (fork ()) { case -1: perror ("FORK"); return (1); case 0: /* Чтение из очереди и выдачу на стандартный вывод */ /* реализуем в порожденном процессе. */ (void) execl ("./msq_child", "msq_child", FTOK_FILE, FTOK_CHAR, (char *) 0); perror ("EXEC"); return (2); /* execl() завершился неудачей */ }



/* Чтение со стандартного ввода и запись в очередь */ /* возложим на родительский процесс */

/* Выработаем ключ для очереди сообщений */ if ((key = ftok (FTOK_FILE, FTOK_CHAR [0])) == (key_t) (-1)) { perror ("FTOK"); return (3); }

/* Получим идентификатор очереди сообщений */ if ((msqid = msgget (key, IPC_CREAT | MSGQ_MODE)) < 0) { perror ("MSGGET"); return (4); }

/* Приступим к отправке сообщений в очередь */ msgbuf.mtype = line_buf.mtype = 1; strncpy (msgbuf.mtext, MY_PROMPT, sizeof (msgbuf.mtext)); if (msgsnd (msqid, (void *) &msgbuf, strlen (msgbuf.mtext) + 1, 0) != 0) { perror ("MSGSND-1"); return (5); } strncpy (msgbuf.mtext, MY_MSG, sizeof (msgbuf.mtext));

while (fgets (line_buf.mtext, sizeof (line_buf.mtext), stdin) != NULL) { if (msgsnd (msqid, (void *) &msgbuf, strlen (msgbuf.mtext) + 1, 0) != 0) { perror ("MSGSND-2"); break; } if (msgsnd (msqid, (void *) &line_buf, strlen (line_buf.mtext) + 1, 0) != 0) { perror ("MSGSND-3"); break; } }

/* Удалим очередь */ if (msgctl (msqid, IPC_RMID, NULL) == -1) { perror ("MSGCTL-IPC_RMID"); return (6); }

return (0); }

Листинг 8.29. Передающая часть программы работы с очередями сообщений.

#include <stdio.h> #include <limits.h> #include <sys/msg.h>

/* Программа получает сообщения из очереди */ /* и копирует их тела на стандартный вывод */

#define MSGQ_MODE 0644

int main (int argc, char *argv []) { key_t key; int msqid; struct mymsgbuf { long mtype; char mtext [LINE_MAX]; } msgbuf;

if (argc != 3) { fprintf (stderr, "Использование: %s имя_файла цепочка_символов\n", argv [0]); return (1); }

/* Выработаем ключ для очереди сообщений */ if ((key = ftok (argv [1], *argv [2])) == (key_t) (-1)) { perror ("CHILD FTOK"); return (2); }

/* Получим идентификатор очереди сообщений */ if ((msqid = msgget (key, IPC_CREAT | MSGQ_MODE)) < 0) { perror ("CHILD MSGGET"); return (3); }

/* Цикл приема сообщений и выдачи строк */ while (msgrcv (msqid, (void *) &msgbuf, sizeof (msgbuf.mtext), 0, 0) > 0) { if (fputs (msgbuf.mtext, stdout) == EOF) { break; } }

return 0; }

Листинг 8.30. Приемная часть программы работы с очередями сообщений.

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


Программа копирует строки со стандартного


#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <sys/wait.h>
/* Программа копирует строки со стандартного ввода на стандартный вывод, */ /* "прокачивая" их через канал. */ /* Используются функции ввода/вывода нижнего уровня */
#define MY_PROMPT "Вводите строки\n" #define MY_MSG "Вы ввели: "
int main (void) { int fd [2]; char buf [1]; int new_line = 1; /* Признак того, что надо выдать сообщение MY_MSG */ /* перед отображением очередной строки */
/* Создадим безымянный канал */ if (pipe (fd) < 0) { perror ("PIPE"); exit (1); }
switch (fork ()) { case -1: perror ("FORK"); exit (2); case 0: /* Чтение из канала и выдачу на стандартный вывод */ /* реализуем в порожденном процессе. */ /* Необходимо закрыть дескриптор, предназначенный */ /* для записи в канал, иначе чтение не завершится */ /* по концу файла */ close (fd [1]); while (read (fd [0], buf, 1) == 1) { if (write (1, buf, 1) != 1) { perror ("WRITE TO STDOUT"); break; } } exit (0); }
/* Чтение со стандартного ввода и запись в канал */ /* возложим на родительский процесс. */ /* Из соображений симметрии закроем дескриптор, */ /* предназначенный для чтения из канала */ close (fd [0]); if (write (fd [1], MY_PROMPT, sizeof (MY_PROMPT) - 1) != sizeof (MY_PROMPT) - 1) { perror ("WRITE TO PIPE-1"); }
while (read (0, buf, 1) == 1) { /* Перед отображением очередной строки */ /* нужно выдать сообщение MY_MSG */ if (new_line) { if (write (fd [1], MY_MSG, sizeof (MY_MSG) - 1) != sizeof (MY_MSG) - 1) { perror ("WRITE TO PIPE-2"); break; } } if (write (fd [1], buf, 1) != 1) { perror ("WRITE TO PIPE-3"); break; } new_line = (buf [0] == '\n'); } close (fd [1]);
(void) wait (NULL); return (0); }
Листинг 8.1. Пример взаимодействия между процессами через канал с помощью функций ввода/вывода нижнего уровня.
Закрыть окно




#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <limits.h> #include <sys/wait.h> #include <assert.h>
/* Программа копирует строки со стандартного ввода на стандартный вывод, */ /* "прокачивая" их через канал. */ /* Используются функции буферизованного ввода/вывода */
int main (void) { int fd [2]; FILE *fp [2]; char line [LINE_MAX];
/* Создадим безымянный канал */ if (pipe (fd) < 0) { perror ("PIPE"); exit (1); }
/* Сформируем потоки по файловым дескрипторам канала */ assert ((fp [0] = fdopen (fd [0], "r")) != NULL); assert ((fp [1] = fdopen (fd [1], "w")) != NULL);
/* Отменим буферизацию вывода */ setbuf (stdout, NULL); setbuf (fp [1], NULL);
switch (fork ()) { case -1: perror ("FORK"); exit (2); case 0: /* Чтение из канала и выдачу на стандартный вывод */ /* реализуем в порожденном процессе. */ /* Необходимо закрыть поток, предназначенный для */ /* записи в канал, иначе чтение не завершится */ /* по концу файла */ fclose (fp [1]); while (fgets (line, sizeof (line), fp [0]) != NULL) { if (fputs (line, stdout) == EOF) { break; } }
exit (0); }
/* Чтение со стандартного ввода и запись в канал */ /* возложим на родительский процесс. */ /* Из соображений симметрии закроем поток, */ /* предназначенный для чтения из канала */ fclose (fp [0]); fputs ("Вводите строки\n", fp [1]); while (fgets (line, sizeof (line), stdin) != NULL) { if ((fputs ("Вы ввели: ", fp [1]) == EOF) || (fputs (line, fp [1]) == EOF)) { break; } } fclose (fp [1]);
(void) wait (NULL); return (0); }
Листинг 8.2. Пример взаимодействия между процессами через канал с помощью функций буферизованного ввода/вывода.
Закрыть окно




#include <stdio.h> int pclose (FILE *stream);
Листинг 8.3. Описание функции pclose().
Закрыть окно




#include <stdio.h> /* Программа печатает несколько первых строк треугольника Паскаля */ #define T_SIZE 16 int main (void) { FILE *outptr; long tp [T_SIZE]; /* Массив для хранения текущей строки треугольника */ int i, j;
/* Инициализируем массив, чтобы далее все элементы */ /* можно было считать и выводить единообразно */ tp [0] = 1; for (i = 1; i < T_SIZE; i++) { tp [i] = 0; }
/* Создадим канал с командой */ if ((outptr = popen ("lp", "w")) == NULL) { perror ("POPEN"); return (-1); }
(void) fprintf (outptr, "\nТреугольник Паскаля:\n");
for (i = 0; i < T_SIZE; i++) { /* Элементы очередной строки нужно считать от конца к началу */ /* Элемент tp [0] пересчитывать не нужно */ for (j = i; j > 0; j--) { tp [j] += tp [j - 1]; } /* Вывод строки треугольника в канал */ for (j = 0; j <= i; j++) { (void) fprintf (outptr, " %ld", tp [j]); } (void) fprintf (outptr, "\n"); }
return (pclose (outptr)); }
Листинг 8.4. Пример создания и использования канала для вывода данных.
Закрыть окно




#include <stdio.h> #include <limits.h> #include <assert.h>
#define MY_CMD "ls -l *.c"
int main (void) { FILE *inptr; char line [LINE_MAX];
assert ((inptr = popen (MY_CMD, "r")) != NULL);
while (fgets (line, sizeof (line), inptr) != NULL) { fputs (line, stdout); }
return (pclose (inptr)); }
Листинг 8.5. Пример создания и использования канала для ввода данных.
Закрыть окно




#include <signal.h> int kill ( pid_t pid, int sig);
Листинг 8.6. Описание функции kill().
Закрыть окно




1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 32) SIGRTMIN 33) SIGRTMIN+1 34) SIGRTMIN+2 35) SIGRTMIN+3 36) SIGRTMIN+4 37) SIGRTMIN+5 38) SIGRTMIN+6 39) SIGRTMIN+7 40) SIGRTMIN+8 41) SIGRTMIN+9 42) SIGRTMIN+10 43) SIGRTMIN+11 44) SIGRTMIN+12 45) SIGRTMIN+13 46) SIGRTMIN+14 47) SIGRTMIN+15 48) SIGRTMAX-15 49) SIGRTMAX-14 50) SIGRTMAX-13 51) SIGRTMAX-12 52) SIGRTMAX-11 53) SIGRTMAX-10 54) SIGRTMAX-9 55) SIGRTMAX-8 56) SIGRTMAX-7 57) SIGRTMAX-6 58) SIGRTMAX-5 59) SIGRTMAX-4 60) SIGRTMAX-3 61) SIGRTMAX-2 62) SIGRTMAX-1 63) SIGRTMAX
Листинг 8.7. Возможный результат выполнения команды kill -l.
Закрыть окно




#include <signal.h> int raise (int sig);
Листинг 8.8. Описание функции raise().
Закрыть окно




#include <stdlib.h> void abort (void);
Листинг 8.9. Описание функции abort().
Закрыть окно




#include <signal.h> int sigaction ( int sig, const struct sigaction *restrict act, struct sigaction *restrict oact);
Листинг 8.10. Описание функции sigaction().
Закрыть окно




save_traps=$(trap) . . . eval "$save_traps"
Листинг 8.11. Пример сохранения и восстановления способа обработки сигналов посредством специальной встроенной команды trap.
Закрыть окно




trap '$HOME/logout' EXIT
Листинг 8.12. Пример использования специальной встроенной команды trap.
Закрыть окно




trap "" PIPE echo "$INITLOG_ARGS -n $0 -s \"$1\" -e 1" >&21 trap - PIPE
Листинг 8.13. Пример использования специальной встроенной команды trap для защиты от ошибок, специфичных для каналов.
Закрыть окно




#include <signal.h> int sigemptyset (sigset_t *set); int sigfillset (sigset_t *set); int sigaddset (sigset_t *set, int signo); int sigdelset (sigset_t *set, int signo); int sigismember (const sigset_t *set, int signo);
Листинг 8.14. Описание функций для работы с наборами сигналов.
Закрыть окно




#include <signal.h> int sigprocmask ( int how, const sigset_t *restrict set, sigset_t *restrict oset);
Листинг 8.15. Описание функции sigprocmask().
Закрыть окно




#include <signal.h> int sigpending (sigset_t *set);
Листинг 8.16. Описание функции sigpending().
Закрыть окно




#include <signal.h> int sigwait (const sigset_t * restrict set, int *restrict sig);
Листинг 8.17. Описание функции sigwait().
Закрыть окно




#include <unistd.h> int pause (void);
Листинг 8.18. Описание функции pause().
Закрыть окно




#include <signal.h> int sigsuspend (const sigset_t *sigmask);
Листинг 8.19. Описание функции sigsuspend().
Закрыть окно




#include <unistd.h> #include <signal.h> #include <stdio.h>
void abort (void) { struct sigaction sact; sigset_t sset;
/* Вытолкнем буфера */ (void) fflush (NULL);
/* Снимем блокировку сигнала SIGABRT */ if ((sigemptyset (&sset) == 0) && (sigaddset (&sset, SIGABRT) == 0)) { (void) sigprocmask (SIG_UNBLOCK, &sset, (sigset_t *) NULL); }
/* Пошлем себе сигнал SIGABRT. */ /* Возможно, его перехватит функция обработки, */ /* и тогда вызывающий процесс может не завершиться */ raise (SIGABRT);
/* Установим подразумеваемую реакцию на сигнал SIGABRT */ sact.sa_handler = SIG_DFL; sigfillset (&sact.sa_mask); sact.sa_flags = 0; (void) sigaction (SIGABRT, &sact, NULL);
/* Снова пошлем себе сигнал SIGABRT */ raise (SIGABRT);
/* Если сигнал снова не помог, попробуем еще одно средство завершения */ _exit (127); }
int main (void) { printf ("Перед вызовом abort()\n"); abort (); printf ("После вызова abort()\n"); return 0; }
Листинг 8.20. Упрощенная реализация функции abort() как пример использования функций работы с сигналами.
Закрыть окно




#include <unistd.h> #include <stdio.h> #include <signal.h> #include <time.h>
/* Функция обработки сигнала SIGALRM. */ /* Она ничего не делает, но игнорировать сигнал нельзя */ static void signal_handler (int sig) { /* В демонстрационных целях распечатаем номер обрабатываемого сигнала */ printf ("Принят сигнал %d\n", sig); }
/* Функция для "засыпания" на заданное число секунд */ /* Результат равен разности между заказанной и фактической */ /* продолжительностью "сна" */ unsigned int sleep (unsigned int seconds) { time_t before, after; unsigned int slept; sigset_t set, oset; struct sigaction act, oact;
if (seconds == 0) { return 0; }
/* Установим будильник на заданное время, */ /* но перед этим блокируем сигнал SIGALRM */ /* и зададим свою функцию обработки для него */ if ((sigemptyset (&set) < 0) || (sigaddset (&set, SIGALRM) < 0) || sigprocmask (SIG_BLOCK, &set, &oset)) { return seconds; }
act.sa_handler = signal_handler; act.sa_flags = 0; act.sa_mask = oset; if (sigaction (SIGALRM, &act, &oact) < 0) { return seconds; }
before = time ((time_t *) NULL); (void) alarm (seconds);
/* Как атомарное действие восстановим старую маску сигналов */ /* (в надежде, что она не блокирует SIGALRM) */ /* и станем ждать доставки обрабатываемого сигнала */ (void) sigsuspend (&oset); /* сигнал доставлен и обработан */
after = time ((time_t *) NULL);
/* Восстановим прежний способ обработки сигнала SIGALRM */ (void) sigaction (SIGALRM, &oact, (struct sigaction *) NULL);
/* Восстановим первоначальную маску сигналов */ (void) sigprocmask (SIG_SETMASK, &oset, (sigset_t *) NULL);
return ((slept = after - before) > seconds ? 0 : (seconds - slept)); }
int main (void) { struct sigaction act;
/* В демонстрационных целях установим обработку прерывания с клавиатуры */ act.sa_handler = signal_handler; (void) sigemptyset (&act.sa_mask); act.sa_flags = 0; (void) sigaction (SIGINT, &act, (struct sigaction *) NULL);
printf ("Заснем на 10 секунд\n"); printf ("Проснулись, не доспав %d секунд\n", sleep (10)); return (0); }
Листинг 8.21. Упрощенная реализация функции sleep() как пример использования механизма сигналов.
Закрыть окно




#include <sys/ipc.h> key_t ftok (const char *path, int id);
Листинг 8.22. Описание функции ftok().
Закрыть окно




#include <sys/msg.h> int msgget (key_t key, int msgflg); int msgsnd (int msqid, const void *msgp, size_t msgsz, int msgflg); ssize_t msgrcv (int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); int msgctl (int msqid, int cmd, struct msqid_ds *buf);
Листинг 8.23. Описание функций для работы с очередями сообщений.
Закрыть окно




#include <stdio.h> #include <sys/ipc.h> #include <sys/msg.h>
/* Программа создает очередь сообщений. */ /* В командной строке задаются имя файла для ftok() */ /* и режим доступа к очереди сообщений */
#define FTOK_CHAR 'G'
int main (int argc, char *argv []) { key_t key; int msqid; int mode = 0;
if (argc != 3) { fprintf (stderr, "Использование: %s маршрутное_имя режим_доступа\n", argv [0]); return (1); }
if ((key = ftok (argv [1], FTOK_CHAR)) == (key_t) (-1)) { perror ("FTOK"); return (2); } (void) sscanf (argv [2], "%o", (unsigned int *) &mode);
if ((msqid = msgget (key, IPC_CREAT | mode)) < 0) { perror ("MSGGET"); return (3); }
return 0; }
Листинг 8.24. Пример программы, создающей очередь сообщений.
Закрыть окно




------ Message Queues -------- key msqid owner perms used- bytes messages 0x47034bac 163840 galat 644 0 0
Листинг 8.25. Возможный результат опроса статуса очередей сообщений.
Закрыть окно




struct msgbuf { long mtype; /* Тип сообщения */ char mtext [1]; /* Текст сообщения */ };
Листинг 8.26. Описание структурного типа для представления сообщений.
Закрыть окно




#define MAXSZTMSG 8192
struct mymsgbuf { long mtype; /* Тип сообщения */ char mtext [MAXSZTMSG]; /* Текст сообщения */ }; struct mymsgbuf msgbuf;
Листинг 8.27. Описание структуры для хранения сообщений.
Закрыть окно




#include <stdio.h> #include <sys/msg.h>
int main (int argc, char *argv []) { int msqid; struct msqid_ds msqid_ds;
if (argc != 3) { fprintf (stderr, "Использование: %s идентификатор_очереди максимальный_размер\n", argv [0]); return (1); }
(void) sscanf (argv [1], "%d", &msqid);
/* Получим исходное значение структуры данных */ if (msgctl (msqid, IPC_STAT, &msqid_ds) == -1) { perror ("IPC_STAT-1"); return (2); } printf ("Максимальный размер очереди до изменения: %ld\n", msqid_ds.msg_qbytes);
(void) sscanf (argv [2], "%d", (int *) &msqid_ds.msg_qbytes);
/* Попробуем внести изменения */ if (msgctl (msqid, IPC_SET, &msqid_ds) == -1) { perror ("IPC_SET"); }
/* Получим новое значение структуры данных */ if (msgctl (msqid, IPC_STAT, &msqid_ds) == -1) { perror ("IPC_STAT-2"); return (3); } printf ("Максимальный размер очереди после изменения: %ld\n", msqid_ds.msg_qbytes);
return 0; }
Листинг 8.28. Пример программы управления очередями сообщений.
Закрыть окно




#include <unistd.h> #include <stdio.h> #include <limits.h> #include <string.h> #include <sys/wait.h> #include <sys/msg.h>
/* Программа копирует строки со стандартного ввода на стандартный вывод, */ /* "прокачивая" их через очередь сообщений */
#define FTOK_FILE "/home/galat" #define FTOK_CHAR "G"
#define MSGQ_MODE 0644
#define MY_PROMPT "Вводите строки\n" #define MY_MSG "Вы ввели: "
int main (void) { key_t key; int msqid; struct mymsgbuf { long mtype; char mtext [LINE_MAX]; } line_buf, msgbuf;
switch (fork ()) { case -1: perror ("FORK"); return (1); case 0: /* Чтение из очереди и выдачу на стандартный вывод */ /* реализуем в порожденном процессе. */ (void) execl ("./msq_child", "msq_child", FTOK_FILE, FTOK_CHAR, (char *) 0); perror ("EXEC"); return (2); /* execl() завершился неудачей */ }
/* Чтение со стандартного ввода и запись в очередь */ /* возложим на родительский процесс */
/* Выработаем ключ для очереди сообщений */ if ((key = ftok (FTOK_FILE, FTOK_CHAR [0])) == (key_t) (-1)) { perror ("FTOK"); return (3); }
/* Получим идентификатор очереди сообщений */ if ((msqid = msgget (key, IPC_CREAT | MSGQ_MODE)) < 0) { perror ("MSGGET"); return (4); }
/* Приступим к отправке сообщений в очередь */ msgbuf.mtype = line_buf.mtype = 1; strncpy (msgbuf.mtext, MY_PROMPT, sizeof (msgbuf.mtext)); if (msgsnd (msqid, (void *) &msgbuf, strlen (msgbuf.mtext) + 1, 0) != 0) { perror ("MSGSND-1"); return (5); } strncpy (msgbuf.mtext, MY_MSG, sizeof (msgbuf.mtext));
while (fgets (line_buf.mtext, sizeof (line_buf.mtext), stdin) != NULL) { if (msgsnd (msqid, (void *) &msgbuf, strlen (msgbuf.mtext) + 1, 0) != 0) { perror ("MSGSND-2"); break; } if (msgsnd (msqid, (void *) &line_buf, strlen (line_buf.mtext) + 1, 0) != 0) { perror ("MSGSND-3"); break; } }
/* Удалим очередь */ if (msgctl (msqid, IPC_RMID, NULL) == -1) { perror ("MSGCTL-IPC_RMID"); return (6); }
return (0); }
Листинг 8.29. Передающая часть программы работы с очередями сообщений.
Закрыть окно




#include <stdio.h> #include <limits.h> #include <sys/msg.h>
/* Программа получает сообщения из очереди */ /* и копирует их тела на стандартный вывод */
#define MSGQ_MODE 0644
int main (int argc, char *argv []) { key_t key; int msqid; struct mymsgbuf { long mtype; char mtext [LINE_MAX]; } msgbuf;
if (argc != 3) { fprintf (stderr, "Использование: %s имя_файла цепочка_символов\n", argv [0]); return (1); }
/* Выработаем ключ для очереди сообщений */ if ((key = ftok (argv [1], *argv [2])) == (key_t) (-1)) { perror ("CHILD FTOK"); return (2); }
/* Получим идентификатор очереди сообщений */ if ((msqid = msgget (key, IPC_CREAT | MSGQ_MODE)) < 0) { perror ("CHILD MSGGET"); return (3); }
/* Цикл приема сообщений и выдачи строк */ while (msgrcv (msqid, (void *) &msgbuf, sizeof (msgbuf.mtext), 0, 0) > 0) { if (fputs (msgbuf.mtext, stdout) == EOF) { break; } }
return 0; }
Листинг 8.30. Приемная часть программы работы с очередями сообщений.
Закрыть окно




#include <sys/sem.h> int semget ( key_t key, int nsems, int semflg); int semop (int semid, struct sembuf *sops, size_t nsops); int semctl (int semid, int semnum, int cmd, ...);
Листинг 8.31. Описание функций для работы с семафорами.
Закрыть окно




sembuf [0].sem_num = 1; sembuf [0].sem_flg = 0; sembuf [0].sem_op = -2;
sembuf [1].sem_num = 0; sembuf [1].sem_flg = IPC_NOWAIT; sembuf [1].sem_op = 0;
Листинг 8.32. Пример задания массива операций над семафорами.
Закрыть окно




union semun { int val; struct semid_ds *buf; unsigned short *array; } arg;
Листинг 8.33. Описание четвертого (дополнительного) аргумента функции semctl().
Закрыть окно




val = semctl (semid, semnum, GETVAL);
arg.val = ...; if (semctl (semid, semnum, SETVAL, arg) == -1) ...;
arg.array = ( unsigned short *) malloc (nsems * sizeof (unsigned short)); err = semctl (semid, 0, GETALL, arg);
for (i = 0; i < nsems; i++) arg.array [i] = ...; err = semctl (semid, 0, SETALL, arg);
lpid = semctl (semid, semnum, GETPID);
ncnt = semctl (semid, semnum, GETNCNT);
zcnt = semctl (semid, semnum, GETZCNT);
Листинг 8.34. Примеры управляющих действий над семафорами.
Закрыть окно




arg.buf = (struct semid_ds *) malloc (sizeof (struct semid_ds); err = semctl (semid, 0, IPC_STAT, arg); arg.buf->sem_perm.mode = 0644; err = semctl (semid, 0, IPC_SET, arg);
Листинг 8.35. Дополнительные примеры управляющих действий над семафорами.
Закрыть окно




#include <unistd.h> #include <stdio.h> #include <sys/sem.h> #include <sys/wait.h>
/* Программа-монитор обеда философов */
#define QPH 5
#define ARG_SIZE 20
int main (void) { int key; /* Ключ набора семафоров */ int semid; /* Идентификатор набора семафоров */ int no; /* Номер философа и/или вилки */ char ssemid [ARG_SIZE], sno [ARG_SIZE], sqph [ARG_SIZE];
/* Создание и инициализация набора семафоров */ /* (по семафору на вилку) */ key = ftok ("phdin.c", 'C'); if ((semid = semget (key, QPH, 0600 | IPC_CREAT)) < 0) { perror ("SEMGET"); return (1); } for (no = 0; no < QPH; no++) { if (semctl (semid, no, SETVAL, 1) < 0) { perror ("SETVAL"); return (2); } }
sprintf (ssemid, "%d", semid); sprintf (sqph, "%d", QPH);
/* Все - к столу */ for (no = 1; no <= QPH; no++) { switch (fork ()) { case -1: perror ("FORK"); return (3); case 0: sprintf (sno, "%d", no); execl ("./phil", "phil", ssemid, sqph, sno, (char *) 0); perror ("EXEC"); return (4); } }
/* Ожидание завершения обеда */ for (no = 1; no <= QPH; no++) { (void) wait (NULL); }
/* Удаление набора семафоров */ if (semctl (semid, 0, IPC_RMID) < 0) { perror ("SEMCTL"); return (5); }
return 0; }
Листинг 8.36. Процесс-монитор для обеда философов.
Закрыть окно




#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <sys/sem.h>
/* Процесс обеда одного философа */
#define ernd (rand () % 3 + 1) #define trnd (rand () % 5 + 1) #define FO 15
int main (int argc, char *argv []) { int semid; /* Идентификатор набора семафоров */ int qph; /* Число философов */ int no; /* Номер философа */ int t; /* Время очередного отрезка еды или беседы */ int fo; /* Время до конца обеда */ struct sembuf sembuf [2];
if (argc != 4) { fprintf (stderr, "Использование: %s идентификатор_набора_семафоров число_философов номер_философа \n", argv [0]); return (1); }
fo = FO; sscanf (argv [1], "%d", &semid); sscanf (argv [2], "%d", &qph); sscanf (argv [3], "%d", &no);
/* Выбор вилок */ sembuf [0].sem_num = no - 1; /* Левая */ sembuf [0].sem_flg = 0; sembuf [1].sem_num = no % qph; /* Правая */ sembuf [1].sem_flg = 0;
while (fo > 0) { /* Обед */
/* Философ говорит */ printf ("Философ %d беседует\n", no); t = trnd; sleep (t); fo -= t; /* Пытается взять вилки */ sembuf [0].sem_op = -1; sembuf [1].sem_op = -1; if (semop (semid, sembuf, 2) < 0) { perror ("SEMOP"); return (1); }
/* Ест */ printf ("Философ %d ест\n", no); t = ernd; sleep (t); fo -= t; /* Отдает вилки */ sembuf [0].sem_op = 1; sembuf [1].sem_op = 1; if (semop (semid, sembuf, 2) < 0) { perror ("SEMOP"); return (2); } }
printf ("Философ %d закончил обед\n", no); return 0; }
Листинг 8.37. Программа, описывающая обед одного философа.
Закрыть окно




/* Обедающие философы. Запуск: mudrecProc [-a | -p | -I -V] [-t число_секунд] имя_философа ... Опции: -t число_секунд - сколько секунд моделируется Стратегии захвата вилок: -a - сначала захватывается вилка с меньшим номером; -I - некорректная (но эффективная) интеллигентная стратегия: во время ожидания уже захваченная вилка кладется; -p - сначала захватывается нечетная вилка; -V - использован групповой захват семафоров. Пример запуска: mudrecProc -p -t 600 A B C D E F G H I J K L M N O P Q R S T U V W X Y Z */ static char rcsid[] __attribute__((unused)) = \ "$Id: mudrecProc.c,v 1.7 2003/11/11 13:14:07 sambor Exp $";
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> #include <fcntl.h> #include <time.h> #include <limits.h> #include <errno.h>
#include <sys/sem.h> #include <sys/msg.h>
union semun { int val; struct semid_ds *buf; unsigned short *array; } arg;
#define max(a,b) ((a)>(b)?(a):(b)) #define min(a,b) ((a)>(b)?(b):(a))
struct mudrec { long num; char *name; int left_fork, right_fork; int eat_time, wait_time, think_time, max_wait_time; int count; };
int Stop; /* Семафор для синхронизации выхода */
/* Различные дескрипторы */ int protokol [2] = {-1, -1}; #define pFdIn (protokol [1]) #define pFdOut (protokol [0])
int semFork; /* Вилки */
int from_fil; /* Очередь для возврата результатов */
/* Разные алгоритмы захвата вилок */ static void get_forks_simple (struct mudrec *this); static void get_forks_parity (struct mudrec *this); static void get_forks_maybe_infinit_time (struct mudrec *this); static void get_forks_use_groups (struct mudrec *this);
/* Используемый метод захвата вилок */ void (*get_forks) (struct mudrec *this) = get_forks_simple;
/* Возвращение вилок */ static void put_forks (struct mudrec *this);
/* * Философы */ void filosof (struct mudrec this) { char buffer [LINE_MAX]; int bytes;
if (fork ()) return;
srandom (getpid ()); /* Очень важно для процессов, иначе получим одно и то же! */ random (); random (); random (); random (); random (); random (); random (); random (); random (); random ();
/* Пока семафор Stop не поднят */ while (!semctl (Stop, 0, GETVAL)) { /* Пора подкрепиться */ { int wait_time, tm;
sprintf (buffer, "%s: хочет есть\n", this.name); bytes = write (pFdIn, buffer, strlen (buffer));
tm = time (0);
(*get_forks) (&this);
wait_time = time (0) - tm; /* Сколько времени получали вилки */ this.wait_time += wait_time; this.max_wait_time = max (wait_time, this.max_wait_time);
sprintf (buffer, "%s: ждал вилок %d сек\n", this.name, wait_time); bytes = write (pFdIn, buffer, strlen (buffer)); }
/* Может, обед уже закончился? */ if (semctl (Stop, 0, GETVAL)) { put_forks (&this); break; }
/* Едим */ { int eat_time = random () % 20 + 1;
sleep (eat_time);
this.eat_time += eat_time; this.count++; sprintf (buffer,"%s: ел %d сек\n", this.name, eat_time); bytes = write (pFdIn, buffer, strlen (buffer)); }
/* Отдаем вилки */ put_forks (&this);
if (semctl (Stop, 0, GETVAL)) break;
/* Размышляем */ { int think_time = random () % 10 + 1;
sleep (think_time);
this.think_time += think_time; } }
sprintf (buffer,"%s: уходит\n", this.name); bytes = write (pFdIn, buffer, strlen (buffer));
msgsnd (from_fil, &this, sizeof (this), 0); /* Отослали статистику своего обеда */
_exit (0); /* ВАЖНО (_): Нам не нужны преждевременные вызовы cleanup_ipc */ }
/* Кладем вилки одну за другой */ static void put_forks (struct mudrec *this) { struct sembuf tmp_buf;
tmp_buf.sem_flg = 0; tmp_buf.sem_op = 1; tmp_buf.sem_num = this->left_fork - 1; semop (semFork, &tmp_buf, 1);
tmp_buf.sem_flg = 0; tmp_buf.sem_op = 1; tmp_buf.sem_num = this->right_fork - 1; semop (semFork, &tmp_buf, 1); }
/* Берем вилки по очереди в порядке номеров */ static void get_forks_simple (struct mudrec *this) { struct sembuf tmp_buf;
int first = min (this->left_fork, this->right_fork); int last = max (this->left_fork, this->right_fork);
tmp_buf.sem_flg = SEM_UNDO; tmp_buf.sem_op = -1; tmp_buf.sem_num = first - 1; semop (semFork, &tmp_buf, 1);
tmp_buf.sem_flg = SEM_UNDO; tmp_buf.sem_op = -1; tmp_buf.sem_num = last - 1; semop (semFork, &tmp_buf, 1); }
/* Берем сначала нечетную вилку (если обе нечетные - то с большим номером) */ static void get_forks_parity (struct mudrec *this) { struct sembuf tmp_buf;
int left = this->left_fork, right = this->right_fork; int first = max ((left & 1) * 1000 + left, (right & 1) * 1000 + right) % 1000; int last = min ((left & 1) * 1000 + left, (right & 1) * 1000 + right) % 1000;
tmp_buf.sem_flg = SEM_UNDO; tmp_buf.sem_op = -1; tmp_buf.sem_num = first - 1; semop (semFork, &tmp_buf, 1);
tmp_buf.sem_flg = SEM_UNDO; tmp_buf.sem_op = -1; tmp_buf.sem_num = last - 1; semop (semFork, &tmp_buf, 1); }
/* Берем вилки по очереди, в произвольном порядке. * Но если вторая вилка не берется сразу, то кладем первую. * То есть философ не расходует вилочное время впустую. */ static void get_forks_maybe_infinit_time (struct mudrec *this) { struct sembuf tmp_buf;
int left = this->left_fork, right = this->right_fork;
for (;;) { tmp_buf.sem_flg = SEM_UNDO; /* Первую вилку берем с ожиданием */ tmp_buf.sem_op = -1; tmp_buf.sem_num = left - 1; semop (semFork, &tmp_buf, 1);
tmp_buf.sem_flg = SEM_UNDO | IPC_NOWAIT; /* Вторую - без ожидания */ tmp_buf.sem_op = -1; tmp_buf.sem_num = right - 1;
if (0 == semop (semFork, &tmp_buf, 1)) return; /* Успех */
tmp_buf.sem_flg = 0; /* Неуспех: возвращаем первую вилку */ tmp_buf.sem_op = 1; tmp_buf.sem_num = left - 1; semop(semFork,&tmp_buf,1);
tmp_buf.sem_flg = SEM_UNDO; /* Отдав первую, ждем вторую */ tmp_buf.sem_op = -1; tmp_buf.sem_num = right - 1; semop (semFork, &tmp_buf, 1);
tmp_buf.sem_flg = SEM_UNDO | IPC_NOWAIT; /* Берем первую вилку без ожидания */ tmp_buf.sem_op = -1; tmp_buf.sem_num = left - 1;
if (0 == semop (semFork, &tmp_buf, 1)) return; /* Успех */
tmp_buf.sem_flg = 0; /* Неуспех: отдаем вторую вилку, */ tmp_buf.sem_op = 1; /* чтобы ждать первую */ tmp_buf.sem_num = right - 1; semop (semFork, &tmp_buf, 1); } }
/* Хватаем обе вилки сразу, используя групповые операции */ static void get_forks_use_groups (struct mudrec *this) { struct sembuf tmp_buf [2];
tmp_buf[0].sem_flg = SEM_UNDO; tmp_buf[0].sem_op = -1; tmp_buf[0].sem_num = this->left_fork - 1; tmp_buf[1].sem_flg = SEM_UNDO; tmp_buf[1].sem_op = -1; tmp_buf[1].sem_num = this->right_fork - 1; semop (semFork, tmp_buf, 2); }
/* * Мелкие служебные функции. */ static void stop (int dummy) { struct sembuf tmp_buf;
tmp_buf.sem_flg = 0; tmp_buf.sem_op = 1; tmp_buf.sem_num = 0; semop (Stop, &tmp_buf, 1); }
void cleanup_ipc (void) { /* * Уничтожение семафоров. */ semctl (semFork, 1, IPC_RMID); semctl (Stop, 1, IPC_RMID);
/* То же с очередью */ msgctl (from_fil, IPC_RMID, NULL); }
static void usage (char name []) { fprintf (stderr,"Использование: %s [-a | -p | -I| -V] [-t число_секунд] имя_философа ...\n", name); exit (1); }
/* * Точка входа демонстрационной программы. */ int main (int argc, char *argv[]) { char buffer [LINE_MAX], *p; int i, n, c; int open_room_time = 300; union semun tmp_arg; int nMudr; struct sigaction sact;
while ((c = getopt (argc, argv, "apIVt:")) != -1) { switch (c) { case 'a': get_forks = get_forks_simple; break; case 'p': get_forks = get_forks_parity; break; case 'I': get_forks = get_forks_maybe_infinit_time; break; case 'V': get_forks = get_forks_use_groups; break; case 't': open_room_time = strtol (optarg, &p, 0); if (optarg [0] == 0 || *p != 0) usage (argv [0]); break; default: usage (argv [0]); } }
nMudr = argc - optind; if (nMudr < 2) usage (argv [0]); /* Меньше двух философов неинтересно ... */
/* * Создание канала для протокола обработки событий */ pipe (protokol);
/* * Создадим семафоры для охраны вилок */ semFork = semget (ftok (argv [0], 2), nMudr, IPC_CREAT | 0777); tmp_arg.val = 1; for (i=1; i <= nMudr; i++) semctl (semFork, i - 1, SETVAL, tmp_arg); /* Начальное значение 1 */
/* Прежде чем впускать философов, обеспечим окончание обеда */ Stop = semget (ftok (argv [0], 3), 1, IPC_CREAT | 0777); tmp_arg.val = 0; semctl (Stop, 0, SETVAL, tmp_arg); /* Начальное значение 0 */
/* Очередь для возврата результатов */ from_fil = msgget (ftok (argv [0], 4), IPC_CREAT | 0777);
atexit (cleanup_ipc); /* Запланировали уничтожение семафоров */ /* и других средств межпроцессного взаимодействия */
/* * Философы входят в столовую */ for (i = 0; i < nMudr; i++, optind++) { struct mudrec next;
memset (&next, 0, sizeof (next));
next.num = i + 1; /* Номер */ next.name = argv [optind]; /* Имя */
/* Указали, какими вилками пользоваться */ next.left_fork = i + 1; next.right_fork = i + 2; if (i == nMudr - 1) next.right_fork = 1; /* Последний пользуется вилкой первого */
filosof (next); }
/* Зададим реакцию на сигналы и установим будильник на конец обеда */ sact.sa_handler = stop; (void) sigemptyset (&sact.sa_mask); sact.sa_flags = 0; (void) sigaction (SIGINT, &sact, (struct sigaction *) NULL); (void) sigaction (SIGALRM, &sact, (struct sigaction *) NULL);
alarm (open_room_time);
/* * Выдача сообщений на стандартный вывод и выход после окончания обеда. */ close (pFdIn); /* Сами должны закрыть, иначе из цикла не выйдем! */ for (;;) { n = read (pFdOut, buffer, LINE_MAX); if ((n == 0) || ((n == -1) && (errno != EINTR))) break; for (i = 0; i < n; i++) putchar (buffer [i]); } close (pFdOut);
/* Распечатали сводную информацию */ { int full_eating_time = 0; int full_waiting_time = 0; int full_thinking_time = 0; for (i = 1; i <= nMudr; i++) { struct mudrec this; /* Получили статистику обеда */ msgrcv (from_fil, &this, sizeof (this), i, 0); /* За счет i получаем */ /* строго по порядку */ full_eating_time += this.eat_time; full_waiting_time += this.wait_time; full_thinking_time += this.think_time;
if (this.count > 0) { float count = this.count; float think_time = this.think_time / count; float eat_time = this.eat_time / count; float wait_time = this.wait_time / count;
printf ("%s: ел %d раз в среднем: думал=%.1f ел=%.1f ждал=%.1f (максимум %d)\n", this.name, this.count, think_time, eat_time, wait_time, this.max_wait_time); } else printf("%s: не поел\n", this.name); } { float total_time = (full_eating_time + full_waiting_time + full_thinking_time) / (float)nMudr;
printf (" Среднее число одновременно едящих = %.3f\n Среднее число одновременно ждущих = %.3f\n", full_eating_time / total_time, full_waiting_time / total_time); } }
/* Сообщим об окончании работы */ printf ("Конец обеда\n");
return 0; }
Листинг 8.38. Второй вариант решения задачи об обедающих философах.
Закрыть окно




-a: A: ел 2 раза в среднем: думал=3.5 ел=11.5 ждал=36.5 (максимум 73) B: ел 3 раза в среднем: думал=5.7 ел=7.7 ждал=20.0 (максимум 41) C: ел 3 раза в среднем: думал=5.7 ел=11.3 ждал=17.0 (максимум 33) D: ел 3 раза в среднем: думал=1.7 ел=16.7 ждал=15.7 (максимум 19) E: ел 1 раз в среднем: думал=10.0 ел=20.0 ждал=73.0 ( максимум 41) Среднее число одновременно едящих = 1.471 Среднее число одновременно ждущих = 2.980 -p: A: ел 3 раза в среднем: думал=3.7 ел=15.3 ждал=16.0 (максимум 34) B: ел 4 раза в среднем: думал=5.0 ел=13.8 ждал=8.2 (максимум 15) C: ел 3 раза в среднем: думал=6.7 ел=3.7 ждал=25.7 (максимум 27) D: ел 4 раза в среднем: думал=5.8 ел=8.5 ждал=13.8 (максимум 28) E: ел 3 раза в среднем: думал=5.3 ел=15.3 ждал=16.7 (максимум 29) Среднее число одновременно едящих = 1.761 Среднее число одновременно ждущих = 2.413
-I: A: ел 5 раз в среднем: думал=4.2 ел=9.4 ждал=6.6 (максимум 15) B: ел 3 раза в среднем: думал=6.3 ел=10.3 ждал=17.0 (максимум 31) C: ел 4 раза в среднем: думал=6.8 ел=7.0 ждал=12.2 (максимум 45) D: ел 3 раза в среднем: думал=4.3 ел=16.0 ждал=13.0 (максимум 16) E: ел 4 раза в среднем: думал=5.8 ел=8.5 ждал=10.8 (максимум 22) Среднее число одновременно едящих = 1.858 Среднее число одновременно ждущих = 2.125
-V: A: ел 5 раз в среднем: думал=5.6 ел=5.6 ждал=8.8 (максимум 17) B: ел 3 раза в среднем: думал=6.3 ел=10.3 ждал=16.7 (максимум 20) C: ел 4 раза в среднем: думал=4.8 ел=11.0 ждал=9.8 (максимум 18) D: ел 4 раза в среднем: думал=5.2 ел=12.0 ждал=8.8 (максимум 15) E: ел 4 раза в среднем: думал=5.2 ел=10.5 ждал=10.2 (максимум 20) Среднее число одновременно едящих = 1.892 Среднее число одновременно ждущих = 2.049
Листинг 8.39. Результаты моделирования поведения философов.
Закрыть окно




#include <sys/shm.h> int shmget ( key_t key, size_t size, int shmflg); void *shmat (int shmid, const void *shmaddr, int shmflg); int shmdt (const void *shmaddr); int shmctl (int shmid, int cmd, struct shmid_ds *buf);
Листинг 8.40. Описание функций для работы с разделяемыми сегментами памяти.
Закрыть окно




#include <unistd.h> #include <stdio.h> #include <sys/shm.h> #include <sys/sem.h> #include <sys/wait.h>
int main (void) { struct region { pid_t fpid; } *shm_ptr;
struct sembuf P = {0, -1, 0}; struct sembuf V = {0, 1, 0};
int shmid; int semid;
shmid = shmget (IPC_PRIVATE, sizeof (struct region), 0777); semid = semget (IPC_PRIVATE, 1, 0777); (void) semctl (semid, 0, SETVAL, 1);
switch (fork ()) { case -1: perror ("FORK"); return (1); case 0: if ((int) (shm_ptr = (struct region *) shmat (shmid, NULL, 0)) == (-1)) { perror ("CHILD-SHMAT"); return (2); }
if (semop (semid, &p, 1) != 0) { perror ("CHILD-SEMOP-P"); return (3); } printf ("Процесс-потомок вошел в критический интервал\n");
shm_ptr->fpid = getpid (); /* Монопольный доступ */
printf ("Процесс- потомок перед выходом из критического интервала\n"); if (semop (semid, &V, 1) != 0) { perror ("CHILD-SEMOP-V"); return (4); }
(void) shmdt (shm_ptr); return 0; }
if ((int) (shm_ptr = (struct region *) shmat (shmid, NULL, 0)) == (-1)) { perror ("PARENT-SHMAT"); return (2); }
if (semop (semid, &p, 1) != 0) { perror ("PARENT-SEMOP-P"); return (3); } printf ("Родительский процесс вошел в критический интервал\n");
shm_ptr->fpid = getpid (); /* Монопольный доступ */
printf ("Родительский процесс перед выходом из критического интервала\n"); if (semop (semid, &V, 1) != 0) { perror ("PARENT-SEMOP-V"); return (4); }
(void) wait (NULL);
printf ("Идентификатор родительского процесса: %d\n", getpid ()); printf ("Идентификатор процесса в разделяемой структуре: %d\n", shm_ptr->fpid);
(void) shmdt (shm_ptr);
(void) semctl (semid, 1, IPC_RMID); (void) shmctl (shmid, IPC_RMID, NULL);
return 0; }
Листинг 8.41. Пример работы с разделяемыми сегментами памяти.
Закрыть окно




Родительский процесс вошел в критический интервал Родительский процесс перед выходом из критического интервала Процесс-потомок вошел в критический интервал Процесс-потомок перед выходом из критического интервала Идентификатор родительского процесса: 2161 Идентификатор процесса в разделяемой структуре: 2162
Листинг 8.42. Возможный результат синхронизации доступа к разделяемым данным.
Закрыть окно




/* * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* Реализация "виртуальной" памяти из одного сегмента. */ /* Используются разделяемые сегменты памяти */ /* и обработка сигнала SIGSEGV */ /* * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include <stdlib.h> #include <stdio.h> #include <sys/stat.h> #include <sys/shm.h> #include <signal.h>
/* Константа, зависящая от реализации */ #define SHM_BASE_ADDR 0x40014000
static int shm_id = -1; static void *shm_addr;
/* Реакция на сигнал SIGSEGV. */ /* Создаем и присоединяем на чтение разделяемый сегмент, */ /* накрывающий переданный адрес. */ /* Если это не помогло, переприсоединяем сегмент на запись */ static void sigsegv_sigaction (int sig, siginfo_t *sig_info, void *addr) { struct shmid_ds shmid_ds;
if (shm_id == -1) { /* Сегмента еще нет. Создадим */ if ((shm_id = shmget (IPC_PRIVATE, SHMLBA, S_IRUSR)) == -1) { perror ("SHMGET"); exit (1); } /* Присоединим сегмент на чтение */ if ((int) (shm_addr = shmat (shm_id, sig_info->si_addr, SHM_RDONLY | SHM_RND)) == (-1)) { perror ("SHMAT-RDONLY"); exit (2); } return; } else { /* Сегмент уже есть, но обращение по адресу вызвало сигнал SIGSEGV. */ /* Значит, это была попытка записи, и сегмент нужно */ /* переприсоединить на запись, поменяв соответственно режим доступа */ if (shmctl (shm_id, IPC_STAT, &shmid_ds) == -1) { perror ("SHMCTL-IPC_STAT"); exit (3); } shmid_ds.shm_perm.mode |= S_IWUSR; if (shmctl (shm_id, IPC_SET, &shmid_ds) == -1) { perror ("SHMCTL-IPC_SET"); exit (4); } (void) shmdt (shm_addr); if (shmat (shm_id, shm_addr, 0) != shm_addr) { perror ("SHMAT-RDWD"); exit (5); } } }
int main (void) { char *test_ptr; struct sigaction sact;
/* Установим реакцию на сигнал SIGSEGV */ (void) sigemptyset (&sact.sa_mask); sact.sa_flags = SA_SIGINFO; sact.sa_sigaction = sigsegv_sigaction; (void) sigaction (SIGSEGV, &sact, (struct sigaction *) NULL);
/* Убедимся, что разделяемые сегменты инициализируются нулями */ test_ptr = (char *) (SHM_BASE_ADDR + 3); printf ("Результат попытки чтения до записи: %x\n", *test_ptr);
/* Попробуем записать */ *test_ptr = 'A'; printf ("Результат попытки чтения после записи: %x\n", *test_ptr);
return (shmctl (shm_id, IPC_RMID, NULL)); }
Листинг 8.43. Пример работы с разделяемыми сегментами памяти и сигналами.
Закрыть окно



Разделяемые сегменты памяти


В стандарте POSIX-2001 разделяемый объект памяти определяется как объект, представляющий собой память, которая может быть параллельно отображена в адресное пространство более чем одного процесса.

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

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

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

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

Описание перечисленных функций представлено в листинге 8.40.

#include <sys/shm.h> int shmget (key_t key, size_t size, int shmflg); void *shmat (int shmid, const void *shmaddr, int shmflg); int shmdt (const void *shmaddr); int shmctl (int shmid, int cmd, struct shmid_ds *buf);

Листинг 8.40. Описание функций для работы с разделяемыми сегментами памяти. (html, txt)

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

struct ipc_perm shm_perm; /* Данные о правах доступа к разделяемому сегменту */ size_t shm_segsz; /* Размер сегмента в байтах */ pid_t shm_lpid; /* Идентификатор процесса, выполнившего последнюю операцию над разделяемым сегментом */ pid_t shm_cpid; /* Идентификатор процесса, создавшего разделяемый сегмент */ shmatt_t shm_nattch; /* Текущее число присоединений сегмента */ time_t shm_atime; /* Время последнего присоединения */ time_t shm_dtime; /* Время последнего отсоединения */ time_t shm_ctime; /* Время последнего изменения посредством shmctl() */


Функция shmget() аналогична msgget() и semget(); аргумент size задает нижнюю границу размера сегмента в байтах; реализация, учитывающая, например, правила выравнивания, имеет право создать разделяемый сегмент большего размера.

Структура shmid_ds инициализируется в соответствии с общими для средств межпроцессного взаимодействия правилами. Поле shm_segsz устанавливается равным значению аргумента size.

Число уникальных идентификаторов разделяемых сегментов памяти ограничено; попытка его превышения ведет к неудачному завершению shmget() (возвращается -1). Вызов shmget() завершится неудачей и тогда, когда значение аргумента size меньше минимально допустимого либо больше максимально допустимого размера разделяемого сегмента.

Чтобы присоединить разделяемый сегмент, используется функция shmat(). Аргумент shmid задает идентификатор разделяемого сегмента; аргумент shmaddr - адрес, по которому сегмент должен быть присоединен, т. е. тот адрес в виртуальном пространстве процесса, который получит начало сегмента. Поскольку свойства сегментов зависят от аппаратных особенностей управления памятью, не всякий адрес является приемлемым. Если установлен флаг SHM_RND, адрес присоединения округляется до величины, кратной константе SHMLBA.

Если shmaddr задан как пустой указатель, реализация выбирает адрес присоединения по своему усмотрению.

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

При успешном завершении функции shmat() результат равен адресу, который получил присоединенный сегмент; в случае неудачи возвращается -1. (Разумеется, для использования результата shmat() в качестве указателя его нужно преобразовать к требуемому типу.)

Отсоединение сегментов производится функцией shmdt(); аргумент shmaddr задает начальный адрес отсоединяемого сегмента.

Управление разделяемыми сегментами осуществляется при помощи функции shmctl(), аналогичной msgctl().


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

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

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

Листинг 8.41. Пример работы с разделяемыми сегментами памяти. (html, txt)

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

Листинг 8.42. Возможный результат синхронизации доступа к разделяемым данным. (html, txt)

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

Листинг 8.43. Пример работы с разделяемыми сегментами памяти и сигналами. (html, txt)

Обратим внимание на использование флагов округления адреса присоединения разделяемого сегмента (SHM_RND) и присоединения только на чтение (SHM_RDONLY), а также обработчика сигналов, задаваемого полем sa_sigaction структуры типа sigaction (в сочетании с флагом SA_SIGINFO) и имеющего доступ к расширенной информации о сигнале и его причинах.


Семафоры


Согласно определению стандарта POSIX-2001, семафор - это минимальный примитив синхронизации, служащий основой для более сложных механизмов синхронизации, определенных в прикладной программе.

У семафора есть значение, которое представляется целым числом в диапазоне от 0 до 32767.

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

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

Описание перечисленных функций представлено в листинге 8.31.

#include <sys/sem.h> int semget (key_t key, int nsems, int semflg); int semop (int semid, struct sembuf *sops, size_t nsops); int semctl (int semid, int semnum, int cmd, ...);

Листинг 8.31. Описание функций для работы с семафорами. (html, txt)

Структура semid_ds, ассоциированная с идентификатором набора семафоров, должна содержать по крайней мере следующие поля.

struct ipc_perm sem_perm; /* Данные о правах доступа к набору семафоров */ unsigned short sem_nsems; /* Число семафоров в наборе */ time_t sem_otime; /* Время последней операции semop() */ time_t sem_ctime; /* Время последнего изменения посредством semctl() */

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

unsigned short semval; /* Значение семафора */ pid_t sempid; /* Идентификатор процесса, выполнившего последнюю операцию над семафором */ unsigned short semncnt; /* Число процессов, ожидающих увеличения текущего значения семафора */ unsigned short semzcnt; /* Число процессов, ожидающих обнуления значения семафора */

Функция semget() аналогична msgget(); аргумент nsems задает число семафоров в наборе. Структура semid_ds инициализируется так же, как msqid_ds.
Безымянные структуры, соответствующие отдельным семафорам, функцией semget() не инициализируются.

Операции, выполняемые посредством функции semop(), задаются массивом sops с числом элементов nsops, состоящим из структур типа sembuf , каждая из которых содержит по крайней мере следующие поля.

unsigned short sem_num; /* Номер семафора в наборе (нумерация с нуля) */ short sem_op; /* Запрашиваемая операция над семафором */ short sem_flg; /* Флаги операции */

Операция над семафором определяется значением поля sem_op: положительное значение предписывает увеличить значение семафора на указанную величину, отрицательное - уменьшить, нулевое - сравнить с нулем. Вторая операция не может быть успешно выполнена, если в результате значение семафора становится отрицательным, а третья - если значение семафора ненулевое.

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

Приведенный в листинге 8.32 массив операций задает уменьшение (с блокировкой) семафора 1 при условии, что значение семафора 0 равно нулю.

sembuf [0].sem_num = 1; sembuf [0].sem_flg = 0; sembuf [0].sem_op = -2;

sembuf [1].sem_num = 0; sembuf [1].sem_flg = IPC_NOWAIT; sembuf [1].sem_op = 0;

Листинг 8.32. Пример задания массива операций над семафорами. (html, txt)

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



Аргументы semid (идентификатор набора семафоров) и semnum (номер семафора в наборе) определяют объект, над которым выполняется управляющее действие, задаваемое значением аргумента cmd. Если объектом является набор, значение semnum игнорируется.

Для некоторых действий задействован четвертый аргумент (см. листинг 8.33).

union semun { int val; struct semid_ds *buf; unsigned short *array; } arg;

Листинг 8.33. Описание четвертого (дополнительного) аргумента функции semctl(). (html, txt)

Среди допустимых действий - GETVAL (получить значение семафора и выдать его в качестве результата) и SETVAL (установить значение семафора равным arg.val). Имеются и аналогичные групповые действия - GETALL (прочитать значения всех семафоров набора и поместить их в массив arg.array) и SETALL (установить значения всех семафоров набора равными значениям элементов массива). Предусмотрены действия, позволяющие выяснить идентификатор процесса, выполнившего последнюю операцию над семафором (GETPID), а также число процессов, ожидающих увеличения/обнуления (GETNCNT/GETZCNT) значения семафора (информация о процессах выдается в качестве результата, см. листинг 8.34).

Листинг 8.34. Примеры управляющих действий над семафорами. (html, txt)

Наконец, для семафоров, как и для очередей сообщений, определены управляющие команды IPC_STAT (получить информацию о состоянии набора семафоров), IPC_SET (переустановить характеристики), IPC_RMID (удалить набор семафоров), представленные в листинге 8.35.

Листинг 8.35. Дополнительные примеры управляющих действий над семафорами. (html, txt)



Листинг 8.35. Дополнительные примеры управляющих действий над семафорами.

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

Предлагаемое решение состоит из двух программ. Первая (см. листинг 8.36) реализует процесс-монитор, который порождает набор семафоров (по одному семафору на каждую вилку), устанавливает начальные значения семафоров (занятой вилке будет соответствовать значение 0, свободной - 1), запускает несколько процессов, представляющих философов, указывая место за столом (в качестве одного из аргументов передается число от 1 до QPH), ожидает, пока все процессы завершатся (все философы съедят свой обед), и удаляет набор семафоров. Предполагается (для нужд функции ftok()), что исходный текст программы находится в файле phdin.c (точнее, что такой файл существует).

#include <unistd.h> #include <stdio.h> #include <sys/sem.h> #include <sys/wait.h>

/* Программа-монитор обеда философов */

#define QPH 5

#define ARG_SIZE 20

int main (void) { int key; /* Ключ набора семафоров */ int semid; /* Идентификатор набора семафоров */ int no; /* Номер философа и/или вилки */ char ssemid [ARG_SIZE], sno [ARG_SIZE], sqph [ARG_SIZE];

/* Создание и инициализация набора семафоров */ /* (по семафору на вилку) */ key = ftok ("phdin.c", 'C'); if ((semid = semget (key, QPH, 0600 | IPC_CREAT)) < 0) { perror ("SEMGET"); return (1); } for (no = 0; no < QPH; no++) { if (semctl (semid, no, SETVAL, 1) < 0) { perror ("SETVAL"); return (2); } }



sprintf (ssemid, "%d", semid); sprintf (sqph, "%d", QPH);

/* Все - к столу */ for (no = 1; no <= QPH; no++) { switch (fork ()) { case -1: perror ("FORK"); return (3); case 0: sprintf (sno, "%d", no); execl ("./phil", "phil", ssemid, sqph, sno, (char *) 0); perror ("EXEC"); return (4); } }

/* Ожидание завершения обеда */ for (no = 1; no <= QPH; no++) { (void) wait (NULL); }

/* Удаление набора семафоров */ if (semctl (semid, 0, IPC_RMID) < 0) { perror ("SEMCTL"); return (5); }

return 0; }

Листинг 8.36. Процесс-монитор для обеда философов.

Вторая программа (см. листинг 8.37) описывает обед каждого философа. Философ какое-то время беседует (случайное значение trnd), затем пытается взять вилки слева и справа от себя, когда ему это удается, некоторое время ест (случайное значение ernd), после чего освобождает вилки. Так продолжается до тех пор, пока не будет съеден весь обед. Предполагается, что выполнимый файл программы называется phil.

#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <sys/sem.h>

/* Процесс обеда одного философа */

#define ernd (rand () % 3 + 1) #define trnd (rand () % 5 + 1) #define FO 15

int main (int argc, char *argv []) { int semid; /* Идентификатор набора семафоров */ int qph; /* Число философов */ int no; /* Номер философа */ int t; /* Время очередного отрезка еды или беседы */ int fo; /* Время до конца обеда */ struct sembuf sembuf [2];

if (argc != 4) { fprintf (stderr, "Использование: %s идентификатор_набора_семафоров число_философов номер_философа \n", argv [0]); return (1); }

fo = FO; sscanf (argv [1], "%d", &semid); sscanf (argv [2], "%d", &qph); sscanf (argv [3], "%d", &no);

/* Выбор вилок */ sembuf [0].sem_num = no - 1; /* Левая */ sembuf [0].sem_flg = 0; sembuf [1].sem_num = no % qph; /* Правая */ sembuf [1].sem_flg = 0;

while (fo > 0) { /* Обед */



/* Философ говорит */ printf ("Философ %d беседует\n", no); t = trnd; sleep (t); fo -= t; /* Пытается взять вилки */ sembuf [0].sem_op = -1; sembuf [1].sem_op = -1; if (semop (semid, sembuf, 2) < 0) { perror ("SEMOP"); return (1); }

/* Ест */ printf ("Философ %d ест\n", no); t = ernd; sleep (t); fo -= t; /* Отдает вилки */ sembuf [0].sem_op = 1; sembuf [1].sem_op = 1; if (semop (semid, sembuf, 2) < 0) { perror ("SEMOP"); return (2); } }

printf ("Философ %d закончил обед\n", no); return 0; }

Листинг 8.37. Программа, описывающая обед одного философа.

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

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

/* Обедающие философы. Запуск: mudrecProc [-a | -p | -I -V] [-t число_секунд] имя_философа ... Опции: -t число_секунд - сколько секунд моделируется Стратегии захвата вилок: -a - сначала захватывается вилка с меньшим номером; -I - некорректная (но эффективная) интеллигентная стратегия: во время ожидания уже захваченная вилка кладется; -p - сначала захватывается нечетная вилка; -V - использован групповой захват семафоров. Пример запуска: mudrecProc -p -t 600 A B C D E F G H I J K L M N O P Q R S T U V W X Y Z */ static char rcsid[] __attribute__((unused)) = \ "$Id: mudrecProc.c,v 1.7 2003/11/11 13:14:07 sambor Exp $";

#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> #include <fcntl.h> #include <time.h> #include <limits.h> #include <errno.h>



#include <sys/sem.h> #include <sys/msg.h>

union semun { int val; struct semid_ds *buf; unsigned short *array; } arg;

#define max(a,b) ((a)>(b)?(a):(b)) #define min(a,b) ((a)>(b)?(b):(a))

struct mudrec { long num; char *name; int left_fork, right_fork; int eat_time, wait_time, think_time, max_wait_time; int count; };

int Stop; /* Семафор для синхронизации выхода */

/* Различные дескрипторы */ int protokol [2] = {-1, -1}; #define pFdIn (protokol [1]) #define pFdOut (protokol [0])

int semFork; /* Вилки */

int from_fil; /* Очередь для возврата результатов */

/* Разные алгоритмы захвата вилок */ static void get_forks_simple (struct mudrec *this); static void get_forks_parity (struct mudrec *this); static void get_forks_maybe_infinit_time (struct mudrec *this); static void get_forks_use_groups (struct mudrec *this);

/* Используемый метод захвата вилок */ void (*get_forks) (struct mudrec *this) = get_forks_simple;

/* Возвращение вилок */ static void put_forks (struct mudrec *this);

/* * Философы */ void filosof (struct mudrec this) { char buffer [LINE_MAX]; int bytes;

if (fork ()) return;

srandom (getpid ()); /* Очень важно для процессов, иначе получим одно и то же! */ random (); random (); random (); random (); random (); random (); random (); random (); random (); random ();

/* Пока семафор Stop не поднят */ while (!semctl (Stop, 0, GETVAL)) { /* Пора подкрепиться */ { int wait_time, tm;

sprintf (buffer, "%s: хочет есть\n", this.name); bytes = write (pFdIn, buffer, strlen (buffer));

tm = time (0);

(*get_forks) (&this);

wait_time = time (0) - tm; /* Сколько времени получали вилки */ this.wait_time += wait_time; this.max_wait_time = max (wait_time, this.max_wait_time);

sprintf (buffer, "%s: ждал вилок %d сек\n", this.name, wait_time); bytes = write (pFdIn, buffer, strlen (buffer)); }

/* Может, обед уже закончился? */ if (semctl (Stop, 0, GETVAL)) { put_forks (&this); break; }

/* Едим */ { int eat_time = random () % 20 + 1;



sleep (eat_time);

this.eat_time += eat_time; this.count++; sprintf (buffer,"%s: ел %d сек\n", this.name, eat_time); bytes = write (pFdIn, buffer, strlen (buffer)); }

/* Отдаем вилки */ put_forks (&this);

if (semctl (Stop, 0, GETVAL)) break;

/* Размышляем */ { int think_time = random () % 10 + 1;

sleep (think_time);

this.think_time += think_time; } }

sprintf (buffer,"%s: уходит\n", this.name); bytes = write (pFdIn, buffer, strlen (buffer));

msgsnd (from_fil, &this, sizeof (this), 0); /* Отослали статистику своего обеда */

_exit (0); /* ВАЖНО (_): Нам не нужны преждевременные вызовы cleanup_ipc */ }

/* Кладем вилки одну за другой */ static void put_forks (struct mudrec *this) { struct sembuf tmp_buf;

tmp_buf.sem_flg = 0; tmp_buf.sem_op = 1; tmp_buf.sem_num = this->left_fork - 1; semop (semFork, &tmp_buf, 1);

tmp_buf.sem_flg = 0; tmp_buf.sem_op = 1; tmp_buf.sem_num = this->right_fork - 1; semop (semFork, &tmp_buf, 1); }

/* Берем вилки по очереди в порядке номеров */ static void get_forks_simple (struct mudrec *this) { struct sembuf tmp_buf;

int first = min (this->left_fork, this->right_fork); int last = max (this->left_fork, this->right_fork);

tmp_buf.sem_flg = SEM_UNDO; tmp_buf.sem_op = -1; tmp_buf.sem_num = first - 1; semop (semFork, &tmp_buf, 1);

tmp_buf.sem_flg = SEM_UNDO; tmp_buf.sem_op = -1; tmp_buf.sem_num = last - 1; semop (semFork, &tmp_buf, 1); }

/* Берем сначала нечетную вилку (если обе нечетные - то с большим номером) */ static void get_forks_parity (struct mudrec *this) { struct sembuf tmp_buf;

int left = this->left_fork, right = this->right_fork; int first = max ((left & 1) * 1000 + left, (right & 1) * 1000 + right) % 1000; int last = min ((left & 1) * 1000 + left, (right & 1) * 1000 + right) % 1000;

tmp_buf.sem_flg = SEM_UNDO; tmp_buf.sem_op = -1; tmp_buf.sem_num = first - 1; semop (semFork, &tmp_buf, 1);

tmp_buf.sem_flg = SEM_UNDO; tmp_buf.sem_op = -1; tmp_buf.sem_num = last - 1; semop (semFork, &tmp_buf, 1); }



/* Берем вилки по очереди, в произвольном порядке. * Но если вторая вилка не берется сразу, то кладем первую. * То есть философ не расходует вилочное время впустую. */ static void get_forks_maybe_infinit_time (struct mudrec *this) { struct sembuf tmp_buf;

int left = this->left_fork, right = this->right_fork;

for (;;) { tmp_buf.sem_flg = SEM_UNDO; /* Первую вилку берем с ожиданием */ tmp_buf.sem_op = -1; tmp_buf.sem_num = left - 1; semop (semFork, &tmp_buf, 1);

tmp_buf.sem_flg = SEM_UNDO | IPC_NOWAIT; /* Вторую - без ожидания */ tmp_buf.sem_op = -1; tmp_buf.sem_num = right - 1;

if (0 == semop (semFork, &tmp_buf, 1)) return; /* Успех */

tmp_buf.sem_flg = 0; /* Неуспех: возвращаем первую вилку */ tmp_buf.sem_op = 1; tmp_buf.sem_num = left - 1; semop(semFork,&tmp_buf,1);

tmp_buf.sem_flg = SEM_UNDO; /* Отдав первую, ждем вторую */ tmp_buf.sem_op = -1; tmp_buf.sem_num = right - 1; semop (semFork, &tmp_buf, 1);

tmp_buf.sem_flg = SEM_UNDO | IPC_NOWAIT; /* Берем первую вилку без ожидания */ tmp_buf.sem_op = -1; tmp_buf.sem_num = left - 1;

if (0 == semop (semFork, &tmp_buf, 1)) return; /* Успех */

tmp_buf.sem_flg = 0; /* Неуспех: отдаем вторую вилку, */ tmp_buf.sem_op = 1; /* чтобы ждать первую */ tmp_buf.sem_num = right - 1; semop (semFork, &tmp_buf, 1); } }

/* Хватаем обе вилки сразу, используя групповые операции */ static void get_forks_use_groups (struct mudrec *this) { struct sembuf tmp_buf [2];

tmp_buf[0].sem_flg = SEM_UNDO; tmp_buf[0].sem_op = -1; tmp_buf[0].sem_num = this->left_fork - 1; tmp_buf[1].sem_flg = SEM_UNDO; tmp_buf[1].sem_op = -1; tmp_buf[1].sem_num = this->right_fork - 1; semop (semFork, tmp_buf, 2); }

/* * Мелкие служебные функции. */ static void stop (int dummy) { struct sembuf tmp_buf;

tmp_buf.sem_flg = 0; tmp_buf.sem_op = 1; tmp_buf.sem_num = 0; semop (Stop, &tmp_buf, 1); }

void cleanup_ipc (void) { /* * Уничтожение семафоров. */ semctl (semFork, 1, IPC_RMID); semctl (Stop, 1, IPC_RMID);

/* То же с очередью */ msgctl (from_fil, IPC_RMID, NULL); }



static void usage (char name []) { fprintf (stderr,"Использование: %s [-a | -p | -I| -V] [-t число_секунд] имя_философа ...\n", name); exit (1); }

/* * Точка входа демонстрационной программы. */ int main (int argc, char *argv[]) { char buffer [LINE_MAX], *p; int i, n, c; int open_room_time = 300; union semun tmp_arg; int nMudr; struct sigaction sact;

while ((c = getopt (argc, argv, "apIVt:")) != -1) { switch (c) { case 'a': get_forks = get_forks_simple; break; case 'p': get_forks = get_forks_parity; break; case 'I': get_forks = get_forks_maybe_infinit_time; break; case 'V': get_forks = get_forks_use_groups; break; case 't': open_room_time = strtol (optarg, &p, 0); if (optarg [0] == 0 || *p != 0) usage (argv [0]); break; default: usage (argv [0]); } }

nMudr = argc - optind; if (nMudr < 2) usage (argv [0]); /* Меньше двух философов неинтересно ... */

/* * Создание канала для протокола обработки событий */ pipe (protokol);

/* * Создадим семафоры для охраны вилок */ semFork = semget (ftok (argv [0], 2), nMudr, IPC_CREAT | 0777); tmp_arg.val = 1; for (i=1; i <= nMudr; i++) semctl (semFork, i - 1, SETVAL, tmp_arg); /* Начальное значение 1 */

/* Прежде чем впускать философов, обеспечим окончание обеда */ Stop = semget (ftok (argv [0], 3), 1, IPC_CREAT | 0777); tmp_arg.val = 0; semctl (Stop, 0, SETVAL, tmp_arg); /* Начальное значение 0 */

/* Очередь для возврата результатов */ from_fil = msgget (ftok (argv [0], 4), IPC_CREAT | 0777);

atexit (cleanup_ipc); /* Запланировали уничтожение семафоров */ /* и других средств межпроцессного взаимодействия */

/* * Философы входят в столовую */ for (i = 0; i < nMudr; i++, optind++) { struct mudrec next;

memset (&next, 0, sizeof (next));

next.num = i + 1; /* Номер */ next.name = argv [optind]; /* Имя */

/* Указали, какими вилками пользоваться */ next.left_fork = i + 1; next.right_fork = i + 2; if (i == nMudr - 1) next.right_fork = 1; /* Последний пользуется вилкой первого */

filosof (next); }



/* Зададим реакцию на сигналы и установим будильник на конец обеда */ sact.sa_handler = stop; (void) sigemptyset (&sact.sa_mask); sact.sa_flags = 0; (void) sigaction (SIGINT, &sact, (struct sigaction *) NULL); (void) sigaction (SIGALRM, &sact, (struct sigaction *) NULL);

alarm (open_room_time);

/* * Выдача сообщений на стандартный вывод и выход после окончания обеда. */ close (pFdIn); /* Сами должны закрыть, иначе из цикла не выйдем! */ for (;;) { n = read (pFdOut, buffer, LINE_MAX); if ((n == 0) || ((n == -1) && (errno != EINTR))) break; for (i = 0; i < n; i++) putchar (buffer [i]); } close (pFdOut);

/* Распечатали сводную информацию */ { int full_eating_time = 0; int full_waiting_time = 0; int full_thinking_time = 0; for (i = 1; i <= nMudr; i++) { struct mudrec this; /* Получили статистику обеда */ msgrcv (from_fil, &this, sizeof (this), i, 0); /* За счет i получаем */ /* строго по порядку */ full_eating_time += this.eat_time; full_waiting_time += this.wait_time; full_thinking_time += this.think_time;

if (this.count > 0) { float count = this.count; float think_time = this.think_time / count; float eat_time = this.eat_time / count; float wait_time = this.wait_time / count;

printf ("%s: ел %d раз в среднем: думал=%.1f ел=%.1f ждал=%.1f (максимум %d)\n", this.name, this.count, think_time, eat_time, wait_time, this.max_wait_time); } else printf("%s: не поел\n", this.name); } { float total_time = (full_eating_time + full_waiting_time + full_thinking_time) / (float)nMudr;

printf (" Среднее число одновременно едящих = %.3f\n Среднее число одновременно ждущих = %.3f\n", full_eating_time / total_time, full_waiting_time / total_time); } }

/* Сообщим об окончании работы */ printf ("Конец обеда\n");

return 0; }

Листинг 8.38. Второй вариант решения задачи об обедающих философах.

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


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

Отметим небольшие терминологические различия в двух приведенных вариантах решения задачи об обедающих философах. Во втором варианте явно выделены начальные и конечные моделируемые события - вход философов в столовую и выход из нее (в первом варианте они просто сидят за столом).

С методической точки зрения второй вариант интересен тем, что в нем использованы все рассмотренные нами средства межпроцессного взаимодействия - каналы, сигналы, очереди сообщений и, конечно, семафоры. (Тонкость: флаг SEM_UNDO обеспечивает корректировку значения семафора при завершении процесса.)

В листинге 8.39 приведена статистика поведения пяти философов для всех четырех стратегий при времени моделирования 100 секунд. Эти результаты говорят в пользу групповых операций над семафорами.

-a: A: ел 2 раза в среднем: думал=3.5 ел=11.5 ждал=36.5 (максимум 73) B: ел 3 раза в среднем: думал=5.7 ел=7.7 ждал=20.0 (максимум 41) C: ел 3 раза в среднем: думал=5.7 ел=11.3 ждал=17.0 (максимум 33) D: ел 3 раза в среднем: думал=1.7 ел=16.7 ждал=15.7 (максимум 19) E: ел 1 раз в среднем: думал=10.0 ел=20.0 ждал=73.0 (максимум 41) Среднее число одновременно едящих = 1.471 Среднее число одновременно ждущих = 2.980 -p: A: ел 3 раза в среднем: думал=3.7 ел=15.3 ждал=16.0 (максимум 34) B: ел 4 раза в среднем: думал=5.0 ел=13.8 ждал=8.2 (максимум 15) C: ел 3 раза в среднем: думал=6.7 ел=3.7 ждал=25.7 (максимум 27) D: ел 4 раза в среднем: думал=5.8 ел=8.5 ждал=13.8 (максимум 28) E: ел 3 раза в среднем: думал=5.3 ел=15.3 ждал=16.7 (максимум 29) Среднее число одновременно едящих = 1.761 Среднее число одновременно ждущих = 2.413

-I: A: ел 5 раз в среднем: думал=4.2 ел=9.4 ждал=6.6 (максимум 15) B: ел 3 раза в среднем: думал=6.3 ел=10.3 ждал=17.0 (максимум 31) C: ел 4 раза в среднем: думал=6.8 ел=7.0 ждал=12.2 (максимум 45) D: ел 3 раза в среднем: думал=4.3 ел=16.0 ждал=13.0 (максимум 16) E: ел 4 раза в среднем: думал=5.8 ел=8.5 ждал=10.8 (максимум 22) Среднее число одновременно едящих = 1.858 Среднее число одновременно ждущих = 2.125

-V: A: ел 5 раз в среднем: думал=5.6 ел=5.6 ждал=8.8 (максимум 17) B: ел 3 раза в среднем: думал=6.3 ел=10.3 ждал=16.7 (максимум 20) C: ел 4 раза в среднем: думал=4.8 ел=11.0 ждал=9.8 (максимум 18) D: ел 4 раза в среднем: думал=5.2 ел=12.0 ждал=8.8 (максимум 15) E: ел 4 раза в среднем: думал=5.2 ел=10.5 ждал=10.2 (максимум 20) Среднее число одновременно едящих = 1.892 Среднее число одновременно ждущих = 2.049

Листинг 8.39. Результаты моделирования поведения философов.


Сигналы


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

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

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

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

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

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

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

С сигналом могут быть ассоциированы действия одного из трех типов.

SIG_DFL

Подразумеваемые действия, зависящие от сигнала. Они описаны в заголовочном файле <signal.h>.

SIG_IGN

Игнорировать сигнал. Доставка сигнала не оказывает воздействия на процесс.

указатель на функцию

Обработать сигнал, выполнив при его доставке заданную функцию. После завершения функции обработки процесс возобновляет выполнение с точки прерывания. Обычно функция обработки вызывается в соответствии со следующим C-заголовком: void func (int signo); где signo - номер доставленного сигнала.

Первоначально, до входа в функцию main(), реакция на все сигналы установлена как SIG_DFL или SIG_IGN.

Функция называется асинхронно-сигнально-безопасной (АСБ), если ее можно вызывать без каких-либо ограничений при обработке сигналов. В стандарте POSIX-2001 имеется список функций, которые должны быть либо повторно входимыми, либо непрерываемыми сигналами, что превращает их в АСБ-функции. В этот список включены 117 функций, в том числе почти все из рассматриваемых нами.

Если сигнал доставляется потоку, а реакция заключается в завершении, остановке или продолжении, весь процесс должен завершиться, остановиться или продолжиться.

Перейдем к изложению возможностей по генерации сигналов. Выше была кратко рассмотрена служебная программа kill как средство терминирования процессов извне. На самом деле она посылает заданный сигнал; то же делает и одноименная функция (см. листинг 8.6).

#include <signal.h> int kill (pid_t pid, int sig);

Листинг 8.6. Описание функции kill(). (html, txt)

Сигнал задается аргументом sig, значение которого может быть нулевым; в этом случае действия функции kill() сводятся к проверке допустимости значения pid (нулевой результат - признак успешного завершения kill()).



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

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

У служебной программы kill имеется полезная опция -l, позволяющая увидеть соответствие между номерами сигналов и их мнемоническими именами. Результат выполнения команды kill -l может выглядеть так, как показано в листинге 8.7.

Листинг 8.7. Возможный результат выполнения команды kill -l. (html, txt)



SIGABRT

Сигнал аварийного завершения процесса. Подразумеваемая реакция предусматривает, помимо аварийного завершения, создание файла с образом памяти процесса.

SIGALRM

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

SIGBUS

Ошибка системной шины как следствие обращения к неопределенной области памяти. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.

SIGCHLD

Завершение, остановка или продолжение порожденного процесса. Подразумеваемая реакция - игнорирование.

SIGCONT

Продолжение процесса, если он был остановлен. Подразумеваемая реакция - продолжение выполнения или игнорирование (если процесс не был остановлен).

SIGFPE

Некорректная арифметическая операция. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.

SIGHUP

Сигнал разъединения. Подразумеваемая реакция - аварийное завершение процесса.

SIGILL

Некорректная команда. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.

SIGINT

Сигнал прерывания, поступивший с терминала. Подразумеваемая реакция - аварийное завершение процесса.

SIGKILL

Уничтожение процесса (этот сигнал нельзя перехватить для обработки или проигнорировать). Подразумеваемая реакция - аварийное завершение процесса.

SIGPIPE

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

SIGQUIT

Сигнал выхода, поступивший с терминала. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.

SIGSEGV

Некорректное обращение к памяти. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.

SIGSTOP

Остановка выполнения (этот сигнал нельзя перехватить для обработки или проигнорировать). Подразумеваемая реакция - остановка процесса.

SIGTERM

Сигнал терминирования. Подразумеваемая реакция - аварийное завершение процесса.

SIGTSTP

Сигнал остановки, поступивший с терминала. Подразумеваемая реакция - остановка процесса.



SIGTTIN

Попытка чтения из фонового процесса. Подразумеваемая реакция - остановка процесса.

SIGTTOU

Попытка записи из фонового процесса. Подразумеваемая реакция - остановка процесса.

SIGUSR1, SIGUSR2

Определяемые пользователем сигналы. Подразумеваемая реакция - аварийное завершение процесса.

SIGPOLL

Опрашиваемое событие. Подразумеваемая реакция - аварийное завершение процесса.

SIGPROF

Срабатывание таймера профилирования. Подразумеваемая реакция - аварийное завершение процесса.

SIGSYS

Некорректный системный вызов. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.

SIGTRAP

Попадание в точку трассировки/прерывания. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.

SIGURG

Высокоскоростное поступление данных в сокет. Подразумеваемая реакция - игнорирование.

SIGVTALRM

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

SIGXCPU

Исчерпан лимит процессорного времени. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.

SIGXFSZ

Превышено ограничение на размер файлов. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.

Процесс (поток управления) может послать сигнал самому себе с помощью функции raise() (см. листинг 8.8). Для процесса вызов raise() эквивалентен kill (getpid(), sig);

#include <signal.h> int raise (int sig);

Листинг 8.8. Описание функции raise().

Посылка сигнала самому себе использована в функции abort() (см. листинг 8.9), вызывающей аварийное завершение процесса. (Заметим, что этого не произойдет, если функция обработки сигнала   SIGABRT не возвращает управления. С другой стороны, abort() отменяет блокирование или игнорирование SIGABRT.)

#include <stdlib.h> void abort (void);

Листинг 8.9. Описание функции abort().

Опросить и изменить способ обработки сигналов позволяет функция sigaction() (см. листинг 8.10).

#include <signal.h> int sigaction (int sig, const struct sigaction *restrict act, struct sigaction *restrict oact);



Листинг 8.10. Описание функции sigaction().

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

void (*sa_handler) (int); /* Указатель на функцию обработки сигнала */ /* или один из макросов SIG_DFL или SIG_IGN */ sigset_t sa_mask; /* Дополнительный набор сигналов, блокируемых */ /* на время выполнения функции обработки */ int sa_flags; /* Флаги, влияющие на поведение сигнала */ void (*sa_sigaction) (int, siginfo_t *, void *); /* Указатель на функцию обработки сигнала */

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

Тип sigset_t может быть целочисленным или структурным и представлять набор сигналов (см. далее).

Тип siginfo_t должен быть структурным по крайней мере со следующими полями:

int si_signo; /* Номер сигнала */ int si_errno; /* Значение переменной errno, ассоциированное с данным сигналом */ int si_code; /* Код, идентифицирующий причину сигнала */ pid_t si_pid; /* Идентификатор процесса, пославшего сигнал */ uid_t si_uid; /* Реальный идентификатор пользователя процесса, пославшего сигнал */ void *si_addr; /* Адрес, вызвавший генерацию сигнала */ int si_status; /* Статус завершения порожденного процесса */ long si_band; /* Событие, связанное с сигналом SIGPOLL */

В заголовочном файле <signal.h> определены именованные константы, предназначенные для работы с полем si_code, значения которого могут быть как специфичными для конкретного сигнала, так и универсальными. К числу универсальных кодов относятся:

SI_USER

Сигнал послан функцией kill().

SI_QUEUE

Сигнал послан функцией sigqueue().

SI_TIMER

Сигнал сгенерирован в результате срабатывания таймера, установленного функцией timer_settime().

SI_ASYNCIO

Сигнал вызван завершением асинхронной операции ввода/вывода.

SI_MESGQ

Сигнал вызван приходом сообщения в пустую очередь сообщений.

Из кодов, специфичных для конкретных сигналов, мы упомянем лишь несколько, чтобы дать представление о степени детализации диагностики, предусмотренной стандартом POSIX-2001. (Из имени константы ясно, к какому сигналу она относится.)



ILL_ILLOPC

Некорректный код операции.

ILL_COPROC

Ошибка сопроцессора.

FPE_INTDIV

Целочисленное деление на нуль.

FPE_FLTOVF

Переполнение при выполнении операции вещественной арифметики.

FPE_FLTSUB

Индекс вне диапазона.

SEGV_MAPERR

Адрес не отображен на объект.

BUS_ADRALN

Некорректное выравнивание адреса.

BUS_ADRERR

Несуществующий физический адрес.

TRAP_BRKPT

Процесс достиг точки прерывания.

TRAP_TRACE

Срабатывание трассировки процесса.

CLD_EXITED

Завершение порожденного процесса.

CLD_STOPPED

Остановка порожденного процесса.

POLL_PRI

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

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

Следующие флаги в поле sa_flags влияют на поведение сигнала   sig.

SA_NOCLDSTOP

Не генерировать сигнал   SIGCHLD при остановке или продолжении порожденного процесса (значение аргумента sig должно равняться SIGCHLD).

SA_RESETHAND

При входе в функцию обработки сигнала   sig установить подразумеваемую реакцию SIG_DFL и очистить флаг SA_SIGINFO (см. далее).

SA_SIGINFO

Если этот флаг не установлен и определена функция обработки сигнала   sig, она вызывается с одним целочисленным аргументом - номером сигнала. Соответственно, в приложении следует использовать поле sa_handler структуры sigaction. При установленном флаге SA_SIGINFO функция обработки вызывается с двумя дополнительными аргументами, как void func (int sig, siginfo_t *info, void *context); второй аргумент указывает на данные, поясняющие причину генерации сигнала, а третий может быть преобразован к указателю на тип ucontext_t - контекст процесса, прерванного доставкой сигнала. В этом случае приложение должно использовать поле sa_sigaction и поля структуры типа siginfo_t.


В частности, если значение si_code неположительно, сигнал был сгенерирован процессом с идентификатором si_pid и реальным идентификатором пользователя si_uid.

SA_NODEFER

По умолчанию обрабатываемый сигнал добавляется к маске сигналов процесса при входе в функцию обработки; флаг SA_NODEFER предписывает не делать этого, если только sig не фигурирует явным образом в sa_mask.

Опросить и изменить способ обработки сигналов можно и на уровне командного интерпретатора, посредством специальной встроенной команды trap:

trap [действие условие ...]

Аргумент "условие" может задаваться как EXIT (завершение командного интерпретатора) или как имя доставленного сигнала (без префикса SIG). При задании аргумента "действие" минус обозначает подразумеваемую реакцию, пустая цепочка ("") - игнорирование. Если в качестве действия задана команда, то при наступлении условия она обрабатывается как eval действие.

Команда trap без аргументов выдает на стандартный вывод список команд, ассоциированных с каждым из условий. Выдача имеет формат, пригодный для восстановления способа обработки сигналов (см. листинг 8.11).

save_traps=$(trap) . . . eval "$save_traps"

Листинг 8.11. Пример сохранения и восстановления способа обработки сигналов посредством специальной встроенной команды trap.

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

trap '$HOME/logout' EXIT

Листинг 8.12. Пример использования специальной встроенной команды trap.

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

trap "" PIPE echo "$INITLOG_ARGS -n $0 -s \"$1\" -e 1" >&21 trap - PIPE



Листинг 8.13. Пример использования специальной встроенной команды trap для защиты от ошибок, специфичных для каналов.

К техническим аспектам можно отнести работу с наборами сигналов, которая выполняется посредством функций, показанных в листинге 8.14. Функции sigemptyset() и sigfillset() инициализируют набор, делая его, соответственно, пустым или "полным". Функция sigaddset() добавляет сигнал   signo к набору set, sigdelset() удаляет сигнал, а sigismember() проверяет вхождение в набор. Обычно признаком завершения является нулевой результат, в случае ошибки возвращается -1. Только sigismember() выдает 1, если сигнал   signo входит в набор set.

#include <signal.h> int sigemptyset (sigset_t *set); int sigfillset (sigset_t *set); int sigaddset (sigset_t *set, int signo); int sigdelset (sigset_t *set, int signo); int sigismember (const sigset_t *set, int signo);

Листинг 8.14. Описание функций для работы с наборами сигналов.

Функция sigprocmask() (см. листинг 8.15) предназначена для опроса и/или изменения маски сигналов процесса, определяющей набор блокируемых сигналов.

#include <signal.h> int sigprocmask (int how, const sigset_t *restrict set, sigset_t *restrict oset);

Листинг 8.15. Описание функции sigprocmask().

Если аргумент set отличен от NULL, он указывает на набор, используемый для изменения текущей маски сигналов. Аргумент how определяет способ изменения; он может принимать одно из трех значений: SIG_BLOCK (результирующая маска получается при объединении текущей и заданной аргументом set), SIG_SETMASK (результирующая маска устанавливается равной set) и SIG_UNBLOCK (маска set вычитается из текущей).

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

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



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

Функция sigpending() (см. листинг 8.16) позволяет выяснить набор блокированных сигналов, ожидающих доставки вызывающему процессу (потоку управления). Дождаться появления подобного сигнала можно с помощью функции sigwait() (см. листинг 8.17).

#include <signal.h> int sigpending (sigset_t *set);

Листинг 8.16. Описание функции sigpending().

#include <signal.h> int sigwait (const sigset_t *restrict set, int *restrict sig);

Листинг 8.17. Описание функции sigwait().

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

Отметим, что стандарт POSIX-2001 не специфицирует воздействие функции sigwait() на обработку сигналов, включенных в набор set. Чтобы дождаться доставки обрабатываемого или терминирующего процесс сигнала, можно воспользоваться функцией pause() (см. листинг 8.18).

#include <unistd.h> int pause (void);

Листинг 8.18. Описание функции pause().

Функция pause() может ждать доставки сигнала неопределенно долго. Возврат из pause() осуществляется после возврата из функции обработки сигнала (результат при этом равен -1). Если прием сигнала вызывает завершение процесса, возврата из функции pause(), естественно, не происходит.

Несмотря на внешнюю простоту, использование функции pause() сопряжено с рядом тонкостей. При наивном подходе сначала проверяют некоторое условие, связанное с сигналом, и, если оно не выполнено (сигнал отсутствует), вызывают pause(). К сожалению, сигнал может быть доставлен в промежутке между проверкой и вызовом pause(), что нарушает логику работы процесса и способно привести к его зависанию. Решить подобную проблему позволяет функция sigsuspend() (см. листинг 8.19) в сочетании с рассмотренной выше функцией sigprocmask().



#include <signal.h> int sigsuspend (const sigset_t *sigmask);

Листинг 8.19. Описание функции sigsuspend().

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

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

В качестве примера использования описанных выше функций работы с сигналами рассмотрим упрощенную реализацию функции abort() (см. листинг 8.20).

#include <unistd.h> #include <signal.h> #include <stdio.h>

void abort (void) { struct sigaction sact; sigset_t sset;

/* Вытолкнем буфера */ (void) fflush (NULL);

/* Снимем блокировку сигнала SIGABRT */ if ((sigemptyset (&sset) == 0) && (sigaddset (&sset, SIGABRT) == 0)) { (void) sigprocmask (SIG_UNBLOCK, &sset, (sigset_t *) NULL); }

/* Пошлем себе сигнал SIGABRT. */ /* Возможно, его перехватит функция обработки, */ /* и тогда вызывающий процесс может не завершиться */ raise (SIGABRT);

/* Установим подразумеваемую реакцию на сигнал SIGABRT */ sact.sa_handler = SIG_DFL; sigfillset (&sact.sa_mask); sact.sa_flags = 0; (void) sigaction (SIGABRT, &sact, NULL);

/* Снова пошлем себе сигнал SIGABRT */ raise (SIGABRT);

/* Если сигнал снова не помог, попробуем еще одно средство завершения */ _exit (127); }

int main (void) { printf ("Перед вызовом abort()\n"); abort (); printf ("После вызова abort()\n"); return 0; }

Листинг 8.20. Упрощенная реализация функции abort() как пример использования функций работы с сигналами.

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



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

#include <unistd.h> #include <stdio.h> #include <signal.h> #include <time.h>

/* Функция обработки сигнала SIGALRM. */ /* Она ничего не делает, но игнорировать сигнал нельзя */ static void signal_handler (int sig) { /* В демонстрационных целях распечатаем номер обрабатываемого сигнала */ printf ("Принят сигнал %d\n", sig); }

/* Функция для "засыпания" на заданное число секунд */ /* Результат равен разности между заказанной и фактической */ /* продолжительностью "сна" */ unsigned int sleep (unsigned int seconds) { time_t before, after; unsigned int slept; sigset_t set, oset; struct sigaction act, oact;

if (seconds == 0) { return 0; }

/* Установим будильник на заданное время, */ /* но перед этим блокируем сигнал SIGALRM */ /* и зададим свою функцию обработки для него */ if ((sigemptyset (&set) < 0) || (sigaddset (&set, SIGALRM) < 0) || sigprocmask (SIG_BLOCK, &set, &oset)) { return seconds; }

act.sa_handler = signal_handler; act.sa_flags = 0; act.sa_mask = oset; if (sigaction (SIGALRM, &act, &oact) < 0) { return seconds; }

before = time ((time_t *) NULL); (void) alarm (seconds);

/* Как атомарное действие восстановим старую маску сигналов */ /* (в надежде, что она не блокирует SIGALRM) */ /* и станем ждать доставки обрабатываемого сигнала */ (void) sigsuspend (&oset); /* сигнал доставлен и обработан */

after = time ((time_t *) NULL);

/* Восстановим прежний способ обработки сигнала SIGALRM */ (void) sigaction (SIGALRM, &oact, (struct sigaction *) NULL);

/* Восстановим первоначальную маску сигналов */ (void) sigprocmask (SIG_SETMASK, &oset, (sigset_t *) NULL);

return ((slept = after - before) > seconds ? 0 : (seconds - slept)); }



int main (void) { struct sigaction act;

/* В демонстрационных целях установим обработку прерывания с клавиатуры */ act.sa_handler = signal_handler; (void) sigemptyset (&act.sa_mask); act.sa_flags = 0; (void) sigaction (SIGINT, &act, (struct sigaction *) NULL);

printf ("Заснем на 10 секунд\n"); printf ("Проснулись, не доспав %d секунд\n", sleep (10)); return (0); }

Листинг 8.21. Упрощенная реализация функции sleep() как пример использования механизма сигналов.

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

Вызвать "недосыпание" приведенной программы можно, послав ей сигнал   SIGALRM (например, посредством команды kill -s SIGALRM идентификатор_процесса) или SIGINT (путем нажатия на клавиатуре терминала комбинации клавиш CTRL+C).


Основные понятия и объекты


В стандарте POSIX-2001 терминал или терминальное устройство определяется как символьный специальный файл, удовлетворяющий спецификациям общего терминального интерфейса.

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

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

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

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

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

В неканоническом режиме входные данные не подвергаются препроцессированию системой, а обработка запроса на чтение зависит от двух параметров - MIN и TIME. Запрос на чтение не будет удовлетворен, пока не поступит по крайней мере MIN байт или не истечет время задержки TIME (время задается в десятых долях секунды).
Нулевое значение TIME трактуется как бесконечная задержка.

Более точно, если MIN > 0, TIME трактуется как задержка между поступлениями байт; следовательно, отсчет времени начинается после прихода очередного байта. Если MIN = 0, TIME означает общее время обслуживания запроса на чтение. Такой подход позволяет эффективно читать во время вспышек активности ввода и не препятствует побайтному вводу.

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

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

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

INTR

Генерирует сигнал прерывания (SIGINT), посылаемый всем процессам, для которых данный терминал является управляющим.

QUIT

Генерирует сигнал выхода.

ERASE

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

KILL

При каноническом режиме ввода уничтожает всю строку.

EOF

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

NL

Стандартный разделитель строк (перевод строки) при каноническом режиме ввода. Его нельзя изменить.

EOL

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

SUSP

Генерирует сигнал остановки.

STOP

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

START

Употребляется для возобновления вывода, приостановленного с помощью символа STOP.

CR

При каноническом режиме ввода и выполнении некоторых дополнительных условий - эквивалент перевода строки.



Обычно не используется.

SUSP

Генерирует сигнал остановки.

STOP

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

START

Употребляется для возобновления вывода, приостановленного с помощью символа STOP.

CR

При каноническом режиме ввода и выполнении некоторых дополнительных условий - эквивалент перевода строки.

Центральную роль в управлении терминалами играет структура termios, определенная во включаемом файле <termios.h>. Она должна содержать по крайней мере следующие поля.

tcflag_t c_iflag; /* Режимы ввода */ tcflag_t c_oflag; /* Режимы вывода */ tcflag_t c_cflag; /* Управляющие режимы */ tcflag_t c_lflag; /* Локальные режимы */ cc_t c_cc [NCCS]; /* Специальные управ- ляющие символы */

Типы tcflag_t, cc_t и фигурирующий далее speed_t должны определяться реализацией посредством typedef как беззнаковые целые.

Обращение к элементам массива c_cc, хранящего специальные управляющие символы, которые могут быть изменены, выполняется с помощью индексов с именами, полученными вставкой буквы V перед названием символа: VEOF, VEOL, VERASE, VINTR, VKILL, VQUIT, VSTART, VSTOP, VSUSP. Кроме того, еще два индекса, VMIN и VTIME, используются для работы со значениями MIN и TIME и могут совпадать с VEOF и VEOL, соответственно (поскольку символы EOF и EOL нужны только в каноническом режиме, а значения MIN и TIME - только в неканоническом).

Поле c_iflag структуры termios описывает основные параметры терминального ввода.

BRKINT

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

ICRNL

Преобразовывать возврат каретки в перевод строки.

IGNBRK

Игнорировать разрыв соединения.

IGNCR

Игнорировать возврат каретки.

IGNPAR

Игнорировать символы с ошибками четности.



INLCR

Преобразовывать перевод строки в возврат каретки.

INPCK

Разрешить контроль четности.

ISTRIP

Отбрасывать старший бит, обрезая байты до семи бит.

IXOFF

Разрешить старт/стопное управление вводом.

IXON

Разрешить старт/стопное управление выводом.

PARMRK

Отмечать ошибки четности.

Поле c_oflag определяет системную обработку вывода. К числу обязательных для поддержки стандарт POSIX-2001 относит только один флаг - OPOST> (постпроцессировать вывод). В расширение XSI входят флаги, определяющие характер постпроцессирования: ONLCR (преобразовывать перевод строки в пару - перевод строки, возврат каретки), OCRNL (преобразовывать возврат каретки в перевод строки), NLDLY (выбрать задержку для перевода строки) и т.п.

Поле управляющих режимов c_cflag описывает аппаратные характеристики линии и терминала: размер символа в битах (CSIZE: от CS5 - 5 бит до CS8 - 8 бит), число стоп-бит (CSTOPB: два стоп-бита), освобождение линии при закрытии последнего файлового дескриптора, ассоциированного с терминалом (HUPCL), контроль четности (PARENB: контроль включен; PARODD: проверка на нечетность) и т.п.

Стандарт осторожен в части представления скорости передачи. Оговаривается только, что скорость хранится в структуре termios как значение типа speed_t, но не утверждается, что она представлена как часть поля c_cflag (хотя в исторически сложившихся реализациях это так). Допустимые значения скорости задаются именованными константами: от B0 - нулевой, означающей разрыв соединения, до B38400 - 38400 бит/сек.

Поле локальных режимов c_lflag структуры termios используется для управления различными характеристиками терминала. В их число входят:

ECHO

Включить эхоотображение.

ECHOE

Отображать символ забоя как тройку (возврат на шаг, пробел, возврат на шаг).

ECHOK

Отображать символ уничтожения строки (по крайней мере путем выдачи перевода строки).

ECHONL

Включить эхоотображение перевода строки.

ICANON

Канонический режим ввода.

ISIG

Разрешить сигналы. Если установлен флаг ISIG, то каждый вводимый символ сравнивается со специальными управляющими символами INTR, QUIT и SUSP.В случае совпадения выполняется ассоциированная функция.


res tty


tty > /tmp/tty. res tty < /tmp/tty.res > /tmp/tty.res
Листинг 9.1. Пример использования служебной программы tty.
Закрыть окно




/dev/ttyS4 not a tty
Листинг 9.2. Возможный результат использования служебной программы tty.
Закрыть окно




#include <unistd.h> int isatty (int fildes); char *ttyname (int fildes);
Листинг 9.3. Описание функций isatty() и ttyname().
Закрыть окно




speed 19200 baud; rows 0; columns 0; line = 0; intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; start = ^Q; stop = ^S; susp = <undef>; rprnt = ^R; werase = ^W; lnext = ^V; flush = ^O; min = 1; time = 0; -parenb -parodd cs8 hupcl -cstopb cread clocal -crtscts -ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr -icrnl ixon ixoff -iuclc -ixany -imaxbel opost -olcuc -ocrnl -onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0 isig icanon -iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt -echoctl echoke
Листинг 9.4. Возможный результат команды stty -a.
Закрыть окно




saved="$(stty -g)" stty новые_характеристики . . . stty $saved
Листинг 9.5. Пример сохранения и восстановления характеристик терминала.
Закрыть окно




if [ -x /usr/bin/tput ]; then if [ "x`tput kbs`" != "x" ]; then stty erase `tput kbs` elif [ -x /usr/bin/wc ]; then if [ "`tput kbs | wc -c `" -gt 0 ]; then stty erase ` tput kbs` fi fi fi
Листинг 9.6. Пример совместного использования утилит stty и tput.
Закрыть окно




#include <termios.h> int tcgetattr (int fildes, struct termios *termios_p); int tcsetattr ( int fildes, int optional_actions, const struct termios *termios_p); int tcflow (int fildes, int action); int tcflush (int fildes, int queue_selector); int tcdrain (int fildes); int tcsendbreak (int fildes, int duration);
Листинг 9.7. Описание функций семейства tc*().
Закрыть окно




#include <termios.h> speed_t cfgetispeed ( const struct termios *termios_p); speed_t cfgetospeed (const struct termios *termios_p); int cfsetispeed (struct termios *termios_p, speed_t speed); int cfsetospeed (struct termios *termios_p, speed_t speed);
Листинг 9.8. Описание функций семейства cf*().
Закрыть окно




#include <poll.h> int poll ( struct pollfd fds [], nfds_t nfds, int timeout);
Листинг 9.9. Описание функции poll().
Закрыть окно




/* * * * * * * * * * * * * * * * * * * * * * * * */ /* Программа запускает shell на псевдотерминале */ /* * * * * * * * * * * * * * * * * * * * * * * * */
#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <fcntl.h> #include <termios.h> #include <signal.h> #include <poll.h> #include <sys/resource.h> #include <curses.h>
/* Действия при завершении процесса */ static void termination (int errcode) { endwin (); exit (errcode); }
/* Функция обработки сигнала SIGCHLD */ static void chldied (int dummy) { /* Просто завершимся*/ termination (34); }
int main (void) { WINDOW *win1, *win2; /* win1 - окно только для рамки */ /* win2 - окно для shell */
int pty, tty; /* Дескрипторы обеих сторон псевдотерминала */ int fr; /* Результат fork'а */ unsigned char ch; /* Прочитанный символ */ struct termios pt; /* Структура для смены характеристик псевдотерминала */ struct pollfd fds [2]; /* Массив параметров для вызова poll */ char ptybuf [L_ctermid]; /* Массив для хранения имени псевдотерминала */ char *s, *t; /* Указатели для перебора компонентов имени псевдотерминала */ int w2lines, w2cols; /* Размер создаваемого окна */ int x, y; /* Координаты в окне */ struct sigaction sact; int i;
initscr (); cbreak (); noecho ();
win1 = newwin (LINES, COLS, 0, 0); box (win1, 0, 0); wrefresh (win1);
w2lines = LINES - 2; w2cols = COLS - 4; win2 = newwin (w2lines, w2cols, 1, 2); scrollok (win2, TRUE);
/* Откроем первый свободный псевдотерминал */ for (s = "pqrs"; *s; s++) { for (t = "0123456789abcdef"; *t; t++) { sprintf (ptybuf, "/dev/pty%c%c", *s, *t); if ((pty = open (ptybuf, O_RDWR)) >= 0) { goto findpty; } } }
fprintf (stderr, "Не удалось найти свободный псевдотерминал\n"); termination (-1);
findpty: ptybuf [5] = 't'; if ((tty = open (ptybuf, O_RDWR)) < 0) { perror ("TTY OPEN ERROR"); termination (-1); }
/* Установим подходящие характеристики псевдотерминала */ if (tcgetattr (pty, &pt) < 0) { perror ("PTY TERMIOS GET ERROR"); return (1); } pt.c_iflag = 0; pt.c_oflag = ONLCR; pt.c_cflag = CS8 | HUPCL; pt.c_lflag = ISIG | ICANON | ECHO | ECHOE | ECHOK; pt.c_cc [VINTR] = 3; /* CTRL+C */ pt.c_cc [VEOF] = 4; /* CTRL+D */ if (tcsetattr (pty, TCSADRAIN, &pt) < 0) { perror ("PTY TERMIOS SET ERROR"); return (2); }
/* То же - для стандартного ввода */ (void) tcgetattr (0, &pt); pt.c_lflag &= ~ISIG; (void) tcsetattr (0, TCSADRAIN, &pt);
/* Установим обработку сигнала о завершении потомка */ sact.sa_handler = chldied; (void) sigemptyset (&sact.sa_mask); sact.sa_flags = 0; (void) sigaction (SIGCHLD, &sact, (struct sigaction *) NULL);
/* Раздвоимся на процесс чтения с клавиатуры и вывода на экран */ /* и на процесс, в рамках которого запустим shell */
if ((fr = fork ()) < 0) { perror ("FORK1 ERROR"); termination (-1); } else if (fr) { /* Это процесс, читающий с клавиатуры */ /* и выводящий на экран */ close (tty);
/* Будем ждать ввода с клавиатуры или псевдотерминала */ fds [0].fd = 0; fds [0].events = POLLIN; fds [1].fd = pty; fds [1].events = POLLIN;
while (1) { if (poll (fds, 2, -1) < 0) { perror ("POLL ERROR"); termination (0); } if (fds [0].revents & POLLIN) { /* Пришел символ со стандартного ввода */ read (0, &ch, 1); write (pty, &ch, 1); } if (fds [1].revents & POLLIN) { /* Пришел символ с псевдотерминала */ read (pty, &ch, 1); switch (ch) { case '\n': { /* Проинтерпретируем перевод строки */ getyx (win2, y, x); if (y == (w2lines - 1)) { wmove (win2, y, w2cols - 1); waddch (win2, (chtype) ch); } else { wmove (win2, y + 1, 0); } break; } default: { /* Символ не интерпретируется */ waddch (win2, (chtype) ch); break; } } wrefresh (win2); } } /* Просто завершимся */ termination (0);
} else { /* Порожденный процесс - запустим в нем shell */ /* Закроем все файлы, кроме псевдотерминала */ for (i = 0; i < RLIMIT_NOFILE; i++) { if (i != tty) { (void) close (i); } }
/* Сделаем процесс лидером сеанса */ (void) setsid ();
/* Свяжем стандартные ввод, вывод и протокол с псевдотерминалом */ (void) fcntl (tty, F_DUPFD, 0); (void) fcntl (tty, F_DUPFD, 0); (void) fcntl (tty, F_DUPFD, 0); close (tty);
/* Сделаем псевдотерминал управляющим */ if ((tty = open (ptybuf, O_RDWR)) < 0) { perror ("TTY OPEN ERROR"); exit (-1); } close (tty);
/* Поместим в окружение параметры псевдотерминала */ { char lnbuf [20]; char clbuf [20];
sprintf (lnbuf, "LINES=%2d", w2lines); sprintf (clbuf, "COLUMNS=%2d", w2cols);
putenv (lnbuf); putenv (clbuf); }
if (execl ("/bin/sh", "sh", (char *) NULL) < 0) { perror ("EXECL ERROR"); exit (-1); } }
return 0; }
Листинг 9.10. Пример программы, использующей псевдотерминалы.
Закрыть окно




#include <unistd.h> pid_t tcgetpgrp (int fildes);
#include <unistd.h> int tcsetpgrp ( int fildes, pid_t pgid_id);
#include <termios.h> pid_t tcgetsid (int fildes);
Листинг 9.11. Описание функций семейства tc*() для работы с управляющими терминалами.
Закрыть окно




#include <stdio.h> char *ctermid (char *s);
Листинг 9.12. Описание функции ctermid().
Закрыть окно



Служебные программы и функции для управления терминалами


Служебная программа

tty

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

Если для примера перенаправить стандартный ввод (см. пример 9.1), можно получить результат, показанный в пример 9.2.

tty > /tmp/tty.res tty < /tmp/tty.res > /tmp/tty.res

Листинг 9.1. Пример использования служебной программы tty. (html, txt)

/dev/ttyS4 not a tty

Листинг 9.2. Возможный результат использования служебной программы tty. (html, txt)

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

#include <unistd.h> int isatty (int fildes); char *ttyname (int fildes);

Листинг 9.3. Описание функций isatty() и ttyname(). (html, txt)

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

Каждый терминал обладает рядом характеристик, которые можно опросить и/или изменить. Для этого служит утилита stty:

stty [ -a | -g] stty характеристика ...

Будучи вызванной без опций и операндов, она выдает значения основных характеристик терминала, ассоциированного со стандартным вводом. Смысл опций таков:

-a

Выдать значение всех установленных характеристик.

-g

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

В частности, выдача команды

stty -a

может выглядеть так, как показано в пример 9.4. Здесь присутствуют как стандартные, так и специфичные для ОС Linux характеристики (минус перед именем характеристики означает, разумеется, что соответствующий флаг не установлен). Например, для опрашиваемого терминала задан канонический режим ввода и постпроцессирование вывода, но отсутствует контроль четности.
Можно видеть также, какие символы назначены на роли специальных управляющих.

Листинг 9.4. Возможный результат команды stty -a. (html, txt)

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

stty kill '^k'

для отмены строки придется нажимать CTRL+K. Вслед за выполнением команды

stty -echo

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

stty echo

Сохранение и восстановление характеристик терминала можно реализовать так, как показано в пример 9.5.

saved="$(stty -g)" stty новые_характеристики . . . stty $saved

Листинг 9.5. Пример сохранения и восстановления характеристик терминала. (html, txt)