Описания структур и объединений
Структура есть объект, состоящий из последовательности именованных элементов. Любой элемент может иметь произвольный тип. Объединение есть объект, который может в каждый момент времени содержать один из нескольких элементов. Спецификаторы структур и объединений имеют одинаковый вид:
спецификатор_структуры_или_объединения: struct { список_структ_описаний }
struct идентификатор { список_структ_описаний }
struct идентификатор union { список_структ_описаний }
union идентификатор { список_структ_описаний }
union идентификатор
Список_структ_описаний - это последовательность описаний элементов структуры или объединения:
список_структ_описаний: структ_описание структ_описание список_структ_описаний
структ_описание: спецификатор_типа список_структ_описателей ;
список_структ_описателей: структ_описатель структ_описатель , список_структ_описателей
Чаще всего структ_описатель - это именно описатель элемента структуры или объединения. Кроме того, элемент структуры может содержать набор бит заданной длины. Такой элемент называют битным полем; его длина, неотрицательное константное_выражение, отделяется от имени поля двоеточием.
структ_описатель: описатель описатель : константное_выражение : константное_выражение
В пределах структуры описываемые объекты имеют адреса, возрастающие соответственно порядку описаний. Начало каждого элемента структуры, не являющегося битным полем, выравнивается в соответствии с его типом; поэтому в структуре могут быть и неименованные промежутки. Битные поля упаковываются в стандартные для данной машины слова, хранящие объекты типа int. Поля не могут пересекать границ слов; отсюда следует, что поле не может быть длиннее слова. Если в текущем слове после размещения предыдущих полей осталось слишком мало места, очередное поле будет храниться, начиная с границы следующего слова.
Структ_описатель без описателя, состоящий только из двоеточия и константного_выражения, обозначает неименованное битное поле; его удобно использовать как заполнитель для согласования с установленным извне расположением данных. Как особый случай, битное поле длины 0 специфицирует выравнивание следующего поля в соответствии с реализационно-зависимыми границами.
Язык не накладывает ограничений на типы объектов, описываемых как битные поля. Следует учитывать, что поля типа int будут рассматриваться как беззнаковые, если их длина меньше слова. По этой причине настоятельно рекомендуется явно описывать подобные поля как unsigned, чтобы текст программы соответствовал реальному положению вещей. Не бывает массивов битных полей, к ним нельзя применять операцию вычисления адреса, &, поэтому не бывает и указателей на битные поля.
Объединение можно представлять себе как структуру, все элементы которой располагаются с одним и тем же отступом 0 от начала и размер которой достаточен, чтобы вместить любой из элементов. В каждый момент времени в объединении может храниться единственный элемент.
Спецификатор_структуры_или_объединения во второй форме, то есть
struct идентификатор { список_структ_описаний }
union идентификатор { список_структ_описаний }
описывает идентификатор, являющийся тегом структуры (тегом объединения), специфицируемой списком_структ_описаний. Последующие описания могут затем использовать третью форму спецификатора:
struct идентификатор union идентификатор
Теги структур позволяют описывать структуры, ссылающиеся сами на себя. Они позволяют также задать длинную часть описания однажды, а использовать многократно. Недопустимо описывать структуры или объединения, содержащие экземпляры самих себя; однако структура или объединение может содержать указатель на экземпляр данного типа.
Третью форму спецификатора_структуры_или_объединения можно использовать перед описанием, дающим полную спецификацию структуры или объединения в случаях, когда не требуется знать размер структуры или объединения. Размер не важен в двух ситуациях: когда описывается указатель на структуру или объединение и когда при помощи конструкции typedef описывается имя, являющееся синонимом структуры или объединения. Тем самым появляется возможность описать, например, пару структур, содержащих указатели друг на друга.
Имена элементов и теги не конфликтуют между собой и с именами обычных переменных. Одно и то же имя не может дважды использоваться в одной структуре, однако в пределах одной области видимости в нескольких разных структурах можно использовать одинаковые имена элементов.
Следующая спецификация, задающая бинарное дерево, является несложным, но важным примером описания структуры:
struct tnode { char tword [20]; int count; struct tnode *left; struct tnode *right; };
Структура содержит массив из 20 символов, целое число и два указателя на аналогичные структуры. Затем с помощью конструкции
struct tnode s, *sp;
можно описать объект s - структуру данного сорта и sp - указатель на структуру данного сорта. В соответствии с этими описаниями выражение
sp->count
обозначает поле count структуры, на которую указывает sp;
s.left
есть указатель на левое поддерево в структуре s;
s.right->tword [0]
есть первый символ поля tword в правом поддереве s.
Описатели
Описание может включать список описателей данных, разделенных запятыми; описателям могут быть сопоставлены инициализаторы.
список_описателей_данных: описатель_данных описатель_данных , список_описателей_данных
описатель_данных: описатель инициализатор
Инициализаторы обсуждаются в пункте Инициализация. Спецификаторы в описании указывают класс памяти и тип объектов, на которые ссылаются при помощи описателей. Описатели имеют следующий синтаксис:
описатель: идентификатор ( описатель )
* описатель описатель ( )
описатель [ константное_выражение ]
Группировка в описателях происходит так же, как и в выражениях.
Определение содержимого библиотеки
Самая важная часть процесса создания разделяемой библиотеки - определение ее содержимого. Некоторые подпрограммы являются очевидными кандидатами для разделяемого использования, другие же - нет. Следует включать в разделяемую библиотеку большие и часто используемые подпрограммы, в отличие от маленьких и редко вызываемых. Наполнение библиотеки определяется конкретными потребностями программистов и других пользователей, для которых библиотека разрабатывается. Впрочем, есть несколько общих принципов, которым целесообразно следовать. Они обсуждаются далее в разделе Какие функции целесообразно включать в библиотеку? См. также разделы Использование импортируемых имен и Настройка разделяемой библиотеки.
Определение точки входа
Вспомогательный заголовок объектных файлов обычного формата, применяемый в ОС UNIX и имеющий структуру a.out, содержит поле для (основной) точки входа этого файла. Правила заполнения это- го поля редактором связей (в порядке их применения) таковы:
Берется значение имени, заданного в опции -e, если эта опция используется.
Используется значение имени _start, если это имя определено.
Используется значение имени main, если это имя определено.
В остальных случаях поле получает нулевое значение.
Таким образом, можно задать значение точки входа, используя опцию -e или предложение управляющего языка вида
_start = выражение
Когда редактор связей вызывается на выполнение командой cc(1), программа пользователя объединяется с инициализирующей программой. Эта последняя после обращения к программе пользователя выполняет системный вызов exit [см. exit(2)], чтобы закрыть файлы и осуществить другие терминирующие действия. Если пользователь вызывает редактор связей сам и/или изменяет точку входа, он должен гарантировать, что программа завершает выполнение системным вызовом exit.
ОПРЕДЕЛЕНИЯ И СОГЛАШЕНИЯ
Дадим определения и оговорим соглашения, которые будут использоваться в дальнейшем изложении.
Определяемые типы
Описания, содержащие "спецификатор класса памяти" typedef, не вызывают отведение памяти, но определяют идентификаторы, которые можно в дальнейшем использовать наравне с ключевыми словами, обозначающими базовые или производные типы.
имя_определяемого_типа: идентификатор
В пределах области видимости описания, содержащего typedef, каждый введенный с его помощью идентификатор становится синтаксически эквивалентным ключевому слову, обозначающему тип, сопоставленный идентификатору по правилам, описанным в пункте Смысл описателей. Например, после описаний
typedef int MILES, *KLICKSP; typedef struct { double re,im; } complex;
конструкции
MILES distance; extern KLICKSP metricp; complex z, *zp;
являются корректными описаниями; тип distance есть int, тип metricp - "указатель на int", тип z - специфицированная структура. zp - указатель на такую структуру.
Конструкция typedef не вводит совершенно новых типов, а только синонимы типов, которые в принципе можно специфицировать и другим способом. В приведенном выше примере можно считать, что distance имеет в точности такой же тип, как и любой другой объект int.
Организация Руководства
Весь материал Руководства разбит на главы следующим образом:
Глава 1 - Программирование в ОС UNIX, обзор.
Описываются черты ОС UNIX, формирующие программное окружение. Имеется в виду концепция программирования, предполагающая максимальное использование готовых программ, а также каналы, специальные файлы, программирование на языке shell и т.д. Как основа для последующего материала определяются три различных уровня программирования, представляемых программистами-одиночками, прикладными программистами и системными программистами.
Глава 2 - Основы программирования.
Описываются основные утилиты, необходимые для получения выполняемых программ.
Последующие главы - средства поддержки разработок, различные описания и руководства.
Эти главы содержит подробную информацию об использовании многих инструментальных средств ОС UNIX.
Связь с языком C
Несмотря на то, что ОС UNIX поддерживает многие языки программирования, а C-компиляторы имеются и в других операционных системах, связь между языком C и ОС UNIX остается очень тесной. Действительно, большая часть самой операционной системы написана на C, а во многих организациях, использующих ОС UNIX, увеличивается доля программ, разрабатываемых на C. Поэтому, несмотря на то, что данное Руководство предназначается программистам независимо от используемого ими языка, все примеры приведены на C, за исключением, естественно, тех случаев, когда обсуждаются вопросы, связанные с другими языками.
Основные элементы правил lex'а
Обязательная секция правил открывается разделителем %%. Если за ней следует секция подпрограмм, секцию правил закрывает еще один разделитель %%. Если второго разделителя нет, весь текст до конца программы трактуется как секция правил.
Каждое правило состоит из спецификации искомого шаблона и действия (или действий), выполняемого, если шаблон найден. Действие должно быть отделено от шаблона одним или несколькими пробелами. (Отметим двойственный смысл термина спецификация - он может означать и весь исходный lex-текст, и, внутри него, представление конкретного распознаваемого шаблона). Входной текст, не содержащий искомых шаблонов, выводится lex'ом без изменений. Поэтому простейшая lex-программа - это открывающий секцию правил разделитель %%. Такая программа выводит входной текст целиком без всяких изменений. В типичных случаях правила, разумеется, имеют более сложный вид.
ОСНОВНЫЕ ПОНЯТИЯ
В статье ld(1) Справочника пользователя перечислены опции командной строки редактора связей, некоторые из которых можно указывать также при вызове C-компилятора cc(1). В настоящей публикации рассматривается управляющий язык редактора связей.
Управляющий язык редактора связей предоставляет следующие возможности:
Описание конфигурации памяти целевого компьютера.
Объединение секций объектного файла в порядке, отличном от подразумеваемого.
Назначение для секций определенных адресов или диапазона адресов памяти.
Определение или переопределение глобальных имен.
При обычных обстоятельствах нет надобности в жестком контроле за объектными файлами и их последующим расположением в памяти. Управляющий язык используется, когда все же необходимо детально контролировать ход и результат работы редактора связей.
Предложения управляющего языка редактора связей помещаются в файл, имя которого указывается в командной строке ld(1). Если файл указан в командной строке и не опознан в качестве объектного модуля или библиотеки, то предполагается, что он содержит предложения управляющего языка.
В следующих разделах определяются основные понятия, знакомство с которыми необходимо для использования управляющего языка.
ОСНОВНЫЕ СПЕЦИФИКАЦИИ
Имена обозначают лексемы или нетерминальные символы. yacc требует, чтобы имена лексем были указаны явно. Хотя лексический анализатор можно включить в файл спецификаций, определение его в отдельном файле, вероятно, более соответствует принципам модульного проектирования. Подобно лексическому анализатору, в файл спецификаций могут быть также включены и другие подпрограммы. Таким образом, каждый файл спецификаций теоретически состоит из трех секций: определений, (грамматических) правил и подпрограмм. Секции отделяются двумя знаками процента %% (знак % используется в yacc-спецификациях как универсальный управляющий).
Если используются все секции, полный файл спецификаций выглядит следующим образом:
определения
%% правила
%% подпрограммы
Секции определений и подпрограмм являются необязательными. Минимальная допустимая yacc-спецификация - это
%% правила
Пробелы, табуляции и переводы строки, которые встречаются вне имен и зарезервированных слов, игнорируются. Комментарии могут быть везде, где допустимо имя. Они заключаются в "скобки" /*...*/, как в языке C.
Секция правил составляется из одного или большего числа грамматических правил. Грамматическое правило имеет вид:
нтс : тело ;
где нтс - нетерминальный символ, а тело - последовательность из нуля или нескольких имен и литералов. Двоеточие и точка с запятой - знаки препинания yacc'а.
Имена могут иметь произвольную длину и должны состоять из букв, точек, подчеркиваний и цифр, однако имя не может начинаться с цифры. Прописные и строчные буквы различаются. Имена, используемые в теле грамматического правила, могут представлять лексемы или нетерминальные символы.
Литерал - это знак, заключенный в одиночные кавычки, '. Как и в языке C, в качестве управляющего используется знак \, восприни-маются также все принятые в C управляющие последовательности. Например, yacc трактует перечисленные ниже последовательности следующим образом:
'\n' перевод строки
'\r' возврат каретки
'\'' одинарная кавычка ' '\\' обратная наклонная черта \ '\t' табуляция
'\b' забой
'\f' переход к новой странице
'\xxx' xxx в восьмеричной записи
По ряду технических причин символ NULL (\0 или 0) нельзя использовать в грамматических правилах.
Если есть несколько грамматических правил с одной и той же левой частью, то, чтобы избежать переписывания левой части, можно использовать вертикальную черту, |. Перед вертикальной чертой точку с запятой ставить не нужно. Например, грамматические правила
A : B C D ; A : E F ; A : G ;
с использованием вертикальной черты могут быть заданы для yacc'а в виде
A : B C D | E F | G ;
Не обязательно, чтобы грамматические правила с одной и той же левой частью появлялись в секции правил все вместе, однако такая запись облегчает чтение спецификации и ее модификацию.
Если нетерминал сопоставляется с пустой цепочкой, это можно выразить следующим образом:
epsilon : ;
yacc истолковывает пустое место после двоеточия как нетерминальный символ, называемый epsilon.
Имена, представляющие лексемы, должны быть описаны. Проще всего это сделать, поместив в секции определений конструкцию вида
%token имя1 имя2 ...
Считается, что всякое имя, не описанное в секции определений, представляет нетерминальный символ. Каждый нетерминальный символ должен встретиться в левой части по крайней мере одного правила.
Из всех нетерминальных символов особую роль играет начальный символ. По умолчанию начальным считается символ, стоящий в левой части первого грамматического правила в секции правил. Можно (и желательно) явно объявить начальный символ в секции определений при помощи ключевого слова %start:
%start начальный_символ
О конце входного текста процедуре разбора сигнализирует специальная лексема, называемая маркером конца. Маркер конца представляется нулем или отрицательным числом. Если прочитанные лексемы, исключая маркер конца, образуют конструкцию, сопоставляющуюся с начальным символом, то после того, как найден маркер конца и исходный текст принят, процедура разбора возвращает управление вызвавшей ее процедуре. Если маркер конца встречается в любом другом контексте, это ошибка.
Возвратить, когда следует, маркер конца, - задача определяемого пользователем лексического анализатора. Обычно маркер конца представляет некоторые вполне очевидные состояния ввода/вывода, такие как конец файла или конец записи.
ОСНОВНЫЕ ВОЗМОЖНОСТИ
Основное действие утилиты make - обновление целевого файла при условии, что все файлы, от которых зависит целевой, существуют и не являются устаревшими. Целевой файл порождается заново, если названные файлы модифицированы, а целевой - нет. Утилита make исследует граф зависимостей. Функционирование make'а основывается на анализе времени последней модификации файлов.
Утилита make действует, опираясь на три источника информации:
Заданный пользователем файл описаний.
Имена файлов и времена последней модификации, получае- мые от файловой системы.
Встроенные правила, позволяющие разрешить некоторые не- домолвки в файле описаний.
В качестве иллюстрации рассмотрим следующий простой пример. Программа prog получается из трех исходных файлов x.c, y.c и z.c путем их компиляции и редактированием связей совместно с библиотекой math. В соответствии с принятыми соглашениями результат работы C-компилятора будет помещен в файлы с именами x.o, y.o и z.o. Предположим также, что файлы x.c и y.c используют общие описания из включаемого файла defs.h, а z.c - не использует. Пусть x.c и y.c содержат строку
#include "defs.h"
Следующая спецификация описывает взаимосвязи и операции:
prog : x.o y.o z.o cc x.o y.o z.o -lm -o prog x.o y.o : defs.h
Если эту информацию поместить в файл с именем makefile, команда
make
будет выполнять операции, необходимые для перегенерации prog
после любых изменений, сделанных в каком-либо из четырех исходных файлов x.c, y.c, z.c или defs.h. В приведенном выше примере в первой строке утверждается, что prog зависит от трех .o-файлов. Если эти файлы имеются в наличии, рассматривается вторая строка, которая описывает, как отредактировать связи между ними, чтобы создать prog. Третья строка гласит, что x.o и y.o зависят от defs.h. Обратившись к файловой системе, make обнаруживает, что имеются три .c-файла, соответствующие требуемым .o- файлам, и применяет встроенные правила порождения объектного файла из исходного C-файла (то есть выполняет команду cc -c).
Если make не может автоматически определить действия, которые требуется выполнить, необходим следующий, более длинный файл описаний:
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 и все файлы в наличии, утилита make известит об этом и прекратит работу. Если, однако, файл defs.h отредактировать, x.c и y.c (но не z.c) будут перекомпилированы; затем из новых файлов x.o и y.o и уже существую- щего файла z.o будет заново собрана программа prog. Если изменен лишь файл y.c, только он и перекомпилируется, после чего последует пересборка prog. Если в командной строке make не за- дано имя цели, создается первый упомянутый в описании целевой файл; в противном случае создаются специфицированные целевые файлы. Команда
make x.o
будет перегенерировать x.o, если изменен x.c или defs.h.
Часто в файл описаний включают правила с мнемоническими именами и командами, которые в действительности не порождают файлов с соответствующими именами. Смысл в том, чтобы воспользоваться средствами make'а по генерации файлов и подстановке макросов (информацию о макросах см. в разделе ФАЙЛЫ ОПИСАНИЙ И ПОДСТАНОВКИ). Мнемонические имена играют роль точек входа, при обращении к которым выполняются определенные действия. Так, точка входа save может служить для копирования определенной совокупности файлов, а точка входа clean - для удаления ненужных промежуточных файлов.
Если после выполнения таких команд файл существует, то для того, чтобы принимать последующие решения, используется время его последней модификации; если же он не существует, используется текущее время.
Еще один полезный прием состоит в том, что при необходимости отслеживать только время выполнения определенных действий целесообразно завести пустой файл, который будет использоваться только как носитель времени последней модификации.
Утилита make использует простой механизм макросов для выполнения подстановок в строках зависимостей и цепочках команд. Макрос можно либо задать аргументами командной строки, либо включить в файл описаний. В обоих случаях макроопределение состоит из имени, за которым следует знак равенства (=), а затем то, что макрос обозначает. При обращении к макросу перед его именем указывается знак $. Имена макросов, состоящие более чем из одного символа, должны заключаться в скобки. Ниже приводятся примеры корректных обращений к макросам:
$(CFLAGS) $2 $(xy) $Z $(Z)
Две последние строки эквивалентны.
$*, $@, $?, $< - это четыре специальных макроса, значения которых изменяются во время выполнения команды. Они описываются в разделе ФАЙЛЫ ОПИСАНИЙ И ПОДСТАНОВКИ. В следующем фрагменте показаны определения и использования некоторых макросов:
OBJECTS = x.o y.o z.o LIBES = -lm prog: $(OBJECTS) cc $(OBJECTS) $(LIBES) -o prog . . .
Команда
make LIBES="-ll -lm"
загружает три объектных файла вместе с библиотекой lex'а (-ll) и математической библиотекой (-lm), потому что в первую очередь используются макроопределения из командной строки, а не одноименные определения в файле описаний. (В командах ОС UNIX аргументы, содержащие пробелы, должны заключаться в кавычки).
В качестве примера использования make'а приведем файл описаний, который может применяться при сопровождении самой утилиты make. Исходный текст утилиты содержится в нескольких C-файлах, а также включает yacc-спецификацию грамматики.
# Файл описаний для утилиты make
FILES = Makefile defs.h main.h doname.c misc.c files.c dosys.c gram.y OBJECTS = main.o doname.o misc.o files.o dosys.o gram.o LIBES = -lld LINTS = lint -p CFLAGS = -O LP = /usr/bin/lp
make: $(OBJECTS) $(CC) $(CFLAGS) $(OBJECTS) $(LIBES) -o make
$(OBJECTS): defs.h
cleanup: -rm *.o gram.c -du install: @size make /bin/make mv make /bin
lint : dosys.c doname.c files.c main.c misc.c gram.c $(LINT) dosys.c doname.c files.c main.c misc.c \ gram.c
# Распечатать файлы, состояние которых не # согласуется с "целевым файлом" print
print: $(FILES) pr $? | $(LP) touch print
Перед выполнением всякой команды утилита make распечатывает ее.
Вызов утилиты make в каталоге, содержащем только указанные исходные файлы и файл описаний, будет иметь следующий результат:
cc -o -c main.c cc -o -c doname.c cc -o -c misc.c cc -o -c flies.c cc -o -c dosys.c yacc gram.y mv y.tab.c gram.c cc -o -c gram.c cc main.o doname.o misc.o files.o dosys.o gram.o -lld \ -o make make: 24700 + 7100 + 18344 = 50144 /bin/make: 25200 + 6900 + 18108 = 50208
Две последние строки являются результатом выполнения команды
size make /bin/make
Печать самой командной строки подавляется знаком @ в файле описаний.
Открытие файлов для блокировки сегментов
Первое требование для блокировки файла или сегмента файла состоит в том, чтобы иметь корректный дескриптор файла. Если будет устанавливаться блокировка на чтение, файл должен быть открыт как минимум на чтение. Соответственно, для блокировки на запись необходим доступ на запись. В следующем примере файл открывается как на чтение, так и на запись.
#include <stdio.h> #include <errno.h> #include <fcntl.h>
int fd; /* дескриптор файла */ char *filename;
main (argc, argv) int argc; char *argv []; { extern void exit (), perror ();
/* Получение имени файла базы данных из командной строки и открытие файла на чтение и запись. */
if (argc < 2) { (void) fprintf (stderr, "Usage: %s file\n", argv[0]); exit (2); } filename = argv [1]; fd = open (filename, O_RDWR); if (fd < 0) { perror (filename); exit (2); } . . .
Теперь файл открыт для выполнения как блокировок, так и функций ввода-вывода. Попробуем теперь установить блокировку.
Отладка процессов, работающих с разделяемыми библиотеками
Возможности такой отладки пока ограничены. Команды и данные из разделяемых библиотек не копируются в выполняемый файл, а sdb(1) не работает с таблицей имен разделяемой библиотеки. Если у Вас создастся впечатление, что ошибка находится вне Вашего файла, Вам, возможно, легче будет провести отладку, пересоздав выполняемый файл, на этот раз с архивной версией библиотеки.
Паскаль
Поскольку Паскаль первоначально проектировался как язык для обучения программированию, он прост в обращении и получил широкое признание. Кроме простоты, к достоинствам языка можно отнести высокую степень структурированности программ и возможность обращения к системным вызовам (аналогично языку C). Удобнее всего использовать Паскаль для небольших программ, что естественно объясняется его первоначальным предназначением. Недостатками языка являются, например, отсутствие возможности инициализации переменных и недостаточные средства для работы с файлами.
Печать и сравнение описаний в базе данных terminfo
Иногда бывает необходимо сравнить описания двух терминалов или просмотреть описание, не обращаясь к каталогу, содержащему исходные тексты описаний terminfo. В обоих случаях можно воспользоваться командой infocmp(1M). Сравнить два описания одного и того же терминала можно, например, так:
mkdir /tmp/old /tmp/new TERMINFO=/tmp/old tic old5420.ti TERMINFO=/tmp/new tic new5420.ti infocmp -A /tmp/old -B /tmp/new -d 5420 5420
Эта последовательность команд сравнивает старое и новое описания терминала 5420.
Чтобы получить исходный текст описания terminfo для 5420, введите
infocmp -I 5420
Перечислимые константы
Имена, описанные как перечисляемые (см. Описания структур и объединений и Описания перечислимых типов), имеют тип int.
Переменные
Переменная - это одна из следующих конструкций:
идентификатор
идентификатор [ выражение ] $терм
Числовое значение любой неинициализированной переменной равно 0, а текстовое значение - пустой цепочке.
Идентификатор, указанный сам по себе, - это простая переменная. Переменная вида идентификатор[выражение] представляет элемент ассоциативного массива, названного при помощи идентификатора. Текстовое значение выражения используется в качестве индекса в массиве. Предпочтительное значение переменных идентификатор и идентификатор[выражение] определяется, исходя из контекста.
Переменная $0 обозначает текущую входную запись. Ее текстовое и числовое значения совпадают со значениями текущей записи. Если текущая входная запись представляет число, то числовое значение $0 равно данному числу, а текстовое значение - соответствующей цепочке символов. Предпочтительным значением $0 является текстовое, если только текущая входная запись не представляет число. $0 нельзя изменить при помощи присваивания.
Переменные $1, $2, ... обозначают первое, второе и т.д. поля. Текстовое и числовое значения $i (1 <= i <= NF) совпадают со значениями i-го поля текущей записи. Так же, как и для $0, если i-е поле представляет число, то числовое значение $i равно данному числу, а текстовое значение - соответствующей цепочке символов. Предпочтительным значением $i является текстовое, если только это поле не представляет число. $i можно изменить при помощи присваивания; соответственно изменяется и значение $0.
В общем случае, $терм обозначает входную запись, если терм имеет числовое значение 0, и i-е поле, если целая часть числового значения терма равна i. Если NF < i <= 100, $i ведет себя так же, как неинициализированная переменная. Манипуляции с $i при i > NF не изменяют значения NF.
Переменные окружения
Всякий раз, когда выполняется утилита make, читаются и добавляются к макроопределениям переменные окружения. Чтобы пользоваться этим механизмом должным образом, важнее всего рассмотреть приоритетность различных определений. Ниже описывается взаимодействие make'а с окружением. Утилита make поддерживает макрос MAKEFLAGS. Данный макрос определяется как цепочка, состоящая из всех входных аргументов-флагов (без знаков -). Макрос экспортируется и тем самым становится доступным вложенным за- пускам make'а. Флаги командной строки и присваивания, содержащиеся в make-файле, обновляют значение MAKEFLAGS. Чтобы описать, как окружение взаимодействует с утилитой make, необходимо рассмотреть макрос MAKEFLAGS.
Выполняясь, make формирует макроопределения в следующем порядке:
Читает переменную окружения MAKEFLAGS. Если она отсутствует либо пуста, внутренняя переменная утилиты make MAKEFLAGS устанавливается равной пустой цепочке. В противном случае каждая буква в значении переменной окружения MAKEFLAGS считается входным аргументом-флагом и соответствующим образом обрабатывается. (Исключение составляют опции -f, -p и -r.)
Читает встроенный список макроопределений.
Читает окружение. Переменные окружения трактуются как макроопределения и помечаются как экспортируемые (в смысле shell'а).
Читает make-файл (файлы). В первую очередь учитываются присваивания, указанные в make-файлах, а не в окружении. Такой порядок выбран для того, чтобы при чтении и обработке make-файла результат был предсказуем. Данное соглашение выполняется, если только не используется опция -e, задающая приоритет окружения. Если имеется явное присваивание переменной MAKEFLAGS, оно также учитывается в первую очередь. Эта возможность бывает удобна при вложенных запусках утилиты make из текущего make-файла.
Полезно привести список приоритетов присваиваний. Ниже перечисляются установки макропределений в порядке возрастания их старшинства:
Внутренние макроопределения. Окружение. Файл (файлы) с описаниями. Командная строка.
Использование опции -e приводит к некоторому изменению порядка:
Внутренние макроопределения. Файл (файлы) с описаниями. Окружение. Командная строка.
Описанный порядок достаточно универсален, чтобы позволить программисту задать make-файл или набор make-файлов, параметры которых могут определяться динамически.
Переменные-поля
Поля в awk'е разделяют по существу все свойства переменных. Они используются в арифметических и текстовых операциях, могут быть инициализированы пустой цепочкой, им можно присвоить другое значение. Например, можно разделить значение второго поля на 1000, чтобы получить площадь в миллионах квадратных миль:
{ $2 /= 1000; print }
вычислить значение поля, исходя из значений двух других:
BEGIN { FS = "\t" } { $4 = 1000 * $3 / $2; print }
присвоить полю новое значение - цепочку:
/USA/ { $1 = "United States" ; print }
Последняя программа заменяет в одной из строк поле USA на United States и печатает новое значение строки:
United States 3615 219 North America
К полям можно обращаться посредством выражений; например, $NF
обозначает последнее поле, а $(NF-1) - второе от конца. Отметим, что скобки в последнем примере необходимы, поскольку $NF-1
есть выражение, значение которого на 1 меньше значения последнего поля.
Переменные с приращением
Переменная с приращением имеет одну из следующих форм:
++пер
--пер
пер++ пер--
Терм ++пер имеет значение пер+1, а по своему действию эквивалентен присваиванию пер = пер+1. Аналогично, терм --пер имеет значение пер-1; он эквивалентен присваиванию пер = пер-1. Далее, терм пер++ имеет значение пер и эффект присваивания пер = пер+1, а терм пер-- имеет значение пер и эффект присваивания пер = пер-1. Предпочтительным значением переменной с приращением является числовое.
Переменные, выражения и присваивания
awk дает возможность выполнять арифметические вычисления и сохранять их результаты в переменных для последующего использования. В качестве примера рассмотрим вывод на печать плотности населения тех стран, информация о которых содержится в файле countries:
{ print $1 (1000000 * $3) / ($2 * 1000) }
(Напомним, что в этом файле население страны указано в миллионах человек, а площадь - в тысячах квадратных миль.) Результатом является количество людей, приходящееся на одну квадратную милю:
Russia 30.289 Canada 6.23053 China 234.561 USA 60.5809 Brazil 35.3013 Australia 4.71698 India 501.97 Argentina 24.2537 Sudan 19.6281 Algeria 19.5652
Формат у этой таблицы получился не очень красивым; если использовать вместо print оператор printf:
{ printf "%10s %6.1f\n", $1, (1000000*$3)/($2*1000) }
результат будет таким:
Russia 30.3 Canada 6.2 China 234.6 USA 60.6 Brazil 35.3 Australia 4.7 India 502.0 Argentina 24.3 Sudan 19.6 Algeria 19.6
Все промежуточные арифметические вычисления выполняются над вещественными числами. Допускаются следующие арифметические операции: +, -, *, /, % (остаток от деления).
Чтобы вычислить суммарное население и число стран из Азии, можно использовать программу
/Asia/ { pop += $3; ++n } END { print "total population of", n, "Asian countries is", pop }
которая печатает:
total population of 3 Asian countries is 1765
Операции ++, --, +=, -=, /=, *=, %= используются в awk'е так же, как в языке C. Операция ++, например, увеличивает значение переменной на единицу. Операции ++ и -- (как и в C) могут быть и префиксными, и постфиксными. Все эти операции можно использовать также и в выражениях.
Первичные выражения
В awk'е шаблоны и действия составляются из выражений. Первичные выражения - это основные "строительные блоки" для выражений; к первичным выражениям относятся:
числовые константы текстовые константы переменные функции
Каждое выражение имеет как числовое, так и текстовое значение; одно из них обычно является предпочтительным. Правила, по которым определяется предпочтительное значение выражения, излагаются ниже.
Вызов функции есть первичное выражение, за которым следует заключенный в скобки список (возможно пустой) выражений, являющихся фактическими параметрами функции. Первичное выражение должно иметь тип "функция, возвращающая ...", а результат вызова функции имеет тип "...". Как поясняется ниже, не встретившийся до сих пор идентификатор, за которым следует левая скобка, неявно, исходя из контекста, описывается как функция, возвращающая целый результат.
Все фактические параметры типа float преобразуются перед вызовом к типу double. Все параметры типов char или unsigned long - к int. Имена массивов преобразуются в указатели. Никаких других преобразований автоматически не делается; в частности, компилятор не сравнивает типы фактических и формальных параметров. Если преобразование необходимо, его следует задать явно (см. Унарные операции, а также пункт Имена типов в разделе ОПИСАНИЯ).
При обработке вызова функции изготавливается копия каждого фактического параметра. Таким образом, в языке C все параметры передаются только по значению. Функция может изменить значения формальных параметров, но эти изменения не повлияют на значения параметров фактических. При передаче указателей следует учитывать, что функция может изменить указуемый объект. Порядок обработки параметров в языке не определяется; отметим, что в разных компиляторах он разный. Допускаются рекурсивные вызовы любой функции.
Первичное выражение, за которым следует точка, а затем идентификатор, также является первичным. Первое выражение должно быть структурой или объединением, а идентификатор должен именовать элемент структуры или объединения. Значение выражения - названный элемент структуры или объединения; это л_значение, если первое выражение - л_значение.
Первичное выражение, за которым следует стрелка -> (составленная из знаков - и >), а затем идентификатор, также является первичным. Первое выражение должно быть указателем на структуру или объединение, а идентификатор должен именовать элемент этой структуры или объединения. Значение выражения - л_значение, обозначающее названный элемент указуемой структуры или объединения. Таким образом, выражение E1->MOS - то же самое, что и (*E1).MOS. Структуры и объединения обсуждаются в пункте Описания структур и объединений раздела ОПИСАНИЯ.
Побитная операция И
выражение_побитного_и: выражение & выражение
Операция & ассоциативна, и выражения, содержащие &, могут быть переупорядочены. Выполняются обычные арифметические преобразования. Результат равен побитной функции И от операндов. Операция применима только к операндам целочисленных типов.
Побитная операция ИЛИ
выражение_побитного_или: выражение | выражение
Операция | ассоциативна, и выражения, содержащие |, могут быть переупорядочены. Выполняются обычные арифметические преобразования. Результат равен побитной функции ИЛИ от операндов. Операция применима только к операндам целочисленных типов.
Побитная операция исключающее ИЛИ
выражение_побитного_исключающего_или: выражение ^ выражение
Операция ^ ассоциативна, и выражения, содержащие ^, могут быть переупорядочены. Выполняются обычные арифметические преобразования. Результат равен побитной функции исключающего ИЛИ от операндов. Операция применима только к операндам целочисленных типов.
Почему для иллюстрации интерфейса используется язык C
Везде далее в этом разделе для иллюстрации интерфейса между языком программирования и операционной системой используется язык C, поскольку механизмы интерфейса ориентированы на этот язык больше, чем на любой другой язык высокого уровня. Таким образом, в данном разделе в действительности описывается интерфейс между ОС UNIX и языком C.
Поддерживаемые языки программирования
В данном разделе речь идет о языках программирования, компиляторы для которых поступают как вместе с ОС UNIX, так и независимо. В будущем планируется расширение спектра доступных языков.
Подготовка исходного текста
Если Вы решили включить текст из существующей архивной библиотеки в создаваемую Вами разделяемую, некоторая его модификация облегчит дальнейшее ведение разделяемой библиотеки. См. ниже раздел Подготовка исходного текста для разделяемой библиотеки.
Подготовка исходного текста для разделяемой библиотеки
Все, что написано на языке C и годится для разделяемой библиотеки, может использоваться и в архивной библиотеке. Обратное, вообще говоря, неверно, так как в разделяемой библиотеке могут оказаться внешние (импортируемые) имена. Рекомендации этого раздела помогут Вам перерабатывать исходный текст функций для разделяемой библиотеки так, чтобы они становились лишь немного медленнее и больше по размеру, чем их версии для архивной библиотеки. Здесь, главным образом, освещаются вопросы организации данных, так как большинство проблем совместимости связано со структурами данных в разделяемых и архивных библиотеках.
Уменьшайте объем глобальных данных
В настоящее время все внешние переменные разделяемой библиотеки глобальны, то есть доступны прикладным программам, что может затруднить поддержку библиотеки. Вам необходимо стремиться уменьшить объем глобальных данных, следуя изложенным ниже советам.
Во-первых, используйте, где возможно, стековые (automatic) переменные. Это уменьшает объем статических данных и количество переменных, доступных прикладным программам.
Во-вторых, проверяйте, действительно ли та или иная переменная должна быть внешней. Статические переменные невидимы для прикладных программ, поэтому их адреса могут быть изменены в последующих версиях библиотеки. Только адреса внешних переменных должны оставаться постоянными.
В-третьих, не определяйте память для переменных при компиляции, а запрашивайте ее на стадии выполнения. Это важно по двум причинам. Уменьшается размер области данных библиотеки, что экономит память процессов, которые будут получать только ту память, которая им действительно нужна. Кроме того, размеры динамически размещаемых переменных могут изменяться без ущерба для совместимости (размеры статически размещаемых переменных нельзя изменить, не изменяя адресов других переменных).
Храните функции и глобальные данные в различных исходных файлах
Отделение команд от данных уменьшает вероятность перемещения данных. Если нужно определить новые экспортируемые переменные, их можно добавить в конец соответствующего файла, не меняя адресов уже определенных переменных.
Из архивной библиотеки редактор внешних связей извлекает отдельные элементы, поэтому программисты обычно помещают функцию и соответствующие ей данные в один и тот же исходный файл. Это хорошо только при условии, что адреса данных определяются в процессе редактирования связей, однако для разделяемых библиотек имеется ряд ограничений. Если внешние (экспортируемые) переменные разбросаны по различным модулям библиотеки, они перемешиваются с невидимыми пользователю данными. Изменение этих невидимых извне данных, например цепочки символов "hello" в следующем примере, приводит к смещению остальных данных, в том числе и экспортируемых.
Рассмотрим два варианта программы:
int head=0; int head=0; func() func() { { . . . . . . p="hello"; p="hello, world"; . . . . . . } } int tail=0; int tail=0;
Пусть относительный виртуальный адрес переменной head будет нулевым для обоих примеров. Текстовые константы тоже будут иметь одинаковые адреса, но их длины различны. Старый адрес tail будет равен 12, а новый - 20, поэтому, если предполагается, что переменная tail будет доступна извне библиотеки, две приведенные версии программы будут несовместимы.
Добавление новых экспортируемых переменных в разделяемую библиотеку может изменить адреса статических имен, но это не влияет на совместимость. Выполняемый файл не может прямо ссылаться на статические имена, поэтому он не зависит от их адресов. В связи с этим, следует собрать вместе все экспортируемые данные и разместить их выше (с меньшими адресами) статических (невидимых) данных. Для этого в списке объектных файлов файла спецификаций указывайте первыми файлы с глобальными данными:
#objects data1.o . . . lastdata.o text1.o text2.o . . .
Если модули с данными включать не первыми, безобидное, казалось бы, изменение, например, замена текстовой константы, может привести к потере работоспособности уже созданных выполняемых файлов.
Пользователи разделяемой библиотеки, независимо от организации исходных файлов библиотеки, получают всю ее область данных во время выполнения. Поэтому Вы можете без опасений хранить описания всех экспортируемых переменных в одном исходном файле. Разумеется, их можно хранить и в нескольких файлах.
Инициализируйте глобальные переменные
Инициализируйте экспортируемые переменные, в том числе и указатели на внешние (импортируемые) имена, хотя это и увеличивает размер разделяемой библиотеки выполнения. Это увеличение касается только файла самой библиотеки. Инициализация этих переменных дает дополнительную гарантию неизменности их адресов в будущем.
C-компилятор ОС UNIX V собирает неинициализированные переменные в отдельную секцию, а редактор внешних связей назначает им адреса в непредсказуемом порядке. Иными словами, порядок следования неинициализированных переменных может измениться после следующего редактирования внешних связей библиотеки. Порядок же инициализированных переменных остается неизменным, что помогает обеспечить совместимость будущих версий библиотеки с предыдущими.
Сохраняйте порядок следования функций в таблице переходов
Новые функции добавляйте в конец таблицы переходов. Создавая новый файл спецификаций библиотеки, старайтесь добиться совместимости с более ранними ее версиями. Для этого добавлять новые функции нужно так, чтобы адреса уже включенных функций не изменялись. В этом случае с новой версией поставляемой Вами библиотеки смогут без перередактирования работать все выполняемые файлы, созданные с предыдущей версией.
Подпрограммы initscr( ), refresh( ) и endwin( )
Подпрограммы initscr(), refresh() и endwin() приводят терминал в состояние "работа с curses", обновляют содержимое экрана и восстанавливают терминал в состоянии "вне curses" соответственно. Для уяснения действия каждой из этих подпрограмм воспользуемся еще раз нашим простым примером.
#include <curses.h>
main () { initscr (); /* Инициализируем терминал и переменные и структуры данных из <curses.h> */ move (LINES/2 - 1, COLS/2 - 4); addstr ("Bulls"); refresh (); /* Выводим данные на экран терминала */ addstr ("Eye"); refresh (); /* Выводим еще на экран терминала */ endwin (); /* Восстанавливаем состояние терминала */ }
Curses-программа начинается обычно с вызова initscr(); это достаточно сделать один раз. Подпрограмма initscr() определяет тип терминала, на котором выполняется программа, по значению переменной окружения $TERM, как это описано в разделе Взаимодействие curses и terminfo. Затем подпрограмма инициализирует все переменные и структуры данных, описанные в файле <curses.h>. initscr() присваивает переменным LINES и COLS значения, соответствующие терминалу, на котором выполняется программа. Например, если используется Teletype 5425, она присвоит значение 24 переменной LINES, а значение COLS станет равным 80. Если возникает ошибка, подпрограмма записывает сообщение об этом в stderr
и завершает работу.
Ввод/вывод при выполнении программы производится подпрограммами, подобными использующимся в примере move() и addstr(). Например,
move (LINES/2 - 1, COLS/2 - 4);
требует перемещения курсора в точку, находящуюся несколько левее центра экрана. Затем
addstr ("Bulls");
требует вывести цепочку символов Bulls. Если программа выполняется на терминале Teletype 5425, эта цепочка будет выведена, начиная с позиции (11, 36).
Внимание!
Все подпрограммы curses, перемещающие курсор, отсчитывают его координаты относительно левого верхнего угла экрана. Координаты (LINES, COLS) этого угла принимаются за (0, 0), а не за (1, 1). Заметьте, что первой идет вертикальная координата, а затем горизонтальная, в отличие от обычной записи 'x, y' для экранных (или графических) координат. Для перемещения курсора в среднюю строку экрана в примере появляется -1, чтобы учесть координаты (0, 0) для угловой позиции.
При вызове подпрограмм типа move() и addstr() содержимое физического экрана терминала не меняется. Экран обновляется только при вызове refresh(). До такого вызова изменяется только внутреннее представление экрана, которое называется окном. Эту весьма важную концепцию мы обсудим далее в разделе Еще об окнах и подпрограмме refresh().
В заключение отметим, что программа, работающая с curses, заканчивается вызовом endwin(). Эта подпрограмма восстанавливает прежнее состояние терминала и помещает курсор в левый нижний угол экрана.
Подпрограммы wnoutrefresh( ) и doupdate( )
Как было сказано ранее, подпрограмма refresh() пересылает данные с stdscr на экран терминала. refresh() является макросом, который расширяется в wrefresh (stdscr), о чем также упоминалось в разделах Что нужно программе для работы с curses и Еще об окнах и подпрограмме refresh().
Подпрограмма wrefresh() предназначена для пересылки данных из окна (будь то stdscr или созданное пользователем) на экран терминала; она вызывает подпрограммы wnoutrefresh() и doupdate(). Аналогичным образом, prefresh() пересылает данные из спецокна на экран, обращаясь к pnoutrefresh() и doupdate().
Пользуясь wnoutrefresh() или pnoutrefresh() (здесь мы для простоты рассмотрим только первую из них) и doupdate(), Вы можете обновлять экран с большей эффективностью, чем обращаясь к wrefresh(). wrefresh() сначала вызывает wnoutrefresh(), которая копирует указанное окно в структуру данных, называемую виртуальным экраном. Виртуальный экран содержит то, что программа собирается вывести на терминал. Вызвав wnoutrefresh(), wrefresh() затем обращается к doupdate(), которая сравнивает виртуальный экран с физическим и производит обновление последнего. Если Вы хотите отобразить содержимое сразу нескольких окон, вызовы wrefresh() приведут к нескольким обращениям к wnoutrefresh() и doupdate(), то есть к нескольким актам вывода данных на экран. Однако, вызвав wnoutrefresh() для каждого окна, а затем один раз doupdate(), можно уменьшить общее количество передаваемых символов и необходимого для передачи времени процессора. Нижеследующая программа использует doupdate()
лишь однажды.
#include <curses.h>
main () { WINDOW *w1, *w2; initscr (); w1 = newwin (2, 6, 0, 3); w2 = newwin (1, 4, 5, 4); waddstr (w1, "Bulls"); wnoutrefresh (w1); waddstr (w2, "Eye"); wnoutrefresh (w2); doupdate (); endwin (); }
В начале этой программы объявляются новые окна. Операторы
w1 = newwin (2, 6, 0, 3); w2 = newwin (1, 4, 5, 4);
объявляют два окна, которые называются w1 и w2, передавая некоторые описания подпрограмме newwin(), которая подробнее описана ниже.
Ниже показано действие wnoutrefresh() и doupdate() на эти два окна и на виртуальный и физический экраны.
Подразумеваемые правила
Для того, чтобы задать информацию о подразумеваемых зависимостях и соответствующих им командах, утилита make использует таблицу суффиксов и набор правил трансформации. По умолчанию список суффиксов выглядит так:
.o | объектный файл |
.c | исходный C-файл |
.c~ | исходный SCCS C-файл |
.f | исходный f77-файл |
.f~ | исходный SCCS f77-файл |
.s | исходный файл на языке ассемблера |
.s~ | исходный SCCS-файл на языке ассемблера |
.y | исходная yacc-грамматика |
.y~ | исходная SCCS yacc-грамматика |
.l | исходная lex-спецификация |
.l~ | исходный SCCS lex-спецификация |
.h | включаемый файл |
.h~ | включаемый SCCS-файл |
.sh | командный файл |
.sh~ | командный SCCS-файл |
Следующий рисунок содержит сводку подразумеваемых путей трансформации. Если имеется два пути, соединяющих определенную пару суффиксов, более длинный путь используется только в тех случаях, когда промежуточный файл существует либо упоминается в описании.
Если требуется файл x.o, а в описании или каталоге найден файл x.c, x.c будет откомпилирован. Если, кроме того, имеется файл x.l, он будет обработан lex'ом, после чего будет откомпилирован результат обработки. Однако, если файл x.c отсутствует, но есть x.l, make воспользуется правилом прямой трансформации и отбросит промежуточный C-файл.
Можно изменить имена некоторых используемых по умолчанию компиляторов, а также опции, с которыми компиляторы вызываются. Для этого предусмотрены макросы с определенными именами. Именам компиляторов соответствуют макросы AS, CC, F77, YACC, LEX. Команда
make CC=newcc
будет приводить к использованию вместо стандартного C-компилятора команды newcc. Чтобы компиляторы запускались с дополнительными опциями, можно устанавливать макросы ASFLAGS, CFLAGS, F77FLAGS, YFLAGS и LFLAGS. Так, вызов
make "CFLAGS=-g"
заставляет команду cc(1) включать в файл отладочную информацию.
Поле номера секции
Ниже перечислены допустимые номера секций:
Обозначение | Номер секции | Смысл |
N_DEBUG | -2 | Специальное имя для символьной отладки |
N_ABS | -1 | Абсолютное имя |
N_UNDEF | 0 | Неопределенное внешнее имя |
N_SCNUM | 1-077777 | Номер той секции, в которой имя определяется |
Специальный номер секции -2 применяется для имен, используемых при символьной отладке. Таковы имена, отмечающие начало структуры, объединения или перечисления; определения типов; имена файлов. Номер секции -1 отмечает имена, имеющие ненастраиваемые значения. Такими именами являются, в частности, автоматические и регистровые переменные, аргументы функций и специальные имена .eos.
За единственным исключением, нулевой номер секции используется для настраиваемых внешних имен, не определенных в данном файле. Единственное же исключение - это повторно определяемое внешнее имя (например, COMMON-блок Фортрана или же неинициализированная переменная, определенная в функции языка C как внешняя). В таблице имен каждого файла, в котором определено такое имя, в поле номера секции соответствующего элемента помещается 0, а в поле значения - положительное число, равное размеру данных для этого имени. Когда же эти файлы объединяются для получения выполняемого объектного файла, редактор внешних связей из всех таких одинаковых входных имен строит одно имя, с номером секции неинициализированных данных. Максимальный размер данных для всех таких входных имен используется при отведении места под данные для выходного имени. Значением выходного имени становится соответствующий виртуальный адрес. Таков единственный случай, когда имя имеет нулевой номер секции и ненулевое значение.
Поле типа
Поле типа элемента таблицы имен содержит информацию о базовом и производных типах соответствующего имени. Компилятор языка C заполняет эти поля только при указании опции -g. Производных типов может быть несколько, но каждое имя имеет ровно один базовый тип. Формат 16-битного поля типа таков:
d6 | d5 | d4 | d3 | d2 | d1 | базовый тип |
Биты с 0-го по 3-й кодируют базовый тип:
Обозначение | Значение | Базовый тип |
T_NULL | 0 | Тип не назначен |
T_VOID | 1 | Пустой тип |
T_CHAR | 2 | Символ |
T_SHORT | 3 | Короткое целое |
T_INT | 4 | Целое |
T_LONG | 5 | Длинное целое |
T_FLOAT | 6 | Вещественное одинарной точности |
T_DOUBLE | 7 | Вещественное двойной точности |
T_STRUCT | 8 | Структура |
T_UNION | 9 | Объединение |
T_ENUM | 10 | Перечисление |
T_MOE | 11 | Элемент перечисления |
T_UCHAR | 12 | Символ без знака |
T_USHORT | 13 | Короткое целое без знака |
T_UINT | 14 | Целое без знака |
T_ULONG | 15 | Длинное целое без знака |
Биты с 4-го по 15-й разделены на шесть двухбитных полей, обозначаемых d1..d6. Эти поля представляют уровни производных типов, перечисленых ниже:
Обозначение | Значение | Производный тип |
DT_NON | 0 | Производный тип отсутствует |
DT_PTR | 1 | Указатель |
DT_FCN | 2 | Функция |
DT_ARY | 3 | Массив |
В следующих примерах иллюстрируется представление типа в элементе таблицы имен.
char *func();
Здесь func есть имя функции, возвращающей указатель на символ. Базовый тип имени func есть 2 (символ), в поле d1 будет помещено число 2 (функция), а в поле d2 - 1 (указатель). Таким образом, в поле типа элемента func будет находиться шестнадцатеричное значение 0x62, которое и интерпретируется как функция, возвращающая указатель на символ.
short *tabptr [10] [25] [3];
Здесь tabptr есть трехмерный массив указателей на короткие целые. Базовый тип для tabptr есть 3 (короткое целое); в каждое из полей d1, d2, d3 будет помещено число 3 (массив), а в поле d4 - 1 (указатель). Таким образом, для трехмерного массива указателей на короткие целые в поле типа соответствующего элемента таблицы имен будет храниться шестнадцатеричное значение 0xff3.
Поле значения имени
Смысл величины, хранящейся в поле значения, зависит от класса памяти соответствующего имени. Эту зависимость иллюстрирует следующая таблица:
Класс памяти | Смысл значения имени |
C_AUTO | Смещение в стеке (в байтах) |
C_EXT | Настраиваемый адрес |
C_STAT | Настраиваемый адрес |
C_REG | Номер регистра |
C_LABEL | Настраиваемый адрес |
C_MOS | Смещение в байтах |
C_ARG | Смещение в стеке (в байтах) |
C_STRTAG | 0 |
C_MOU | 0 |
C_UNTAG | 0 |
C_TPDEF | 0 |
C_ENTAG | 0 |
C_MOE | Значение элемента перечисления |
C_REGPARM | Номер регистра |
C_FIELD | Смещение в битах |
C_BLOCK | Настраиваемый адрес |
C_FCN | Настраиваемый адрес |
C_EOS | Длина |
C_FILE | См. текст ниже |
C_ALIAS | Номер синонима |
C_HIDDEN | Настраиваемый адрес |
Для имен с классом памяти C_FILE в поле значения указывается номер следующего элемента для специального имени .file. Таким образом, в таблице имен элементы .file образуют однонаправленный список. В поле значения последнего элемента .file хранится номер элемента таблицы имен, отвечающего первому глобальному имени.
Настраиваемые имена имеют значение, равное виртуальному адресу соответствующих команд или данных. Эти значения изменяются при обработке секции редактором связей.
Получение информации о блокировке
Имеется возможность выявить процесс, мешающий установить блокировку. С помощью полученной информации можно узнать, какие бликоровки действуют для файла. Пусть блокировка устанавливается как в предыдущем примере, но при обращении к fcntl(2) используется операция F_GETLK. Если запрашиваемую блокировку установить нельзя, информация о первой мешающей этому блокировке будет возвращена процессу через структуру, адрес которой был передан системному вызову fcntl(2). В результате данные, переданные fcntl(2), будут заменены информацией о противодействующей блокировке. Эта информация включает два ранее не упоминавшихся элемента данных: l_pid и l_sysid. Они используются только операцией F_GETLK. (Для систем, не поддерживающих распределенную архитектуру, значение l_sysid игнорируется.) Два упомянутых поля полностью идентифицируют процесс, установивший блокировку.
Если блокировка, запрашиваемая системным вызовом fcntl(2) с использованием операции F_GETLK, может быть установлена, значением поля l_type становится F_UNLCK, а остальные поля в структуре не изменяются. Используем описанную возможность для вывода информации о всех сегментах файла, заблокированных другими процессами. Заметим, что если на один сегмент файла установлено несколько блокировок, то только первая из них будет найдена.
struct flock lck;
/* Получение и вывод информации о сегментах файла, блокированных на запись */ (void) printf ("ид-р сис. ид-р проц. тип начало длина\n"); lck.l_whence = 0; lck.l_start = 0L; lck.l_len = 0L;
do { lck.l_type = F_WRLCK; (void) fcntl (fd, F_GETLK, &lck); if (lck.l_type != F_UNLCK) { (void) printf("%9d %9d %c %6d %5d\n", lck.l_sysid, 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);
Системный вызов fcntl(2) с операцией F_GETLK всегда завершается успешно (то есть процесс не будет приостановлен и вызов не завершится аварийно), если, конечно, значения переданных параметров корректны.
Функция lockf(3C) с операцией F_TEST также может быть использована для выявления процесса, мешающего установить блокировку. Однако эта функция не способна определить заблокированный участок сегмента и то, какой процесс заблокировал его. Программа, использующая lockf(3C) для проверки факта блокировки файла, приведена ниже:
/* Выявление заблокированного сегмента */
/* Установка на начало файла */ (void) lseek (fd, 0, 0L);
/* Нулевой размер тестируемого сегмента означает проверку до конца файла */ if (lockf (fd, F_TEST, 0L) < 0) { switch (errno) { case EACCES: case EAGAIN: (void) printf ("Файл блокирован другим процессом\n"); break; case EBADF: /* Некорректные аргументы lockf */ perror ("lockf"); break; default: (void) printf ( "lockf: непредусмотренная ошибка <%d>\n",errno); break; } }
Процесс, порожденный в результате выполнения системного вызова fork(2), наследует копии дескрипторов файлов, открытых процессом-предком. Кроме того, порождающий и порожденный процессы (для каждого файла) разделяют общий указатель текущей позиции в файле. Если процесс-предок изменит указатель файла, то изменение затронет и порожденный процесс. Такое положение влечет важные следствия при использовании блокировки сегментов. Если значение поля l_whence равно 1, то l_start указывает смещение начала блокируемого сегмента относительно указателя текущей позиции в файле. Если и порождающий, и порожденный процессы блокируют сегменты одного и того же файла, то существует вероятность, что при этом будет использоваться указатель текущей позиции, измененный другим процессом. Эта проблема существует и в случае использования функции lockf(3C) и является следствием требований стандарта /usr/group на блокировку сегментов. Если системный вызов fork(2) используется в программе наряду с блокировкой сегментов, то порожденный процесс при использовании любого метода блокировки должен закрыть и вновь открыть файл. В результате будет создан новый указатель текущей позиции в файле, которым можно будет манипулировать, не сталкиваясь с отмеченной проблемой. Другое решение состоит в том, чтобы использовать системный вызов fcntl(2) со значением поля l_whence 0 или 2, что делает блокировку независимой от указателя текущей позиции.
Порядок создания
Процесс построения разделяемой библиотеки системы UNIX состоит из шести основных шагов:
Выбор адресов секций команд и данных.
Выбор маршрутного имени разделяемой библиотеки выполнения.
Определение содержимого библиотеки.
Подготовка исходного текста.
Создание файла спецификаций библиотеки.
Создание с помощью mkshlib(1) разделяемых библиотек сборки и выполнения.
Далее подробно рассматривается каждый из этих шагов.
Поток управления
lint стремится выявлять недостижимые фрагменты программы. Он выдает сообщения об операторах без меток, следующих непосредственно за операторами goto, break, continue и return. lint пытается выявлять бесконечные циклы (частные случаи - циклы с заголовками while(1) и for(;;)) и трактует следующие за ними операторы как недостижимые. lint также выдает сообщения о циклах, в которые нельзя попасть через заголовок. В корректных программах могут быть такие циклы, однако подобный стиль считается плохим. Для подавления сообщений о недостижимых фрагментах программы служит опция -b.
lint не обладает информацей, достаточной для того, чтобы определить, какие из вызываемых функций никогда не возвращают управление. Так, вызов exit() может стать причиной недостижимости некоторого фрагмента, чего lint, однако, не обнаружит. Наиболее серьезные последствия этого связаны с опредлением возвращаемых функцией значений (см. пункт Значения функций). Если некоторый фрагмент в программе мыслится как недостижимый, причем lint'у это непонятно, в соответствующее место программы можно вставить комментарий
/* NOTREACHED */
который будет информировать lint о том, что данный фрагмент недостижим (без выдачи диагностического сообщения).
Программы, сгенерированные при помощи yacc'а и, в особенности, lex'а, могут содержать сотни недостижимых операторов break, но пользы от подобных сообщений немного. Обычно с такими операторами ничего поделать нельзя, и сообщения загромождали бы выдачу lint'а. Рекомендуется при работе с подобными исходными текстами вызывать lint с опцией -b.
ПРАВИЛА ВИДИМОСТИ
Не обязательно компилировать C-программу всю целиком, единовременно. Исходный текст программы можно поместить в несколько файлов, а предварительно откомпилированные процедуры загружать из библиотек. Функции программы взаимодействуют между собой как при помощи явных вызовов, так и манипулируя внешними данными.
В языке C рассматриваются два рода видимости: во-первых, та, которую можно назвать лексической видимостью идентификатора, она по существу описывается областью программы, где идентификатор можно использовать, не получая диагностики "неопределенный идентификатор"; и во-вторых, видимость, связанная с внешними идентификаторами и характеризующаяся правилом, устанавливающим, в каких случаях одинаковые внешние идентификаторы обозначают один и тот же объект.
в среде операционной системы UNIX.
Данное Руководство содержит информацию о программировании в среде операционной системы UNIX. Целью Руководства не является обучение написанию программ. Напротив, обсуждаются готовые тексты программ, и внимание при этом акцентируется на других видах деятельности, являющихся частью всего процесса разработки программ.
Многие коммерческие программные системы имеют в своем составе программы, управляющие вводом и выводом на терминал. Такая программа может перемещать курсор, показывать меню, делить экран терминала на окна или форматировать экран для облегчения ввода и извлечения информации из базы данных.
В этой главе объясняется, как разрабатывать программы, работающие с терминалом в системе UNIX, используя пакет подпрограмм, называемый cursescurses/terminfo . Этот пакет включает библиотеку подпрограмм на языке C, базу данных и набор вспомогательных средств системы UNIX. Назначение данной главы - не описание всех компонент пакета, а предоставление пользователю возможности немедленно приступить к написанию программ, работающих с терминалом. Здесь рассматриваются только наиболее часто используемые подпрограммы, в отношении же остальных делается ссылка на curses(3X) и terminfo(4) в Справочнике программиста. Держите эту книгу под рукой на случай, если Вам нужно будет узнать подробности о какой-либо из описанных (или не описанных) здесь подпрограмм.
Для использования cursescurses/terminfo нужно быть знакомым с языком программирования C, так как подпрограммы пакета написаны именно на нем. Кроме того, необходимо знать стандартный пакет средств ввода/вывода системы UNIX для языка C [см. stdio(3S)]. Вы сможете создавать разнообразные программы для работы с терминалом, пользуясь этими знаниями и пониманием принятого в системе UNIX принципа опоры на работу, уже сделанную другими.
Данная глава делится на пять разделов:
Введение
В этом разделе кратко описываются curses, terminfo и другие компоненты пакета подпрограмм для работы с терминалом.
Использование подпрограмм пакета curses
В этом разделе описываются основные подпрограммы, составляющие библиотеку curses(3X). Рассказывается о подпрограммах вывода на экран, чтения с экрана, построения окон. Описываются также подпрограммы, реализующие более сложные функции - линейную графику, использование терминальных программируемых меток, работу с несколькими терминалами одновременно. Приводится множество примеров, демонстрирующих эффективность использования этих подпрограмм.
Использование подпрограмм пакета terminfo
В этом разделе описываются те подпрограммы пакета curses, которые непосредственно взаимодействуют с базой данных terminfo для реализации таких возможностей, как программируемые функциональные клавиши.
Использование базы данных terminfo
В этом разделе описывается база данных terminfo, соответствующие средства поддержки и их взаимодействие с библиотекой curses.
Примеры программ, работающих с curses
В этом разделе приведены тексты шести программ, иллюстрирующие использование подпрограмм пакета curses.
Предложения определения секций
Предложение SECTIONS предназначено для указания способа комбинирования входных секций и размещения выходных секций (как в целом в виртуальной памяти, так и относительно других секций), а также для переименования выходных секций.
По умолчанию, если предложения SECTIONS отсутствуют, все одноименные входные секции объединяются в выходной секции с тем же именем. Если редактируются внешние связи двух объектных файлов, из которых первый содержит секции s1 и s2, а второй - секции s3
и s4, то выходной объектный файл будет содержать четыре секции: s1, s2, s3 и s4. Порядок этих секций будет зависеть от порядка, в котором редактор связей будет просматривать входные файлы.
Синтаксис предложений SECTIONS таков:
SECTIONS { имя_выходной_секции_1 : { спецификации_файлов, операторы_присваивания
} имя_выходной_секции_2 : { спецификации_файлов, операторы_присваивания
} . . . }
В оставшейся части этого раздела обсуждаются различные виды предложений определения секций.
Предыстория
Возможно, Вы уже читали о том, что первоначально реализация новой операционной системы, впоследствии названной UNIX, проводилась группой Кена Томпсона и Денниса Ритчи на неиспользуемом компьютере DEC PDP-7, стоявшем в коридоре AT&T Bell Laboratories. Причем целью этих работ было построение удобного окружения для собственного использования. Никто не собирался разрабатывать операционную систему, способную завоевать мировую известность.
Интересно проследить последовательность, в которой реализовывались различные элементы операционной системы. Сначала была построена простая файловая система, не имеющая древовидной структуры. Затем она была организована как система каталогов и файлов. Говоря о файлах, необходимо подчеркнуть две основные идеи. Во-первых, в ОС UNIX данные, программы, каталоги и даже физические устройства рассматриваются как файлы того или иного типа. Во-вторых, сам файл понимается как одномерный массив байт, лишенный какой-либо другой структуры. Очевидность и простота такого подхода способствовали построению удобного окружения как для программистов, так и для других пользователей.
Следующая идея состояла в том, чтобы рассматривать выполняющиеся программы как процессы. При этом каждый процесс может порождать другой и обмениваться с ним информацией. В результате такого подхода стало естественным разделение сегментов команд между несколькими процессами (это является характерной особенностью ОС UNIX).
Наконец, когда были реализованы команды для различных действий с файлами и ассемблер для генерации выполняемых программ, система уже могла автономно функционировать.
Следующим важным шагом было приобретение компьютера DEC PDP-11 и установка на нем новой операционной системы. По мнению Ритчи это была большая удача, поскольку PDP-11 была очень хорошей машиной и успех самого компьютера в определенной степени способствовал признанию операционной системы, ставшей известной под названием UNIX.
К 1972 году в системе была реализована возможность организации межпроцессных каналов (такого способа связи, при котором вывод одного процесса становится вводом другого). Операционная система была переписана на языках более высокого уровня (сначала на языке B, затем C) и получила название UNIX, придуманное Брайаном Керниганом. Таким образом, задача, поставленная Томсоном и Ритчи, то есть построение удобного программного окружения, была выполнена.
Стоит отметить, что реализация ОС UNIX проходила в атмосфере, существенно отличающейся от той, в которой разрабатывается большинство операционных систем, имеющих коммерческих успех, когда дюжины талантливых программистов напряженно работают в обстановке абсолютной секретности, причем сроки завершения работ бывают очень жесткими. В противоположность такому подходу, ОС UNIX созревала примерно в течение десяти лет. С самого начала она привлекла внимание большого числа замечательных специалистов, многие из которых использовали систему для проведения собственных разработок, с одной стороны, и внесли свой вклад в ее развитие, с другой стороны.
Начиная с 1971 года система стала использоваться внутри AT&T Bell Laboratories, а вскоре (в 1974 году) стала продаваться колледжам и университетам, причем цена была невысокой и никакой поддержки не предполагалось. Эти, так называемые исследовательские версии, нумеровались арабскими цифрами, начиная с 7. Иногда они самостоятельно развивались и обогащали систему новыми элементами. Например, широко известный экранный редактор vi был добавлен к системе Уильямом Джоем из Университета Беркли. В 1979 году, согласно коммерческим требованиям, AT&T начала поставлять поддерживаемые версии ОС UNIX. Они нумеровались римскими цифрами, и кроме того, часто снабжались номером модификации. Например, текущая версия называется UNIX System V Release 3.1.
Версии ОС UNIX, поставляемые AT&T сейчас, разрабатываются в обстановке, более типичной для промышленного производства программного обеспечения. Совершенствование системы происходит в ответ на требования рынка. Тем не менее, система по-прежнему выражает передовую идеологию ее разработчиков, и несет отпечаток университетской атмосферы, в которой они работают. Это свойство системы часто называют ее философией, имея в виду способ использования UNIX'а опытными программистами.
Преобразование termcap-описания в terminfo-описание
Предостережение
База данных terminfo разработана в качестве замены базы данных termcap. Такой переход нельзя выполнить мгновенно, поскольку с использованием termcap (и для работы с ней) написано множество программ и процессов. Любой переход от termcap к terminfo требует некоторого опыта работы с обеими базами данных. С описаниями в этих базах данных необходимо обращаться весьма осторожно, поскольку от них зависит правильное функционирование Вашего терминала.
Команда captoinfo(1M) преобразует описание в формате termcap(4) в описание в формате terminfo(4). captoinfo получает файл в формате termcap и выводит эквивалентное описание в формате terminfo на стандартный вывод. Например, командная строка
captoinfo /etc/termcap
преобразует файл /etc/termcap в исходный текст в формате terminfo, с сохранением комментариев и другой информации, не относящейся собственно к описанию, если она присутствует. Командная строка
captoinfo
ищет описание текущего терминала в базе данных termcap (они задаются значениями переменных окружения $TERM и $TERMCAP соответственно) и преобразует его в формат terminfo.
Если нужны описания терминала как в формате termcap, так и в формате terminfo, можно хранить только описание terminfo и использовать infocmp -C для получения описания в формате termcap.
Если Вы компилировали программы командой cc(1) с опциями -ltermcap или -ltermlib, они будут работоспособны и в дальнейшем. Однако, вместо этих опций следует использовать -lcurses.
ПРЕОБРАЗОВАНИЯ В ОПЕРАЦИЯХ
Ряд операций может, в зависимости от операндов, вызывать преобразование значения операнда из одного типа в другой. В данном разделе описывается результат, ожидаемый от таких преобразований. Сводка преобразований, необходимых в большинстве обычных операций, приводится в пункте Арифметические преобразования. При обсуждении отдельных операций сводка будет дополняться, там где это требуется.
Препроцессор
#define идентификатор цепочка_лексем #define идентификатор (идентификатор,...) цепочка_лексем #undef идентификатор #include "имя_файла"
#include <имя_файла>
#if ограниченное_константное_выражение #ifdef идентификатор #ifndef идентификатор #elif ограниченное_константное_выражение #else
#endif
#line константа "имя_файла" #ident "версия"
Прикладные программисты
Программисты данной группы разрабатывают программные системы, которыми будут пользоваться непрограммисты. Разработка крупной прикладной системы требует участия целых коллективов программистов. Членами таких коллективов могут быть сотрудники как фирмы-заказчика, так и фирмы-разработчика. Заметим, что некоторые программисты, занятые разработкой прикладного программного обеспечения, в большей степени занимаются организацией процесса разработки программ, а не собственно программированием.
Информация, необходимая программистам данной категории, включает все темы следующей главы, а также:
Системы управления программным обеспечением.
Блокировку файлов и сегментов. Межпроцессное взаимодействие. Разделяемую память. Усовершенствованные методы отладки программ.
В этой связи настоятельно рекомендуем изучить том, посвященный Интегрированной Среде Разработки Программ (ИСРП).
ПРИМЕНЕНИЕ AWK'А
Задача оставшейся части данной главы - показать синтаксические конструкции awk'а в действии. Разделы соответствуют следующим рассматриваемым вопросам:
Ввод/вывод. Шаблоны. Действия. Специфические возможности awk'а.
В качестве примера мы пройдем
В качестве примера мы пройдем все шаги создания небольшой разделяемой библиотеки. В текст включены необходимые пояснения. Несколько надуманный текст программ примера важен не сам по себе, а как иллюстрация к возникающим проблемам.
Наша библиотека будет называться libexam. Пусть ее исходный текст первоначально представляет собой единственный файл, содержащий следующее:
/* первоначальный вид exam.c */
#include <stdio.h>
extern int strlen (); extern char *malloc, *strcpy ();
int count = 0; char *Error;
char *excopy (e) char *e; { char *new;
++count; if ((new=malloc (strlen (e)+1))==0) { Error="Нет памяти"; return 0; } return strcpy (new, e); }
excount () { fprintf (stderr, "сч. вып. %d\n", count); return count: }
Для начала выберем адреса библиотечных секций команд и данных (из адресов областей, выделенных для личного пользования). Заметим, что начальные адреса секций должны быть кратны 1 Мб:
.text 0x74000000 .data 0x74100000
Далее, определим маршрутное имя разделяемой библиотеки выполнения:
/my/directory/libexam_s
Теперь нужно определить внешние (импортируемые) имена библиотеки (рекомендации по этому поводу см. в разделе Использование импортируемых имен): malloc, strcpy, strlen, fprintf и _iob. Макросы для этих имен мы поместим во включаемый файл, причем обратите внимание, что на _iob нет прямой ссылки, это делается через макрос из <stdio.h>, определяющий stderr. Заметим также, что именам даны префиксы _libexam_. Указатели на импортируемые имена, будучи доступными извне, могут совпасть с какими-либо другими именами. Использование имени библиотеки в качестве префикса уменьшает вероятность такого конфликта.
/* новый файл import.h */
#define malloc (*_libexam_malloc) #define strcpy (*_libexam_strcpy) #define strlen (*_libexam_strlen) #define fprintf (*_libexam_fprintf) #define _iob (*_libexam__iob)
extern char *malloc (); extern char *strcpy (); extern int strlen (); extern int fprintf ();
Примечание
Объект _iob не описывается в файле import.h как внешний. Предполагается, что это делается во включаемом файле <stdio.h>.
Нам понадобится еще исходный файл, содержащий указатели на импортируемые имена. Помните, что все глобальные данные должны быть проинициализированы.
/* новый файл import.c */
#include <stdio.h>
char *(*_libexam_malloc) () = 0; char *(*_libexam_strcpy) () = 0; int (*_libexam_strlen) () = 0; int (*_libexam_fprintf) () = 0; FILE (*_libexam__iob) [] = 0;
Наконец, посмотрим, какие глобальные переменные библиотеки должны быть доступны использующим ее программам (см. ниже рекомендацию "уменьшать объем глобальных даннных"). К переменной count обращаются через excount(), поэтому она может не быть описана как внешняя. Опишем ее как static (это уже должно было быть сделано и для архивной перемещаемой библиотеки).
Глобальные данные библиотеки необходимо переместить в отдельные исходные файлы (см. ниже рекомендацию "хранить функции и глобальные данные в различных исходных файлах"). Осталась единственная глобальная переменная Error, и ее нужно проинициализировать (см. ниже рекомендацию "инициализировать глобальные переменные"). Error должна остаться глобальной, поскольку через нее передается информация в вызывающую программу.
/* новый файл global.c */
char *Error = 0;
В связи с перечисленными изменениями исходный файл должен теперь выглядеть так (обратите внимание, что имена функций необходимо описать как внешние):
/* измененный exam.c */
#include "import.h"
#include <stdio.h>
extern int strlen (); extern char *malloc, *strcpy ();
static int count = 0; extern char *Error;
char *excopy (e) char *e; { char *new;
++count; if ((new=malloc (strlen (e)+1))==0) { Error="Нет памяти"; return 0; } return strcpy (new, e); }
excount () { fprintf (stderr, "сч. вып. %d\n", count); return count: }
Примечание
Файл import.h должен включаться ранее файла <stdio.h>.
Теперь нужно ввести файл спецификаций разделяемой библиотеки для последующего выполнения mkshlib(1):
/* новый файл libexam.sl */
1 #target /my/directory/libexam_s 2 #address .text 0x74000000 3 #address .data 0x74100000
4 #branch 5 excopy 1 6 excount 2
7 #objects 8 import.o 9 global.o 10 exam.o
11 #init import. o 12 _libexam_malloc malloc 13 _libexam_strcpy strcpy 14 _libexam_strlen strlen 15 _libexam_fprintf fprintf 16 _libexam__iob _iob
(Номера строк включены для удобства ссылок; они не являются частью примера.) Вкратце, в этом файле спецификаций указано следующее. В строке 1 задается маршрутное имя разделяемой библиотеки выполнения. В дальнейшем библиотека должна быть помещена в этот файл, чтобы использующие ее программы могли выпол- няться. Виртуальные адреса для секций команд и данных библиотеки задаются в строках 2 и 3 соответственно. В строках 4-6 описывается таблица. В строках 5 и 6 функциям excopy() и excount()
назначаются элементы 1 и 2 таблицы переходов. Напомним, что ее элементы должны назначаться только именам внешних функций.
В строках 7-10 перечисляются объектные файлы, из которых и будут собраны разделяемые библиотеки сборки и выполнения. В разделяемой библиотеке сборки каждый из них будет оформлен как отдельный элемент. В разделяемой библиотеке выполнения указанный порядок следования этих файлов сохранится. Первыми нужно указывать файлы данных, иначе, например, изменение размера статических переменных в exam.o привело бы к изменению адресов внешних переменных, после чего ранее созданные программы не смогли бы больше работать с этой библиотекой.
В строках 11-16 находятся данные о внешних именах файла import.o. Их можно понимать как своего рода операторы присваивания, например, _libexam_malloc будет содержать адрес malloc и т.д.
Далее, как и для архивной библиотеки, нужно создать объектные файлы:
cc -c import.c global.c exam.c
Осталось запустить mkshlib(1) для создания разделяемой библиотеки сборки и разделяемой библиотеки выполнения:
mkshlib -s libexam.sl -t libexam_s -h libexam_s.a
Если компиляция исходных текстов прошла нормально, mkshlib(1) создаст разделяемую библиотеку сборки libexam_s.a и разделяемую библиотеку выполнения libexam_s.
Пример программы
Чтобы показать, как работают различные отладочные команды, и проанализировать их выдачу, мы написали программу, которая открывает и читает входной файл и выполняет от одной до трех функций, в зависимости от указанных в командной строке опций. Все, что делает эта программа, можно проделать и с помощью карманного калькулятора; она предназначена лишь для демонстрации отладочных средств.
Приводимая в данном разделе выдача различных отладочных средств может несколько отличаться от выдачи Вашего компьютера. Для получения более подробной информации об этих командах лучше всего обратиться к Справочнику пользователя.
Сначала приведем содержимое файла restate.c, содержащего исходный текст главной программы.
/* Файл с главной программой -- restate.c */
#include <stdio.h> #include "recdef.h"
#define TRUE 1 #define FALSE 0
main (argc, argv) int argc; char *argv []; { FILE *fopen (), *fin; void exit (); int getopt (); int oflag = FALSE; int pflag = FALSE; int rflag = FALSE; int ch; struct rec first; extern int opterr; extern float oppty (), pft (), rfe ();
if (argc < 2) { (void) fprintf (stderr, "%s: Не указана опция\n", argv [0]); (void) fprintf (stderr, "Использование: %s -rpo\n", argv [0]); exit (2); }
opterr = FALSE;
while ((ch = getopt (argc, argv, "opr")) != EOF) { switch (ch) { case 'o': oflag = TRUE; break; case 'p': pflag = TRUE; break; case 'r': rflag = TRUE; break; default: (void) fprintf (stderr,"Использование: %s -opr\n", argv [0]); exit (2); } }
if ((fin = fopen ("info", "r")) == NULL) { (void) fprintf (stderr, "%s: Неудача при открытии файла %s\n", argv [0], "info"); exit (2); }
if (fscanf (fin, "%s%f%f%f%f%f%f", first.pname, &first.ppx, &first.dp, &first.i, &first.c, &first.t, &first.spx) != 7) { (void) fprintf (stderr, "%s: Неудача при чтении первой записи из %s\n", argv [0], "info"); exit (2); }
printf ("Наименование: %s\n", first.pname);
if (oflag) printf ("Приемлемая цена: $%#5.2f\n", oppty (&first));
if (pflag) printf ("Ожидаемая прибыль (потери): $%#7.2f\n", pft (&first));
if (rflag) printf("Фондоотдача: %#3.2f%%\n", rfe (&first)); }
Файл oppty. c содержит исходный текст одноименной функции.
/* Приемлемая цена -- oppty.c */
#include "recdef.h"
float oppty (ps) struct rec *ps; { return (ps->i/12 * ps->t * ps->dp); }
В файле pft.c описана функция для вычисления прибыли.
/* Прибыль -- pft.c */
#include "recdef.h"
float pft (ps) struct rec *ps; { return (ps->spx - ps->ppx + ps->c); }
В файле rfe.c описана функция для вычисления фондоотдачи.
/* Фондоотдача -- rfe.c */
#include "recdef.h"
float rfe (ps) struct rec *ps; { return (100 * (ps->spx - ps->c) / ps->spx); }
Наконец, приведем содержимое включаемого файла recdef.h.
/* Включаемый файл -- recdef.h */
struct rec { /* Структура для хранения исходных данных */ char pname [25]; float ppx; float dp; float i; float c; float t; float spx; };
Пример программы, работающей с terminfo
Программа termhl - это простой пример использования подпрограмм terminfo. Она является вариантом программы highlight (см. раздел Примеры программ, работающих с curses), в котором не используются возможности curses более высокого уровня. Программу termhl можно использовать в качестве фильтра, вставляющего последовательности символов, необходимые для включения режимов подсветки и подчеркивания и для отключения всех атрибутов.
/* Версия программы highlight уровня terminfo */
#include <curses.h> #include <term.h>
int ulmode = 0; /* Режим подчеркивания */
main (argc, argv) int argc; char **argv; { FILE *fd; int c, c2; int outch();
if (argc > 2) { fprintf (stderr, "Откуда: termhl [file]\n"); exit(1); }
if (argc == 2) { fd = fopen (argv [1], "r"); if (fd == NULL) { perror (argv [1]); exit(2); } } else { fd = stdin; } setupterm ((char*) 0, 1, (int*) 0);
for (;;) { c = getc (fd); if (c == EOF) break (); if (c == '\') { c2 = getc (fd); switch (c2) { case 'B': tputs (enter_bold_mode, 1, outch); continue; case 'U': tputs (enter_underline_mode, 1, outch); ulmode = 1; continue; case 'N': tputs (exit_attribute_mode, 1, outch); ulmode = 0; continue; } putch (c); putch (c2); } else putch (c); }
fclose (fd); fflush (stdout); resetterm (); exit (0); }
/* Функция putch подобна putchar, */ /* но проверяет необходимость подчеркивания */
putch (c) int c; { outch (c); if (ulmode && underline_char) { outch ('\b'); tputs (underlihe_char, 1, outch); } }
/* outchar передается tputs в качестве параметра */ /* как функция, которую нужно вызывать вместо putchar */
outch (c) int c; { putchar (c); }
Познакомимся поближе с подпрограммами terminfo на примере использования функции tputs (cap, affcnt, outc) в этой программе. tputs() добавляет символы-заполнители. Некоторые терминалы имеют возможность задерживать вывод. Их описания в базе данных terminfo, быть может, содержат цепочки, подобные $<20>, что означает задержку на 20 миллисекунд (см. ниже раздел Указание характеристик терминала). tputs генерирует достаточное для задержки на это время количество символов-заполнителей.
tputs() имеет три параметра. Первый является цепочкой символов, определяющей характеристику (атрибут вывода), которую необходимо реализовать. Второй - количество затрагиваемых этим строк. (Некоторые запросы требуют добавления символов-заполнителей в количестве, зависящем от этого параметра. Например, запрос на вставку строки, insert_line, может привести к копированию всех строк ниже текущей, для чего нужно время, пропорциональное количеству копируемых строк. Если ни одна строка не затрагивается, в качестве affcnt используется 1. Единица вместо нуля берется для надежности, так как affcnt умножается на некоторый интервал времени, а умножение чего-либо на 0 дает 0). Третий параметр - это подпрограмма, вызываемая для вывода каждого символа.
Для понимания желательности использования подпрограмм уровня curses вместо подпрограмм уровня terminfo (там, где это возможно), полезно обратить внимание на проверку значения underline_char в примере. Некоторые терминалы имеют управляющий символ, указывающий начало подчеркивания, и еще один символ, указывающий конец подчеркивания. Другим необходимо указание кода подчеркивания при выводе каждого символа. Программа termhl хранит значение этого режима, и, если необходимо, выводит underline_char для подчеркивания очередного символа. Из-за наличия такого рода мелких деталей, с которыми программы уровня terminfo
должны разбираться самостоятельно, и предпочтительнее работать на уровне curses.
Программа termhl была написана для иллюстрации применения подпрограмм terminfo, поэтому она получилась сложнее, чем могла бы быть. Вместо непосредственного вывода enter_bold_mode, enter_underline_mode и exit_attribute_mode можно было бы использовать подпрограмму vidattr [см. curses(3X)]. Фактически, в последнем случае программа была бы более надежна, поскольку существует несколько способов изменения атрибутов вывода.
Несмотря на то, что все
Несмотря на то, что все усилия были направлены на точную передачу изображения, появляющегося на экране терминала, возможно, что Ваша система будет формировать несколько другую выдачу, поскольку некоторые экранные изображения зависят от конкретной конфигурации компьютера. Кроме того, различия в модификациях операционной системы также могут приводить к некоторым отличиям изображения.
Мы пытались отладить все включенные в Руководство примеры программ, чтобы они компилировались и работали в том виде, как они приведены в тексте. Имеющиеся в Руководстве фрагменты программ, хоть и не компилировались, написаны с той же степенью аккуратности.
ПРИМЕРЫ ПРОГРАММ, РАБОТАЮЩИХ С CURSES
Далее приводятся примеры, демонстрирующие использование подпрограмм curses.
Присваивание целым переменным long-значений
Присваивание переменным, описанным как int, значений типа long, может вызвать ошибки, поскольку оно сопровождается усечением значения. Это может произойти в программах, которые не полностью переработаны применительно к использованию конструкций typedef. Когда тип переменной, определяемый посредством typedef, изменяется с int на long, программа может перестать работать, если часть промежуточных результатов заносится в int-переменные и при этом усекается. Чтобы подавить сообщения о присваивании long-значений int-переменным, можно использовать опцию -a.
Процессы
В ОС UNIX всякий раз, когда Вы выполняете команду, запускается процесс, идентифицируемый и отслеживаемый операционной системой. Характерной особенностью ОС UNIX является то, что одни процессы могут порождаться другими. Это происходит чаще, чем может показаться. Например, когда Вы входите в систему, запускается процесс, скорее всего это shell. Если затем войти в редактор РК, запустить из него shell и выполнить команду ps -f, на экране появится примерно следующее:
UID PID PPID C STIME TTY TIME COMMAND userow 94 1 0 10:15:56 tty6 0:02 -sh userow 116 94 0 10:16:25 tty6 0:00 -sh userow 125 116 0 10:16:27 tty6 9:38 /dss/rk/rk.20.02 -hrk=/dss/rk/d.hrk -nocore -in=stdin -out=stdout -display=D211 userow 285 125 1 13:11:10 tty6 0:00 sh userow 290 285 10 13:11:58 tty6 0:00 ps -f
Таким образом, у пользователя userow, проделавшего описанные выше действия, имеется 5 активных процессов. Интересно проследить переключения в колонках идентификатор процесса (PID) и идентификатор родительского процесса (PPID). Shell, запущенный при входе пользователя userow в систему, имеет идентификатор процесса 94; его предком является процесс инициализации (его идентификатор равен 1). Процесс с идентификатором 116 порожден для выполнения shell-процедуры rk; он является предком процесса с идентификатором 125 и т.д.
Точно так же новые процессы порождаются при выполнении Ваших собственных программ. Действительно, ввод команды запуска Вашей программы означает запрос к shell'у на порождение еще одного процесса, построенного из Вашего выполняемого файла со всеми функциями, помещенными в него редактором связей.
Можно рассуждать следующим образом: "Хорошо, когда я работаю с компьютером в интерактивном режиме, удобно иметь возможность запускать поочередно различные программы. Но зачем нужно, чтобы из одной программы можно было запускать другую, почему бы не создать один большой выполняемый файл, объединяющий все необходимые программы?"
Если Ваша программа сама является интерактивной и предоставляет пользователю широкий выбор функций, необходимо иметь возможность запускать из нее другие программы, в зависимости от удовлетворения при ее выполнении тех или иных условий. (Например, если конец месяца, то приготовить отчет). Типичные причины, по которым не принято создавать один большой выполняемый файл, состоят в следующем:
Размер выполняемого файла может оказаться слишком большим, что может привести к нарушению системного ограничения на размер процесса.
Не все объектные файлы, которые Вы хотите использовать, могут находиться под Вашим контролем.
Таким образом, имеется достаточно аргументов в пользу необходимости создания новых процессов. Для создания процессов есть три основных средства:
system(3S) - запрос к shell'у на выполнение команды.
exec(2) - завершить один процесс и приступить к выполнению другого.
fork(2) - создать копию данного процесса.
Прочие возможности пакета curses
Умея пользоваться основными подпрограммами curses для ввода/вывода на экран и управления окнами, можно создавать терминальные программы, удовлетворяющие потребности большинства пользователей. Вместе с тем, в curses имеются подпрограммы, которые позволяют делать больше, чем просто вводить и выводить символы и работать с окнами. На следующих нескольких страницах кратко описываются некоторые из этих подпрограмм, которые помогут Вам выводить простые графические изображения, использовать программируемые метки терминала и обращаться из одной программы к нескольким терминалам.
Прежде, чем использовать эти возможности, следует освоить подпрограммы ввода/вывода и управления окнами, которые были рассмотрены выше в этой главе, а также в руководстве curses(3X).
Предостережение
Подпрограммы, описываемые далее в разделах Линии на экране и прочая графика и Использование программируемых меток, имеются только в системе UNIX V версия 3.1. Программа, работающая с ними, возможно, не сможет выполняться на более ранних версиях системы UNIX. Для работы с этими подпрограммами необходимо иметь систему UNIX V версия 3.1 и соответствующую версию библиотеки curses.
Prof(1)
С помощью команды prof(1) можно выяснить, сколько времени было истрачено на выполнение различных фрагментов программы, а также сколько раз вызывалась та или иная функция. Чтобы использовать эту команду, необходимо откомпилировать файлы с опцией -p. После выполнения полученной программы будет сформирован файл mon.out, используемый командой prof для получения статистики. Кроме mon.out, используется файл a.out (или другой выполняемый файл).
Чтобы получить результаты профилирования для нашего примера, необходимо предпринять следующие шаги:
Откомпилировать файлы с опцией -p:
cc -p restate.c oppty.c pft.c rfe.c
Выполнив программу, сформировать файл mon.out:
a.out -opr
Выполнить команду prof:
prof a.out
Ниже приведен результат выполнения последнего шага. Выполняя одну и ту же программу несколько раз подряд, мы можем получать несколько различные цифровые данные. Заметим также, что статистика, выдаваемая для такой маленькой программы как наша, не слишком полезна.
%Time Seconds Cumsecs #Calls msec/call Name 50.0 0.02 0.02 14 1.2 ungetc 50.0 0.02 0.03 mcount% 0.0 0.00 0.03 1 0. rfe 0.0 0.00 0.03 4 0. getopt 0.0 0.00 0.03 1 0. pft 0.0 0.00 0.03 1 0. creat 0.0 0.00 0.03 4 0. printf 0.0 0.00 0.03 2 0. profil 0.0 0.00 0.03 1 0. fscanf 0.0 0.00 0.03 1 0. _doscan 0.0 0.00 0.03 6 0. atof 0.0 0.00 0.03 1 0. _filbuf 0.0 0.00 0.03 3 0. strchr 0.0 0.00 0.03 1 0. strcmp 0.0 0.00 0.03 4 0. ldexp 0.0 0.00 0.03 4 0. frexp 0.0 0.00 0.03 1 0. fopen 0.0 0.00 0.03 1 0. _findiop 0.0 0.00 0.03 1 0. oppty 0.0 0.00 0.03 4 0. _doprnt 0.0 0.00 0.03 1 0. main 0.0 0.00 0.03 3 0. fcvt 0.0 0.00 0.03 3 0. fwrite 0.0 0.00 0.03 4 0. _xflsbuf 0.0 0.00 0.03 1 0. _wrtchk 0.0 0.00 0.03 2 0. _findbuf 0.0 0.00 0.03 4 0. _bufsync 0.0 0.00 0.03 3 0. getenv 0.0 0.00 0.03 2 0. isatty 0.0 0.00 0.03 2 0. ioctl 0.0 0.00 0.03 1 0. malloc 0.0 0.00 0.03 1 0. open 0.0 0.00 0.03 1 0. read 0.0 0.00 0.03 2 0. sbrk 0.0 0.00 0.03 1 0. strcpy 0.0 0.00 0.03 1 0. strlen 0.0 0.00 0.03 5 0. write