UNIX с точки зрения программиста СОДЕРЖАНИЕ 1. Введение 3 2. Ввод/вывод 4 2.1. Создание и удаление файлов 5 2.2. Информация о состоянии файла, изменение его атрибутов 9 2.3. Открытие и закрытие файла 11 2.4. Чтение и запись данных 14 2.5. Блокировки 18 2.6. Пакет stdio(3S) 21 3. Процессы 22 3.1. Порождение процессов 22 3.2. Взаимодействие параллельных процессов 24 3.3. Каналы 24 3.4. Сигналы 26 3.5. Очереди сообщений 27 3.5.1. Создание очередей сообщений 28 3.5.2. Отправление и получение сообщений 32 3.5.3. Управление очередями сообщений 34 3.5.4. Пример: сеть DMFS 36 3.6. Семафоры 36 3.6.1. Создание множества семафоров 37 3.6.2. Операции над множествами семафоров 38 3.6.3. Управление множествами семафоров 39 3.6.4. Пример: обед философов 41 3.7. Разделяемые сегменты памяти 44 3.7.1. Создание разделяемых сегментов памяти 44 3.7.2. Присоединение и отсоединение сегментов 45 3.7.3. Управление разделяемыми сегментами памяти 46 3.7.4. Пример: критическая область 46 3.8. Резюме 47 4. Пакет управления терминалом curses/terminfo 48 4.1. Введение 48 4.2. Использование подпрограмм пакета curses 49 4.2.1. Что нужно программе для работы с curses 49 4.2.2. Компиляция программы, которая использует curses 51 4.2.3. Вывод 51 4.2.4. Опции ввода 55 4.2.5. Ввод 55 4.2.6. Атрибуты вывода 58 4.2.7. Работа с окнами 59 4.2.8. Работа с несколькими терминалами сразу 62 4.3. Использование подпрограмм пакета terminfo 64 4.4. Использование базы данных terminfo 65 5. Утилита make 70 5.1. Основные возможности 70 5.2. Структура make-файлов 72 5.3. Макросы 72 5.4. Суффиксы и правила трансформации 74 5.5. Пример: библиотека libio 75 5.6. Запуск утилиты make 76 5.7. Резюме 78 Приложение A. Системные вызовы и библиотечные функции ОС UNIX 79 Приложение B. Сводка синтаксиса системных вызовов и библиотечных функций 82 1. ВВЕДЕНИЕ Данная публикация является второй частью методического материа- ла для пользователей рабочей станции БЕСТА. Цель публикации - сообщить начальные сведения о средствах программирования, пре- доставляемых операционной системой UNIX для решения следующих задач: Организация ввода/вывода. Управление параллельными процессами и взаимодействие процессов. Управление терминалом. При описании перечисленных средств показывается, как воспользо- ваться ими в программах, написанных на языке C. Поэтому предпо- лагается, что пользователь, хотя бы в общих чертах, знаком с этим языком. Кроме того, в описании используются основные поня- тия системы UNIX, введенные в первой части методического мате- риала. Методический материал содержит также описание утилиты make. 2. ВВОД/ВЫВОД Ввод и вывод данных и доступ к файлам из программ пользователей в системе UNIX можно организовать, используя либо несколько системных вызовов, работающих с файлами, либо стандартный пакет ввода/вывода языка C, состоящий из целого множества библиотеч ных функций и макросов. Системные вызовы - это запросы программы к ядру операционной системы на выполнение некоторых действий. Библиотечные функции - это заранее запрограммированные модули, реализующие те или иные стандартные возможности. Внешне обращения к системным вы- зовам и библиотечным функциям ничем не отличаются и выглядят как вызовы обычных функций языка. Отличия обусловлены тем, что коды, реализующие системные вызовы, находятся в ядре ОС, а при их выполнении происходит переключение из адресного пространства вызывающего процесса к пространству ядра. Системные вызовы, предназначенные для организации ввода/вывода, позволяют прикладной программе: Создавать и удалять файлы. Получать информацию о состоянии файлов. Открывать и закрывать файлы, используемые программой. Передавать данные из файла в программу (чтение) и из программы в файл (запись). Выполнять управляющие операции над открытыми файлами. Нижний уровень программных средств ввода/вывода реализуют сис- темные вызовы, которые обсуждаются в последующих разделах. Скажем несколько слов о модели файлов, принятой в ОС UNIX (на помним, что архитектура файловой системы рассматривалась в пер- вой части данного руководства). Обычный файл данных трактуется как непрерывный сегмент байт пе- ременной длины. Доступ к данным осуществляется при помощи опе- раций чтения/записи. С сегментом связан указатель текущей пози- ции, управление которым зависит от специфики устройства, содер- жащиего файл. Если устройство допускает позиционирование, опе- рация чтения позволяет ввести, начиная с любой позиции, после- довательность лежащих друг за другом байт, операция записи поз- воляет произвольную непрерывную последовательность байт обно- вить. Если устройство не допускает позиционирования, к лежащей в файле информации доступ возможен, только начиная с текущей позиции. (На самом деле непрерывный сегмент байт переменной длины реализуется ядром системы UNIX сложной структурой данных, основанной на связанных блоках фиксированного размера; однако обычному программисту, для которого предназначен методический материал, этот нижний уровень ядра недоступен.) Файл-каталог состоит из последовательности записей, характери- зующих файлы, принадлежащие этому каталогу. На уровне хранения он представляется такой же структурой, что и обычный файл (фор- мат каталога описан в dir(2)). Чтение данных из каталога может быть вызвано желанием получить информацию об элементах катало га; следует однако отметить, что во многих случаях с той же целью можно воспользоваться и другими, более специальными вызо- вами. Поскольку запись в каталог - это составная часть недели- мого действия над файлом-элементом каталога, запись в каталог запрещена. Характерной чертой UNIXа является возможность обмениваться ин формацией с внешними устройствами по тем же правилам, что и с текстовыми файлами. Чтобы получить доступ к внешнему устройству (например, к терминалу), достаточно для соответствующего ему специального файла (такого, как /dev/console - системный терми нал) воспользоваться обычными системными вызовами. Особенности функционирования конкретных устройств учитываются при этом со- ответствующими драйверами. Среди атрибутов, связанных с каждым файлом, - идентификаторы владельца и группы владельца файла, а также режим доступа к файлу. Эти атрибуты определяют права на выполнение операций над данным файлом различными процессами. 2.1. Создание и удаление файлов Для создания новых обычных файлов используется системный вызов creat(2): |int creat (path, mode) |char *path; |int mode; Функция creat имеет два аргумента: первый - указатель на цепоч- ку символов, задающую маршрутное имя создаваемого файла; второй - целое число, определяющее режим доступа к файлу. Режим доступа к файлу удобно задавать восьмеричным трехзначным числом, например: |0644 Здесь старшие три бита задают права доступа к файлу для процес- са, идентификатор которого совпадает с идентификатором владель- ца (соответственно, право на чтение, запись и выполнение), средние три бита - права членов группы, а младшие три бита - права всех остальных. Указанное значение режима доступа задает права на чтение и запись для владельца файла и право на чтение для всех остальных. Полным правам на выполнение всех операций соответствует значение 0777; если режим доступа к файлу равен 0, операции над ним может выполнять только суперпользователь. Каждому процессу соответствует таблица открытых файлов; создан- ный файл открывается, а информация о нем заносится в эту табли- цу. Размеры таблицы определяются системным параметром NOFILES, поэтому процесс не может иметь открытыми одновременно более чем NOFILES файлов. Возвращаемое системным вызовом creat небольшое целое число, называемое дескриптором файла, является индексом в таблице открытых файлов и принимает значения от 0 до (NOFILES - 1). Дескриптор используется в операциях ввода/вывода для иден- тификации открытых файлов. Следующий фрагмент может быть использован для создания в теку- щем каталоге файла с именем example и режимом доступа 0755 (при этом в переменную df в случае успешного завершения системного вызова будет занесен соответствующий дескриптор): |int df; | . . . |df = creat ("example", 0755); Иногда системный вызов может завершиться неудачей, например, если в таблице процесса не осталось свободных дескрипторов фай- лов. В таком случае creat возвращает в качестве результата -1, а внешней переменной errno присваивается код ошибки, позволяю- щий определить причину ее возникновения. Переменная errno, а также мнемоники для кодов ошибок определены в стандартном вклю чаемом файле . Для того, чтобы сформировать системное сообщение об ошибке, можно воспользоваться библиотечной функци- ей perror(3C), которая помещает в стандартный протокол описание последней ошибки. Например, в результате выполнения программы |#include | |main () |{ | int n = 1; | char name [8]; | | do | sprintf (name, "file%d", n++); | while (creat (name, 0777) >= 0); | perror ("CREAT failed"); | printf ("errno = %d\n", errno); |} будет выдано такое сообщение: |CREAT failed: Too many open files |errno = 24 (При этом в текущем каталоге будут созданы файлы file1, file2, ...) Подчеркнем, что подобный стиль уведомления о неудачном заверше- нии является общим для большинства системных вызовов и библио- течных функций. Неудача определяется возвращением результата, невозможного в другом случае, - почти всегда это -1 или пустой указатель NULL; код ошибки заносится в переменную errno. Файл необязательно создавать в текущем каталоге; в качестве ар- гумента creat может быть передано полное маршрутное имя, напри- мер: |df = creat ("/usr/khristov/sample", 0755); Атрибуты создаваемого файла определяются следующим образом: идентификаторы владельца и группы файла устанавливаются равны- ми, соответственно, идентификаторам пользователя и группы про- цесса; режим доступа файла определяется по аргументу mode. При этом учитывается так называемая маска режима создания файлов - характеристика, связанная с каждым процессом. Биты аргумента mode, соответствующие единичным битам маски, обнуляются. Значе- ние маски можно опросить и изменить при помощи системного вызо- ва umask(2); вероятнее всего это значение равно 022, что соот- ветствует маскированию права записи для всех пользователей кроме владельца файла. Выполним, например, следующую программу: |main () |{ | int mask; | | if (creat ("myfile1", 0777) < 0) perror ("CREAT"); | mask = umask (0777); | if (creat ("myfile2", 0777) < 0) perror ("CREAT"); | mask = umask (0); | if (creat ("myfile3", 0777) < 0) perror ("CREAT"); |} Команда |ls -l myfile? приведет к выдаче сообщения, подобного |-rwxr-xr-x 1 khristov sys 0 Oct 17 17:10 myfile1 |---------- 1 khristov sys 0 Oct 17 17:10 myfile2 |-rwxrwxrwx 1 khristov sys 0 Oct 17 17:10 myfile3 Если файл, который пытаются создать при помощи creat, уже су- ществует, он опустошается (размер становится равным 0), а режим доступа и владелец не изменяются. Перечислим несколько условий, которые могут привести к неудач- ному завершению вызова creat: компонент маршрутного имени не существует или не является каталогом; для компонента маршрута отсутствует право на поиск; создание файла требует записи в ка- талог, права на запись в который нет; файл существует и являет- ся каталогом. Последнее условие вызвано тем, что при создании каталога требу- ется выполнить некоторые дополнительные действия по сравнению с созданием обычного файла. В частности, во вновь созданный ката- лог сразу заносятся элементы "." и "..". Для создания каталогов используется системный вызов mkdir(2): |int mkdir (path, mode) |char *path; |int mode; Смысл аргументов и правила, по которым определяются атрибуты создаваемого каталога, - такие же, что и для вызова creat. При успешном завершении результат mkdir равен 0. Если указанный каталог уже существует, системный вызов завершится неудачей. Все перечисленные выше условия неудачного завершения creat, кроме последнего, применимы и для mkdir. Чтобы удалить обычный файл, надо воспользоваться системным вы- зовом unlink(2): |int unlink (path) |char *path; Более точно, системный вызов unlink удаляет элемент каталога, заданный маршрутным именем, на которое указывает аргумент path. Реальное удаление происходит, когда все ссылки на файл удалены и нет процесса, для которого этот файл открыт. Занимаемое фай- лом пространство освобождается и файл перестает существовать. Если же при удалении последней ссылки файл открыт одним или несколькими процессами, уничтожение файла откладывается до зак- рытия его всеми этими процессами. При успешном завершении ре- зультат unlink равен 0. Попытка удалить ссылку на каталог при помощи unlink приводит к неудаче (результат равен -1). В данном случае надо воспользо- ваться системным вызовом rmdir(2): |int rmdir (path) |char *path; который удаляет каталог с маршрутным именем path. При этом ка- талог не должен содержать элементов, отличных от "." и "..". 2.2. Информация о состоянии файла, изменение его атрибутов Для получения информации о файле используются системные вызовы группы stat(2): |int stat (path, buf) |char *path; |struct stat *buf; | |int fstat (fd, buf) |int fd; |struct stat *buf; Системный вызов stat предоставляет информацию о поименованном файле: аргумент path указывает на маршрутное имя файла. Чтобы получить информацию, достаточно иметь право на поиск во всех каталогах, входящих в маршрутное имя. Системный вызов fstat предоставляет информацию об открытом файле, задаваемом дескрип- тором файла fd, который может быть результатом creat (или како- го-либо другого системного вызова, возвращающего дескриптор открытого файла). В обоих случаях аргумент buf является указателем на стуктуру типа stat, в которую помещается информация о файле. Данная структура определена в стандартном включаемом файле , поэтому в программу, использующую вызовы stat или fstat, следует включать строки |#include |#include (Во включаемом файле вводятся базовые системные типы данных, участвующие в определении struct stat.) После выполнения системного вызова структура, на которую указы- вает buf, будет содержать, в частности, следующую информацию: режим доступа к файлу, идентификатор владельца и группы вла- дельца файла, размер файла, времена последнего доступа к дан- ным, последней модификации данных, последнего изменения статуса файла. Вставим в программу, создающую файлы myfile1, myfile2, myfile3, обращения к fstat: |#include |#include | |main () |{ | int df, mask; | struct stat stst; | if ((df = creat ("myfile1", 0777)) < 0) | perror ("CREAT"); | (void) fstat (df, &stst); | printf ("myfile1 mode: %o\n", 0777 & stst.st_mode); | | mask = umask (0777); | | if ((df = creat ("myfile2", 0777)) < 0) | perror ("CREAT"); | (void) fstat (df,&stst); | printf ("myfile2 mode: %o\n", 0777 & stst.st_mode); | | mask = umask (0); | | if ((df = creat ("myfile3", 0777)) < 0) | perror ("CREAT"); | (void) fstat (df, &stst); | printf ("myfile3 mode: %o\n", 0777 & stst.st_mode); |} Выполняясь, эта программа может выдать такие сообщения: |myfile1 mode: 755 |myfile2 mode: 0 |myfile3 mode: 777 Для изменения атрибутов существующего файла служат системные вызовы chmod(2) (изменение режима доступа к файлу) и chown(2) (изменение владельца и группы владельца файла). 2.3. Открытие и закрытие файла Для того, чтобы подготовить файл к выполнению над ним операций чтения или записи, предназначен системный вызов open(2): |int open (path, oflag [, mode]) |char *path; |int oflag, mode; Аргумент path является указателем на маршрутное имя файла. При успешном завершении вызов open возвращает дескриптор для ука- занного файла и устанавливает флаги статуса файла в соответст- вии со значением аргумента oflag. Значение oflag задается как побитное ИЛИ флагов, которые описываются ниже (мнемонические имена флагов определены в стандартном включаемом файле ). Прежде всего, в комбинации флагов должен быть указан один из трех следующих: O_RDONLY - открыть файл только на чтение, O_W- RONLY - только на запись, O_RDWR - на чтение и запись: |#include | ... |if ((df = open ("myfile2", O_WRONLY)) < 0) ... Если для указанного файла нет прав на выполнение операций, за- даваемых значением oflag, вызов open завершается неудачей. В приведенном фрагменте к неудаче (результат равен -1) может при- вести отсутствие права на запись в файл: |---------- 1 khristov sys 0 Oct 17 17:10 myfile2 Надо что-то сделать с правами, например, при помощи системного вызова chmod: |(void) chmod ("myfile2", 0777); Для обычных файлов предусмотрено еще несколько флагов. Обычно при открытии файла указатель текущей позиции устанавли- вается на начало файла и может произвольным образом изменяться в результате последующих операций чтения/записи данных (если только мы не имеем дело с устройством последовательного досту па). Однако в некоторых случаях бывает полезно оговорить, что выводимые данные надо добавлять к уже имеющемуся содержимому файла, дописывая их в конец. Это позволяет сделать флаг O_AP- PEND - если он установлен, то перед каждой операцией записи указатель текущей позиции помещается в конец файла: |df = open ("myfile2", O_WRONLY | O_APPEND); Опустошить файл (т.е. сделать его размер равным 0), позволяет флаг O_TRUNC: |df = open ("myfile2", O_WRONLY | O_APPEND | O_TRUNC); Вызов open позволяет при необходимости создавать открываемый файл. Для этого надо установить флаг статуса O_CREAT. Его ис- пользование предотвратит неудачное завершение open в ситуации, когда файл не существует. Если файл существует, флаг игнориру- ется. Если не существует - создается, причем режим доступа к нему определяется с учетом необязательного в остальных случаях аргумента mode. Все атрибуты файла устанавливаются по тем же правилам, что и при создании файла посредством creat. Поскольку вызов creat опустошает уже существующий файл и открывает его на запись, приведенные ниже операторы абсолютно эквивалентны: |df = creat ("myfile", 0777); |df = open ("myfile", O_WRONLY | O_TRUNC | O_CREAT, 0777); Отметим, что в случае, если myfile уже существует, а право на запись в него отсутствует, оба этих вызова завершаются неудачей. Если Вам нужна гарантия, что открываемый файл создается заново, в дополнение к флагу O_CREAT установите O_EXCL: такая попытка открыть уже существующий файл приведет к неудачному завершению вызова open: |df = open ("myfile", O_WRONLY | O_CREAT | O_EXCL, 0777); При решении некоторых задач требуется, чтобы программа, выпол няющая операцию записи в файл, приостанавливалась до тех пор пока не будет завершено физическое обновление данных (обычно вывод буферизуется). Для этого предусмотрен флаг O_SYNC, воз- действующий на последующие операции записи. Его использование делает программу более надежной, правда, за счет скорости вы- полнения. Действие еще одного флага, O_NDELAY, будет рассмотрено далее при обсуждении механизма блокировок. Значения флагов статуса файла, установленные при его создании или открытии, можно опросить и изменить. Чтобы сделать это, следует воспользоваться вызовом fcntl(2), который предназначен для выполнения разнообразных управляющих операций над открытым файлом: |int fcntl (fd, cmd, arg) |int fd, cmd, arg; Параметр fd задает дескриптор открытого файла; параметр cmd со- держит управляющую команду (ее дополнительные аргументы могут быть переданы через параметр arg). Для работы с флагами статуса файла предусмотрены команды F_GETFL (получить флаги статуса) и F_SETFL (установить флаги статуса равными значению arg). При выполнении команды F_GETFL результат, возвращаемый fcntl, равен значению флагов статуса. Управляющие команды, также, как и име- на флагов, определены в стандартном включаемом файле . Пример: |#include |int flags; | ... |flags = fcntl (fd, F_GETFL, 0); |(void) fcntl (fd, F_SETFL, flags | O_SYNC); С помощью fcntl не могут быть переустановлены флаги O_RDONLY O_WRONLY, O_RDWR, O_CREAT, O_EXCL, O_TRUNC. Чтобы изменить зна- чение этих флагов, Вам придется закрыть файл и открыть его за- ново с иным набором флагов: |fd = creat ("myfile", O_WRONLY); |... |(void) close (fd); |fd = open ("myfile", O_RDONLY); Закрыть файл и освободить соответствующий ему дескриптор позво- ляет системный вызов close(2): |int close (fd) |int fd; Аргумент fd - это дескриптор файла, полученный в результате вы- полнения таких вызовов, как creat или open. Приведенная ниже подпрограмма использует вызовы open и close для создания файла-замка (т.е. файла, наличие либо отсутствие которого проверяется другими компонентами программы; содержа- тельная информация в нем не хранится): |#include |int gate () |{ | int df; | df = open ("gate", O_WRONLY | O_CREAT, 0); | if (df < 0) return (-1); | if (close (df) < 0) return (-1); | return (0); |} 2.4. Чтение и запись данных Чтение данных из файла выполняет системный вызов read(2): |int read (fd, buf, nbyte) |int fd; |char buf [ ]; |unsigned nbyte; Вызов read пытается прочитать nbyte байт из файла, ассоцииро- ванного с дескриптором fd, в буфер buf. Для устройств, допускающих позиционирование, read выполняет чтение из файла, начиная с указателя текущей позиции, ассоции- рованного с дескриптором fd. После завершения операции указа- тель текущей позиции файла увеличивается на количество прочи- танных байт. Для устройств же последовательного доступа значе- ние указателя текущей позиции файла неопределено. Поэтому чте- ние для такого устройства всегда выполняется с текущей позиции. При успешном завершении read возвращает количество байт, реаль- но прочитанных и помещенных в буфер; это значение может ока- заться меньше значения аргумента nbyte, если в файле осталось меньше, чем nbyte байт. Например, если текущая позиция совпада- ла с концом файла, результат будет равен 0. В случае ошибки возвращается -1. Следующая программа читает недлинные сообщения с текущего тер- минала (/dev/tty - специальный файл, ассоциированный с текущим терминалом, т.е. с терминалом, с которого запущена программа): |#include |#define MAXSIZE 128 |main () |{ | int df, err; | char buf [MAXSIZE]; | /* Открытие на чтение специального файла, | ассоциированного с текущим терминалом */ | df = open ("/dev/tty", O_RDONLY); | if (df < 0) { | perror ("OPEN"); | exit (1); | } | /* Ввод с терминала */ | err = read (df, buf, MAXSIZE); | if (err < 0) { | perror ("READ"); | exit (1); | } | buf [err] = '\0'; | printf ("Вы ввели: %s\n", buf); | (void) close (df); | exit (0); |} Запись данных в файл выполняет системный вызов write(2): |int write (fd, buf, nbyte) |int fd; |char buf [ ]; |unsigned nbyte; Вызов write пытается записать nbyte байт из буфера buf, в файл, ассоциированный с дескриптором fd. Также, как и при выполнении read, начальная позиция определяет- ся либо значением указателя текущей позиции файла для устройств произвольного доступа, либо текущей позицией для устройства последовательного доступа. После завершения записи указатель текущей позиции файла увеличивается на количество записанных байт. При попытке записать большее число байт, чем позволяет макси- мальный размер файла [см. ulimit(2)], записывается столько байт, сколько возможно. Например, пусть в файле осталось 20 байт до достижения максимального размера. Тогда попытка записи в него большего числа байт приводит к тому, что реально пишется 20 байт и результат равен 20. Последующая попытка записи нену- левого числа байт приводит к ошибке; в случае неудачного завер- шения возвращается -1. Следующая программа выводит приветствие на текущий терминал: |#include |char msg [] = "HELLO !!!\n"; |main () |{ | int df, err; | /* Открытие на запись специального файла, | ассоциированного с текущим терминалом */ | df = open ("/dev/tty", O_WRONLY); | if (df < 0) { | perror ("OPEN"); | exit (1); | } | /* Вывод на терминал */ | err = write (df, msg, strlen (msg)); | if (err < 0) { | perror ("WRITE"); | exit (1); | } | (void) close (df); | exit (0); |} Программа prnmyself выводит свой собственный исходный текст на терминал. При этом используется следующий прием: данные фикси- рованными порциями читаются из файла и выводятся на терминал; процесс повторяется до тех пор, пока число реально прочитанных байт совпадает с указанным (т.е. пока не обнаружен конец фай- ла). |#define SIZE 16 |main () |{ | int dfr, dfw; | int err; | int nb; | char buf [SIZE]; | dfr = open ("prnmyself.c", O_RDONLY); | dfw = open ("/dev/tty", O_WRONLY); | if (dfr < 0 || dfw < 0) { | perror ("OPEN"); exit (1); | } | do { | err = nb = read (dfr, buf, SIZE); | if (err < 0) { | perror ("READ"); exit (1); | } | err = write (dfw, buf, nb); | if (err < 0) { | perror ("WRITE"); exit (1); | } | } while (nb == SIZE); | (void) close (dfw); | (void) close (dfr); |} Если устройство, содержащее файл, допускает позиционирование, указатель текущей позиции в файле может быть передвинут при по- мощи вызова lseek(2): |long lseek (fd, offset, whence) |int fd; |long offset; |int whence; Аргумент fd, как обычно, задает дескриптор файла. Новое значе- ние указателя текущей позиции вычисляется в зависимости от зна- чения аргумента whence: если whence равно 0, значение указателя устанавливается равным offset; если 1, к текущему значению ука- зателя прибавляется значение offset; если 2, значением указате- ля становится размер файла плюс offset. При успешном завершении lseek возвращает новое значение указа- теля; в случае ошибки результат равен -1. Для файлов, ассоции- рованных с устройствами, которые не поддерживают передвижение указателя текущей позиции, значение указателя не определено. Приведем несколько примеров. Установка указателя текущей пози- ции в начало файла: |(void) lseek (fd, 0, 0); в конец: |(void) lseek (fd, 0, 2); Приращение указателя: |(void) lseek (fd, inc, 1); 2.5. Блокировки Механизм блокировок используется для ограничения доступа к обычным файлам. Назначение этого механизма - дать возможность программам, одновременно обрабатывающим одни и те же данные, синхронизировать свою работу. Блокировка связывается с сегмен- том файла (произвольным последовательным участком файла). Раз личают блокировку на запись и на чтение. Блокировка за запись используется для получения монопольного доступа к сегменту фай- ла. Когда сегмент блокируется на запись, никакие другие процес- сы не имеют возможности заблокировать на чтение или запись этот же сегмент или сегмент, пересекающийся с ним. Пока процесс бло кирует сегмент на запись, можно гарантировать, что никакие дру- гие процессы не будут модифицировать или читать данные из этого сегмента. Блокировка на чтение используется для того, чтобы ог- раничить доступ к сегментам. Такой способ блокировки позволяет нескольким процессам читать один и тот же сегмент одновременно: когда сегмент заблокирован на чтение, другие процессы также мо- гут заблокировать на чтение весь сегмент или его часть, однако никакие сегменты, заблокированные на запись, с ним пересекаться не могут. Для установки блокировки на чтение требуется, чтобы файл был открыт как минимум на чтение. Соответственно, для блокировки на запись необходим доступ на запись. Кроме того, при выполнении операций ввода/вывода блокировки учитываются только в том слу- чае, если установлен соответствующий флаг. Флаг учета блокиров- ки задается таким режимом доступа к файлу, в котором взведен бит переустановки идентификатора группы (бит 02000) и отсутст- вует право на выполнение членами группами (бит 00010). Установить флаг учета блокировки можно, например, так: |#include |#include | |int mode; |struct stat buf; | . . . |if (stat ("myfile", &buf) < 0) { | perror ("STAT"); | exit (1); |} |/* Получение текущего режима доступа */ |mode = buf.st_mode; |/* Удаление разрешения на выполнение членами группы */ |mode &= ~010; |/* Взведение бита переустановки идентификатора группы */ |mode |= 02000; |if (chmod ("myfile", mode) < 0) { | perror ("CHMOD"); | exit (1); |} | . . . Если не указано противное, при попытке чтения из файла с уста- новленным флагом учета блокировки и при наличии блокировки на запись (другим процессом) того сегмента файла, который должен быть прочитан, читающий процесс откладывается до снятия блоки- ровки. Аналогично, если установлен флаг учета блокировки и тот сегмент файла, в который производится попытка записи, заблоки- рован другим процессом, записывающий процесс откладывается до снятия блокировки сегмента. Предусмотрен и другой режим: если установлен (например, при открытии файла) флаг статуса O_NDELAY и выполнению операции чтения (записи) препятствует соответствующая блокировка, сис- темный вызов read (write) завершается неудачей и возвращает значение -1. Для того, чтобы установить блокировку, надо воспользоваться вы- зовом fcntl. Блокировка сегмента описывается следующей структу- рой (ее определение содержится в файле ): |struct flock { | short l_type; | short l_whence; | long l_start; | long l_len; | short l_sysid; | short l_pid; |}; Структура struct flock содержит поля, определяющие для сегмента файла тип блокировки (l_type), смещение (l_start), размер (l_len), идентификатор процесса (l_pid). Поле l_whence задает тип смещения l_start (0 - абсолютное, от начала файла; 1 - от- носительно указателя текущей позиции). Поле l_sysid использует- ся только в случае распределенной файловой системы. Начало конец блокируемой области могут выходить за конец файла. Можно определить блокировку, всегда действующую до конца файла: зна чение поля l_len полагается равным 0. Значение поля l_type, равное F_RDLCK, соответствует блокировке на чтение, а значение F_WRLCK - блокировке на запись. Несколько примеров: |#include |struct flock lck; |lck.l_type = F_RDLCK; /* Блокировка на чтение */ |lck.l_whence = 0; /* всего файла */ |lck.l_start = 0; |lck.l_len = 0; |lck.l_type = F_WRLCK; /* Блокировка на запись */ |lck.l_whence = 0; /* сегмента файла */ |lck.l_start = pos; |lck.l_len = sizeof (struct rcrd); Установка блокировки осуществляется управляющими командами F_SETLK и F_SETLKW системного вызова fcntl: |if (fcntl (fd, F_SETLK, &lck) == -1) ... |(void) fcntl (fd, F_SETLKW, &lck); Если блокировка не может быть установлена, выполнение команды F_SETLK немедленно завершается и возвращает -1. Команда F_SETLKW отличается от предыдущей тем, что в аналогичной ситуа- ции процесс переходит в состояние ожидания до тех пор, пока нужный сегмент файла не будет разблокирован. Чтобы снять блокировку, также можно воспользоваться командами F_SETLK или F_SETLKW. Для этого значение поля l_type должно быть установлено равным F_UNLCK. Получить характеристики блокировки, мешающей установить новую блокировку, позволяет управляющая команда F_GETLK (новая блоки- ровка задается структурой, на которую при обращении к fcntl указывает аргумент arg). Если запрашиваемую блокировку устано- вить нельзя, информация о первой мешающей этому блокировке по- мещается в ту же структуру; в частности, будет задано значение поля l_pid, идентифицирующее процесс, который установил блоки- ровку. Если нет помех для создания нужной блокировки, полю l_type присваивается значение F_UNLCK, а остальные поля в структуре не изменяются. Заметим, что если на один сегмент ус- тановлено несколько блокировок, то найдена будет только одна из них - первая. Приведенный ниже фрагмент получает и выводит информацию о сег- ментах файла, заблокированных на запись: |#include |struct flock lck; | |(void) printf ("ид-р проц. тип начало длина\n"); |lck.l_whence = 0; |lck.l_start = 0; |lck.l_len = 0; |do { | lck.l_type = F_WRLCK; | (void) fcntl (fd, F_GETLK, &lck); | if (lck.l_type != F_UNLCK) { | (void) printf ("%9d %c %6d %5d\n", | lck.l_pid, | (lck.l_type == F_WRLCK) ? 'W' : 'R', | lck.l_start, lck.l_len); | /* Если эта блокировка покрывает остаток файла, | нет нужды выявлять другие блокировки */ | if (lck.l_len == 0) break; | /* Иначе поищем новую блокировку после найденной */ | lck.l_start += lck.l_len; | } |} while (lck.l_type != F_UNLCK); 2.6. Пакет stdio(3S) Системные вызовы реализуют нижний уровень ввода/вывода. Во мно- гих случаях предпочтительнее использование более высокоуровне- вых библиотечных функций из стандартного пакета stdio(3S), ко- торый предоставляет средства эффективного буферизованного вво- да/вывода; программы, использующие стандартный пакет, как пра- вило, более мобильны. 3. ПРОЦЕССЫ Одно из основных понятий ОС UNIX - понятие процесса. Процессом называется программа, находящаяся в стадии выполнения. Харак- терной особенностью UNIXа является то, что одни процессы могут динамически порождаться другими. Каждый активный процесс в системе имеет уникальный номер, назы- ваемый идентификатором процесса (pid). Кроме того, процессу со- ответствует идентификатор родительского процесса (ppid). Значе- ние идентификатора процесса лежит в пределах от 0 до 30000. Среди других атрибутов процесса - окружение, набор открытых файлов, текущий и корневой каталоги, а также идентификаторы пользователя и группы, которые используются для определения прав доступа к файлам. UNIX предоставляет программисту широкий спектр средств межпро- цессной связи: каналы, сигналы, очереди сообщений, семафоры, разделяемые сегменты памяти. В данной главе описываются системные вызовы, которые могут ис пользоваться в пользовательских программах для порождения и ор ганизации взаимодействия параллельных процессов. 3.1. Порождение процессов Новые процессы создаются при помощи системного вызова fork, имеющего следующий синтаксис: |int fork ( ) Вызов fork приводит к созданию нового (порожденного) процесса точной копии процесса, сделавшего вызов (родительского). Порож денный процесс наследует у родительского большинство его атри бутов. К ним относятся открытые файлы, переменные окружения, текущий каталог, идентификаторы пользователя и группы и т.д. Единственное существенное отличие между родительским и порож- денным процессами заключается в том, что каждый из них имеет свой уникальный идентификатор (и, естественно, различные иден- тификаторы родительского процесса). В случае успешного заверше ния fork возвращает порожденному процессу 0, а родительскому процессу - идентификатор порожденного процесса (в случае неуда- чи возвращается -1). Поскольку возвращаемые fork'ом значения различны для обеих ко пий, родительский и порожденный процессы могут далее выполнять- ся по-разному. Например, процесс-предок переходит в состояние ожидания завершения процесса-потомка (выдав системный вызов wa- it(2)), либо, если процесс-потомок запущен асинхронно, продол- жает выполняться одновременно с ним. Процесс-потомок при помощи системного вызова из группы exec(2) (execl, execv, execle, execve, execlp, execvp) подменяет программу, которая определяет поведение процесса, и передает ей управление и список аргумен- тов. Напомним, что функция main C-программы выглядит в общем случае следующим образом: |main (argc, argv, envp) |int argc; |char **argv, **envp; где argc равен количеству аргументов, argv - массив указателей собственно на аргументы и envp - массив указателей на цепочки символов, образующие окружение. Параметры argc, argv и envp оп- ределяются исходя из командной строки, запускающей C-программу. Принято соглашение, по которому значение argc не меньше 1, а первый элемент массива argv указывает на цепочку символов, со- держащую имя нового выполняемого файла. Аналогичный смысл имеют формальные параметры системных вызовов группы exec. Опишем подробнее синтаксис одного из них: |int execle (path, arg0, arg1, ..., argn, (char*) 0, envp) |char *path, *arg0, *arg1, ..., *argn, *envp []; Аргумент path указывает на маршрутное имя нового выполняемого файла. Аргументы arg0, arg1, ..., argn - это указатели на це- почки символов, ограниченные нулевыми байтами. Эти цепочки об- разуют доступный новому процессу список аргументов. По соглаше- нию, arg0 должен присутствовать и указывать на цепочку симво- лов, равную path (или последнему компоненту path). Массив envp содержит указатели на цепочки символов, ограниченные нулевыми байтами. Эти цепочки образуют окружение нового процесса. За последним занятым элементом массива envp должен следовать пус- той указатель. Если системный вызов exec закончился успешно, то он не может вернуть управление, так как вызвавшая программа уже заменена новой программой. Возврат из системного вызова exec свидетель- ствует об ошибке. В таком случае результат равен -1. Заканчивая выполнение, процесс-потомок издает системный вызов exit, который терминирует обратившийся к нему процесс. Если процесс-предок находится в состоянии вызова wait, то wait за- вершается, выдавая процессу-предку в качестве результата иден- тификатор терминировавшегося процесса и код его завершения. Процесс-предок при этом осуществляет некоторые завершающие дей- ствия и продолжает выполнение своей программы. Выполняющая описанные в этом разделе действия программа может иметь следующий вид: |int stat, pid; |char *arg1, *arg2; | if ((pid = fork ()) < 0) { | /* Вызов fork завершился неудачей ... */ | } | else if (pid == 0) { | /* Порожденный процесс */ | (void) execl ("/bin/prog", "prog", arg1, (char *) 0); | exit (2); /* execl () завершился неудачей */ | } | else | /* Родительский процесс */ | (void) wait (&stat); Упомянем также библиотечную функцию system(3S), позволяющую в пользовательской программе выполнить команду shell'а: |int system (string) |char *string; Аргумент string трактуется shell'ом как командная строка и мо- жет содержать имя и аргументы любой выполняемой программы или стандартной команды ОС UNIX. При обращении к system вызвавшая программа ожидает завершения выполнения переданной команды, а затем продолжает выполнение со следующего выполняемого операто- ра. Возвращаемое функцией system значение - код завершения shell'а. Пример: |code = system ("cd /usr/bin; ls > lst"); 3.2. Взаимодействие параллельных процессов В предыдущем разделе рассмотрены программные средства ОС UNIX, позволяющие динамически порождать процессы. В практических при- ложениях обычно требуется организовать достаточно сложное взаи- модействие параллельных процессов, заключающееся в обмене дан- ными, синхронизации выполнения, уведомлении других процессов о произошедших исключительных ситуациях и т.д. Механизмы межпро- цессной связи, предоставляемые UNIXом, описываются в последую- щих разделах. 3.3. Каналы Как говорилось при обсуждении UNIXа с точки зрения пользовате- ля, при работе в рамках shell'а часто используются конвейеры, т.е. выполнение программ организуется таким образом, что вывод одной программы является вводом другой. Аналогичная возможность предоставляется программисту: между двумя взаимодействующими процессами для передачи данных может быть создан канал, т.е. файл особого типа, в который один из процессов будет писать данные, а другой - их читать. Для созда- ния канала используется системный вызов pipe(2): |int pipe (fd) |int fd [2]; Вызов pipe возвращает два дескриптора файла fd[0] и fd[1]. Дескриптор fd[0] открыт на чтение, fd[1] - на запись. Емкость канала ограничена, он буферизует до 5120 байт данных. Взаимодействие между процессами через канал может быть установ лено следующим образом: один из процессов создает канал и сооб щает другому значение соответствующего дескриптора. После этого процессы обмениваются данными через канал при помощи обычных вызовов read и write. Пример: |char msg [] = "Hello !!!\n"; | |main () |{ | int pid, status; | int fd [2]; | char buf [16]; | /* Создание канала */ | if (pipe (fd) < 0) { | perror ("PIPE"); | exit (1); | } | switch (pid = fork ()) { | case -1: | perror ("FORK"); | exit (1); | case 0: /* Чтение из канала */ | (void) read (fd [0], buf, sizeof (buf)); | printf ("msg = %s\n", buf); | exit (0); | } /* Запись в канал */ | (void) write (fd [1], msg, sizeof (msg)); | (void) wait (&status); |} Если не указано противное, обмен данными через канал синхрони- зируется: процесс, пытающийся читать из пустого канала, приос- танавливается до тех пор, пока данные не будут записаны в ка- нал; с другой стороны, запись в полный канал задерживается до тех пор, пока необходимое для записи место не освободится. Что- бы отменить такой режим взаимодействия, надо связать с дескрип- торами канала флаг статуса O_NDELAY (это может быть сделано при помощи системного вызова fcntl(2)). В этом случае системный вы- зов read (или write), который невозможно выполнить немедленно, завершается неудачей. Библиотечная функция popen(3S), как и функция system, вызывает выполнение указанной команды shell'а. Отличие заключается в том, что при использовании функции popen между вызвавшей ее программой и командой создается канал. С помощью функций пакета стандартного ввода/вывода в этот канал можно выводить символы и цепочки символов. 3.4. Сигналы Термин сигнал в ОС UNIX означает сообщение, посылаемое операци- онной системой выполняющимся процессам. Чаще всего результатом получения сигнала является прекращение выполнения процесса. Не- которые сигналы генерируются, если процесс пытается выполнить недопустимые действия; другие сигналы могут инициироваться обычным пользователем для своих собственных процессов, или су перпользователем для всех процессов. Чтобы послать сигнал из одного процесса другому, имеющему такой же идентификатор пользователя, можно воспользоваться системным вызовом kill(2): |int kill (pid, sig) |int pid, sig; где pid - это идентификатор процесса, которому посылается сиг- нал, а sig - целое число от 1 до 19, обозначающее посылаемый сигнал. Сигналы определяются в стандартном включаемом файл . Перечислим некоторые из них: SIGINT (прерыва- ние), SIGILL (некорректная команда), SIGKILL (уничтожение про- цесса), SIGBUS (ошибка шины), SIGCLD (завершение порожденного процесса), SIGALARM (будильник), SIGUSR1 и SIGUSR2 (определяе- мые пользователем сигналы) и т.д. Пример: |err = kill (1964, SIGKILL); С помощью системного вызова signal(2): |void (*signal (sig, func)) ( ) |int sig; |void (*func) ( ); можно выбрать один из трех допустимых способов реакции на полу- чаемые сигналы: установить стандартную реакцию (значение аргу- мента func равно SIG_DFL), игнорировать (SIG_IGN), определить собственную функцию обработки (передается адрес функции). Аргу- мент sig задает сигнал. Если установлена стандартная реакция, при получении сигнала sig процесс терминируется со всеми завершающими действиями. Если установлена реакция SIG_IGN, сигнал sig игнорируется. Наконец, если установлен перехват сигнала, при получении сигнала sig вы- полняется функция обработки сигнала func; в качестве аргумента функции func передается номер сигнала sig. После завершения функции обработки процесс, получивший сигнал, возобновляет вы- полнение с точки прерывания. Сигнал SIGKILL не может игнорироваться и перехватываться, поэ- тому его нельзя указывать в качестве аргумента sig. Ниже приведен фрагмент исходного текста функции system(), де- монстрирующий, как используется вызов signal для того, чтобы на некоторое время установить режим игнорирования сигнала SIGINT (вызов wait ожидает прихода сигнала SIGCLD): |#include | |int system(s) |char *s; |{ | int status, pid; | void (*istat) (); | | if ((pid = fork ()) == 0) | (void) execl ("/bin/sh", "sh", "-c", s, (char *) 0); | . . . | istat = signal (SIGINT, SIG_IGN); | (void) wait (&status); | (void) signal (SIGINT, istat); | . . . |} 3.5. Очереди сообщений Механизм очередей сообщений позволяет процессам взаимодейство вать, обмениваясь данными. Данные передаются между процессами дискретными порциями, называемыми сообщениями. Процессы выпол- няют над сообщениями две основные операции - прием и отправле- ние [msgop(2)]. Процессы, отправляющие или принимающие сообще- ние, могут приостанавливаться, если требуемую операцию невоз- можно выполнить немедленно. В частности, могут быть отложены попытки отправить сообщение в очередь, заполненную до отказа, получить сообщение из пустой очереди и т.п. ("операции с блоки- ровкой"). Если же указано, что приостанавливать процесс нельзя, "операции без блокировки" либо выполняются немедленно, либо за- вершаются неудачей. Прежде чем процессы смогут обмениваться сообщениями, один из них должен создать очередь [msgget(2)]. В момент создания опре- деляются первоначальные права на выполнение операций для раз- личных процессов. Процессы, обладающие соответствующими правами, могут выполнять также различные управляющие действия над очередями [msgctl(2)]. Программы, использующие очереди сообщений, как правило, должны включать стандартные файлы определений , и . 3.5.1. Создание очередей сообщений Для создания очереди сообщений используется системный вызов msgget: |int msgget (key, msgflg) |key_t key; |int msgflg; Аргумент key (тип key_t вводится в как синоним long) - ключ, играющий роль внешнего идентификатора очереди. Если с ключом еще не связана очередь, вызов msgget формирует новую очередь, создает ассоциированную с ней структуру данных и возвращает в качестве результата выделенный уникальный иденти фикатор очереди - положительное целое число. Этот же системный вызов позволяет по ключу узнать уникальный идентификатор уже существующей очереди. Аргумент msgflg содержит информацию о флагах, воздействующих на выполнение msgget, и о правах на выполнение операций над созда- ваемой очередью. Предусмотрено два флага: IPC_CREAT и IPC_EXCL. Чтобы создать очередь, необходимо установить флаг IPC_CREAT: |key = ...; |mode = ...; |msqid = msgget (key, IPC_CREAT | mode); Если бы в приведенном примере флаг не был установлен, а очередь с указанным ключом отсутствовала, системный вызов завершился бы неудачей, возвратив -1. Если Вы хотите убедиться в том, что очередь с указанным ключом создается заново, в дополнение к флагу IPC_CREAT установите IPC_EXCL: в этом случае попытка создать уже существующую оче- редь завершится неудачей: |msqid = msgget (key, IPC_CREAT | IPC_EXCL | mode); Один из тонких вопросов, связанных с порождением очереди сооб- щений, заключается в выборе ключа. Всем процессам, которые на- мереваются работать с общей очередью сообщений, для того, чтобы получить идентификатор msqid, необходимо знать ключ данной оче- реди. Задание ключа одинаковым константным значением во всех этих программах небезопасно. В самом деле, может оказаться так, что тот же ключ будет случайно задействован и другими програм- мами (вряд ли Вы станете придумывать очень оригинальный ключ). Как одно из возможных решений названной проблемы, рекомендуется использование библиотечной функции ftok(), которая по двум ар- гументам - имени файла и символу - вычисляет действительно "уникальный" ключ. Кроме того, в некоторых случаях можно посо- ветовать воспользоваться предопределенным ключом IPC_PRIVATE: |msqid = msgget (IPC_PRIVATE, flags | mode); Если указан данный ключ, вне зависимости от набора установлен- ных флагов flags выделяется новый идентификатор очереди сообще- ний и создаются ассоциированные с ним очередь и структура дан- ных. Элементарные права на выполнение операций - это права на чтение из очереди и запись в нее (т.е. на прием/отправление сообщений) для владельца, членов группы и прочих пользователей: |+----------------------+---------------------+ || Права на операции |Восьмеричное значение| |+----------------------+---------------------| || Чтение для владельца | 0400 | || Запись для владельца | 0200 | || Чтение для группы | 0040 | || Запись для группы | 0020 | || Чтение для остальных | 0004 | || Запись для остальных | 0002 | |+----------------------+---------------------+ В каждом конкретном случае нужная комбинация прав задается как результат побитного ИЛИ значений, соответствующих элементарным правам. Так, правам на чтение/запись для владельца и на чтение для членов группы и прочих пользователей соответствует восьме- ричное значение 0644: |msqid = msgget (key, IPC_CREAT | 0644); При создании очереди определяются идентификаторы владельца и группы владельца очереди; они полагаются равными идентификатору пользователя и, соответственно, идентификатору группы вызываю- щего процесса. Кроме того, устанавливается длина очереди - мак- симально допустимое число байт в ней. Структуру прав доступа к очереди стоит тщательно продумать. Следует отметить, что это не права программы, создающей очередь или работающей с ней, но права пользователя, который эту прог- рамму выполняет. Ниже приведен простейший пример программы, в которой создается очередь сообщений с правами доступа, указанными в командной строке (ключ вычисляется с помощью библиотечной функции ftok(); предполагается, что msgget.c - имя файла, содержащего исходный текст данной программы): |#include |#include |#include |#include |main (argc, argv) |int argc; |char *argv []; |{ | key_t key; | int msqid, mode; | if (argc != 2) { | fprintf (stderr, "usage: %s mode\n", argv [0]); | exit (1); | } | key = ftok ("msgget.c", 'M'); | sscanf (argv[1], "%o", &mode); | msqid = msgget (key, IPC_CREATE | mode); | if (msqid < 0) perror ("msgget"); |} Приведем определение структуры данных, ассоциированной с очередью сообщений: |struct msqid_ds { | struct ipc_perm msg_perm; /* Структура прав на | выполнение операций */ | struct msg *msg_first; /* Указатель на первое | сообщение в очереди */ | struct msg *msg_last; /* Указатель на последнее | сообщение в очереди */ | ushort msg_cbytes; /* Текущее число байт | в очереди */ | ushort msg_qnum; /* Число сообщений | в очереди */ | ushort msg_qbytes; /* Макс. допустимое число | байт в очереди */ | ushort msg_lspid; /* Ид-р последнего | отправителя */ | ushort msg_lrpid; /* Ид-р последнего | получателя */ | time_t msg_stime; /* Время последнего | отправления */ | time_t msg_rtime; /* Время последнего | получения */ | time_t msg_ctime; /* Время последнего | изменения */ |}; Информацию об имеющихся в системе очередях сообщений можно по- лучить при помощи shell-команды ipcs(1). Так, команда: |ipcs -q может привести к выдаче, подобной: |IPC status from /dev/kmem as of Mon Oct 29 14:54:21 1990 |T ID KEY MODE OWNER GROUP |Message Queues: |q 0 0x00000001 -Rrw-rw-rw- root root |q 1 0x00000002 -Rrw-rw-rw- root root |q 2 0x000007a7 -Rrw--w--w- root root Опция -q обозначает указание выводить информацию об используе- мых очередях сообщений. В простейшем случае выводится тип средства связи (q - очередь сообщений), идентификатор, ключ, режим доступа и флаги, владелец и группа. Чтобы удалить очередь сообщений средствами shell'а, можно вос- пользоваться командой ipcrm(1). Пример: |ipcrm -q 2 3.5.2. Отправление и получение сообщений Операции отправки/приема сообщений выполняют вызовы msgsnd и msgrcv; msgsnd помещает сообщения в очередь, а msgrcv читает и "достает" их оттуда: |int msgsnd (msqid, msgp, msgsz, msgflg) |int msqid; |struct msgbuf *msgp; |int msgsz, msgflg; |int msgrcv (msqid, msgp, msgsz, msgtyp, msgflg) |int msqid; |struct msgbuf *msgp; |long msgtyp; |int msgsz, msgflg; Первый аргумент в обоих случаях задает идентификатор очереди; второй является указателем на структуру, содержащую сообщение. Сообщение состоит из двух частей: текста (последовательности байт) и так называемого типа (положительного целого числа). Тип, указанный при отправлении сообщений, используется впос- ледствии при выборе сообщения из очереди. Аргумент msgsz опре- деляет длину сообщения; аргумент msgflg задает флаги. В зависимости от того, какое значение указано в качестве аргу- мента msgtyp вызова msgrcv, из очереди выбирается то или иное сообщение. Если значение аргумента равно нулю, запрашивается первое сообщение в очереди, если больше нуля - первое сообщение типа msgtyp, а если меньше нуля - первое сообщение наименьшего из типов, которые не превосходят абсолютной величины аргумента msgtyp. Пусть, например, в очередь последовательно помещены со- общения с типами 5, 3 и 2. Тогда вызов |msgrcv (msqid, msg, size, 0, flags); выберет из очереди сообщение с типом 5, поскольку оно отправле- но первым; вызов |msgrcv (msqid, msg, size, -4, flags); - последнее сообщение, так как 2 - это наименьший из возможных типов в указанном диапазоне; наконец, вызов |msgrcv (msqid, msg, size, 3, flags); - сообщение с типом 3. Во многих приложениях взаимодействующим посредством очереди со- общений процессам требуется синхронизировать свое выполнение. Например, процесс-получатель, пытавшийся прочитать сообщение и обнаруживший, что очередь пуста (либо сообщение указанного типа отсутствует), должен иметь возможность подождать, пока процесс- отправитель не поместит в очередь требуемое сообщение. Анало- гичным образом, процесс, желающий отправить сообщение в оче- редь, в которой нет достаточного для него места, может ожидать его освобождения в результате чтения сообщений другими процес- сами. Процесс, вызвавший подобного рода "операцию с блокиров- кой", приостанавливается до тех пор, пока либо нельзя будет вы- полнить операцию, либо очередь будет ликвидирована. С другой стороны, имеются приложения, в которых подобные ситуации должны приводить к немедленному (и неудачному) завершению системного вызова. Если не указано противное, вызовы msgsnd и msgrcv, выполняют операции с блокировкой, например: |msgsnd (msqid, msg, size, 0); |msgrcv (msqid, msg, size, type, 0); Чтобы выполнить операцию без блокировки, необходимо установить флаг IPC_NOWAIT: |msgsnd (msqid, msg, size, IPC_NOWAIT); |msgrcv (msqid, msg, size, type, IPC_NOWAIT); Формальный параметр msgp указывает на значение структурного ти па, определение которого содержится в стандартном включаемом файле и выглядит так: |struct msgbuf { | long mtype; /* Тип сообщения */ | char mtext [1]; /* Текст сообщения */ |}; Для хранения реальных сообщений в пользовательской программе следует определить аналогичную структуру, указав желаемый раз- мер сообщения, например: |#define MAXSZTMSG 8192 | struct mymsgbuf { | long mtype; /* Тип сообщения */ | char mtext [MAXSZTMSG]; /* Текст сообщения */ |}; |struct mymsgbuf msgbuf; В качестве параметра msgsz (максимальная длина ожидаемого собще- ния) вызова msgrcv обычно указывается размер текстового буфера, например: |sz = msgrcv (msqid, msgbuf, MAXSZTMSG, type, 0); Если не указано противное, в случае, когда длина выбранного со- общения больше, чем msgsz, вызов завершается неудачей. Если же установить флаг MSG_NOERROR, длинное сообщение обрезается до msgsz байт. Отброшенная часть сообщения пропадает и вызывающий процесс не получает никакого уведомления о том, что сообщение обрезано: |sz = msgrcv (msqid, msgbuf, MAXSZTMSG, type, MSG_NOERROR); При успешном завершении msgsnd возвращает 0, а msgrcv - значе- ние, равное числу реально полученных байт; при неудаче возвра- щается -1. 3.5.3. Управление очередями сообщений Процессы, обладающие достаточными правами доступа, могут полу- чать информацию о состоянии очереди, изменять ряд характерис- тик, удалять очередь. Для этого предназначен системный вызов msgctl: |int msgctl (msqid, cmd, buf) |int msqid, cmd; |struct msqid_ds *buf; Управляющее действие определяется значением аргумента cmd. Д пустимых значений три: IPC_STAT - получить информацию о состоя- нии очереди, IPC_SET - переустановить характеристики очереди, IPC_RMID - удалить очередь. Команды IPC_STAT и IPC_SET используют для хранения информации об очереди определенную в прикладной программе структуру типа msqid_ds, указатель на которую содержит аргумент buf: IPC_STAT копирует в нее структуру данных, ассоциированную с очередью, а IPC_SET, наоборот, в соответствии с ней обновляет ассоциирован ную структуру. Команда IPC_SET позволяет переустановить значения идентификато- ров пользователя и группы, прав на операции, максимально допус- тимого числа байт в очереди. Ниже приводится программа, изменя- ющая размер очереди. Отметим, что перед выполнением IPC_SET программа получает информацию о текущем состоянии очереди при помощи IPC_STAT; это необходимо, поскольку системный вызов из- меняет всю структуру целиком. |#include |#include |#include |main (argc, argv) |int argc; |char *argv []; |{ | int msqid, rtrn; | struct msqid_ds msqid_ds, *buf; | if (argc != 3) { | fprintf (stderr, "Usage: %s msqid qbytes\n", | argv [0]); | exit (1); | } | buf = &msqid_ds; | sscanf (argv [1], "%d", &msqid); | /* Получить исходное значение структуры данных */ | if (msgctl (msqid, IPC_STAT, buf) == -1) { | perror ("IPC_STAT"); | exit (1); | } | sscanf (argv [2], "%d", &buf->msg_qbytes); | /* Внести изменения */ | if (msgctl (msqid, IPC_SET, buf) == -1) { | perror ("IPC_SET"); | exit (1); | } |} Следующий пример демонстрирует удаление очереди: |#include |#include | |main (argc, argv) |int argc; |char *argv []; |{ | int msqid; | sscanf (argv [1], "%d", &msqid); | if (msgctl (msqid, IPC_RMID, 0) == -1) { | perror ("IPC_RMID |} Назовем права, достаточные для управления очередью. Чтобы вы- полнить действие IPC_SET или IPC_RMID, процесс должен иметь идентификатор пользователя, равный либо идентификаторам созда- теля или владельца очереди, либо идентификатору суперпользова- теля. Увеличивать размер очереди имеет право только суперполь- зователь. Чтобы выполнить IPC_STAT, требуется право на чтение. 3.5.4. Пример: сеть DMFS В качестве примера использования описанного выше механизма оче- редей сообщений рассмотрим фрагмент реализации сети DMFS на компьютере Беста. Сеть на каждом из компьютеров обслуживается несколькими системными процессами (один из них называется X25) и парой очередей ("системной" и "пользовательской"), с которыми взаимодействуют прикладные процессы. | +------------------+ |+-------> X25 +-----+ || +------------------+ | || | || --------------------------- | |+-- системная очередь <--+---------+ | --------------------------- | | | | | | --------------------------- | | |+-- пользовательская очередь <--+ | || --------------------------- | || | || +------------------+ | |+-------> прикладной +---------------+ | | процесс | | +------------------+ Обращение прикладного процесса к сети начинается с запроса о выделении канала (канал идентифицируется положительным целым числом, не превосходящим 256). Содержащие данный запрос сообще- ния помещаются прикладными процессами в системную очередь; им приписываеся тип 257. Получив и обработав такое сообщение, про- цесс X25 передает прикладному процессу номер выделенного кана- ла, помещая его в пользовательскую очередь. Прикладной процесс, получив из пользовательской очереди номер канала, использует его значение в качестве типа сообщений, которые в дальнейшем будут отправляться процессу X25 через системную очередь. Таким образом, системная очередь может содержать сообщения 257 типов: один из них используется для служебных целей, а остальные - для моделирования отдельных каналов. 3.6. Семафоры Рассматриваемые в данном разделе средства позволяют процессам взаимодействовать, изменяя значения объектов, называемых сема- форами. Значение семафора - это целое число в диапазоне от 0 до 32767. Поскольку во многих приложениях требуется более одного семафора, UNIX дает возможность создавать множества семафоров [semget(2)]. Допускаются групповые операции над множествами [semop(2)], в рамках которых для любого семафора из множества можно сделать следующее: увеличить значение, уменьшить значе- ние, дождаться обнуления. Различают операции с блокировкой (процессы, их выполняющие, приостанавливаются, если требуемое действие невозможно выполнить немедленно) и без (semop либо вы полняется немедленно, либо завершается неудачей). Процессы, обладающие соответствующими правами, могут выполнять также различные управляющие действия над семафорами [semctl(2)]. Средства, предоставляемые для работы с семафорами, имеют много общих черт с теми, которые используются в случае очередей сооб- щений: системные вызовы имеют сходный синтаксис, совпадает смысл многих флагов и управляющих команд, часть стандартных включаемых файлов является общей. Программы, работающие с семафорами, как правило, должны вклю- чать стандартные файлы определений , и . 3.6.1. Создание множества семафоров Для создания множества семафоров используется системный выз semget: |int semget (key, nsems, semflg) |key_t key; |int nsems, semflg; Целочисленное значение, возвращаемое в случае успешного завер- шения, есть уникальный идентификатор множества семафоров. В случае неудачи результат равен -1. Смысл аргументов key и semflg тот же, что и у соответствующих аргументов вызова msgget. Аргумент nsems задает число семафоров в множестве. Превышение системных параметров SEMMNI, SEMMNS и SEMMSL при по пытке создать новое множество ведет к неудаче. SEMMNI определя- ет максимально допустимое число идентификаторов множеств сема- форов в системе; SEMMNS - максимальное общее число семафоров в системе; SEMMSL - максимум семафоров в одном множестве. Oпределение структуры данных, ассоциированной с каждым множест- вом семафоров, выглядит так: |struct semid_ds { | struct ipc_perm sem_perm; /* Структура прав на | выполнение операций */ | struct sem *sem_base; /* Указатель на первый | семафор в множестве */ | ushort sem_nsems; /* Количество семафоров в | множестве */ | time_t sem_otime; /* Время последней операции */ | time_t sem_ctime; /* Время последнего | изменения */ |}; 3.6.2. Операции над множествами семафоров Операции над множествами семафоров выполняет вызов semop: |int semop (semid, sops, nsops) |int semid; |struct sembuf sops [ ]; |unsigned nsops; При успешном завершении его результат равен нулю; в случае неу дачи возвращается -1. В качестве аргумента semid указывается идентификатор множества семафоров, полученный ранее при помощи вызова semget. Аргумент sops (массив структур) определяет, над какими семафорами будут выполняться операции и какие именно. Аргумент nsops специфици- рует размер массива sops, т.е. число элементарных операций, вы- полняемых вызовом semop. Максимально допустимый размер массива определяется системным параметром SEMOPM. Напомним, что число семафоров в множестве ограничивается другим параметром SEMMSL. Поэтому возможна ситу- ация, в которой число семафоров в каком-либо множестве больше, чем SEMOPM. Структура, описывающая операцию над одним семафором, определя- ется так: |struct sembuf { | short sem_num; /* Номер семафора */ | short sem_op; /* Операция над семафором */ | short sem_flg; /* Флаги операции */ |}; Номер семафора указывает конкретный семафор в множестве, над которым должна быть выполнена данная операция (семафоры в мно- жестве нумеруются, начиная с нуля). Выполняемая операция задается значением поля sem_op: положи- тельное значение предписывает увеличить значение семафора на указанную величину; отрицательное - уменьшить; нулевое - срав нить с нулем. Вторая операция не может быть успешно выполнена, если в результате значение семафора становится отрицательным; третья - если значение семафора ненулевое. Если не указано противное, над семафором выполняется операция с блокировкой: процесс приостанавливается до тех пор, пока значе- ние семафора благодаря действиям других процессов не позволит успешно завершить операцию. Чтобы выполнить операцию без блоки- ровки, в поле sem_flg надо установить флаг IPC_NOWAIT. Выполнение массива операций с точки зрения пользовательского процесса является неделимым действием. Это значит, во-первых, что если операции выполняются, то только все вместе и, во-вто- рых, что никакой другой процесс не может получить доступ к про- межуточному состоянию множества семафоров, когда часть операций из массива уже выполнилась, а другая часть еще не успела. Опе- рационная система, разумеется, выполняет операции из массива по очереди, причем порядок не оговаривается. Если очередная опера- ция не может быть выполнена, то эффект предыдущих операций ан нулируется, а системный вызов приостанавливается (операция с блокировкой) или немедленно завершается неудачей (операция без блокировки). Подчеркнем, что в случае неудачного завершения вы зова semop значения всех семафоров в множестве останутся неиз- менными. Например, приведенный ниже массив операций задает уменьшение (с блокировкой) семафора 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; В массив операций не следует включать несколько операций над одним и тем же семафором: такое действие (даже если оно осмыс- ленно) может иметь результат, отличный от того, который Вы ожи даете. 3.6.3. Управление множествами семафоров Процессы могут получать информацию о состоянии множества сема- форов, изменять ряд его характеристик, удалить множество. Для этого предназначен системный вызов semctl: |int semctl (semid, semnum, cmd, arg) |int semid, cmd; |int semnum; |union semun { | int val; | struct semid_ds *buf | ushort *array; |} arg; Аргументы semid (идентификатор множества семафоров) и semnum (номер семафора в множестве) определяют множество или отдельный семафор, над которым выполняется управляющее действие. Назначе- ние аргумента arg зависит от действия, задаваемого значением аргумента cmd. Среди допустимых действий - GETVAL (получить значение семафора и выдать его в качестве результата) и SETVAL (установить значе- ние семафора равным arg.val): |val = semctl (semid, no, GETVAL, 0); |if (semctl (semid, no, SETVAL, arg) == -1) ...; Имеются и аналогичные групповые действия - GETALL (прочитать значения всех семафоров множества и поместить их в массив, на который указывает arg.array) и SETALL (установить значения всех семафоров множества равными значениям элементов массива): |for (i = 0; i < qsem; i++) arr [i] = ...; |err = semctl (semid, 0, SETALL, arr); Предусмотрены действия, позволяющие узнать, сколько процессов ожидает увеличения/обнуления (GETNCNT/GETZCNT) значения семафо- ра. В обоих случаев число ожидающих процессов выдается в ка- честве результата: |rtrn = semctl (semid, no, GETNCNT, 0); |rtrn = semctl (semid, no, GETZCNT, 0); Наконец, для семафоров, как и для очередей сообщений, определ ны управляющие команды IPC_STAT (получить информацию о состоя- нии множества семафоров), IPC_SET (переустановить характеристи- ки состояния), IPC_RMID (удалить множество семафоров). Для хранения информации о состоянии множества семафоров исполь- зуется структура, определенная в пользовательской программе, на которую указывает arg.buf. |struct semid_ds semid_ds; |union semun { | int val; | struct semid_ds *buf; | ushort array [MAXSETSIZE]; |} arg; | ... |arg.buf = &semid_ds; | ... |rtrn = semctl (semid, 0, IPC_STAT, arg.buf); |arg.buf->sem_perm.mode = 0666; |rtrn = semctl (semid, 0, IPC_SET, arg.buf); Для выполнения тех или иных управляющих действий процессы долж- ны обладать достаточными правами доступа. Так, чтобы выполнить IPC_SET или IPC_RMID, процесс должен иметь идентификатор поль зователя, равный либо идентификаторам создателя или владельца множества, либо идентификатору суперпользователя. Для выполне ния SETVAL и SETALL требуется право на изменение, а для выпол- нения других действий - право на чтение. 3.6.4. Пример: обед философов В качестве примера использования множества семафоров рассмотрим известную задачу об обедающих философах. Задача заключается в следующем: за круглым столом сидит несколько философов; в каж- дый момент времени философ либо беседует, либо ест. Для еды фи- лософу одновременно требуется две вилки. Поэтому, прежде чем в очередной раз перейти от беседы к приему пищи, философу надо дождаться, пока не освободятся обе вилки - слева и справа от него, и взять их в руки. Немного поев, философ кладет вилки на стол и вновь присоединяется к беседе. Требуется разработать программную модель обеда философов. Главное в этой задаче - корректная дисциплина захвата и освобождения вилок. В самом де- ле, если, например, каждый из философов одновременно с другими возьмется за вилку, лежащую слева от него, и будет ждать осво- бождения правой, обед не завершится никогда. Предлагаемое решение состоит из двух программ. Первая программа реализует процесс-монитор, который порождает множество семафо ров (по одному семафору на каждую вилку), устанавливает началь- ные значения семафоров (занятой вилке будет соответствовать значение 0, свободной - 1), запускает несколько процессов, представляющих философов, указывая место за столом (в качестве одного из аргументов передается число от 1 до QPH), ожидает, пока все процессы завершатся (т.е. все философы съедят свой обед), и удаляет множество семафоров. |#include |#include |#include |#define QPH 5 |main () |{ | char ssemid [10], sno [10], sqph [10]; | int key, semid, no, st, semid; | int arr [QPH]; | /* Создание множества семафоров */ | key = ftok ("phdin.c", 'C'); | if ((semid = semget (key, QPH, 0600 | IPC_CREAT)) < 0) { | perror ("SEMGET"); | exit (1); | } | for (no = 0; no < QPH; no++) arr [no] = 1; | if (semctl (semid, 0, SETALL, arr) < 0) { | perror ("SETALL"); | exit (1); | } | sprintf (ssemid, "%d", semid); | sprintf (sqph, "%d", QPH); | /* Все - к столу */ | for (no = 1; no <= QPH; no++) { | switch (fork ()) { | case -1: | perror ("FORK"); | exit (1); | case 0: | sprintf (sno, "%d", no) | execlp ("phil", "phil", ssemid, sqph, sno, | (char *) 0); | perror ("EXEC"); | exit (1); | } | } | /* Ожидание завершения обеда */ | for (no = 1; no <= QPH; no++) wait (&st); | /* Удаление множества семафоров */ | if (semctl (semid, 0, IPC_RMID, 0) < 0) { | perror ("SEMCTL"); | exit (1); | } |} Вторая программа описывает обед каждого философа. Философ како- е-то время беседует (случайное значение trnd), затем пытается взять вилки слева и справа от себя, когда ему это удается, не- которое время ест (случайное значение ernd), после чего осво- бождает вилки. Это продолжается до тех пор, пока не будет съеден весь обед. |#include |#include |#include |#define ernd (rand () % 3 + 1) |#define trnd (rand () % 5 + 1) |#define FO 15 |main (argc, argv) |int argc; |char *argv []; |{ | int no, qph, t, semid, fo; | struct sembuf sembuf [2]; | 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) { /* Обед */ | /* Философ говорит */ | sl = trnd; sleep (sl); | /* Пытается взять вилки */ | sembuf [0].sem_op = -1; | sembuf [1].sem_op = -1; | if (semop (semid, sembuf, 2) < 0) { | perror ("SEMOP"); | exit (1); | } | /* Ест */ | t = ernd; sleep (t); fo -= t; | /* Отдает вилки */ | sembuf [0].sem_op = 1; | sembuf [1].sem_op = 1; | if (semop (semid, sembuf, 2) < 0) { | perror ("SEMOP"); | exit (1); | } | exit (0); |} Отметим, что возможность выполнения групповых операций над се- мафорами предельно упростило решение. 3.7. Разделяемые сегменты памяти Описываемое здесь средство межпроцессной связи позволяет про- цессам иметь общие области виртуальной памяти, разделяя содер- жащуюся в них информацию. Единицей разделяемой памяти являются сегменты. Разделение памяти обеспечивает наиболее быстрый обмен данными между процессами. Работа с разделяемой памятью начинается с того, что один из взаимодействующих процессов создает разделяемый сегмент, специ- фицируя первоначальные права доступа к нему и его размер в бай- тах [shmget(2)]. Чтобы получить доступ к разделяемому сегменту, его нужно присо- единить, т.е. разместить сегмент в виртуальном пространстве процесса. После присоединения, в соответствии с правами досту- па, процессы могут читать данные из сегмента и записывать их (быть может, синхронизируя свои действия с помощью семафоров). Когда разделяемый сегмент становится ненужным, его следует от- соединить [shmop(2)]. Предусмотрена возможность выполнения управляющих действий над разделяемыми сегментами [shmctl(2)]. 3.7.1. Создание разделяемых сегментов памяти Для создания разделяемого сегмента памяти служит системный вы зов shmget: |int shmget (key, size, shmflg) |key_t key; |int size, shmflg; Возвращаемое значение - уникальный идентификатор разделяемого сегмента памяти (shmid). Смысл аргументов key и shmflg тот же, что и у соответствующих аргументов вызова semget. Аргумент size задает размер сегмента в байтах. Число уникальных идентификаторов разделяемых сегментов ограни- чено; попытка его превышения ведет к неудачному завершению сис темного вызова (возвращается -1). shmget завершится неудачей и тогда, когда значение аргумента size меньше, чем минимальный, либо больше, чем максимальный размер разделяемого сегмента. При порождении разделяемого сегмента создается ассоциированная с ним структура данных shmid_ds (ее определение, как и опреде- ление некоторых других типов и констант, содержится в стандарт- ном включаемом файле ). 3.7.2. Присоединение и отсоединение сегментов Чтобы присоединить разделяемый сегмент, используется вызов shmat: |int shmat (shmid, shmaddr, shmflg) |int shmid; |char *shmaddr; |int shmflg; Аргумент shmid задает идентификатор разделяемого сегмента; (в качестве shmid указывается значение, полученное ранее при помо- щи вызова shmget); аргумент shmaddr - адрес, по которому сег- мент должен быть присоединен, то есть тот адрес в виртуальном пространстве пользователя, который получит начало сегмента. Поскольку свойства сегментов зависят от аппаратных особенностей управления памятью, не всякий адрес является приемлемым. Можно порекомендовать адреса вида |0x80000000 |0x80040000 |0x80080000 | . . . Если значение shmaddr равно нулю, система выбирает адрес присо- единения по своему усмотрению. При успешном завершении shmat результат равен адресу, который получил присоединенный сегмент; в случае неудачи возвращается -1. Разумеется, чтобы использовать результат shmat как указа- тель, его нужно преобразовать к требуемому типу. Аргумент shmflg используется для передачи вызову shmat флагов. Упомянем флаг SHM_RDONLY, предписывающий присоединить сегмент только для чтения; если он не установлен, присоединенный сег- мент будет доступен и на чтение, и на запись (если процесс об ладает соответствующими правами): |ptr = (char*) shmat (shmid, shmaddr, SHM_RDONLY); |rtrn = shmat (shmid, 0, 0); Отсоединение сегментов производится вызовом shmdt (аргумент shmaddr задает начальный адрес отсоединяемого сегмента): |int shmdt (shmaddr) |char *shmaddr; 3.7.3. Управление разделяемыми сегментами памяти Управление разделяемыми сегментами осуществляется при помощи системного вызова shmctl: |int shmctl (shmid, cmd, buf) |int shmid, cmd; |struct shmid_ds *buf; Использование данного вызова полностью аналогично использованию вызова msgctl. Как и для очередей сообщений, для разделяемых сегментов определены управляющие команды IPC_STAT (получить ин формацию о состоянии разделяемого сегмента), IPC_SET (переуста- новить характеристики состояния), IPC_RMID (удалить разделяемый сегмент): |#include |#include |#include |struct shmid_ds shmid_ds, *buf; |buf = &shmid_ds; | ... |rtrn = shmctl (shmid, IPC_STAT, buf); Следующий вызов (ликвидация разделяемого сегмента) обязательно должен выполняться после того, как удалена последняя ссылка на сегмент: |rtrn = shmctl (shmid, IPC_RMID, 0); 3.7.4. Пример: критическая область Аппарат разделяемых сегментов предоставляет нескольким процес- сам возможность одновременного доступа к общей области памяти. Для того, чтобы обеспечить корректность доступа, процессы долж ны тем или иным способом синхронизировать свои действия. В к честве средства синхронизации удобно использовать семафор. Ниже показана реализация так называемой критической области - меха- низма, обеспечивающего взаимное исключение разделяющих общие данные процессов. Чтобы "создать" подобный механизм, надо породить разделяемый сегмент, присоединить его во всех процессах, которым предостав- ляется доступ к разделяемым данным, а также породить и проини- циализировать простейший семафор: |struct region { | . . . | int a; | . . . |}; |struct region *ptr; |shmid = shmget (key, sizeof (struct region), IPC_CREAT); |ptr = shmat (shmid, 0, 0); |semid = semget (key, 1, IPC_CREAT); |(void) semctl (semid, 0, SETVAL, 1); Монопольный доступ к структуре, на которую указывает ptr, обес печивает фрагмент следующего вида: |struct sembuf P = {0, -1, 0}; |struct sembuf V = {0, 1, 0}; | |err = semop (semid, &P, 1); | . . . |ptr->a = ...; /* Монопольный доступ */ | ... |err = semop (semid, &V, 1); 3.8. Резюме Выше были изложены первоначальные сведения о разнообразных программных средствах ОС UNIX, которые могут использоваться для порождения и организации взаимодействия параллельных процессов. Решая ту или иную задачу, с учетом ее специфики Вы сможете выб- рать подходящий механизм межпроцессной связи. Формальное описа- ние всех упомянутых системных вызовов и библиотечных функций содержится в Справочнике Программиста. 4. ПАКЕТ УПРАВЛЕНИЯ ТЕРМИНАЛОМ CURSES/TERMINFO 4.1. Введение Для разработки интерактивных программ в системе UNIX пользова- телю предоставляется пакет curses/terminfo. Этот пакет включает в себя библиотеку процедур на языке C, базу данных и набор вспомогательных средств системы UNIX. Здесь описываются основ- ные возможности, предоставляемые пакетом для работы с термина- лом. Полное описание компонентов пакета содержится в статьях curses(3X) и terminfo(4) Справочника программиста. Большое чис- ло примеров использования curses можно найти в соответствующей главе Руководства Программиста. curses - это библиотека высокоуровневых подпрограмм, которые делают следующее: Осуществляют ввод и вывод данных на экран терминала. Управляют вводом и выводом - например, подсвечивают вы- водимые данные или подавляют отображение вводимых сим волов на экране ("эхо"). Работают с несколькими образами экрана (окнами). Осуществляют ввод/вывод, работая одновременно с нес- колькими терминалами. Выполняют некоторые другие функции. terminfo - это: Группа подпрограмм нижнего уровня, предназначенных для управления терминалом. База данных, содержащая описания многих типов термина- лов, с которыми могут работать программы, основанные на curses. Эти описания содержат характеристики терминала и то, как терминал выполняет различные операции. Использование базы данных терминалов позволяет минимизировать зависимость интерактивных программ от типа терминала. Подпрограммы пакета curses извлекают из базы данных terminfo информацию о типе текущего терминала, на котором выполняется программа. Предположим, Ваша программа выполняется на терминале Tatung ET10 в режиме эмуляции D211. Чтобы отработать должным образом, программа должна иметь информацию о характеристиках терминала. Требуемая информация хранится в описании этого терминала в базе данных terminfo. Чтобы получить ее, программе, работающей с curses, достаточно знать название текущего терминала. Это наз- вание можно передать, поместив его в переменную окружения $TERM. Располагая значением $TERM, программа извлекает описание текущего терминала из базы данных terminfo. Пусть, например, в .profile включены следующие строки: |TERM=D211 |export TERM |tput init В первой строке устанавливается название терминала, во второй оно помещается в окружение. Третья строка требует от системы UNIX проинициализировать текущий терминал, то есть обеспечить соответствие его состояния описанию в базе данных terminfo. Ес- ли, имея такие строки в файле .profile, Вы запустите программу, работающую с curses, она получит нужную ей информацию. Кроме упомянутых выше, пакет управления терминалом содержит компоненты tic(1M) (компилятор описаний терминалов для базы данных terminfo) и infocmp(1M) (средство для печати и сравнения скомпилированных описаний терминалов). 4.2. Использование подпрограмм пакета curses 4.2.1. Что нужно программе для работы с curses Если программа использует пакет curses, она должна включать файл и вызывать подпрограммы initscr(), refresh(), или ей подобные, а также endwin(). В файле определяются используемые в пакете параметры и константы. Кроме того, определяется несколько глобальных пе- ременных и структур данных. Так, файл содержит опре- деление глобальных переменных LINES и COLS, которым назначаются размеры экрана. Вводится собственный тип символа chtype, содер- жащий код символа и атрибуты. Здесь же определяются констант OK и ERR. Большинство подпрограмм curses возвращают OK при нор- мальном завершении и ERR при возникновении ошибки. Многие подпрограммы curses определены в как макросы, обращающиеся к другим подпрограммам и макросам из curses. Нап- ример, refresh() является макросом. Определение |#define refresh() wrefresh(stdscr) показывает, что вызов refresh расширяется в обращение к также входящей в curses подпрограмме wrefresh(). Подпрограммы initscr(), refresh() и endwin() приводят терминал в состояние "работа с curses", обновляют содержимое экрана и восстанавливают терминал в состоянии "вне curses" соответствен- но. Поясним действия этих подпрограмм на следующем примере: |#include | |main () |{ | initscr (); /* Инициализируем терминал, | переменные и структуры данных | из */ | move (LINES/2 - 1, COLS/2 - 4); | addstr ("Hello"); | refresh (); /* Выводим данные на экран | терминала */ | addstr ("World"); | refresh (); /* Выводим еще на экран | терминала */ | endwin (); /* Восстанавливаем состояние | терминала */ |} Curses-программа начинается обычно с вызова initscr(); это дос- таточно сделать один раз. Подпрограмма initscr() определяет тип текущего терминала по значению переменной окружения $TERM. За- тем подпрограмма инициализирует глобальные переменные, описа ные в файле , в частности, переменные LINES и COLS. Ввод/вывод производится подпрограммами, подобными использующим- ся в примере move() и addstr(). Например, |move (LINES/2 - 1, COLS/2 - 4); вызывает перемещение курсора. Затем |addstr ("Hello"); требует вывести цепочку символов Hello. При вызове подпрограмм типа addstr() содержимое физического эк- рана терминала не меняется. Экран обновляется только при вызове refresh(). До этого изменяется только внутреннее представление экрана, называемое окном. При вызове refresh() все накопленные для вывода данные пересылаются на экран терминала. В содержится описание принятого по умолчанию окна stdscr (стандартное окно), размеры которого совпадают с разме рами физического экрана. stdscr имеет тип WINDOW*, указатель на структуру языка C, которую можно представлять себе в виде дву мерного массива символов. Программа всегда отслеживает как сос тояние stdscr, так и состояние физического экрана. refresh() их сравнивает и посылает на терминал последовательность символов, приводящую экран в соответствующий содержимому stdscr вид. Вы бирается один из многих способов сделать это, с учетом характе- ристик терминала и возможного сходства того, что есть на экране и того, что содержится в окне. Выходной поток оптимизируется таким образом, чтобы он содержал как можно меньше символов. Программа, работающая с curses, заканчивается вызовом endwin(). Эта подпрограмма восстанавливает прежнее состояние терминала. 4.2.2. Компиляция программы, которая использует curses Компилируйте такую программу, как обычную программу на языке C, командой cc(1). Подпрограммы пакета хранятся в библиотек /usr/lib/libcurses.a. Чтобы редактор связей просматривал эту библиотеку, в команде cc необходимо указать опцию -l. Общий формат требуемой командной строки таков: |cc файл.c -lcurses -o файл Файл.c - это имя файла с исходным текстом программы; файл - имя выполнимого объектного файла. Чтобы пакету curses был известен тип текущего терминала, поль- зователь должен соответствующим образом установить переменную окружения $TERM, например, включив в свой файл .profile строки: |TERM=тип_текущего_терминала |export TERM |tput init 4.2.3. Вывод Curses содержит функции, позволяющие выводить одиночный символ или цепочку символов, форматировать строку из нескольких вход- ных аргументов, перемещать курсор, возможно, одновременно вы- полняя вывод данных, очищать весь экран или его часть. Необходимо сделать следующее предостережение: если Вы работаете с curses, Вам не следует использовать для ввода/вывода другие подпрограммы или обращения к системе, например, read(2) или write(2); это может привести к нежелательным эффектам. Функция addch() записывает один символ в stdscr: |int addch (ch) |chtype ch; addch() производит некоторую перекодировку, преобразовывая сим- вол перевода строки в последовательность, обеспечивающую очист- ку до конца строки и переход к новой строке; символ табуляции - в соответствующее количество пробелов; другие управляющие сим- волы - в их запись в нотации ^X. |#include | |main () |{ | initscr (); | addch ('a'); | refresh (); | endwin (); |} Функция addstr() выводит цепочку символов в stdscr: |int addstr (str) |char *str; addstr() вызывает addch() для вывода каждого символа и произво- дит ту же перекодировку. printw() осуществляет форматированный вывод в stdscr: |int printw(fmt [,arg...]) |char *fmt; Подобно printf(3S), printw() получает формат и переменное коли- чество аргументов. Как и addstr(), printw() обращается к addch() для вывода каждого символа. Функция move() занимается перемещением курсора: |int move (y, x); |int y, x; move() устанавливает курсор в позицию "строка y, колонка x" ок- на stdscr. Первым аргументом move() является координата y, а вторым - x. Координаты левого верхнего угла stdscr равны (0,0), а правого нижнего - (LINES-1,COLS-1). Попытка сдвинуть курсор в позицию, не находящуюся между (0,0) и (LINES-1,COLS-1), приво- дит к ошибке. Перемещение курсора можно производить одновременно с выводом данных: mvaddch (y, x, ch) перемещает курсор и выводит символ; mvaddstr (y, x, str) перемещает курсор и выводит цепочку симво- лов; mvprintw (y, x, fmt[,arg...]) перемещает курсор и выводит форматированную строку. Пример: |#include | |main() |{ | initscr (); | addstr ("Курсор должен быть здесь -->"); | addstr (" если move () работает."); | printw ("\n\n\nНажмите для завершения теста."); | move (0, 28); | refresh (); | getch (); /* Вводит , см. ниже */ | endwin (); |} Эта программа выводит следующее: | +-----------------------------------------------------------------+ | |Курсор должен быть здесь -->_если move () работает. | | | | | | | | |Нажмите для завершения теста. | | | ... | | | | | | | | +-----------------------------------------------------------------+ После нажатия возврата каретки экран будет выглядеть так: | +-----------------------------------------------------------------+ | |Курсор должен быть здесь -->_ | | | | | | | | |Нажмите для завершения теста. | | | ... | | | | | | | | +-----------------------------------------------------------------+ Следующие функции занимаются очисткой части либо всего экрана: |int clear ( ) |int erase ( ) |int clrtoeol ( ) |int clrtobot ( ) Подпрограммы clear() и erase() заполняют все окно stdscr пробе- лами. clear() допускает наличие на экране мусора, о котором она не знает; эта подпрограмма вызывает сначала erase(), а затем clearok(), которая вызывает полную очистку физического экрана при следующем вызове refresh(). initscr() автоматически вызыва- ет clear(). clrtoeol() заменяет остаток строки пробелами. clrtobot() заме- няет остаток экрана пробелами. Обе подпрограммы выполняют свои действия с позиции, в которой находится курсор (включительно). Пример: |#include | |main () |{ | initscr (); | addstr ("Нажмите для удаления отсюда "); | addstr ("до конца строки и далее."); | addstr ("\nУдалите это тоже.\nИ это."); | move (0, 32); | refresh (); | getch (); | clrtobot (); | refresh (); | endwin (); |} Эта программа выводит следующее: | +-----------------------------------------------------------------+ | |Нажмите для удаления отсюда_до конца строки и далее. | | |Удалите это тоже. | | |И это. | | | | | | ... | | | | | +-----------------------------------------------------------------+ Обратите внимание на два вызова refresh() - первый выводит этот текст на экран, второй очищает экран с отмеченного курсором места до конца. После нажатия возврата каретки экран будет выглядеть так: | +-----------------------------------------------------------------+ | |Нажмите для удаления отсюда | | | | | | | | | ... | | | | | |$ _ | | +-----------------------------------------------------------------+ 4.2.4. Опции ввода Система UNIX производит различные дополнительные действия с символами, вводимыми пользователем, например, отображает вводи- мые символы на экране терминала (такое отображение мы будем на- зывать эхом), обрабатывает символы забоя, уничтожения и т.д. Curses полностью берет на себя управление экраном терминала, поэтому его подпрограммы могут отключать эхо, производимое сис- темой UNIX, и при необходимости делают его самостоятельно. Иногда требуется программно отключать стандартную для системы UNIX интерпретацию вводимых символов. Curses предоставляет нес- колько подпрограмм, позволяющих управлять интерпретацией. Как правило, в интерактивной терминальной программе использует- ся комбинация режимов noecho() и cbreak(). Пусть, например, нужно отображать вводимые символы не там, где находится курсор. Для отключения эха предназначена подпрограмма noecho(). Однако, хотя noecho() и подавляет отображение вводимых символов, симво- лы забоя и уничтожения обрабатываются обычным образом. Для отк- лючения этого режима используется подпрограмма cbreak(). Не следует устанавливать одновременно nocbreak() и noecho(). В зависимости от состояния драйвера терминала при вводе символа, это может привести к выводу нежелательных данных на экран. Опишем перечисленные подпрограммы подробнее: |int echo ( ) |int noecho ( ) |int cbreak ( ) |int nocbreak ( ) echo() устанавливает режим "эхо" - отображение символов на эк- ране по мере их ввода. При старте программы этот режим установ- лен. noecho() отключает режим "эхо". cbreak() включает режим "прерывание при вводе каждого символа". Программа получает символ сразу после его ввода; символы забоя, уничтожения и конца файла не обрабатываются. nocbreak() восста- навливает обычный режим "построчный ввод", устанавливаемый при запуске программы. 4.2.5. Ввод Подпрограммы curses, предназначенные для чтения с текущего тер- минала, позволяют читать символы по одному, читать строку, за- канчивающуюся символом перевода строки, сканировать вводимые символы, извлекая значения и присваивая их переменным из списка аргументов. Первичной является подпрограмма getch(), возвращающая значение одного введенного символа. Она подобна подпрограмме getchar(3S) из библиотеки языка C, но может производить и некоторые допол- нительные, зависящие от терминала, действия. getch() удобно ис- пользовать вместе с подпрограммой keypad() из пакета curses. Это позволяет распознавать и обрабатывать, как один символ, последовательности, начинающиеся с ESC, передаваемые при нажа тии клавиш управления курсором или функциональных клавиш. Функция getch() читает один символ и возвращает его значение: |int getch ( ) Приведем пример: |#include | |main () |{ | int ch; | initscr (); | cbreak (); | addstr ("Введите любой символ: "); | refresh (); | ch=getch (); | printw ("\n\n\nВы ввели '%c'.\n", ch); | refresh (); | endwin (); |} Первый refresh() выводит цепочку символов, указанную в addstr(), из stdscr на экран терминала: | +-----------------------------------------------------------------+ | |Введите любой символ: _ | | | | | | | | | ... | | | | | | | | +-----------------------------------------------------------------+ Пусть на клавиатуре нажали w. getch() принимает символ и его значение присваивается ch. Наконец, второй раз вызывается refresh() и экран становится таким: | +-----------------------------------------------------------------+ | |Введите любой символ: w | | | | | | | | |Вы ввели 'w'. | | | ... | | | | | |$ _ | | +-----------------------------------------------------------------+ Функция getstr(): |int getstr (str) |char *str; читает символы и сохраняет их в буфере до тех пор, пока не бу- дет нажат возврат каретки, перевод строки или клавиша ввода. getstr() не проверяет буфер на переполнение. Прочитанные симво- лы пересылаются в цепочку str. getstr() является макросом и вы- зывает getch() для чтения каждого символа. Пример: |#include | |main () |{ | char str [256]; | initscr (); | cbreak (); | addstr ("Введите строку символов,"); | addstr (" оканчивающуюся :\n\n"); | refresh (); | getstr (str); | printw ("\n\n\nВы ввели \n'%s'\n", str); | refresh (); | endwin (); |} Допустим, Вы ввели строку "Мне нравится изучать систему UNIX". После нажатия возврата каретки экран будет выглядеть так: | +-----------------------------------------------------------------+ | |Введите строку символов, оканчивающуюся : | | | | | |Мне нравится изучать систему UNIX | | | | | | | | |Вы ввели | | |'Мне нравится изучать систему UNIX' | | | ... | | | | | |$ _ | | +-----------------------------------------------------------------+ Функция scanw() сканирует введенную строку: |int scanw (fmt [,arg...]) |char *fmt; scanw() вызывает getstr(). Подобно scanf(3S), scanw() использу- ет формат для преобразования введенной строки и присваивает значения переменному количеству аргументов. 4.2.6. Атрибуты вывода Рассказывая об addch(), мы упомянули, что эта подпрограмма вы- водит один знак типа chtype. chtype состоит из двух частей: ин- формации о самом символе и информации о наборе атрибутов, свя- занных с этим символом. Кроме того, с окном всегда связан набор атрибутов, которые автоматически присваиваются каждому выводи- мому символу. Перечислим несколько атрибутов: A_BLINK - мерца- ние, A_BOLD - повышенная яркость или жирный шрифт, A_DIM - по- ниженная яркость, A_REVERSE - инвертированное отображение, A_UNDERLINE - подчеркивание. Не каждый терминал может отобра- жать все перечисленные атрибуты. Если терминал не может реали- зовать запрошенный атрибут, curses пытается заменить его похо- жим, а если и это невозможно, то атрибут игнорируется. Для управления атрибутами вывода используются следующие подп- рограммы curses: |int attron (attrs) |chtype attrs; |int attrset (attrs) |chtype attrs; |int attroff (attrs) |chtype attrs; attron() включает запрошенные атрибуты attrs, сохраняя те, ко- торые уже включены. attrset() включает запрошенные атрибуты attrs вместо тех, которые включены в момент обращения к ней. attroff() выключает запрошенные атрибуты attrs, если они вклю чены. attrset (0) отключает все атрибуты. Атрибуты можно вклю- чать по одному или в комбинации. Например, чтобы вывести яркий мерцающий текст, используется attrset (A_BOLD | A_BLINK). При мер: | . . . | printw ("Мерцающее "); | attrset (A_BLINK); | printw ("слово"); | attrset (0); | printw (" бросается в глаза.\n"); | . . . | refresh (); 4.2.7. Работа с окнами Как уже говорилось, по умолчанию вывод данных осуществляется в окно stdscr, совпадающее со всем экраном. Однако curses позво- ляет создавать и использовать произвольное число дополнительных окон. Каждое окно соответствует некоторому прямоугольнику физи- ческого экрана и может перекрываться с другими. Окна полезны для одновременного ведения нескольких образов экрана. Например, во многих приложениях используются два окна: одно для ввода/вы- вода собственно данных, а другое - для вывода сообщений об ошибках, чтобы эти сообщения не портили содержимое основного окна. Программист может по желанию обновлять то или иное окно. Некоторые подпрограммы пакета curses предназначены для работы с окнами особого типа, называемые спецокнами. Спецокно - это та- кое окно, размер которого не ограничивается размером экрана и которое не связано с каким-либо определенным местом на экране. Их можно применять, если Вам нужны очень большие окна, или же такие, которые нужно отображать на экран частями. Возможные взаимосвязи между несколькими окнами, спецокнами и экраном терминала показаны на рисунке: | +---------------------------------------------+ | | экран терминала | | | +----------+ +----------+ | | | | окно | | окно | | | | | | | | | | | | | | | +------------+--------------+ | | | | | | | | спецокно | | | | | +-+--------+ | | | | | | | | | | спец- | | | | | | | | | | | окно | | | | | | | +----------+ | +--------+-+ | | | | | +----------+----------+--+ | | | | | | окно | | | | | | | | | | | | +------------+--------------+ | | +----------+----------+--+ | | +---------------+----------+------------------+ | | | | +----------+ Для вывода данных в окна используются подпрограммы, похожие на те, которые работают с stdscr. Единственная разница состоит в том, что необходимо указать окно, к которому относится опера- ция. Как правило, имена этих подпрограмм получаются путем до- бавления буквы w в начало названия соответствующей подпрограммы для stdscr, а также имени окна в качестве первого параметра. Например, если нужно вывести символ c в окно mywin, addch ('c') превращается в waddch (mywin,'c'). Следующие работающие с окна- ми подпрограммы соответствуют тем, которые были описаны ранее: |waddch (win, ch) |mvwaddch (win, y, x, ch) |waddstr (win, str) |mvwaddstr (win, y, x, str) |wprintw (win, fmt[,arg...]) |mvwprintw (win, y, x, fmt[,arg...]) |wmove (win, y, x) |wclear (win) |werase (win) |wclrtoeol (win) |wclrtobot (win) |wrefresh (win) Все перечисленные подпрограммы, за исключением wrefresh(), мо- гут использоваться и со спецокнами. Вместо нее используется подпрограмма prefresh(): |prefresh (pad, pminr, pminc, sminr, sminc, smaxr, smaxc) Дополнительные аргументы задают место на экране, куда пойдет вывод. Аргументы pminr и pminc задают левый верхний угол облас- ти спецокна, которая будет изображаться; sminr, sminc, smaxr, smaxc задают прямоугольник на экране, в котором будет вестись отображение. Правый нижний угол изображаемого прямоугольника в спецокне вычисляется по заданнным координатам на экране. Оба прямоугольника не должны выходить за пределы спецокна и экрана. Подпрограмма wrefresh() предназначена для пересылки данных из произвольного окна на экран терминала; она вызывает подпрограм- мы wnoutrefresh() и doupdate(). (Аналогичным образом, pref- resh() использует подпрограмму pnoutrefresh().) Пользуясь wnoutrefresh(), можно обновлять экран с большей эф- фективностью, чем обращаясь к wrefresh(). wrefresh() сначала вызывает wnoutrefresh(), копирующую указанное окно в структуру данных, называемую виртуальным экраном. Виртуальный экран со- держит то, что программа собирается вывести на терминал. Затем wrefresh() обращается к doupdate(), которая сравнивает вирту альный экран с физическим и производит обновление последнего. Если Вы хотите отобразить содержимое сразу нескольких окон, вызвав wnoutrefresh() для каждого окна, а затем один раз doup- date(), можно уменьшить общее число передаваемых символов. При- веденная ниже программа использует doupdate() лишь однажды. |#include | |main () |{ | WINDOW *w1, *w2; | initscr (); | w1 = newwin (2, 6, 0, 3); | w2 = newwin (1, 4, 5, 4); | waddstr (w1, "Hello"); | wnoutrefresh (w1); | waddstr (w2, "World"); | wnoutrefresh (w2); | doupdate (); | endwin (); |} В начале этой программы объявляются новые окна. Операторы | w1 = newwin (2, 6, 0, 3); | w2 = newwin (1, 4, 5, 4); создают два окна, которые называются w1 и w2. Далее перечислены подпрограммы, которые применяются для созда- ния и удаления окон (спецокон): WINDOW *newwin (nlines, ncols, begin_y, begin_x) |int nlines, ncols, begin_y, begin_x; |WINDOW *newpad (nlines, ncols) |int nlines, ncols; |int delwin (win) |WINDOW *win; newwin() возвращает указатель на структуру данных, описывающую содержимое нового окна. Аргументы nlines и ncols задают размер окна, а аргументы begin_y и begin_x - координаты [относительно (0, 0)] его левого верхнего угла. Новое окно размером с целый экран создается вызовом newwin (0, 0, 0, 0). newpad() возвраща- ет указатель на новое спецокно. delwin() удаляет указанное окно (спецокно), освобождая при этом всю связанную с ним память. 4.2.8. Работа с несколькими терминалами сразу Curses позволяет выводить данные сразу на несколько терминалов. К сожалению, подпрограммы curses не решают всех возникающих при этом проблем. Например, сами прикладные программы должны опре- делять типы терминалов. Стандартный способ с использованием пе- ременной окружения $TERM не срабатывает, поскольку каждый про цесс может знать только о своем собственном окружении. Концепция работы curses со многими терминалами заключается в поддержании текущего терминала. Все обращения к подпрограммам относятся к этому текущему терминалу. Когда программе нужно ра- ботать с некоторым терминалом, ей следует установить его в ка- честве текущего и вызывать обычные подпрограммы curses. Ссылки на терминал в curses-программе имеют тип SCREEN*. Новый терминал инициализируется путем обращения к newterm (type, outfd, infd). newterm возвращает указатель на новый терминал, type - это цепочка символов, содержащая тип используемого тер- минала. outfd имеет тип FILE* и является указателем на файл [stdio(3S)], который используется для вывода на терминал, а infd указывает на файл для ввода с терминала. Вызов newterm() заменяет обычное обращение к initscr(), которое расширяется в newterm (getenv ("TERM"), stdout, stdin). Для изменения текуще- го терминала необходимо вызвать set_term (sp), где sp указывает на терминал, который должен стать текущим. Каждый терминал будет иметь свои собственные режимы работы окна. Он должен быть проинициализирован соответствующим вызовом newterm(), для него отдельным образом должны быть установлены режимы работы, например, cbreak() или noecho(). Для каждого терминала отдельно должны вызываться endwin() и refresh(). Ниже приведен простейший пример двухтерминальной curses-прог- раммы. Эта программа выводит текст сообщения на терминал, с ко- торого она была вызвана, а также на терминал, имя и тип которо го указывается в командной строке. |#include |SCREEN *me, *you; |SCREEN *set_term (); |FILE *fdyou; |main (argc, argv) |int argc; |char **argv; |{ | char *getenv (); | int c; | if (argc != 4) { | fprintf (stderr, "usage: %s oterm otype message\n", | argv [0]); | exit (1); | } | fdyou = fopen (argv [1], "w+"); | me = newterm (getenv ("TERM"), stdout, stdin); | /* Инициализация своего терминала */ | you = newterm (argv [2], fdyou, fdyou); | /* Инициализация второго терминала */ | set_term (me); /* Устанавливаем режимы терминалов */ | noecho (); /* Отменяем эхо */ | cbreak (); /* Включаем cbreak */ | set_term (you); /* Другой терминал */ | noecho (); /* Отменяем эхо */ | cbreak (); /* Включаем cbreak */ | /* Выдаем сообщение на оба терминала */ | set_term (me); | mvaddstr (0, 0, argv[3]); | refresh (); | set_term (you); | mvaddstr (0, 0, argv[3]); | refresh (); | sleep(10); /* Немного подождем */ | /* Очищаем первый терминал */ | set_term (me); | clear (); /* Все обновляем */ | refresh (); | endwin (); /* Для выхода из curses */ | /* Очищаем второй терминал */ | set_term (you); | clear (); /* Все обновляем */ | refresh (); | endwin (); /* Для выхода из curses */ | exit (0); |} 4.3. Использование подпрограмм пакета terminfo Иногда могут понадобиться подпрограммы более низкого уровня, чем те, которые предлагает пакет curses. Такие подпрограммы со- держатся в пакете terminfo. Они не работают непосредственно с экраном терминала, а дают пользователю возможность узнать зна- чения характеристик терминала и последовательности символов, которые терминалом управляют. Подпрограммы terminfo полезно использовать в случаях, когда Вам нужны только некоторые возможности терминала, либо если Вы пи- шете программу для посылки специальной строки на терминал, нап- ример, для программирования функциональной клавиатуры, установ- ки позиций табуляции и т.д. В остальных случаях подпрограмм terminfo лучше избегать: использование подпрограмм уровня cur- ses повысит степень переносимости программ. При работе с подп- рограммами уровня terminfo прикладной программе, в частности, придется самостоятельно решать проблему вывода данных на экран. В состав пакета terminfo, входят, в частности, такие функции, как setupterm() (инициализация терминала), reset_shell_mode() (восстановление характеристик терминала), putp() (вывод цепочки символов, реализующей требуемую характеристику терминала). Полное описание пакета terminfo содержится в статье curses(3X) Справочника программиста. 4.4. Использование базы данных terminfo База данных terminfo содержит описания многих терминалов, с ко торыми может работать как пакет curses, так и некоторые другие средства системы UNIX. Каждое описание терминала представляет собой скомпилированный файл, содержащий название терминала и перечень его возможных действий и характеристик. Если Вы желаете написать curses-программу, предназначенную для выполнения на терминале, который еще не описан, Вам придется создать его описание самостоятельно. Для этого необходимо: ука- зать известные Вам названия терминала, выяснить и перечислить характеристики терминала, компилировать описание терминала, тестировать это описание. В качестве примера мы построим описа- ние некоторого гипотетического терминала myterm. Название терминала - это информация, с которой начинается его описание в terminfo. Название может включать несколько имен, разделенных вертикальной чертой (|). Первое имя - это наиболее часто используемое сокращение. Последним надо указать имя, пол- ностью идентифицирующее терминал (обычно это название, которым пользуется фирма-производитель). Остальные имена - известные синонимы имени терминала. Название заканчивается запятой. Приведем название из описания терминала TATUNG ET10: |d211|TATUNG ET10 in D211 mode, Для нашего условного терминала myterm опишем название так: |myterm|mytm|mine|fancy|terminal|My FANCY Terminal, Чтобы правильно описать новый терминал, следует выяснить значе- ния его характеристик. Для этого надо изучить руководство по работе с ним. В руководстве должна содержаться информация о возможностях терминала и соответствующих им последовательностях управляющих символов. Описание терминала содержит последовательность разделенных за- пятыми полей, состоящих из принятого в terminfo сокращения наз- вания характеристики и, возможно, ее значения для данного тер- минала. Характеристики бывают трех типов: булевы, означающие что терминал имеет соответствующую возможность, числовые, обыч- но используемые для задания размеров, и символьные, задающие последовательность кодов, которую нужно послать на терминал, чтобы выполнить соответствующее действие. Описание характеристик терминала D211 выглядит следующим обра- зом: |am, bw, xon, |cols#80, lines#24, |acsc=+$\,%.'\s&j)k#l"m(n5q-t_u^?v8w+x!~;, bel=^G, |blink=^C^N, civis=^^FQ0, clear=\f, cnorm=^^FQ1, cr=\r, |cub1=^Y, cud1=^Z, cuf1=^X, cup=^P%p2%c%p1%c, cuu1=^W, |cvvis=^^FQ2, dim=^\, el=^K, home=\b, ht=\t, kHOM=^^\b, |kLFT=^^^Y, kRIT=^^^X, ka1=^^\\, ka3=^^], kc1=^^\^, |kc3=^^_, kclr=\f, kcub1=^Y, kcud1=^Z, kcuf1=^X, |kcuu1=^W, kel=^K, kf1=^^q, kf10=^^z, kf11=^^{, |kf12=^^|, kf13=^^}, kf14=^^~, kf15=^^p, kf2=^^r, |kf3=^^s, kf4=^^t, kf5=^^u, kf6=^^v, kf7=^^w, kf8=^^x, |kf9=^^y, khome=\b, nel=\n, rev=^V, rmacs=^^O, rmul=^U, |sgr0=^B^U^O^]^^O, smacs=^^N, smul=^T, Каждая описываемая характеристика указана по имени. Например, то, что терминал D211 имеет автоматические границы (т.е. при достижении конца строки автоматически выполняется возврат ка ретки и перевод строки), отражается булевой характеристикой am. За числовыми характеристиками следует знак # и числовое значе- ние.Для терминала D211 cols#80 задает число колонок, равное 80. Наконец, символьные характеристики, например, bel (код для по- дачи звукового сигнала), задаются именем, за которыми следует знак = и цепочка символов, завершающаяся запятой. На терминале D211 кодом, передаваемым для подачи звукового сигнала, является CTRL+G. Поэтому характеристика данной возможности описывается полем bel=^G,. Для удобства записи управляющих последовательностей применяется ряд соглашений. Последовательности \E и \e обозначают символ ESCAPE (код ASCII 033), ^x обозначает CTRL+x для всех допусти- мых x, а последовательности \n, \l, \r, \t, \b, \f и \s предс- тавляют соответственно перевод строки, переход к новой строке, возврат каретки, горизонтальную табуляцию, возврат на шаг, пе- реход к новой странице и пробел. Символы можно задавать трехз- начным восьмеричным числом после знака \ (например, \123). Теперь, когда мы узнали, как описываются характеристики терми- нала, опишем их для myterm. Следующие характеристики являются общими для большинства терми- налов (в скобках после каждой характеристики следует ее имя для terminfo): Автоматический переход в начало следующей строки, когда курсор достигает правой границы (am). Способность подавать звуковой сигнал. Командой подачи сигнала является CTRL+G (bel). Ширина экрана - 80 колонок. (cols). Высота экрана - 30 строк. (lines). Используется протокол xon/xoff (xon). Добавим к описанию характеристики, управляющие содержимым экра- на (мы считаем, что терминал myterm имеет перечисленные ниже возможности): Возврат каретки - CTRL+M (cr). Перемещение курсора на строку вверх - CTRL+K (cuu1). Перемещение курсора на строку вниз - CTRL+J (cud1). Перемещение курсора на позицию влево - CTRL+H (cub1). Перемещение курсора на позицию вправо - CTRL+L (cuf1). Переход к инвертированному отображению - ESC D (smso). Отмена инвертированного отображения - ESC Z (rmso). Очистка конца строки - ESC K (el). Еще одна группа характеристик представляет собой последователь- ности символов, генерируемые при нажатии клавиш на клавиатуре. Пусть наш гипотетический терминал имеет несколько таких клавиш: Забой - CTRL+H (kbs). Стрелка вверх - ESC [A (kcuu1). Стрелка вниз - ESC [B (kcud1 Стрелка вправо - ESC [C (kcuf1). Стрелка влево - ESC [D (kcub1). Описание терминала myterm, включающее все упомянутые характе- ристики, будет выглядеть так: |myterm|mytm|mine|fancy|terminal|My FANCY Terminal, | am, bel=^G, cols#80, lines#30, xon, | cr=^M, cuu1=^K, cud1=^G, cub1=^H, cuf1=^L, | smso=\ED, rmso=\EZ, el=\EK, kbs=^H, | kcuu1=\E[A, kcud1=\E[B, kcuf1=\E[C, kcub1=\E[D, Подчеркнем, что для нормальной работы с терминалом curses-прог- рамма требует, чтобы его описание включало, как минимум, харак теристики, касающиеся перемещения курсора и очистки экрана. Описания терминалов в базе данных terminfo переводятся из ис- ходного в компилированный формат компилятором tic(1M). Пусть, например, описание myterm будет называться myterm.ti. Скомпили- рованное описание myterm обычно помещается в /usr/lib/termin- fo/m/myterm (по первой букве названия терминала). Приведенная ниже командная строка задает компиляцию описания для нашего ги- потетического терминала (опция -v требует вывода трассировочной информации о ходе компиляции): |tic -v myterm.ti Для отладки описания можно воспользоваться командой tput(1). Эта команда выводит значение характеристики - числовое или текстовое, в зависимости от ее типа. Формат команды tput таков: |tput [-T тип] характеристика Опция -Tтип задает тип терминала, о котором вы запрашиваете ин- формацию. По умолчанию в качестве имени терминала берется зна- чение переменной окружения $TERM. Например, командная строка: |tput clear выводит последовательность для очистки экрана. Следующая ко- мандная строка выводит число колонок на экране: |tput cols Чтобы тестировать описание терминала, можно попытаться выпол- нить на нем какую-нибудь интерактивную программу, использующую разнообразные возможности управления термналом, например, ре- дактор vi. Если программа выполняется на новом терминале так же, как и на других терминалах, новое описание пригодно для ис- пользования. Иногда бывает необходимо просмотреть исходный текст описания терминала. Для этого можно воспользоваться infocmp(1M). Чтобы получить исходный текст описания terminfo для d211, введите |infocmp -I d211 5. УТИЛИТА MAKE Реализация практически каждой программной системы состоит из достаточно большого числа исходных файлов. Для того, чтобы сформировать из них конечный продукт - программу, - выполняются разнообразные порождающие процедуры. Всякий раз, когда модифи- цированы некоторые из исходных файлов, часть шагов порождения (какую именно - зависит от внесенных изменений) приходится пов торять. Утилита make(1) автоматизирует процесс получения свежих версий программ. 5.1. Основные возможности Использование make позволяет определить все зависимости между файлами, составляющими целевую программу, воздействие модифика ций одних файлов на другие, точную последовательность действий, необходимых для порождения новой версии программы. Эта информа- ция с помощью текстового редактора помещается в файл с описани- ями (по умолчанию его имя makefile или Makefile). Опираясь на содержимое файла с описаниями, make определяет, какие команды следует передать shell'у для выполнения, чтобы гарантировать автоматическое формирование целевой программы. Алгоритм, лежащий в основе make, в общих чертах выглядит так. В файле с описаниями находится целевой файл. Проверяются все фай- лы, от которых зависит целевой: если какой-либо файл не сущест- вует либо устарел, он порождается заново (проверка сопровожда- ется просмотром описаний, при котором этот файл в свою очередь трактуется как целевой). Наконец, если какой-либо из этих фай- лов модифицирован позже целевого, последний пересоздается. По добный рекурсивный просмотр зависимостей позволяет при формиро- вании новой версии программы порождать заново только те файлы, которые прямо или косвенно затронуты последними изменениями. Рассмотрим простейший пример. Программа prog получается из трех исходных файлов x.c, y.c и z.c путем их компиляции и редактиро- вания связей совместно с библиотекой math. В соответствии с принятыми соглашениями результат работы C-компилятора будет по- мещен в файлы с именами x.o, y.o и z.o. Предположим также, что файлы x.c и y.c используют общие описания из включаемого файла defs.h, а z.c - не использует. Названные взаимосвязи и требуе мые команды описываются так: |prog : x.o y.o z.o | cc x.o y.o z.o -lm -o prog |x.o : x.c defs.h | cc -c x.c |y.o : y.c defs.h | cc -c y.c |z.o : z.c | cc -c z.c В первой строке утверждается, что prog зависит от трех .o- лов. Вторая строка указывает, как отредактировать связи между ними, чтобы создать prog. Третья строка гласит, что x.o зависит от x.c и defs.h; четвертая описывает, как получить файл x.o если он отсутствует или устарел. Сходный смысл имеют и последу- ющие строки. Если эту информацию поместить в файл с именем makefile, команда |make будет выполнять операции, необходимые для перегенерации prog после любых изменений, сделанных в каком-либо из четырех исход ных файлов x.c, y.c, z.c или defs.h. Если ни один из исходных или объектных файлов не был изменен с момента последнего порождения prog и все файлы в наличии, ути- лита make известит об этом и прекратит работу. Если, однако, файл defs.h отредактировать, x.c и y.c (но не z.c) будут пере- компилированы; затем из новых файлов x.o и y.o и уже существую- щего файла z.o будет заново собрана программа prog. Если изме- нен лишь файл y.c, только он и перекомпилируется, после чего последует пересборка prog. Перед выполнением всякой команды утилита make распечатывает ее. Если изменен файл defs.h, будет распечатано следующее: |cc -c x.c |cc -c y.c |cc x.o y.o z.o -lm -o prog Если в командной строке make не задано имя целевого файла, соз- дается первый упомянутый в описании целевой файл (в нашем слу- чае - файл prog); в противном случае создаются специфицирован- ные целевые файлы. Команда |make x.o будет перегенерировать x.o, если изменен x.c или defs.h. Для того, чтобы упростить формирование файлов описаний, make использует "подразумеваемые" трансформации. Так, например, если для порождения целевого файла требуется некоторый объектный файл (.o), а в текущем каталоге имеется соответствующий ему ис ходный C-файл (файл с тем же именем и с расширением .c), make применяет встроенное правило порождения объектного файла из ис- ходного (то есть выполняет команду cc -c). Приведенный make-файл можно поэтому переписать следующим обра- зом |prog : x.o y.o z.o | cc x.o y.o z.o -lm -o prog |x.o y.o : defs.h Список суффиксов, которые распознаются make'ом и которым соот ветствуют подразумеваемые правила трансформации, выглядит так: .o (объектный файл), .c (C-файл), .f (f77-файл), .s (файл на языке ассемблера), .y (yacc-грамматика), .l (lex-спецификация), .h (включаемый файл), .sh (командный файл). 5.2. Структура make-файлов Общий вид точки входа в файле описаний таков: |цель1 [цель2..] : [зависимость1..] [; команды ] [#..] |[\t команды] [#..] Указанные в скобках компоненты могут быть опущены, цели и зави- симости, которые задают имена файлов, являются цепочками из букв, цифр, символов . и /. При обработке строки интерпретиру- ются метасимволы shell'а, такие как * и ?. Команды могут быть указаны после точки с запятой в строке зависимостей, или в строках, начинающихся с табуляции, которые следуют сразу за строкой зависимостей. Признаком комментария является символ #; все символы за ним до конца строки игнорируются. Слишком длинную строку, не являющуюся комментарием, можно про должить, используя знак \. Если последним символом строки явля- ется символ \, то он, а также перевод строки и все следующие з ним пробелы и табуляции заменяются на одиночный пробел. 5.3. Макросы В файлах описаний предусмотрен механизм макросов. Макроопреде- ление состоит из имени (цепочка букв и/или цифр), за которым следует знак равенства, а затем цепочка символов, сопоставляе- мая имени. Примеры корректных макроопределений: |2 = xyz |abc = -ll -ly -lm |LIBES = Последнее определение сопоставляет LIBES с пустой цепочкой. Макрос, нигде не определенный явным образом, имеет в качестве значения пустую цепочку. Макроопределения можно не только включать в файл описаний, но и задавать аргументами командной строки, например: |make abc="-ll -ly -lm" При обращении к макросу перед его именем указывается знак $. Имена макросов, состоящие более чем из одного символа, должны заключаться в скобки. Примеры корректных обращений к макросам (две последние строки эквивалентны): |$(LIBES) |$(abc) |$2 |$(2) Некоторые макроопределения являются встроенными, например: |# Предопределенные макросы |MAKE=make |CC=cc |CFLAGS=-O |LD=ld |LDFLAGS= |YACC=yacc |YFLAGS= Следующий фрагмент демонстрирует все упомянутые разновидности макросов: |OBJECTS = x.o y.o z.o |prog: $(OBJECTS) | $(CC) $(OBJECTS) $(LIBES) -o prog | . . . Команда |make LIBES="-ll -lm" загружает три объектных файла вместе с библиотекой lex'а (-ll) и математической библиотекой (-lm). $*, $@, $?, $< - это четыре специальных встроенных макроса, значения которых изменяются во время выполнения команды. Перед вызовом любой команды устанавливаются некоторые встроенные мак- росы. Макрос $@ устанавливается равным полному имени текущего целевого файла, а макрос $? - цепочке имен файлов, которые ока- зались более свежими, чем целевой. Пример: |prog: x.c y.c z.c | cc -c $? | cc -o $@ x.o y.o z.o Макросы $< и $* используются при определении неявных правил трансформации (см. далее). Приведенное выше обращение к макросу вида $(макро) является частным случаем более мощной конструкции. В общем случае обра- щение к макросу выглядит так: |$(макро:цепочка1=цепочка2) Подобная конструкция преобразуется следующим образом. Значение $(макро) рассматривается как набор разделенных пробелами или табуляциями цепочек символов, оканчивающихся подцепочкой цепоч- ка1; при подстановке все вхождения цепочки1 заменяются на це- почку2. Такая форма преобразований макросов была выбрана из-за того, что make обычно имеет дело с окончаниями имен файлов. В следующей команде | $(CC) -c $(CFLAGS) $(?:.o=.c) значение макроса $? - цепочка имен объектных файлов, оказавщих- ся более свежими, чем целевой. Допустим: |$? = x.o y.o Тогда обращение $(?:.o=.c) транслируется в |x.c y.c 5.4. Суффиксы и правила трансформации Как уже упоминалось, make использует таблицу встроенных правил, определяющих, как преобразовать файл с одним суффиксом в файл с другим суффиксом. Имена правил трансформации - это просто конкатенации суффиксов файлов до и после трансформации. Так, правило трансформации .c файла в .o-файл называется .c.o. Если в пользовательских файлах описаний не задано явной последовательности команд, использует- ся последовательность команд, соответствующая правилу .c.o Пользователь может переопределить встроенное правило трансфор- мации. Если команда порождается при помощи одного из таких пра- вил, посредством макроса $* можно вычислить префикс имени целе- вого файла; макрос $< обозначает полное имя файла из строки за- висимостей, вызвавшего выполнение действия. Например, встроен ное правило, используемое для поддержания архивных библиотек, выглядит так: |.c.a: | $(CC) -c $(CFLAGS) $< | $(AR) $(ARFLAGS) $@ $*.o | rm -f $*.o Здесь $< обозначает имя c.-файла, соответствующий которому эле- мент библиотеки (объектный файл) устарел; значение макроса $@ в данном случае равно имени библиотеки. 5.5. Пример: библиотека libio В качестве иллюстрации возможностей утилиты make рассмотрим файл описаний /usr/src/uts/io/Makefile, предназначенный для поддержания архивной библиотеки libio (драйверы внешних уст- ройств). В данном файле содержится переопределение встроенного правила .c.a. Используемая конструкция вида |библиотека(файл.o) обозначает обращение к элементу библиотеки. |LIBNAME = ../libio |INCRT = /usr/include |CFLAGS = -c -O -Dm68k -I$(INCRT) |FILES =\ | $(LIBNAME)(clk.o)\ | $(LIBNAME)(smd.o)\ | $(LIBNAME)(smqic.o)\ | $(LIBNAME)(sio25.o)\ | $(LIBNAME)(rd.o)\ | $(LIBNAME)(isio.o)\ | $(LIBNAME)(iscsi.o)\ | $(LIBNAME)(mpcc.o)\ | $(LIBNAME)(pit.o)\ | $(LIBNAME)(rr2.o)\ | $(LIBNAME)(gpib.o)\ | $(LIBNAME)(agc.o) |all: $(FILES) |.c.a: | $(CC) $(CFLAGS) $< | chmod 664 $*.o | chgrp bin $*.o | chown bin $*.o | ar rv $@ $*.o | -rm -f $*.o | ... |$(LIBNAME)(isio.o):isio.c | $(INCRT)/sys/param.h\ | $(INCRT)/sys/types.h\ | $(INCRT)/sys/dir.h\ | $(INCRT)/sys/file.h\ | $(INCRT)/sys/signal.h\ | $(INCRT)/sys/tty.h\ | $(INCRT)/sys/user.h\ | $(INCRT)/sys/errno.h\ | $(INCRT)/sys/termio.h\ | $(INCRT)/sys/conf.h\ | $(INCRT)/sys/sysinfo.h\ | $(INCRT)/sys/Port.h\ | $(INCRT)/sys/tstmode.h | ... |$(LIBNAME)(rd.o):rd.c\ | $(INCRT)/sys/param.h\ | $(INCRT)/sys/types.h\ | $(INCRT)/sys/dir.h\ | $(INCRT)/sys/file.h\ | $(INCRT)/sys/signal.h\ | $(INCRT)/sys/tty.h\ | $(INCRT)/sys/user.h\ | $(INCRT)/sys/errno.h\ | $(INCRT)/sys/termio.h\ | $(INCRT)/sys/conf.h\ | $(INCRT)/sys/sysinfo.h\ | $(INCRT)/sys/Port | $(INCRT)/sys/tstmode.h | ... 5.6. Запуск утилиты make Командная строка, запускающая утилиту make, выглядит следующим образом: |make [опции] [макроопределения] [целевые_файлы] Полный перечень допустимых опций приводится в статье make(1) Справочника Пользователя. Мы назовем только часть из них. Чтобы понять, какие действия будет делать make, очень удобно использовать опцию -n. Обращение |make -n предписывает выводить команды, которые make должен вычислять, не выполняя их на самом деле. Обращение |make -s напротив, подавляет вывод командных строк перед их выполнением. Если абсолютно точно известно, что изменение в файле касается только его внешнего представления (например, вставка коммента- рия во включаемый файл), использование опции -t может сэконо- мить уйму времени. Вместо запуска большого числа ненужных пере- компиляций make просто обновит времена изменения затронутых файлов: make -ts Опцию -t следует использовать крайне осторожно, поскольку такой режим обработки разрушает всю информацию о более ранних измене- ниях. make обычно прекращает работу, если какая-либо команда возвра- щает ненулевой код завершения (чаще всего это признак ошибки). Чтобы ошибки игнорировались, в командной строке make'а можно указать опцию -i. Ошибка игнорируется также, если командная строка в файле описаний начинается со знака минус. Если извест- но, что программа возвращает бессодержательное значение, перед строкой, ее запускающей, полезно указывать минус. Иногда бывает полезно указать опцию -k. В этом случае при воз- никновении ошибки выполнение команд, связанных с текущей зави- симостью, прекращается, однако обработка других зависимостей продолжается. Опция -f имя_файла задает имя файла описаний: |make -f my.mk Имя файла - обозначает стандартный ввод. Если опция не указана, читается файл с именем makefile или Makefile. 5.7. Резюме Использование утилиты make для управления версиями программ, даже самых небольших, является весьма характерной чертой прог- раммирования в ОС UNIX. Рекомендуем не отходить от этой тради ции. Имеется еще одна причина овладеть make'ом: make используетс для перегенерации операционной системы (файл описаний, управля ющий процессом перегенерации, - /usr/src/uts/Makefile).