Основы программирования СОДЕРЖАНИЕ 1. Введение 2. Выбор языка программирования 2.1. Поддерживаемые языки программирования 2.1.1. Язык C 2.1.2. Фортран 2.1.3. Паскаль 2.1.4. Ассемблер 2.2. Специализированные языки программирования 2.2.1. awk(1) 2.2.2. lex(1) 2.2.3. yacc(1) 2.2.4. m4(1) 2.2.5. bc(1) и dc(1) 2.2.6. curses(3X) 3. Когда программа написана 3.1. Компиляция и редактирование связей 3.1.1. Компиляция C-программ 3.1.2. Компиляция Фортран-программ 3.1.3. Диагностические сообщения при компиляции 3.1.4. Редактирование внешних связей 4. Интерфейс между языком программирования и ОС UNIX 4.1. Почему для иллюстрации интерфейса используется язык C 4.2. Как аргументы передаются в программу 4.3. Системные вызовы и библиотечные функции 4.3.1. Классификация системных вызовов и библиотечных функций 4.3.2. Где можно найти справочную информацию 4.3.3. Как системные вызовы и библиотечные функции используются в C-программах 4.4. Включаемые файлы 4.5. Библиотеки объектных файлов 4.6. Ввод/вывод 4.6.1. Стандартные открытые файлы 4.6.2. Именованные файлы 4.6.3. Низкоуровневый ввод/вывод и почему не стоит им пользо- ваться 4.7. Управление окружением и получение информации о его состоя- нии 4.8. Процессы 4.8.1. system(3s) 4.8.2. exec(2) 4.8.3. fork(2) 4.8.4. Каналы 4.9. Обработка ошибок 4.10. Сигналы и прерывания 5. Анализ и отладка 5.1. Пример программы 5.2. cflow(1) 5.3. ctrace(1) 5.4. cxref(1) 5.5. lint(1) 5.6. prof(1) 5.7. size(1) 5.8. strip(1) 5.9. sdb(1) 6. Средства организации разработки программного обеспечения 6.1. Утилита make(1) 6.2. Работа с архивами 6.3. Использование системы SCCS программистами-одиночками 1. ВВЕДЕНИЕ Данная глава предназначается тем, кто только учится программи- ровать в среде ОС UNIX. По классификации, приведенной в преды- дущей главе, это, как правило, программисты-одиночки, не инте- ресующиеся программированием глубоко. Может оказаться, что в данной главе и в связанной с ней справочной информации имеется все, что им необходимо для подготовки и запуска программ на компьютере. Программистам с более высокими требованиями, например, участву- ющим в различных прикладных проектах или разрабатывающим прог- раммы, которые будут переноситься на другие компьютеры, следует рассматривать эту главу всего лишь как введение. 2. ВЫБОР ЯЗЫКА ПРОГРАММИРОВАНИЯ Какой язык программирования использовать? На этот вопрос можно ответить, например, так: "Я всегда пишу программы на ФОРГОЛе, потому что знаю его лучше всего." В некоторых обстоятельствах это действительно разумное решение. Но если предположить, что Вы знаете более одного языка, что различные языки имеют сильные и слабые стороны, что, зная один язык, относительно легко изу- чить другой, вопрос выбора языка становится более содержатель- ным. При выборе языка можно попытаться ответить на следующие вопросы: Какова природа задачи, которую Вы программируете? Предстоит ли реализовать сложный алгоритм, или же нужно написать простую процедуру из нескольких строк? Имеет ли задача много независимых частей? Можно ли поделить программу на несколько раздельно ком- пилируемых функций, или это будет один модуль? Как скоро программа должна быть готова? Нужно ли написать программу быстро, не заботясь об ее эффективности, или имеется достаточно времени для раз- работки наиболее эффективной программы? Какова область применения программы? Будет ли программа использоваться только ее автором, или она будет широко распространяться? Будет ли программа переноситься на другие системы? Как долго будет эксплуатироваться программа? Будет ли она использована всего несколько раз, или пла- нируется ее применение в течение нескольких лет? 2.1. Поддерживаемые языки программирования В данном разделе речь идет о языках программирования, компиля- торы для которых поступают как вместе с ОС UNIX, так и незави- симо. В будущем планируется расширение спектра доступных язы- ков. 2.1.1. Язык C Язык C тесно связан с ОС UNIX, так как первоначально был разра- ботан именно для реализации ядра операционной системы. Поэтому, в первую очередь, он очень удобен для программирования задач, использующих системные вызовы операционной системы, например, для организации низкоуровнего ввода/вывода, управления памятью или физическими устройствами, организации связи между процесса- ми и т.д. Кроме того, язык C может успешно применяться и для реализации программ, не требующих такого непосредственного вза- имодействия с операционной системой. При выборе языка програм- мирования следует иметь в виду следующие характеристики языка C: Набор типов данных: символьный, целый, длинный целый, вещественный, и вещественный двойной точности. Наличие низкоуровневых возможностей (напомним, что большая часть ядра ОС UNIX написана на C). Возможность определения производных типов данных, таких как массивы, функции, указатели, структуры и объедине- ния. Наличие многомерных массивов. Возможность определения указателей на данные конкретно- го типа и выполнения арифметических действий над указа- телями с учетом типа адресуемых ими данных. Побитные операции. Множество управляющих конструкций: if, if-else, switch, while, do-while и for. Высокая степень мобильности программ. Язык C естественным образом ориентирован на структурное прог- раммирование. Большие программы подразделяются на функции, ко- торые можно считать отдельно компилируемыми единицами. Кроме облегчения внесения изменений в программы, при таком подходе в наибольшей степени реализуется идеология программирования в ОС UNIX: стараться в максимальной степени использовать уже имеющи- еся программы. Язык C довольно труден в изучении. Чтобы научиться программиро- вать на C, используя все его возможности, необходимо несколько месяцев интенсивной практики. Поэтому, если Вы программируете лишь эпизодически, лучше выбрать какой-нибудь другой, более простой язык. 2.1.2. Фортран Несмотря на то, что Фортран является старейшим из языков высо- кого уровня, он и сейчас успешно применяется для математических расчетов. Так, на Фортране удобно программировать задачи ста- тистического анализа и другие научные приложения. Основной целью при проектировании языка было достижение высокой эффек- тивности при выполнении программ. Цель была достигнута, но для этого пришлось в некоторой степени пожертвовать гибкостью язы- ка. Например, имеется только один вид оператора цикла. Кроме того, тексты программ должны иметь довольно жесткий формат. От- метим, правда, что использование препроцессоров несколько сгла- живает указанные недостатки. 2.1.3. Паскаль Поскольку Паскаль первоначально проектировался как язык для обучения программированию, он прост в обращении и получил широ- кое признание. Кроме простоты, к достоинствам языка можно от- нести высокую степень структурированности программ и возмож- ность обращения к системным вызовам (аналогично языку C). Удоб- нее всего использовать Паскаль для небольших программ, что ес- тественно объясняется его первоначальным предназначением. Не- достатками языка являются, например, отсутствие возможности инициализации переменных и недостаточные средства для работы с файлами. 2.1.4. Ассемблер Этот язык наиболее близок к аппаратуре и поэтому является ма- шинно-зависимым и не является мобильным. Необходимость програм- мирования на ассемблере возникает, как правило, при невозмож- ности воспользоваться языками высокого уровня. 2.2. Специализированные языки программирования Кроме описанных языков программирования, в ОС UNIX имеются спе- циализированные языки, перечисленные ниже. 2.2.1. awk(1) awk(1) (буквы в названии языка представляют инициалы его авто- ров) отыскивает во входном файле строки, соответствующие шабло- ну, описанному в файле спецификаций. Найдя такую строку, awk выполняет действия, также заданные в файле спецификаций. Зачас- тую программа из двух строк на awk может сделать то же, что и программа на две страницы на таких языках как C или Фортран. В качестве примера рассмотрим следующую задачу. Пусть имеется множество записей, состоящих из двух полей, первое из которых является ключом, а второе содержит число. Требуется каждую группу записей с одним и тем же ключевым значением заменить од- ной записью с тем же ключом и с числом, равным сумме чисел во всех записях группы. Таким образом, в результирующем наборе за- писей уже не будет совпадающих значений ключа. Если предполо- жить, что исходный набор записей отсортирован по ключевому по- лю, то алгоритм решения задачи может выглядеть, например, так: Прочитать первую запись в рабочую область. Читать записи, пока не встретится признак конца файла (EOF); при этом: Если ключ текущей записи совпадает с ключом записи в рабочей области, прибавить к числу в рабочей об- ласти число из текущей записи. В противном случае добавить запись из рабочей области к результату и поместить в рабочую область текущую запись. Если достигнут конец файла, добавить к результату пос- леднюю запись из рабочей области. Программа на awk, выполняющая те же действия, может быть, нап- ример, такой: { qty[$1] += $2 } END { for (key in qty) print key, qty[key] } Отметим, что в отличие от описанного выше алгоритма, для этой программы не требуется, чтобы входной файл был отсортирован. 2.2.2. lex(1) С помощью lex(1) можно сгенерировать C-программу, которая рас- познает во входном потоке регулярные выражения, специфицирован- ные пользователем, после чего выполняет некоторые действия, также заданные пользователем, и формирует выходной поток для следующей программы. 2.2.3. yacc(1) С помощью yacc(1) можно сгенерировать С-программу для выполне- ния синтаксического анализа входного потока согласно правилам, заданным в файле спецификаций. Кроме описания грамматики, этот файл содержит описания действий, которые должны быть выполнены, когда лексемы во входном потоке сопоставляются с тем или иным правилом грамматики. Для выполнения лексем во входном потоке удобно использовать lex. 2.2.4. m4(1) m4(1) - это макропроцессор, который можно использовать как препроцессор для языка ассемблера и C. 2.2.5. bc(1) и dc(1) bc(1) позволяет использовать терминал как программируемый каль- кулятор. С помощью bc можно выполнить арифметические вычисле- ния, специфицированные в файле или поступающие со стандартного ввода. В действительности bc является препроцессором к dc(1). Можно пользоваться непосредственно dc, но это трудно, поскольку он работает с обратной польской записью. 2.2.6. curses(3X) Хотя в действительности это библиотека C-функций, curses(3X) упоминается здесь, поскольку его функции можно рассматривать как подъязык для выполнения операций с экраном терминала. Если Вы пишете программы, предполагающие организацию экранного ин- терфейса с пользователем, Вам будет полезно знать возможности этого пакета. В заключении краткого обзора, напомним, что не следует упускать возможность использовать shell-процедуры. 3. КОГДА ПРОГРАММА НАПИСАНА Как правило, при компиляции программ в среде ОС UNIX в качестве последних двух фаз выполняются ассемблирование и редактирование внешних связей. При этом ассемблерный текст, то есть результат предыдущих фаз компиляции, транслируется в команды машинного языка компьютера, на котором программа будет выполняться. Ре- дактор внешних связей разрешает все неопределенные ссылки и де- лает объектный файл выполняемым. Для большинства языков прог- раммирования в ОС UNIX ассемблер и редактор связей генерируют Объектные Файлы Обычного Формата. В результате утилиты, обраба- тывающие объектные файлы, могут работать на разных компьютерах под управлением различных версий ОС UNIX, поскольку форматы объектных файлов совпадают. Объектный файл обычного формата содержит: Заголовок файла. Вспомогательный заголовок системы UNIX. Таблицу заголовков секций. Содержимое соответствующих секций. Информацию о настройке ссылок. Информацию о номерах строк. Таблицу имен. Таблицу цепочек. Объектный файл состоит из секций. Обычно их по крайней мере две: .text и .data. В некоторых объектных файлах может быть также секция .bss, содержащая неинициализированные данные. Наз- вание этой секции происходит от мнемоники ассемблерной псевдо- команды bss (block started by symbol). Используя опции компиля- торов, можно добиться включения в объектный файл дополнительной информации. Например, при компиляции с опцией -g добавляются номера строк и другая информация, необходимая для символьной отладки. Даже занимаясь программированием в течение многих лет, можно особенно не задумываться о содержимом и организации объ- ектных файлов обычного формата, поэтому мы не будем сейчас ос- танавливаться на этом вопросе. Обычному формату объектных фай- лов посвящена отдельная глава. 3.1. Компиляция и редактирование связей Команда, с помощью которой можно выполнить компиляцию, зависит от используемого языка программирования. Например: Для C-программ по команде cc(1) выполняется компиляция и редактирование связей. Для выполнения тех же действий в случае Фортран-прог- рамм следует воспользоваться командой svs(1). Настоятельно рекомендуется выполнять компиляцию и редактирова- ние связей в рамках Интегрированной Среды Разработки Программ (ИСРП). ИСРП, в зависимости от расширения имени файла, вызовет нужный компилятор или редактор связей. Файлы с исходными текс- тами C-программ должны иметь расширение .c, Фортран-файлы - расширение .for, файлы, управляющие процессом редактирования внешних связей, - .bld. Выгода от использования ИСРП состоит в том, что не нужно помнить опции команд запуска компиляторов или библиотеки, необходимые для редактирования внешних связей. В рамках ИСРП функционирует отладчик КРОТ. Чтобы воспользовать- ся его услугами, компиляцию и редактирование связей нужно про- изводить с опцией -krot. 3.1.1. Компиляция C-программ Файлы с исходными текстами C-программ должны иметь расширение .c, например mycode.c. Команда вызова компилятора имеет следую- щий вид: cc mycode.c При успешном исходе компиляции после нее будет выполнено редак- тирование связей и сгенерирован выполняемый файл a.out. Для управления процессом компиляции и редактирования связей ко- манда cc(1) имеет несколько опций. Перечислим наиболее употре- бительные из них: -c Подавляется фаза редактирования связей. В этом случае генерируется объектный файл (в нашем приме- ре mycode.o), который позже может быть использован для редактирования связей с помощью команды cc без опции -c. -g Генерируется дополнительная информация о перемен- ных и операторах языка для символьной отладки. Ес- ли Вы планируете отлаживаться в рамках ИСРП, ис- пользуйте вместо -g опцию -krot. -О Объектная программа подвергается оптимизации. В результате применения данной опции сокращается размер объектного файла и увеличивается скорость выполнения. Эта опция логически несовместима с оп- цией -g. Обычно она используется, когда программа уже отлажена. -p Объектная программа допускает использование утили- ты prof(1) для получения временного профиля выпол- нения. Удобно использовать эту опцию для выявления процедур, реализация которых требует совершенство- вания. -o вых_файл Выполняемый файл, полученный после редактирования связей, будет иметь имя вых_файл, а не a.out. Остальные опции, используемые с командой cc, описаны в Справоч- нике пользователя. Если имя файла, указанного в команде cc, оканчивается на .s, считается, что в этом файле содержится программа на языке ас- семблера. В результате пропускаются все фазы, предшествующие ассемблированию. 3.1.2. Компиляция Фортран-программ Файлы с исходными текстами Фортран-программ должны иметь расши- рение .for, для вызова компилятора служит команда svs(1). При необходимости воспользоваться отладчиком следует указывать оп- цию -krot. 3.1.3. Диагностические сообщения при компиляции C-компилятор генерирует сообщения для тех операторов программы, которые не удалось скомпилировать. Как правило, смысл этих со- общений очевиден, но, как и в большинстве компиляторов для дру- гих языков, они часто указывают не сам ошибочный оператор, а оператор, расположенный на некотором расстоянии от него. Напри- мер, если Вы случайно поставите точку с запятой в конце условия оператора if, то последующий подоператор else будет отмечен как синтаксическая ошибка. В случае, когда между if и else имеется блок из нескольких операторов, выданный компилятором номер строки синтаксической ошибки будет существенно отличаться от истинного. Другим распространенным источником синтаксических ошибок являются несбалансированные фигурные скобки. 3.1.5. Редактирование внешних связей Команда ld(1) вызывает редактор внешних связей непосредственно. Однако обычно эта команда не используется. Как правило, приме- няется команда запуска системы компиляции того или иного языка (например, cc), которая сама вызывает редактор связей. Редактор связей объединяет несколько объектных файлов в один, выполняет настройку ссылок, включает процедуры инициализации и генерирует таблицу имен, используемую отладчиком. Разумеется, можно выпол- нять редактирование связей и в случае единственного объектного файла. Результат редактирования связей по умолчанию помещается в файл a.out. Файлы, указанные в команде ld и не являющиеся объектными (имена объектных файлов обычно оканчиваются на .o), рассматриваются как архивные библиотеки или файлы, содержащие директивы редак- тора связей. Команда ld имеет 16 опций, мы опишем четыре из них. -o вых_файл Используя эту опцию, можно сообщить редактору свя- зей имя файла, которое следует использовать вместо a.out. Здесь нужно указать имя, по которому вы хо- тите в будущем вызывать Вашу программу. Конечно, можно достигнуть того же результата, выполнив ко- манду mv a.out progname -l библ Редактор связей будет использовать библиотеку с именем libбибл.a, где библ является цепочкой сим- волов длиной не более 9. Данная опция позволяет дополнить список просматриваемых библиотек. Напри- мер, библиотека libc.a используется по умолчанию при вызове редактора связей посредством cc, а ма- тематическая библиотека libm.a, если это необходи- мо, должна быть указана с помощью данной опции. Опция -l может встречаться в команде ld несколько раз с различными значениями библ. Поскольку библи- отека просматривается, когда встречается ее имя, порядок указания опций -l существен. Наиболее бе- зопасно указывать опцию -l в конце командной стро- ки. Опция -l связана а опцией -L. -L каталог Последовательность поиска библиотек вида libбибл.a изменяется следующим образом: сначала производится поиск в указанном каталоге, а затем в каталогах, принимаемых по умолчанию, обычно /lib и /usr/lib. Опцию удобно применять в случае, если у Вас имеет- ся несколько версий библиотеки и Вы хотите исполь- зовать одну из них. Если библиотека найдена в ука- занном с помощью опции -L каталоге, ее поиск в других каталогах не производится. Поскольку опция -L влияет на поиск библиотек, указанных с помощью опции -l, в командной строке она должна предшест- вовать -l. -u имя В таблицу имен заносится объект имя как непреде- ленный. Такая возможность полезна, когда загружа- ются только библиотечные файлы, поскольку в на- чальный момент таблица имен пуста и нужна какая- либо неразрешенная ссылка, чтобы начать загрузку первой подпрограммы. Если редактор связей вызывается посредством команды cc, в вы- полняемый файл включается процедура инициализации (обычно для C-программ это /lib/crt0.o). После выполнения главной программы процедура инициализации обращается к системному вызову exit(2). Редактор связей воспринимает файлы, содержащие директивы для него. Подробному описанию управляющего языка редактора связей посвящена отдельная глава. 4. ИНТЕРФЕЙС МЕЖДУ ЯЗЫКОМ ПРОГРАММИРОВАНИЯ И ОС UNIX Выполнение программы на компьютере зависит от различных особен- ностей операционной системы. Некоторые из них, например загруз- ка программы в основную память или инициализация выполнения, с точки зрения программы не видны. Они, в сущности, планируются редактором связей заранее, когда он помечает объектный файл как выполняемый. Программистам редко приходится иметь дело с этими вопросами непосредственно. Однако другие вопросы, такие как ввод/вывод, действия с файла- ми, выделение памяти требуют участия программиста. Взаимодейст- вие между программой и операционной системой обычно называется интерфейсом между ними. В данном разделе освещаются следующие темы: Передача аргументов в программу. Системные вызовы и функции. Включаемые файлы и библиотеки. Ввод/Вывод. Процессы. Обработка ошибок, сигналы и прерывания. 4.1. Почему для иллюстрации интерфейса используется язык C Везде далее в этом разделе для иллюстрации интерфейса между языком программирования и операционной системой используется язык C, поскольку механизмы интерфейса ориентированиы на этот язык больше, чем на любой другой язык высокого уровня. Таким образом, в данном разделе в действительности описывается интер- фейс между ОС UNIX и языком C. 4.2. Как аргументы передаются в программу Информация или управляющие параметры могут передаваться в прог- рамму как аргументы командной строки при запуске программы. При этом указанные в командной строке аргументы передаются функции main() через два ее параметра, первый из которых содержит коли- чество аргументов, а второй является массивом указателей на це- почки символов, содержащие передаваемую в качестве аргументов информацию (выполнение любой C-программы начинается с функции с именем main). Заметим, что поскольку количество аргументов программы передается ей во время запуска, программа не обязана знать заранее, сколько аргументов следует ожидать. Обычно параметры функции main() имеют имена argc и argv, хотя это и не обязательно. argc - целое число, равное количеству пе- редаваемых аргументов. Это число всегда больше или равно 1, поскольку сама команда считается первым аргументом, и argv[0] является указателем на цепочку символов, представляющую коман- ду. Остальные элементы массива argv - это указатели на цепочки символов содержащие аргументы. Все упомянутые цепочки символов оканчиваются нулевым байтом. Если во время запуска программы ей будут передаваться аргумен- ты, следует в тексте программы предусмотреть их обработку. Пе- редаваемая информация может, например, содержать: Управляющие параметры. В этом случае передаваемая ин- формация используется для установки внутренних флагов программы с целью управления ходом выполнения. Переменное имя файла. Приведем примеры фрагментов программ, иллюстрирующих эти два способа использования аргументов программы. #include #define FALSE 0 #define TRUE 1 main (argc, argv) int argc; char *argv []; { void exit (); int oflag = FALSE; int pflag = FALSE; /* Функциональные флаги */ int rflag = FALSE; int ch; while ((ch = getopt (argc, argv, "opr")) != EOF) { /* Если имеются опции, присвоить флагам TRUE. Если опций нет, вывести сообщение об ошибке */ 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); } } . . . } #include main (argc, argv) int argc; char *argv []; { FILE *fopen (), *fin; void perror (), exit (); if (argc > 1) { if ((fin = fopen (argv [1], "r")) == NULL) { /* Первое %s - для вывода имени программы. Второе %s - для вывода имени файла, который не удалось открыть */ (void) fprintf (stderr, "%s: неудача при попытке открыть файл %s: ", argv [0], argv [1]); perror (""); exit (2); } } . . . } При разборе shell'ом командной строки аргументами считаются лю- бые последовательности непробельных символов, разделенные про- белами или знаками табуляции. Последовательность символов, зак- люченных в двойные кавычки (например, "abc def"), считается од- ним аргументом, даже если в ней встречаются пробелы или табуля- ции. Разумеется, проверка корректности передаваемых аргументов полностью возлагается на программиста. Кроме argc и argv, у функции main() есть еще один параметр, обычно обозначаемый как envp. Это массив указателей на цепочки символов, образующих окружение. Более подробная информация о envp имеется в Справочнике программиста в статьях exec(2) и en- viron(5). 4.3. Системные вызовы и библиотечные функции Системные вызовы - это запросы программы к ядру операционной системы на выполнение некоторых действий. Библиотечные функции - это заранее запрограммированные модули, которые используются для поддержки некоторых возможностей языка программирования. Внешне обращения к системным вызовам и библиотечным функциям ничем не отличаются и выглядят как вызовы обычных функций язы- ка. Однако некоторые отличия все же имеются: Во время редактирования связей команды, реализующие библиотечные функции, копируются в объектный файл Вашей программы, а коды, реализующие системные вызовы, нахо- дятся в ядре ОС. Библиотечные функции выполняются точно так же, как и Ваши собственные функции. А при выполнении системных вызовов происходит переключение из адресного прост- ранства вызывающего процесса к пространству ядра. Это означает, что несмотря на то, что библиотечные функции уве- личивают размер выполняемого файла, накладные расходы на перек- лючение при их выполнении меньше, чем при выполнении системных вызовов. 4.3.1. Классификация системных вызовов и библиотечных функций Системные вызовы можно достаточно естественно разделить на сле- дующие категории: Доступ к файлам. Действия с файлами и каталогами. Управление процессами. Управление окружением и получение информации о его сос- тоянии. Библиотечные функции можно разделить на категории в зависимости от разделов Справочника программиста, в которых находятся их описания. Однако первая часть раздела 3 (3C и 3S) содержит опи- сания весьма большого числа функций, и имеет смысл провести дальнейшую классификацию. Функции подкласса 3S предоставляют средства эффективно- го буферизованного ввода/вывода. Функции подкласса 3C выполняют различные задачи. И объединяет то, что они хранятся в библиотеке libc.a. Можно разделить эти функции на следующие группы: Действия с цепочками символов. Преобразование символов. Классификация символов. Управление окружением. Управление памятью. В следующей таблице приведен список функций стандартного ввода/ вывода. Все они описаны в разделе 3 Справочника программиста. В Справочнике часто в одной статье описывается не одна, а нес- колько связанных функций. В таблице приводится соответствие названий статей и описываемых в них функций: в левой колонке находится название статьи, а все остальные имена в данной стро- ке - это имена функций, описываемых в статье. │ Имена функций │ Назначение │ ------------------------------- ------------------------------ │fclose fflush │ Закрыть поток или вытолкнуть │ │ │ его буфера. │ │ │ │ │ferror feof clearerr fileno │ Опрос состояния потока │ │ │ │ │fopen freopen fdopen │ Открыть поток. │ │ │ │ │fread fwrite │ Двоичный ввод/вывод. │ │ │ │ │fseek rewind ftell │ Установка текущей позиции по-│ │ │ тока │ │ │ │ │getc getchar fgetc getw │ Считывание символа или слова │ │ │ из потока │ │ │ │ │gets fgets │ Считывание цепочки символов │ │ │ из потока. │ │ │ │ │popen pclose │ Создание и ликвидация канала │ │ │ между программой и командой │ │ │ │ │printf fprintf sprintf │ Вывод с преобразованием по │ │ │ формату. │ │ │ │ │putc putchar fputc putw │ Запись в поток символа или │ │ │ слова. │ │ │ │ │puts fputs │ Запись в поток цепочки сим- │ │ │ волов. │ │ │ │ │scanf fscanf sscanf │ Ввод с преобразованием по │ │ │ формату. │ │ │ │ │setbuf setvbuf │ Назначение буферов для потока│ │ │ │ │system │ Выполнение команды shell'а. │ │ │ │ │tmpfile │ Создание временного файла. │ │ │ │ │tmpnam tempnam │ Создание имен временных фай- │ │ │ лов │ │ │ │ │ungetc │ Вставка символа в поток ввода│ │ │ │ │vprintf vfprintf vsprintf │ Форматный вывод списка аргу- │ │ │ ментов, заданного по правилам│ │ │ varargs. │ ------------------------------- ------------------------------ При использовании всех функций, перечисленных в этой таблице, необходимо включить в программу оператор #include Следующая таблица содержит список функций, предназначенных для обработки цепочек символов. В Справочнике программиста они опи- саны в одной статье string(3C). │ Операции над цепочками символов │ ------------------- ------------------------------------------ │strcat (s1, s2) │Добавить копию s2 к концу s1. │ │ │ │ │strncat (s1, s2, n)│Добавить n символов из s2 к концу s1. │ │ │ │ │strcmp (s1, s2) │Сравнить две цепочки символов. Возвращает│ │ │целое число, меньшее, большее или равное│ │ │нулю в зависимости от того, предшествует│ │ │ли s1 лексикографически s2, следует за ней│ │ │или совпадает с ней. │ │ │ │ │strncmp (s1, s2, n)│Сравнить n символов из двух цепочек. │ │ │ │ │strcpy (s1, s2) │Копировать символы из s2 в s1 до тех пор, │ │ │пока не будет скопирован нулевой байт (\0)│ │ │ │ │strncpy (s1, s2, n)│Скопировать n символов из s2 в s1. Если s2│ │ │содержит более n символов, она будет усе-│ │ │чена, если меньше n - в s1 будут добавлены│ │ │нулевые байты. │ │ │ │ │strdup (s) │Возвращает указатель на новую цепочку сим-│ │ │волов, являющуюся копией s. │ │ │ │ │strchr (s, c) │Возвращает указатель на первое вхождение│ │ │символа c в цепочку s, или NULL, если s │ │ │не содержит c. │ │ │ │ │strrchr (s, c) │Возвращает указатель на последнее вхожде-│ │ │ние символа c в цепочку s, или NULL, │ │ │если s не содержит c. │ │ │ │ │strlen (s) │Возвращает число символов в s до ближайше-│ │ │го нулевого байта │ │ │ │ │strpbrk (s1, s2) │Возвращает указатель на первое вхождение в│ │ │s1 какого-либо символа из s2,либо NULL,ес-│ │ │ли s1 не содержит ни одного символа из s2.│ │ │ │ │strspn (s1, s2) │Возвращает длину начального фрагмента s1, │ │ │состоящего только из символов, содержащих-│ │ │ся в s2. │ │ │ │ │strtok (s1, s2) │Находит включение символов из s2 в s1. │ ------------------- ------------------------------------------ При использовании всех функций, перечисленных в этой таблице, необходимо включить в программу оператор #include Включаемый файл содержит внешние описания функций обработки цепочек символов. Следующая таблица содержит список макросов, предназначенных для классификации ASCII-символов. В Справочнике программиста они описаны в статье ctype(3C). │ Классификация символов │ ------------ ------------------------------------------------- │isalpha (c) │c - буква? │ │ │ │ │isupper (c) │c - большая буква? │ │ │ │ │islower (c) │c - малая буква? │ │ │ │ │isdigit (c) │c - цифра: [0-9]? │ │ │ │ │isxdigit (c)│c - шестнадцатеричная цифра: [0-9], [A-F] или │ │ │ [a-f]? │ │isalnum (c) │c - алфавитно-цифровой символ (буква или цифра)? │ │ │ │ │isspace (c) │c - пробел, табуляция, возврат каретки, перевод │ │ │ строки, вертикальная табуляция или символ пе-│ │ │ рехода к новой странице? │ │ │ │ │ispunct (c) │c - знак пунктуации (то есть не управляющий и не │ │ │ алфавитно-цифровой символ)? │ │ │ │ │isprint (c) │c - печатный символ? [Коды таких символов распо- │ │ │ лагаются в диапазоне от 040 (пробел) до 0176 │ │ │ (тильда).] │ │ │ │ │isgraph (c) │c - печатный символ, но не пробел? │ │ │ │ │iscntrl (c) │c - управляющий символ (код меньше 040) или сим-│ │ │ вол забоя (0177)? │ │ │ │ │isascii (c) │c является ASCII-символом (код меньше 0200)? │ ------------ ------------------------------------------------- При использовании всех функций, перечисленных в этой таблице, необходимо включить в программу оператор #include Эти функции возвращают отличное от нуля значение, если указан- ное в правой части таблицы условие истинно, в противном случае возвращается нуль. Следующие две таблицы содержат список функций и макросов, ис- пользуемых для преобразования символов, целых чисел или цепочек символов из одного представления в другое. │ Имена функций │ Назначение │ ----------------- -------------------------------------------- │ecvt fcvt gcvt │Преобразование вещественного числа в цепочку│ │ │символов. │ │ │ │ │l3tol ltol3 │Преобразование 3-байтного целого числа в│ │ │длинное целое и обратно. │ │ │ │ │strtod atof │Преобразование цепочки символов в веществен-│ │ │ное число двойной точности. │ │ │ │ │strtol atol atoi │Преобразов-е цепочки символов в целое число.│ ----------------- -------------------------------------------- │conv(3C): Преобразование символов │ -------- ----------------------------------------------------- │toupper │Функция преобразования малой буквы в большую. │ │ │ │ │_toupper│Макрос преобразования малой буквы в большую. │ │ │ │ │tolower │Функция преобразования большой буквы в малую. │ │ │ │ │_tolower│Макрос преобразования большой буквы в малую. │ │ │ │ │toascii │Обнуляет у аргумента все биты, не являющиеся частью │ │ │стандартного ASCII-символа; предназначен для достиже-│ │ │ния совместимости с другими системами. │ -------- ----------------------------------------------------- При использовании макросов из последней таблицы необходимо включить в программу оператор #include 4.3.2. Где можно найти справочную информацию Системные вызовы описаны в алфавитном порядке в разделе 2 Спра- вочника программиста. Информация о библиотечных функциях содер- жится в разделе 3. В данном Руководстве выше были описаны функ- ции из первого подраздела раздела 3. В остальных его подразде- лах имеется информация о: 3M - функциях, составляющих математическую библиотеку, libm. 3X - различных специальных функциях. 4.3.3. Как системные вызовы и библиотечные функции используются в C-программах Как правильно пользоваться системными вызовами и библиотечными функциями, объясняется в соответствующих статьях Справочника программиста. Чтобы чтение Справочника было наиболее эффектив- ным, необходимо знать типичную структуру его статей. Рассмот- рим, например, статью gets(3S). GETS(3S) GETS(3S) gets, fgets - чтение цепочки символов из потока СИНТАКСИС #include char *gets (s) char *s; char *fgets (s, n, stream) char *s; int n; FILE *stream; ОПИСАНИЕ Функция gets читает символы из стандартного потока вво- да stdin в область памяти, на которую указывает аргу- мент s. Чтение производится до тех пор, пока не встре- тится перевод строки или конец файла. Символ перевода строки отбрасывается, а прочитанная цепочка ограничива- ется нулевым байтом. Функция fgets считывает (n-1) символов из потока ввода stream в область памяти, на которую указывает аргумент s. Чтение производится до тех пор, пока не встретится перевод строки (в отличие от gets он не отбрасывается) или конец файла. Прочитанная цепочка символов ограничи- вается нулевым байтом. СМ. ТАКЖЕ ferror(3S), fopen(3S), fread(3S), getc(3S), scanf(3S). ДИАГНОСТИКА Если первым прочитанным символом окажется признак конца файла, то есть фактически ни одного символа не будет считано, то обе функции возвращают пустой указатель NULL. Если обнаружена ошибка чтения, например, при по- пытке использовать эти функции для файлов, не открытых на чтение, то также возвращается NULL. В остальных слу- чаях возвращается значение указателя s. В этом примере в одной статье описываются две связанные функ- ции: gets() и fgets(). Обе функции считывают цепочку символов из потока, но делают это несколько по-разному. В разделе ОПИСА- НИЕ объясняется, как действует каждая из них. Раздел СИНТАКСИС содержит информацию о том, как нужно обращать- ся к описываемым функциям из программы. Заметим, что в первой строке этого раздела записано: #include Это означает, что программе, использующей функции gets() или fgets(), необходима информация из включаемого файла стандартно- го ввода/вывода. Обычно оператор #include помещается в начале исходного текста. Ниже будет приведена версия файла , которую можно просмотреть, чтобы понять, что используется функ- циями gets() и fgets(). Далее в разделе СИНТАКСИС приводится формальное описание функ- ций. Из формального описания можно узнать: Тип объекта, возвращаемого функцией. В нашем примере, обе функции gets() и fgets(), возвра- щают указатель на символ. Какой объект или объекты следует передавать функции при вызове. Передаваемые объекты указываются в скобках после имени функции. Например, в нашем примере из формального опи- сания следует, что функции gets() нужно передавать ука- затель на символ. Более подробная информация о переда- ваемых объектах приводится в разделе ОПИСАНИЕ. Как эти объекты будут интерпретироваться функцией. Описание char *s; в функции gets() означает, что s будет рассматриваться, как указатель на символ. Следует иметь в виду, что в языке C при передаче аргументов имя массива преобразу- ется в указатель на начало этого массива. Мы рассмотрели простой пример описания функции gets(). Если Вы хотите проверить себя на более сложном примере, попытайтесь по- нять значение различных элементов описания функции fgets(). Рассматривая функцию fgets(), мы сталкиваемся еще с одной осо- бенностью языка C. Третий аргумент этой функции - stream - есть файл с ассоциированными с ним буферами, описанный как указатель на производный тип FILE. Где определяется этот тип? Правильно! В файле . Завершая обсуждение способов вызова функций, описанных в Спра- вочнике программиста, приведем пример фрагмента программы, в котором вызывается функция gets(): #include main () { char sarray [80]; for (;;) { if (gets (sarray) != NULL) { . . . /* Выполнить что-либо с цепочкой символов */ . . . } } } Можно задать вопрос: "Откуда функция gets() считывает симво- лы?". Ответ: "Со стандартного ввода". Под стандартным вводом обычно понимается то, что вводится с клавиатуры терминала, на котором была набрана команда, запустившая выполнение программы, или вывод другой программы, направленный функции gets(). То, что функция gets() считывает информацию со стандартного ввода, можно узнать из раздела ОПИСАНИЕ в статье Справочника. Действи- тельно, там написано: "Функция gets читает символы из стандарт- ного потока ввода...". Стандартный поток ввода определен в фай- ле . Ниже приводится содержимое файла : #ifndef _NFILE #define _NFILE 20 #define BUFSIZ 1024 /* Размер буфера при выводе в небуферизованный файл */ #define _SBFSIZ 8 typedef struct { int _cnt; unsigned char *_ptr; unsigned char *_base; char _flag; char _file; } FILE; /* Флаг _IOLBF означает, что файловый вывод будет буфе- ризоваться построчно. _IONBF, _IOLBF и _IOFBF могут использоваться не только как флаги, но и как значения "типа" для передачи функции setvbuf */ #define _IOFBF 0000 #define _IOREAD 0001 #define _IOWRT 0002 #define _IONBF 0004 #define _IOMYBUF 0010 #define _IOEOF 0020 #define _IOERR 0040 #define _IOLBF 0100 #define _IORW 0200 #ifndef NULL #define NULL 0 #endif #ifndef EOF #define EOF (-1) #endif #define stdin (&_iob[0]) #define stdout (&_iob[1]) #define stderr (&_iob[2]) #define _bufend(p) _bufendtab[(p)->_file] #define _bufsiz(p) (_bufend(p) - (p)->_base) #ifndef lint #define getc(p) (--(p)->_cnt < 0 ? _filbuf(p) : \ (int) *(p)->_ptr++) #define putc(x, p) (--(p)->_cnt < 0 ? \ _flsbuf((unsigned char) (x), (p)) : \ (int) (*(p)->_ptr++ = \ (unsigned char) (x))) #define getchar() getc(stdin) #define putchar(x) putc((x), stdout) #define clearerr(p) ((void) ((p)->_flag &= \ ~(_IOERR | _IOEOF))) #define feof(p) ((p)->_flag & _IOEOF) #define ferror(p) ((p)->_flag & _IOERR) #define fileno(p) (p)->_file #endif extern FILE _iob[_NFILE]; extern FILE *fopen(), *fdopen(), *freopen(), *popen(), *tmpfile(); extern long ftell(); extern void rewind(), setbuf(); extern char *ctermid(), *cuserid(), *fgets(), *gets(), *tempnam(), *tmpnam(); extern int fclose(), fflush(), fread(), fwrite(), fseek(), fgetc(), getw(), pclose(), printf(), fprintf(), sprintf(), vprintf(), vfprintf(), vsprintf(), fputc(), putw(), puts(), fputs(), scanf(), fscanf(), sscanf(), setvbuf(), system(), ungetc(); extern unsigned char *_bufendtab[]; #define L_ctermid 9 #define L_cuserid 9 #define P_tmpdir "/usr/tmp/" #define L_tmpnam (sizeof (P_tmpdir) + 15) #endif 4.4. Включаемые файлы В предыдущих разделах данной главы часто упоминался файл , был также приведен его полный текст. - это наиболее часто используемый включаемый файл при программирова- нии на C в ОС UNIX. Разумеется, существует много других включа- емых файлов. Включаемые файлы содержат определения и описания, одновременно используемые более чем одной функцией. Как правило, имена вклю- чаемых файлов имеют расширение .h. Содержимое включаемых файлов обрабатывается препроцессором языка C во время компиляции. Для указания препроцессору о необходимости включения файла применя- ется директива #include, которую нужно поместить в текст прог- раммы. Вообще, директивой препроцессора считаются строки прог- раммы, начинающиеся с символа #. Чаще всего используются дирек- тивы #include и #define. Как уже говорилось, директива #include используется для вызова (и обработки) содержимого указанного в ней включаемого файла. Директива #define указывает препроцессо- ру, что в тексте программы необходимо заменить каждое вхождение определяемого имени на цепочку лексем. Например, директива #define _NFILE 20 устанавливает максимальное допустимое количество файлов, однов- ременно открытых программой, равным 20. Полный список директив препроцессора приведен в статье cpp(1). В тексте Справочника программиста упоминается около 45 различ- ных включаемых файлов. При этом всегда в директиве #include имя включаемого файла изображается в угловых скобках <>. Пример: #include Угловые скобки в этом случае обозначают, что препроцессор будет считать, что включаемый файл расположен в определенном катало- ге. Как правило, таким каталогом является /usr/include. Если Вы хотите какие-либо собственные определения или описания сделать доступными для нескольких файлов, Вы можете создать .h-файл с помощью любого редактора, поместить его в подходящий каталог, и указать его имя в директиве #include следующим образом: #include "../defs/rec.h" В данном случае необходимо указать в кавычках ("") относитель- ное маршрутное имя файла. Указывать полное маршрутное имя файла не рекомендуется, так как в этом случае могут возникнуть труд- ности при переносе программ, а также другие организационные проблемы. Чтобы не указывать полные маршрутные имена, можно при компиляции программ пользоваться опцией препроцессора -Iката- лог. Если указана эта опция, препроцессор разыскивает включае- мые файлы, имена которых указаны в кавычках, следующим образом. Сначала производится поиск в каталоге, в котором расположен компилируемый файл, затем в каталогах, указанных в опции (опци- ях) -I, и, наконец, в каталогах из стандартного списка. Заметим также, что все включаемые файлы, имена которых указаны в угло- вых скобках <>, сначала отыскиваются в списке каталогов, задан- ных с помощью опции -I, а затем в каталогах из стандартного списка. 4.5. Библиотеки объектных файлов В ОС UNIX объектные файлы часто объединяют в архивы (библиоте- ки); по соглашению, имена библиотек имеют расширение .a. Напри- мер, в библиотеке libc.a хранятся объектные файлы системных вы- зовов, описанных в разделе 2 Справочника программиста, а также функций (именно функций, а не макросов), описанных в подразде- лах 3S и 3C раздела 3. Как правило, библиотека libc.a находится в каталоге /lib. Кроме /lib, часто используется каталог /usr/ lib, куда помещают прикладные библиотеки. Во время редактирования связей в выполняемый файл подгружаются копии объектных модулей из архива. Если редактирование связей выполняется по команде cc, требуемые модули по умолчанию отыс- киваются в библиотеке libc.a. Если нужно, чтобы поиск произво- дился в библиотеке libбибл.a, отличной от просматриваемой по умолчанию, следует явно указать нужную библиотеку с помощью оп- ции -lбибл. Например, если Ваша программа использует для управ- ления экраном функции из пакета curses(3X), указание опции -lcurses приведет к тому, что редактор связей будет просматривать биб- лиотеки /lib/libcurses.a или /usr/lib/libcurses.a и использо- вать для разрешения ссылок в Вашей программе ту из них, которую найдет первой. Чтобы изменить порядок просмотра архивных библиотек, можно ис- пользовать опцию -Lкаталог. Если в командной строке опция -L указана перед опцией -l, то редактор связей сначала будет ис- кать указанную в опции -l архивную библиотеку в заданном ката- логе, а уже затем в /lib и /usr/lib. Это особенно удобно при тестировании новой версии функции, уже существующей в стандарт- ном архиве. Использование различных версий библиотек возможно, поскольку, разрешив однажды ссылку, редактор связей прекращает дальнейший поиск. Именно поэтому опция -L должна предшествовать опции -l в командной строке. 4.6. Ввод/вывод Ввод/вывод уже упоминался в данной главе в связи с системными вызовами и библиотечными функциями. Стандартный пакет ввода/вы- вода языка C состоит из целого множества библиотечных функций. Кроме того, имеется несколько системных вызовов, предназначен- ных для организации ввода/вывода. Далее вопросы ввода/вывода в языке C будут обсуждаться несколько более подробно. К таким вопросам можно отнести: Создание и удаление файлов. Открытие и закрытие файлов, используемых программой. Передачу информации из файла в программу (чтение). Передачу информации из программы в файл (запись). В данном разделе будут описаны несколько функций, предназначен- ных для передачи информации, но в основном будут обсуждаться функции для различных манипуляций с файлами. 4.6.1. Стандартные открытые файлы Программы могут иметь несколько открытых файлов одновременно. Максимально допустимое число открытых в программе файлов может быть различным для разных систем; как правило, оно равно 20. Число файлов, открытых для выполнения операций стандартного ввода/вывода, задается константой _NFILE во включаемом файле . Обычно имеется три стандартных открытых файла. В файле , примерно в середине его текста, имеются три директивы #define, устанавливающие stdin, stdout и stderr равными адресам _iob[0], _iob[1] и _iob[2] соответственно. Массив _iob содержит информацию о потоках, открытых программой. Номер элемента мас- сива _iob называется также дескриптором файла. По умолчанию в OC UNIX все три стандартных открытых файла ассоциированы с тер- миналом. Так как эти файлы постоянно открыты, то при использовании функ- ций и макросов для выполнения операций ввода/вывода со стан- дартными потоками ввода и вывода stdin и stdout нет необходи- мости открывать и закрывать файлы. Например, упоминавшаяся выше функция gets() читает символы из потока стандартного ввода stdin; функция puts() записывает завершающуюся нулевым байтом цепочку символов в поток стандартного вывода stdout. Имеются и другие функции для работы с этими потоками, например, для орга- низации посимвольного или форматного ввода/вывода и другие. Кроме упомянутых функций, имеются также функции, выполняющие операции ввода/вывода с потоками, отличными от stdin и stdout. К таким функциям относится, например, fprintf(), которая выпол- няет те же действия, что и функция printf(), но вывод при этом направляется в указанный поток, например, stderr. Для того, чтобы при выполнении программы операции чтения и записи выпол- нялись с нужными файлами, можно также воспользоваться средства- ми shell'а. В заключении приведем пример указания потоков ввода и вывода. Пусть требуется отделить сообщения об ошибках и обыч- ный вывод программы, направляемый в stdout. Это может потребо- ваться, например, когда вывод программы будет использоваться в качестве ввода для другой программы. В таком случае для органи- зации обычного вывода и выдачи сообщений можно использовать различные модификации одной и той же функции из пакета стан- дартного ввода/вывода, одна из которых направляет вывод в stdo- ut, а другая - в указанный в качестве ее аргумента поток. 4.6.2. Именованные файлы Чтобы осуществлять операции ввода/вывода с потоками, отличными от stdin, stdout и stderr, необходимо предварительно открыть их. Это можно проделать с помощью стандартной библиотечной функции fopen(). Получив в качестве аргумента маршрутное имя файла (то есть имя, под которым файл зарегистрирован в файловой системе ОС UNIX), функция fopen() организует поток, ассоцииро- ванный с этим файлом, и возвращает указатель на структуру типа FILE. Указатель впоследствии будет передаваться функциям, вы- полняющим запись и чтение. Тип FILE определен во включаемом файле . Чтобы открыть поток, в Вашей программе должна быть соответствущая директива #include и описание вида FILE *fin; Описание говорит о том, что fin является указателем на структу- ру типа FILE. Чтобы связать конкретный файл с таким указателем, то есть открыть файл, ассоциировать с ним поток и поместить в адресуемую указателем структуру информацию об этом потоке, нуж- но включить в программу оператор, подобный следующему: fin = fopen ("filename", "r"); где filename - это маршрутное имя открываемого файла. "r" озна- чает, что файл открывается на чтение. Этот аргумент называется обычно режимом доступа к файлу. Как можно догадаться, имеются режимы чтения, записи, а также одновременно чтения и записи. Так как попытка открыть файл может завершиться неудачей, вызов функции fopen(), как правило, включают в условный оператор. Пример: if ((fin = fopen ("filename", "r")) == NULL) (void) fprintf (stderr, "%s: Неудача при открытии файла %s\n",argv[0], "filename"); Здесь используется тот факт, что функция fopen() возвращает NULL в случае, если указанный файл не удается открыть. Если файл удалось открыть, в последующих операциях ввода/вывода для ссылки на этот файл используется указатель fin. Пример: int c; c = getc (fin); В этом примере макрос getc() считывает из потока один символ и помещает его в целую переменную c. Хотя из потока считываются символы, переменная c описана как целая, поскольку макрос getc() возвращает целое. Чтение символа часто включается в ка- кую-либо управляющую конструкцию, например так: while ((c = getc (fin)) != EOF) . . . Здесь символы будут считываться до тех пор, пока не будет дос- тигнут конец файла. Константы EOF, NULL, а также макрос getc() определены в . getc() и другие функции и макросы, сос- тавляющие пакет стандартного ввода/вывода, поддерживают продви- жение указателя в буфере, ассоциированном с файлом. В случае достижения указателем конца буфера необходимая подкачка симво- лов из файла в буфер (или запись символов из буфера в файл, в случае вывода) производятся функциями стандартного ввода/вывода и самой ОС UNIX. Эти действия системы не видны программе и программисту. Для разрыва связи между дескриптора файла в Вашей программе и файлом, то есть для его закрытия, используется функция fclo- se(). После ее успешного выполнения дескриптор может быть зак- реплен за другим файлом с помощью следующего вызова функции fo- pen(). Такое переиспользование дескрипторов для различных пото- ков может быть необходимым, если Ваша программа открывает много файлов. Для выходных файлов рекомендуется вызывать fclose(), поскольку это гарантирует, что перед закрытием файла будет вы- ведено все содержимое ассоциированного с ним буфера. Системный вызов exit() закрывает все открытые в программе файлы, однако этот вызов еще и полностью завершает выполнение программы, поэ- тому его использование безопасно, только если Вы уверены, что сделано все, что нужно. 4.6.3. Низкоуровневый ввод/вывод и почему не стоит им пользоваться Под низкоуровневым понимается ввод/вывод с применением систем- ных вызовов, описанных в разделе 2 Справочника программиста, а не функций и макросов из стандартного пакета ввода/вывода. Даже если кажется, что эти системные вызовы очень подходят для реше- ния Ваших проблем, без них, скорее всего, можно обойтись. Можно много лет программировать на C в ОС UNIX, не используя систем- ные вызовы для организации ввода/вывода и доступа к файлам. Ис- пользование этих системных вызовов не рекомендуется, поскольку их реализация более системно-зависима, чем соответствующих функций из пакета стандартного ввода/вывода. В результате прог- раммы будут менее мобильными и, видимо, не более эффективными. 4.7. Управление окружением и получение информации о его состоянии В некоторых обстоятельствах может потребоваться информация об окружении на компьютере, а также возможность управления окруже- нием. Для этого имеется набор системных вызовов. Некоторые из них приведены в следующей таблице: │ Имена функций │ Назначение │ ---------------------- --------------------------------------- │chdir │Изменение текущего каталога. │ │ │ │ │chmod │Изменение режима доступа к файлу. │ │ │ │ │chown │Изменение владельца и группы файла. │ │ │ │ │getpid getpgrp getppid│Получение идентификаторов процесса. │ │ │ │ │getuid geteuid getgid │Получение идентификаторов пользователя.│ │ │ │ │ioctl │Управление устройствами. │ │ │ │ │link unlink │Создание или удаление ссылки на файл. │ │ │ │ │mount umount │Монтирование/размонтирование файловой │ │ │системы. │ │ │ │ │nice │Изменение приоритета процесса │ │ │ │ │stat fstat │Получение статуса файла │ │ │ │ │time │Получение системного времени. │ │ │ │ │ulimit │Получение или изменение ограничений │ │ │процесса. │ │ │ │ │uname │Получение имени текущей UNIX-системы. │ ---------------------- --------------------------------------- Как можно заметить, многие из приведенных в таблице функций эк- вивалентны соответствующим командам shell'а. Действительно, не- обходимые действия по управлению окружением можно выполнить с помощью shell'а. Тем не менее, упомянутые функции могут исполь- зоваться в C-программах как часть интерфейса между ОС UNIX и языком C. Описание этих функций содержится в разделе 2 Справоч- ника программиста. 4.8. Процессы В ОС 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) - создать копию данного процесса. 4.8.1. system(3S) Формальное описание функции system() выглядит так: #include int system (string) char *string; Здесь аргумент string трактуется shell'ом как командная строка. Таким образом, string может содержать имя и аргументы любой вы- полняемой программы или стандартной команды ОС UNIX. Если аргу- менты передаваемой команды заранее не известны, то для формиро- вания нужного значения string можно воспользоваться функцией sprintf(). Возвращаемое функцией system() значение является ко- дом завершения shell'а. При обращении к system() вызвавшая программа ожидает завершения выполнения переданной команды, а затем продолжает выполнение со следующего выполняемого операто- ра. 4.8.2. exec(2) exec() - это наименование целого семейства функций: execv(), execl(), execle(), execve(), execlp() и execvp(). Все они прев- ращают вызвавший процесс в другой. Отличия между функциями зак- лючаются в способе представления аргументов. Например, функция execl() может быть вызвана следующим образом: execl ("/bin/prog", "prog", arg1, arg2, (char*) 0); Аргументы функции execl() имеют следующий смысл: /bin/prog Маршрутное имя нового выполняемого файла. prog Имя, которое новый процесс получит как argv[0]. arg1, ... Указатели на цепочки символов, содержащие аргументы программы prog. (char *) 0 Пустая ссылка, отмечающая конец списка ар- гументов. Более подробная информация об этих функциях содержится в Спра- вочнике программиста. Ключевым свойством функций семейства exec является то, что в случае их успешного завершения управление не возвращается, поскольку вызвавший процесс заменен новым. При этом новый процесс наследует у старого его идентификатор и дру- гие характеристики. Если обращение к exec() завершается неуда- чей, управление возвращается в вызвавшую программу с результа- том -1. Причина неудачи может быть установлена по значению пе- ременной errno (см. ниже). 4.8.3. fork(2) Системный вызов fork() создает новый процесс - точную копию вызвавшего процесса. В этом случае новый процесс называется по- рожденным процессом, а вызвавший процесс - родительским. Единственное существенное отличие между этими двумя процессами заключается в том, что порожденный процесс имеет свой уникаль- ный идентификатор. В случае успешного завершения fork() возвра- щает порожденному процессу 0, а родительскому процессу - иден- тификатор порожденного процесса. Идея получения двух одинаковых процессов может показаться несколько странной, однако: Поскольку возвращаемые значения различны для порожден- ного и родительского процессов, процессы могут выпол- няться по-разному в зависимости от возвращаемого значе- ния. Порожденный процесс может сказать: "Отлично, я - порож- денный процесс. Скорее всего я превращусь в совершенно другой процесс с помощью вызова exec()." Родительский процесс может сказать: "Порожденный мной процесс будет превращен в новый вызовом exec(). А я вы- зову wait(2) и буду ждать, пока этот процесс не завер- шится." Чтобы переложить приведенные рассуждения на язык C, можно вклю- чить в программу операторы, аналогичные следующим: #include int ch_stat, ch_pid, status; char *progarg1; char *progarg2; void exit (); extern int errno; if ((ch_pid = fork ()) < 0 ) { /* Вызов fork завершился неудачей ... проверка переменной errno */ } else if (ch_pid == 0) { /* Порожденный процесс */ (void) execl ("/bin/prog","prog",arg1,arg2,(char*)0); exit (2); /* execl() завершился неудачей */ } else { /* Родительский процесс */ while ((status = wait (&ch_stat)) != ch_pid) { if (status < 0 && errno == ECHILD) break; errno = 0; } } Поскольку при вызове exec() идентификатор порожденного процесса наследуется новым процессом, родительский процесс знает этот идентификатор. Действие приведенного фрагмента программы сво- дится к остановке выполнения одной программы и запуску другой, с последующим возвращением в ту точку первой программы, где бы- ло остановлено ее выполнение. В точности то же самое происходит при вызове функции system(3S). Действительно, реализация sys- tem() такова, что при ее выполнении вызываются fork(), exec() и wait(). Следует иметь в виду, что в данный пример включено минимальное количество проверок различных ошибок. Так, необходимо проявлять осторожность при совместной работе с файлами. Помимо именован- ных файлов, новый процесс, порожденный вызовом fork() или exec(), наследует три открытых файла: stdin, stdout и stderr. Если вывод родительского процесса буферизован и должен появить- ся до того, как порожденный процесс начнет свой вывод, буфера должны быть вытолкнуты до вызова fork(). А если родительский и порожденный процессы читают из некоторого потока, то прочитан- ное одним процессом уже не может быть прочитано другим, пос- кольку при чтении продвигается указатель текущей позиции в фай- ле. 4.8.4. Каналы В окружении ОС UNIX при работе в рамках shell'а часто использу- ются каналы, то есть выполнение программ огранизуется таким об- разом, что вывод одной программы является вводом другой. Напри- мер, чтобы узнать количество архивных файлов на Вашем компьюте- ре, можно ввести следующую команду: echo /lib/*.a /usr/lib/*.a | wc -w Эта команда выводит имена всех файлов из каталогов /lib и /usr/lib, оканчивающиеся на .a, и направляет результат команде wc(1), которая подсчитывает количество файлов. Особенностью интерфейса между ОС UNIX и языком C является воз- можность создания каналов между Вашим процессом и выполняемой shell'ом командой, или между двумя взаимодействующими процес- сами. В первом случае используется функция popen(3S), входящая в стандартный пакет ввода/вывода, а во втором - системный вызов pipe(2). Функция popen() напоминает функцию system() тем, что она вызы- вает выполнение указанной команды shell'а. Отличие заключается в том, что при использовании функции popen() между вызвавшей ее программой и командой создается канал. С помощью функций пакета стандартного ввода/вывода можно выводить символы и цепочки сим- волов в этот канал точно так же, как в stdout или именованные файлы. Канал остается открытым до тех пор, пока не будет вызва- на функция pclose(). Типичным применением popen() является ор- ганизация канала для выдачи информации на устройство печати ко- мандой lp(1): #include main () { FILE *pptr; char *outstring; if ((pptr = popen ("lp", "w")) != NULL) { for (;;) { . . . /* Организация вывода */ . . . (void) fprintf (pptr, "%s\n", outstring); . . . } . . . pclose (pptr); } . . . } 4.9. Обработка ошибок Внутри C-программ необходимо проверять с требуемой строгостью правильность данных, а также допустимость значений, возвращае- мых функциями. Для системных вызовов, описанных в разделе 2 Справочника программиста, имеется стандартный способ определе- ния вероятной причины неудачного завершения. В случае неудачного завершения почти всегда системные вызовы ОС UNIX возвращают вызвавшей программе значение -1. (Если Вы прос- мотрите описание системных вызовов в разделе 2, Вы увидите, что имеется все же несколько вызовов, для которых возвращаемое зна- чение не определено, но это исключения.) При неудачном заверше- нии, кроме возврата значения -1, системные вызовы помещают код ошибки во внешнюю переменную errno. Чтобы переменная errno была доступна Вашей программе, необходимо включить в программу опе- ратор #include При успешном завершении системного вызова значение переменной errno не изменяется, поэтому оно имеет смысл только в случае, когда какой-либо системный вызов вернул -1. Список кодов ошибок приведен в Справочнике программиста в статье intro(2). Для того, чтобы по коду ошибки, помещенному в errno, вывести в стандартный протокол сообщение об ошибке, можно воспользоваться функцией perror(3C). 4.10. Сигналы и прерывания Термины сигнал и прерывание в контексте ОС UNIX являются сино- нимами: оба означают сообщения, посылаемые операционной систе- мой выполняющимся процессам. Как правило, результатом получения сигнала является прекращение выполнения процесса. Некоторые сигналы генерируются, если процесс пытается выполнить недопус- тимые действия; другие сигналы могут инициироваться обычным пользователем для своих собственных процессов, или суперпользо- вателем для всех процессов. Чтобы послать сигнал из одного процесса другому, также имеющему Ваш идентификатор пользователя, можно воспользоваться системным вызовом kill(2). Он имеет следующий формат: int kill (pid, sig) int pid, sig; где pid - это идентификатор процесса, которому посылается сиг- нал, а sig - целое число от 1 до 19, обозначающее посылаемый сигнал. Название kill является некоторым преувеличением - дале- ко не все сигналы смертельны. Ниже приведены некоторые сигналы, определенные во включаемом файле . #define SIGHUP 1 /* Освобождение линии */ #define SIGINT 2 /* Прерывание */ #define SIGQUIT 3 /* Выход */ #define SIGILL 4 /* Некорректная команда. Не пере- устанавливается при перехвате */ #define SIGTRAP 5 /* Трассировочное прерывание. Не пере- устанавливается при перехвате */ #define SIGIOT 6 /* Машинная команда IOT */ #define SIGABRT 6 /* Рекомендуемый синоним предыдущего */ #define SIGEMT 7 /* Машинная команда EMT */ #define SIGFPE 8 Исключительная ситуация при выполнении операции с вещественными числами */ #define SIGKILL 9 /* Уничтожение процесса. Не перехватывается и не игнорируется */ #define SIGBUS 10 /* Ошибка шины */ #define SIGSEGV 11 /* Некорректное обращение к сегменту памяти */ #define SIGSYS 12 /* Некорректный параметр системного вызова */ #define SIGPIPE 13 /* Запись в канал, из которого некому читать */ #define SIGALRM 14 /* Будильник */ #define SIGTERM 15 /* Программный сигнал завершения */ #define SIGUSR1 16 /* Определяемый пользователем сигнал 1 */ #define SIGUSR2 17 /* Определяемый пользователем сигнал 2 */ #define SIGCLD 18 /* Завершение порожденного процесса */ #define SIGPWR 19 /* Ошибка питания */ /* Сигналы SIGWIND и SIGPHONE используются только в UNIX/PC */ /*#define SIGWIND 20 */ /* Изменение окна */ /*#define SIGPHONE 21*/ /* Изменение строки состояния */ #define SIGPOLL 22 /* Регистрация выборочного события */ #define NSIG 23 /* Максимально допустимый номер сигнала. Сигналы могут иметь номера от 1 до NSIG-1 */ #define MAXSIG 32 /* Размер u_signal[], NSIG-1<=MAXSIG. MAXSIG больше, чем сейчас необходимо. В будущем, возможно, будут добавлены новые сигналы, при этом не придется менять user.h */ С помощью системного вызова signal(2) можно выбрать один из трех возможных способов реакции на получаемые сигналы. Имеется возможность: Установить стандартную реакцию на сигнал. Игнорировать сигнал. Задать собственную функцию для обработки сигнала. 5. АНАЛИЗ И ОТЛАДКА ПРОГРАММ В ОС UNIX имеется несколько команд, помогающих обнаруживать сделанные ошибки или предупреждающих о возможных неприятностях. 5.1. Пример программы Чтобы показать, как работают различные отладочные команды, и проанализировать их выдачу, мы написали программу, которая отк- рывает и читает входной файл и выполняет от одной до трех функ- ций, в зависимости от указанных в командной строке опций. Все, что делает эта программа, можно проделать и с помощью карманно- го калькулятора; она предназначена лишь для демонстрации отла- дочных средств. Приводимая в данном разделе выдача различных отладочных средств может несколько отличаться от выдачи Вашего компьютера. Для по- лучения более подробной информации об этих командах лучше всего обратиться к Справочнику пользователя. Сначала приведем содержимое файла restate.c, содержащего исход- ный текст главной программы. /* Файл с главной программой -- restate.c */ #include #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; }; 5.2. cflow(1) Команда cflow(1) выдает граф внешних ссылок для C-, YACC-, LEX-, а также ассемблерных и объектных файлов. Используя файлы нашего примера, с помощью команды cflow restate.c oppty.c pft.c rfe.c можно получить следующий результат: 1 main: int(), 2 fprintf: <> 3 exit: <> 4 getopt: <> 5 fopen: <> 6 fscanf: <> 7 printf: <> 8 oppty: float(), 9 pft: float(), 10 rfe: float(), Та же команда с опцией -r заменяет отношение "вызывающий-вызы- ваемый" на обратное. Результат выполнения команды: 1 exit: <> 2 main : <> 3 fopen: <> 4 main : 2 5 fprintf: <> 6 main : 2 7 fscanf: <> 8 main : 2 9 getopt: <> 10 main : 2 11 main: int(), 12 oppty: float(), 13 main : 2 14 pft: float(), 15 main : 2 16 printf: <> 17 main : 2 18 rfe: float(), 19 main : 2 Команда cflow с опцией -ix включает в результат ссылки на внеш- ние и статические данные. В нашем примере есть только одна та- кая ссылка - opterr. Результат: 1 main: int(), 2 fprintf: <> 3 exit: <> 4 opterr: <> 5 getopt: <> 6 fopen: <> 7 fscanf: <> 8 printf: <> 9 oppty: float(), 10 pft: float(), 11 rfe: float(), Если указать и опцию -r, и опцию -ix, результат будет следую- щим: 1 exit: <> 2 main : <> 3 fopen: <> 4 main : 2 5 fprintf: <> 6 main : 2 7 fscanf: <> 8 main : 2 9 getopt: <> 10 main : 2 11 main: int(), 12 oppty: float(), 13 main : 2 14 opterr: <> 15 main : 2 16 pft: float(), 17 main : 2 18 printf: <> 19 main : 2 20 rfe: float(), 21 main : 2 5.3. ctrace(1) Используя команду ctrace(1), можно проследить выполнение C- программы по операторам. ctrace читает исходный текст программы из файла и добавляет в него операторы печати для вывода значе- ний переменных после каждого выполненного оператора. Результат выполнения команды следует направить во временный .c-файл, ко- торый затем использовать как входной для команды cc. Во время выполнения полученного в разультате файла a.out будет генериро- ваться вывод, в котором содержится много полезной информации о событиях в Вашей программе. С помощью опций команды ctrace можно ограничить количество ите- раций при выполнении циклов. В исходный текст программы можно ставить функции, включающие и выключающие трассировку. Таким образом можно организовать трассировку только необходимых участков программы. При обращении к ctrace можно указать только один файл с C-прог- раммой. Поэтому, чтобы проиллюстрировать действие ctrace на на- шем примере, необходимо выполнить следующие команды: ctrace restate.c > ct1.c ctrace oppty.c > ct2.c ctrace pft.c > ct3.c ctrace rfe.c > ct4.c Здесь имена выходных файлов выбраны совершенно произвольно. Можно использовать любые подходящие имена. Выбранные имена фай- лов должны оканчиваться на .c, поскольку эти файлы будут ис- пользоваться как входные для системы компиляции языка C: cc -o ctt ct1.c ct2.c ct3.c ct4.c Затем команда ctt -opr выдаст приведенный ниже результат на стандартный вывод (stdo- ut). (Напомним, что программа читает исходные данные из файла info.) Разумеется, этот результат можно направить в какой-ни- будь файл или вывести на печать для последующей обработки или изучения. 9 main (argc, argv) 24 if (argc < 2) /* argc == 2 */ 32 opterr = FALSE; /* FALSE == 0 */ /* opterr == 0 */ 34 while ((ch = getopt (argc, argv, "opr")) != EOF) /* argc == 2 */ /* argv == 2147483384 */ /* ch == 111 or 'o' */ { 35 switch (ch) /* ch == 111 or 'o' */ 36 case 'o': 37 oflag = TRUE; /* TRUE == 1 */ /* oflag == 1 */ 38 break; 50 } 34 while ((ch = getopt (argc, argv, "opr")) != EOF) /* argc == 2 */ /* argv == 2147483384 */ /* ch == 112 or 'p' */ { 35 switch (ch) /* ch == 112 or 'p' */ 39 case 'p': 40 pflag = TRUE; /* TRUE == 1 */ /* pflag == 1 */ 41 break; 50 } 34 while ((ch = getopt (argc, argv, "opr")) != EOF) /* argc == 2 */ /* argv == 2147483384 */ /* ch == 114 or 'r' */ { 35 switch (ch) /* ch == 114 or 'r' */ 42 case 'r': 43 rflag = TRUE; /* TRUE == 1 */ /* rflag == 1 */ 44 break; 50 } 34 while ((ch = getopt (argc, argv, "opr")) != EOF) /* argc == 2 */ /* argv == 2147483384 */ /* ch == -1 */ 52 if ((fin = fopen ("info", "r")) == NULL) /* fin == 1052602 */ 59 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) /* fin == 1052602 */ /* first.pname == 2147483294 */ 68 printf ("Наименование: %s\n", first.pname); /* first.pname == 2147483294 or "Tutti_Frutti" */ Наименование: Tutti_Frutti 70 if (oflag) /* oflag == 1 */ 71 printf ("Приемлемая цена: $%#5.2f\n", oppty (&first)); 5 oppty (ps) 8 return (ps->i/12 * ps->t * ps->dp); /* ps->i == 1074108825 */ /* ps->t == 1073741824 */ /* ps->dp == 1074339512 */ Приемлемая цена: $4321.00 73 if (pflag) /* pflag == 1 */ 74 printf ("Ожидаемая прибыль (потери): $%#7.2f\n", pft (&first)); 5 pft (ps) 8 return (ps->spx - ps->ppx + ps->c); /* ps->spx == 1076101120 */ /* ps->ppx == 1072693248 */ /* ps->c == 1073259479 */ Ожидаемая прибыль (потери): $12345.00 77 if (rflag) /* rflag == 1 */ 78 printf("Фондоотдача: %#3.2f%%\n", rfe (&first)); 5 rfe (ps) 8 return (100 * (ps->spx - ps->c) / ps->spx); /* ps->spx == 1076101120 */ /* ps->c == 1073259479 */ Фондоотдача: 95.00% /* return */ Используя в качестве примера правильно работающую программу, трудно продемонстрировать возможности ctrace. Интереснее было бы обнаружить с помощью ctrace ошибку. По-видимому, эта утилита наиболее полезна в случае, когда программа выполняется до кон- ца, но ее результат не совпадает с ожидаемым. 5.4. cxref(1) Команда cxref(1) анализирует группу .c-файлов и строит для каж- дого файла таблицу перекрестных ссылок на автоматические, ста- тические и глобальные имена. Ниже приводится результат выполнения команды cxref -c -o cx.op restate.c oppty.c pft.c rfe.c Этот результат помещается в файл, в нашем примере это cx.op. Опция -c приводит к тому, что результат выполнения команды для четырех указанных файлов сливается в одну общую таблицу перек- рестных ссылок. restate.c: oppty.c: pft.c: rfe.c: SYMBOL FILE FUNCTION LINE BUFSIZ /usr/include/stdio.h -- *12 EOF /usr/include/stdio.h -- 43 *44 restate.c -- 34 FALSE restate.c -- *7 16 17 18 32 FILE /usr/include/stdio.h -- *23 L_ctermid /usr/include/stdio.h -- *79 L_cuserid /usr/include/stdio.h -- *80 L_tmpnam /usr/include/stdio.h -- *82 NULL /usr/include/stdio.h -- 40 *41 restate.c -- 52 P_tmpdir /usr/include/stdio.h -- *81 TRUE restate.c -- *6 37 40 43 _IOE F /usr/include/stdio.h -- *35 _IOERR /usr/include/stdio.h -- *36 _IOFBF /usr/include/stdio.h -- *30 _IOLBF /usr/include/stdio.h -- *37 _IOMYBUF /usr/include/stdio.h -- *34 _IONBF /usr/include/stdio.h -- *33 _IOREAD /usr/include/stdio.h -- *31 _IORW /usr/include/stdio.h -- *38 _IOWRT /usr/include/stdio.h -- *32 _NFILE 9 *10 67 _SBFSIZ /usr/include/stdio.h -- *15 _base /usr/include/stdio.h -- *20 _bufend() /usr/include/stdio.h -- *51 _bufendtab /usr/include/stdio.h -- *77 _bufsiz() /usr/include/stdio.h -- *52 _cnt /usr/include/stdio.h -- *18 _file /usr/include/stdio.h -- *22 _flag /usr/include/stdio.h -- *21 _iob /usr/include/stdio.h -- *67 restate.c main 25 27 46 53 62 _ptr /usr/include/stdio.h -- *19 argc restate.c -- 9 restate.c main *10 24 34 argv restate.c -- 9 restate.c main *11 26 28 34 47 55 64 c ./recdef.h -- *8 pft.c pft 8 restate.c main 60 rfe.c rfe 8 ch restate.c main *19 34 35 clearerr() /usr/include/stdio.h -- *61 ctermid() /usr/include/stdio.h -- *71 cuserid() /usr/include/stdio.h -- *71 dp ./recdef.h -- *6 oppty.c oppty 8 restate.c main 60 exit() restate.c main *14 29 48 56 65 fclose() /usr/include/stdio.h -- *72 fdopen() /usr/include/stdio.h -- *68 feof() /usr/include/stdio.h -- *62 ferror() /usr/include/stdio.h -- *63 fflush() /usr/include/stdio.h -- *72 fgetc() /usr/include/stdio.h -- *72 fgets() /usr/include/stdio.h -- *71 fileno() /usr/include/stdio.h -- *64 fin restate.c main *13 52 59 first restate.c main *20 59 60 61 68 71 75 78 fopen() /usr/include/stdio.h -- *68 restate.c main 13 52 fprintf() /usr/include/stdio.h -- *73 restate.c main 25 27 46 53 62 fputc() /usr/include/stdio.h -- *74 fputs() /usr/include/stdio.h -- *75 fread() /usr/include/stdio.h -- *72 freopen() /usr/include/stdio.h -- *68 fscanf() /usr/include/stdio.h -- *75 restate.c main 59 fseek() /usr/include/stdio.h -- *72 ftell() /usr/include/stdio.h -- *69 fwrite() /usr/include/stdio.h -- *72 getc() /usr/include/stdio.h -- *55 getchar() /usr/include/stdio.h -- *59 getopt() restate.c main *15 34 gets() /usr/include/stdio.h -- *71 getw() /usr/include/stdio.h -- *73 i ./recdef.h -- *7 oppty.c oppty 8 restate.c main 60 lint /usr/include/stdio.h -- 54 main() restate.c -- *9 oflag restate.c main *16 37 70 oppty() oppty.c -- *5 restate.c main *22 71 opterr restate.c main *21 32 p /usr/include/stdio.h -- 51 *51 52 *52 55 *55 56 *56 57 58 61 *61 62 *62 63 *63 64 *64 pclose() /usr/include/stdio.h -- *73 pflag restate.c main *17 40 73 pft() pft.c -- *5 restate.c main *22 75 pname ./recdef.h -- *4 restate.c main 59 68 popen() /usr/include/stdio.h -- *68 ppx ./recdef.h -- *5 pft.c pft 8 restate.c main 60 printf() /usr/include/stdio.h -- *73 restate.c main 68 71 74 78 ps oppty.c -- 5 oppty.c oppty *6 8 pft.c -- 5 pft.c pft *6 8 rfe.c -- 5 rfe.c rfe *6 8 putc() /usr/include/stdio.h -- *56 putchar() /usr/include/stdio.h -- *60 puts() /usr/include/stdio.h -- *75 putw() /usr/include/stdio.h -- *74 rec ./recdef.h -- *3 oppty.c oppty 6 pft.c pft 6 restate.c main 20 rfe.c rfe 6 rewind() /usr/include/stdio.h -- *70 rfe() restate.c main *22 78 rfe.c -- *5 rflag restate.c main *18 43 77 scanf() /usr/include/stdio.h -- *75 setbuf() /usr/include/stdio.h -- *70 setvbuf() /usr/include/stdio.h -- *76 sprintf() /usr/include/stdio.h -- *73 spx ./recdef.h -- *10 pft.c pft 8 restate.c main 61 rfe.c rfe 8 sscanf() /usr/include/stdio.h -- *75 stderr /usr/include/stdio.h -- *49 restate.c -- 25 27 46 53 62 stdin /usr/include/stdio.h -- *47 stdout /usr/include/stdio.h -- *48 system() /usr/include/stdio.h -- *76 t ./recdef.h -- *9 oppty.c oppty 8 restate.c main 61 tempnam() /usr/include/stdio.h -- *71 tmpfile() /usr/include/stdio.h -- *68 tmpnam() /usr/include/stdio.h -- *71 ungetc() /usr/include/stdio.h -- *76 vfprintf() /usr/include/stdio.h -- *74 vprintf() /usr/include/stdio.h -- *74 vsprintf() /usr/include/stdio.h -- *74 x /usr/include/stdio.h -- *56 57 58 60 *60 5.5. lint(1) Утилита lint(1) обнаруживает в C-программах конструкции, ко- торые могут привести к ошибкам во время выполнения, расточитель- но используют ресурсы или могут снизить мобильность программ. Например, в результате выполнения команды lint restate.c oppty.c pft.c rfe.c получаем: restate.c: restate.c ============== (79) warning: main() returns random value to invocation environment oppty.c: pft.c: pfe.c: ============== function returns value which is always ignored printf Для облегчения ориентации в тексте программ сообщения об ошиб- ках содержат номера строк. 5.6. 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 3 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 5.7. size(1) Команда size(1) выдает количество байт, занимаемое тремя секци- ями (.text, .data и .bss) объектного файла обычного формата при загрузке его в память для выполнения. Применяя эту команду к нашему объектному файлу, получаем: 3876 + 1228 + 2240 = 7344 Не следует путать это число с количеством символов в объектном файле, полученное с помощью команды ls -l. В этом случае в ре- зультат будут входить также таблица имен и некоторая другая ин- формация, не используемая при выполнении файла. 5.8. strip(1) Команда strip(1) удаляет из объектного файла обычного формата таблицу имен и информацию о номерах строк. После применения ко- манды strip количество символов в файле, выдаваемое командой ls -l, приближается к значению, получаемому по команде size. Но файл все еще содержит некоторые заголовки, которые не входят в секции .text, .data или .bss. После выполнения команды strip данный файл уже не допускает символьной отладки. 5.9. sdb(1) sdb(1) - это символьный отладчик (Symbolic Debugger). Слово "символьный" означает, что во время отладки можно использовать символические имена из программы. С помощью sdb можно отлажи- вать программы на языках C и Фортран 77 [если использовался компилятор f77(1)]. sdb можно использовать двумя способами: ли- бо для контролируемого выполнения программы, либо для анализа образа памяти, оставшегося после аварийного завершения програм- мы. В первом случае можно проследить, что происходит при выпол- нении программы до того места, где она аварийно завершается (или обойти это место, с тем чтобы продолжить выполнение). Во втором случае можно анализировать состояние на момент аварийно- го завершения программы, что, возможно, позволит выяснить при- чину неправильного функционирования. Отладчик sdb(1) подробно описан в Справочнике пользователя. Еще раз подчеркнем, что использование отладчика КРОТ предпочтитель- нее. 6. СРЕДСТВА ОРГАНИЗАЦИИ РАЗРАБОТКИ ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ Следующие три утилиты предназначены для организации процесса разработки программного обеспечения. 6.1. Утилита make(1) Если исходный текст программы размещается в нескольких файлах, при изменении одного из них необходимо выяснять, какие еще мо- дули необходимо перекомпилировать. Используя утилиту make(1), можно записать все зависимости между файлами, гарантировав та- ким образом автоматическую перекомпиляцию всех модулей, зависи- мых от измененного. Даже управление такой простой программой, как рассматривавшаяся выше при обсуждении средств отладки, об- легчается, если пользоваться утилитой make. Для использования утилиты make необходимо с помощью редактора сформировать файл с описаниями. Этот файл (по умолчанию его имя суть makefile) содержит информацию, используемую командой make для построения целевого файла. Целевым файлом обычно является выполняемая программа. Файл описаний обычно содержит информацию трех типов: Информацию о зависимостях между модулями, из которых составляется целевая программа. Выполняемые команды, необходимые для формирования целе- вой программы. Используя информацию о зависимостях, ma- ke определяет, какие команды следует передать shell'у для выполнения. Макроопределения, позволяющие использовать сокращения. Макроопределения, заданные внутри файла описаний, могут быть заменены макроопределениями, задаваемыми в команд- ной строке при вызове утилиты make. Утилита make работает, проверяя время последнего изменения мо- дулей, указанных в файле описаний. Если при этом оказывается, что у некоторого модуля время последнего изменения меньше, чем у модуля, от которого он зависит, то указанная команда (обычно компиляция) направляется shell'у для выполнения. Команда make допускает три типа аргументов: опции, макроопреде- ления и имена целевых файлов. Если в опции командной строки не задано имя файла с описаниями, то make отыскивает в текущем ка- талоге файлы с именами makefile или Makefile. Ниже приводится пример файла описаний. OBJECTS = restate.o oppty.o pft.o rfe.o all: restate restate: $(OBJECTS) $(CC) $(CFLAGS) $(LDFLAGS) $(OBJECTS) -o restate $(OBJECTS): ./recdef.h clean: rm -f $(OBJECTS) clobber: clean rm -f restate В данном примере Целевой файл restate определяется как зависимый от че- тырех объектных файлов, каждый из которых определяется как зависимый от включаемого файла "recdef.h", и, по умолчанию, от соответствующих файлов с исходным текс- том. Макроопределение OBJECTS введено как удобное сокращение для ссылки на все составляющие модули. Если Вы хотите тестировать или отлаживать результаты изменения одного из компонентов программы restate, для изменения опций команды cc можно, например, воспользоваться командой make CFLAGS=-g restate Мы провели очень короткий обзор утилиты make; этой утилите пос- вящена отдельная глава Руководства. 6.2. Работа с архивами Чаще всего архивные файлы используются для хранения объектных файлов, составляющих библиотеку. Имя библиотеки может указы- ваться в командной строке при вызове редактора связей (или в опции редактора связей команды cc). В результате при разрешении ссылок редактор связей просматривает таблицу имен архивного файла. Команда ar(1) служит для создания архивных файлов, различных действий над их содержимым, а также для операций над их табли- цами имен. Синтаксис команды ar несколько отличается от приня- того в ОС UNIX. В командной строке необходимо задать ключ - один из символов из набора drqtpmx. Значение ключа определяет действия команды ar. Для изменения способа выполнения требуемой операции ключ может быть скомбинирован с одним или несколькими символами из набора vuaibcls. Общий вид команды ar: ar -ключ [позиционирующее_имя] а_файл [имя]... где позиционирующее_имя - это имя элемента архива, которое ис- пользуется с некоторыми ключевыми символами для управления раз- мещением файлов в архиве; а_файл - это имя архивного файла. По соглашению, имена архивных файлов имеют расширение .a (напри- мер, libc.a - архивный файл, содержащий объектные файлы стан- дартных функций языка C). Наконец, может быть указано одно или несколько имен, задающих файлы, над которыми будет выполнена операция, определенная значением ключа. С помощью следующей команды можно создать архивный файл, содер- жащий объектные модули, используемые в программе restate: ar -rv rste.a restate.o oppty.o pft.o rfe.o Если в текущем каталоге нет других объектных файлов, то же мож- но проделать с помощью команды ar -rv rste.a *.o В результате выполнения последней команды на экран будет выве- дено следующее: ar: creating rste.a a - oppty.o a - pft.o a - restate.o a - rfe.o Команда nm(1) используется для получения различной информации из таблицы имен объектных файлов обычного формата. Объектные файлы могут, но не обязаны, храниться в архиве. Ниже приводится результат выполнения команды nm -f rste.a Опция -f предписывает выдачу полной информации. Предполагается, что при компиляции использовалась опция -g. Symbols from rste.a[oppty.o]: Name Value Class Type Size Line Section oppty.c | | file | | | | rec | |strtag| struct| 50| | pname | 0|strmem| char[25]| 25| |(ABS) ppx | 26|strmem| float| | |(ABS) dp | 30|strmem| float| | |(ABS) i | 34|strmem| float| | |(ABS) c | 38|strmem| float| | |(ABS) t | 42|strmem| float| | |(ABS) spx | 46|strmem| float| | |(ABS) .eos | |endstr| | 50| |(ABS) oppty | 0|extern| float( )| 100| |.text .bf | 0|fcn | | | 7|.text ps | 8|argm't| *struct-rec| 50| |(ABS) .ef | 80|fcn | | | 3|.text .text | 0|static| | 4| |.text .data | 100|static| | | |.data .bss | 100|static| | | |.bss Symbols from rste.a[pft.o]: Name Value Class Type Size Line Section pft.c | | file | | | | rec | |strtag| struct| 50| | pname | 0|strmem| char[25]| 25| |(ABS) ppx | 26|strmem| float| | |(ABS) dp | 30|strmem| float| | |(ABS) i | 34|strmem| float| | |(ABS) c | 38|strmem| float| | |(ABS) t | 42|strmem| float| | |(ABS) spx | 46|strmem| float| | |(ABS) .eos | |endstr| | 50| |(ABS) pft | 0|extern| float( )| 88| |.text .bf | 0|fcn | | | 7|.text ps | 8|argm't| *struct-rec| 50| |(ABS) .ef | 68|fcn | | | 3|.text .text | 0|static| | 4| |.text .data | 88|static| | | |.data .bss | 88|static| | | |.bss Symbols from rste.a[restate.o]: Name Value Class Type Size Line Section restate.c | | file | | | | .0fake | |strtag| struct| 14 | | _cnt | 0|strmem| int| | |(ABS) _ptr | 4|strmem| *Uchar| | |(ABS) _base | 8|strmem| *Uchar| | |(ABS) _flag | 12|strmem| char| | |(ABS) _file | 13|strmem| char| | |(ABS) .eos | |endstr| | 14| |(ABS) rec | |strtag| struct| 50| | pname | 0|strmem| char[25]| 25| |(ABS) ppx | 26|strmem| float| | |(ABS) dp | 30|strmem| float| | |(ABS) i | 34|strmem| float| | |(ABS) c | 38|strmem| float| | |(ABS) t | 42|strmem| float| | |(ABS) spx | 46|strmem| float| | |(ABS) .eos | |endstr| | 50| |(ABS) main | 0|extern| int( )| 600| |.text .bf | 0|fcn | | | 12|.text argc | 8|argm't| int| | |(ABS) argv | 12|argm't| **char| | |(ABS) fin | -4|auto | *struct-.0fake| 14| |(ABS) oflag | -8|auto | int| | |(ABS) pflag | -12|auto | int| | |(ABS) rflag | -16|auto | int| | |(ABS) ch | -20|auto | int| | |(ABS) first | -70|auto | struct-rec| 50| |(ABS) .ef | 580|fcn | | | 68|.text FILE | |typdef| struct-.0fake| 14 | | .text | 0|static| | 31| 40|.text .data | 600|static| | | |.data .bss | 892|static| | | |.bss _iob | 0|extern| | | | fprintf | 0|extern| | | | exit | 0|extern| | | | opterr | 0|extern| | | | getopt | 0|extern| | | | fopen | 0|extern| | | | fscanf | 0|extern| | | | printf | 0|extern| | | | oppty | 0|extern| | | | pft | 0|extern| | | | rfe | 0|extern| | | | Symbols from rste.a[rfe.o]: Name Value Class Type Size Line Section rfe.c | | | | | | rec | |strtag| struct| 50| | pname | 0|strmem| char[25]| 25| |(ABS) ppx | 26|strmem| float| | |(ABS) dp | 30|strmem| float| | |(ABS) i | 34|strmem| float| | |(ABS) c | 38|strmem| float| | |(ABS) t | 42|strmem| float| | |(ABS) spx | 46|strmem| float| | |(ABS) .eos | |endstr| | 50| |(ABS) rfe | 0|extern| float( )| 104| |.text .bf | 0|fcn | | | 7|.text ps | 8|argm't| *struct-rec| 50| |(ABS) .ef | 84|fcn | | | 3|.text .text | 0|static| | 4| |.text .data | 104|static| | | |.data .bss | 104|static| | | |.bss Для работы команды nm требуется, чтобы все содержимое архива было составлено из объектных модулей. Если поместить в архив что-либо другое, то при выполнении команды nm будет выдано со- общение: nm: rste.a: bad magic 6.3. Использование системы SCCS программистами-одиночками SCCS (Source Code Control System) - это набор утилит, предназ- наченных для управления версиями исходных текстов программ. Ес- ли версии программы отслеживаются с помощью SCCS, то в любой момент времени возможно редактирование только одной версии. Когда измененный текст программы возвращается SCCS, записывают- ся только сделанные изменения. Каждая версия имеет свой с_иден- тификатор. Указывая с_идентификатор, можно выделить из SCCS- файла более раннюю версию текста программы. Если отредактиро- вать и возвратить SCCS для сохранения такую раннюю версию, в дереве версий будет начата новая ветвь. В состав SCCS входят утилиты admin(1), cdc(1), comb(1), delta(1), get(1), prs(1), rmdel(1), sccsdiff(1), val(1), what(1). Все они описаны в Спра- вочнике пользователя. Как правило, считается, что SCCS применяется для управления большими программными проектами. Тем не менее любой пользова- тель вполне может организовать SCCS-систему для личных нужд.